diff --git a/.github/ci-duration-pr.md b/.github/ci-duration-pr.md new file mode 100644 index 00000000000..92ca142ac68 --- /dev/null +++ b/.github/ci-duration-pr.md @@ -0,0 +1,59 @@ +## Thinking Path + +> - Paperclip is the open source app people use to manage AI agents for work. +> - Contributors need predictable CI feedback. +> - Test shards use historical per-file costs to distribute their work. +> - New and changed tests make those costs stale. +> - This automated PR refreshes the costs from successful protected-master runs. +> - The refresh changes placement, while preserving test inventory and runner authorization. + +## Linked Issues or Issue Description + +**What happened?** + +Test costs changed after the last timing refresh. Each manifest records the source runs and measurement coverage. + +**Expected behavior** + +Parallel test jobs should have similar workloads. Timing metadata must never authorize skipping tests. + +**Steps to reproduce** + +Run `node scripts/ci/refresh-test-durations.mjs` with GitHub Actions read access and a full master checkout. + +## What Changed + +- Refresh general-server and serialized timing weights from recent successful master verification runs. + +## Verification + +- The collector validates repository, workflow, branch, event, attempt, successful jobs, commit ancestry and measurement coverage. +- Timing validation and full shard coverage tests pass before this branch is pushed. +- Review the source run links and coverage recorded in both manifests. +- Required PR checks and review must pass before merge. GitHub may require approval to start checks for a PR created with `GITHUB_TOKEN`. + +## Risks + +- Runner load can distort measurements. Medians reduce this noise. Missing measurements retain previous weights. +- The changes affect balance, not which tests execute. There is no automatic merge or AWS authorization change. + +## Model Used + +- No model ran this refresh. The generator was authored with OpenAI Codex (GPT-6); exact deployment ID and context window were not exposed. It uses code execution and repository inspection. + +## Checklist + +- [x] I have included a thinking path that traces from project context to this change +- [x] I have specified the model used (with version and capability details) +- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work (routine CI timing maintenance) +- [x] I have searched GitHub for duplicate or related PRs (the workflow pauses while a refresh PR is open) +- [x] I have described the issue in-PR following the bug issue template +- [x] I have not referenced internal/instance-local Paperclip issues or links +- [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details +- [x] I have run the relevant timing and shard tests and they pass +- [x] I have added or updated tests where applicable (existing timing and shard tests cover these generated values) +- [x] I have updated relevant documentation (the manifests record source runs and coverage) +- [x] I have considered and documented any risks above +- [ ] All Paperclip CI gates are green +- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups +- [x] I will address all Greptile and reviewer comments before requesting merge diff --git a/.github/scripts/tests/ci-plan.test.mjs b/.github/scripts/tests/ci-plan.test.mjs index 81fcc16475a..ab4f04c2994 100644 --- a/.github/scripts/tests/ci-plan.test.mjs +++ b/.github/scripts/tests/ci-plan.test.mjs @@ -294,6 +294,10 @@ test("complete merge trees handle more than 300 files, base advancement, renames assert.ok(result.changed.includes("ui/src/deleted.ts")); assert.ok(result.changed.includes("ui/src/renamed.ts")); assert.ok(!result.changed.includes(".github/workflows/unrelated.yml")); assert.equal(plan(result.changed).forceReason, null); + const shallow = path.join(directory, "shallow"); + git("clone", "--quiet", "--depth", "2", `file://${directory}`, shallow); + assert.equal(execFileSync("git", ["rev-parse", "--is-shallow-repository"], { cwd: shallow, encoding: "utf8" }).trim(), "true"); + assert.deepEqual(mergeDiff(head, prHead, shallow), result); assert.throws(() => mergeDiff(head, base, directory), /expected PR head/); assert.throws(() => mergeDiff(prHead, prHead, directory), /merge commit/); assert.throws(() => mergeDiff("f".repeat(40), prHead, directory)); diff --git a/.github/workflows/ci-test-durations.yml b/.github/workflows/ci-test-durations.yml new file mode 100644 index 00000000000..a17f6f45e1f --- /dev/null +++ b/.github/workflows/ci-test-durations.yml @@ -0,0 +1,69 @@ +name: Refresh CI test durations + +on: + schedule: + - cron: '41 7 * * 1' + workflow_dispatch: + +permissions: + actions: read + contents: write + pull-requests: write + +concurrency: + group: refresh-ci-test-durations + cancel-in-progress: false + +jobs: + refresh: + if: github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + persist-credentials: false + fetch-depth: 0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 24 + package-manager-cache: false + - name: Check for an outstanding refresh + id: pending + env: + GH_TOKEN: ${{ github.token }} + run: | + count="$(gh pr list --repo paperclipai/paperclip --state open --search 'in:title "refresh measured test durations"' --limit 100 --json headRefName --jq '[.[] | select(.headRefName | startswith("codex/refresh-ci-durations-"))] | length')" + echo "count=$count" >> "$GITHUB_OUTPUT" + - name: Refresh from successful protected-master runs + if: steps.pending.outputs.count == '0' + env: + GH_TOKEN: ${{ github.token }} + run: node scripts/ci/refresh-test-durations.mjs + - name: Verify timing data and complete shard coverage + if: steps.pending.outputs.count == '0' + run: node --test scripts/ci/test-selection-timings.test.mjs scripts/__tests__/run-vitest-stable-shard.test.mjs + - name: Open a timing-only pull request + if: steps.pending.outputs.count == '0' + env: + GH_TOKEN: ${{ github.token }} + run: | + if git diff --quiet; then exit 0; fi + # The collector must not cause unrelated code to enter the bot PR. + git diff --name-only -z | node --input-type=module -e ' + import {readFileSync} from "node:fs"; + import assert from "node:assert/strict"; + const allowed = new Set(["scripts/general-server-shard-durations.json", "scripts/serialized-shard-durations.json"]); + for (const file of readFileSync(0,"utf8").split("\0").filter(Boolean)) assert.ok(allowed.has(file), file); + ' + branch="codex/refresh-ci-durations-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + git switch -c "$branch" + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add scripts/general-server-shard-durations.json scripts/serialized-shard-durations.json + git commit -m 'chore(ci): refresh measured test durations' -m 'Co-Authored-By: Paperclip ' + gh auth setup-git + git push origin "$branch" + gh pr create --repo paperclipai/paperclip --base master --head "$branch" \ + --title 'chore(ci): refresh measured test durations' --body-file .github/ci-duration-pr.md diff --git a/.github/workflows/pr-trusted.yml b/.github/workflows/pr-trusted.yml index dae203d6216..09e6144e041 100644 --- a/.github/workflows/pr-trusted.yml +++ b/.github/workflows/pr-trusted.yml @@ -44,7 +44,8 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false - fetch-depth: 0 + # The merge commit and its two parent trees are the complete diff. + fetch-depth: 2 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24 diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index e8c98c6f6e5..0f28897914f 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -274,6 +274,30 @@ ID saved with the plan, even when GitHub increments the run attempt. Turbo hashe `PAPERCLIP_CI_SELECTION_HASH`, so a passing subset cannot satisfy a full run or a different selection. +Graph discovery runs at most two isolated Node processes concurrently. Each +project retains its own Vite configuration, aliases and module graph. Any worker +error or timeout rejects the whole refinement and retains the original task plan. +The selector checkout needs only the merge commit and its two parents, not the +repository's complete history. + +Server test costs are recorded in CI with tests, hooks, collection, setup and +environment preparation included. The weekly **Refresh CI test durations** +workflow reads two successful protected-master Cloud readiness runs and opens a +timing-only PR. It never executes code from logs, changes test selection rules, +or grants AWS access. GitHub may require a maintainer to approve the bot PR's +workflow runs; the refresh never merges itself. An outstanding refresh PR pauses +new refreshes. Both timing manifests include source runs and measurement coverage. +The refresh requires at least 90% coverage of each current lane and uses median +measurements; missing files retain their old weights. Historical logs can bootstrap +test/hook costs until structured loading/setup measurements are available. + +To reproduce a refresh from a full checkout with GitHub Actions read access: + +```sh +node scripts/ci/refresh-test-durations.mjs [master-run-id ...] +node --test scripts/ci/test-selection-timing*.test.mjs +``` + Set the repository variable `CI_VITEST_SELECTION=off` to disable file selection while keeping the existing task-level optimization. Empty or `enabled` enables it; other values disable it. `full-ci`, profiling, and task-level `full`/`shadow` diff --git a/scripts/ci/refresh-test-durations.mjs b/scripts/ci/refresh-test-durations.mjs new file mode 100644 index 00000000000..8e6753f1542 --- /dev/null +++ b/scripts/ci/refresh-test-durations.mjs @@ -0,0 +1,143 @@ +import assert from "node:assert/strict"; +import { execFile, execFileSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify, stripVTControlCharacters } from "node:util"; +import { timingPrefix } from "./test-timing-reporter.mjs"; + +const repository = "paperclipai/paperclip"; +const api = `/repos/${repository}`; +const execute = promisify(execFile); +const github = async (endpoint, json = true) => { + const { stdout } = await execute("gh", ["api", endpoint], { + encoding: "utf8", maxBuffer: json ? 4 * 1024 * 1024 : 128 * 1024 * 1024, timeout: 120_000, + }); + return json ? JSON.parse(stdout) : stdout; +}; + +export function validateRun(run) { + assert.equal(run.repository?.full_name, repository); + assert.equal(run.repository?.id, 1170821064); + assert.equal(run.head_repository?.id, 1170821064); + assert.equal(run.head_branch, "master"); + assert.equal(run.event, "push"); + assert.equal(run.path, ".github/workflows/cloud-readiness.yml"); + assert.equal(run.status, "completed"); + assert.equal(run.conclusion, "success"); + assert.match(run.head_sha, /^[a-f0-9]{40}$/); + assert.ok(Number.isSafeInteger(run.id) && run.id > 0); + assert.ok(Number.isSafeInteger(run.run_attempt) && run.run_attempt > 0); +} + +export function parseTimings(log, inventory) { + const fallback = new Map(), measured = new Map(); + for (const raw of log.split("\n")) { + const line = stripVTControlCharacters(raw); + const offset = line.indexOf(timingPrefix); + if (offset >= 0) { + const record = JSON.parse(line.slice(offset + timingPrefix.length)); + assert.equal(record.version, 1); + assert.equal(typeof record.file, "string"); + assert.match(record.file, /^server\/(src|scripts)\/.+\.test\.[cm]?[jt]s$/); + assert.ok(!record.file.split("/").some(part => part === ".." || part === "." || part === "")); + assert.ok(!record.file.includes("\\")); + assert.ok(Number.isFinite(record.durationMs) && record.durationMs >= 0 && record.durationMs <= 3_600_000); + // A trusted source run can predate a deletion or a lane reassignment. + if (!inventory.has(record.file)) continue; + measured.set(record.file, Math.max(measured.get(record.file) ?? 0, record.durationMs)); + continue; + } + // Bootstrap from pre-reporter runs. These values omit import/setup cost; + // structured measurements take precedence as soon as master emits them. + const match = line.match(/@paperclipai\/server\s+([^\s]+\.test\.[cm]?[jt]s)\s+\(\d+ tests?[^)]*\)\s+(\d+)ms\s*$/); + if (!match) continue; + const file = `server/${match[1]}`, ms = Number(match[2]); + if (inventory.has(file) && ms <= 3_600_000) fallback.set(file, Math.max(fallback.get(file) ?? 0, ms)); + } + return new Map([...fallback].map(([file, ms]) => [file, { ms, complete: false }]) + .concat([...measured].map(([file, ms]) => [file, { ms, complete: true }]))); +} + +export function updatedDurations(previous, inventory, samples, sources) { + const measured = [...inventory].filter(file => samples.some(sample => sample.has(file))); + assert.ok(measured.length >= inventory.size * 0.9, `Incomplete timing coverage: ${measured.length}/${inventory.size}`); + const durations = {}; + let complete = 0; + for (const file of [...inventory].sort()) { + const values = samples.flatMap(sample => sample.has(file) ? [sample.get(file)] : []); + const full = values.filter(value => value.complete); + if (full.length) complete++; + const ms = (full.length ? full : values).map(value => value.ms).sort((a, b) => a - b); + if (!ms.length) { + if (Number.isFinite(previous.durations[file]) && previous.durations[file] >= 0) durations[file] = previous.durations[file]; + continue; + } + const middle = Math.floor(ms.length / 2); + // A small floor prevents clock resolution from giving a suite zero cost. + durations[file] = Math.max(100, Math.ceil(ms.length % 2 ? ms[middle] : (ms[middle - 1] + ms[middle]) / 2)); + } + return { + $comment: "Generated by node scripts/ci/refresh-test-durations.mjs from successful protected-master Cloud readiness runs. Median per-file cost includes tests, hooks, collection, setup and environment preparation when the timing reporter is present; older logs bootstrap test/hook durations only. Missing files retain their prior weight or the partitioner's median fallback. Timing changes only partition work; they never authorize skipping a test.", + unit: "ms", sources, coverage: { measured: measured.length, withSetup: complete, inventory: inventory.size }, durations, + }; +} + +export async function refreshDurations(runIds = []) { + const inventory = JSON.parse(execFileSync(process.execPath, ["scripts/run-vitest-stable.mjs", "--mode", "general", + "--group", "general-server", "--shard-index", "0", "--shard-count", "1", "--dry-run"], { + encoding: "utf8", env: { ...process.env, GITHUB_WORKFLOW: "PR", PAPERCLIP_CI_TEST_SELECTION: "" }, + })); + const lanes = { + general: { files: new Set(inventory.selectedGeneralServerSuites), path: "scripts/general-server-shard-durations.json", samples: [] }, + serialized: { files: new Set(inventory.selectedSerializedSuites), path: "scripts/serialized-shard-durations.json", samples: [] }, + }; + if (!runIds.length) { + const listed = await github(`${api}/actions/workflows/cloud-readiness.yml/runs?branch=master&event=push&status=success&per_page=3`); + runIds = listed.workflow_runs.map(run => run.id).slice(0, 2); + } + assert.ok(runIds.length > 0 && runIds.length <= 3, "Expected one to three successful master runs"); + const sources = []; + for (const id of runIds) { + assert.match(String(id), /^\d+$/); + const run = await github(`${api}/actions/runs/${id}`); + validateRun(run); + execFileSync("git", ["merge-base", "--is-ancestor", run.head_sha, "HEAD"]); + const jobs = []; + for (let page = 1; ; page++) { + const result = await github(`${api}/actions/runs/${id}/attempts/${run.run_attempt}/jobs?per_page=100&page=${page}`); + jobs.push(...result.jobs); + if (result.jobs.length < 100) break; + } + const samples = { general: new Map(), serialized: new Map() }; + const selected = jobs.map(job => ({ job, lane: /General tests \(server \(\d+\/\d+\)\)$/.test(job.name) ? "general" + : /Serialized tests \(\d+\/\d+\)$/.test(job.name) ? "serialized" : null })).filter(item => item.lane); + assert.ok(selected.length, "No server timing jobs found"); + // Keep API traffic and log buffers bounded. Logs are parsed data, never code. + for (let start = 0; start < selected.length; start += 3) { + await Promise.all(selected.slice(start, start + 3).map(async ({ job, lane }) => { + assert.equal(job.run_id, run.id); + assert.equal(job.conclusion, "success"); + const parsed = parseTimings(await github(`${api}/actions/jobs/${job.id}/logs`, false), lanes[lane].files); + for (const [file, value] of parsed) { + const previous = samples[lane].get(file); + if (!previous || (value.complete && !previous.complete) + || (value.complete === previous.complete && value.ms > previous.ms)) samples[lane].set(file, value); + } + })); + } + for (const lane of Object.keys(lanes)) lanes[lane].samples.push(samples[lane]); + sources.push({ run: run.id, attempt: run.run_attempt, sha: run.head_sha, url: run.html_url }); + } + // Validate both lanes before replacing either checked-in manifest. + const updates = Object.values(lanes).map(lane => [lane.path, + updatedDurations(JSON.parse(readFileSync(lane.path, "utf8")), lane.files, lane.samples, sources)]); + for (const [file, data] of updates) { + writeFileSync(file, JSON.stringify(data, null, 2) + "\n"); + console.log(`${file}: ${data.coverage.measured}/${data.coverage.inventory} measured; ${data.coverage.withSetup} include setup`); + } +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + refreshDurations(process.argv.slice(2)).catch(error => { console.error(error); process.exitCode = 1; }); +} diff --git a/scripts/ci/select-projects.mjs b/scripts/ci/select-projects.mjs new file mode 100644 index 00000000000..a5ef304ecf0 --- /dev/null +++ b/scripts/ci/select-projects.mjs @@ -0,0 +1,60 @@ +import { fork } from "node:child_process"; +import { availableParallelism } from "node:os"; +import { fileURLToPath } from "node:url"; + +// Each Vite context stays in its own process: aliases, plugins, globals and +// heaps cannot leak between projects. Two contexts bound memory on hosted CI. +export async function selectProjects({ projects, changed, root = process.cwd(), concurrency = Math.min(2, availableParallelism()) }) { + if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 2) throw new Error("Expected one or two graph workers"); + const results = new Array(projects.length), children = new Set(); + let next = 0, failure; + const work = async () => { + while (!failure && next < projects.length) { + const index = next++; + try { + results[index] = await new Promise((resolve, reject) => { + const env = { ...process.env }; + delete env.NODE_TEST_CONTEXT; + const child = fork(fileURLToPath(import.meta.url), [], { + cwd: root, env, execArgv: ["--max-old-space-size=2048"], stdio: ["ignore", "inherit", "inherit", "ipc"], + }); + children.add(child); + let result; + const timer = setTimeout(() => child.kill("SIGKILL"), 180_000); + child.on("message", message => { result = message; }); + child.once("error", error => { + clearTimeout(timer); + children.delete(child); + child.kill("SIGKILL"); + reject(error); + }); + child.once("exit", (code, signal) => { + clearTimeout(timer); + children.delete(child); + if (code === 0 && result?.graph?.project === projects[index]) resolve(result.graph); + else reject(new Error(result?.error || `Graph worker ${projects[index]} exited ${signal || code}`)); + }); + child.send({ root, project: projects[index], changed }); + }); + } catch (error) { + failure ??= error; + for (const child of children) child.kill("SIGKILL"); + } + } + }; + await Promise.all(Array.from({ length: Math.min(concurrency, projects.length) }, work)); + if (failure) throw failure; + return results; +} + +if (process.send && process.argv[1] === fileURLToPath(import.meta.url)) { + process.once("message", async input => { + try { + const { selectProjectTests } = await import("./vitest-selection-graph.mjs"); + const graph = await selectProjectTests(input); + process.send({ graph }, () => process.exit(0)); + } catch (error) { + process.send({ error: error.message }, () => process.exit(1)); + } + }); +} diff --git a/scripts/ci/select-tests.mjs b/scripts/ci/select-tests.mjs index 5c49976f7c1..16b85191615 100644 --- a/scripts/ci/select-tests.mjs +++ b/scripts/ci/select-tests.mjs @@ -6,6 +6,7 @@ import assert from "node:assert/strict"; import { refineTestPlan, selectionDurations } from "./test-selection-plan.mjs"; import { productInputs } from "../../.github/scripts/ci-inputs.mjs"; import { mergeDiff } from "../../.github/scripts/ci-diff.mjs"; +import { selectProjects } from "./select-projects.mjs"; export function selectionEligibility(plan) { if (plan?.version !== 1 || plan.mode !== "enabled" || plan.forceReason) return "Full/shadow/forced task plan"; @@ -43,9 +44,13 @@ export async function createTestSelection(plan, head) { if (plan.jobs[job].run) for (const entry of plan.jobs[job].matrix.include) groups.add(entry.group ?? "serialized"); } groups.delete("general-chat"); // Keep its line sharding and DB ownership intact. - const owners = {}, projects = new Set(); + const owners = {}, projects = new Set(), inventories = new Map(); + const inventoryFor = group => { + if (!inventories.has(group)) inventories.set(group, runnerInventory(group)); + return inventories.get(group); + }; for (const group of groups) { - const inventory = runnerInventory(group); + const inventory = inventoryFor(group); if (group === "serialized" || group === "general-server-without-chat") { owners[group] = { "@paperclipai/server": group === "serialized" ? inventory.selectedSerializedSuites : inventory.selectedGeneralServerSuites }; @@ -55,12 +60,10 @@ export async function createTestSelection(plan, head) { for (const project of inventory.workspaceProjects) projects.add(project); } } - const { selectProjectTests } = await import("./vitest-selection-graph.mjs"); - const graphs = []; const changed = productInputs(plan.changed); - for (const project of projects) { - const graph = await selectProjectTests({ project, changed }); - graphs.push(graph); + const graphs = await selectProjects({ projects: [...projects], changed }); + for (const graph of graphs) { + const { project } = graph; console.log(`${project}: ${graph.selected.length}/${graph.inventory.length} files, ${graph.alwaysRun.length} retained conservatively, ${graph.seconds.toFixed(1)}s`); for (const owner of Object.values(owners)) if (owner[project] === null) owner[project] = graph.inventory; } @@ -68,8 +71,8 @@ export async function createTestSelection(plan, head) { for (const file of changed) assert.ok(covered.has(file), `Change outside the resolved test graphs: ${file}`); const server = graphs.find((graph) => graph.project === "@paperclipai/server"); if (server) { - const general = runnerInventory("general-server-without-chat"); - const native = runnerInventory("general-server-native-runner"); + const general = inventoryFor("general-server-without-chat"); + const native = inventoryFor("general-server-native-runner"); assert.deepEqual([...general.selectedGeneralServerSuites, ...general.selectedSerializedSuites, ...native.selectedGeneralServerSuites, "server/src/__tests__/chat-channels.integration.test.ts"].sort(), server.inventory, "Server inventory must be completely covered by existing lane ownership"); diff --git a/scripts/ci/test-selection-projects.integration.test.mjs b/scripts/ci/test-selection-projects.integration.test.mjs new file mode 100644 index 00000000000..89d657437bc --- /dev/null +++ b/scripts/ci/test-selection-projects.integration.test.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { selectProjects } from "./select-projects.mjs"; + +test("parallel project graphs preserve isolation, ordering and conservative selection", async t => { + const root = realpathSync(mkdtempSync(path.join(tmpdir(), "parallel-graphs-"))); + t.after(() => rmSync(root, { recursive: true, force: true })); + symlinkSync(path.resolve("node_modules"), path.join(root, "node_modules"), "dir"); + const write = (file, source) => writeFileSync(path.join(root, file), source); + write(".gitignore", "node_modules/\n"); + write("vitest.config.mjs", 'export default {test:{projects:["one", "two"]}}'); + for (const name of ["one", "two"]) { + mkdirSync(path.join(root, name)); + write(`${name}/vitest.config.mjs`, `export default {resolve:{alias:{"@leaf":${JSON.stringify(path.join(root, name, "leaf.ts"))}}},test:{name:${JSON.stringify(name)},include:["*.test.ts"]}}`); + write(`${name}/leaf.ts`, "export const value = 1;"); + write(`${name}/leaf.test.ts`, 'import {value} from "@leaf"; throw new Error("must not execute tests " + value);'); + write(`${name}/opaque.test.ts`, 'import {readFileSync} from "node:fs"; readFileSync(process.env.INPUT);'); + } + execFileSync("git", ["init", "-q"], { cwd: root }); + execFileSync("git", ["add", "."], { cwd: root }); + const options = { root, projects: ["two", "one"], changed: ["one/leaf.ts"] }; + const serial = await selectProjects({ ...options, concurrency: 1 }); + const parallel = await selectProjects({ ...options, concurrency: 2 }); + const stable = graphs => graphs.map(({ seconds, ...graph }) => graph); + assert.deepEqual(stable(parallel), stable(serial)); + assert.deepEqual(parallel[0].selected, ["two/opaque.test.ts"]); + assert.deepEqual(parallel[1].selected, ["one/leaf.test.ts", "one/opaque.test.ts"]); + await assert.rejects(selectProjects({ ...options, projects: ["missing", "one"] }), /missing/); + await assert.rejects(selectProjects({ ...options, concurrency: 3 }), /one or two/); +}); diff --git a/scripts/ci/test-selection-timing-reporter.integration.test.mjs b/scripts/ci/test-selection-timing-reporter.integration.test.mjs new file mode 100644 index 00000000000..0dd1b4a87d6 --- /dev/null +++ b/scripts/ci/test-selection-timing-reporter.integration.test.mjs @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { parseTimings } from "./refresh-test-durations.mjs"; + +test("the real Vitest reporter includes import/setup cost and preserves failures", t => { + const repo = process.cwd(), root = realpathSync(mkdtempSync(path.join(tmpdir(), "file-cost-"))); + t.after(() => rmSync(root, { recursive: true, force: true })); + mkdirSync(path.join(root, "server/src"), { recursive: true }); + symlinkSync(path.join(repo, "node_modules"), path.join(root, "node_modules"), "dir"); + writeFileSync(path.join(root, "vitest.config.mjs"), `export default {test:{name:"@paperclipai/server",include:["server/src/*.test.mjs"],setupFiles:["./setup.mjs"],reporters:["default",${JSON.stringify(path.join(repo, "scripts/ci/test-timing-reporter.mjs"))}]}}`); + writeFileSync(path.join(root, "setup.mjs"), 'await new Promise(resolve=>setTimeout(resolve,60));'); + writeFileSync(path.join(root, "server/src/probe.test.mjs"), 'import {test} from "vitest"; await new Promise(resolve=>setTimeout(resolve,60)); test("pass",()=>{});'); + const env = { ...process.env }; delete env.NODE_TEST_CONTEXT; + const run = () => spawnSync(process.execPath, [path.join(repo, "node_modules/vitest/vitest.mjs"), "run"], { + cwd: root, env, encoding: "utf8", timeout: 30_000, + }); + const result = run(); + assert.equal(result.status, 0, result.stdout + result.stderr); + const file = "server/src/probe.test.mjs"; + const measurement = parseTimings(result.stdout, new Set([file])).get(file); + assert.equal(measurement.complete, true); + assert.ok(measurement.ms >= 110, JSON.stringify(measurement)); + writeFileSync(path.join(root, "server/src/probe.test.mjs"), 'import {test} from "vitest"; test("fail",()=>{throw new Error("expected failure")});'); + const failed = run(); + assert.equal(failed.status, 1); + assert.equal(parseTimings(failed.stdout, new Set([file])).size, 0); +}); diff --git a/scripts/ci/test-selection-timings.test.mjs b/scripts/ci/test-selection-timings.test.mjs new file mode 100644 index 00000000000..617e6715029 --- /dev/null +++ b/scripts/ci/test-selection-timings.test.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFileSync } from "node:fs"; +import { fileTiming, timingPrefix } from "./test-timing-reporter.mjs"; +import { parseTimings, updatedDurations, validateRun } from "./refresh-test-durations.mjs"; + +const file = "server/src/slow.test.ts", second = "server/src/fast.test.ts"; +test("file costs include loading and setup, and failed files cannot become samples", () => { + const module = { project: { name: "@paperclipai/server" }, moduleId: `${process.cwd()}/${file}`, + state: () => "passed", diagnostic: () => ({ duration: 100, collectDuration: 200, setupDuration: 30, + prepareDuration: 20, environmentSetupDuration: 10 }) }; + assert.deepEqual(fileTiming(module), { version: 1, file, durationMs: 360 }); + assert.equal(fileTiming({ ...module, state: () => "failed" }), null); + assert.equal(fileTiming({ ...module, diagnostic: () => ({ duration: NaN }) }), null); +}); + +test("structured measurements replace bootstrap logs without allowing arbitrary paths", () => { + const inventory = new Set([file]); + const log = `2026-10-31T00:00:00Z ✓ @paperclipai/server src/slow.test.ts (2 tests) 100ms\n`; + assert.deepEqual(parseTimings(log, inventory).get(file), { ms: 100, complete: false }); + const record = { version: 1, file, durationMs: 360 }; + assert.deepEqual(parseTimings(log + timingPrefix + JSON.stringify(record), inventory).get(file), { ms: 360, complete: true }); + assert.equal(parseTimings(timingPrefix + JSON.stringify({ ...record, file: "server/src/deleted.test.ts" }), inventory).size, 0); + for (const invalid of [{ ...record, file: "../../package.json" }, { ...record, durationMs: -1 }, + { ...record, file: "server/src/../outside.test.ts" }, + { ...record, durationMs: "360" }, { ...record, version: 2 }]) { + assert.throws(() => parseTimings(timingPrefix + JSON.stringify(invalid), inventory)); + } +}); + +test("refresh uses medians, removes obsolete paths and refuses partial lane coverage", () => { + const previous = { durations: { [file]: 2, "server/src/removed.test.ts": 42 } }; + const samples = [new Map([[file, { ms: 2500, complete: false }], [second, { ms: 1, complete: false }]]), + new Map([[file, { ms: 3000, complete: true }], [second, { ms: 200, complete: false }]]), + new Map([[file, { ms: 4000, complete: true }]])]; + const result = updatedDurations(previous, new Set([file, second]), samples, []); + assert.deepEqual(result.durations, { [second]: 101, [file]: 3500 }); + assert.throws(() => updatedDurations(previous, new Set([file, second]), [new Map()], []), /Incomplete timing/); +}); + +test("only successful same-repository master push verification can supply weights", () => { + const run = { repository: { id: 1170821064, full_name: "paperclipai/paperclip" }, head_repository: { id: 1170821064 }, + head_branch: "master", event: "push", path: ".github/workflows/cloud-readiness.yml", status: "completed", + conclusion: "success", head_sha: "a".repeat(40), id: 1, run_attempt: 1 }; + validateRun(run); + for (const override of [{ head_repository: { id: 123 } }, { head_branch: "feature" }, { event: "pull_request" }, + { conclusion: "failure" }, { status: "in_progress" }, { path: ".github/workflows/pr.yml" }, { run_attempt: 0 }]) { + assert.throws(() => validateRun({ ...run, ...override })); + } + const workflow = readFileSync(".github/workflows/ci-test-durations.yml", "utf8"); + assert.match(workflow, /runs-on: ubuntu-latest/); + assert.match(workflow, /github.ref == 'refs\/heads\/master'/); + assert.doesNotMatch(workflow, /pull_request_target|runs-on\/fleet|--force/); +}); diff --git a/scripts/ci/test-selection-workflow.test.mjs b/scripts/ci/test-selection-workflow.test.mjs index f0ee415a55a..3e76205685a 100644 --- a/scripts/ci/test-selection-workflow.test.mjs +++ b/scripts/ci/test-selection-workflow.test.mjs @@ -61,6 +61,7 @@ test("a published selection controls the matrix and the required aggregate", () test("publication follows successful upload and manifests are downloaded without ignoring failures", () => { const selection = block("test_selection"); + assert.match(selection, /fetch-depth: 2/); assert.ok(selection.indexOf("Upload validated selection") < selection.indexOf("Publish the executable plan")); assert.match(selection, /timeout-minutes: 5/); assert.match(selection, /vars.CI_VITEST_SELECTION == '' \|\| vars.CI_VITEST_SELECTION == 'enabled'/); diff --git a/scripts/ci/test-timing-reporter.mjs b/scripts/ci/test-timing-reporter.mjs new file mode 100644 index 00000000000..cbc46660e2b --- /dev/null +++ b/scripts/ci/test-timing-reporter.mjs @@ -0,0 +1,23 @@ +import path from "node:path"; + +// One bounded metadata record per server file. No test names, output, or secrets. +// The refresh job reads these as data from successful protected-master runs. +export const timingPrefix = "PAPERCLIP_TEST_TIMING "; + +export function fileTiming(module, root = process.cwd()) { + if (module.project.name !== "@paperclipai/server" || module.state() !== "passed") return null; + const file = path.relative(root, module.moduleId).split(path.sep).join("/"); + if (!/^server\/(?:src|scripts)\/.+\.test\.[cm]?[jt]s$/.test(file)) return null; + const timing = module.diagnostic(); + const phases = [timing.duration, timing.collectDuration, timing.setupDuration, + timing.prepareDuration, timing.environmentSetupDuration]; + if (phases.some(value => !Number.isFinite(value) || value < 0)) return null; + return { version: 1, file, durationMs: Math.ceil(phases.reduce((sum, value) => sum + value, 0)) }; +} + +export default class TestTimingReporter { + onTestModuleEnd(module) { + const record = fileTiming(module); + if (record) console.log(timingPrefix + JSON.stringify(record)); + } +} diff --git a/scripts/general-server-shard-durations.json b/scripts/general-server-shard-durations.json index e29facede31..119c7bd96b2 100644 --- a/scripts/general-server-shard-durations.json +++ b/scripts/general-server-shard-durations.json @@ -1,879 +1,1319 @@ { - "$comment": "Per-suite wall-clock durations (ms) for the general-server vitest lane, used by scripts/general-server-shard.mjs to balance suites across the PR shard matrix. Averaged from two real PR runs of .github/workflows/pr.yml (actions runs 38076844891 and 38076972444, 2026-10-10, on the runs-on paperclip-public-pr-x64 fleet) by diffing consecutive per-suite completion timestamps in the 'Run grouped general test suites' logs across every shard — that captures each suite's true serial cost (import, setup, and run) under maxWorkers=1. The first suite of each shard is measured from Vitest's RUN banner, so per-shard startup lands on a measured suite instead of vanishing. The chat and native-runner suites run in their own PR lanes, so their entries carry over from the 2026-09-15 measurement. Suites missing here get the median weight, so the manifest only needs occasional refreshes; regenerate the same way when the lane's shard steps drift apart by more than the heaviest suite.", + "$comment": "Generated by node scripts/ci/refresh-test-durations.mjs from successful protected-master Cloud readiness runs. Median per-file cost includes tests, hooks, collection, setup and environment preparation when the timing reporter is present; older logs bootstrap test/hook durations only. Missing files retain their prior weight or the partitioner's median fallback. Timing changes only partition work; they never authorize skipping a test.", "unit": "ms", + "sources": [ + { + "run": 41202111422, + "attempt": 1, + "sha": "c2971badde04b602fa9fe698015e1f3653b39e6b", + "url": "https://github.com/paperclipai/paperclip/actions/runs/41202111422" + }, + { + "run": 41201965500, + "attempt": 1, + "sha": "d5766e40748ac5881c4b9879fad1d5a61d9b493d", + "url": "https://github.com/paperclipai/paperclip/actions/runs/41201965500" + } + ], + "coverage": { + "measured": 1285, + "withSetup": 0, + "inventory": 1309 + }, "durations": { - "server/scripts/verify-runner-vendor-dependencies.test.mjs": 186, - "server/src/__tests__/access-service.test.ts": 8447, - "server/src/__tests__/access-validators.test.ts": 744, - "server/src/__tests__/activity-credential-access.test.ts": 7944, - "server/src/__tests__/activity-log-responsible-user.test.ts": 8073, - "server/src/__tests__/activity-service.test.ts": 10246, - "server/src/__tests__/adapter-cost-failure-paths.test.ts": 5615, - "server/src/__tests__/adapter-models.test.ts": 1434, - "server/src/__tests__/adapter-registry.test.ts": 1448, - "server/src/__tests__/adapter-session-codecs.test.ts": 858, - "server/src/__tests__/agent-auth-jwt.test.ts": 737, - "server/src/__tests__/agent-auth-middleware.test.ts": 1290, - "server/src/__tests__/agent-avatars.test.ts": 8287, - "server/src/__tests__/agent-commentary-daytona-smoke.test.ts": 1254, - "server/src/__tests__/agent-commentary-helper.test.ts": 10976, - "server/src/__tests__/agent-commentary-skills.test.ts": 930, - "server/src/__tests__/agent-commentary.integration.test.ts": 15224, - "server/src/__tests__/agent-conversations.test.ts": 16683, - "server/src/__tests__/agent-default-configure-grants.test.ts": 9156, - "server/src/__tests__/agent-directory-probe.test.ts": 1493, - "server/src/__tests__/agent-directory-working-copies.test.ts": 45751, - "server/src/__tests__/agent-file-checkpoints.test.ts": 1528, - "server/src/__tests__/agent-harness-verification.test.ts": 2356, - "server/src/__tests__/agent-hire-ai-connections.test.ts": 17137, - "server/src/__tests__/agent-identity-master-key.test.ts": 519, - "server/src/__tests__/agent-identity.test.ts": 21467, - "server/src/__tests__/agent-instruction-git-exclusion.test.ts": 430, - "server/src/__tests__/agent-instruction-revisions.test.ts": 9689, - "server/src/__tests__/agent-instruction-working-copies.test.ts": 11715, - "server/src/__tests__/agent-instructions-service.test.ts": 1367, - "server/src/__tests__/agent-invokability.test.ts": 1116, - "server/src/__tests__/agent-permissions-service.test.ts": 680, - "server/src/__tests__/agent-persona-persistence.test.ts": 8731, - "server/src/__tests__/agent-profile-avatar.test.ts": 10471, - "server/src/__tests__/agent-shortname-collision.test.ts": 2260, - "server/src/__tests__/agent-skill-contract.test.ts": 201, - "server/src/__tests__/agent-task-run-telemetry.test.ts": 8482, - "server/src/__tests__/agentmail-api.test.ts": 1381, - "server/src/__tests__/agents-adapter-config-user-secret.test.ts": 10074, - "server/src/__tests__/agents-claude-oauth-binding.test.ts": 11793, - "server/src/__tests__/agents-paperclip-runner-skills.test.ts": 780, - "server/src/__tests__/agents-pending-approval-config.test.ts": 8779, - "server/src/__tests__/agents-service-clear-error.test.ts": 9643, - "server/src/__tests__/agents-service-secret-bindings.test.ts": 9581, - "server/src/__tests__/aggregator-app-discovery.test.ts": 250, - "server/src/__tests__/aggregator-app-sync.test.ts": 10208, - "server/src/__tests__/ai-auth-failure.test.ts": 777, - "server/src/__tests__/ai-connection-session.test.ts": 2974, - "server/src/__tests__/ai-connections.test.ts": 33549, - "server/src/__tests__/ai-legacy-compatibility.test.ts": 9149, - "server/src/__tests__/ai-provider-routing.test.ts": 749, - "server/src/__tests__/announcement-animation.test.ts": 1315, - "server/src/__tests__/announcement-feed.test.ts": 1395, - "server/src/__tests__/announcement-publisher.test.ts": 1360, - "server/src/__tests__/api-compression.test.ts": 266, - "server/src/__tests__/app-hmr-port.test.ts": 6653, - "server/src/__tests__/app-private-hostname-gate.test.ts": 7728, - "server/src/__tests__/app-vite-dev-routing.test.ts": 5394, - "server/src/__tests__/approvals-service.test.ts": 2191, - "server/src/__tests__/artifact-review-documents-service.test.ts": 8702, - "server/src/__tests__/attachment-types.test.ts": 164, - "server/src/__tests__/attention-service.test.ts": 12212, - "server/src/__tests__/authorization-service.test.ts": 10426, - "server/src/__tests__/aws-secrets-manager-provider.test.ts": 263, - "server/src/__tests__/better-auth-credential-signup.integration.test.ts": 8331, - "server/src/__tests__/better-auth.test.ts": 955, - "server/src/__tests__/board-claim.test.ts": 7534, - "server/src/__tests__/board-mutation-guard.test.ts": 235, - "server/src/__tests__/board-native-attachments.integration.test.ts": 9772, - "server/src/__tests__/body-limits.test.ts": 163, - "server/src/__tests__/browser-use-connection.test.ts": 18179, - "server/src/__tests__/browser-use-viewport.test.ts": 800, - "server/src/__tests__/browser-use-work.test.ts": 591, - "server/src/__tests__/browser-use.test.ts": 746, - "server/src/__tests__/budgets-service.test.ts": 9254, - "server/src/__tests__/build-commit.test.ts": 163, - "server/src/__tests__/build-version.test.ts": 152, - "server/src/__tests__/built-in-agents.test.ts": 25931, - "server/src/__tests__/bundled-plugins.test.ts": 282, - "server/src/__tests__/change-consent-gate.test.ts": 7435, - "server/src/__tests__/chat-action-work.test.ts": 599, + "server/scripts/bundle-runtime.test.mjs": 882, + "server/scripts/prepare-runner-vendor.test.mjs": 555, + "server/scripts/verify-runner-vendor-dependencies.test.mjs": 100, + "server/src/__tests__/access-service.test.ts": 3201, + "server/src/__tests__/access-validators.test.ts": 100, + "server/src/__tests__/activity-credential-access.test.ts": 555, + "server/src/__tests__/activity-log-responsible-user.test.ts": 417, + "server/src/__tests__/activity-service.test.ts": 2370, + "server/src/__tests__/adapter-cost-failure-paths.test.ts": 6615, + "server/src/__tests__/adapter-models.test.ts": 100, + "server/src/__tests__/adapter-registry.test.ts": 100, + "server/src/__tests__/adapter-session-codecs.test.ts": 100, + "server/src/__tests__/agent-auth-identity-transfer.test.ts": 988, + "server/src/__tests__/agent-auth-jwt.test.ts": 100, + "server/src/__tests__/agent-auth-middleware.test.ts": 118, + "server/src/__tests__/agent-avatars.test.ts": 6911, + "server/src/__tests__/agent-chat-messages.test.ts": 9821, + "server/src/__tests__/agent-commentary-daytona-smoke.test.ts": 100, + "server/src/__tests__/agent-commentary-helper.test.ts": 10943, + "server/src/__tests__/agent-commentary-reader.test.ts": 883, + "server/src/__tests__/agent-commentary-skills.test.ts": 100, + "server/src/__tests__/agent-commentary.integration.test.ts": 4054, + "server/src/__tests__/agent-configuration-revision.test.ts": 100, + "server/src/__tests__/agent-conversation-delivery.test.ts": 724, + "server/src/__tests__/agent-conversations.test.ts": 8377, + "server/src/__tests__/agent-default-configure-grants.test.ts": 926, + "server/src/__tests__/agent-directory-probe.test.ts": 1001, + "server/src/__tests__/agent-directory-working-copies.test.ts": 56721, + "server/src/__tests__/agent-file-checkpoints.test.ts": 433, + "server/src/__tests__/agent-harness-verification-preflight.test.ts": 817, + "server/src/__tests__/agent-harness-verification-remote.test.ts": 100, + "server/src/__tests__/agent-harness-verification.test.ts": 100, + "server/src/__tests__/agent-hire-ai-connections.test.ts": 13188, + "server/src/__tests__/agent-identity-master-key.test.ts": 336, + "server/src/__tests__/agent-identity.test.ts": 19008, + "server/src/__tests__/agent-instance-secrets.test.ts": 1423, + "server/src/__tests__/agent-instruction-git-exclusion.test.ts": 323, + "server/src/__tests__/agent-instruction-revisions.test.ts": 2037, + "server/src/__tests__/agent-instruction-working-copies.test.ts": 12982, + "server/src/__tests__/agent-instructions-service.test.ts": 132, + "server/src/__tests__/agent-invokability.test.ts": 100, + "server/src/__tests__/agent-observability-test.test.ts": 327, + "server/src/__tests__/agent-observability.test.ts": 1736, + "server/src/__tests__/agent-permissions-service.test.ts": 100, + "server/src/__tests__/agent-persona-persistence.test.ts": 676, + "server/src/__tests__/agent-profile-avatar.test.ts": 900, + "server/src/__tests__/agent-project-access.test.ts": 1277, + "server/src/__tests__/agent-record-read-projections.test.ts": 1231, + "server/src/__tests__/agent-run-authentication.test.ts": 1047, + "server/src/__tests__/agent-runner-platform.test.ts": 5359, + "server/src/__tests__/agent-runtime-assignments.test.ts": 3254, + "server/src/__tests__/agent-runtime-connector-access.test.ts": 3507, + "server/src/__tests__/agent-runtime-hooks.test.ts": 12111, + "server/src/__tests__/agent-runtime-settings.test.ts": 100, + "server/src/__tests__/agent-runtime-skills.test.ts": 2298, + "server/src/__tests__/agent-runtime-tool-definitions.test.ts": 5356, + "server/src/__tests__/agent-shortname-collision.test.ts": 100, + "server/src/__tests__/agent-skill-contract.test.ts": 100, + "server/src/__tests__/agent-task-run-telemetry.test.ts": 1074, + "server/src/__tests__/agent-timer-deadlines.test.ts": 34751, + "server/src/__tests__/agentmail-api.test.ts": 191, + "server/src/__tests__/agents-adapter-config-user-secret.test.ts": 3035, + "server/src/__tests__/agents-claude-oauth-binding.test.ts": 3673, + "server/src/__tests__/agents-paperclip-runner-skills.test.ts": 100, + "server/src/__tests__/agents-pending-approval-config.test.ts": 1063, + "server/src/__tests__/agents-service-clear-error.test.ts": 888, + "server/src/__tests__/agents-service-secret-bindings.test.ts": 1188, + "server/src/__tests__/aggregator-app-discovery.test.ts": 100, + "server/src/__tests__/aggregator-app-sync.test.ts": 1276, + "server/src/__tests__/ai-auth-failure.test.ts": 100, + "server/src/__tests__/ai-connection-session.test.ts": 100, + "server/src/__tests__/ai-connections.test.ts": 35214, + "server/src/__tests__/ai-legacy-compatibility.test.ts": 1186, + "server/src/__tests__/ai-provider-routing.test.ts": 100, + "server/src/__tests__/announcement-animation.test.ts": 215, + "server/src/__tests__/announcement-feed.test.ts": 181, + "server/src/__tests__/announcement-publisher.test.ts": 126, + "server/src/__tests__/antigravity-ai-connections.integration.test.ts": 3026, + "server/src/__tests__/antigravity-ai-runtime.test.ts": 100, + "server/src/__tests__/antigravity-connection-models.test.ts": 100, + "server/src/__tests__/antigravity-connection-probe.test.ts": 100, + "server/src/__tests__/antigravity-local-ai-login.test.ts": 607, + "server/src/__tests__/antigravity-managed-subscription.test.ts": 154, + "server/src/__tests__/api-compression.test.ts": 100, + "server/src/__tests__/app-hmr-port.test.ts": 100, + "server/src/__tests__/app-private-hostname-gate.test.ts": 100, + "server/src/__tests__/app-vite-dev-routing.test.ts": 100, + "server/src/__tests__/approval-queue-seeding-work.test.ts": 23898, + "server/src/__tests__/approval-retention-work.test.ts": 14735, + "server/src/__tests__/approvals-service.test.ts": 100, + "server/src/__tests__/artifact-review-documents-service.test.ts": 1311, + "server/src/__tests__/attachment-types.test.ts": 100, + "server/src/__tests__/attention-service.test.ts": 72265, + "server/src/__tests__/authorization-privacy-query.test.ts": 2277, + "server/src/__tests__/authorization-run-context.test.ts": 695, + "server/src/__tests__/authorization-service.test.ts": 4417, + "server/src/__tests__/aws-secrets-manager-provider.test.ts": 100, + "server/src/__tests__/better-auth-credential-signup.integration.test.ts": 603, + "server/src/__tests__/better-auth.test.ts": 100, + "server/src/__tests__/board-claim.test.ts": 595, + "server/src/__tests__/board-mutation-guard.test.ts": 100, + "server/src/__tests__/board-native-attachments.integration.test.ts": 1429, + "server/src/__tests__/body-limits.test.ts": 100, + "server/src/__tests__/braintrust-connection.test.ts": 4346, + "server/src/__tests__/browser-use-connection.test.ts": 15921, + "server/src/__tests__/browser-use-viewport.test.ts": 100, + "server/src/__tests__/browser-use-work.test.ts": 100, + "server/src/__tests__/browser-use.test.ts": 100, + "server/src/__tests__/budget-enforcement-scoping.test.ts": 8416, + "server/src/__tests__/budget-enforcement-worker.test.ts": 100, + "server/src/__tests__/budget-live-attempts.test.ts": 5344, + "server/src/__tests__/budgets-history.test.ts": 1093, + "server/src/__tests__/budgets-service.test.ts": 3024, + "server/src/__tests__/build-commit.test.ts": 100, + "server/src/__tests__/build-version.test.ts": 100, + "server/src/__tests__/built-in-agents.test.ts": 31388, + "server/src/__tests__/bundled-plugin-package.test.ts": 100, + "server/src/__tests__/bundled-plugin-registration.test.ts": 3262, + "server/src/__tests__/bundled-plugin-runtime.test.ts": 105, + "server/src/__tests__/bundled-plugins.test.ts": 100, + "server/src/__tests__/case-handoff-legacy.integration.test.ts": 8748, + "server/src/__tests__/case-mutation-fence.integration.test.ts": 2256, + "server/src/__tests__/change-consent-gate.test.ts": 970, + "server/src/__tests__/chat-action-work.test.ts": 100, "server/src/__tests__/chat-channels.integration.test.ts": 428744, - "server/src/__tests__/chat-completion-delivery.test.ts": 13194, - "server/src/__tests__/chat-delivery-work.test.ts": 638, - "server/src/__tests__/chat-interaction-publications.test.ts": 9294, - "server/src/__tests__/chat-project-tools.test.ts": 18910, - "server/src/__tests__/chat-webhook-diagnostics.test.ts": 3738, - "server/src/__tests__/chat-webhook-public-url.test.ts": 1448, - "server/src/__tests__/claude-local-adapter-environment.test.ts": 1405, - "server/src/__tests__/claude-local-adapter.test.ts": 901, - "server/src/__tests__/claude-local-execute.test.ts": 6842, - "server/src/__tests__/claude-local-skill-sync.test.ts": 857, - "server/src/__tests__/cleanup-removal-service.test.ts": 10580, - "server/src/__tests__/cli-invocation-safety.test.ts": 1157, - "server/src/__tests__/cloud-control-task-drain.test.ts": 747, - "server/src/__tests__/cloud-image-bundled-plugins.test.ts": 236, - "server/src/__tests__/cloud-image-sentry.test.ts": 227, - "server/src/__tests__/cloud-instance.test.ts": 165, - "server/src/__tests__/cloud-lifecycle-sync.test.ts": 194, - "server/src/__tests__/cloud-portfolio-error.test.ts": 168, - "server/src/__tests__/cloud-runtime-identity.test.ts": 10931, - "server/src/__tests__/cloud-tenant-company-provisioning.test.ts": 8210, - "server/src/__tests__/cloud-tenant-transient-db-retry.test.ts": 1774, - "server/src/__tests__/cloud-warm-standby.test.ts": 2838, - "server/src/__tests__/codex-auth-reconciliation.test.ts": 1286, - "server/src/__tests__/codex-local-adapter-environment.test.ts": 1111, - "server/src/__tests__/codex-local-adapter.test.ts": 878, - "server/src/__tests__/codex-local-execute.test.ts": 5333, - "server/src/__tests__/codex-local-skill-injection.test.ts": 881, - "server/src/__tests__/codex-local-skill-sync.test.ts": 814, - "server/src/__tests__/companies-service.test.ts": 11742, - "server/src/__tests__/company-artifacts-service.test.ts": 11341, - "server/src/__tests__/company-cloud-floor.test.ts": 1295, - "server/src/__tests__/company-import-cloud-floor.test.ts": 1454, - "server/src/__tests__/company-portability-import-batching.test.ts": 9596, - "server/src/__tests__/company-quota-windows.test.ts": 712, - "server/src/__tests__/company-search-extract-service.test.ts": 8217, - "server/src/__tests__/company-search-service.test.ts": 10949, - "server/src/__tests__/company-skill-import-boundary.test.ts": 9611, - "server/src/__tests__/company-skill-policy-service.test.ts": 8324, - "server/src/__tests__/company-skill-test-runs-service.test.ts": 14357, - "server/src/__tests__/company-skills-catalog-service.test.ts": 13056, - "server/src/__tests__/company-skills-detail.test.ts": 9399, - "server/src/__tests__/company-skills-service.test.ts": 29990, - "server/src/__tests__/company-skills.test.ts": 2402, - "server/src/__tests__/company-transfer-runs.test.ts": 7509, - "server/src/__tests__/composio-app-setup.test.ts": 11457, - "server/src/__tests__/config-file.test.ts": 823, - "server/src/__tests__/confirmation-comment-resolution.test.ts": 11157, - "server/src/__tests__/connection-aggregator-fallback.test.ts": 12200, - "server/src/__tests__/connection-grant-resolution.test.ts": 2283, - "server/src/__tests__/connection-instructions.test.ts": 9542, - "server/src/__tests__/connection-intents-service.test.ts": 11298, - "server/src/__tests__/container-init-reaping.test.ts": 160, - "server/src/__tests__/cross-issue-influence-limit-postgres.test.ts": 8263, - "server/src/__tests__/cross-issue-influence-limit.test.ts": 1202, - "server/src/__tests__/cursor-local-adapter-environment.test.ts": 3187, - "server/src/__tests__/cursor-local-adapter.test.ts": 878, - "server/src/__tests__/cursor-local-execute.test.ts": 4835, - "server/src/__tests__/cursor-local-skill-injection.test.ts": 889, - "server/src/__tests__/cursor-local-skill-sync.test.ts": 810, + "server/src/__tests__/chat-completion-delivery.test.ts": 4762, + "server/src/__tests__/chat-delivery-work.test.ts": 100, + "server/src/__tests__/chat-execution-defaults.test.ts": 100, + "server/src/__tests__/chat-failed-run-retry-boundary.test.ts": 100, + "server/src/__tests__/chat-interaction-publications.test.ts": 1850, + "server/src/__tests__/chat-native-progress-work.test.ts": 6501, + "server/src/__tests__/chat-project-tools.test.ts": 10109, + "server/src/__tests__/chat-receiver-work.test.ts": 17234, + "server/src/__tests__/chat-run-milestone-candidates.test.ts": 530, + "server/src/__tests__/chat-run-milestone-governed-work.test.ts": 4665, + "server/src/__tests__/chat-run-milestone-identity-work.test.ts": 1963, + "server/src/__tests__/chat-run-milestone-recovery-work.test.ts": 1567, + "server/src/__tests__/chat-run-milestone-resource-work.test.ts": 1605, + "server/src/__tests__/chat-run-milestone-work.test.ts": 17670, + "server/src/__tests__/chat-webhook-diagnostics.test.ts": 103, + "server/src/__tests__/chat-webhook-public-url.test.ts": 100, + "server/src/__tests__/claude-local-adapter-environment.test.ts": 500, + "server/src/__tests__/claude-local-adapter.test.ts": 100, + "server/src/__tests__/claude-local-execute.test.ts": 6069, + "server/src/__tests__/claude-local-skill-sync.test.ts": 100, + "server/src/__tests__/cleanup-removal-service.test.ts": 1549, + "server/src/__tests__/cli-invocation-safety.test.ts": 1161, + "server/src/__tests__/cloud-bundled-package-digest.test.ts": 886, + "server/src/__tests__/cloud-configuration-migration-postgres.test.ts": 1228, + "server/src/__tests__/cloud-configuration-readers.test.ts": 100, + "server/src/__tests__/cloud-control-task-drain.test.ts": 100, + "server/src/__tests__/cloud-image-bundled-plugins.test.ts": 100, + "server/src/__tests__/cloud-image-sentry.test.ts": 100, + "server/src/__tests__/cloud-instance.test.ts": 100, + "server/src/__tests__/cloud-lifecycle-sync.test.ts": 100, + "server/src/__tests__/cloud-portfolio-error.test.ts": 100, + "server/src/__tests__/cloud-runtime-config.test.ts": 3714, + "server/src/__tests__/cloud-runtime-identity.test.ts": 625, + "server/src/__tests__/cloud-tenant-company-provisioning.test.ts": 1119, + "server/src/__tests__/cloud-tenant-transient-db-retry.test.ts": 560, + "server/src/__tests__/cloud-warm-standby.test.ts": 152, + "server/src/__tests__/codex-auth-reconciliation.test.ts": 100, + "server/src/__tests__/codex-local-adapter-environment.test.ts": 212, + "server/src/__tests__/codex-local-adapter.test.ts": 100, + "server/src/__tests__/codex-local-execute.test.ts": 4585, + "server/src/__tests__/codex-local-skill-injection.test.ts": 100, + "server/src/__tests__/codex-local-skill-sync.test.ts": 100, + "server/src/__tests__/commentary-permission-migration.test.ts": 484, + "server/src/__tests__/committed-chat-response-authority.test.ts": 100, + "server/src/__tests__/companies-service.test.ts": 3307, + "server/src/__tests__/company-artifacts-service.test.ts": 1920, + "server/src/__tests__/company-cloud-floor.test.ts": 1602, + "server/src/__tests__/company-import-cloud-floor.test.ts": 1462, + "server/src/__tests__/company-portability-import-batching.test.ts": 953, + "server/src/__tests__/company-quota-windows.test.ts": 100, + "server/src/__tests__/company-search-extract-service.test.ts": 849, + "server/src/__tests__/company-search-service.test.ts": 3551, + "server/src/__tests__/company-skill-import-boundary.test.ts": 2686, + "server/src/__tests__/company-skill-policy-service.test.ts": 821, + "server/src/__tests__/company-skill-test-runs-service.test.ts": 12056, + "server/src/__tests__/company-skills-catalog-service.test.ts": 21086, + "server/src/__tests__/company-skills-detail.test.ts": 1635, + "server/src/__tests__/company-skills-empty-runtime.test.ts": 100, + "server/src/__tests__/company-skills-observability.test.ts": 2011, + "server/src/__tests__/company-skills-runtime-concurrency.test.ts": 2600, + "server/src/__tests__/company-skills-runtime-selection.test.ts": 2781, + "server/src/__tests__/company-skills-service.test.ts": 85096, + "server/src/__tests__/company-skills.test.ts": 100, + "server/src/__tests__/company-transfer-runs.test.ts": 666, + "server/src/__tests__/company-work-batch.test.ts": 100, + "server/src/__tests__/composio-app-setup.test.ts": 2057, + "server/src/__tests__/config-file.test.ts": 100, + "server/src/__tests__/confirmation-comment-resolution.test.ts": 2985, + "server/src/__tests__/connection-aggregator-fallback.test.ts": 10947, + "server/src/__tests__/connection-grant-resolution.test.ts": 100, + "server/src/__tests__/connection-install-capabilities.test.ts": 100, + "server/src/__tests__/connection-instructions.test.ts": 1910, + "server/src/__tests__/connection-intents-service.test.ts": 3228, + "server/src/__tests__/connection-management.integration.test.ts": 121805, + "server/src/__tests__/connector-tool-policy.test.ts": 564, + "server/src/__tests__/container-init-reaping.test.ts": 100, + "server/src/__tests__/conversation-settlement-transfer.test.ts": 3461, + "server/src/__tests__/coordinator-handoff.test.ts": 2973, + "server/src/__tests__/copilot-connection-probe.test.ts": 100, + "server/src/__tests__/cost-data-quality.test.ts": 1446, + "server/src/__tests__/cross-issue-influence-limit-postgres.test.ts": 668, + "server/src/__tests__/cross-issue-influence-limit.test.ts": 100, + "server/src/__tests__/cursor-local-adapter-environment.test.ts": 215, + "server/src/__tests__/cursor-local-adapter.test.ts": 100, + "server/src/__tests__/cursor-local-execute.test.ts": 512, + "server/src/__tests__/cursor-local-skill-injection.test.ts": 100, + "server/src/__tests__/cursor-local-skill-sync.test.ts": 100, + "server/src/__tests__/custom-image-cleanup-work.test.ts": 100, "server/src/__tests__/customer-success-cloud.test.ts": 1410, - "server/src/__tests__/customer-success.test.ts": 8115, - "server/src/__tests__/dashboard-disconnect-retry.test.ts": 2267, - "server/src/__tests__/dashboard-service.test.ts": 8100, - "server/src/__tests__/db-errors.test.ts": 158, - "server/src/__tests__/decision-model-provider.test.ts": 842, - "server/src/__tests__/decision-models.test.ts": 12580, - "server/src/__tests__/decision-retention-service.test.ts": 9478, - "server/src/__tests__/decision-training.test.ts": 11230, - "server/src/__tests__/decision-wakeup.test.ts": 159, - "server/src/__tests__/decisions-service.test.ts": 12826, - "server/src/__tests__/delivery-work-coordinator.test.ts": 648, - "server/src/__tests__/dev-runner-native-binary.test.ts": 165, - "server/src/__tests__/dev-runner-options.test.ts": 169, - "server/src/__tests__/dev-runner-output.test.ts": 183, - "server/src/__tests__/dev-runner-paths.test.ts": 164, - "server/src/__tests__/dev-runner-snapshot.test.ts": 173, - "server/src/__tests__/dev-runner-worktree.test.ts": 163, - "server/src/__tests__/dev-server-status.test.ts": 293, - "server/src/__tests__/dev-watch-ignore.test.ts": 165, - "server/src/__tests__/device-login-credential-read.test.ts": 516, - "server/src/__tests__/device-login-reaper.test.ts": 1179, - "server/src/__tests__/device-login-service.test.ts": 8386, - "server/src/__tests__/disposition-repair.test.ts": 2374, - "server/src/__tests__/distribution-plugin-catalog.test.ts": 295, - "server/src/__tests__/docker-build-stamp.test.ts": 176, - "server/src/__tests__/docker-entrypoint.test.ts": 221, - "server/src/__tests__/document-annotations-service.test.ts": 10788, - "server/src/__tests__/documents-service.test.ts": 8463, - "server/src/__tests__/documents.test.ts": 1114, - "server/src/__tests__/dot-invitations.test.ts": 9385, - "server/src/__tests__/dot-oauth-onboarding.test.ts": 10191, - "server/src/__tests__/duplex-observability-recorder.test.ts": 164, - "server/src/__tests__/durable-chat-wakeup.test.ts": 20437, - "server/src/__tests__/durable-directory.test.ts": 1224, - "server/src/__tests__/effective-run-config-fingerprints.test.ts": 584, - "server/src/__tests__/email-address-check.test.ts": 1391, - "server/src/__tests__/email-channels.integration.test.ts": 39659, - "server/src/__tests__/email-idle-work.test.ts": 2288, - "server/src/__tests__/embedded-postgres-supervisor.test.ts": 164, - "server/src/__tests__/env-file-policy.test.ts": 173, - "server/src/__tests__/environment-acquisition-diagnostics.test.ts": 2105, - "server/src/__tests__/environment-capability-contract.test.ts": 1977, - "server/src/__tests__/environment-config.test.ts": 1983, - "server/src/__tests__/environment-custom-image-terminal-ws.test.ts": 2776, - "server/src/__tests__/environment-custom-images-service.test.ts": 13530, - "server/src/__tests__/environment-driver-traits.test.ts": 150, - "server/src/__tests__/environment-execution-target-capabilities.test.ts": 799, - "server/src/__tests__/environment-execution-target-duplex-kill-switch.test.ts": 839, - "server/src/__tests__/environment-execution-target-duplex.test.ts": 1637, - "server/src/__tests__/environment-execution-target.test.ts": 873, - "server/src/__tests__/environment-live-ssh.test.ts": 1679, - "server/src/__tests__/environment-probe.test.ts": 2062, - "server/src/__tests__/environment-run-orchestrator.test.ts": 2169, - "server/src/__tests__/environment-runtime-driver-contract.test.ts": 6427, - "server/src/__tests__/environment-runtime-duplex-bridge-input.test.ts": 5642, - "server/src/__tests__/environment-runtime.test.ts": 26265, - "server/src/__tests__/environment-service.test.ts": 7768, - "server/src/__tests__/environment-sync-out-error.test.ts": 709, - "server/src/__tests__/environment-test-harness.test.ts": 735, - "server/src/__tests__/error-handler.test.ts": 1155, - "server/src/__tests__/execution-lock-orphan-cleanup.test.ts": 9929, - "server/src/__tests__/execution-workspace-policy.test.ts": 766, - "server/src/__tests__/execution-workspace-reopen.test.ts": 10462, - "server/src/__tests__/execution-workspace-runtime-control-conflict.test.ts": 2686, - "server/src/__tests__/execution-workspaces-service.test.ts": 33572, - "server/src/__tests__/export-fidelity.test.ts": 7293, - "server/src/__tests__/external-objects-service.test.ts": 9447, - "server/src/__tests__/fast-response-models.test.ts": 167, - "server/src/__tests__/fast-response-provider.test.ts": 972, - "server/src/__tests__/fast-responses.test.ts": 10807, - "server/src/__tests__/feedback-flush-controller.test.ts": 4249, - "server/src/__tests__/feedback-service.test.ts": 8478, - "server/src/__tests__/feedback-share-client.test.ts": 172, - "server/src/__tests__/file-delivery-bridges.test.ts": 16263, - "server/src/__tests__/file-resources-git-scan-load.test.ts": 4087, - "server/src/__tests__/file-resources.test.ts": 19949, - "server/src/__tests__/first-admin-claim.test.ts": 8121, - "server/src/__tests__/folders-service.test.ts": 8899, - "server/src/__tests__/forbidden-tokens.test.ts": 170, - "server/src/__tests__/gemini-local-adapter-environment.test.ts": 915, - "server/src/__tests__/gemini-local-adapter.test.ts": 913, - "server/src/__tests__/gemini-local-execute.test.ts": 1108, - "server/src/__tests__/gemini-local-skill-sync.test.ts": 824, - "server/src/__tests__/general-capability-classifier.test.ts": 2001, - "server/src/__tests__/generic-mcp-connection.test.ts": 23336, - "server/src/__tests__/git-credentials.test.ts": 2266, - "server/src/__tests__/github-commit-details.test.ts": 2278, - "server/src/__tests__/github-connection-events.test.ts": 9238, - "server/src/__tests__/github-grant-metadata.test.ts": 2300, - "server/src/__tests__/github-operation-credentials.test.ts": 11187, - "server/src/__tests__/github-skill-source.test.ts": 2913, - "server/src/__tests__/google-sheets-gallery.test.ts": 2307, - "server/src/__tests__/grok-local-skill-sync.test.ts": 783, - "server/src/__tests__/health-workspace-readiness.test.ts": 2039, - "server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts": 14233, - "server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts": 12611, - "server/src/__tests__/heartbeat-agent-session-message.test.ts": 3483, - "server/src/__tests__/heartbeat-archived-company-guard.test.ts": 10431, - "server/src/__tests__/heartbeat-auto-checkout.test.ts": 3274, - "server/src/__tests__/heartbeat-chat-task-link.test.ts": 3135, - "server/src/__tests__/heartbeat-comment-wake-batching.test.ts": 48398, - "server/src/__tests__/heartbeat-context-summary.test.ts": 3034, - "server/src/__tests__/heartbeat-cost-accounting.test.ts": 2824, - "server/src/__tests__/heartbeat-direct-adapter-native-isolation.test.ts": 12836, - "server/src/__tests__/heartbeat-issue-rewake-throttle.test.ts": 14666, - "server/src/__tests__/heartbeat-ledger-billing-code.test.ts": 2939, - "server/src/__tests__/heartbeat-list.test.ts": 9777, - "server/src/__tests__/heartbeat-local-environment.test.ts": 11581, - "server/src/__tests__/heartbeat-lock-release-on-reassignment.test.ts": 12529, - "server/src/__tests__/heartbeat-managed-clone-credentials.test.ts": 4062, - "server/src/__tests__/heartbeat-model-fallback-warning.test.ts": 2941, - "server/src/__tests__/heartbeat-native-cleanup-admission.test.ts": 11377, - "server/src/__tests__/heartbeat-native-runner-cancellation.test.ts": 2802, - "server/src/__tests__/heartbeat-native-runner-selection.test.ts": 1173, - "server/src/__tests__/heartbeat-native-status-context.test.ts": 2793, - "server/src/__tests__/heartbeat-orphaned-active-lease-sweep.test.ts": 11197, - "server/src/__tests__/heartbeat-pending-cleanup-sweep.test.ts": 11748, - "server/src/__tests__/heartbeat-plugin-environment.test.ts": 9857, - "server/src/__tests__/heartbeat-project-env.test.ts": 3014, - "server/src/__tests__/heartbeat-project-repositories.test.ts": 21194, - "server/src/__tests__/heartbeat-queued-run-claim-isolation.test.ts": 12879, - "server/src/__tests__/heartbeat-referenced-projects.test.ts": 9793, - "server/src/__tests__/heartbeat-remote-referenced-projects.test.ts": 2938, - "server/src/__tests__/heartbeat-responsible-user-invariant.test.ts": 24742, - "server/src/__tests__/heartbeat-retry-scheduling.test.ts": 19896, - "server/src/__tests__/heartbeat-reviewed-chat-binding.integration.test.ts": 16691, - "server/src/__tests__/heartbeat-run-event-sequencing.test.ts": 19692, - "server/src/__tests__/heartbeat-run-lease-release-terminalization.test.ts": 10782, - "server/src/__tests__/heartbeat-run-log.test.ts": 3106, - "server/src/__tests__/heartbeat-run-status-payload.test.ts": 3009, - "server/src/__tests__/heartbeat-run-summary.test.ts": 186, - "server/src/__tests__/heartbeat-run-terminalize-before-release.test.ts": 10597, - "server/src/__tests__/heartbeat-running-followup.test.ts": 3054, - "server/src/__tests__/heartbeat-runtime-mcp-servers.test.ts": 11951, - "server/src/__tests__/heartbeat-runtime-skills.test.ts": 13151, - "server/src/__tests__/heartbeat-runtime-state.test.ts": 10413, - "server/src/__tests__/heartbeat-scheduling-suppression.test.ts": 2983, - "server/src/__tests__/heartbeat-slack-invitation.test.ts": 12647, - "server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts": 62987, - "server/src/__tests__/heartbeat-start-lock.test.ts": 174, - "server/src/__tests__/heartbeat-status-preserving-telemetry.test.ts": 9550, - "server/src/__tests__/heartbeat-task-drain-admission-release.test.ts": 13413, - "server/src/__tests__/heartbeat-task-drain.test.ts": 2891, - "server/src/__tests__/heartbeat-task-session-reset.test.ts": 10038, - "server/src/__tests__/heartbeat-timer-wake-session-reset-pf4.test.ts": 3049, - "server/src/__tests__/heartbeat-workspace-branch-containment.test.ts": 18234, - "server/src/__tests__/heartbeat-workspace-busy.test.ts": 28002, - "server/src/__tests__/heartbeat-workspace-finalize-branch.test.ts": 13263, - "server/src/__tests__/heartbeat-workspace-ready-comment.test.ts": 2827, - "server/src/__tests__/heartbeat-workspace-session.test.ts": 3933, - "server/src/__tests__/heartbeat-worktree-suppression.test.ts": 12281, - "server/src/__tests__/heartbeat-zombie-guard.test.ts": 2823, - "server/src/__tests__/helpers/truncate-with-deadlock-retry.test.ts": 345, - "server/src/__tests__/hire-hook.test.ts": 630, - "server/src/__tests__/hiring-operational-examples.test.ts": 700, - "server/src/__tests__/http-adapter-remote-fetch.test.ts": 181, - "server/src/__tests__/http-log-policy.test.ts": 175, - "server/src/__tests__/http-log-redaction.test.ts": 1483, - "server/src/__tests__/human-directed-work.test.ts": 10053, - "server/src/__tests__/idle-accounting-work.test.ts": 8526, - "server/src/__tests__/idle-admission.test.ts": 377, - "server/src/__tests__/idle-database-backup.test.ts": 12993, - "server/src/__tests__/idle-local-work.test.ts": 178, - "server/src/__tests__/idle-sleep-safety.test.ts": 24972, - "server/src/__tests__/inbox-dismissals.test.ts": 11123, - "server/src/__tests__/instance-settings-cloud-defaults.test.ts": 1179, - "server/src/__tests__/instance-settings-managed-overlay.test.ts": 1161, - "server/src/__tests__/instance-settings-operator-defaults.test.ts": 1156, - "server/src/__tests__/instance-settings-service.test.ts": 1098, - "server/src/__tests__/instrumentation.test.ts": 230, - "server/src/__tests__/interaction-resolution-cross-issue-cap-postgres.test.ts": 11676, - "server/src/__tests__/invite-join-grants.test.ts": 706, - "server/src/__tests__/invite-rate-limit.test.ts": 153, - "server/src/__tests__/invite-token-entropy.test.ts": 3531, - "server/src/__tests__/issue-blocker-attention.test.ts": 11127, - "server/src/__tests__/issue-comment-redaction.test.ts": 10738, - "server/src/__tests__/issue-continuation-summary.test.ts": 1166, - "server/src/__tests__/issue-execution-policy-persistence.test.ts": 9062, - "server/src/__tests__/issue-execution-policy.test.ts": 873, - "server/src/__tests__/issue-existing-branch-validation-status.test.ts": 1396, - "server/src/__tests__/issue-goal-fallback.test.ts": 164, - "server/src/__tests__/issue-interactions-read-only-contract.test.ts": 160, - "server/src/__tests__/issue-liveness.test.ts": 900, - "server/src/__tests__/issue-monitor-scheduler.test.ts": 29650, - "server/src/__tests__/issue-recovery-actions.test.ts": 18737, - "server/src/__tests__/issue-references-service.test.ts": 8595, - "server/src/__tests__/issue-remove-cascade.test.ts": 9294, - "server/src/__tests__/issue-review-attention.test.ts": 9710, - "server/src/__tests__/issue-review-policy.test.ts": 7578, - "server/src/__tests__/issue-rewake-throttle.test.ts": 158, - "server/src/__tests__/issue-runtime-workspace-binding.test.ts": 9585, - "server/src/__tests__/issue-thread-interactions-service.test.ts": 15275, - "server/src/__tests__/issue-thread-interactions-telemetry.test.ts": 9603, - "server/src/__tests__/issue-tree-control-service-unit.test.ts": 1138, - "server/src/__tests__/issue-tree-control-service.test.ts": 9622, - "server/src/__tests__/issues-list-query-parsing.test.ts": 10346, - "server/src/__tests__/issues-parent-id-alias.test.ts": 10268, - "server/src/__tests__/issues-user-context.test.ts": 2295, - "server/src/__tests__/join-request-dedupe.test.ts": 171, - "server/src/__tests__/json-parse-error-handler.test.ts": 1367, - "server/src/__tests__/json-schema-secret-refs.test.ts": 161, - "server/src/__tests__/kimi-local-skill-sync.test.ts": 831, - "server/src/__tests__/legacy-continuation-authority.test.ts": 13939, - "server/src/__tests__/legacy-finalization-regression.test.ts": 10146, - "server/src/__tests__/legacy-runtime-tool-refresh.test.ts": 918, - "server/src/__tests__/legacy-workspace-restore-recovery.test.ts": 10444, - "server/src/__tests__/live-events-toast.test.ts": 712, - "server/src/__tests__/live-events-ws.test.ts": 1357, - "server/src/__tests__/local-ai-credential-file.test.ts": 171, - "server/src/__tests__/local-ai-credentials.test.ts": 823, - "server/src/__tests__/local-ai-login-policy.test.ts": 162, - "server/src/__tests__/local-service-supervisor.test.ts": 4591, - "server/src/__tests__/log-redaction.test.ts": 165, - "server/src/__tests__/logger-tz.test.ts": 193, - "server/src/__tests__/login-session-runtime.test.ts": 1220, - "server/src/__tests__/managed-config.test.ts": 710, - "server/src/__tests__/managed-loopback-auth.test.ts": 8012, - "server/src/__tests__/managed-runtime-public-url.test.ts": 733, - "server/src/__tests__/managed-worktree-inspection.test.ts": 2519, - "server/src/__tests__/mcp-http.test.ts": 189, - "server/src/__tests__/memory-connectors.test.ts": 2277, - "server/src/__tests__/merged-pr-confirmation-sweep.test.ts": 9905, - "server/src/__tests__/monthly-spend-service.test.ts": 3181, - "server/src/__tests__/native-cancellation-request.integration.test.ts": 9488, - "server/src/__tests__/native-connection-baseline-fixture.test.ts": 14182, - "server/src/__tests__/native-event-boundary-golden.test.ts": 7599, - "server/src/__tests__/native-finalization-migration.test.ts": 7280, - "server/src/__tests__/native-finalization-recovery.test.ts": 17018, - "server/src/__tests__/native-interaction-bridge.test.ts": 9388, - "server/src/__tests__/native-procedure-measurement.test.ts": 2848, - "server/src/__tests__/native-run-finalizer.test.ts": 19882, - "server/src/__tests__/native-runner-input-boundary.test.ts": 204, - "server/src/__tests__/native-runner-phase6.integration.test.ts": 19600, - "server/src/__tests__/native-sandbox-lifecycle.test.ts": 2850, - "server/src/__tests__/native-session-resumption.test.ts": 32645, - "server/src/__tests__/native-status-arbiter-corpus.test.ts": 27304, - "server/src/__tests__/native-workspace-sync-history.test.ts": 5477, - "server/src/__tests__/native-workspace-sync.test.ts": 1275, - "server/src/__tests__/oauth-client-metadata.test.ts": 2323, - "server/src/__tests__/openclaw-gateway-adapter.test.ts": 843, - "server/src/__tests__/opencode-local-adapter.test.ts": 801, - "server/src/__tests__/opencode-local-skill-sync.test.ts": 813, - "server/src/__tests__/owner-chat-instruction-authority.test.ts": 11518, - "server/src/__tests__/paperclip-env.test.ts": 778, - "server/src/__tests__/paperclip-issue-update-helper.test.ts": 4937, - "server/src/__tests__/paperclip-semantic-conformance.test.ts": 11300, - "server/src/__tests__/paperclip-skill-utils.test.ts": 8496, - "server/src/__tests__/parse-status-filter.test.ts": 2204, - "server/src/__tests__/persisted-workspace-source.test.ts": 4374, - "server/src/__tests__/photon/channel.integration.test.ts": 27031, - "server/src/__tests__/photon/photon.test.ts": 1180, - "server/src/__tests__/pi-local-adapter-environment.test.ts": 912, - "server/src/__tests__/pi-local-execute.test.ts": 1068, - "server/src/__tests__/pi-local-skill-sync.test.ts": 806, - "server/src/__tests__/plan-task-fixture.test.ts": 12967, - "server/src/__tests__/plugin-access-authorization-host-services.test.ts": 10822, - "server/src/__tests__/plugin-agent-sessions.test.ts": 2409, - "server/src/__tests__/plugin-config-startup-delivery.test.ts": 8163, - "server/src/__tests__/plugin-database.test.ts": 8634, - "server/src/__tests__/plugin-dev-watcher.test.ts": 255, - "server/src/__tests__/plugin-environment-driver-ready-recovery.test.ts": 6732, - "server/src/__tests__/plugin-environment-driver-sandbox-capabilities.test.ts": 167, - "server/src/__tests__/plugin-environment-driver-seam.test.ts": 854, - "server/src/__tests__/plugin-execution-workspace-bridge.test.ts": 185, - "server/src/__tests__/plugin-host-services-span.test.ts": 3026, - "server/src/__tests__/plugin-idle-manager.test.ts": 3863, - "server/src/__tests__/plugin-idle-rpc.test.ts": 282, - "server/src/__tests__/plugin-install-autobuild.test.ts": 10710, - "server/src/__tests__/plugin-install-guard.test.ts": 1356, - "server/src/__tests__/plugin-lifecycle-restart.test.ts": 1168, - "server/src/__tests__/plugin-loader-error-retry.test.ts": 1419, - "server/src/__tests__/plugin-local-folders.test.ts": 212, - "server/src/__tests__/plugin-managed-agents.test.ts": 11887, - "server/src/__tests__/plugin-managed-routines.test.ts": 9690, - "server/src/__tests__/plugin-managed-skills.test.ts": 11378, - "server/src/__tests__/plugin-orchestration-apis.test.ts": 14904, - "server/src/__tests__/plugin-organization-switcher.test.ts": 736, - "server/src/__tests__/plugin-sandbox-provider-config-validation.test.ts": 177, - "server/src/__tests__/plugin-sdk-orchestration-contract.test.ts": 858, - "server/src/__tests__/plugin-sdk-testing.test.ts": 737, - "server/src/__tests__/plugin-secrets-handler.test.ts": 9449, - "server/src/__tests__/plugin-telemetry-bridge.test.ts": 2951, - "server/src/__tests__/plugin-tenant-isolation.test.ts": 10381, - "server/src/__tests__/plugin-tool-dispatcher-pluginDbId.test.ts": 665, - "server/src/__tests__/plugin-ui-static.test.ts": 1332, - "server/src/__tests__/plugin-worker-manager-duplex.test.ts": 2651, - "server/src/__tests__/plugin-worker-manager.test.ts": 4242, - "server/src/__tests__/primary-agent.test.ts": 11142, - "server/src/__tests__/privacy-production-review.test.ts": 12043, - "server/src/__tests__/private-hostname-guard.test.ts": 221, - "server/src/__tests__/private-json-etag.test.ts": 217, - "server/src/__tests__/project-goal-validation.test.ts": 8385, - "server/src/__tests__/project-icon-persistence.test.ts": 8365, - "server/src/__tests__/project-list-metrics.test.ts": 1149, - "server/src/__tests__/project-repositories-persistence.test.ts": 9637, - "server/src/__tests__/project-repositories.test.ts": 2275, - "server/src/__tests__/project-shortname-resolution.test.ts": 1181, - "server/src/__tests__/project-tools-pagination.test.ts": 1051, - "server/src/__tests__/provider-trace-store.test.ts": 653, - "server/src/__tests__/public-mcp-client-metadata.test.ts": 748, - "server/src/__tests__/public-mcp.test.ts": 25756, - "server/src/__tests__/qa-routine-secrets-e2e.test.ts": 11519, - "server/src/__tests__/question-response-delivery.test.ts": 13572, - "server/src/__tests__/quota-windows-service.test.ts": 1324, - "server/src/__tests__/quota-windows.test.ts": 859, - "server/src/__tests__/railway-connection.test.ts": 11007, - "server/src/__tests__/railway-ssh.test.ts": 1249, - "server/src/__tests__/railway.test.ts": 272, - "server/src/__tests__/recovery-classifiers.test.ts": 2321, - "server/src/__tests__/recovery-observability.test.ts": 8196, - "server/src/__tests__/recovery-stale-issue-lock-sweep.test.ts": 11076, - "server/src/__tests__/redact-sensitive.test.ts": 177, - "server/src/__tests__/remote-http-endpoint-guard.test.ts": 175, - "server/src/__tests__/remote-http-rebinding.test.ts": 1077, - "server/src/__tests__/remote-mcp-connectors.test.ts": 11408, - "server/src/__tests__/remote-mcp-pending.test.ts": 2325, - "server/src/__tests__/remote-mcp-protocol.test.ts": 177, - "server/src/__tests__/remote-mcp-redaction.test.ts": 169, - "server/src/__tests__/remote-pi-companion.test.ts": 1019, - "server/src/__tests__/rerun-survival.integration.test.ts": 1255, - "server/src/__tests__/resource-lifecycle-events.test.ts": 12158, - "server/src/__tests__/reusable-leases-default.test.ts": 721, - "server/src/__tests__/routable-blocked.test.ts": 167, - "server/src/__tests__/routine-run-telemetry.test.ts": 9407, - "server/src/__tests__/routines-service.test.ts": 17843, - "server/src/__tests__/run-continuations.test.ts": 648, + "server/src/__tests__/customer-success.test.ts": 1099, + "server/src/__tests__/dashboard-disconnect-retry.test.ts": 1647, + "server/src/__tests__/dashboard-service.test.ts": 954, + "server/src/__tests__/database-backup-work.test.ts": 100, + "server/src/__tests__/daytona-connection.test.ts": 7526, + "server/src/__tests__/daytona-http-boundary.test.ts": 697, + "server/src/__tests__/db-errors.test.ts": 100, + "server/src/__tests__/deadline-work.test.ts": 100, + "server/src/__tests__/decision-effect-recovery-work.test.ts": 7190, + "server/src/__tests__/decision-expiry-work.test.ts": 6743, + "server/src/__tests__/decision-history.test.ts": 827, + "server/src/__tests__/decision-model-provider.test.ts": 100, + "server/src/__tests__/decision-models.test.ts": 2948, + "server/src/__tests__/decision-retention-service.test.ts": 3618, + "server/src/__tests__/decision-target-work.test.ts": 7026, + "server/src/__tests__/decision-training.test.ts": 1892, + "server/src/__tests__/decision-wakeup.test.ts": 100, + "server/src/__tests__/decisions-service.test.ts": 8739, + "server/src/__tests__/delivery-work-coordinator.test.ts": 100, + "server/src/__tests__/deployment-connections.test.ts": 11881, + "server/src/__tests__/dev-runner-native-binary.test.ts": 100, + "server/src/__tests__/dev-runner-options.test.ts": 100, + "server/src/__tests__/dev-runner-output.test.ts": 100, + "server/src/__tests__/dev-runner-paths.test.ts": 100, + "server/src/__tests__/dev-runner-snapshot.test.ts": 100, + "server/src/__tests__/dev-runner-worktree.test.ts": 100, + "server/src/__tests__/dev-server-status.test.ts": 130, + "server/src/__tests__/dev-watch-ignore.test.ts": 100, + "server/src/__tests__/device-login-credential-read.test.ts": 468, + "server/src/__tests__/device-login-reaper.test.ts": 100, + "server/src/__tests__/device-login-service.test.ts": 1051, + "server/src/__tests__/disposition-repair.test.ts": 100, + "server/src/__tests__/distribution-plugin-catalog.test.ts": 100, + "server/src/__tests__/docker-build-stamp.test.ts": 100, + "server/src/__tests__/docker-entrypoint.test.ts": 358, + "server/src/__tests__/document-annotations-service.test.ts": 1433, + "server/src/__tests__/documents-service.test.ts": 1150, + "server/src/__tests__/documents.test.ts": 100, + "server/src/__tests__/dot-invitations.test.ts": 1589, + "server/src/__tests__/dot-oauth-onboarding.test.ts": 2022, + "server/src/__tests__/duplex-observability-recorder.test.ts": 100, + "server/src/__tests__/durable-chat-wakeup.test.ts": 25780, + "server/src/__tests__/durable-directory.test.ts": 1278, + "server/src/__tests__/effective-run-config-fingerprints.test.ts": 100, + "server/src/__tests__/email-address-check.test.ts": 103, + "server/src/__tests__/email-channels.integration.test.ts": 26680, + "server/src/__tests__/email-idle-work.test.ts": 100, + "server/src/__tests__/embedded-postgres-supervisor.test.ts": 100, + "server/src/__tests__/env-file-policy.test.ts": 100, + "server/src/__tests__/environment-acquisition-diagnostics.test.ts": 1677, + "server/src/__tests__/environment-capability-contract.test.ts": 100, + "server/src/__tests__/environment-cleanup-work.test.ts": 11510, + "server/src/__tests__/environment-config.test.ts": 100, + "server/src/__tests__/environment-custom-image-terminal-ws.test.ts": 2566, + "server/src/__tests__/environment-custom-images-service.test.ts": 5132, + "server/src/__tests__/environment-driver-traits.test.ts": 100, + "server/src/__tests__/environment-execution-target-capabilities.test.ts": 100, + "server/src/__tests__/environment-execution-target-duplex-kill-switch.test.ts": 100, + "server/src/__tests__/environment-execution-target-duplex.test.ts": 100, + "server/src/__tests__/environment-execution-target.test.ts": 100, + "server/src/__tests__/environment-live-ssh.test.ts": 966, + "server/src/__tests__/environment-probe.test.ts": 100, + "server/src/__tests__/environment-run-orchestrator.test.ts": 100, + "server/src/__tests__/environment-runtime-driver-contract.test.ts": 1498, + "server/src/__tests__/environment-runtime-duplex-bridge-input.test.ts": 385, + "server/src/__tests__/environment-runtime.test.ts": 30510, + "server/src/__tests__/environment-service.test.ts": 3548, + "server/src/__tests__/environment-skills.test.ts": 7748, + "server/src/__tests__/environment-sync-out-error.test.ts": 100, + "server/src/__tests__/environment-task-authority.integration.test.ts": 529, + "server/src/__tests__/environment-task-runtime.test.ts": 100, + "server/src/__tests__/environment-test-harness.test.ts": 100, + "server/src/__tests__/error-handler.test.ts": 100, + "server/src/__tests__/eval-feedback-recovery.integration.test.ts": 1360, + "server/src/__tests__/execution-control-work.test.ts": 14891, + "server/src/__tests__/execution-lock-orphan-cleanup.test.ts": 1389, + "server/src/__tests__/execution-workspace-compaction.test.ts": 4581, + "server/src/__tests__/execution-workspace-policy.test.ts": 100, + "server/src/__tests__/execution-workspace-reopen.test.ts": 10356, + "server/src/__tests__/execution-workspace-runtime-control-conflict.test.ts": 3467, + "server/src/__tests__/execution-workspaces-service.test.ts": 17150, + "server/src/__tests__/export-fidelity.test.ts": 696, + "server/src/__tests__/external-objects-service.test.ts": 1389, + "server/src/__tests__/fast-response-models.test.ts": 100, + "server/src/__tests__/fast-response-provider.test.ts": 106, + "server/src/__tests__/fast-responses.test.ts": 3445, + "server/src/__tests__/feedback-flush-controller.test.ts": 100, + "server/src/__tests__/feedback-redaction.test.ts": 100, + "server/src/__tests__/feedback-service.test.ts": 1643, + "server/src/__tests__/feedback-share-client.test.ts": 100, + "server/src/__tests__/file-delivery-bridges.test.ts": 8500, + "server/src/__tests__/file-resources-git-scan-load.test.ts": 685, + "server/src/__tests__/file-resources.test.ts": 4289, + "server/src/__tests__/first-admin-claim.test.ts": 376, + "server/src/__tests__/folders-service.test.ts": 1779, + "server/src/__tests__/forbidden-tokens.test.ts": 100, + "server/src/__tests__/gateway-credential-expiry.test.ts": 4439, + "server/src/__tests__/gemini-local-adapter-environment.test.ts": 105, + "server/src/__tests__/gemini-local-adapter.test.ts": 100, + "server/src/__tests__/gemini-local-execute.test.ts": 281, + "server/src/__tests__/gemini-local-skill-sync.test.ts": 100, + "server/src/__tests__/general-capability-classifier.test.ts": 100, + "server/src/__tests__/generic-mcp-connection.test.ts": 13212, + "server/src/__tests__/git-credentials.test.ts": 100, + "server/src/__tests__/git-expiration-auth-fallback.test.ts": 100, + "server/src/__tests__/git-reproducible-managed-checkouts.test.ts": 4097, + "server/src/__tests__/git-restore-point-expiration-config.test.ts": 100, + "server/src/__tests__/git-restore-point-expiration.test.ts": 43649, + "server/src/__tests__/git-restore-point-ledger-config.test.ts": 100, + "server/src/__tests__/git-restore-point-ledger.test.ts": 2717, + "server/src/__tests__/git-restore-point.test.ts": 7454, + "server/src/__tests__/git-workspace-activity.test.ts": 13651, + "server/src/__tests__/git-workspace-base-storage.test.ts": 408, + "server/src/__tests__/git-workspace-contents.test.ts": 602, + "server/src/__tests__/git-workspace-expiration-eligibility.test.ts": 11087, + "server/src/__tests__/git-workspace-expiration.test.ts": 127807, + "server/src/__tests__/git-workspace-fetch-head.test.ts": 1119, + "server/src/__tests__/github-catalog-timing.test.ts": 888, + "server/src/__tests__/github-commit-details.test.ts": 100, + "server/src/__tests__/github-connection-events.test.ts": 2229, + "server/src/__tests__/github-grant-metadata.test.ts": 100, + "server/src/__tests__/github-health-recovery.test.ts": 100, + "server/src/__tests__/github-operation-credentials.test.ts": 3473, + "server/src/__tests__/github-review-wait.test.ts": 1230, + "server/src/__tests__/github-skill-source.test.ts": 15585, + "server/src/__tests__/google-sheets-gallery.test.ts": 100, + "server/src/__tests__/grok-bot-routine.test.ts": 12801, + "server/src/__tests__/grok-dispatch-work.test.ts": 5780, + "server/src/__tests__/grok-local-skill-sync.test.ts": 100, + "server/src/__tests__/health-workspace-readiness.test.ts": 104, + "server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts": 11149, + "server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts": 9465, + "server/src/__tests__/heartbeat-agent-session-message.test.ts": 724, + "server/src/__tests__/heartbeat-agent-status-recovery.test.ts": 1192, + "server/src/__tests__/heartbeat-archived-company-guard.test.ts": 929, + "server/src/__tests__/heartbeat-auto-checkout.test.ts": 100, + "server/src/__tests__/heartbeat-case-handoff.test.ts": 3346, + "server/src/__tests__/heartbeat-case-terminal-handoff.test.ts": 1416, + "server/src/__tests__/heartbeat-chat-task-link.test.ts": 100, + "server/src/__tests__/heartbeat-comment-wake-batching.test.ts": 48217, + "server/src/__tests__/heartbeat-context-summary.test.ts": 100, + "server/src/__tests__/heartbeat-cost-accounting.test.ts": 100, + "server/src/__tests__/heartbeat-direct-adapter-native-isolation.test.ts": 11196, + "server/src/__tests__/heartbeat-execution-configuration.test.ts": 20017, + "server/src/__tests__/heartbeat-git-restore-point-resume.test.ts": 15431, + "server/src/__tests__/heartbeat-instruction-working-copy.test.ts": 10009, + "server/src/__tests__/heartbeat-issue-rewake-throttle.test.ts": 5692, + "server/src/__tests__/heartbeat-ledger-billing-code.test.ts": 100, + "server/src/__tests__/heartbeat-list.test.ts": 1096, + "server/src/__tests__/heartbeat-local-environment.test.ts": 8777, + "server/src/__tests__/heartbeat-lock-release-on-reassignment.test.ts": 2557, + "server/src/__tests__/heartbeat-managed-clone-credentials.test.ts": 1599, + "server/src/__tests__/heartbeat-model-fallback-warning.test.ts": 100, + "server/src/__tests__/heartbeat-native-cleanup-admission.test.ts": 3338, + "server/src/__tests__/heartbeat-native-runner-cancellation.test.ts": 100, + "server/src/__tests__/heartbeat-native-status-context.test.ts": 100, + "server/src/__tests__/heartbeat-orphaned-active-lease-sweep.test.ts": 2143, + "server/src/__tests__/heartbeat-pending-cleanup-sweep.test.ts": 2549, + "server/src/__tests__/heartbeat-plugin-environment.test.ts": 8071, + "server/src/__tests__/heartbeat-project-env.test.ts": 100, + "server/src/__tests__/heartbeat-project-repositories.test.ts": 117824, + "server/src/__tests__/heartbeat-queued-run-claim-isolation.test.ts": 14266, + "server/src/__tests__/heartbeat-referenced-projects.test.ts": 1014, + "server/src/__tests__/heartbeat-remote-referenced-projects.test.ts": 100, + "server/src/__tests__/heartbeat-responsible-user-invariant.test.ts": 26919, + "server/src/__tests__/heartbeat-retry-scheduling.test.ts": 13452, + "server/src/__tests__/heartbeat-reviewed-chat-binding.integration.test.ts": 2806, + "server/src/__tests__/heartbeat-run-event-atomic-append.test.ts": 1036, + "server/src/__tests__/heartbeat-run-event-sequencing.test.ts": 5842, + "server/src/__tests__/heartbeat-run-lease-release-terminalization.test.ts": 1097, + "server/src/__tests__/heartbeat-run-log.test.ts": 100, + "server/src/__tests__/heartbeat-run-output.test.ts": 7368, + "server/src/__tests__/heartbeat-run-privacy-disconnect-retry.test.ts": 367, + "server/src/__tests__/heartbeat-run-status-payload.test.ts": 100, + "server/src/__tests__/heartbeat-run-summary.test.ts": 100, + "server/src/__tests__/heartbeat-run-terminalize-before-release.test.ts": 1815, + "server/src/__tests__/heartbeat-running-followup.test.ts": 100, + "server/src/__tests__/heartbeat-runtime-mcp-servers.test.ts": 3100, + "server/src/__tests__/heartbeat-runtime-services.test.ts": 13024, + "server/src/__tests__/heartbeat-runtime-skills.test.ts": 14019, + "server/src/__tests__/heartbeat-runtime-state.test.ts": 1057, + "server/src/__tests__/heartbeat-scheduling-suppression.test.ts": 100, + "server/src/__tests__/heartbeat-session-preparation.test.ts": 26380, + "server/src/__tests__/heartbeat-slack-invitation.test.ts": 8771, + "server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts": 130330, + "server/src/__tests__/heartbeat-start-lock.test.ts": 100, + "server/src/__tests__/heartbeat-status-preserving-telemetry.test.ts": 688, + "server/src/__tests__/heartbeat-task-context.test.ts": 11920, + "server/src/__tests__/heartbeat-task-directory-restore.test.ts": 7290, + "server/src/__tests__/heartbeat-task-drain-admission-release.test.ts": 14014, + "server/src/__tests__/heartbeat-task-drain.test.ts": 100, + "server/src/__tests__/heartbeat-task-session-reset.test.ts": 651, + "server/src/__tests__/heartbeat-timer-wake-session-reset-pf4.test.ts": 100, + "server/src/__tests__/heartbeat-undispatched-execution-wait.test.ts": 1674, + "server/src/__tests__/heartbeat-workspace-branch-containment.test.ts": 26522, + "server/src/__tests__/heartbeat-workspace-busy.test.ts": 41306, + "server/src/__tests__/heartbeat-workspace-finalize-branch.test.ts": 8083, + "server/src/__tests__/heartbeat-workspace-ready-comment.test.ts": 100, + "server/src/__tests__/heartbeat-workspace-session.test.ts": 438, + "server/src/__tests__/heartbeat-workspace-source-trust.test.ts": 70465, + "server/src/__tests__/heartbeat-worktree-suppression.test.ts": 5350, + "server/src/__tests__/heartbeat-zombie-guard.test.ts": 100, + "server/src/__tests__/helpers/external-test-database.test.ts": 100, + "server/src/__tests__/helpers/truncate-with-deadlock-retry.test.ts": 100, + "server/src/__tests__/hire-hook.test.ts": 100, + "server/src/__tests__/hiring-operational-examples.test.ts": 100, + "server/src/__tests__/historical-cleanup-verification.test.ts": 16240, + "server/src/__tests__/historical-provider-credentials.test.ts": 8749, + "server/src/__tests__/http-adapter-remote-fetch.test.ts": 100, + "server/src/__tests__/http-log-policy.test.ts": 100, + "server/src/__tests__/http-log-redaction.test.ts": 277, + "server/src/__tests__/human-directed-work.test.ts": 1183, + "server/src/__tests__/idle-accounting-work.test.ts": 3876, + "server/src/__tests__/idle-admission.test.ts": 100, + "server/src/__tests__/idle-connection-history.test.ts": 25776, + "server/src/__tests__/idle-database-backup.test.ts": 9210, + "server/src/__tests__/idle-issue-schedule.test.ts": 29447, + "server/src/__tests__/idle-local-work.test.ts": 100, + "server/src/__tests__/idle-native-finalization.test.ts": 35038, + "server/src/__tests__/idle-response-waits.test.ts": 19697, + "server/src/__tests__/idle-scheduled-work.test.ts": 100, + "server/src/__tests__/idle-sleep-safety.test.ts": 77732, + "server/src/__tests__/import-spool-cleanup-work.test.ts": 1264, + "server/src/__tests__/inbound-queue-discovery.test.ts": 2429, + "server/src/__tests__/inbox-dismissals.test.ts": 1420, + "server/src/__tests__/instance-settings-cloud-defaults.test.ts": 100, + "server/src/__tests__/instance-settings-managed-overlay.test.ts": 100, + "server/src/__tests__/instance-settings-operator-defaults.test.ts": 100, + "server/src/__tests__/instance-settings-service.test.ts": 100, + "server/src/__tests__/instrumentation.test.ts": 100, + "server/src/__tests__/interaction-resolution-cross-issue-cap-postgres.test.ts": 2896, + "server/src/__tests__/invite-join-grants.test.ts": 100, + "server/src/__tests__/invite-rate-limit.test.ts": 100, + "server/src/__tests__/invite-token-entropy.test.ts": 100, + "server/src/__tests__/isolated-repository-rehome.test.ts": 66430, + "server/src/__tests__/isolated-task-directory-retention-config.test.ts": 100, + "server/src/__tests__/isolated-task-directory-retention.test.ts": 27655, + "server/src/__tests__/isolated-test-home.test.ts": 186, + "server/src/__tests__/issue-blocker-attention.test.ts": 3005, + "server/src/__tests__/issue-comment-receipt-postgres.test.ts": 1007, + "server/src/__tests__/issue-comment-redaction.test.ts": 1592, + "server/src/__tests__/issue-continuation-summary.test.ts": 100, + "server/src/__tests__/issue-creation-source-read.test.ts": 1293, + "server/src/__tests__/issue-dependency-restore-wake-policy.test.ts": 100, + "server/src/__tests__/issue-execution-policy-persistence.test.ts": 582, + "server/src/__tests__/issue-execution-policy.test.ts": 100, + "server/src/__tests__/issue-existing-branch-validation-status.test.ts": 100, + "server/src/__tests__/issue-goal-fallback.test.ts": 100, + "server/src/__tests__/issue-liveness.test.ts": 100, + "server/src/__tests__/issue-monitor-scheduler.test.ts": 35941, + "server/src/__tests__/issue-recovery-actions.test.ts": 10937, + "server/src/__tests__/issue-references-service.test.ts": 1165, + "server/src/__tests__/issue-remove-cascade.test.ts": 888, + "server/src/__tests__/issue-review-attention.test.ts": 1005, + "server/src/__tests__/issue-review-policy.test.ts": 791, + "server/src/__tests__/issue-rewake-throttle.test.ts": 100, + "server/src/__tests__/issue-runtime-workspace-binding.test.ts": 592, + "server/src/__tests__/issue-thread-interactions-service.test.ts": 8093, + "server/src/__tests__/issue-thread-interactions-telemetry.test.ts": 1063, + "server/src/__tests__/issue-tree-control-service-unit.test.ts": 100, + "server/src/__tests__/issue-tree-control-service.test.ts": 1362, + "server/src/__tests__/issues-creation-visibility.test.ts": 2709, + "server/src/__tests__/issues-list-query-parsing.test.ts": 1251, + "server/src/__tests__/issues-parent-id-alias.test.ts": 1712, + "server/src/__tests__/issues-unread-inbox.test.ts": 958, + "server/src/__tests__/issues-user-context.test.ts": 100, + "server/src/__tests__/join-request-dedupe.test.ts": 100, + "server/src/__tests__/join-request-retention-work.test.ts": 10564, + "server/src/__tests__/json-parse-error-handler.test.ts": 100, + "server/src/__tests__/json-schema-secret-refs.test.ts": 100, + "server/src/__tests__/kimi-connection-probe.test.ts": 228, + "server/src/__tests__/kimi-local-skill-sync.test.ts": 100, + "server/src/__tests__/legacy-continuation-authority.test.ts": 6012, + "server/src/__tests__/legacy-finalization-regression.test.ts": 2679, + "server/src/__tests__/legacy-runtime-tool-refresh.test.ts": 100, + "server/src/__tests__/legacy-workspace-restore-recovery.test.ts": 2647, + "server/src/__tests__/live-events-run-read.test.ts": 2851, + "server/src/__tests__/live-events-toast.test.ts": 100, + "server/src/__tests__/live-events-ws.test.ts": 100, + "server/src/__tests__/liveness.test.ts": 164, + "server/src/__tests__/local-ai-credential-file.test.ts": 100, + "server/src/__tests__/local-ai-credentials.test.ts": 100, + "server/src/__tests__/local-ai-login-policy.test.ts": 100, + "server/src/__tests__/local-encrypted-cipher.test.ts": 113, + "server/src/__tests__/local-service-supervisor.test.ts": 2420, + "server/src/__tests__/log-redaction.test.ts": 100, + "server/src/__tests__/logger-tz.test.ts": 100, + "server/src/__tests__/login-cleanup-work.test.ts": 9121, + "server/src/__tests__/login-session-runtime.test.ts": 100, + "server/src/__tests__/managed-cloud-prerequisites.test.ts": 146, + "server/src/__tests__/managed-config.test.ts": 100, + "server/src/__tests__/managed-loopback-auth.test.ts": 856, + "server/src/__tests__/managed-repository-checkout.test.ts": 527, + "server/src/__tests__/managed-runtime-public-url.test.ts": 100, + "server/src/__tests__/managed-skill-content.test.ts": 43407, + "server/src/__tests__/managed-worktree-inspection.test.ts": 456, + "server/src/__tests__/mcp-http.test.ts": 100, + "server/src/__tests__/memory-connectors.test.ts": 100, + "server/src/__tests__/merged-pr-confirmation-sweep.test.ts": 688, + "server/src/__tests__/monthly-spend-service.test.ts": 100, + "server/src/__tests__/muse-client-contract.test.ts": 5919, + "server/src/__tests__/muse-control.test.ts": 72080, + "server/src/__tests__/muse-terminal-settlement.test.ts": 20440, + "server/src/__tests__/native-cancellation-request.integration.test.ts": 3650, + "server/src/__tests__/native-connection-baseline-fixture.test.ts": 1824, + "server/src/__tests__/native-environment-settlement-work.test.ts": 11811, + "server/src/__tests__/native-event-boundary-golden.test.ts": 759, + "server/src/__tests__/native-finalization-migration.test.ts": 1897, + "server/src/__tests__/native-finalization-recovery.test.ts": 1697, + "server/src/__tests__/native-finalization-settled-candidates.test.ts": 1438, + "server/src/__tests__/native-harness-backup-expiration-config.test.ts": 100, + "server/src/__tests__/native-harness-backup-expiration.test.ts": 14338, + "server/src/__tests__/native-interaction-bridge.test.ts": 1696, + "server/src/__tests__/native-observer-replay.test.ts": 1961, + "server/src/__tests__/native-procedure-measurement.test.ts": 224, + "server/src/__tests__/native-recovery-notice-work.test.ts": 6788, + "server/src/__tests__/native-result-commit-work.test.ts": 6141, + "server/src/__tests__/native-run-projection-work.test.ts": 4624, + "server/src/__tests__/native-runner-input-boundary.test.ts": 100, + "server/src/__tests__/native-sandbox-lifecycle.test.ts": 100, + "server/src/__tests__/native-session-resumption.test.ts": 15272, + "server/src/__tests__/native-settled-process-release.test.ts": 4415, + "server/src/__tests__/native-status-arbiter-corpus.test.ts": 47290, + "server/src/__tests__/native-status-reassessment-work.test.ts": 29967, + "server/src/__tests__/native-workspace-finalization-work.test.ts": 4728, + "server/src/__tests__/native-workspace-sync-history.test.ts": 29644, + "server/src/__tests__/native-workspace-sync-retention-config.test.ts": 100, + "server/src/__tests__/native-workspace-sync-retention.test.ts": 2679, + "server/src/__tests__/native-workspace-sync.test.ts": 112, + "server/src/__tests__/nested-clone-migration.test.ts": 66205, + "server/src/__tests__/oauth-client-metadata.test.ts": 100, + "server/src/__tests__/onboarding-first-task-admission.test.ts": 13858, + "server/src/__tests__/onepassword-connection.test.ts": 2891, + "server/src/__tests__/openclaw-gateway-adapter.test.ts": 100, + "server/src/__tests__/opencode-local-adapter.test.ts": 100, + "server/src/__tests__/opencode-local-skill-sync.test.ts": 100, + "server/src/__tests__/owner-chat-instruction-authority.test.ts": 3425, + "server/src/__tests__/paperclip-env.test.ts": 100, + "server/src/__tests__/paperclip-issue-update-helper.test.ts": 4230, + "server/src/__tests__/paperclip-semantic-conformance.test.ts": 2459, + "server/src/__tests__/paperclip-skill-utils.test.ts": 7845, + "server/src/__tests__/parse-status-filter.test.ts": 100, + "server/src/__tests__/persisted-workspace-source.test.ts": 2863, + "server/src/__tests__/photon/channel.integration.test.ts": 28010, + "server/src/__tests__/photon/photon.test.ts": 195, + "server/src/__tests__/pi-local-adapter-environment.test.ts": 125, + "server/src/__tests__/pi-local-execute.test.ts": 400, + "server/src/__tests__/pi-local-skill-sync.test.ts": 100, + "server/src/__tests__/plan-task-fixture.test.ts": 6543, + "server/src/__tests__/plugin-access-authorization-host-services.test.ts": 766, + "server/src/__tests__/plugin-agent-sessions.test.ts": 361, + "server/src/__tests__/plugin-config-startup-delivery.test.ts": 557, + "server/src/__tests__/plugin-database.test.ts": 855, + "server/src/__tests__/plugin-dev-watcher.test.ts": 168, + "server/src/__tests__/plugin-environment-driver-ready-recovery.test.ts": 100, + "server/src/__tests__/plugin-environment-driver-sandbox-capabilities.test.ts": 100, + "server/src/__tests__/plugin-environment-driver-seam.test.ts": 100, + "server/src/__tests__/plugin-environment-driver-selection.test.ts": 470, + "server/src/__tests__/plugin-execution-workspace-bridge.test.ts": 100, + "server/src/__tests__/plugin-fresh-workspaces.integration.test.ts": 1269, + "server/src/__tests__/plugin-host-services-span.test.ts": 100, + "server/src/__tests__/plugin-idle-manager.test.ts": 6623, + "server/src/__tests__/plugin-idle-rpc.test.ts": 1201, + "server/src/__tests__/plugin-inspection-concurrency.test.ts": 137, + "server/src/__tests__/plugin-install-autobuild.test.ts": 4720, + "server/src/__tests__/plugin-install-guard.test.ts": 100, + "server/src/__tests__/plugin-lifecycle-delivery.test.ts": 1395, + "server/src/__tests__/plugin-lifecycle-restart.test.ts": 100, + "server/src/__tests__/plugin-loader-error-retry.test.ts": 100, + "server/src/__tests__/plugin-local-folders.test.ts": 100, + "server/src/__tests__/plugin-managed-agents.test.ts": 3436, + "server/src/__tests__/plugin-managed-routines.test.ts": 1286, + "server/src/__tests__/plugin-managed-skills.test.ts": 12933, + "server/src/__tests__/plugin-operational-reads-tenant-scope.test.ts": 589, + "server/src/__tests__/plugin-orchestration-apis.test.ts": 5194, + "server/src/__tests__/plugin-organization-switcher.test.ts": 100, + "server/src/__tests__/plugin-runner-task.integration.test.ts": 1355, + "server/src/__tests__/plugin-sandbox-provider-config-validation.test.ts": 100, + "server/src/__tests__/plugin-scheduled-work.test.ts": 68364, + "server/src/__tests__/plugin-sdk-orchestration-contract.test.ts": 100, + "server/src/__tests__/plugin-sdk-testing.test.ts": 100, + "server/src/__tests__/plugin-secrets-handler.test.ts": 697, + "server/src/__tests__/plugin-task-credential-writeback.test.ts": 1824, + "server/src/__tests__/plugin-telemetry-bridge.test.ts": 100, + "server/src/__tests__/plugin-tenant-isolation.test.ts": 977, + "server/src/__tests__/plugin-tool-dispatcher-pluginDbId.test.ts": 100, + "server/src/__tests__/plugin-ui-static.test.ts": 1173, + "server/src/__tests__/plugin-warm-tasks.integration.test.ts": 1913, + "server/src/__tests__/plugin-worker-manager-duplex.test.ts": 1717, + "server/src/__tests__/plugin-worker-manager.test.ts": 3537, + "server/src/__tests__/plugin-workspaces.integration.test.ts": 4314, + "server/src/__tests__/primary-agent.test.ts": 1645, + "server/src/__tests__/principal-access-compatibility.test.ts": 653, + "server/src/__tests__/privacy-production-review.test.ts": 5940, + "server/src/__tests__/private-hostname-guard.test.ts": 100, + "server/src/__tests__/private-json-etag.test.ts": 100, + "server/src/__tests__/project-git-authorization.test.ts": 2705, + "server/src/__tests__/project-goal-validation.test.ts": 648, + "server/src/__tests__/project-icon-persistence.test.ts": 733, + "server/src/__tests__/project-instance-secrets.test.ts": 1139, + "server/src/__tests__/project-list-metrics.test.ts": 100, + "server/src/__tests__/project-repositories-persistence.test.ts": 1043, + "server/src/__tests__/project-repositories.test.ts": 100, + "server/src/__tests__/project-shortname-resolution.test.ts": 100, + "server/src/__tests__/project-skill-setup-identity.test.ts": 100, + "server/src/__tests__/project-skill-sources.test.ts": 11685, + "server/src/__tests__/project-tools-pagination.test.ts": 100, + "server/src/__tests__/project-workspace-provider.integration.test.ts": 14272, + "server/src/__tests__/project-workspace-root-provenance.test.ts": 1085, + "server/src/__tests__/prompt-injection-gateway.test.ts": 2602, + "server/src/__tests__/prompt-injection-prevention.test.ts": 2711, + "server/src/__tests__/prompt-injection-provider.test.ts": 338, + "server/src/__tests__/provider-trace-retention-work.test.ts": 100, + "server/src/__tests__/provider-trace-store.test.ts": 100, + "server/src/__tests__/public-mcp-client-metadata.test.ts": 100, + "server/src/__tests__/public-mcp-dispatch-shutdown.test.ts": 100, + "server/src/__tests__/public-mcp.test.ts": 14056, + "server/src/__tests__/qa-routine-secrets-e2e.test.ts": 1030, + "server/src/__tests__/question-response-delivery.test.ts": 4675, + "server/src/__tests__/queued-comment-consumption.test.ts": 6170, + "server/src/__tests__/quota-windows-service.test.ts": 100, + "server/src/__tests__/quota-windows.test.ts": 100, + "server/src/__tests__/railway-connection.test.ts": 2657, + "server/src/__tests__/railway-ssh.test.ts": 1029, + "server/src/__tests__/railway.test.ts": 100, + "server/src/__tests__/recovery-classifiers.test.ts": 100, + "server/src/__tests__/recovery-cutover.test.ts": 550, + "server/src/__tests__/recovery-observability.test.ts": 1427, + "server/src/__tests__/recovery-stale-issue-lock-sweep.test.ts": 2501, + "server/src/__tests__/redact-sensitive.test.ts": 100, + "server/src/__tests__/remote-http-endpoint-guard.test.ts": 100, + "server/src/__tests__/remote-http-rebinding.test.ts": 918, + "server/src/__tests__/remote-mcp-connectors.test.ts": 3142, + "server/src/__tests__/remote-mcp-pending.test.ts": 100, + "server/src/__tests__/remote-mcp-protocol.test.ts": 100, + "server/src/__tests__/remote-mcp-redaction.test.ts": 100, + "server/src/__tests__/remote-pi-companion.test.ts": 1269, + "server/src/__tests__/remove-test-home.test.ts": 100, + "server/src/__tests__/repository-admission-tracing.test.ts": 3065, + "server/src/__tests__/repository-object-cache.test.ts": 1500, + "server/src/__tests__/repository-root-reader-queries.test.ts": 1268, + "server/src/__tests__/required-quality-review.test.ts": 2318, + "server/src/__tests__/rerun-survival.integration.test.ts": 100, + "server/src/__tests__/resource-lifecycle-events.test.ts": 2223, + "server/src/__tests__/retained-native-session-cleanup-work.test.ts": 10264, + "server/src/__tests__/reusable-leases-default.test.ts": 100, + "server/src/__tests__/routable-blocked.test.ts": 100, + "server/src/__tests__/routine-run-telemetry.test.ts": 728, + "server/src/__tests__/routine-schedule-work.test.ts": 13066, + "server/src/__tests__/routine-status-reconciliation.test.ts": 7567, + "server/src/__tests__/routines-service.test.ts": 10252, + "server/src/__tests__/run-authority-read-projections.test.ts": 802, + "server/src/__tests__/run-continuations.test.ts": 100, + "server/src/__tests__/run-evidence-migration-work.test.ts": 100, "server/src/__tests__/run-failure-sentry-real-sdk.test.ts": 792, - "server/src/__tests__/run-failure-sentry.test.ts": 7601, - "server/src/__tests__/run-identity.test.ts": 8747, - "server/src/__tests__/run-liveness.test.ts": 712, - "server/src/__tests__/run-secret-redaction.test.ts": 604, - "server/src/__tests__/run-trust-preset.test.ts": 8139, - "server/src/__tests__/runner-connection-eval-fixtures.test.ts": 11889, - "server/src/__tests__/runner-project-icon-contract.test.ts": 1030, - "server/src/__tests__/runtime-api.test.ts": 171, - "server/src/__tests__/runtime-skill-cache.test.ts": 684, - "server/src/__tests__/sandbox-orphan-cleanup-spool.test.ts": 213, - "server/src/__tests__/sandbox-provider-runtime.test.ts": 158, - "server/src/__tests__/secret-provider-registry.test.ts": 224, - "server/src/__tests__/secrets-service-dangling-user-secret.test.ts": 8869, - "server/src/__tests__/secrets-service-user-secret-owner-scoped.test.ts": 8535, - "server/src/__tests__/secrets-service.test.ts": 12971, - "server/src/__tests__/sentry-dsn.test.ts": 164, - "server/src/__tests__/sentry.test.ts": 8683, - "server/src/__tests__/server-info.test.ts": 258, - "server/src/__tests__/server-package-build-script.test.ts": 166, - "server/src/__tests__/server-package-otel-peer-metadata.test.ts": 166, - "server/src/__tests__/server-package-sentry-peer-metadata.test.ts": 144, - "server/src/__tests__/server-startup-feedback-export.test.ts": 2652, - "server/src/__tests__/service-read-disconnect-retry.test.ts": 4602, - "server/src/__tests__/service-version.test.ts": 169, - "server/src/__tests__/shared-telemetry-events.test.ts": 171, - "server/src/__tests__/skill-source-git-snapshot.test.ts": 3138, - "server/src/__tests__/skill-source-github-access.test.ts": 2463, - "server/src/__tests__/skill-source-scan-limit.test.ts": 176, - "server/src/__tests__/skill-sources.test.ts": 11245, - "server/src/__tests__/skills-catalog-service.test.ts": 220, - "server/src/__tests__/slack-conversation-lifecycle.test.ts": 12152, - "server/src/__tests__/smoke-lab.test.ts": 10616, - "server/src/__tests__/source-trust.test.ts": 8122, - "server/src/__tests__/speko-call-report.test.ts": 709, - "server/src/__tests__/speko-delivery-diagnostics.test.ts": 742, - "server/src/__tests__/speko-protocol.test.ts": 757, - "server/src/__tests__/speko-provider.test.ts": 815, - "server/src/__tests__/startup-refusals.test.ts": 190, - "server/src/__tests__/static-index-html.test.ts": 228, - "server/src/__tests__/static-ui-cache.test.ts": 158, - "server/src/__tests__/static-ui-compression.test.ts": 255, - "server/src/__tests__/status-card-update-engine.test.ts": 772, - "server/src/__tests__/status-cards.test.ts": 13788, - "server/src/__tests__/storage-local-provider.test.ts": 196, - "server/src/__tests__/storage-s3-multipart.test.ts": 245, - "server/src/__tests__/subscription-refresh.test.ts": 940, - "server/src/__tests__/subscriptions.test.ts": 10904, - "server/src/__tests__/summary-slots.test.ts": 11719, - "server/src/__tests__/task-search-quality.test.ts": 10050, - "server/src/__tests__/task-search-query.test.ts": 1159, - "server/src/__tests__/task-watchdogs-classifier.test.ts": 2281, - "server/src/__tests__/task-watchdogs-incident-fixture.test.ts": 169, - "server/src/__tests__/task-watchdogs-scheduler.test.ts": 11102, - "server/src/__tests__/teams-catalog-install-no-overrides.test.ts": 14095, - "server/src/__tests__/teams-catalog-service.test.ts": 782, - "server/src/__tests__/telemetry-client-flush.test.ts": 178, - "server/src/__tests__/tool-access-connector-telemetry.test.ts": 9714, - "server/src/__tests__/tool-access-policy-service.test.ts": 9912, - "server/src/__tests__/tool-access-service.test.ts": 51001, - "server/src/__tests__/tool-connection-removal.test.ts": 11665, - "server/src/__tests__/tool-connection-visibility.test.ts": 3084, - "server/src/__tests__/tool-content-guards.test.ts": 178, - "server/src/__tests__/tool-gateway-connector-telemetry.test.ts": 12506, - "server/src/__tests__/tool-gateway-discovery-http.test.ts": 10729, - "server/src/__tests__/tool-gateway-listing-memory.test.ts": 10922, - "server/src/__tests__/tool-gateway-service.test.ts": 16209, - "server/src/__tests__/tool-gateway.test.ts": 40912, - "server/src/__tests__/tool-grant-scope.test.ts": 10918, - "server/src/__tests__/tool-oauth-legacy-backfill.test.ts": 8636, - "server/src/__tests__/tool-review-queue-unsigned-request.test.ts": 9588, - "server/src/__tests__/trust-preset-resolver.test.ts": 745, - "server/src/__tests__/trust-proxy.test.ts": 214, - "server/src/__tests__/ui-branding.test.ts": 162, - "server/src/__tests__/version.test.ts": 186, - "server/src/__tests__/vite-html-renderer.test.ts": 157, - "server/src/__tests__/vitest-chat-shards.test.ts": 6218, - "server/src/__tests__/voice-session-store.integration.test.ts": 23337, - "server/src/__tests__/work-product-runtime-reconciliation.test.ts": 2358, - "server/src/__tests__/work-product-workspace-validation.test.ts": 9523, - "server/src/__tests__/work-products.test.ts": 2273, - "server/src/__tests__/work-scheduler.test.ts": 170, - "server/src/__tests__/work-timeline-service.test.ts": 11755, - "server/src/__tests__/workspace-git-inspection-cache.test.ts": 172, - "server/src/__tests__/workspace-instance-cleanup.test.ts": 182, - "server/src/__tests__/workspace-login-handoff-endpoint.test.ts": 1121, - "server/src/__tests__/workspace-login-handoff-issuer.test.ts": 663, - "server/src/__tests__/workspace-login-handoff.test.ts": 176, - "server/src/__tests__/workspace-operations-reconciliation.test.ts": 8533, - "server/src/__tests__/workspace-readiness.test.ts": 714, - "server/src/__tests__/workspace-reaper-cooldown-config.test.ts": 759, - "server/src/__tests__/workspace-runtime-control-recovery.test.ts": 8883, - "server/src/__tests__/workspace-runtime-exposure-reservation.test.ts": 11865, + "server/src/__tests__/run-failure-sentry.test.ts": 12040, + "server/src/__tests__/run-identity-projections.test.ts": 1584, + "server/src/__tests__/run-identity.test.ts": 2500, + "server/src/__tests__/run-liveness.test.ts": 100, + "server/src/__tests__/run-secret-redaction.test.ts": 100, + "server/src/__tests__/run-trust-preset.test.ts": 906, + "server/src/__tests__/runner-api-recovery.test.ts": 100, + "server/src/__tests__/runner-connection-eval-fixtures.test.ts": 6717, + "server/src/__tests__/runner-project-icon-contract.test.ts": 100, + "server/src/__tests__/runtime-api.test.ts": 100, + "server/src/__tests__/runtime-skill-cache.test.ts": 474, + "server/src/__tests__/sandbox-creation-operations.test.ts": 36500, + "server/src/__tests__/sandbox-orphan-cleanup-spool.test.ts": 100, + "server/src/__tests__/sandbox-provider-runtime.test.ts": 100, + "server/src/__tests__/sandbox-provider-schema-reads.test.ts": 934, + "server/src/__tests__/sandbox-snapshot-qualification.test.ts": 100, + "server/src/__tests__/satisfied-connection-intents.test.ts": 31907, + "server/src/__tests__/scheduled-work-postgres.test.ts": 3868, + "server/src/__tests__/scheduled-work.test.ts": 100, + "server/src/__tests__/scoped-deadline-work.test.ts": 268, + "server/src/__tests__/secret-provider-registry.test.ts": 100, + "server/src/__tests__/secrets-service-dangling-user-secret.test.ts": 574, + "server/src/__tests__/secrets-service-user-secret-owner-scoped.test.ts": 970, + "server/src/__tests__/secrets-service.test.ts": 4798, + "server/src/__tests__/sentry-dsn.test.ts": 100, + "server/src/__tests__/sentry-links.test.ts": 100, + "server/src/__tests__/sentry.test.ts": 11014, + "server/src/__tests__/server-info.test.ts": 132, + "server/src/__tests__/server-package-build-script.test.ts": 100, + "server/src/__tests__/server-package-otel-peer-metadata.test.ts": 100, + "server/src/__tests__/server-package-sentry-peer-metadata.test.ts": 100, + "server/src/__tests__/server-startup-feedback-export.test.ts": 1197, + "server/src/__tests__/service-read-disconnect-retry.test.ts": 1686, + "server/src/__tests__/service-version.test.ts": 100, + "server/src/__tests__/shared-telemetry-events.test.ts": 100, + "server/src/__tests__/sidebar-badges.test.ts": 2903, + "server/src/__tests__/skill-package-store.test.ts": 16364, + "server/src/__tests__/skill-repository-dependencies.test.ts": 221, + "server/src/__tests__/skill-repository-package.test.ts": 100, + "server/src/__tests__/skill-source-branches.test.ts": 100, + "server/src/__tests__/skill-source-git-snapshot.test.ts": 2921, + "server/src/__tests__/skill-source-github-access.test.ts": 100, + "server/src/__tests__/skill-source-retry.test.ts": 100, + "server/src/__tests__/skill-source-scan-limit.test.ts": 100, + "server/src/__tests__/skill-sources.test.ts": 15128, + "server/src/__tests__/skill-version-cache.test.ts": 5920, + "server/src/__tests__/skill-version-retention-config.test.ts": 100, + "server/src/__tests__/skill-version-retention-work.test.ts": 100, + "server/src/__tests__/skill-version-retention.test.ts": 24026, + "server/src/__tests__/skills-catalog-service.test.ts": 100, + "server/src/__tests__/slack-authorization-revision.test.ts": 6895, + "server/src/__tests__/slack-conversation-lifecycle.test.ts": 18914, + "server/src/__tests__/slack-development-manager.test.ts": 100, + "server/src/__tests__/slack-tool-preparation.test.ts": 2982, + "server/src/__tests__/smoke-lab.test.ts": 1021, + "server/src/__tests__/source-trust.test.ts": 861, + "server/src/__tests__/speko-call-report.test.ts": 100, + "server/src/__tests__/speko-delivery-diagnostics.test.ts": 100, + "server/src/__tests__/speko-protocol.test.ts": 100, + "server/src/__tests__/speko-provider.test.ts": 100, + "server/src/__tests__/startup-refusals.test.ts": 100, + "server/src/__tests__/static-index-html.test.ts": 100, + "server/src/__tests__/static-ui-cache.test.ts": 100, + "server/src/__tests__/static-ui-compression.test.ts": 100, + "server/src/__tests__/static-ui-idle.test.ts": 100, + "server/src/__tests__/status-card-update-engine.test.ts": 100, + "server/src/__tests__/status-cards.test.ts": 5119, + "server/src/__tests__/storage-local-provider.test.ts": 100, + "server/src/__tests__/storage-s3-failure-diagnostics.test.ts": 100, + "server/src/__tests__/storage-s3-multipart.test.ts": 100, + "server/src/__tests__/subscription-refresh.test.ts": 419, + "server/src/__tests__/subscriptions.test.ts": 3053, + "server/src/__tests__/summary-slots.test.ts": 2996, + "server/src/__tests__/task-admission-deadline.test.ts": 100, + "server/src/__tests__/task-cold-open.test.ts": 1073, + "server/src/__tests__/task-interruption-query.test.ts": 810, + "server/src/__tests__/task-list-projection-query.test.ts": 3172, + "server/src/__tests__/task-search-quality.test.ts": 1493, + "server/src/__tests__/task-search-query.test.ts": 100, + "server/src/__tests__/task-toast-eligibility.test.ts": 810, + "server/src/__tests__/task-watchdogs-classifier.test.ts": 100, + "server/src/__tests__/task-watchdogs-incident-fixture.test.ts": 100, + "server/src/__tests__/task-watchdogs-scheduler.test.ts": 4986, + "server/src/__tests__/task-workspace-admission.test.ts": 100, + "server/src/__tests__/task-workspace-api.test.ts": 5319, + "server/src/__tests__/task-workspace-binding.test.ts": 5327, + "server/src/__tests__/task-workspace-child-trust.test.ts": 2113, + "server/src/__tests__/task-workspace-repositories-persistence.test.ts": 2524, + "server/src/__tests__/task-workspace-repositories-trust.test.ts": 9775, + "server/src/__tests__/task-workspace-repositories.test.ts": 100, + "server/src/__tests__/task-workspace-source-copy-privacy.test.ts": 1294, + "server/src/__tests__/task-workspace-tools.test.ts": 100, + "server/src/__tests__/teams-catalog-install-no-overrides.test.ts": 7367, + "server/src/__tests__/teams-catalog-service.test.ts": 100, + "server/src/__tests__/telemetry-client-flush.test.ts": 100, + "server/src/__tests__/terminal-workspace-work.test.ts": 100, + "server/src/__tests__/tool-access-connector-telemetry.test.ts": 1382, + "server/src/__tests__/tool-access-oauth-protocol.integration.test.ts": 1532, + "server/src/__tests__/tool-access-policy-service.test.ts": 5970, + "server/src/__tests__/tool-access-service.test.ts": 47096, + "server/src/__tests__/tool-catalog-publication.test.ts": 2877, + "server/src/__tests__/tool-connection-health-work.test.ts": 46978, + "server/src/__tests__/tool-connection-installs.test.ts": 1663, + "server/src/__tests__/tool-connection-removal.test.ts": 3201, + "server/src/__tests__/tool-connection-visibility.test.ts": 100, + "server/src/__tests__/tool-content-guards.test.ts": 425, + "server/src/__tests__/tool-effective-assignments.test.ts": 14101, + "server/src/__tests__/tool-gateway-connector-telemetry.test.ts": 4900, + "server/src/__tests__/tool-gateway-discovery-http.test.ts": 2246, + "server/src/__tests__/tool-gateway-listing-memory.test.ts": 6963, + "server/src/__tests__/tool-gateway-service.test.ts": 71351, + "server/src/__tests__/tool-gateway.test.ts": 25272, + "server/src/__tests__/tool-grant-scope.test.ts": 1909, + "server/src/__tests__/tool-oauth-legacy-backfill.test.ts": 595, + "server/src/__tests__/tool-profile-entry-order.test.ts": 470, + "server/src/__tests__/tool-profile-entry-validation.test.ts": 2432, + "server/src/__tests__/tool-review-queue-unsigned-request.test.ts": 518, + "server/src/__tests__/trust-preset-resolver.test.ts": 100, + "server/src/__tests__/trust-proxy.test.ts": 100, + "server/src/__tests__/ui-branding.test.ts": 100, + "server/src/__tests__/unikraft-connection.test.ts": 3861, + "server/src/__tests__/unikraft-http-boundary.test.ts": 100, + "server/src/__tests__/version.test.ts": 100, + "server/src/__tests__/vite-html-renderer.test.ts": 100, + "server/src/__tests__/vitest-chat-shards.test.ts": 11283, + "server/src/__tests__/voice-call-reports.test.ts": 7238, + "server/src/__tests__/voice-inbound-cleanup.test.ts": 8132, + "server/src/__tests__/voice-inbound-retention.test.ts": 3060, + "server/src/__tests__/voice-recovery-work.test.ts": 16028, + "server/src/__tests__/voice-session-store.integration.test.ts": 40116, + "server/src/__tests__/work-product-runtime-reconciliation.test.ts": 100, + "server/src/__tests__/work-product-workspace-validation.test.ts": 619, + "server/src/__tests__/work-products.test.ts": 100, + "server/src/__tests__/work-scheduler.test.ts": 100, + "server/src/__tests__/work-timeline-service.test.ts": 1429, + "server/src/__tests__/workspace-authorization-query.test.ts": 2038, + "server/src/__tests__/workspace-compaction.test.ts": 645, + "server/src/__tests__/workspace-git-inspection-cache.test.ts": 100, + "server/src/__tests__/workspace-history-user-journey.test.ts": 3651, + "server/src/__tests__/workspace-host-provision.test.ts": 100, + "server/src/__tests__/workspace-instance-cleanup.test.ts": 100, + "server/src/__tests__/workspace-login-handoff-endpoint.test.ts": 138, + "server/src/__tests__/workspace-login-handoff-issuer.test.ts": 100, + "server/src/__tests__/workspace-login-handoff.test.ts": 100, + "server/src/__tests__/workspace-operations-reconciliation.test.ts": 618, + "server/src/__tests__/workspace-preferences.test.ts": 477, + "server/src/__tests__/workspace-preparation-history.test.ts": 6319, + "server/src/__tests__/workspace-readiness.test.ts": 100, + "server/src/__tests__/workspace-reaper-cooldown-config.test.ts": 100, + "server/src/__tests__/workspace-runtime-control-recovery.test.ts": 1437, + "server/src/__tests__/workspace-runtime-exposure-reservation.test.ts": 3431, "server/src/__tests__/workspace-runtime-https-live-exercise.test.ts": 2771, - "server/src/__tests__/workspace-runtime-leases.test.ts": 8734, - "server/src/__tests__/workspace-runtime-start-terminality.test.ts": 5643, - "server/src/__tests__/workspace-runtime.test.ts": 136113, - "server/src/__tests__/worktree-config.test.ts": 1283, - "server/src/__tests__/worktree-seed-server-spawn.test.ts": 3931, - "server/src/__tests__/write-build-stamp.test.ts": 227, - "server/src/__tests__/write-path-membership.test.ts": 1348, - "server/src/adapters/claude-agent-id-header.test.ts": 168, - "server/src/adapters/http/execute.test.ts": 740, - "server/src/adapters/plugin-loader.test.ts": 189, - "server/src/adapters/registry.test.ts": 1429, - "server/src/embedded-postgres-owner.test.ts": 162, - "server/src/http/server.test.ts": 684, - "server/src/instrumentation.test.ts": 176, - "server/src/lib/request-promise-memo.test.ts": 165, - "server/src/middleware/cloud-tenant-actor.test.ts": 1326, - "server/src/modules/active-run-watchdog/adapters/process.test.ts": 770, - "server/src/modules/active-run-watchdog/application/use-cases.test.ts": 177, - "server/src/modules/active-run-watchdog/domain/policy.test.ts": 178, - "server/src/modules/agent-lifecycle/__tests__/diagnostics.test.ts": 2361, - "server/src/modules/agent-lifecycle/__tests__/lifecycle.test.ts": 12810, - "server/src/modules/agent-lifecycle/__tests__/policy-sweep.test.ts": 9782, - "server/src/modules/run-dispatch/adapters/postgres.test.ts": 12179, - "server/src/modules/run-dispatch/application/use-cases.test.ts": 165, - "server/src/modules/run-dispatch/domain/policy.test.ts": 166, - "server/src/modules/run-dispatch/domain/wake-context.test.ts": 160, - "server/src/modules/wake-queue/adapters/postgres.test.ts": 11781, - "server/src/modules/wake-queue/adapters/queued-comment-postgres.test.ts": 7983, - "server/src/modules/wake-queue/application/queued-comment-use-cases.test.ts": 177, - "server/src/modules/wake-queue/application/use-cases.test.ts": 2659, - "server/src/modules/wake-queue/domain/context.test.ts": 2334, - "server/src/modules/wake-queue/domain/policy.test.ts": 197, - "server/src/realtime/runner-prp-outbound.test.ts": 184, - "server/src/realtime/runner-prp-ws.test.ts": 1006, - "server/src/routes/adapters.test.ts": 2025, - "server/src/routes/chat-channels.identity.test.ts": 4371, - "server/src/routes/chat-channels.webhook.test.ts": 3732, - "server/src/routes/connection-intents.test.ts": 2823, - "server/src/routes/setup-token-route.test.ts": 24121, - "server/src/routes/tool-access-connection-intent.test.ts": 3456, - "server/src/runtime-tools-token.test.ts": 158, - "server/src/services/__tests__/git-connection-failure.test.ts": 173, - "server/src/services/__tests__/mcp-connection-failure.test.ts": 177, - "server/src/services/__tests__/process-loss-diagnostics.test.ts": 162, - "server/src/services/__tests__/run-failure-diagnostics.test.ts": 845, - "server/src/services/__tests__/run-failure-report.test.ts": 10795, - "server/src/services/acknowledged-native-stop.test.ts": 624, - "server/src/services/adapter-execution-control.test.ts": 721, - "server/src/services/adapter-execution-phase.test.ts": 162, - "server/src/services/adapter-login-lease.test.ts": 166, - "server/src/services/adapter-models-env.test.ts": 162, - "server/src/services/adapter-registry-bootstrap.reconcile.test.ts": 1517, - "server/src/services/adapter-registry-bootstrap.test.ts": 1482, - "server/src/services/ai-auth-failure.test.ts": 765, - "server/src/services/ai-connection-configuration-failure.test.ts": 161, - "server/src/services/ai-connection-usage.test.ts": 841, - "server/src/services/anthropic-pricing.test.ts": 708, - "server/src/services/app-webhook.test.ts": 166, - "server/src/services/approved-execution-wait.test.ts": 156, - "server/src/services/attention-resolver-audience.test.ts": 2253, - "server/src/services/board-auth.test.ts": 622, - "server/src/services/chat-communication-guidance.test.ts": 728, - "server/src/services/chat-control-admission-retry.test.ts": 2203, - "server/src/services/chat-discord-adapter-patch.test.ts": 3907, - "server/src/services/chat-discord-command-registration-store.test.ts": 8644, - "server/src/services/chat-discord-command-registration.test.ts": 297, - "server/src/services/chat-discord-modal-wire.test.ts": 1892, - "server/src/services/chat-discord-native-command.test.ts": 1878, - "server/src/services/chat-discord-native-commands.test.ts": 484, - "server/src/services/chat-discord-native-interaction.test.ts": 2339, - "server/src/services/chat-discord-native-modal.test.ts": 1876, - "server/src/services/chat-discord-question-forms.test.ts": 246, - "server/src/services/chat-discord.test.ts": 491, - "server/src/services/chat-github-attachments.test.ts": 2802, - "server/src/services/chat-github-events.test.ts": 1170, - "server/src/services/chat-github-provider-stress.test.ts": 1679, - "server/src/services/chat-github-receipt-reactions.test.ts": 375, - "server/src/services/chat-github-review-policy.test.ts": 733, - "server/src/services/chat-github-webhook-config.test.ts": 280, - "server/src/services/chat-github-wizard-origin.test.ts": 2280, - "server/src/services/chat-inbound-wakeup-publications.test.ts": 617, - "server/src/services/chat-interaction-arbitration.test.ts": 6762, - "server/src/services/chat-interaction-publications.test.ts": 1265, - "server/src/services/chat-outbound-attachment-hydration.test.ts": 3916, - "server/src/services/chat-provider-inventory.test.ts": 190, - "server/src/services/chat-provider-lifecycle.test.ts": 186, - "server/src/services/chat-provider-links.test.ts": 162, - "server/src/services/chat-publication-batches.test.ts": 737, - "server/src/services/chat-publication-errors.test.ts": 189, - "server/src/services/chat-publication-projection.test.ts": 196, - "server/src/services/chat-publication-ready-stream.test.ts": 1939, - "server/src/services/chat-publication-reconciliation.test.ts": 180, - "server/src/services/chat-publication-stream.test.ts": 1022, - "server/src/services/chat-publication-text-parts.test.ts": 13871, - "server/src/services/chat-question-forms.test.ts": 867, - "server/src/services/chat-run-publications.test.ts": 2246, - "server/src/services/chat-sdk-runtime.adapters.test.ts": 2430, - "server/src/services/chat-sdk-runtime.test.ts": 2005, - "server/src/services/chat-sdk-state.test.ts": 177, - "server/src/services/chat-slack-native-modal.test.ts": 2161, - "server/src/services/chat-slack-receipts.test.ts": 4303, - "server/src/services/chat-slack-sessions.test.ts": 200, - "server/src/services/chat-task-url.test.ts": 2162, - "server/src/services/chat-teams-credentials.test.ts": 768, - "server/src/services/chat-teams-file-consent-runtime.test.ts": 2023, - "server/src/services/chat-teams-file-consent.contract.test.ts": 716, - "server/src/services/chat-teams-file-consent.test.ts": 443, - "server/src/services/chat-teams-file-publication.test.ts": 8569, - "server/src/services/chat-teams-file-transfers.test.ts": 11310, - "server/src/services/chat-teams-inline-image-intake.test.ts": 1811, - "server/src/services/chat-teams-inline-images.test.ts": 1953, - "server/src/services/chat-teams-native-modal.test.ts": 2384, - "server/src/services/chat-teams-personal-recipient.test.ts": 644, - "server/src/services/chat-telegram-draft-stop.test.ts": 301, - "server/src/services/chat-telegram-ephemeral-runtime.test.ts": 2037, - "server/src/services/chat-telegram-ephemeral.test.ts": 221, - "server/src/services/chat-telegram-media-intake.test.ts": 1853, - "server/src/services/chat-telegram-photo.test.ts": 262, - "server/src/services/chat-telegram-rich-intake.test.ts": 1731, - "server/src/services/chat-telegram-stop-subscription.test.ts": 161, - "server/src/services/chat-telegram-video-note.test.ts": 1730, - "server/src/services/cloud-runtime-identity.test.ts": 635, - "server/src/services/cloud-warm-standby.test.ts": 605, - "server/src/services/connector-telemetry.test.ts": 1207, - "server/src/services/connectors/slack-access.test.ts": 1147, - "server/src/services/connectors/slack-native-search.test.ts": 795, - "server/src/services/environment-custom-image-terminal-sessions.test.ts": 172, - "server/src/services/execution-allowlist.test.ts": 171, - "server/src/services/execution-continuation-provenance.test.ts": 8783, - "server/src/services/execution-continuation.test.ts": 8239, - "server/src/services/execution-control-reconciliation.test.ts": 14764, - "server/src/services/execution-policy-bootstrap.test.ts": 1492, - "server/src/services/execution-projection-read-retry.test.ts": 2712, - "server/src/services/execution-projection.test.ts": 739, - "server/src/services/execution-recovery-attempt.test.ts": 150, - "server/src/services/explicit-native-continuation.test.ts": 39692, - "server/src/services/fireflies-webhook.test.ts": 171, - "server/src/services/gmail-tool-governance.test.ts": 2503, - "server/src/services/google-chat-tool-policy.test.ts": 172, - "server/src/services/heartbeat-github-launchers.test.ts": 1082, - "server/src/services/heartbeat-run-runtime-status.test.ts": 184, - "server/src/services/heartbeat-runner-provider-config.test.ts": 1038, - "server/src/services/heartbeat-stop-metadata.test.ts": 785, - "server/src/services/heartbeat/queue.test.ts": 10333, - "server/src/services/heartbeat/recovery.test.ts": 10484, - "server/src/services/heartbeat/retries.test.ts": 10211, - "server/src/services/heartbeat/run-completion.test.ts": 14764, - "server/src/services/heartbeat/run-control.test.ts": 11572, - "server/src/services/heartbeat/run-lifecycle.test.ts": 12026, - "server/src/services/heartbeat/run-log.test.ts": 782, - "server/src/services/heartbeat/run-preparation.test.ts": 2755, - "server/src/services/heartbeat/run-state.test.ts": 10457, - "server/src/services/heartbeat/runtime-execution.test.ts": 13791, - "server/src/services/heartbeat/runtime-selection.test.ts": 13055, - "server/src/services/heartbeat/scheduling.test.ts": 12915, - "server/src/services/heartbeat/task-markdown.test.ts": 1253, - "server/src/services/heartbeat/workspace-preparation.test.ts": 14044, - "server/src/services/heartbeat/workspaces.test.ts": 3288, - "server/src/services/hot-restart.test.ts": 630, - "server/src/services/isolated-task-directory.test.ts": 987, - "server/src/services/issue-comment-wakeup.test.ts": 154, - "server/src/services/issue-dependency-wakeups.test.ts": 602, - "server/src/services/issue-prefix.test.ts": 851, - "server/src/services/issue-question-context.test.ts": 7590, - "server/src/services/issue-queued-comment-queue.test.ts": 158, - "server/src/services/issue-read-timing.test.ts": 180, - "server/src/services/issue-thread-interaction-resolution.test.ts": 741, - "server/src/services/issue-thread-interactions.test.ts": 6562, - "server/src/services/legacy-controller-lease.test.ts": 10206, - "server/src/services/legacy-execution-recovery.test.ts": 2262, - "server/src/services/local-ai-browser-login.test.ts": 1873, - "server/src/services/login-command.test.ts": 1515, - "server/src/services/managed-environments.test.ts": 2054, - "server/src/services/managed-resource-drift.test.ts": 165, - "server/src/services/mcp-tool-names.test.ts": 154, - "server/src/services/native-runtime/acpx-qualification.test.ts": 1152, - "server/src/services/native-runtime/agent-instruction-tools.integration.test.ts": 10966, - "server/src/services/native-runtime/agent-instruction-tools.test.ts": 2619, - "server/src/services/native-runtime/assigned-mcp-tools.test.ts": 2476, - "server/src/services/native-runtime/chat-attachment-read.test.ts": 10858, - "server/src/services/native-runtime/chat-attachment-reuse.test.ts": 9459, - "server/src/services/native-runtime/codex-model-fallback.test.ts": 784, - "server/src/services/native-runtime/codex-runtime-compatibility.test.ts": 154, - "server/src/services/native-runtime/completion-contracts.test.ts": 636, - "server/src/services/native-runtime/confirmation-comment-api.integration.test.ts": 13210, - "server/src/services/native-runtime/continuation-contract.test.ts": 8112, - "server/src/services/native-runtime/create-skill.integration.test.ts": 14421, - "server/src/services/native-runtime/current-wake-comments.test.ts": 9866, - "server/src/services/native-runtime/evidence-classifier.test.ts": 978, - "server/src/services/native-runtime/external-chat-wait.integration.test.ts": 58620, - "server/src/services/native-runtime/grok-runner-credentials.test.ts": 182, - "server/src/services/native-runtime/handoff-plan-context.test.ts": 8275, - "server/src/services/native-runtime/historical-task-questions.test.ts": 10211, - "server/src/services/native-runtime/managed-native-credentials.test.ts": 158, - "server/src/services/native-runtime/native-agent-runtime-inheritance.test.ts": 1083, - "server/src/services/native-runtime/native-cancellation-request.test.ts": 666, - "server/src/services/native-runtime/native-chat-workspace.test.ts": 10149, + "server/src/__tests__/workspace-runtime-leases.test.ts": 1854, + "server/src/__tests__/workspace-runtime-start-terminality.test.ts": 3515, + "server/src/__tests__/workspace-runtime.test.ts": 64915, + "server/src/__tests__/worktree-config.test.ts": 485, + "server/src/__tests__/worktree-seed-server-spawn.test.ts": 1395, + "server/src/__tests__/write-build-stamp.test.ts": 100, + "server/src/__tests__/write-path-membership.test.ts": 1665, + "server/src/adapters/claude-agent-id-header.test.ts": 100, + "server/src/adapters/cloud-external-adapters.test.ts": 2414, + "server/src/adapters/http/execute.test.ts": 100, + "server/src/adapters/plugin-loader.test.ts": 100, + "server/src/adapters/registry.test.ts": 100, + "server/src/embedded-postgres-owner.test.ts": 100, + "server/src/http/server.test.ts": 100, + "server/src/lib/request-promise-memo.test.ts": 100, + "server/src/lib/single-flight.test.ts": 100, + "server/src/middleware/cloud-tenant-actor.test.ts": 140, + "server/src/middleware/cloud-tenant-sync-postgres.test.ts": 8031, + "server/src/modules/active-run-watchdog/adapters/process.test.ts": 100, + "server/src/modules/active-run-watchdog/application/use-cases.test.ts": 100, + "server/src/modules/active-run-watchdog/domain/policy.test.ts": 100, + "server/src/modules/agent-lifecycle/__tests__/diagnostics.test.ts": 100, + "server/src/modules/agent-lifecycle/__tests__/driver-scope.test.ts": 100, + "server/src/modules/agent-lifecycle/__tests__/lifecycle.test.ts": 5205, + "server/src/modules/agent-lifecycle/__tests__/policy-sweep.test.ts": 788, + "server/src/modules/agent-lifecycle/__tests__/setup.test.ts": 1310, + "server/src/modules/agent-lifecycle/__tests__/verification-retry.test.ts": 1265, + "server/src/modules/run-dispatch/adapters/postgres.test.ts": 8480, + "server/src/modules/run-dispatch/application/use-cases.test.ts": 100, + "server/src/modules/run-dispatch/domain/policy.test.ts": 100, + "server/src/modules/run-dispatch/domain/wake-context.test.ts": 100, + "server/src/modules/wake-queue/adapters/operator-stop.test.ts": 100, + "server/src/modules/wake-queue/adapters/postgres.test.ts": 6467, + "server/src/modules/wake-queue/adapters/queued-comment-postgres.test.ts": 835, + "server/src/modules/wake-queue/adapters/queued-operator-stop-postgres.test.ts": 631, + "server/src/modules/wake-queue/adapters/release-run-read.test.ts": 610, + "server/src/modules/wake-queue/application/queued-comment-use-cases.test.ts": 100, + "server/src/modules/wake-queue/application/use-cases.test.ts": 100, + "server/src/modules/wake-queue/domain/context.test.ts": 100, + "server/src/modules/wake-queue/domain/policy.test.ts": 100, + "server/src/realtime/live-event-delivery.cost.test.ts": 100, + "server/src/realtime/live-event-delivery.test.ts": 100, + "server/src/realtime/runner-prp-outbound.test.ts": 100, + "server/src/realtime/runner-prp-ws.test.ts": 100, + "server/src/routes/adapters.test.ts": 100, + "server/src/routes/chat-channels.identity.test.ts": 147, + "server/src/routes/chat-channels.webhook.test.ts": 100, + "server/src/routes/connection-intents.test.ts": 100, + "server/src/routes/setup-token-route.test.ts": 4207, + "server/src/routes/tool-access-connection-intent.test.ts": 100, + "server/src/routes/voice-sessions.auth.test.ts": 100, + "server/src/runtime-tools-token.test.ts": 100, + "server/src/services/__tests__/git-connection-failure.test.ts": 100, + "server/src/services/__tests__/mcp-connection-failure.test.ts": 100, + "server/src/services/__tests__/process-loss-diagnostics.test.ts": 100, + "server/src/services/__tests__/run-failure-diagnostics.test.ts": 100, + "server/src/services/__tests__/run-failure-report.test.ts": 6038, + "server/src/services/__tests__/sentry-run-links.test.ts": 547, + "server/src/services/acknowledged-native-stop.test.ts": 100, + "server/src/services/adapter-execution-control.test.ts": 100, + "server/src/services/adapter-execution-phase.test.ts": 100, + "server/src/services/adapter-login-lease.test.ts": 100, + "server/src/services/adapter-models-env.test.ts": 100, + "server/src/services/adapter-registry-bootstrap.reconcile.test.ts": 100, + "server/src/services/adapter-registry-bootstrap.test.ts": 100, + "server/src/services/adapter-settings-local.test.ts": 100, + "server/src/services/adapter-settings-postgres.test.ts": 871, + "server/src/services/agent-environment-test-status.test.ts": 100, + "server/src/services/agent-runner-selection.test.ts": 103, + "server/src/services/ai-auth-failure.test.ts": 100, + "server/src/services/ai-connection-configuration-failure.test.ts": 100, + "server/src/services/ai-connection-usage.test.ts": 100, + "server/src/services/anthropic-pricing.test.ts": 100, + "server/src/services/app-webhook.test.ts": 100, + "server/src/services/approved-execution-wait.test.ts": 100, + "server/src/services/attention-resolver-audience.test.ts": 100, + "server/src/services/board-auth.test.ts": 3550, + "server/src/services/budget-live-usage.test.ts": 100, + "server/src/services/budget-tracing.test.ts": 100, + "server/src/services/bundled-skill-release-validation.test.ts": 812, + "server/src/services/case-agent-read.test.ts": 133, + "server/src/services/case-deletion.test.ts": 1010, + "server/src/services/case-handoff-privacy.test.ts": 1071, + "server/src/services/case-handoffs.test.ts": 23032, + "server/src/services/case-task-documents.test.ts": 875, + "server/src/services/chat-channels/authorization-run-read.test.ts": 677, + "server/src/services/chat-channels/github/retry.test.ts": 100, + "server/src/services/chat-channels/github/review-heading.test.ts": 100, + "server/src/services/chat-channels/github/review-instructions.test.ts": 100, + "server/src/services/chat-channels/github/webhook-ingress.test.ts": 2193, + "server/src/services/chat-channels/inbound/wakeup-authorization-error.test.ts": 100, + "server/src/services/chat-channels/run-origin.test.ts": 1074, + "server/src/services/chat-channels/slack/adapter.test.ts": 100, + "server/src/services/chat-channels/slack/cards.test.ts": 100, + "server/src/services/chat-channels/slack/question-forms.test.ts": 100, + "server/src/services/chat-channels/slack/receipt-timing.test.ts": 100, + "server/src/services/chat-channels/slack/signing-secret-preflight.test.ts": 100, + "server/src/services/chat-communication-guidance.test.ts": 100, + "server/src/services/chat-control-admission-retry.test.ts": 100, + "server/src/services/chat-discord-adapter-patch.test.ts": 3402, + "server/src/services/chat-discord-command-registration-store.test.ts": 4433, + "server/src/services/chat-discord-command-registration.test.ts": 105, + "server/src/services/chat-discord-modal-wire.test.ts": 100, + "server/src/services/chat-discord-native-command.test.ts": 179, + "server/src/services/chat-discord-native-commands.test.ts": 100, + "server/src/services/chat-discord-native-interaction.test.ts": 126, + "server/src/services/chat-discord-native-modal.test.ts": 100, + "server/src/services/chat-discord-question-forms.test.ts": 100, + "server/src/services/chat-discord.test.ts": 100, + "server/src/services/chat-github-attachments.test.ts": 553, + "server/src/services/chat-github-events.test.ts": 100, + "server/src/services/chat-github-management.test.ts": 100, + "server/src/services/chat-github-provider-stress.test.ts": 100, + "server/src/services/chat-github-receipt-reactions.test.ts": 100, + "server/src/services/chat-github-review-policy.test.ts": 100, + "server/src/services/chat-github-review-repair.test.ts": 1221, + "server/src/services/chat-github-tools.test.ts": 584, + "server/src/services/chat-github-webhook-config.test.ts": 100, + "server/src/services/chat-github-wizard-origin.test.ts": 100, + "server/src/services/chat-inbound-wakeup-publications.test.ts": 100, + "server/src/services/chat-interaction-arbitration.test.ts": 485, + "server/src/services/chat-interaction-continuation.test.ts": 2391, + "server/src/services/chat-interaction-forms.test.ts": 100, + "server/src/services/chat-interaction-publications.test.ts": 100, + "server/src/services/chat-monitor-continuation.test.ts": 4687, + "server/src/services/chat-outbound-attachment-hydration.test.ts": 100, + "server/src/services/chat-provider-inventory.test.ts": 100, + "server/src/services/chat-provider-lifecycle.test.ts": 100, + "server/src/services/chat-provider-links.test.ts": 100, + "server/src/services/chat-publication-batches.test.ts": 100, + "server/src/services/chat-publication-errors.test.ts": 100, + "server/src/services/chat-publication-projection.test.ts": 100, + "server/src/services/chat-publication-ready-stream.test.ts": 260, + "server/src/services/chat-publication-reconciliation.test.ts": 100, + "server/src/services/chat-publication-stream.test.ts": 937, + "server/src/services/chat-publication-text-parts.test.ts": 15611, + "server/src/services/chat-question-forms.test.ts": 100, + "server/src/services/chat-reply-lineage-context.test.ts": 3305, + "server/src/services/chat-run-publications.test.ts": 100, + "server/src/services/chat-sdk-runtime.adapters.test.ts": 517, + "server/src/services/chat-sdk-runtime.test.ts": 643, + "server/src/services/chat-sdk-state.test.ts": 100, + "server/src/services/chat-slack-native-modal.test.ts": 155, + "server/src/services/chat-slack-receipts.test.ts": 4147, + "server/src/services/chat-slack-sessions.test.ts": 100, + "server/src/services/chat-task-url.test.ts": 100, + "server/src/services/chat-teams-credentials.test.ts": 100, + "server/src/services/chat-teams-file-consent-runtime.test.ts": 169, + "server/src/services/chat-teams-file-consent.contract.test.ts": 102, + "server/src/services/chat-teams-file-consent.test.ts": 100, + "server/src/services/chat-teams-file-publication.test.ts": 1674, + "server/src/services/chat-teams-file-transfers.test.ts": 13945, + "server/src/services/chat-teams-inline-image-intake.test.ts": 100, + "server/src/services/chat-teams-inline-images.test.ts": 149, + "server/src/services/chat-teams-native-modal.test.ts": 100, + "server/src/services/chat-teams-personal-recipient.test.ts": 100, + "server/src/services/chat-telegram-draft-stop.test.ts": 100, + "server/src/services/chat-telegram-ephemeral-runtime.test.ts": 100, + "server/src/services/chat-telegram-ephemeral.test.ts": 100, + "server/src/services/chat-telegram-media-intake.test.ts": 100, + "server/src/services/chat-telegram-photo.test.ts": 100, + "server/src/services/chat-telegram-rich-intake.test.ts": 100, + "server/src/services/chat-telegram-stop-subscription.test.ts": 100, + "server/src/services/chat-telegram-video-note.test.ts": 100, + "server/src/services/cloud-runtime-identity.test.ts": 100, + "server/src/services/cloud-warm-standby.test.ts": 100, + "server/src/services/connector-runtime.test.ts": 100, + "server/src/services/connector-telemetry.test.ts": 100, + "server/src/services/connectors/onepassword/onepassword.test.ts": 100, + "server/src/services/connectors/onepassword/worker.test.ts": 100, + "server/src/services/connectors/slack-access.test.ts": 100, + "server/src/services/connectors/slack-authority.test.ts": 100, + "server/src/services/connectors/slack-endpoint-preparation.test.ts": 109, + "server/src/services/connectors/slack-native-search.test.ts": 100, + "server/src/services/connectors/supabase/index.test.ts": 105, + "server/src/services/connectors/vercel/index.test.ts": 100, + "server/src/services/conversation-continuation.test.ts": 636, + "server/src/services/environment-custom-image-terminal-sessions.test.ts": 100, + "server/src/services/execution-allowlist.test.ts": 100, + "server/src/services/execution-blocker-read.test.ts": 2520, + "server/src/services/execution-continuation-history-read.test.ts": 1081, + "server/src/services/execution-continuation-provenance.test.ts": 852, + "server/src/services/execution-continuation.test.ts": 999, + "server/src/services/execution-control-reconciliation.test.ts": 1586, + "server/src/services/execution-policy-bootstrap.test.ts": 100, + "server/src/services/execution-preparation-timing.test.ts": 100, + "server/src/services/execution-projection-external-provider.test.ts": 763, + "server/src/services/execution-projection-read-retry.test.ts": 1538, + "server/src/services/execution-projection-recovery.test.ts": 423, + "server/src/services/execution-projection.test.ts": 100, + "server/src/services/execution-recovery-attempt.test.ts": 100, + "server/src/services/execution-timing-context.test.ts": 100, + "server/src/services/execution-transaction-timing.test.ts": 534, + "server/src/services/explicit-native-continuation.test.ts": 36842, + "server/src/services/fireflies-webhook.test.ts": 100, + "server/src/services/github-connection-events.test.ts": 100, + "server/src/services/gmail-archive-tool-policy.test.ts": 100, + "server/src/services/gmail-tool-governance.test.ts": 100, + "server/src/services/google-chat-tool-policy.test.ts": 100, + "server/src/services/grok-bot/webhook.test.ts": 100, + "server/src/services/heartbeat-github-launchers.test.ts": 379, + "server/src/services/heartbeat-run-runtime-status.test.ts": 100, + "server/src/services/heartbeat-runner-provider-config.test.ts": 100, + "server/src/services/heartbeat-stop-metadata.test.ts": 100, + "server/src/services/heartbeat/agent-status.test.ts": 10426, + "server/src/services/heartbeat/budget-cancellation.test.ts": 12674, + "server/src/services/heartbeat/environment-selection.test.ts": 11272, + "server/src/services/heartbeat/external-provider.test.ts": 100, + "server/src/services/heartbeat/instruction-working-copy.test.ts": 100, + "server/src/services/heartbeat/native-recovery-deadlines.test.ts": 100, + "server/src/services/heartbeat/native-stop-evidence-read.test.ts": 7181, + "server/src/services/heartbeat/preparation-timing-context.test.ts": 100, + "server/src/services/heartbeat/preparation-timing.test.ts": 100, + "server/src/services/heartbeat/process-settlement.test.ts": 213, + "server/src/services/heartbeat/queue.test.ts": 15095, + "server/src/services/heartbeat/recovery.test.ts": 5568, + "server/src/services/heartbeat/retries.test.ts": 974, + "server/src/services/heartbeat/run-comment-selection.test.ts": 539, + "server/src/services/heartbeat/run-completion.test.ts": 18962, + "server/src/services/heartbeat/run-control-reads.test.ts": 6868, + "server/src/services/heartbeat/run-control.test.ts": 30191, + "server/src/services/heartbeat/run-lifecycle.test.ts": 12966, + "server/src/services/heartbeat/run-log.test.ts": 100, + "server/src/services/heartbeat/run-output.test.ts": 100, + "server/src/services/heartbeat/run-preparation.review-path.test.ts": 100, + "server/src/services/heartbeat/run-preparation.test.ts": 100, + "server/src/services/heartbeat/run-state.test.ts": 1598, + "server/src/services/heartbeat/runtime-execution.test.ts": 11940, + "server/src/services/heartbeat/runtime-selection.test.ts": 13373, + "server/src/services/heartbeat/scheduling.test.ts": 48182, + "server/src/services/heartbeat/system-notice-response.test.ts": 100, + "server/src/services/heartbeat/task-markdown.test.ts": 100, + "server/src/services/heartbeat/upgrade-readiness.test.ts": 2897, + "server/src/services/heartbeat/workspace-preparation.test.ts": 101696, + "server/src/services/heartbeat/workspaces.test.ts": 483, + "server/src/services/hot-restart.test.ts": 100, + "server/src/services/isolated-remote-workspace-source-lock.test.ts": 3261, + "server/src/services/isolated-remote-workspace-source.test.ts": 100, + "server/src/services/isolated-task-directory.test.ts": 232, + "server/src/services/issue-comment-wakeup.test.ts": 100, + "server/src/services/issue-dependency-wakeups.test.ts": 100, + "server/src/services/issue-prefix.test.ts": 228, + "server/src/services/issue-question-context.test.ts": 404, + "server/src/services/issue-queued-comment-queue.test.ts": 100, + "server/src/services/issue-read-timing.test.ts": 100, + "server/src/services/issue-thread-interaction-resolution.test.ts": 100, + "server/src/services/issue-thread-interactions.test.ts": 10290, + "server/src/services/legacy-controller-lease.test.ts": 7177, + "server/src/services/legacy-execution-recovery.test.ts": 100, + "server/src/services/legacy-explicit-continuation-recovery.test.ts": 1584, + "server/src/services/local-ai-browser-login.test.ts": 2499, + "server/src/services/login-command.test.ts": 100, + "server/src/services/managed-environments.test.ts": 100, + "server/src/services/managed-resource-drift.test.ts": 100, + "server/src/services/mcp-tool-names.test.ts": 100, + "server/src/services/muse-maintenance.test.ts": 9644, + "server/src/services/native-runtime/acpx-qualification.test.ts": 100, + "server/src/services/native-runtime/agent-instruction-tools.integration.test.ts": 3047, + "server/src/services/native-runtime/agent-instruction-tools.test.ts": 100, + "server/src/services/native-runtime/antigravity-candidate-admission.test.ts": 100, + "server/src/services/native-runtime/antigravity-credential-recovery.test.ts": 100, + "server/src/services/native-runtime/antigravity-runner-credentials.test.ts": 567, + "server/src/services/native-runtime/assigned-mcp-tools.test.ts": 445, + "server/src/services/native-runtime/case-context-projection.test.ts": 100, + "server/src/services/native-runtime/chat-attachment-read.test.ts": 3027, + "server/src/services/native-runtime/chat-attachment-reuse.test.ts": 1877, + "server/src/services/native-runtime/codex-model-fallback.test.ts": 100, + "server/src/services/native-runtime/codex-runtime-compatibility.test.ts": 100, + "server/src/services/native-runtime/completion-contracts.test.ts": 100, + "server/src/services/native-runtime/confirmation-comment-api.integration.test.ts": 2778, + "server/src/services/native-runtime/continuation-contract.test.ts": 563, + "server/src/services/native-runtime/create-skill.integration.test.ts": 3777, + "server/src/services/native-runtime/current-wake-comments.test.ts": 754, + "server/src/services/native-runtime/evidence-classifier.test.ts": 100, + "server/src/services/native-runtime/external-chat-wait.integration.test.ts": 120964, + "server/src/services/native-runtime/grok-runner-credentials.test.ts": 100, + "server/src/services/native-runtime/handoff-plan-context.test.ts": 712, + "server/src/services/native-runtime/historical-task-questions.test.ts": 2428, + "server/src/services/native-runtime/managed-native-credentials.test.ts": 100, + "server/src/services/native-runtime/muse-native-follow-up.test.ts": 1149, + "server/src/services/native-runtime/muse-native-visibility.test.ts": 4861, + "server/src/services/native-runtime/native-agent-runtime-inheritance.test.ts": 100, + "server/src/services/native-runtime/native-artifact-preparation.test.ts": 173, + "server/src/services/native-runtime/native-cancellation-request.test.ts": 100, + "server/src/services/native-runtime/native-case-handoff.test.ts": 924, + "server/src/services/native-runtime/native-case-tools.integration.test.ts": 17474, + "server/src/services/native-runtime/native-chat-workspace.test.ts": 496, "server/src/services/native-runtime/native-codex-runner.integration.test.ts": 224015, - "server/src/services/native-runtime/native-codex-runner.test.ts": 2297, - "server/src/services/native-runtime/native-completion-feedback.test.ts": 10473, - "server/src/services/native-runtime/native-completion-format-feedback.test.ts": 1987, - "server/src/services/native-runtime/native-continuation.test.ts": 776, - "server/src/services/native-runtime/native-deliverable-feedback.test.ts": 598, - "server/src/services/native-runtime/native-execution-input.test.ts": 1659, - "server/src/services/native-runtime/native-github-access.test.ts": 2623, - "server/src/services/native-runtime/native-harness-backup-cleanup.test.ts": 167, - "server/src/services/native-runtime/native-maintenance-no-launch.test.ts": 599, - "server/src/services/native-runtime/native-permission-decline.test.ts": 986, - "server/src/services/native-runtime/native-permission-projection.test.ts": 2223, - "server/src/services/native-runtime/native-plan-wait-receipts.test.ts": 161, - "server/src/services/native-runtime/native-plan-wait.test.ts": 2710, - "server/src/services/native-runtime/native-provider-capacity.integration.test.ts": 8906, - "server/src/services/native-runtime/native-provider-failure.integration.test.ts": 10743, - "server/src/services/native-runtime/native-provider-failure.test.ts": 177, - "server/src/services/native-runtime/native-provider-overload.integration.test.ts": 10700, - "server/src/services/native-runtime/native-question-bridge.test.ts": 30037, - "server/src/services/native-runtime/native-replacement-evidence.test.ts": 169, - "server/src/services/native-runtime/native-restart-recovery.test.ts": 1541, - "server/src/services/native-runtime/native-review-participant.test.ts": 1139, - "server/src/services/native-runtime/native-review-prompt.test.ts": 152, - "server/src/services/native-runtime/native-run-finalizer-telemetry.test.ts": 12067, - "server/src/services/native-runtime/native-run-trace.test.ts": 187, - "server/src/services/native-runtime/native-runner-attachment-staging.test.ts": 2326, - "server/src/services/native-runtime/native-runner-file-handoff.test.ts": 11450, - "server/src/services/native-runtime/native-runner-ownership.test.ts": 607, + "server/src/services/native-runtime/native-codex-runner.test.ts": 100, + "server/src/services/native-runtime/native-completion-feedback.test.ts": 2337, + "server/src/services/native-runtime/native-completion-format-feedback.test.ts": 100, + "server/src/services/native-runtime/native-confirmation-replay.test.ts": 3479, + "server/src/services/native-runtime/native-continuation.test.ts": 100, + "server/src/services/native-runtime/native-controller-recovery.integration.test.ts": 1083, + "server/src/services/native-runtime/native-controller-recovery.test.ts": 100, + "server/src/services/native-runtime/native-credential-updates.test.ts": 628, + "server/src/services/native-runtime/native-deliverable-feedback.test.ts": 100, + "server/src/services/native-runtime/native-deliverable-text.test.ts": 100, + "server/src/services/native-runtime/native-execution-input.test.ts": 100, + "server/src/services/native-runtime/native-execution-lease.test.ts": 100, + "server/src/services/native-runtime/native-failed-workspace-recovery.test.ts": 14571, + "server/src/services/native-runtime/native-finalization-reconciler-timing.test.ts": 100, + "server/src/services/native-runtime/native-finalization-recovery-state.test.ts": 733, + "server/src/services/native-runtime/native-github-access.test.ts": 10722, + "server/src/services/native-runtime/native-harness-backup-cleanup.test.ts": 100, + "server/src/services/native-runtime/native-maintenance-no-launch.test.ts": 100, + "server/src/services/native-runtime/native-permission-decline.test.ts": 100, + "server/src/services/native-runtime/native-permission-projection.test.ts": 100, + "server/src/services/native-runtime/native-plan-wait-receipts.test.ts": 100, + "server/src/services/native-runtime/native-plan-wait.test.ts": 1378, + "server/src/services/native-runtime/native-plugin-workspace-finalization.test.ts": 100, + "server/src/services/native-runtime/native-preserved-monitor-wait.test.ts": 17047, + "server/src/services/native-runtime/native-provider-capacity.integration.test.ts": 698, + "server/src/services/native-runtime/native-provider-failure.integration.test.ts": 2983, + "server/src/services/native-runtime/native-provider-failure.test.ts": 100, + "server/src/services/native-runtime/native-provider-overload.integration.test.ts": 2058, + "server/src/services/native-runtime/native-question-bridge.test.ts": 26511, + "server/src/services/native-runtime/native-reconciliation-timing.test.ts": 100, + "server/src/services/native-runtime/native-recovery-usage-delivery.test.ts": 100, + "server/src/services/native-runtime/native-replacement-evidence.test.ts": 100, + "server/src/services/native-runtime/native-restart-recovery.test.ts": 100, + "server/src/services/native-runtime/native-review-participant.test.ts": 100, + "server/src/services/native-runtime/native-review-prompt.test.ts": 100, + "server/src/services/native-runtime/native-routine-status.test.ts": 1203, + "server/src/services/native-runtime/native-run-finalizer-telemetry.test.ts": 4409, + "server/src/services/native-runtime/native-run-trace-context.test.ts": 100, + "server/src/services/native-runtime/native-run-trace.test.ts": 100, + "server/src/services/native-runtime/native-runner-attachment-staging.test.ts": 100, + "server/src/services/native-runtime/native-runner-execution-driver.test.ts": 100, + "server/src/services/native-runtime/native-runner-file-handoff.test.ts": 3556, + "server/src/services/native-runtime/native-runner-ownership.test.ts": 100, "server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts": 7940, - "server/src/services/native-runtime/native-safe-replacement.test.ts": 28876, - "server/src/services/native-runtime/native-session-executor.test.ts": 127716, - "server/src/services/native-runtime/native-session-handoff.test.ts": 8274, - "server/src/services/native-runtime/native-session-resume.test.ts": 9887, - "server/src/services/native-runtime/native-ssh-command-runner.test.ts": 1142, - "server/src/services/native-runtime/native-tool-trace.test.ts": 581, + "server/src/services/native-runtime/native-safe-replacement.test.ts": 25642, + "server/src/services/native-runtime/native-session-executor.test.ts": 66527, + "server/src/services/native-runtime/native-session-handoff.test.ts": 555, + "server/src/services/native-runtime/native-session-resume-projection.test.ts": 882, + "server/src/services/native-runtime/native-session-resume.test.ts": 580, + "server/src/services/native-runtime/native-session-resumption-binding.test.ts": 7377, + "server/src/services/native-runtime/native-skill-test-scope.test.ts": 2466, + "server/src/services/native-runtime/native-skill-test-status.test.ts": 960, + "server/src/services/native-runtime/native-ssh-command-runner.test.ts": 262, + "server/src/services/native-runtime/native-staged-attachment-recovery.test.ts": 404, + "server/src/services/native-runtime/native-stop-accounting.integration.test.ts": 1133, + "server/src/services/native-runtime/native-task-session-publication.test.ts": 1956, + "server/src/services/native-runtime/native-terminal-status-projection.test.ts": 1925, + "server/src/services/native-runtime/native-tool-trace.test.ts": 100, + "server/src/services/native-runtime/native-usage-accounting.test.ts": 100, + "server/src/services/native-runtime/native-warm-capability.test.ts": 100, "server/src/services/native-runtime/native-warm-transition-restart.integration.test.ts": 2492, - "server/src/services/native-runtime/native-workspace-best-effort.test.ts": 1161, + "server/src/services/native-runtime/native-workspace-best-effort.test.ts": 100, "server/src/services/native-runtime/native-workspace-export-resume.live.test.ts": 2865, - "server/src/services/native-runtime/native-workspace-export-retry.test.ts": 12329, - "server/src/services/native-runtime/native-workspace-finalizer-failure.test.ts": 2318, - "server/src/services/native-runtime/native-workspace-finalizer.test.ts": 9543, - "server/src/services/native-runtime/paperclip-control-plane-port.test.ts": 20670, + "server/src/services/native-runtime/native-workspace-export-retry.test.ts": 15190, + "server/src/services/native-runtime/native-workspace-finalizer-failure.test.ts": 100, + "server/src/services/native-runtime/native-workspace-finalizer.test.ts": 10343, + "server/src/services/native-runtime/native-workspace-lock-keepalive.test.ts": 100, + "server/src/services/native-runtime/native-workspace-platform-proof.test.ts": 100, + "server/src/services/native-runtime/native-workspace-reconstruction.test.ts": 34841, + "server/src/services/native-runtime/paperclip-control-plane-port.test.ts": 13900, + "server/src/services/native-runtime/paperclip-control-plane-timing.test.ts": 100, "server/src/services/native-runtime/paperclip-runner-real-server.integration.test.ts": 2492, - "server/src/services/native-runtime/paperclip-runner-tool-authority.test.ts": 10054, - "server/src/services/native-runtime/remote-deliverable-file.test.ts": 735, + "server/src/services/native-runtime/paperclip-runner-tool-authority.test.ts": 4819, + "server/src/services/native-runtime/plugin-runner-credentials.test.ts": 100, + "server/src/services/native-runtime/plugin-runner-task.test.ts": 126, + "server/src/services/native-runtime/provider-pack-archive.test.ts": 19384, + "server/src/services/native-runtime/remote-deliverable-file.test.ts": 782, + "server/src/services/native-runtime/remote-launch-assets.test.ts": 565, + "server/src/services/native-runtime/remote-provider-pack-authority.test.ts": 100, + "server/src/services/native-runtime/remote-provider-pack-verification.test.ts": 100, "server/src/services/native-runtime/remote-provider-pack.test.ts": 158, - "server/src/services/native-runtime/runner-api-files.test.ts": 168, - "server/src/services/native-runtime/runner-api-response.test.ts": 411, - "server/src/services/native-runtime/runner-api-rollout.test.ts": 151, - "server/src/services/native-runtime/runner-api.integration.test.ts": 27868, - "server/src/services/native-runtime/runner-api.test.ts": 1949, - "server/src/services/native-runtime/runner-bridge-tools.test.ts": 2461, - "server/src/services/native-runtime/runner-prp-coordinator.test.ts": 9624, - "server/src/services/native-runtime/runner-task-attachments.test.ts": 10554, - "server/src/services/native-runtime/runtime-context.test.ts": 2186, - "server/src/services/native-runtime/runtime-mode.test.ts": 1075, - "server/src/services/native-runtime/runtime-request-resolution-authority.test.ts": 629, - "server/src/services/native-runtime/status-arbiter.test.ts": 177, - "server/src/services/native-runtime/stopped-codex-turn.test.ts": 178, - "server/src/services/native-runtime/task-creation-backlog.test.ts": 10405, - "server/src/services/native-runtime/task-monitor.integration.test.ts": 10207, - "server/src/services/native-runtime/task-reassignment.test.ts": 10500, - "server/src/services/native-runtime/update-skill.integration.test.ts": 12293, - "server/src/services/onboarding-first-task-assets.test.ts": 741, - "server/src/services/onboarding-greeting.test.ts": 741, - "server/src/services/openrouter-models.test.ts": 165, - "server/src/services/paperclip-cloud-connector-enrollment.test.ts": 1301, - "server/src/services/paperclip-cloud-connector.test.ts": 1220, - "server/src/services/provider-trace-workspace-diff-reprojection.test.ts": 1049, - "server/src/services/question-interaction-answers.test.ts": 2315, - "server/src/services/question-pattern-validation.test.ts": 2294, - "server/src/services/recovery/legacy-continuation.test.ts": 165, - "server/src/services/recovery/native-runner-routing.test.ts": 2198, - "server/src/services/recovery/provider-failure-classification.test.ts": 2382, - "server/src/services/recovery/review-path-recovery.test.ts": 2334, - "server/src/services/recovery/service.pause-durability.test.ts": 2435, - "server/src/services/recovery/status-only-context.test.ts": 181, - "server/src/services/recovery/stranded-notice.test.ts": 1190, - "server/src/services/recovery/successful-run-handoff.test.ts": 1122, - "server/src/services/remote-agent-profiles.test.ts": 608, - "server/src/services/remote-url-credentials.test.ts": 167, - "server/src/services/responsible-user-denial-run-outcomes.test.ts": 1228, - "server/src/services/routines-formatter-cache.test.ts": 3628, - "server/src/services/run-cancellation.test.ts": 158, - "server/src/services/run-log-store-cancellation.test.ts": 456, - "server/src/services/run-log-store.test.ts": 867, - "server/src/services/run-scratch.test.ts": 174, - "server/src/services/runner-goal-control-broker.test.ts": 157, - "server/src/services/runner-goals.test.ts": 8268, - "server/src/services/runtime-exposure/broker-client.test.ts": 316, - "server/src/services/runtime-exposure/exposure-manager.test.ts": 699, - "server/src/services/runtime-exposure/loopback-listener.test.ts": 181, - "server/src/services/runtime-exposure/port-pair.test.ts": 727, - "server/src/services/runtime-exposure/port-reservation.test.ts": 688, - "server/src/services/runtime-exposure/tailscale-hostname.test.ts": 153, - "server/src/services/safe-native-chat-progress.test.ts": 166, - "server/src/services/session-workspace-cwd.test.ts": 160, - "server/src/services/setup-token-reaper.test.ts": 2025, - "server/src/services/setup-token-session.test.ts": 8660, - "server/src/services/setup-token-transport-binding.test.ts": 2151, - "server/src/services/skill-tools.test.ts": 760, - "server/src/services/slack-mcp-error.test.ts": 191, - "server/src/services/tool-discovery-scheduler.test.ts": 164, - "server/src/services/tool-profile-binding-precedence.test.ts": 167, - "server/src/services/vercel-connect.test.ts": 211, - "server/src/services/workspace-git-operation-scheduler.test.ts": 1686, - "server/src/services/workspace-git-snapshot-streaming.test.ts": 116534, - "server/src/services/workspace-restore-reconciliation.test.ts": 8823, - "server/src/services/workspace-runtime-exposure-backfill.test.ts": 2178, - "server/src/services/workspace-runtime-exposure.test.ts": 14607, - "server/src/services/workspace-runtime-read-model.test.ts": 1136, - "server/src/services/workspace-runtime-ready-comment.test.ts": 2250, - "server/src/shutdown.test.ts": 311, - "server/src/url-utils.test.ts": 166 + "server/src/services/native-runtime/remote-runner-recovery-diagnostics.test.ts": 182, + "server/src/services/native-runtime/remote-runner-recovery-probe.test.ts": 100, + "server/src/services/native-runtime/remote-warm-session-recovery.test.ts": 100, + "server/src/services/native-runtime/runner-api-files.test.ts": 100, + "server/src/services/native-runtime/runner-api-log-redaction.test.ts": 100, + "server/src/services/native-runtime/runner-api-response.test.ts": 263, + "server/src/services/native-runtime/runner-api-rollout.test.ts": 100, + "server/src/services/native-runtime/runner-api.integration.test.ts": 27316, + "server/src/services/native-runtime/runner-api.test.ts": 821, + "server/src/services/native-runtime/runner-bridge-tools.test.ts": 100, + "server/src/services/native-runtime/runner-operation-timing.test.ts": 100, + "server/src/services/native-runtime/runner-prp-coordinator.test.ts": 1490, + "server/src/services/native-runtime/runner-task-attachments.test.ts": 1967, + "server/src/services/native-runtime/runner-tool-results.test.ts": 186, + "server/src/services/native-runtime/runtime-asset-retention.test.ts": 100, + "server/src/services/native-runtime/runtime-context.test.ts": 521, + "server/src/services/native-runtime/runtime-mode.test.ts": 100, + "server/src/services/native-runtime/runtime-request-resolution-authority.test.ts": 100, + "server/src/services/native-runtime/setup-readiness.test.ts": 2559, + "server/src/services/native-runtime/status-arbiter.test.ts": 100, + "server/src/services/native-runtime/stopped-codex-turn.test.ts": 100, + "server/src/services/native-runtime/task-creation-backlog.test.ts": 1610, + "server/src/services/native-runtime/task-monitor.integration.test.ts": 3539, + "server/src/services/native-runtime/task-reassignment.test.ts": 2123, + "server/src/services/native-runtime/tool-receipt-recovery.integration.test.ts": 998, + "server/src/services/native-runtime/update-skill.integration.test.ts": 5004, + "server/src/services/object-byte-journal.test.ts": 785, + "server/src/services/object-run-log-store.test.ts": 1165, + "server/src/services/onboarding-first-task-assets.test.ts": 100, + "server/src/services/onboarding-greeting.test.ts": 100, + "server/src/services/openrouter-models.test.ts": 100, + "server/src/services/paperclip-cloud-connector-enrollment.test.ts": 118, + "server/src/services/paperclip-cloud-connector.test.ts": 100, + "server/src/services/plugin-task-panels.test.ts": 100, + "server/src/services/presentation-ownership-read.test.ts": 4186, + "server/src/services/provider-fresh-workspace.test.ts": 100, + "server/src/services/provider-trace-workspace-diff-reprojection.test.ts": 100, + "server/src/services/public-mcp/events-work.test.ts": 19772, + "server/src/services/question-interaction-answers.test.ts": 2579, + "server/src/services/question-pattern-validation.test.ts": 2122, + "server/src/services/recovery/issue-state-read.test.ts": 794, + "server/src/services/recovery/legacy-continuation.test.ts": 100, + "server/src/services/recovery/legacy-disposition-read.test.ts": 563, + "server/src/services/recovery/native-runner-routing.test.ts": 100, + "server/src/services/recovery/provider-failure-classification.test.ts": 100, + "server/src/services/recovery/reconciliation-run-read.test.ts": 897, + "server/src/services/recovery/review-path-recovery.test.ts": 100, + "server/src/services/recovery/run-context-read.test.ts": 517, + "server/src/services/recovery/run-result-read.test.ts": 659, + "server/src/services/recovery/service.pause-durability.test.ts": 100, + "server/src/services/recovery/status-only-context.test.ts": 100, + "server/src/services/recovery/stranded-notice.test.ts": 100, + "server/src/services/recovery/successful-run-handoff.test.ts": 100, + "server/src/services/remote-agent-profiles.test.ts": 100, + "server/src/services/remote-url-credentials.test.ts": 100, + "server/src/services/responsible-user-denial-run-outcomes.test.ts": 100, + "server/src/services/retained-google-credential-cleanup.test.ts": 113, + "server/src/services/routines-formatter-cache.test.ts": 679, + "server/src/services/run-cancellation.test.ts": 100, + "server/src/services/run-cost-accounting.test.ts": 6448, + "server/src/services/run-log-store-cancellation.test.ts": 243, + "server/src/services/run-log-store.test.ts": 703, + "server/src/services/run-scratch.test.ts": 100, + "server/src/services/runner-environment-lifecycle.test.ts": 100, + "server/src/services/runner-goal-control-broker.test.ts": 100, + "server/src/services/runner-goals.test.ts": 1362, + "server/src/services/runtime-exposure/broker-client.test.ts": 170, + "server/src/services/runtime-exposure/exposure-manager.test.ts": 100, + "server/src/services/runtime-exposure/loopback-listener.test.ts": 100, + "server/src/services/runtime-exposure/port-pair.test.ts": 100, + "server/src/services/runtime-exposure/port-reservation.test.ts": 100, + "server/src/services/runtime-exposure/tailscale-hostname.test.ts": 100, + "server/src/services/safe-native-chat-progress.test.ts": 100, + "server/src/services/sandbox-absence-proof.test.ts": 100, + "server/src/services/session-workspace-cwd.test.ts": 100, + "server/src/services/setup-token-reaper.test.ts": 100, + "server/src/services/setup-token-session.test.ts": 1590, + "server/src/services/setup-token-transport-binding.test.ts": 277, + "server/src/services/skill-local-import-boundary.test.ts": 100, + "server/src/services/skill-package-recovery.test.ts": 1075, + "server/src/services/skill-package-work.test.ts": 7741, + "server/src/services/skill-source-sync-work.test.ts": 13591, + "server/src/services/skill-tools.test.ts": 100, + "server/src/services/slack-mcp-error.test.ts": 100, + "server/src/services/system-recovery-state.test.ts": 100, + "server/src/services/task-interruption-presentation.test.ts": 100, + "server/src/services/task-retry.test.ts": 1716, + "server/src/services/tool-discovery-scheduler.test.ts": 100, + "server/src/services/tool-profile-binding-precedence.test.ts": 100, + "server/src/services/usage-receipts-read.test.ts": 842, + "server/src/services/vercel-connect.test.ts": 100, + "server/src/services/workspace-git-operation-scheduler.test.ts": 1556, + "server/src/services/workspace-git-snapshot-streaming.test.ts": 28543, + "server/src/services/workspace-recovery-source.test.ts": 1467, + "server/src/services/workspace-recovery.test.ts": 259120, + "server/src/services/workspace-restore-reconciliation.test.ts": 1020, + "server/src/services/workspace-runtime-exposure-backfill.test.ts": 100, + "server/src/services/workspace-runtime-exposure.test.ts": 10645, + "server/src/services/workspace-runtime-read-model.test.ts": 100, + "server/src/services/workspace-runtime-ready-comment.test.ts": 100, + "server/src/shutdown.test.ts": 183, + "server/src/startup-recovery-timing.test.ts": 100, + "server/src/storage/config.test.ts": 100, + "server/src/url-utils.test.ts": 100 } } diff --git a/scripts/serialized-shard-durations.json b/scripts/serialized-shard-durations.json index 59edf33891c..ef10d1e26f0 100644 --- a/scripts/serialized-shard-durations.json +++ b/scripts/serialized-shard-durations.json @@ -1,150 +1,188 @@ { - "$comment": "Per-suite wall-clock durations (ms) for serialized route/authz verification, used by scripts/run-vitest-stable.mjs in PR and release shards. Refreshed from successful Cloud readiness run 34705914878 (2026-09-12), jobs 103585803684, 103585803688, 103585803716, 103585803734, and 103585803754. Each weight spans consecutive [test:run] suite-start timestamps; the last suite ends at the first post-job cleanup timestamp. This includes each isolated Vitest process startup, import, collection, tests, and shutdown. All 145 suites are sampled exactly once. New or renamed suites use the median weight. Refresh from a successful complete run when observed shard times drift.", + "$comment": "Generated by node scripts/ci/refresh-test-durations.mjs from successful protected-master Cloud readiness runs. Median per-file cost includes tests, hooks, collection, setup and environment preparation when the timing reporter is present; older logs bootstrap test/hook durations only. Missing files retain their prior weight or the partitioner's median fallback. Timing changes only partition work; they never authorize skipping a test.", "unit": "ms", + "sources": [ + { + "run": 41202111422, + "attempt": 1, + "sha": "c2971badde04b602fa9fe698015e1f3653b39e6b", + "url": "https://github.com/paperclipai/paperclip/actions/runs/41202111422" + }, + { + "run": 41201965500, + "attempt": 1, + "sha": "d5766e40748ac5881c4b9879fad1d5a61d9b493d", + "url": "https://github.com/paperclipai/paperclip/actions/runs/41201965500" + } + ], + "coverage": { + "measured": 163, + "withSetup": 0, + "inventory": 163 + }, "durations": { - "server/src/__tests__/access-routes-hidden-floor.test.ts": 11187, - "server/src/__tests__/access-routes-permissions-upgrade.test.ts": 18919, - "server/src/__tests__/activity-routes.test.ts": 5963, - "server/src/__tests__/adapter-auth-signal-routes.test.ts": 14105, - "server/src/__tests__/adapter-model-refresh-routes.test.ts": 10257, - "server/src/__tests__/adapter-routes-authz.test.ts": 7991, - "server/src/__tests__/adapter-routes.test.ts": 8580, - "server/src/__tests__/agent-action-audit-routes.test.ts": 17869, - "server/src/__tests__/agent-adapter-validation-routes.test.ts": 24906, - "server/src/__tests__/agent-cross-tenant-authz-routes.test.ts": 9297, - "server/src/__tests__/agent-device-login-routes.test.ts": 25216, - "server/src/__tests__/agent-hire-idempotency-routes.test.ts": 16903, - "server/src/__tests__/agent-instructions-routes.test.ts": 15221, - "server/src/__tests__/agent-live-run-routes.test.ts": 30747, - "server/src/__tests__/agent-permissions-routes.test.ts": 9849, - "server/src/__tests__/agent-secrets-routes.test.ts": 16629, - "server/src/__tests__/agent-skills-routes.test.ts": 24106, - "server/src/__tests__/agent-test-environment-routes.test.ts": 20121, - "server/src/__tests__/approval-routes-idempotency.test.ts": 6366, - "server/src/__tests__/artifact-review-document-routes.test.ts": 11415, - "server/src/__tests__/assets.test.ts": 7738, - "server/src/__tests__/auth-routes.test.ts": 3471, - "server/src/__tests__/auth-session-route.test.ts": 4282, - "server/src/__tests__/authz-company-access.test.ts": 3348, - "server/src/__tests__/authz-existence-oracle-guard.test.ts": 312, - "server/src/__tests__/authz-secret-context.test.ts": 3899, - "server/src/__tests__/board-chat-route-feature-flag.test.ts": 1251, - "server/src/__tests__/bootstrap-claim-routes.test.ts": 5821, - "server/src/__tests__/built-in-agent-routes.test.ts": 6607, - "server/src/__tests__/cases-routes.test.ts": 17725, - "server/src/__tests__/cli-auth-routes.test.ts": 8422, - "server/src/__tests__/cloud-routes.test.ts": 3475, - "server/src/__tests__/companies-route-cross-company-authz.test.ts": 10608, - "server/src/__tests__/companies-route-path-guard.test.ts": 3520, - "server/src/__tests__/company-branding-route.test.ts": 6319, - "server/src/__tests__/company-import-transfer-routes.test.ts": 10852, - "server/src/__tests__/company-portability-routes.test.ts": 5037, - "server/src/__tests__/company-portability.test.ts": 6030, - "server/src/__tests__/company-search-extract-routes.test.ts": 11382, - "server/src/__tests__/company-search-rate-limit-routes.test.ts": 11592, - "server/src/__tests__/company-skill-policy-routes.test.ts": 9969, - "server/src/__tests__/company-skills-import-authz-routes.test.ts": 16725, - "server/src/__tests__/company-skills-routes.test.ts": 6666, - "server/src/__tests__/company-user-directory-route.test.ts": 5254, - "server/src/__tests__/costs-service.test.ts": 10178, - "server/src/__tests__/decision-queues-routes.test.ts": 9909, - "server/src/__tests__/document-annotation-routes.test.ts": 12372, - "server/src/__tests__/environment-custom-image-routes.test.ts": 8712, - "server/src/__tests__/environment-instance-routes.test.ts": 8738, - "server/src/__tests__/environment-routes.test.ts": 5462, - "server/src/__tests__/environment-selection-route-guards.test.ts": 9749, - "server/src/__tests__/execution-workspace-runtime-lease-route.test.ts": 12231, - "server/src/__tests__/execution-workspaces-routes.test.ts": 6598, - "server/src/__tests__/express5-auth-wildcard.test.ts": 1098, - "server/src/__tests__/external-object-routes.test.ts": 14559, - "server/src/__tests__/folders-routes.test.ts": 3122, - "server/src/__tests__/health-dev-server-token.test.ts": 3808, - "server/src/__tests__/health.test.ts": 3664, - "server/src/__tests__/heartbeat-dependency-scheduling.test.ts": 22771, - "server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts": 26411, - "server/src/__tests__/heartbeat-process-recovery.test.ts": 165058, - "server/src/__tests__/inbox-agent-policy-routes.test.ts": 17418, - "server/src/__tests__/inbox-archive-routes.test.ts": 18276, - "server/src/__tests__/instance-database-backups-routes.test.ts": 3602, - "server/src/__tests__/instance-settings-routes.test.ts": 3263, - "server/src/__tests__/invite-accept-existing-member.test.ts": 5373, - "server/src/__tests__/invite-accept-gateway-defaults.test.ts": 16661, - "server/src/__tests__/invite-accept-replay.test.ts": 11438, - "server/src/__tests__/invite-create-route.test.ts": 5108, - "server/src/__tests__/invite-expiry.test.ts": 11055, - "server/src/__tests__/invite-join-manager.test.ts": 10519, - "server/src/__tests__/invite-list-route.test.ts": 10911, - "server/src/__tests__/invite-logo-route.test.ts": 10811, - "server/src/__tests__/invite-onboarding-text.test.ts": 9920, - "server/src/__tests__/invite-rate-limit-route.test.ts": 11305, - "server/src/__tests__/invite-summary-route.test.ts": 12300, - "server/src/__tests__/invite-test-resolution-route.test.ts": 10497, - "server/src/__tests__/invite-url-public-base-url.test.ts": 5781, - "server/src/__tests__/issue-activity-events-routes.test.ts": 13914, - "server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts": 50945, - "server/src/__tests__/issue-assigned-backlog-contract-routes.test.ts": 9454, - "server/src/__tests__/issue-assignee-invokability-routes.test.ts": 9340, - "server/src/__tests__/issue-attachment-routes.test.ts": 18688, - "server/src/__tests__/issue-blocker-diagnostics-routes.test.ts": 17348, - "server/src/__tests__/issue-closed-workspace-routes.test.ts": 9378, - "server/src/__tests__/issue-comment-attribution-audit-routes.test.ts": 16687, - "server/src/__tests__/issue-comment-cancel-routes.test.ts": 9120, - "server/src/__tests__/issue-comment-reopen-routes.test.ts": 9744, - "server/src/__tests__/issue-create-deduplication-routes.test.ts": 18131, - "server/src/__tests__/issue-created-from-routes.test.ts": 18051, - "server/src/__tests__/issue-dependency-wakeups-routes.test.ts": 12754, - "server/src/__tests__/issue-document-restore-routes.test.ts": 11341, - "server/src/__tests__/issue-execution-policy-routes.test.ts": 17384, - "server/src/__tests__/issue-feedback-routes.test.ts": 10562, - "server/src/__tests__/issue-identifier-routes.test.ts": 17386, - "server/src/__tests__/issue-list-assignee-filter-routes.test.ts": 18762, - "server/src/__tests__/issue-list-updatedsince-filter-routes.test.ts": 17534, - "server/src/__tests__/issue-onboarding-first-task-routes.test.ts": 19600, - "server/src/__tests__/issue-queued-comments-routes.test.ts": 28267, - "server/src/__tests__/issue-scheduled-retry-routes.test.ts": 17617, - "server/src/__tests__/issue-stale-execution-lock-routes.test.ts": 17166, - "server/src/__tests__/issue-stalled-review-decision-routes.test.ts": 18751, - "server/src/__tests__/issue-subtree-diagnostics-routes.test.ts": 18263, - "server/src/__tests__/issue-telemetry-routes.test.ts": 9904, - "server/src/__tests__/issue-thread-interaction-routes.test.ts": 39373, - "server/src/__tests__/issue-tree-control-routes.test.ts": 3616, - "server/src/__tests__/issue-update-comment-wakeup-routes.test.ts": 15540, - "server/src/__tests__/issue-wake-diagnostics-routes.test.ts": 17422, - "server/src/__tests__/issue-watchdogs-routes.test.ts": 22439, - "server/src/__tests__/issue-workspace-command-authz.test.ts": 9358, - "server/src/__tests__/issues-checkout-wakeup.test.ts": 1064, - "server/src/__tests__/issues-goal-context-routes.test.ts": 9431, - "server/src/__tests__/issues-service.test.ts": 76576, - "server/src/__tests__/llms-routes.test.ts": 3399, - "server/src/__tests__/low-trust-red-team-routes.test.ts": 27945, - "server/src/__tests__/managed-agent-profile-routes-authz.test.ts": 3624, - "server/src/__tests__/multilingual-issues-routes.test.ts": 17623, - "server/src/__tests__/onboarding-seed-route.test.ts": 13936, - "server/src/__tests__/openapi-routes.test.ts": 4089, - "server/src/__tests__/openclaw-invite-prompt-route.test.ts": 5131, - "server/src/__tests__/opencode-local-adapter-environment.test.ts": 8676, - "server/src/__tests__/permissions-upgrade-boundary-routes.test.ts": 16917, - "server/src/__tests__/plugin-install-route-security.test.ts": 13924, - "server/src/__tests__/plugin-routes-authz.test.ts": 5693, - "server/src/__tests__/plugin-scoped-api-routes.test.ts": 6245, - "server/src/__tests__/project-goal-telemetry-routes.test.ts": 6735, - "server/src/__tests__/project-routes-env.test.ts": 5412, - "server/src/__tests__/project-workspace-managed-sandbox-routes.test.ts": 6840, - "server/src/__tests__/projects-list-archived-routes.test.ts": 17390, - "server/src/__tests__/redaction.test.ts": 1125, - "server/src/__tests__/resource-memberships-routes.test.ts": 17692, - "server/src/__tests__/routine-document-annotation-routes.test.ts": 3749, - "server/src/__tests__/routines-e2e.test.ts": 20181, - "server/src/__tests__/routines-routes.test.ts": 6778, - "server/src/__tests__/secret-proposals-routes.test.ts": 21824, - "server/src/__tests__/secrets-routes-claude-oauth-service.test.ts": 11110, - "server/src/__tests__/secrets-routes.test.ts": 10410, - "server/src/__tests__/sidebar-preferences-routes.test.ts": 4495, - "server/src/__tests__/summary-slot-routes.test.ts": 6451, - "server/src/__tests__/teams-catalog-routes.test.ts": 4948, - "server/src/__tests__/user-profile-routes.test.ts": 9198, - "server/src/__tests__/workspace-command-authz.test.ts": 1044, - "server/src/__tests__/workspace-runtime-routes-authz.test.ts": 6915, - "server/src/__tests__/workspace-runtime-service-authz.test.ts": 10428 + "server/src/__tests__/access-routes-hidden-floor.test.ts": 4202, + "server/src/__tests__/access-routes-permissions-upgrade.test.ts": 3661, + "server/src/__tests__/activity-routes.test.ts": 15018, + "server/src/__tests__/adapter-auth-signal-routes.test.ts": 14386, + "server/src/__tests__/adapter-model-refresh-routes.test.ts": 7225, + "server/src/__tests__/adapter-routes-authz.test.ts": 13466, + "server/src/__tests__/adapter-routes.test.ts": 3359, + "server/src/__tests__/agent-action-audit-routes.test.ts": 4488, + "server/src/__tests__/agent-adapter-validation-routes.test.ts": 77234, + "server/src/__tests__/agent-cross-tenant-authz-routes.test.ts": 4565, + "server/src/__tests__/agent-device-login-routes.test.ts": 4861, + "server/src/__tests__/agent-hire-auth-inheritance-routes.test.ts": 3045, + "server/src/__tests__/agent-hire-idempotency-routes.test.ts": 1271, + "server/src/__tests__/agent-instructions-routes.test.ts": 3836, + "server/src/__tests__/agent-live-run-routes.test.ts": 69136, + "server/src/__tests__/agent-permissions-routes.test.ts": 3946, + "server/src/__tests__/agent-runner-qualification-routes.test.ts": 1009, + "server/src/__tests__/agent-secrets-routes.test.ts": 1104, + "server/src/__tests__/agent-skills-routes.test.ts": 4859, + "server/src/__tests__/agent-test-environment-routes.test.ts": 6760, + "server/src/__tests__/ai-connection-router.test.ts": 19925, + "server/src/__tests__/announcements-routes.test.ts": 626, + "server/src/__tests__/approval-routes-idempotency.test.ts": 3352, + "server/src/__tests__/artifact-review-document-routes.test.ts": 7798, + "server/src/__tests__/assets.test.ts": 8543, + "server/src/__tests__/auth-routes.test.ts": 100, + "server/src/__tests__/auth-session-route.test.ts": 100, + "server/src/__tests__/authz-company-access.test.ts": 100, + "server/src/__tests__/authz-existence-oracle-guard.test.ts": 100, + "server/src/__tests__/authz-secret-context.test.ts": 100, + "server/src/__tests__/board-chat-route-feature-flag.test.ts": 895, + "server/src/__tests__/bootstrap-claim-routes.test.ts": 100, + "server/src/__tests__/browser-use-route-scope.test.ts": 136, + "server/src/__tests__/built-in-agent-routes.test.ts": 6740, + "server/src/__tests__/cases-routes.test.ts": 4496, + "server/src/__tests__/cli-auth-routes.test.ts": 17331, + "server/src/__tests__/cloud-routes.test.ts": 100, + "server/src/__tests__/companies-route-cross-company-authz.test.ts": 11340, + "server/src/__tests__/companies-route-path-guard.test.ts": 100, + "server/src/__tests__/company-branding-route.test.ts": 4804, + "server/src/__tests__/company-import-transfer-routes.test.ts": 1428, + "server/src/__tests__/company-portability-routes.test.ts": 2672, + "server/src/__tests__/company-portability.test.ts": 1315, + "server/src/__tests__/company-search-extract-routes.test.ts": 115, + "server/src/__tests__/company-search-rate-limit-routes.test.ts": 100, + "server/src/__tests__/company-skill-policy-routes.test.ts": 626, + "server/src/__tests__/company-skills-import-authz-routes.test.ts": 1524, + "server/src/__tests__/company-skills-routes.test.ts": 3409, + "server/src/__tests__/company-user-directory-route.test.ts": 100, + "server/src/__tests__/cost-data-quality-routes.test.ts": 100, + "server/src/__tests__/costs-service.test.ts": 1070, + "server/src/__tests__/decision-queues-routes.test.ts": 2092, + "server/src/__tests__/document-annotation-routes.test.ts": 8418, + "server/src/__tests__/email-routes-provider-errors.test.ts": 100, + "server/src/__tests__/environment-custom-image-routes.test.ts": 100, + "server/src/__tests__/environment-instance-routes.test.ts": 100, + "server/src/__tests__/environment-routes.test.ts": 198, + "server/src/__tests__/environment-selection-route-guards.test.ts": 119, + "server/src/__tests__/execution-workspace-runtime-lease-route.test.ts": 2694, + "server/src/__tests__/execution-workspaces-routes.test.ts": 100, + "server/src/__tests__/express5-auth-wildcard.test.ts": 100, + "server/src/__tests__/external-object-routes.test.ts": 4096, + "server/src/__tests__/folders-routes.test.ts": 1358, + "server/src/__tests__/health-dev-server-token.test.ts": 104, + "server/src/__tests__/health.test.ts": 100, + "server/src/__tests__/heartbeat-dependency-scheduling.test.ts": 27885, + "server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts": 47816, + "server/src/__tests__/heartbeat-process-recovery.test.ts": 324638, + "server/src/__tests__/heartbeat-run-privacy-routes.test.ts": 4967, + "server/src/__tests__/inbox-agent-policy-routes.test.ts": 831, + "server/src/__tests__/inbox-archive-routes.test.ts": 2794, + "server/src/__tests__/instance-database-backups-routes.test.ts": 100, + "server/src/__tests__/instance-settings-routes.test.ts": 3161, + "server/src/__tests__/invite-accept-existing-member.test.ts": 100, + "server/src/__tests__/invite-accept-gateway-defaults.test.ts": 584, + "server/src/__tests__/invite-accept-replay.test.ts": 100, + "server/src/__tests__/invite-create-route.test.ts": 7648, + "server/src/__tests__/invite-expiry.test.ts": 100, + "server/src/__tests__/invite-join-manager.test.ts": 100, + "server/src/__tests__/invite-list-route.test.ts": 457, + "server/src/__tests__/invite-logo-route.test.ts": 100, + "server/src/__tests__/invite-onboarding-text.test.ts": 100, + "server/src/__tests__/invite-rate-limit-route.test.ts": 5390, + "server/src/__tests__/invite-summary-route.test.ts": 6262, + "server/src/__tests__/invite-test-resolution-route.test.ts": 4135, + "server/src/__tests__/invite-url-public-base-url.test.ts": 4075, + "server/src/__tests__/issue-access-grants-routes.test.ts": 4910, + "server/src/__tests__/issue-activity-events-routes.test.ts": 9271, + "server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts": 9524, + "server/src/__tests__/issue-assigned-backlog-contract-routes.test.ts": 4067, + "server/src/__tests__/issue-assignee-invokability-routes.test.ts": 125, + "server/src/__tests__/issue-attachment-routes.test.ts": 4226, + "server/src/__tests__/issue-blocker-diagnostics-routes.test.ts": 1268, + "server/src/__tests__/issue-closed-workspace-routes.test.ts": 4032, + "server/src/__tests__/issue-comment-attribution-audit-routes.test.ts": 4466, + "server/src/__tests__/issue-comment-cancel-routes.test.ts": 3762, + "server/src/__tests__/issue-comment-reopen-routes.test.ts": 501, + "server/src/__tests__/issue-create-deduplication-routes.test.ts": 1646, + "server/src/__tests__/issue-created-from-routes.test.ts": 828, + "server/src/__tests__/issue-creation-source-routes.test.ts": 1836, + "server/src/__tests__/issue-dependency-wakeups-routes.test.ts": 8947, + "server/src/__tests__/issue-document-restore-routes.test.ts": 7787, + "server/src/__tests__/issue-execution-policy-routes.test.ts": 3678, + "server/src/__tests__/issue-feedback-routes.test.ts": 3786, + "server/src/__tests__/issue-identifier-routes.test.ts": 622, + "server/src/__tests__/issue-list-assignee-filter-routes.test.ts": 6773, + "server/src/__tests__/issue-list-updatedsince-filter-routes.test.ts": 772, + "server/src/__tests__/issue-onboarding-first-task-routes.test.ts": 6124, + "server/src/__tests__/issue-queued-comments-routes.test.ts": 47964, + "server/src/__tests__/issue-scheduled-retry-routes.test.ts": 7804, + "server/src/__tests__/issue-stale-execution-lock-routes.test.ts": 2222, + "server/src/__tests__/issue-stalled-review-decision-routes.test.ts": 2218, + "server/src/__tests__/issue-status-version-route-service.test.ts": 881, + "server/src/__tests__/issue-subtree-diagnostics-routes.test.ts": 1299, + "server/src/__tests__/issue-summaries-routes.test.ts": 832, + "server/src/__tests__/issue-telemetry-routes.test.ts": 4919, + "server/src/__tests__/issue-thread-interaction-routes.test.ts": 14647, + "server/src/__tests__/issue-tree-control-routes.test.ts": 1004, + "server/src/__tests__/issue-update-comment-wakeup-routes.test.ts": 4204, + "server/src/__tests__/issue-wake-diagnostics-routes.test.ts": 1493, + "server/src/__tests__/issue-watchdogs-routes.test.ts": 6085, + "server/src/__tests__/issue-workspace-command-authz.test.ts": 4361, + "server/src/__tests__/issues-checkout-wakeup.test.ts": 100, + "server/src/__tests__/issues-goal-context-routes.test.ts": 100, + "server/src/__tests__/issues-service.test.ts": 14248, + "server/src/__tests__/llms-routes.test.ts": 1700, + "server/src/__tests__/low-trust-red-team-routes.test.ts": 21637, + "server/src/__tests__/managed-agent-profile-routes-authz.test.ts": 100, + "server/src/__tests__/multilingual-issues-routes.test.ts": 990, + "server/src/__tests__/muse-qualification-routes.test.ts": 100, + "server/src/__tests__/onboarding-seed-route.test.ts": 2184, + "server/src/__tests__/openapi-routes.test.ts": 808, + "server/src/__tests__/openclaw-invite-prompt-route.test.ts": 100, + "server/src/__tests__/opencode-local-adapter-environment.test.ts": 6051, + "server/src/__tests__/openrouter-attribution.test.ts": 100, + "server/src/__tests__/permissions-upgrade-boundary-routes.test.ts": 1292, + "server/src/__tests__/plugin-install-route-security.test.ts": 3548, + "server/src/__tests__/plugin-routes-authz.test.ts": 3421, + "server/src/__tests__/plugin-scoped-api-routes.test.ts": 2549, + "server/src/__tests__/project-goal-telemetry-routes.test.ts": 3418, + "server/src/__tests__/project-routes-env.test.ts": 4210, + "server/src/__tests__/project-workspace-managed-sandbox-routes.test.ts": 6883, + "server/src/__tests__/projects-list-archived-routes.test.ts": 1152, + "server/src/__tests__/redaction.test.ts": 100, + "server/src/__tests__/resource-memberships-routes.test.ts": 1629, + "server/src/__tests__/routine-document-annotation-routes.test.ts": 2027, + "server/src/__tests__/routines-e2e.test.ts": 17403, + "server/src/__tests__/routines-routes.test.ts": 6826, + "server/src/__tests__/secret-proposals-routes.test.ts": 9151, + "server/src/__tests__/secrets-routes-claude-oauth-service.test.ts": 1360, + "server/src/__tests__/secrets-routes.test.ts": 198, + "server/src/__tests__/sidebar-preferences-routes.test.ts": 3043, + "server/src/__tests__/subscription-routes.test.ts": 100, + "server/src/__tests__/summary-slot-routes.test.ts": 5832, + "server/src/__tests__/task-connector-read-routes.test.ts": 122, + "server/src/__tests__/task-title-routes.test.ts": 4602, + "server/src/__tests__/teams-catalog-routes.test.ts": 3849, + "server/src/__tests__/user-profile-routes.test.ts": 1209, + "server/src/__tests__/workspace-command-authz.test.ts": 100, + "server/src/__tests__/workspace-preferences-routes.test.ts": 100, + "server/src/__tests__/workspace-restore-repair-route.test.ts": 1666, + "server/src/__tests__/workspace-runtime-routes-authz.test.ts": 3430, + "server/src/__tests__/workspace-runtime-service-authz.test.ts": 926 } } diff --git a/server/src/services/delivery-work-coordinator.ts b/server/src/services/delivery-work-coordinator.ts index caf710edf29..184caa27c75 100644 --- a/server/src/services/delivery-work-coordinator.ts +++ b/server/src/services/delivery-work-coordinator.ts @@ -1,4 +1,4 @@ -import { subscribeDatabaseWork, databaseWorkPending, reconcileDatabaseWork } from "@paperclipai/db"; +import { subscribeDatabaseWork, databaseWorkPending, reconcileDatabaseWork } from "@paperclipai/db/work-signals"; import { createWorkScheduler } from "./work-scheduler.js"; import { type DeliveryQueue } from "./delivery-work-notifications.js"; import { beginIdleTrackedWork } from "./task-admission.js"; diff --git a/server/src/services/delivery-work-notifications.ts b/server/src/services/delivery-work-notifications.ts index 79f0f5d1d3d..631de5cf49a 100644 --- a/server/src/services/delivery-work-notifications.ts +++ b/server/src/services/delivery-work-notifications.ts @@ -1,4 +1,4 @@ -import { signalDatabaseWork, subscribeDatabaseWork } from "@paperclipai/db"; +import { signalDatabaseWork, subscribeDatabaseWork } from "@paperclipai/db/work-signals"; /** Topics for existing durable queues; register intent before their writes. */ export const DELIVERY_QUEUES = { diff --git a/ui/src/api/api-error.ts b/ui/src/api/api-error.ts new file mode 100644 index 00000000000..442e48def37 --- /dev/null +++ b/ui/src/api/api-error.ts @@ -0,0 +1,37 @@ +import { apiErrorMessage, parseRetryAfter } from "./errors"; + +export interface ApiErrorOptions { + /** Raw server error code; defaults to `body.error` when that is a string. */ + code?: string | null; + /** Server-requested delay from the `Retry-After` header, in milliseconds. */ + retryAfterMs?: number | null; +} + +export class ApiError extends Error { + status: number; + body: unknown; + /** + * The raw `body.error` string (for example `tenant_app_unavailable`). Match + * on this, never on `message`: `message` is readable copy for people and may + * differ from the code (see `apiErrorMessage` in `api/errors.ts`). + */ + code: string | null; + retryAfterMs: number | null; + + constructor(message: string, status: number, body: unknown, options: ApiErrorOptions = {}) { + super(message); + this.name = "ApiError"; + this.status = status; + this.body = body; + const rawError = body && typeof body === "object" ? (body as { error?: unknown }).error : undefined; + this.code = options.code !== undefined ? options.code : typeof rawError === "string" ? rawError : null; + this.retryAfterMs = options.retryAfterMs ?? null; + } +} + +/** Build the `ApiError` for a non-OK response whose body has been read. */ +export function apiErrorFromResponse(res: Response, body: unknown): ApiError { + return new ApiError(apiErrorMessage(res.status, body), res.status, body, { + retryAfterMs: parseRetryAfter(res.headers?.get("Retry-After") ?? null), + }); +} diff --git a/ui/src/api/client.ts b/ui/src/api/client.ts index ae5cabd9450..7b4b8f8c195 100644 --- a/ui/src/api/client.ts +++ b/ui/src/api/client.ts @@ -1,46 +1,11 @@ import { getPageVisibility, getVisibilityHeaderValue } from "@/lib/page-visibility"; import { tenantSessionRecovery } from "@/lib/tenant-session-recovery"; -import { apiErrorMessage, parseRetryAfter } from "./errors"; +import { ApiError, apiErrorFromResponse } from "./api-error"; +export { ApiError, apiErrorFromResponse, type ApiErrorOptions } from "./api-error"; import { readApiJson } from "./response"; const BASE = "/api"; -export interface ApiErrorOptions { - /** Raw server error code; defaults to `body.error` when that is a string. */ - code?: string | null; - /** Server-requested delay from the `Retry-After` header, in milliseconds. */ - retryAfterMs?: number | null; -} - -export class ApiError extends Error { - status: number; - body: unknown; - /** - * The raw `body.error` string (for example `tenant_app_unavailable`). Match - * on this, never on `message`: `message` is readable copy for people and may - * differ from the code (see `apiErrorMessage` in `api/errors.ts`). - */ - code: string | null; - retryAfterMs: number | null; - - constructor(message: string, status: number, body: unknown, options: ApiErrorOptions = {}) { - super(message); - this.name = "ApiError"; - this.status = status; - this.body = body; - const rawError = body && typeof body === "object" ? (body as { error?: unknown }).error : undefined; - this.code = options.code !== undefined ? options.code : typeof rawError === "string" ? rawError : null; - this.retryAfterMs = options.retryAfterMs ?? null; - } -} - -/** Build the `ApiError` for a non-OK response whose body has been read. */ -export function apiErrorFromResponse(res: Response, body: unknown): ApiError { - return new ApiError(apiErrorMessage(res.status, body), res.status, body, { - retryAfterMs: parseRetryAfter(res.headers?.get("Retry-After") ?? null), - }); -} - // --- Dev/QA outage simulator ---------------------------------------------------- // // Reproduces a deploy blip without a deploy. Dev and QA builds only: diff --git a/ui/src/api/errors.test.ts b/ui/src/api/errors.test.ts index c1fc62b6cc8..e2b706e4a5b 100644 --- a/ui/src/api/errors.test.ts +++ b/ui/src/api/errors.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ApiError } from "./client"; +import { ApiError } from "./api-error"; import { apiErrorMessage, classifyError, diff --git a/ui/src/components/RunInvocationCard.test.tsx b/ui/src/components/RunInvocationCard.test.tsx index 603b77d8a72..51bc90e199a 100644 --- a/ui/src/components/RunInvocationCard.test.tsx +++ b/ui/src/components/RunInvocationCard.test.tsx @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; import { ThemeProvider } from "../context/ThemeContext"; -import { RunInvocationCard } from "../pages/AgentDetail"; +import { RunInvocationCard } from "./run-log/RunDetails"; describe("RunInvocationCard", () => { it("keeps verbose invocation details collapsed by default", () => { diff --git a/ui/src/components/run-log/LogViewer.tsx b/ui/src/components/run-log/LogViewer.tsx new file mode 100644 index 00000000000..505a214e8bb --- /dev/null +++ b/ui/src/components/run-log/LogViewer.tsx @@ -0,0 +1,646 @@ +import { TranscriptSequence } from "../../lib/transcript-sequence"; +import { readTranscriptRequest } from "../transcript/read-transcript-request"; +import { useTranscriptLiveStream } from "../../context/LiveUpdatesProvider"; +import { liveRunEvent, mergeRunEvents } from "../../lib/live-run-event"; +import { mergeRunLogChunks, readChunkSeq } from "../../lib/run-log-chunks"; +import { usePageVisibility } from "../../lib/page-visibility"; +import { useCallback, useEffect, useMemo, useState, useRef } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { heartbeatsApi } from "../../api/heartbeats"; +import { instanceSettingsApi } from "../../api/instanceSettings"; +import { ApiError } from "../../api/api-error"; +import { queryKeys } from "../../lib/queryKeys"; +import { toolsApi } from "../../api/tools"; +import { getUIAdapter, buildTranscript, onAdapterChange } from "../../adapters"; +import { cn } from "../../lib/utils"; +import { Button } from "@/components/ui/button"; +import { RunTranscriptView, type TranscriptMode } from "../transcript/RunTranscriptView"; +import { LIVE_TRANSCRIPT_RENDER_LIMIT, MAX_LIVE_EVENTS, MAX_LIVE_LOG_LINES } from "../../lib/live-log-buffer"; +import { type HeartbeatRun, type HeartbeatRunEvent } from "@paperclipai/shared"; +import { + type RunLogChunk, + type ScrollContainer, + shouldPollRunShellLog, + asRecord, + asNonEmptyString, + buildHeartbeatProgressLogLine, + heartbeatProgressLogLineKey, + findScrollContainer, + readScrollMetrics, + LIVE_SCROLL_BOTTOM_TOLERANCE_PX, + scrollToContainerBottom, + RUN_LOG_PAGE_BYTES, + redactPathValue, + WorkspaceOperationsSection, + RunInvocationCard, + redactPathText, +} from "./RunDetails"; + +export function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: string }) { + const { visible } = usePageVisibility(); + const [events, setEvents] = useState([]); + const [hasMoreEvents, setHasMoreEvents] = useState(false); + const [eventsError, setEventsError] = useState(null); + const hasMoreEventsRef = useRef(false); + const [loadingMoreEvents, setLoadingMoreEvents] = useState(false); + const loadMoreEventsRef = useRef<(() => void) | null>(null); + const [logLines, setLogLines] = useState([]); + const [loading, setLoading] = useState(true); + const [logLoading, setLogLoading] = useState(!!run.logRef); + const [logError, setLogError] = useState(null); + const [logOffset, setLogOffsetState] = useState(0); + const logOffsetRef = useRef(0); + const setLogOffset = useCallback((next: number | ((previous: number) => number)) => { + logOffsetRef.current = typeof next === "function" ? next(logOffsetRef.current) : next; + setLogOffsetState(logOffsetRef.current); + }, []); + const logMergeRefs = useRef({ seenChunkKeys: new Set(), trimmedSeqFloorByRun: new Map() }); + const [hasMoreLog, setHasMoreLog] = useState(false); + const [loadingMoreLog, setLoadingMoreLog] = useState(false); + const [isFollowing, setIsFollowing] = useState(false); + const [transcriptMode, setTranscriptMode] = useState("nice"); + const logEndRef = useRef(null); + const pendingLogLineRef = useRef(""); + const seenProgressLogLineKeysRef = useRef>(new Set()); + const scrollContainerRef = useRef(null); + const isFollowingRef = useRef(false); + const lastMetricsRef = useRef<{ scrollHeight: number; distanceFromBottom: number }>({ + scrollHeight: 0, + distanceFromBottom: Number.POSITIVE_INFINITY, + }); + const isLive = run.status === "running" || run.status === "queued"; + const shouldPollShellLog = shouldPollRunShellLog(run.status); + const { data: workspaceOperations = [] } = useQuery({ + queryKey: queryKeys.runWorkspaceOperations(run.id), + queryFn: () => heartbeatsApi.workspaceOperations(run.id), + refetchInterval: isLive ? 2000 : false, + }); + + function isRunLogUnavailable(err: unknown): boolean { + return err instanceof ApiError && err.status === 404; + } + + function appendLogLines(incoming: RunLogChunk[]) { + setLogLines((previous) => mergeRunLogChunks(run.id, previous, incoming.map((line) => ({ + ...line, dedupeKey: `log:${run.id}:${line.ts}:${line.stream}:${line.chunk}`, + })), logMergeRefs.current, isLive ? MAX_LIVE_LOG_LINES : Number.POSITIVE_INFINITY).chunks); + } + + function appendLogContent(content: string, finalize = false) { + if (!content && !finalize) return; + const combined = `${pendingLogLineRef.current}${content}`; + const split = combined.split("\n"); + pendingLogLineRef.current = split.pop() ?? ""; + if (finalize && pendingLogLineRef.current) { + split.push(pendingLogLineRef.current); + pendingLogLineRef.current = ""; + } + + const parsed: RunLogChunk[] = []; + for (const line of split) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown; seq?: unknown }; + const stream = + raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; + const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; + const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); + if (!chunk) continue; + const seq = readChunkSeq(raw.seq); + logSequenceRef.current.confirm(seq); + parsed.push({ ts, stream, chunk, seq }); + } catch { + // ignore malformed lines + } + } + + if (parsed.length > 0) { + // Live runs stream forever, so cap the retained tail. Terminated runs are + // paginated by the user via "Load more log" and keep their full history. + appendLogLines(parsed); + } + } + + const refreshEventsRef = useRef<((force?: boolean) => void) | null>(null); + const refreshLogRef = useRef<((force?: boolean) => void) | null>(null); + const eventSequenceRef = useRef(new TranscriptSequence()); + const logSequenceRef = useRef(new TranscriptSequence()); + const eventsReadCursorRef = useRef(0); + const eventsCatchupCursorRef = useRef(0); + const retainedEventHistoryThroughRef = useRef(0); + const logIdentityRef = useRef(run.logRef ?? null); + const retiredLogRefs = useRef(new Set()); + const wasLiveRef = useRef(isLive); + const bindLog = (ref: unknown): "same" | "changed" | "stale" => { + if (typeof ref !== "string" || !ref || ref === logIdentityRef.current) return "same"; + if (retiredLogRefs.current.has(ref)) return "stale"; + const prior = logIdentityRef.current; + logIdentityRef.current = ref; + if (!prior) return "same"; + retiredLogRefs.current.add(prior); + if (retiredLogRefs.current.size > 8) retiredLogRefs.current.delete(retiredLogRefs.current.values().next().value!); + pendingLogLineRef.current = ""; + logMergeRefs.current = { seenChunkKeys: new Set(), trimmedSeqFloorByRun: new Map() }; + logSequenceRef.current = new TranscriptSequence(); + setLogLines([]); setLogOffset(0); + return "changed"; + }; + useEffect(() => { + eventsReadCursorRef.current = 0; eventsCatchupCursorRef.current = 0; retainedEventHistoryThroughRef.current = 0; eventSequenceRef.current = new TranscriptSequence(); logSequenceRef.current = new TranscriptSequence(); + wasLiveRef.current = isLive; + logIdentityRef.current = run.logRef ?? null; retiredLogRefs.current.clear(); + hasMoreEventsRef.current = false; setHasMoreEvents(false); setLoadingMoreEvents(false); setEventsError(null); + setEvents([]); setLoading(true); + }, [run.id, run.companyId]); + useEffect(() => { if (isLive) wasLiveRef.current = true; }, [isLive]); + + function appendEvents(rows: HeartbeatRunEvent[], retainHistory = false) { + if (retainHistory) retainedEventHistoryThroughRef.current = Math.max( + retainedEventHistoryThroughRef.current, rows.at(-1)?.seq ?? 0, + ); + setEvents(previous => { + const merged = mergeRunEvents(previous, rows); + if (!isLive && !wasLiveRef.current) return merged; + const retainedThrough = Math.max(200, retainedEventHistoryThroughRef.current); + const prefix = merged.filter(row => row.seq <= retainedThrough); + const tail = merged.filter(row => row.seq > retainedThrough); + return [...prefix, ...tail.slice(-MAX_LIVE_EVENTS)]; + }); + } + + const stream = useTranscriptLiveStream(isLive ? [run.id] : [], { companyId: run.companyId, enabled: visible }, event => { + if (event.companyId !== run.companyId) return; + const payload = asRecord(event.payload); + if (!payload || payload.runId !== run.id) return; + if (event.type === "heartbeat.run.log") { + const binding = bindLog(payload.logRef); + if (binding === "stale") return; + const seq = readChunkSeq(payload.seq); + if (payload.truncated === true) logSequenceRef.current.requireHistory(seq); + const gap = logSequenceRef.current.observe(seq); + if (binding === "changed" || payload.truncated === true || gap) refreshLogRef.current?.(); + if (payload.truncated === true) return; + const chunk = typeof payload.chunk === "string" ? payload.chunk : ""; + if (!chunk) return; + const channel = payload.stream === "stderr" || payload.stream === "system" ? payload.stream : "stdout"; + appendLogLines([{ ts: asNonEmptyString(payload.ts) ?? event.createdAt, stream: channel, chunk, seq }]); + return; + } + if (event.type === "heartbeat.run.progress") { + const line = buildHeartbeatProgressLogLine(payload, event.createdAt); + if (!line) return; + const key = heartbeatProgressLogLineKey(line); + if (seenProgressLogLineKeysRef.current.has(key)) return; + seenProgressLogLineKeysRef.current.add(key); appendLogLines([line]); + return; + } + if (event.type === "heartbeat.run.status") { + refreshEventsRef.current?.(true); refreshLogRef.current?.(true); return; + } + const row = liveRunEvent(event); + if (!row) return; + const commitSignal = row.message === null && row.payload === null; + if (commitSignal) eventSequenceRef.current.requireHistory(row.seq); + if (eventSequenceRef.current.observe(row.seq)) refreshEventsRef.current?.(); + if (commitSignal) return; + appendEvents([row]); + }); + + useEffect(() => { + setLoadingMoreEvents(false); + if (!visible) return; + let cancelled = false; + let timer: number | undefined; + const abort = new AbortController(); + let reading = false; + let pendingRefresh = false; + let failedRead: { requested: boolean; catchup: boolean } | null = null; + const read = async (requested = false, catchup = false) => { + if (cancelled) return; + if (reading) { if (catchup) pendingRefresh = true; return; } + if (hasMoreEventsRef.current && !requested && !catchup) return; + reading = true; pendingRefresh = false; setLoadingMoreEvents(true); setEventsError(null); + window.clearTimeout(timer); timer = undefined; + let failed = false; + try { + const cursor = catchup ? Math.max(eventsCatchupCursorRef.current, eventsReadCursorRef.current) : eventsReadCursorRef.current; + const initializesHistory = eventsReadCursorRef.current === 0; + const rows = await readTranscriptRequest(signal => heartbeatsApi.events(run.id, cursor, 1000, { signal }), abort.signal); + if (cancelled) return; + appendEvents(rows, !catchup || initializesHistory); + const next = Math.max(cursor, rows.at(-1)?.seq ?? cursor); + eventsCatchupCursorRef.current = Math.max(eventsCatchupCursorRef.current, next); + eventSequenceRef.current.confirm(next); + if (catchup) { + if (rows.length === 1000 && next > cursor) pendingRefresh = true; + } + if (!catchup || initializesHistory) { + eventsReadCursorRef.current = next; + hasMoreEventsRef.current = rows.length === 1000 && next > cursor; + setHasMoreEvents(hasMoreEventsRef.current); + } + failed = eventSequenceRef.current.needsCatchup; + failedRead = null; + } catch { + failed = true; failedRead = { requested, catchup }; + if (!cancelled) setEventsError("Could not load events. Try again."); + } + if (cancelled) return; + reading = false; setLoadingMoreEvents(false); setLoading(false); + if (pendingRefresh) void read(false, true); + else if (failed || (isLive && !stream.healthy && !hasMoreEventsRef.current)) { + timer = window.setTimeout(() => void read(requested, catchup || eventSequenceRef.current.needsCatchup), 2000); + } + }; + refreshEventsRef.current = () => { void read(false, wasLiveRef.current); }; + loadMoreEventsRef.current = () => { void read(failedRead?.requested ?? true, failedRead?.catchup ?? false); }; + void read(false, wasLiveRef.current && (stream.healthy || eventsReadCursorRef.current > 0)); + return () => { + cancelled = true; refreshEventsRef.current = null; loadMoreEventsRef.current = null; + abort.abort(); if (timer !== undefined) window.clearTimeout(timer); + }; + }, [visible, run.id, run.companyId, run.status, stream.healthy, stream.epoch]); + + const getScrollContainer = useCallback((): ScrollContainer => { + if (scrollContainerRef.current) return scrollContainerRef.current; + const container = findScrollContainer(logEndRef.current); + scrollContainerRef.current = container; + return container; + }, []); + + const updateFollowingState = useCallback(() => { + const container = getScrollContainer(); + const metrics = readScrollMetrics(container); + lastMetricsRef.current = metrics; + const nearBottom = metrics.distanceFromBottom <= LIVE_SCROLL_BOTTOM_TOLERANCE_PX; + isFollowingRef.current = nearBottom; + setIsFollowing((prev) => (prev === nearBottom ? prev : nearBottom)); + }, [getScrollContainer]); + + useEffect(() => { + scrollContainerRef.current = null; + lastMetricsRef.current = { + scrollHeight: 0, + distanceFromBottom: Number.POSITIVE_INFINITY, + }; + + if (!isLive) { + isFollowingRef.current = false; + setIsFollowing(false); + return; + } + + updateFollowingState(); + }, [isLive, run.id, updateFollowingState]); + + useEffect(() => { + if (!isLive) return; + const container = getScrollContainer(); + updateFollowingState(); + + if (container === window) { + window.addEventListener("scroll", updateFollowingState, { passive: true }); + } else { + container.addEventListener("scroll", updateFollowingState, { passive: true }); + } + window.addEventListener("resize", updateFollowingState); + return () => { + if (container === window) { + window.removeEventListener("scroll", updateFollowingState); + } else { + container.removeEventListener("scroll", updateFollowingState); + } + window.removeEventListener("resize", updateFollowingState); + }; + }, [isLive, run.id, getScrollContainer, updateFollowingState]); + + // Auto-scroll only for live runs when following + useEffect(() => { + if (!isLive || !isFollowingRef.current) return; + + const container = getScrollContainer(); + const previous = lastMetricsRef.current; + const current = readScrollMetrics(container); + const growth = Math.max(0, current.scrollHeight - previous.scrollHeight); + const expectedDistance = previous.distanceFromBottom + growth; + const movedAwayBy = current.distanceFromBottom - expectedDistance; + + // If user moved away from bottom between updates, release auto-follow immediately. + if (movedAwayBy > LIVE_SCROLL_BOTTOM_TOLERANCE_PX) { + isFollowingRef.current = false; + setIsFollowing(false); + lastMetricsRef.current = current; + return; + } + + scrollToContainerBottom(container, "auto"); + const after = readScrollMetrics(container); + lastMetricsRef.current = after; + if (!isFollowingRef.current) { + isFollowingRef.current = true; + } + setIsFollowing((prev) => (prev ? prev : true)); + }, [events.length, logLines.length, isLive, getScrollContainer]); + + // Reset only when the log source changes, never when visibility changes. + useEffect(() => { + pendingLogLineRef.current = ""; + logMergeRefs.current = { seenChunkKeys: new Set(), trimmedSeqFloorByRun: new Map() }; + seenProgressLogLineKeysRef.current = new Set(); + setLogLines([]); + setLogOffset(0); + setHasMoreLog(false); + setLoadingMoreLog(false); + setLogError(null); + }, [run.id, run.logRef, setLogOffset]); + + useEffect(() => { + if (!visible) return; + if (!run.logRef && !shouldPollShellLog) { setLogLoading(false); return; } + let cancelled = false; + let timer: number | undefined; + const abort = new AbortController(); + setLogLoading(true); + let reading = false; + let pendingRefresh = false; + const load = async () => { + if (cancelled || reading) return; + reading = true; pendingRefresh = false; + window.clearTimeout(timer); timer = undefined; + let failed = false; + try { + for (;;) { + const offset = logOffsetRef.current; + const result = await readTranscriptRequest(signal => heartbeatsApi.log(run.id, offset, RUN_LOG_PAGE_BYTES, { signal }), abort.signal); + if (cancelled) return; + const binding = bindLog(result.logRef); + if (binding === "stale") { failed = true; break; } + if (binding === "changed" && offset !== 0) continue; + appendLogContent(result.content, result.nextOffset === undefined); + const next = result.nextOffset ?? offset + new TextEncoder().encode(result.content).byteLength; + setLogOffset(next); setLogError(null); + setHasMoreLog(!wasLiveRef.current && result.nextOffset !== undefined && !!result.content); + if (!wasLiveRef.current || result.nextOffset === undefined || !result.content || next <= offset) break; + } + failed ||= logSequenceRef.current.needsCatchup; + } catch (err) { + if (cancelled) return; + failed = shouldPollShellLog || !isRunLogUnavailable(err); + if (!isRunLogUnavailable(err)) setLogError(err instanceof Error ? err.message : "Failed to load run log"); + } + if (cancelled) return; + reading = false; + setLogLoading(false); + if (pendingRefresh) void load(); + else if (failed || (shouldPollShellLog && !stream.healthy)) timer = window.setTimeout(() => void load(), 2000); + }; + refreshLogRef.current = (force = false) => { if (force) pendingRefresh = true; void load(); }; + void load(); + return () => { cancelled = true; refreshLogRef.current = null; abort.abort(); if (timer !== undefined) window.clearTimeout(timer); }; + }, [visible, run.id, run.companyId, run.logRef, isLive ? null : run.logBytes, shouldPollShellLog, stream.healthy, stream.epoch]); + + async function loadMorePersistedLog() { + if (loadingMoreLog || !hasMoreLog) return; + setLoadingMoreLog(true); + setLogError(null); + try { + const result = await heartbeatsApi.log(run.id, logOffset, RUN_LOG_PAGE_BYTES); + appendLogContent(result.content, result.nextOffset === undefined); + const next = result.nextOffset ?? logOffset + new TextEncoder().encode(result.content).byteLength; + setLogOffset(next); + setHasMoreLog(result.nextOffset !== undefined); + } catch (err) { + setLogError(err instanceof Error ? err.message : "Failed to load more run log"); + } finally { + setLoadingMoreLog(false); + } + } + + const censorUsernameInLogs = useQuery({ + queryKey: queryKeys.instance.generalSettings, + queryFn: () => instanceSettingsApi.getGeneral(), + }).data?.censorUsernameInLogs === true; + + const adapterInvokePayload = useMemo(() => { + const evt = events.find((e) => e.eventType === "adapter.invoke"); + return redactPathValue(asRecord(evt?.payload ?? null), censorUsernameInLogs); + }, [censorUsernameInLogs, events]); + + // NOTE: adapter is NOT memoized because external adapters replace their + // parseStdoutLine asynchronously after dynamic parser loading. Memoizing + // on adapterType alone would stale the transcript with the fallback parser. + // We subscribe to adapter registry changes to force transcript recomputation. + const [parserTick, setParserTick] = useState(0); + const adapter = getUIAdapter(adapterType); + + useEffect(() => { + return onAdapterChange(() => setParserTick((t) => t + 1)); + }, []); + + const transcript = useMemo( + () => buildTranscript(logLines, adapter, { censorUsernameInLogs }), + [adapter, censorUsernameInLogs, logLines, parserTick], + ); + const toolDecisionLookup = useQuery({ + queryKey: queryKeys.tools.runDecisions(run.companyId, run.id), + queryFn: () => toolsApi.getRunDecisionLookup(run.companyId, run.id), + enabled: Boolean(run.companyId && run.id), + refetchInterval: isLive ? 3000 : false, + staleTime: isLive ? 1000 : 30_000, + }); + + useEffect(() => { + setTranscriptMode("nice"); + }, [run.id]); + + if (loading && logLoading) { + return

Loading run logs...

; + } + + if (events.length === 0 && logLines.length === 0 && !logError && !eventsError) { + return

No log events.

; + } + + const levelColors: Record = { + info: "text-foreground", + warn: "text-yellow-600 dark:text-yellow-400", + error: "text-red-600 dark:text-red-400", + }; + + const streamColors: Record = { + stdout: "text-foreground", + stderr: "text-red-600 dark:text-red-300", + system: "text-blue-600 dark:text-blue-300", + }; + + return ( +
+ + {adapterInvokePayload && ( + + )} + +
+ + Transcript ({transcript.length}) + +
+
+ {(["nice", "raw"] as const).map((mode) => ( + + ))} +
+ {isLive && !isFollowing && ( + + )} + {isLive && ( + + + + + + Live + + )} +
+
+
+ + {hasMoreLog && ( +
+ + + Showing the first {Math.round(logOffset / 1024).toLocaleString("en-US")} KB + {typeof run.logBytes === "number" && run.logBytes > 0 + ? ` of ${Math.round(run.logBytes / 1024).toLocaleString("en-US")} KB` + : ""} + +
+ )} + {logError && ( +
+ {logError} +
+ )} +
+
+ + {(run.status === "failed" || run.status === "timed_out") && ( +
+
Failure details
+ {run.error && ( +
+ Error: + {redactPathText(run.error, censorUsernameInLogs)} +
+ )} + {run.stderrExcerpt && run.stderrExcerpt.trim() && ( +
+
stderr excerpt
+
+                {redactPathText(run.stderrExcerpt, censorUsernameInLogs)}
+              
+
+ )} + {run.resultJson && ( +
+
adapter result JSON
+
+                {JSON.stringify(redactPathValue(run.resultJson, censorUsernameInLogs), null, 2)}
+              
+
+ )} + {run.stdoutExcerpt && run.stdoutExcerpt.trim() && !run.resultJson && ( +
+
stdout excerpt
+
+                {redactPathText(run.stdoutExcerpt, censorUsernameInLogs)}
+              
+
+ )} +
+ )} + + {(events.length > 0 || eventsError) && ( +
+
Events ({events.length})
+ {eventsError &&

{eventsError}

} + {(hasMoreEvents || eventsError) && ( + + )} +
+ {events.map((evt) => { + const color = evt.color + ?? (evt.level ? levelColors[evt.level] : null) + ?? (evt.stream ? streamColors[evt.stream] : null) + ?? "text-foreground"; + + return ( +
+ + {new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })} + + + {evt.stream ? `[${evt.stream}]` : ""} + + + {evt.message + ? redactPathText(evt.message, censorUsernameInLogs) + : evt.payload + ? JSON.stringify(redactPathValue(evt.payload, censorUsernameInLogs)) + : ""} + +
+ ); + })} +
+
+ )} +
+ ); +} diff --git a/ui/src/components/run-log/RunDetails.tsx b/ui/src/components/run-log/RunDetails.tsx new file mode 100644 index 00000000000..003ce025ba5 --- /dev/null +++ b/ui/src/components/run-log/RunDetails.tsx @@ -0,0 +1,490 @@ +import { useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { heartbeatsApi } from "../../api/heartbeats"; +import { redactCommandText as redactCommandSecretText } from "@paperclipai/adapter-utils"; +import { cn, relativeTime } from "../../lib/utils"; +import { ChevronRight } from "lucide-react"; +import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; +import { type HeartbeatRun, type WorkspaceOperation } from "@paperclipai/shared"; +import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils"; +import { Badge } from "@/components/ui/badge"; + +export const RUN_LOG_PAGE_BYTES = 256_000; + +const REDACTED_ENV_VALUE = "***REDACTED***"; + +const SECRET_ENV_KEY_RE = + /(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i; + +const COMMAND_ENV_KEY_RE = /(^command$|^cmd$|command[-_]?line|resolved[-_]?command|PAPERCLIP_RESOLVED_COMMAND)/i; + +const JWT_VALUE_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?$/; + +export function redactPathText(value: string, censorUsernameInLogs: boolean) { + return redactHomePathUserSegments(value, { enabled: censorUsernameInLogs }); +} + +export function redactPathValue(value: T, censorUsernameInLogs: boolean): T { + return redactHomePathUserSegmentsInValue(value, { enabled: censorUsernameInLogs }); +} + +function redactCommandText(value: string, censorUsernameInLogs: boolean): string { + return redactPathText(redactCommandSecretText(value, REDACTED_ENV_VALUE), censorUsernameInLogs); +} + +function shouldRedactSecretValue(key: string, value: unknown): boolean { + if (SECRET_ENV_KEY_RE.test(key)) return true; + if (typeof value !== "string") return false; + return JWT_VALUE_RE.test(value); +} + +function redactEnvValue(key: string, value: unknown, censorUsernameInLogs: boolean): string { + if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + (value as { type?: unknown }).type === "secret_ref" + ) { + return "***SECRET_REF***"; + } + if (shouldRedactSecretValue(key, value)) return REDACTED_ENV_VALUE; + if (value === null || value === undefined) return ""; + if (typeof value === "string" && COMMAND_ENV_KEY_RE.test(key)) return redactCommandText(value, censorUsernameInLogs); + if (typeof value === "string") return redactPathText(value, censorUsernameInLogs); + try { + return JSON.stringify(redactPathValue(value, censorUsernameInLogs)); + } catch { + return redactPathText(String(value), censorUsernameInLogs); + } +} + +function formatEnvForDisplay(envValue: unknown, censorUsernameInLogs: boolean): string { + const env = asRecord(envValue); + if (!env) return ""; + + const keys = Object.keys(env); + if (keys.length === 0) return ""; + + return keys + .sort() + .map((key) => `${key}=${redactEnvValue(key, env[key], censorUsernameInLogs)}`) + .join("\n"); +} + +export const LIVE_SCROLL_BOTTOM_TOLERANCE_PX = 32; + +export type ScrollContainer = Window | HTMLElement; + +function isWindowContainer(container: ScrollContainer): container is Window { + return container === window; +} + +function isElementScrollContainer(element: HTMLElement): boolean { + const overflowY = window.getComputedStyle(element).overflowY; + return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay"; +} + +export function findScrollContainer(anchor: HTMLElement | null): ScrollContainer { + let parent = anchor?.parentElement ?? null; + while (parent) { + if (isElementScrollContainer(parent)) return parent; + parent = parent.parentElement; + } + return window; +} + +export function readScrollMetrics(container: ScrollContainer): { scrollHeight: number; distanceFromBottom: number } { + if (isWindowContainer(container)) { + const pageHeight = Math.max( + document.documentElement.scrollHeight, + document.body.scrollHeight, + ); + const viewportBottom = window.scrollY + window.innerHeight; + return { + scrollHeight: pageHeight, + distanceFromBottom: Math.max(0, pageHeight - viewportBottom), + }; + } + + const viewportBottom = container.scrollTop + container.clientHeight; + return { + scrollHeight: container.scrollHeight, + distanceFromBottom: Math.max(0, container.scrollHeight - viewportBottom), + }; +} + +export function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBehavior = "auto") { + if (isWindowContainer(container)) { + const pageHeight = Math.max( + document.documentElement.scrollHeight, + document.body.scrollHeight, + ); + window.scrollTo({ top: pageHeight, behavior }); + return; + } + + container.scrollTo({ top: container.scrollHeight, behavior }); +} + +export type RunLogChunk = { + seq?: number; + ts: string; + stream: "stdout" | "stderr" | "system"; + chunk: string; +}; + +export function asRecord(value: unknown): Record | null { + if (typeof value !== "object" || value === null || Array.isArray(value)) return null; + return value as Record; +} + +export function asNonEmptyString(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +export function buildHeartbeatProgressLogLine( + payload: Record, + fallbackTimestamp: string, +): RunLogChunk | null { + const message = asNonEmptyString(payload.message); + if (!message) return null; + const phase = asNonEmptyString(payload.phase); + const ts = asNonEmptyString(payload.updatedAt) ?? fallbackTimestamp; + const chunk = phase ? `[${phase}] ${message}` : message; + return { ts, stream: "system", chunk }; +} + +export function heartbeatProgressLogLineKey(line: RunLogChunk): string { + return `${line.ts}\u0000${line.stream}\u0000${line.chunk}`; +} + +export function shouldPollRunShellLog(status: HeartbeatRun["status"]): boolean { + return status === "running"; +} + +export function RunInvocationCard({ + payload, + censorUsernameInLogs, +}: { + payload: Record; + censorUsernameInLogs: boolean; +}) { + const rawCommandLine = [ + typeof payload.command === "string" ? payload.command : null, + ...(Array.isArray(payload.commandArgs) + ? payload.commandArgs.filter((value): value is string => typeof value === "string") + : []), + ] + .filter((value): value is string => Boolean(value)) + .join(" "); + const commandLine = rawCommandLine ? redactCommandText(rawCommandLine, censorUsernameInLogs) : ""; + + const hasAdvancedDetails = + commandLine.length > 0 + || (Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0) + || payload.prompt !== undefined + || payload.context !== undefined + || payload.env !== undefined; + + return ( +
+
Invocation
+ {typeof payload.adapterType === "string" && ( +
Adapter: {payload.adapterType}
+ )} + {typeof payload.cwd === "string" && ( +
Working dir: {payload.cwd}
+ )} + {hasAdvancedDetails && ( + + + + Details + + + {commandLine && ( +
+ Command: + {commandLine} +
+ )} + {Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0 && ( +
+
Command notes
+
    + {payload.commandNotes + .filter((value): value is string => typeof value === "string" && value.trim().length > 0) + .map((note, idx) => ( +
  • + {note} +
  • + ))} +
+
+ )} + {payload.prompt !== undefined && ( +
+
Prompt
+
+                  {typeof payload.prompt === "string"
+                    ? redactPathText(payload.prompt, censorUsernameInLogs)
+                    : JSON.stringify(redactPathValue(payload.prompt, censorUsernameInLogs), null, 2)}
+                
+
+ )} + {payload.context !== undefined && ( +
+
Context
+
+                  {JSON.stringify(redactPathValue(payload.context, censorUsernameInLogs), null, 2)}
+                
+
+ )} + {payload.env !== undefined && ( +
+
Environment
+
+                  {formatEnvForDisplay(payload.env, censorUsernameInLogs)}
+                
+
+ )} +
+
+ )} +
+ ); +} + +function parseStoredLogContent(content: string): RunLogChunk[] { + const parsed: RunLogChunk[] = []; + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown }; + const stream = + raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; + const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; + const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); + if (!chunk) continue; + parsed.push({ ts, stream, chunk }); + } catch { + // Ignore malformed log lines. + } + } + return parsed; +} + +function workspaceOperationPhaseLabel(phase: WorkspaceOperation["phase"]) { + switch (phase) { + case "worktree_prepare": + return "Worktree setup"; + case "workspace_config_freshness": + return "Config freshness"; + case "workspace_provision": + return "Provision"; + case "workspace_teardown": + return "Teardown"; + case "worktree_cleanup": + return "Worktree cleanup"; + default: + return phase; + } +} + +function workspaceOperationStatusTone(status: WorkspaceOperation["status"]) { + switch (status) { + case "succeeded": + return "border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-300"; + case "failed": + return "border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300"; + case "running": + return "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-300"; + case "skipped": + return "border-yellow-500/20 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"; + default: + return "border-border bg-muted/40 text-muted-foreground"; + } +} + +function WorkspaceOperationStatusBadge({ status }: { status: WorkspaceOperation["status"] }) { + return ( + + {status.replace("_", " ")} + + ); +} + +function WorkspaceOperationLogViewer({ + operation, + censorUsernameInLogs, +}: { + operation: WorkspaceOperation; + censorUsernameInLogs: boolean; +}) { + const [open, setOpen] = useState(false); + const { data: logData, isLoading, error } = useQuery({ + queryKey: ["workspace-operation-log", operation.id], + queryFn: () => heartbeatsApi.workspaceOperationLog(operation.id), + enabled: open && Boolean(operation.logRef), + refetchInterval: open && operation.status === "running" ? 2000 : false, + }); + + const chunks = useMemo( + () => (logData?.content ? parseStoredLogContent(logData.content) : []), + [logData?.content], + ); + + return ( +
+ + {open && ( +
+ {isLoading &&
Loading log...
} + {error && ( +
+ {error instanceof Error ? error.message : "Failed to load workspace operation log"} +
+ )} + {!isLoading && !error && chunks.length === 0 && ( +
No persisted log lines.
+ )} + {chunks.length > 0 && ( +
+ {chunks.map((chunk, index) => ( +
+ + {new Date(chunk.ts).toLocaleTimeString("en-US", { hour12: false })} + + + [{chunk.stream}] + + {redactPathText(chunk.chunk, censorUsernameInLogs)} +
+ ))} +
+ )} +
+ )} +
+ ); +} + +export function WorkspaceOperationsSection({ + operations, + censorUsernameInLogs, +}: { + operations: WorkspaceOperation[]; + censorUsernameInLogs: boolean; +}) { + if (operations.length === 0) return null; + + return ( +
+
+ Workspace ({operations.length}) +
+
+ {operations.map((operation) => { + const metadata = asRecord(operation.metadata); + return ( +
+
+
{workspaceOperationPhaseLabel(operation.phase)}
+ +
+ {relativeTime(operation.startedAt)} + {operation.finishedAt && ` to ${relativeTime(operation.finishedAt)}`} +
+
+ {operation.command && ( +
+ Command: + {operation.command} +
+ )} + {operation.cwd && ( +
+ Working dir: + {operation.cwd} +
+ )} + {(asNonEmptyString(metadata?.branchName) + || asNonEmptyString(metadata?.baseRef) + || asNonEmptyString(metadata?.worktreePath) + || asNonEmptyString(metadata?.repoRoot) + || asNonEmptyString(metadata?.cleanupAction)) && ( +
+ {asNonEmptyString(metadata?.branchName) && ( +
Branch: {metadata?.branchName as string}
+ )} + {asNonEmptyString(metadata?.baseRef) && ( +
Base ref: {metadata?.baseRef as string}
+ )} + {asNonEmptyString(metadata?.worktreePath) && ( +
Worktree: {metadata?.worktreePath as string}
+ )} + {asNonEmptyString(metadata?.repoRoot) && ( +
Repo root: {metadata?.repoRoot as string}
+ )} + {asNonEmptyString(metadata?.cleanupAction) && ( +
Cleanup: {metadata?.cleanupAction as string}
+ )} +
+ )} + {typeof metadata?.created === "boolean" && ( +
+ {metadata.created ? "Created by this run" : "Reused existing workspace"} +
+ )} + {operation.stderrExcerpt && operation.stderrExcerpt.trim() && ( +
+
stderr excerpt
+
+                    {redactPathText(operation.stderrExcerpt, censorUsernameInLogs)}
+                  
+
+ )} + {operation.stdoutExcerpt && operation.stdoutExcerpt.trim() && ( +
+
stdout excerpt
+
+                    {redactPathText(operation.stdoutExcerpt, censorUsernameInLogs)}
+                  
+
+ )} + {operation.logRef && ( + + )} +
+ ); + })} +
+
+ ); +} diff --git a/ui/src/pages/AgentDetail.log-visibility.test.tsx b/ui/src/pages/AgentDetail.log-visibility.test.tsx index d8e486fd84d..b2cd2945358 100644 --- a/ui/src/pages/AgentDetail.log-visibility.test.tsx +++ b/ui/src/pages/AgentDetail.log-visibility.test.tsx @@ -5,8 +5,7 @@ import type { HeartbeatRun, HeartbeatRunEvent, LiveEvent } from "@paperclipai/sh import { afterEach, expect, it, vi } from "vitest"; import { __liveUpdatesTestUtils, type CompanyLiveEventHandler } from "../context/LiveUpdatesProvider"; import { LiveTranscriptInterests } from "../lib/live-transcript-interests"; -import { LogViewer } from "./AgentDetail"; -import { LogViewer as ProductionLogViewer } from "./AgentDetail.production"; +import { LogViewer } from "../components/run-log/LogViewer"; const { log, events, empty } = vi.hoisted(() => ({ log: vi.fn(), events: vi.fn(async (_runId?: string, _cursor?: number, _limit?: number, _options?: { signal?: AbortSignal }): Promise => []), empty: [] })); vi.mock("../api/heartbeats", () => ({ heartbeatsApi: { log, events } })); @@ -25,7 +24,7 @@ vi.mock("../components/transcript/RunTranscriptView", () => ({ (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); vi.useRealTimers(); log.mockReset(); events.mockReset(); events.mockResolvedValue([]); }); -it.each([LogViewer, ProductionLogViewer])("retains legacy history and reads only the next offset on visibility recovery (%#)", async (Viewer) => { +it("retains legacy history and reads only the next offset on visibility recovery", async () => { const visibility = vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); const row = (seq: number, chunk: string) => JSON.stringify({ seq, ts: `2026-09-10T12:00:0${seq}Z`, stream: "stdout", chunk }) + "\n"; const first = row(1, "retained history"); @@ -36,7 +35,7 @@ it.each([LogViewer, ProductionLogViewer])("retains legacy history and reads only document.body.append(container); const root = createRoot(container); try { - await act(async () => root.render()); + await act(async () => root.render()); expect(container.textContent).toContain("retained history"); await act(async () => { visibility.mockReturnValue("hidden"); @@ -55,7 +54,7 @@ it.each([LogViewer, ProductionLogViewer])("retains legacy history and reads only await act(async () => complete({ content: second, nextOffset: first.length + second.length })); expect(container.textContent).toContain("retained history new output"); log.mockResolvedValueOnce({ content: first, nextOffset: first.length }); - await act(async () => root.render()); + await act(async () => root.render()); expect(log).toHaveBeenLastCalledWith("run-2", 0, expect.any(Number), expect.objectContaining({ signal: expect.any(AbortSignal) })); expect(container.textContent).not.toContain("new output"); } finally { @@ -64,7 +63,7 @@ it.each([LogViewer, ProductionLogViewer])("retains legacy history and reads only } }); -it.each([LogViewer, ProductionLogViewer])("uses shared streaming and retains fallback reads until negotiation (%#)", async (Viewer) => { +it("uses shared streaming and retains fallback reads until negotiation", async () => { vi.useFakeTimers(); vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); const socket = vi.fn(); @@ -77,7 +76,7 @@ it.each([LogViewer, ProductionLogViewer])("uses shared streaming and retains fal const subscription = { companyId: "company-1", transcripts, subscribe: () => () => {} }; const { LiveEventSubscriptionContext } = __liveUpdatesTestUtils; try { - await act(async () => root.render()); + await act(async () => root.render()); const initialReads = log.mock.calls.length; await act(async () => vi.advanceTimersByTimeAsync(2000)); expect(log.mock.calls.length).toBeGreaterThan(initialReads); @@ -98,7 +97,7 @@ it.each([LogViewer, ProductionLogViewer])("uses shared streaming and retains fal expect(messages.at(-1)?.runIds).toEqual([]); }); -it.each([LogViewer, ProductionLogViewer])("re-enables cancelled history paging after hiding and showing a completed run (%#)", async Viewer => { +it("re-enables cancelled history paging after hiding and showing a completed run", async () => { const visibility = vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); log.mockResolvedValue({ content: "", nextOffset: 0 }); const rows = Array.from({ length: 1001 }, (_, index) => ({ id: index + 1, seq: index + 1, @@ -113,7 +112,7 @@ it.each([LogViewer, ProductionLogViewer])("re-enables cancelled history paging a const root = createRoot(container); const more = () => [...container.querySelectorAll("button")].find(button => /Load more events|Loading\.\.\./.test(button.textContent ?? "")); try { - await act(async () => root.render()); + await act(async () => root.render()); await act(async () => more()!.click()); expect(more()!.disabled).toBe(true); const cancelledSignal = events.mock.calls[1]![3]!.signal; @@ -132,7 +131,7 @@ it.each([LogViewer, ProductionLogViewer])("re-enables cancelled history paging a } finally { await act(async () => root.unmount()); } }, 30_000); -it.each([LogViewer, ProductionLogViewer])("bounds automatic event history and pages further only on request (%#)", async Viewer => { +it("bounds automatic event history and pages further only on request", async () => { vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); log.mockResolvedValue({ content: "", nextOffset: 0 }); const rows = Array.from({ length: 2_001 }, (_, index) => ({ id: index + 1, seq: index + 1, @@ -144,7 +143,7 @@ it.each([LogViewer, ProductionLogViewer])("bounds automatic event history and pa const container = document.createElement("div"); const root = createRoot(container); try { - await act(async () => root.render()); + await act(async () => root.render()); expect(events).toHaveBeenCalledTimes(1); expect(container.textContent).toContain("invocation-retained-proof"); const more = [...container.querySelectorAll("button")].find(button => button.textContent === "Load more events"); @@ -161,7 +160,7 @@ it.each([LogViewer, ProductionLogViewer])("bounds automatic event history and pa } finally { await act(async () => root.unmount()); } }, 30_000); -it.each([LogViewer, ProductionLogViewer])("preserves saved event details and catches up payload-free commit signals (%#)", async Viewer => { +it("preserves saved event details and catches up payload-free commit signals", async () => { vi.useFakeTimers(); vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); log.mockResolvedValue({ content: "", nextOffset: 0 }); @@ -186,7 +185,7 @@ it.each([LogViewer, ProductionLogViewer])("preserves saved event details and cat const root = createRoot(container); const { LiveEventSubscriptionContext } = __liveUpdatesTestUtils; try { - await act(async () => root.render()); + await act(async () => root.render()); await act(async () => { transcripts.open({ send: text => messages.push(JSON.parse(text)) }); transcripts.receive({ type: "live.capabilities", version: 1, capabilities: ["run-interests"] }); @@ -214,13 +213,13 @@ it.each([LogViewer, ProductionLogViewer])("preserves saved event details and cat expect(container.textContent).toContain("saved event details"); expect(container.textContent).toContain("live row 2205"); expect(container.textContent).toContain("Events (2200)"); - await act(async () => root.render()); + await act(async () => root.render()); expect(container.textContent).toContain("saved event details"); expect(container.textContent).toContain("live row 2205"); } finally { await act(async () => root.unmount()); } }, 30_000); -it.each([LogViewer, ProductionLogViewer])("merges saved and live rows with colliding database and run-sequence IDs (%#)", async Viewer => { +it("merges saved and live rows with colliding database and run-sequence IDs", async () => { vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); log.mockResolvedValue({ content: "", nextOffset: 0 }); @@ -240,7 +239,7 @@ it.each([LogViewer, ProductionLogViewer])("merges saved and live rows with colli for (const listener of listeners) listener(event); }; try { - await act(async () => root.render()); + await act(async () => root.render()); await act(async () => { emit(2, "live second row"); emit(3, "live third row"); }); expect(container.textContent).toContain("saved first row"); expect(container.textContent).toContain("live second row"); @@ -250,9 +249,9 @@ it.each([LogViewer, ProductionLogViewer])("merges saved and live rows with colli }); it.each([ - { Viewer: LogViewer, stalled: "log" }, { Viewer: ProductionLogViewer, stalled: "log" }, - { Viewer: LogViewer, stalled: "events" }, { Viewer: ProductionLogViewer, stalled: "events" }, -])("finishes a slow $stalled page while the other reader catches up (%#)", async ({ Viewer, stalled }) => { + { stalled: "log" }, + { stalled: "events" }, +])("finishes a slow $stalled page while the other reader catches up", async ({ stalled }) => { vi.useFakeTimers(); vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); log.mockResolvedValue({ content: "" }); @@ -277,7 +276,7 @@ it.each([ let finishEvents!: (value: HeartbeatRunEvent[]) => void; const held = stalled === "log" ? log : events; try { - await act(async () => root.render()); + await act(async () => root.render()); await act(async () => { transcripts.open({ send: text => messages.push(JSON.parse(text)) }); transcripts.receive({ type: "live.capabilities", version: 1, capabilities: ["run-interests"] }); @@ -312,7 +311,7 @@ it.each([ } finally { await act(async () => root.unmount()); } }); -it.each([LogViewer, ProductionLogViewer])("shows and retries an initial event-history failure instead of claiming empty history (%#)", async Viewer => { +it("shows and retries an initial event-history failure instead of claiming empty history", async () => { vi.useFakeTimers(); vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); log.mockResolvedValue({ content: "", nextOffset: 0 }); @@ -321,7 +320,7 @@ it.each([LogViewer, ProductionLogViewer])("shows and retries an initial event-hi const container = document.createElement("div"); const root = createRoot(container); try { - await act(async () => root.render()); + await act(async () => root.render()); expect(container.textContent).toContain("Could not load events. Try again."); expect(container.textContent).not.toContain("No log events."); await act(async () => vi.advanceTimersByTimeAsync(2000)); @@ -341,7 +340,7 @@ it.each([LogViewer, ProductionLogViewer])("shows and retries an initial event-hi }); -it.each([LogViewer, ProductionLogViewer])("recovers a paginated live tail and preserves pages explicitly loaded afterward (%#)", async Viewer => { +it("recovers a paginated live tail and preserves pages explicitly loaded afterward", async () => { vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); log.mockResolvedValue({ content: "", nextOffset: 0 }); const run = { id: "run-1", companyId: "company-1", agentId: "agent-1", status: "running" } as HeartbeatRun; @@ -356,7 +355,7 @@ it.each([LogViewer, ProductionLogViewer])("recovers a paginated live tail and pr subscribe(fn: CompanyLiveEventHandler) { listeners.add(fn); return () => { listeners.delete(fn); }; } }; const container = document.createElement("div"), root = createRoot(container); const { LiveEventSubscriptionContext } = __liveUpdatesTestUtils; - const render = async (status = "running") => act(async () => root.render()); + const render = async (status = "running") => act(async () => root.render()); const emit = (row: HeartbeatRunEvent) => { for (const listener of listeners) listener({ id: row.seq, companyId: run.companyId, type: "heartbeat.run.event", createdAt: row.createdAt.toISOString(), payload: { ...row } }); }; try { @@ -389,7 +388,7 @@ it.each([LogViewer, ProductionLogViewer])("recovers a paginated live tail and pr }, 60_000); -it.each([LogViewer, ProductionLogViewer].flatMap(Viewer => ["completed-return", "catchup-retry"].map(mode => ({ Viewer, mode }))))("preserves event read mode for $mode (%#)", async ({ Viewer, mode }) => { +it.each(["completed-return", "catchup-retry"].map(mode => ({ mode })))("preserves event read mode for $mode", async ({ mode }) => { vi.useFakeTimers(); const visibility = vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); log.mockResolvedValue({ content: "", nextOffset: 0 }); @@ -409,7 +408,7 @@ it.each([LogViewer, ProductionLogViewer].flatMap(Viewer => ["completed-return", const container = document.createElement("div"), root = createRoot(container); const { LiveEventSubscriptionContext } = __liveUpdatesTestUtils; try { - await act(async () => root.render()); + await act(async () => root.render()); expect(events).toHaveBeenCalledTimes(1); if (mode === "completed-return") { await act(async () => { visibility.mockReturnValue("hidden"); document.dispatchEvent(new Event("visibilitychange")); }); @@ -435,7 +434,7 @@ it.each([LogViewer, ProductionLogViewer].flatMap(Viewer => ["completed-return", } finally { await act(async () => root.unmount()); } }, 60_000); -it.each([LogViewer, ProductionLogViewer])("recovers the latest saved tail when acknowledgement aborts the initial history read (%#)", async Viewer => { +it("recovers the latest saved tail when acknowledgement aborts the initial history read", async () => { vi.useFakeTimers(); vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); log.mockResolvedValue({ content: "", nextOffset: 0 }); @@ -453,7 +452,7 @@ it.each([LogViewer, ProductionLogViewer])("recovers the latest saved tail when a const container = document.createElement("div"), root = createRoot(container); const { LiveEventSubscriptionContext } = __liveUpdatesTestUtils; try { - await act(async () => root.render()); + await act(async () => root.render()); const initialSignal = events.mock.calls[0]![3]!.signal!; await act(async () => { transcripts.open({ send: text => sent.push(JSON.parse(text)) }); diff --git a/ui/src/pages/AgentDetail.production.tsx b/ui/src/pages/AgentDetail.production.tsx index 2bc1ee7838d..c692d065f2f 100644 --- a/ui/src/pages/AgentDetail.production.tsx +++ b/ui/src/pages/AgentDetail.production.tsx @@ -1,12 +1,6 @@ -import { TranscriptSequence } from "../lib/transcript-sequence"; -import { readTranscriptRequest } from "../components/transcript/read-transcript-request"; -import { useTranscriptLiveStream } from "../context/LiveUpdatesProvider"; -import { liveRunEvent, mergeRunEvents } from "../lib/live-run-event"; import { useSelectedHeartbeatRun } from "../hooks/useSelectedHeartbeatRun"; import { AgentCharacter } from "../components/AgentCharacter"; import { characterStateForAgent } from "@paperclipai/shared"; -import { mergeRunLogChunks, readChunkSeq } from "../lib/run-log-chunks"; -import { usePageVisibility } from "../lib/page-visibility"; import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { useParams, useNavigate, useLocation, Link, Navigate, useBeforeUnload, type NavigateFunction } from "@/lib/router"; import { useQuery, useMutation, useQueryClient, type QueryClient } from "@tanstack/react-query"; @@ -41,11 +35,8 @@ import { PageTabBar } from "../components/PageTabBar"; import { adapterLabels, roleLabels, help } from "../components/agent-config-primitives"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { useAdapterCapabilities } from "@/adapters/use-adapter-capabilities"; -import { redactCommandText as redactCommandSecretText } from "@paperclipai/adapter-utils"; import { MarkdownEditor } from "../components/MarkdownEditor"; import { assetsApi } from "../api/assets"; -import { toolsApi } from "../api/tools"; -import { getUIAdapter, buildTranscript, onAdapterChange } from "../adapters"; import { StatusBadge } from "../components/StatusBadge"; import { MarkdownBody } from "../components/MarkdownBody"; import { CopyText } from "../components/CopyText"; @@ -99,32 +90,23 @@ import { import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Input } from "@/components/ui/input"; -import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView"; import { AgentToolsTab } from "./AgentToolsTab"; import { AgentChannelsPanel } from "../components/chat/AgentChannelsPanel"; import { useChatConnectorsEnabled } from "@/hooks/useChatConnectorsEnabled"; -import { - LIVE_TRANSCRIPT_RENDER_LIMIT, - MAX_LIVE_EVENTS, - MAX_LIVE_LOG_LINES, -} from "../lib/live-log-buffer"; import { isUuidLike, type Agent, type AgentDetail as AgentDetailRecord, type BudgetPolicySummary, type HeartbeatRun, - type HeartbeatRunEvent, type AgentRuntimeState, type LiveEvent, - type WorkspaceOperation, isResponsibleUserDenialCode, responsibleUserLabel, } from "@paperclipai/shared"; import { ResponsibleUserDenialNotice } from "../components/ResponsibleUserDenialNotice"; import { RunWorkspaceRecoverySurface } from "../components/RunWorkspaceRecoverySurface"; import { buildPermissionsForTrustPreset, getTrustPreset } from "../lib/trust-policy-ui"; -import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils"; import { agentRouteRef } from "../lib/utils"; import { isStarred, @@ -133,6 +115,17 @@ import { useResourceMemberships, } from "../hooks/useResourceMemberships"; import { Badge } from "@/components/ui/badge"; +import { LogViewer } from "../components/run-log/LogViewer"; +import { asRecord, asNonEmptyString } from "../components/run-log/RunDetails"; + +export { LogViewer } from "../components/run-log/LogViewer"; +export { + type RunLogChunk, + shouldPollRunShellLog, + buildHeartbeatProgressLogLine, + heartbeatProgressLogLineKey, + RunInvocationCard, +} from "../components/run-log/RunDetails"; const runStatusIcons: Record = { succeeded: { icon: CheckCircle2, color: "text-green-600 dark:text-green-400" }, @@ -144,58 +137,12 @@ const runStatusIcons: Record `${entry.name}${entry.status !== "active" && entry.status !== "idle" ? ` (${entry.status})` : ""}`) .join(" -> ") ?? agent.name; } -function redactPathText(value: string, censorUsernameInLogs: boolean) { - return redactHomePathUserSegments(value, { enabled: censorUsernameInLogs }); -} - -function redactPathValue(value: T, censorUsernameInLogs: boolean): T { - return redactHomePathUserSegmentsInValue(value, { enabled: censorUsernameInLogs }); -} - -function redactCommandText(value: string, censorUsernameInLogs: boolean): string { - return redactPathText(redactCommandSecretText(value, REDACTED_ENV_VALUE), censorUsernameInLogs); -} - -function shouldRedactSecretValue(key: string, value: unknown): boolean { - if (SECRET_ENV_KEY_RE.test(key)) return true; - if (typeof value !== "string") return false; - return JWT_VALUE_RE.test(value); -} - -function redactEnvValue(key: string, value: unknown, censorUsernameInLogs: boolean): string { - if ( - typeof value === "object" && - value !== null && - !Array.isArray(value) && - (value as { type?: unknown }).type === "secret_ref" - ) { - return "***SECRET_REF***"; - } - if (shouldRedactSecretValue(key, value)) return REDACTED_ENV_VALUE; - if (value === null || value === undefined) return ""; - if (typeof value === "string" && COMMAND_ENV_KEY_RE.test(key)) return redactCommandText(value, censorUsernameInLogs); - if (typeof value === "string") return redactPathText(value, censorUsernameInLogs); - try { - return JSON.stringify(redactPathValue(value, censorUsernameInLogs)); - } catch { - return redactPathText(String(value), censorUsernameInLogs); - } -} - function isMarkdown(pathValue: string) { return pathValue.toLowerCase().endsWith(".md"); } @@ -211,19 +158,6 @@ function shouldUseMarkdownInstructionsEditor(input: { return isMarkdown(input.selectedPath); } -function formatEnvForDisplay(envValue: unknown, censorUsernameInLogs: boolean): string { - const env = asRecord(envValue); - if (!env) return ""; - - const keys = Object.keys(env); - if (keys.length === 0) return ""; - - return keys - .sort() - .map((key) => `${key}=${redactEnvValue(key, env[key], censorUsernameInLogs)}`) - .join("\n"); -} - const sourceLabels: Record = { timer: "Timer", assignment: "Assignment", @@ -231,60 +165,6 @@ const sourceLabels: Record = { automation: "Automation", }; -const LIVE_SCROLL_BOTTOM_TOLERANCE_PX = 32; -type ScrollContainer = Window | HTMLElement; - -function isWindowContainer(container: ScrollContainer): container is Window { - return container === window; -} - -function isElementScrollContainer(element: HTMLElement): boolean { - const overflowY = window.getComputedStyle(element).overflowY; - return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay"; -} - -function findScrollContainer(anchor: HTMLElement | null): ScrollContainer { - let parent = anchor?.parentElement ?? null; - while (parent) { - if (isElementScrollContainer(parent)) return parent; - parent = parent.parentElement; - } - return window; -} - -function readScrollMetrics(container: ScrollContainer): { scrollHeight: number; distanceFromBottom: number } { - if (isWindowContainer(container)) { - const pageHeight = Math.max( - document.documentElement.scrollHeight, - document.body.scrollHeight, - ); - const viewportBottom = window.scrollY + window.innerHeight; - return { - scrollHeight: pageHeight, - distanceFromBottom: Math.max(0, pageHeight - viewportBottom), - }; - } - - const viewportBottom = container.scrollTop + container.clientHeight; - return { - scrollHeight: container.scrollHeight, - distanceFromBottom: Math.max(0, container.scrollHeight - viewportBottom), - }; -} - -function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBehavior = "auto") { - if (isWindowContainer(container)) { - const pageHeight = Math.max( - document.documentElement.scrollHeight, - document.body.scrollHeight, - ); - window.scrollTo({ top: pageHeight, behavior }); - return; - } - - container.scrollTo({ top: container.scrollHeight, behavior }); -} - type AgentDetailView = "dashboard" | "instructions" | "configuration" | "secrets" | "skills" | "tools" | "channels" | "runs" | "audit" | "budget"; export const AGENT_DETAIL_TABS: ReadonlyArray<{ value: AgentDetailView; label: string }> = [ @@ -390,375 +270,12 @@ function runMetrics(run: HeartbeatRun) { }; } -export type RunLogChunk = { - seq?: number; - ts: string; - stream: "stdout" | "stderr" | "system"; - chunk: string; -}; - -function asRecord(value: unknown): Record | null { - if (typeof value !== "object" || value === null || Array.isArray(value)) return null; - return value as Record; -} - -function asNonEmptyString(value: unknown): string | null { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -export function buildHeartbeatProgressLogLine( - payload: Record, - fallbackTimestamp: string, -): RunLogChunk | null { - const message = asNonEmptyString(payload.message); - if (!message) return null; - const phase = asNonEmptyString(payload.phase); - const ts = asNonEmptyString(payload.updatedAt) ?? fallbackTimestamp; - const chunk = phase ? `[${phase}] ${message}` : message; - return { ts, stream: "system", chunk }; -} - -export function heartbeatProgressLogLineKey(line: RunLogChunk): string { - return `${line.ts}\u0000${line.stream}\u0000${line.chunk}`; -} - -export function shouldPollRunShellLog(status: HeartbeatRun["status"]): boolean { - return status === "running"; -} - export function runDetailRefetchIntervalMs(status: HeartbeatRun["status"]): 5000 | 15000 | false { if (status === "queued") return 5000; if (status === "running") return 15000; return false; } -export function RunInvocationCard({ - payload, - censorUsernameInLogs, -}: { - payload: Record; - censorUsernameInLogs: boolean; -}) { - const rawCommandLine = [ - typeof payload.command === "string" ? payload.command : null, - ...(Array.isArray(payload.commandArgs) - ? payload.commandArgs.filter((value): value is string => typeof value === "string") - : []), - ] - .filter((value): value is string => Boolean(value)) - .join(" "); - const commandLine = rawCommandLine ? redactCommandText(rawCommandLine, censorUsernameInLogs) : ""; - - const hasAdvancedDetails = - commandLine.length > 0 - || (Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0) - || payload.prompt !== undefined - || payload.context !== undefined - || payload.env !== undefined; - - return ( -
-
Invocation
- {typeof payload.adapterType === "string" && ( -
Adapter: {payload.adapterType}
- )} - {typeof payload.cwd === "string" && ( -
Working dir: {payload.cwd}
- )} - {hasAdvancedDetails && ( - - - - Details - - - {commandLine && ( -
- Command: - {commandLine} -
- )} - {Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0 && ( -
-
Command notes
-
    - {payload.commandNotes - .filter((value): value is string => typeof value === "string" && value.trim().length > 0) - .map((note, idx) => ( -
  • - {note} -
  • - ))} -
-
- )} - {payload.prompt !== undefined && ( -
-
Prompt
-
-                  {typeof payload.prompt === "string"
-                    ? redactPathText(payload.prompt, censorUsernameInLogs)
-                    : JSON.stringify(redactPathValue(payload.prompt, censorUsernameInLogs), null, 2)}
-                
-
- )} - {payload.context !== undefined && ( -
-
Context
-
-                  {JSON.stringify(redactPathValue(payload.context, censorUsernameInLogs), null, 2)}
-                
-
- )} - {payload.env !== undefined && ( -
-
Environment
-
-                  {formatEnvForDisplay(payload.env, censorUsernameInLogs)}
-                
-
- )} -
-
- )} -
- ); -} - -function parseStoredLogContent(content: string): RunLogChunk[] { - const parsed: RunLogChunk[] = []; - for (const line of content.split("\n")) { - const trimmed = line.trim(); - if (!trimmed) continue; - try { - const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown }; - const stream = - raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; - const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; - const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); - if (!chunk) continue; - parsed.push({ ts, stream, chunk }); - } catch { - // Ignore malformed log lines. - } - } - return parsed; -} - -function workspaceOperationPhaseLabel(phase: WorkspaceOperation["phase"]) { - switch (phase) { - case "worktree_prepare": - return "Worktree setup"; - case "workspace_config_freshness": - return "Config freshness"; - case "workspace_provision": - return "Provision"; - case "workspace_teardown": - return "Teardown"; - case "worktree_cleanup": - return "Worktree cleanup"; - default: - return phase; - } -} - -function workspaceOperationStatusTone(status: WorkspaceOperation["status"]) { - switch (status) { - case "succeeded": - return "border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-300"; - case "failed": - return "border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300"; - case "running": - return "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-300"; - case "skipped": - return "border-yellow-500/20 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"; - default: - return "border-border bg-muted/40 text-muted-foreground"; - } -} - -function WorkspaceOperationStatusBadge({ status }: { status: WorkspaceOperation["status"] }) { - return ( - - {status.replace("_", " ")} - - ); -} - -function WorkspaceOperationLogViewer({ - operation, - censorUsernameInLogs, -}: { - operation: WorkspaceOperation; - censorUsernameInLogs: boolean; -}) { - const [open, setOpen] = useState(false); - const { data: logData, isLoading, error } = useQuery({ - queryKey: ["workspace-operation-log", operation.id], - queryFn: () => heartbeatsApi.workspaceOperationLog(operation.id), - enabled: open && Boolean(operation.logRef), - refetchInterval: open && operation.status === "running" ? 2000 : false, - }); - - const chunks = useMemo( - () => (logData?.content ? parseStoredLogContent(logData.content) : []), - [logData?.content], - ); - - return ( -
- - {open && ( -
- {isLoading &&
Loading log...
} - {error && ( -
- {error instanceof Error ? error.message : "Failed to load workspace operation log"} -
- )} - {!isLoading && !error && chunks.length === 0 && ( -
No persisted log lines.
- )} - {chunks.length > 0 && ( -
- {chunks.map((chunk, index) => ( -
- - {new Date(chunk.ts).toLocaleTimeString("en-US", { hour12: false })} - - - [{chunk.stream}] - - {redactPathText(chunk.chunk, censorUsernameInLogs)} -
- ))} -
- )} -
- )} -
- ); -} - -function WorkspaceOperationsSection({ - operations, - censorUsernameInLogs, -}: { - operations: WorkspaceOperation[]; - censorUsernameInLogs: boolean; -}) { - if (operations.length === 0) return null; - - return ( -
-
- Workspace ({operations.length}) -
-
- {operations.map((operation) => { - const metadata = asRecord(operation.metadata); - return ( -
-
-
{workspaceOperationPhaseLabel(operation.phase)}
- -
- {relativeTime(operation.startedAt)} - {operation.finishedAt && ` to ${relativeTime(operation.finishedAt)}`} -
-
- {operation.command && ( -
- Command: - {operation.command} -
- )} - {operation.cwd && ( -
- Working dir: - {operation.cwd} -
- )} - {(asNonEmptyString(metadata?.branchName) - || asNonEmptyString(metadata?.baseRef) - || asNonEmptyString(metadata?.worktreePath) - || asNonEmptyString(metadata?.repoRoot) - || asNonEmptyString(metadata?.cleanupAction)) && ( -
- {asNonEmptyString(metadata?.branchName) && ( -
Branch: {metadata?.branchName as string}
- )} - {asNonEmptyString(metadata?.baseRef) && ( -
Base ref: {metadata?.baseRef as string}
- )} - {asNonEmptyString(metadata?.worktreePath) && ( -
Worktree: {metadata?.worktreePath as string}
- )} - {asNonEmptyString(metadata?.repoRoot) && ( -
Repo root: {metadata?.repoRoot as string}
- )} - {asNonEmptyString(metadata?.cleanupAction) && ( -
Cleanup: {metadata?.cleanupAction as string}
- )} -
- )} - {typeof metadata?.created === "boolean" && ( -
- {metadata.created ? "Created by this run" : "Reused existing workspace"} -
- )} - {operation.stderrExcerpt && operation.stderrExcerpt.trim() && ( -
-
stderr excerpt
-
-                    {redactPathText(operation.stderrExcerpt, censorUsernameInLogs)}
-                  
-
- )} - {operation.stdoutExcerpt && operation.stdoutExcerpt.trim() && ( -
-
stdout excerpt
-
-                    {redactPathText(operation.stdoutExcerpt, censorUsernameInLogs)}
-                  
-
- )} - {operation.logRef && ( - - )} -
- ); - })} -
-
- ); -} - export function AgentDetail() { const { companyPrefix, agentId, tab: urlTab, runId: urlRunId } = useParams<{ companyPrefix?: string; @@ -3734,617 +3251,6 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType, adapterConfig } ); } -/* ---- Log Viewer ---- */ - -export function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: string }) { - const { visible } = usePageVisibility(); - const [events, setEvents] = useState([]); - const [hasMoreEvents, setHasMoreEvents] = useState(false); - const [eventsError, setEventsError] = useState(null); - const hasMoreEventsRef = useRef(false); - const [loadingMoreEvents, setLoadingMoreEvents] = useState(false); - const loadMoreEventsRef = useRef<(() => void) | null>(null); - const [logLines, setLogLines] = useState([]); - const [loading, setLoading] = useState(true); - const [logLoading, setLogLoading] = useState(!!run.logRef); - const [logError, setLogError] = useState(null); - const [logOffset, setLogOffsetState] = useState(0); - const logOffsetRef = useRef(0); - const setLogOffset = useCallback((next: number | ((previous: number) => number)) => { - logOffsetRef.current = typeof next === "function" ? next(logOffsetRef.current) : next; - setLogOffsetState(logOffsetRef.current); - }, []); - const logMergeRefs = useRef({ seenChunkKeys: new Set(), trimmedSeqFloorByRun: new Map() }); - const [hasMoreLog, setHasMoreLog] = useState(false); - const [loadingMoreLog, setLoadingMoreLog] = useState(false); - const [isFollowing, setIsFollowing] = useState(false); - const [transcriptMode, setTranscriptMode] = useState("nice"); - const logEndRef = useRef(null); - const pendingLogLineRef = useRef(""); - const seenProgressLogLineKeysRef = useRef>(new Set()); - const scrollContainerRef = useRef(null); - const isFollowingRef = useRef(false); - const lastMetricsRef = useRef<{ scrollHeight: number; distanceFromBottom: number }>({ - scrollHeight: 0, - distanceFromBottom: Number.POSITIVE_INFINITY, - }); - const isLive = run.status === "running" || run.status === "queued"; - const shouldPollShellLog = shouldPollRunShellLog(run.status); - const { data: workspaceOperations = [] } = useQuery({ - queryKey: queryKeys.runWorkspaceOperations(run.id), - queryFn: () => heartbeatsApi.workspaceOperations(run.id), - refetchInterval: isLive ? 2000 : false, - }); - - function isRunLogUnavailable(err: unknown): boolean { - return err instanceof ApiError && err.status === 404; - } - - function appendLogLines(incoming: RunLogChunk[]) { - setLogLines((previous) => mergeRunLogChunks(run.id, previous, incoming.map((line) => ({ - ...line, dedupeKey: `log:${run.id}:${line.ts}:${line.stream}:${line.chunk}`, - })), logMergeRefs.current, isLive ? MAX_LIVE_LOG_LINES : Number.POSITIVE_INFINITY).chunks); - } - - function appendLogContent(content: string, finalize = false) { - if (!content && !finalize) return; - const combined = `${pendingLogLineRef.current}${content}`; - const split = combined.split("\n"); - pendingLogLineRef.current = split.pop() ?? ""; - if (finalize && pendingLogLineRef.current) { - split.push(pendingLogLineRef.current); - pendingLogLineRef.current = ""; - } - - const parsed: RunLogChunk[] = []; - for (const line of split) { - const trimmed = line.trim(); - if (!trimmed) continue; - try { - const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown; seq?: unknown }; - const stream = - raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; - const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; - const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); - if (!chunk) continue; - const seq = readChunkSeq(raw.seq); - logSequenceRef.current.confirm(seq); - parsed.push({ ts, stream, chunk, seq }); - } catch { - // ignore malformed lines - } - } - - if (parsed.length > 0) { - // Live runs stream forever, so cap the retained tail. Terminated runs are - // paginated by the user via "Load more log" and keep their full history. - appendLogLines(parsed); - } - } - - const refreshEventsRef = useRef<((force?: boolean) => void) | null>(null); - const refreshLogRef = useRef<((force?: boolean) => void) | null>(null); - const eventSequenceRef = useRef(new TranscriptSequence()); - const logSequenceRef = useRef(new TranscriptSequence()); - const eventsReadCursorRef = useRef(0); - const eventsCatchupCursorRef = useRef(0); - const retainedEventHistoryThroughRef = useRef(0); - const logIdentityRef = useRef(run.logRef ?? null); - const retiredLogRefs = useRef(new Set()); - const wasLiveRef = useRef(isLive); - const bindLog = (ref: unknown): "same" | "changed" | "stale" => { - if (typeof ref !== "string" || !ref || ref === logIdentityRef.current) return "same"; - if (retiredLogRefs.current.has(ref)) return "stale"; - const prior = logIdentityRef.current; - logIdentityRef.current = ref; - if (!prior) return "same"; - retiredLogRefs.current.add(prior); - if (retiredLogRefs.current.size > 8) retiredLogRefs.current.delete(retiredLogRefs.current.values().next().value!); - pendingLogLineRef.current = ""; - logMergeRefs.current = { seenChunkKeys: new Set(), trimmedSeqFloorByRun: new Map() }; - logSequenceRef.current = new TranscriptSequence(); - setLogLines([]); setLogOffset(0); - return "changed"; - }; - useEffect(() => { - eventsReadCursorRef.current = 0; eventsCatchupCursorRef.current = 0; retainedEventHistoryThroughRef.current = 0; eventSequenceRef.current = new TranscriptSequence(); logSequenceRef.current = new TranscriptSequence(); - wasLiveRef.current = isLive; - logIdentityRef.current = run.logRef ?? null; retiredLogRefs.current.clear(); - hasMoreEventsRef.current = false; setHasMoreEvents(false); setLoadingMoreEvents(false); setEventsError(null); - setEvents([]); setLoading(true); - }, [run.id, run.companyId]); - useEffect(() => { if (isLive) wasLiveRef.current = true; }, [isLive]); - - function appendEvents(rows: HeartbeatRunEvent[], retainHistory = false) { - if (retainHistory) retainedEventHistoryThroughRef.current = Math.max( - retainedEventHistoryThroughRef.current, rows.at(-1)?.seq ?? 0, - ); - setEvents(previous => { - const merged = mergeRunEvents(previous, rows); - if (!isLive && !wasLiveRef.current) return merged; - const retainedThrough = Math.max(200, retainedEventHistoryThroughRef.current); - const prefix = merged.filter(row => row.seq <= retainedThrough); - const tail = merged.filter(row => row.seq > retainedThrough); - return [...prefix, ...tail.slice(-MAX_LIVE_EVENTS)]; - }); - } - - const stream = useTranscriptLiveStream(isLive ? [run.id] : [], { companyId: run.companyId, enabled: visible }, event => { - if (event.companyId !== run.companyId) return; - const payload = asRecord(event.payload); - if (!payload || payload.runId !== run.id) return; - if (event.type === "heartbeat.run.log") { - const binding = bindLog(payload.logRef); - if (binding === "stale") return; - const seq = readChunkSeq(payload.seq); - if (payload.truncated === true) logSequenceRef.current.requireHistory(seq); - const gap = logSequenceRef.current.observe(seq); - if (binding === "changed" || payload.truncated === true || gap) refreshLogRef.current?.(); - if (payload.truncated === true) return; - const chunk = typeof payload.chunk === "string" ? payload.chunk : ""; - if (!chunk) return; - const channel = payload.stream === "stderr" || payload.stream === "system" ? payload.stream : "stdout"; - appendLogLines([{ ts: asNonEmptyString(payload.ts) ?? event.createdAt, stream: channel, chunk, seq }]); - return; - } - if (event.type === "heartbeat.run.progress") { - const line = buildHeartbeatProgressLogLine(payload, event.createdAt); - if (!line) return; - const key = heartbeatProgressLogLineKey(line); - if (seenProgressLogLineKeysRef.current.has(key)) return; - seenProgressLogLineKeysRef.current.add(key); appendLogLines([line]); - return; - } - if (event.type === "heartbeat.run.status") { - refreshEventsRef.current?.(true); refreshLogRef.current?.(true); return; - } - const row = liveRunEvent(event); - if (!row) return; - const commitSignal = row.message === null && row.payload === null; - if (commitSignal) eventSequenceRef.current.requireHistory(row.seq); - if (eventSequenceRef.current.observe(row.seq)) refreshEventsRef.current?.(); - if (commitSignal) return; - appendEvents([row]); - }); - - useEffect(() => { - setLoadingMoreEvents(false); - if (!visible) return; - let cancelled = false; - let timer: number | undefined; - const abort = new AbortController(); - let reading = false; - let pendingRefresh = false; - let failedRead: { requested: boolean; catchup: boolean } | null = null; - const read = async (requested = false, catchup = false) => { - if (cancelled) return; - if (reading) { if (catchup) pendingRefresh = true; return; } - if (hasMoreEventsRef.current && !requested && !catchup) return; - reading = true; pendingRefresh = false; setLoadingMoreEvents(true); setEventsError(null); - window.clearTimeout(timer); timer = undefined; - let failed = false; - try { - const cursor = catchup ? Math.max(eventsCatchupCursorRef.current, eventsReadCursorRef.current) : eventsReadCursorRef.current; - const initializesHistory = eventsReadCursorRef.current === 0; - const rows = await readTranscriptRequest(signal => heartbeatsApi.events(run.id, cursor, 1000, { signal }), abort.signal); - if (cancelled) return; - appendEvents(rows, !catchup || initializesHistory); - const next = Math.max(cursor, rows.at(-1)?.seq ?? cursor); - eventsCatchupCursorRef.current = Math.max(eventsCatchupCursorRef.current, next); - eventSequenceRef.current.confirm(next); - if (catchup) { - if (rows.length === 1000 && next > cursor) pendingRefresh = true; - } - if (!catchup || initializesHistory) { - eventsReadCursorRef.current = next; - hasMoreEventsRef.current = rows.length === 1000 && next > cursor; - setHasMoreEvents(hasMoreEventsRef.current); - } - failed = eventSequenceRef.current.needsCatchup; - failedRead = null; - } catch { - failed = true; failedRead = { requested, catchup }; - if (!cancelled) setEventsError("Could not load events. Try again."); - } - if (cancelled) return; - reading = false; setLoadingMoreEvents(false); setLoading(false); - if (pendingRefresh) void read(false, true); - else if (failed || (isLive && !stream.healthy && !hasMoreEventsRef.current)) { - timer = window.setTimeout(() => void read(requested, catchup || eventSequenceRef.current.needsCatchup), 2000); - } - }; - refreshEventsRef.current = () => { void read(false, wasLiveRef.current); }; - loadMoreEventsRef.current = () => { void read(failedRead?.requested ?? true, failedRead?.catchup ?? false); }; - void read(false, wasLiveRef.current && (stream.healthy || eventsReadCursorRef.current > 0)); - return () => { - cancelled = true; refreshEventsRef.current = null; loadMoreEventsRef.current = null; - abort.abort(); if (timer !== undefined) window.clearTimeout(timer); - }; - }, [visible, run.id, run.companyId, run.status, stream.healthy, stream.epoch]); - - const getScrollContainer = useCallback((): ScrollContainer => { - if (scrollContainerRef.current) return scrollContainerRef.current; - const container = findScrollContainer(logEndRef.current); - scrollContainerRef.current = container; - return container; - }, []); - - const updateFollowingState = useCallback(() => { - const container = getScrollContainer(); - const metrics = readScrollMetrics(container); - lastMetricsRef.current = metrics; - const nearBottom = metrics.distanceFromBottom <= LIVE_SCROLL_BOTTOM_TOLERANCE_PX; - isFollowingRef.current = nearBottom; - setIsFollowing((prev) => (prev === nearBottom ? prev : nearBottom)); - }, [getScrollContainer]); - - useEffect(() => { - scrollContainerRef.current = null; - lastMetricsRef.current = { - scrollHeight: 0, - distanceFromBottom: Number.POSITIVE_INFINITY, - }; - - if (!isLive) { - isFollowingRef.current = false; - setIsFollowing(false); - return; - } - - updateFollowingState(); - }, [isLive, run.id, updateFollowingState]); - - useEffect(() => { - if (!isLive) return; - const container = getScrollContainer(); - updateFollowingState(); - - if (container === window) { - window.addEventListener("scroll", updateFollowingState, { passive: true }); - } else { - container.addEventListener("scroll", updateFollowingState, { passive: true }); - } - window.addEventListener("resize", updateFollowingState); - return () => { - if (container === window) { - window.removeEventListener("scroll", updateFollowingState); - } else { - container.removeEventListener("scroll", updateFollowingState); - } - window.removeEventListener("resize", updateFollowingState); - }; - }, [isLive, run.id, getScrollContainer, updateFollowingState]); - - // Auto-scroll only for live runs when following - useEffect(() => { - if (!isLive || !isFollowingRef.current) return; - - const container = getScrollContainer(); - const previous = lastMetricsRef.current; - const current = readScrollMetrics(container); - const growth = Math.max(0, current.scrollHeight - previous.scrollHeight); - const expectedDistance = previous.distanceFromBottom + growth; - const movedAwayBy = current.distanceFromBottom - expectedDistance; - - // If user moved away from bottom between updates, release auto-follow immediately. - if (movedAwayBy > LIVE_SCROLL_BOTTOM_TOLERANCE_PX) { - isFollowingRef.current = false; - setIsFollowing(false); - lastMetricsRef.current = current; - return; - } - - scrollToContainerBottom(container, "auto"); - const after = readScrollMetrics(container); - lastMetricsRef.current = after; - if (!isFollowingRef.current) { - isFollowingRef.current = true; - } - setIsFollowing((prev) => (prev ? prev : true)); - }, [events.length, logLines.length, isLive, getScrollContainer]); - - // Reset only when the log source changes, never when visibility changes. - useEffect(() => { - pendingLogLineRef.current = ""; - logMergeRefs.current = { seenChunkKeys: new Set(), trimmedSeqFloorByRun: new Map() }; - seenProgressLogLineKeysRef.current = new Set(); - setLogLines([]); - setLogOffset(0); - setHasMoreLog(false); - setLoadingMoreLog(false); - setLogError(null); - }, [run.id, run.logRef, setLogOffset]); - - useEffect(() => { - if (!visible) return; - if (!run.logRef && !shouldPollShellLog) { setLogLoading(false); return; } - let cancelled = false; - let timer: number | undefined; - const abort = new AbortController(); - setLogLoading(true); - let reading = false; - let pendingRefresh = false; - const load = async () => { - if (cancelled || reading) return; - reading = true; pendingRefresh = false; - window.clearTimeout(timer); timer = undefined; - let failed = false; - try { - for (;;) { - const offset = logOffsetRef.current; - const result = await readTranscriptRequest(signal => heartbeatsApi.log(run.id, offset, RUN_LOG_PAGE_BYTES, { signal }), abort.signal); - if (cancelled) return; - const binding = bindLog(result.logRef); - if (binding === "stale") { failed = true; break; } - if (binding === "changed" && offset !== 0) continue; - appendLogContent(result.content, result.nextOffset === undefined); - const next = result.nextOffset ?? offset + new TextEncoder().encode(result.content).byteLength; - setLogOffset(next); setLogError(null); - setHasMoreLog(!wasLiveRef.current && result.nextOffset !== undefined && !!result.content); - if (!wasLiveRef.current || result.nextOffset === undefined || !result.content || next <= offset) break; - } - failed ||= logSequenceRef.current.needsCatchup; - } catch (err) { - if (cancelled) return; - failed = shouldPollShellLog || !isRunLogUnavailable(err); - if (!isRunLogUnavailable(err)) setLogError(err instanceof Error ? err.message : "Failed to load run log"); - } - if (cancelled) return; - reading = false; - setLogLoading(false); - if (pendingRefresh) void load(); - else if (failed || (shouldPollShellLog && !stream.healthy)) timer = window.setTimeout(() => void load(), 2000); - }; - refreshLogRef.current = (force = false) => { if (force) pendingRefresh = true; void load(); }; - void load(); - return () => { cancelled = true; refreshLogRef.current = null; abort.abort(); if (timer !== undefined) window.clearTimeout(timer); }; - }, [visible, run.id, run.companyId, run.logRef, isLive ? null : run.logBytes, shouldPollShellLog, stream.healthy, stream.epoch]); - - async function loadMorePersistedLog() { - if (loadingMoreLog || !hasMoreLog) return; - setLoadingMoreLog(true); - setLogError(null); - try { - const result = await heartbeatsApi.log(run.id, logOffset, RUN_LOG_PAGE_BYTES); - appendLogContent(result.content, result.nextOffset === undefined); - const next = result.nextOffset ?? logOffset + new TextEncoder().encode(result.content).byteLength; - setLogOffset(next); - setHasMoreLog(result.nextOffset !== undefined); - } catch (err) { - setLogError(err instanceof Error ? err.message : "Failed to load more run log"); - } finally { - setLoadingMoreLog(false); - } - } - - const censorUsernameInLogs = useQuery({ - queryKey: queryKeys.instance.generalSettings, - queryFn: () => instanceSettingsApi.getGeneral(), - }).data?.censorUsernameInLogs === true; - - const adapterInvokePayload = useMemo(() => { - const evt = events.find((e) => e.eventType === "adapter.invoke"); - return redactPathValue(asRecord(evt?.payload ?? null), censorUsernameInLogs); - }, [censorUsernameInLogs, events]); - - // NOTE: adapter is NOT memoized because external adapters replace their - // parseStdoutLine asynchronously after dynamic parser loading. Memoizing - // on adapterType alone would stale the transcript with the fallback parser. - // We subscribe to adapter registry changes to force transcript recomputation. - const [parserTick, setParserTick] = useState(0); - const adapter = getUIAdapter(adapterType); - - useEffect(() => { - return onAdapterChange(() => setParserTick((t) => t + 1)); - }, []); - - const transcript = useMemo( - () => buildTranscript(logLines, adapter, { censorUsernameInLogs }), - [adapter, censorUsernameInLogs, logLines, parserTick], - ); - const toolDecisionLookup = useQuery({ - queryKey: queryKeys.tools.runDecisions(run.companyId, run.id), - queryFn: () => toolsApi.getRunDecisionLookup(run.companyId, run.id), - enabled: Boolean(run.companyId && run.id), - refetchInterval: isLive ? 3000 : false, - staleTime: isLive ? 1000 : 30_000, - }); - - useEffect(() => { - setTranscriptMode("nice"); - }, [run.id]); - - if (loading && logLoading) { - return

Loading run logs...

; - } - - if (events.length === 0 && logLines.length === 0 && !logError && !eventsError) { - return

No log events.

; - } - - const levelColors: Record = { - info: "text-foreground", - warn: "text-yellow-600 dark:text-yellow-400", - error: "text-red-600 dark:text-red-400", - }; - - const streamColors: Record = { - stdout: "text-foreground", - stderr: "text-red-600 dark:text-red-300", - system: "text-blue-600 dark:text-blue-300", - }; - - return ( -
- - {adapterInvokePayload && ( - - )} - -
- - Transcript ({transcript.length}) - -
-
- {(["nice", "raw"] as const).map((mode) => ( - - ))} -
- {isLive && !isFollowing && ( - - )} - {isLive && ( - - - - - - Live - - )} -
-
-
- - {hasMoreLog && ( -
- - - Showing the first {Math.round(logOffset / 1024).toLocaleString("en-US")} KB - {typeof run.logBytes === "number" && run.logBytes > 0 - ? ` of ${Math.round(run.logBytes / 1024).toLocaleString("en-US")} KB` - : ""} - -
- )} - {logError && ( -
- {logError} -
- )} -
-
- - {(run.status === "failed" || run.status === "timed_out") && ( -
-
Failure details
- {run.error && ( -
- Error: - {redactPathText(run.error, censorUsernameInLogs)} -
- )} - {run.stderrExcerpt && run.stderrExcerpt.trim() && ( -
-
stderr excerpt
-
-                {redactPathText(run.stderrExcerpt, censorUsernameInLogs)}
-              
-
- )} - {run.resultJson && ( -
-
adapter result JSON
-
-                {JSON.stringify(redactPathValue(run.resultJson, censorUsernameInLogs), null, 2)}
-              
-
- )} - {run.stdoutExcerpt && run.stdoutExcerpt.trim() && !run.resultJson && ( -
-
stdout excerpt
-
-                {redactPathText(run.stdoutExcerpt, censorUsernameInLogs)}
-              
-
- )} -
- )} - - {(events.length > 0 || eventsError) && ( -
-
Events ({events.length})
- {eventsError &&

{eventsError}

} - {(hasMoreEvents || eventsError) && ( - - )} -
- {events.map((evt) => { - const color = evt.color - ?? (evt.level ? levelColors[evt.level] : null) - ?? (evt.stream ? streamColors[evt.stream] : null) - ?? "text-foreground"; - - return ( -
- - {new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })} - - - {evt.stream ? `[${evt.stream}]` : ""} - - - {evt.message - ? redactPathText(evt.message, censorUsernameInLogs) - : evt.payload - ? JSON.stringify(redactPathValue(evt.payload, censorUsernameInLogs)) - : ""} - -
- ); - })} -
-
- )} -
- ); -} - /* ---- Keys Tab ---- */ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }) { diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 0fc95f3d046..c0355d59429 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -1,7 +1,3 @@ -import { TranscriptSequence } from "../lib/transcript-sequence"; -import { readTranscriptRequest } from "../components/transcript/read-transcript-request"; -import { useTranscriptLiveStream } from "../context/LiveUpdatesProvider"; -import { liveRunEvent, mergeRunEvents } from "../lib/live-run-event"; import { useSelectedHeartbeatRun } from "../hooks/useSelectedHeartbeatRun"; import { grokBotApi } from "@/api/grokBot"; import { GrokBotWorkSummary } from "@/components/GrokBotWorkSummary"; @@ -12,8 +8,6 @@ import type { AgentInstructionCandidate, AgentInstructionsBundle } from "@paperc import { InstructionHistory } from "../components/InstructionHistory"; import { AgentCharacter } from "../components/AgentCharacter"; import { characterStateForAgent } from "@paperclipai/shared"; -import { mergeRunLogChunks, readChunkSeq } from "../lib/run-log-chunks"; -import { usePageVisibility } from "../lib/page-visibility"; import { lazy, Suspense, useCallback, useEffect, useMemo, useState, useRef } from "react"; import { useParams, useNavigate, useLocation, Link, Navigate, useBeforeUnload, type NavigateFunction } from "@/lib/router"; import { useQuery, useMutation, useQueryClient, type QueryClient } from "@tanstack/react-query"; @@ -45,11 +39,8 @@ import { agentDisplayAdapterType, getAdapterDisplay } from "../adapters/adapter- import { adapterLabels, roleLabels, help } from "../components/agent-config-primitives"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { useAdapterCapabilities } from "@/adapters/use-adapter-capabilities"; -import { redactCommandText as redactCommandSecretText } from "@paperclipai/adapter-utils"; import { MarkdownEditor } from "../components/MarkdownEditor"; import { assetsApi } from "../api/assets"; -import { toolsApi } from "../api/tools"; -import { getUIAdapter, buildTranscript, onAdapterChange } from "../adapters"; import { StatusBadge } from "../components/StatusBadge"; import { AgentLifecycleStatus, useAgentLifecycleStatus } from "../components/AgentLifecycleStatus"; import { MarkdownBody } from "../components/MarkdownBody"; @@ -106,24 +97,16 @@ import { import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Input } from "@/components/ui/input"; -import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView"; import { AgentToolsTab } from "./AgentToolsTab"; import { AgentChannelsPanel } from "../components/chat/AgentChannelsPanel"; -import { - LIVE_TRANSCRIPT_RENDER_LIMIT, - MAX_LIVE_EVENTS, - MAX_LIVE_LOG_LINES, -} from "../lib/live-log-buffer"; import { isUuidLike, type Agent, type AgentDetail as AgentDetailRecord, type HeartbeatRun, - type HeartbeatRunEvent, type AgentRuntimeState, type Issue, type LiveEvent, - type WorkspaceOperation, isResponsibleUserDenialCode, responsibleUserLabel, } from "@paperclipai/shared"; @@ -136,7 +119,6 @@ import { runRequestedProviderTrace, } from "../components/ProviderTraceStatusBadge"; import { buildPermissionsForTrustPreset, getTrustPreset } from "../lib/trust-policy-ui"; -import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils"; import { agentRouteRef } from "../lib/utils"; import { humanAgentCreatorUserId, resolveAgentCreatorLabel } from "../lib/agent-creator"; import { @@ -154,6 +136,17 @@ import { parseAgentDetailView, type AgentDetailView, } from "./agent-detail-navigation"; +import { LogViewer } from "../components/run-log/LogViewer"; +import { asRecord, asNonEmptyString } from "../components/run-log/RunDetails"; + +export { LogViewer } from "../components/run-log/LogViewer"; +export { + type RunLogChunk, + shouldPollRunShellLog, + buildHeartbeatProgressLogLine, + heartbeatProgressLogLineKey, + RunInvocationCard, +} from "../components/run-log/RunDetails"; const AgentAdvancedTab = lazy(() => import("./AgentAdvancedTab").then(module => ({ default: module.AgentAdvancedTab }))); @@ -167,58 +160,12 @@ const runStatusIcons: Record `${entry.name}${entry.status !== "active" && entry.status !== "idle" ? ` (${entry.status})` : ""}`) .join(" -> ") ?? agent.name; } -function redactPathText(value: string, censorUsernameInLogs: boolean) { - return redactHomePathUserSegments(value, { enabled: censorUsernameInLogs }); -} - -function redactPathValue(value: T, censorUsernameInLogs: boolean): T { - return redactHomePathUserSegmentsInValue(value, { enabled: censorUsernameInLogs }); -} - -function redactCommandText(value: string, censorUsernameInLogs: boolean): string { - return redactPathText(redactCommandSecretText(value, REDACTED_ENV_VALUE), censorUsernameInLogs); -} - -function shouldRedactSecretValue(key: string, value: unknown): boolean { - if (SECRET_ENV_KEY_RE.test(key)) return true; - if (typeof value !== "string") return false; - return JWT_VALUE_RE.test(value); -} - -function redactEnvValue(key: string, value: unknown, censorUsernameInLogs: boolean): string { - if ( - typeof value === "object" && - value !== null && - !Array.isArray(value) && - (value as { type?: unknown }).type === "secret_ref" - ) { - return "***SECRET_REF***"; - } - if (shouldRedactSecretValue(key, value)) return REDACTED_ENV_VALUE; - if (value === null || value === undefined) return ""; - if (typeof value === "string" && COMMAND_ENV_KEY_RE.test(key)) return redactCommandText(value, censorUsernameInLogs); - if (typeof value === "string") return redactPathText(value, censorUsernameInLogs); - try { - return JSON.stringify(redactPathValue(value, censorUsernameInLogs)); - } catch { - return redactPathText(String(value), censorUsernameInLogs); - } -} - function isMarkdown(pathValue: string) { return pathValue.toLowerCase().endsWith(".md"); } @@ -234,19 +181,6 @@ function shouldUseMarkdownInstructionsEditor(input: { return isMarkdown(input.selectedPath); } -function formatEnvForDisplay(envValue: unknown, censorUsernameInLogs: boolean): string { - const env = asRecord(envValue); - if (!env) return ""; - - const keys = Object.keys(env); - if (keys.length === 0) return ""; - - return keys - .sort() - .map((key) => `${key}=${redactEnvValue(key, env[key], censorUsernameInLogs)}`) - .join("\n"); -} - const sourceLabels: Record = { timer: "Timer", assignment: "Assignment", @@ -254,60 +188,6 @@ const sourceLabels: Record = { automation: "Automation", }; -const LIVE_SCROLL_BOTTOM_TOLERANCE_PX = 32; -type ScrollContainer = Window | HTMLElement; - -function isWindowContainer(container: ScrollContainer): container is Window { - return container === window; -} - -function isElementScrollContainer(element: HTMLElement): boolean { - const overflowY = window.getComputedStyle(element).overflowY; - return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay"; -} - -function findScrollContainer(anchor: HTMLElement | null): ScrollContainer { - let parent = anchor?.parentElement ?? null; - while (parent) { - if (isElementScrollContainer(parent)) return parent; - parent = parent.parentElement; - } - return window; -} - -function readScrollMetrics(container: ScrollContainer): { scrollHeight: number; distanceFromBottom: number } { - if (isWindowContainer(container)) { - const pageHeight = Math.max( - document.documentElement.scrollHeight, - document.body.scrollHeight, - ); - const viewportBottom = window.scrollY + window.innerHeight; - return { - scrollHeight: pageHeight, - distanceFromBottom: Math.max(0, pageHeight - viewportBottom), - }; - } - - const viewportBottom = container.scrollTop + container.clientHeight; - return { - scrollHeight: container.scrollHeight, - distanceFromBottom: Math.max(0, container.scrollHeight - viewportBottom), - }; -} - -function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBehavior = "auto") { - if (isWindowContainer(container)) { - const pageHeight = Math.max( - document.documentElement.scrollHeight, - document.body.scrollHeight, - ); - window.scrollTo({ top: pageHeight, behavior }); - return; - } - - container.scrollTo({ top: container.scrollHeight, behavior }); -} - /** @deprecated Use AGENT_DETAIL_NAVIGATION for contextual navigation. */ export const AGENT_DETAIL_TABS = AGENT_DETAIL_NAVIGATION.flatMap((section) => section.items); @@ -404,375 +284,12 @@ function runMetrics(run: HeartbeatRun) { }; } -export type RunLogChunk = { - seq?: number; - ts: string; - stream: "stdout" | "stderr" | "system"; - chunk: string; -}; - -function asRecord(value: unknown): Record | null { - if (typeof value !== "object" || value === null || Array.isArray(value)) return null; - return value as Record; -} - -function asNonEmptyString(value: unknown): string | null { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -export function buildHeartbeatProgressLogLine( - payload: Record, - fallbackTimestamp: string, -): RunLogChunk | null { - const message = asNonEmptyString(payload.message); - if (!message) return null; - const phase = asNonEmptyString(payload.phase); - const ts = asNonEmptyString(payload.updatedAt) ?? fallbackTimestamp; - const chunk = phase ? `[${phase}] ${message}` : message; - return { ts, stream: "system", chunk }; -} - -export function heartbeatProgressLogLineKey(line: RunLogChunk): string { - return `${line.ts}\u0000${line.stream}\u0000${line.chunk}`; -} - -export function shouldPollRunShellLog(status: HeartbeatRun["status"]): boolean { - return status === "running"; -} - export function runDetailRefetchIntervalMs(status: HeartbeatRun["status"]): 5000 | 15000 | false { if (status === "queued") return 5000; if (status === "running") return 15000; return false; } -export function RunInvocationCard({ - payload, - censorUsernameInLogs, -}: { - payload: Record; - censorUsernameInLogs: boolean; -}) { - const rawCommandLine = [ - typeof payload.command === "string" ? payload.command : null, - ...(Array.isArray(payload.commandArgs) - ? payload.commandArgs.filter((value): value is string => typeof value === "string") - : []), - ] - .filter((value): value is string => Boolean(value)) - .join(" "); - const commandLine = rawCommandLine ? redactCommandText(rawCommandLine, censorUsernameInLogs) : ""; - - const hasAdvancedDetails = - commandLine.length > 0 - || (Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0) - || payload.prompt !== undefined - || payload.context !== undefined - || payload.env !== undefined; - - return ( -
-
Invocation
- {typeof payload.adapterType === "string" && ( -
Adapter: {payload.adapterType}
- )} - {typeof payload.cwd === "string" && ( -
Working dir: {payload.cwd}
- )} - {hasAdvancedDetails && ( - - - - Details - - - {commandLine && ( -
- Command: - {commandLine} -
- )} - {Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0 && ( -
-
Command notes
-
    - {payload.commandNotes - .filter((value): value is string => typeof value === "string" && value.trim().length > 0) - .map((note, idx) => ( -
  • - {note} -
  • - ))} -
-
- )} - {payload.prompt !== undefined && ( -
-
Prompt
-
-                  {typeof payload.prompt === "string"
-                    ? redactPathText(payload.prompt, censorUsernameInLogs)
-                    : JSON.stringify(redactPathValue(payload.prompt, censorUsernameInLogs), null, 2)}
-                
-
- )} - {payload.context !== undefined && ( -
-
Context
-
-                  {JSON.stringify(redactPathValue(payload.context, censorUsernameInLogs), null, 2)}
-                
-
- )} - {payload.env !== undefined && ( -
-
Environment
-
-                  {formatEnvForDisplay(payload.env, censorUsernameInLogs)}
-                
-
- )} -
-
- )} -
- ); -} - -function parseStoredLogContent(content: string): RunLogChunk[] { - const parsed: RunLogChunk[] = []; - for (const line of content.split("\n")) { - const trimmed = line.trim(); - if (!trimmed) continue; - try { - const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown }; - const stream = - raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; - const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; - const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); - if (!chunk) continue; - parsed.push({ ts, stream, chunk }); - } catch { - // Ignore malformed log lines. - } - } - return parsed; -} - -function workspaceOperationPhaseLabel(phase: WorkspaceOperation["phase"]) { - switch (phase) { - case "worktree_prepare": - return "Worktree setup"; - case "workspace_config_freshness": - return "Config freshness"; - case "workspace_provision": - return "Provision"; - case "workspace_teardown": - return "Teardown"; - case "worktree_cleanup": - return "Worktree cleanup"; - default: - return phase; - } -} - -function workspaceOperationStatusTone(status: WorkspaceOperation["status"]) { - switch (status) { - case "succeeded": - return "border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-300"; - case "failed": - return "border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300"; - case "running": - return "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-300"; - case "skipped": - return "border-yellow-500/20 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"; - default: - return "border-border bg-muted/40 text-muted-foreground"; - } -} - -function WorkspaceOperationStatusBadge({ status }: { status: WorkspaceOperation["status"] }) { - return ( - - {status.replace("_", " ")} - - ); -} - -function WorkspaceOperationLogViewer({ - operation, - censorUsernameInLogs, -}: { - operation: WorkspaceOperation; - censorUsernameInLogs: boolean; -}) { - const [open, setOpen] = useState(false); - const { data: logData, isLoading, error } = useQuery({ - queryKey: ["workspace-operation-log", operation.id], - queryFn: () => heartbeatsApi.workspaceOperationLog(operation.id), - enabled: open && Boolean(operation.logRef), - refetchInterval: open && operation.status === "running" ? 2000 : false, - }); - - const chunks = useMemo( - () => (logData?.content ? parseStoredLogContent(logData.content) : []), - [logData?.content], - ); - - return ( -
- - {open && ( -
- {isLoading &&
Loading log...
} - {error && ( -
- {error instanceof Error ? error.message : "Failed to load workspace operation log"} -
- )} - {!isLoading && !error && chunks.length === 0 && ( -
No persisted log lines.
- )} - {chunks.length > 0 && ( -
- {chunks.map((chunk, index) => ( -
- - {new Date(chunk.ts).toLocaleTimeString("en-US", { hour12: false })} - - - [{chunk.stream}] - - {redactPathText(chunk.chunk, censorUsernameInLogs)} -
- ))} -
- )} -
- )} -
- ); -} - -function WorkspaceOperationsSection({ - operations, - censorUsernameInLogs, -}: { - operations: WorkspaceOperation[]; - censorUsernameInLogs: boolean; -}) { - if (operations.length === 0) return null; - - return ( -
-
- Workspace ({operations.length}) -
-
- {operations.map((operation) => { - const metadata = asRecord(operation.metadata); - return ( -
-
-
{workspaceOperationPhaseLabel(operation.phase)}
- -
- {relativeTime(operation.startedAt)} - {operation.finishedAt && ` to ${relativeTime(operation.finishedAt)}`} -
-
- {operation.command && ( -
- Command: - {operation.command} -
- )} - {operation.cwd && ( -
- Working dir: - {operation.cwd} -
- )} - {(asNonEmptyString(metadata?.branchName) - || asNonEmptyString(metadata?.baseRef) - || asNonEmptyString(metadata?.worktreePath) - || asNonEmptyString(metadata?.repoRoot) - || asNonEmptyString(metadata?.cleanupAction)) && ( -
- {asNonEmptyString(metadata?.branchName) && ( -
Branch: {metadata?.branchName as string}
- )} - {asNonEmptyString(metadata?.baseRef) && ( -
Base ref: {metadata?.baseRef as string}
- )} - {asNonEmptyString(metadata?.worktreePath) && ( -
Worktree: {metadata?.worktreePath as string}
- )} - {asNonEmptyString(metadata?.repoRoot) && ( -
Repo root: {metadata?.repoRoot as string}
- )} - {asNonEmptyString(metadata?.cleanupAction) && ( -
Cleanup: {metadata?.cleanupAction as string}
- )} -
- )} - {typeof metadata?.created === "boolean" && ( -
- {metadata.created ? "Created by this run" : "Reused existing workspace"} -
- )} - {operation.stderrExcerpt && operation.stderrExcerpt.trim() && ( -
-
stderr excerpt
-
-                    {redactPathText(operation.stderrExcerpt, censorUsernameInLogs)}
-                  
-
- )} - {operation.stdoutExcerpt && operation.stdoutExcerpt.trim() && ( -
-
stdout excerpt
-
-                    {redactPathText(operation.stdoutExcerpt, censorUsernameInLogs)}
-                  
-
- )} - {operation.logRef && ( - - )} -
- ); - })} -
-
- ); -} - export function AgentDetail() { const { companyPrefix, agentId, tab: urlTab, runId: urlRunId } = useParams<{ companyPrefix?: string; @@ -1392,7 +909,6 @@ export function AgentDetail() { /> )} - {actionError &&

{actionError}

} {isPendingApproval && (
@@ -2142,7 +1658,6 @@ export function ConfigurationTab({ canConfigureProviderTrace={canConfigureProviderTrace} /> : null} - {content === "permissions" ? } permissions={agent.permissions} @@ -4021,617 +3536,6 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType, adapterConfig } ); } -/* ---- Log Viewer ---- */ - -export function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: string }) { - const { visible } = usePageVisibility(); - const [events, setEvents] = useState([]); - const [hasMoreEvents, setHasMoreEvents] = useState(false); - const [eventsError, setEventsError] = useState(null); - const hasMoreEventsRef = useRef(false); - const [loadingMoreEvents, setLoadingMoreEvents] = useState(false); - const loadMoreEventsRef = useRef<(() => void) | null>(null); - const [logLines, setLogLines] = useState([]); - const [loading, setLoading] = useState(true); - const [logLoading, setLogLoading] = useState(!!run.logRef); - const [logError, setLogError] = useState(null); - const [logOffset, setLogOffsetState] = useState(0); - const logOffsetRef = useRef(0); - const setLogOffset = useCallback((next: number | ((previous: number) => number)) => { - logOffsetRef.current = typeof next === "function" ? next(logOffsetRef.current) : next; - setLogOffsetState(logOffsetRef.current); - }, []); - const logMergeRefs = useRef({ seenChunkKeys: new Set(), trimmedSeqFloorByRun: new Map() }); - const [hasMoreLog, setHasMoreLog] = useState(false); - const [loadingMoreLog, setLoadingMoreLog] = useState(false); - const [isFollowing, setIsFollowing] = useState(false); - const [transcriptMode, setTranscriptMode] = useState("nice"); - const logEndRef = useRef(null); - const pendingLogLineRef = useRef(""); - const seenProgressLogLineKeysRef = useRef>(new Set()); - const scrollContainerRef = useRef(null); - const isFollowingRef = useRef(false); - const lastMetricsRef = useRef<{ scrollHeight: number; distanceFromBottom: number }>({ - scrollHeight: 0, - distanceFromBottom: Number.POSITIVE_INFINITY, - }); - const isLive = run.status === "running" || run.status === "queued"; - const shouldPollShellLog = shouldPollRunShellLog(run.status); - const { data: workspaceOperations = [] } = useQuery({ - queryKey: queryKeys.runWorkspaceOperations(run.id), - queryFn: () => heartbeatsApi.workspaceOperations(run.id), - refetchInterval: isLive ? 2000 : false, - }); - - function isRunLogUnavailable(err: unknown): boolean { - return err instanceof ApiError && err.status === 404; - } - - function appendLogLines(incoming: RunLogChunk[]) { - setLogLines((previous) => mergeRunLogChunks(run.id, previous, incoming.map((line) => ({ - ...line, dedupeKey: `log:${run.id}:${line.ts}:${line.stream}:${line.chunk}`, - })), logMergeRefs.current, isLive ? MAX_LIVE_LOG_LINES : Number.POSITIVE_INFINITY).chunks); - } - - function appendLogContent(content: string, finalize = false) { - if (!content && !finalize) return; - const combined = `${pendingLogLineRef.current}${content}`; - const split = combined.split("\n"); - pendingLogLineRef.current = split.pop() ?? ""; - if (finalize && pendingLogLineRef.current) { - split.push(pendingLogLineRef.current); - pendingLogLineRef.current = ""; - } - - const parsed: RunLogChunk[] = []; - for (const line of split) { - const trimmed = line.trim(); - if (!trimmed) continue; - try { - const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown; seq?: unknown }; - const stream = - raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; - const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; - const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); - if (!chunk) continue; - const seq = readChunkSeq(raw.seq); - logSequenceRef.current.confirm(seq); - parsed.push({ ts, stream, chunk, seq }); - } catch { - // ignore malformed lines - } - } - - if (parsed.length > 0) { - // Live runs stream forever, so cap the retained tail. Terminated runs are - // paginated by the user via "Load more log" and keep their full history. - appendLogLines(parsed); - } - } - - const refreshEventsRef = useRef<((force?: boolean) => void) | null>(null); - const refreshLogRef = useRef<((force?: boolean) => void) | null>(null); - const eventSequenceRef = useRef(new TranscriptSequence()); - const logSequenceRef = useRef(new TranscriptSequence()); - const eventsReadCursorRef = useRef(0); - const eventsCatchupCursorRef = useRef(0); - const retainedEventHistoryThroughRef = useRef(0); - const logIdentityRef = useRef(run.logRef ?? null); - const retiredLogRefs = useRef(new Set()); - const wasLiveRef = useRef(isLive); - const bindLog = (ref: unknown): "same" | "changed" | "stale" => { - if (typeof ref !== "string" || !ref || ref === logIdentityRef.current) return "same"; - if (retiredLogRefs.current.has(ref)) return "stale"; - const prior = logIdentityRef.current; - logIdentityRef.current = ref; - if (!prior) return "same"; - retiredLogRefs.current.add(prior); - if (retiredLogRefs.current.size > 8) retiredLogRefs.current.delete(retiredLogRefs.current.values().next().value!); - pendingLogLineRef.current = ""; - logMergeRefs.current = { seenChunkKeys: new Set(), trimmedSeqFloorByRun: new Map() }; - logSequenceRef.current = new TranscriptSequence(); - setLogLines([]); setLogOffset(0); - return "changed"; - }; - useEffect(() => { - eventsReadCursorRef.current = 0; eventsCatchupCursorRef.current = 0; retainedEventHistoryThroughRef.current = 0; eventSequenceRef.current = new TranscriptSequence(); logSequenceRef.current = new TranscriptSequence(); - wasLiveRef.current = isLive; - logIdentityRef.current = run.logRef ?? null; retiredLogRefs.current.clear(); - hasMoreEventsRef.current = false; setHasMoreEvents(false); setLoadingMoreEvents(false); setEventsError(null); - setEvents([]); setLoading(true); - }, [run.id, run.companyId]); - useEffect(() => { if (isLive) wasLiveRef.current = true; }, [isLive]); - - function appendEvents(rows: HeartbeatRunEvent[], retainHistory = false) { - if (retainHistory) retainedEventHistoryThroughRef.current = Math.max( - retainedEventHistoryThroughRef.current, rows.at(-1)?.seq ?? 0, - ); - setEvents(previous => { - const merged = mergeRunEvents(previous, rows); - if (!isLive && !wasLiveRef.current) return merged; - const retainedThrough = Math.max(200, retainedEventHistoryThroughRef.current); - const prefix = merged.filter(row => row.seq <= retainedThrough); - const tail = merged.filter(row => row.seq > retainedThrough); - return [...prefix, ...tail.slice(-MAX_LIVE_EVENTS)]; - }); - } - - const stream = useTranscriptLiveStream(isLive ? [run.id] : [], { companyId: run.companyId, enabled: visible }, event => { - if (event.companyId !== run.companyId) return; - const payload = asRecord(event.payload); - if (!payload || payload.runId !== run.id) return; - if (event.type === "heartbeat.run.log") { - const binding = bindLog(payload.logRef); - if (binding === "stale") return; - const seq = readChunkSeq(payload.seq); - if (payload.truncated === true) logSequenceRef.current.requireHistory(seq); - const gap = logSequenceRef.current.observe(seq); - if (binding === "changed" || payload.truncated === true || gap) refreshLogRef.current?.(); - if (payload.truncated === true) return; - const chunk = typeof payload.chunk === "string" ? payload.chunk : ""; - if (!chunk) return; - const channel = payload.stream === "stderr" || payload.stream === "system" ? payload.stream : "stdout"; - appendLogLines([{ ts: asNonEmptyString(payload.ts) ?? event.createdAt, stream: channel, chunk, seq }]); - return; - } - if (event.type === "heartbeat.run.progress") { - const line = buildHeartbeatProgressLogLine(payload, event.createdAt); - if (!line) return; - const key = heartbeatProgressLogLineKey(line); - if (seenProgressLogLineKeysRef.current.has(key)) return; - seenProgressLogLineKeysRef.current.add(key); appendLogLines([line]); - return; - } - if (event.type === "heartbeat.run.status") { - refreshEventsRef.current?.(true); refreshLogRef.current?.(true); return; - } - const row = liveRunEvent(event); - if (!row) return; - const commitSignal = row.message === null && row.payload === null; - if (commitSignal) eventSequenceRef.current.requireHistory(row.seq); - if (eventSequenceRef.current.observe(row.seq)) refreshEventsRef.current?.(); - if (commitSignal) return; - appendEvents([row]); - }); - - useEffect(() => { - setLoadingMoreEvents(false); - if (!visible) return; - let cancelled = false; - let timer: number | undefined; - const abort = new AbortController(); - let reading = false; - let pendingRefresh = false; - let failedRead: { requested: boolean; catchup: boolean } | null = null; - const read = async (requested = false, catchup = false) => { - if (cancelled) return; - if (reading) { if (catchup) pendingRefresh = true; return; } - if (hasMoreEventsRef.current && !requested && !catchup) return; - reading = true; pendingRefresh = false; setLoadingMoreEvents(true); setEventsError(null); - window.clearTimeout(timer); timer = undefined; - let failed = false; - try { - const cursor = catchup ? Math.max(eventsCatchupCursorRef.current, eventsReadCursorRef.current) : eventsReadCursorRef.current; - const initializesHistory = eventsReadCursorRef.current === 0; - const rows = await readTranscriptRequest(signal => heartbeatsApi.events(run.id, cursor, 1000, { signal }), abort.signal); - if (cancelled) return; - appendEvents(rows, !catchup || initializesHistory); - const next = Math.max(cursor, rows.at(-1)?.seq ?? cursor); - eventsCatchupCursorRef.current = Math.max(eventsCatchupCursorRef.current, next); - eventSequenceRef.current.confirm(next); - if (catchup) { - if (rows.length === 1000 && next > cursor) pendingRefresh = true; - } - if (!catchup || initializesHistory) { - eventsReadCursorRef.current = next; - hasMoreEventsRef.current = rows.length === 1000 && next > cursor; - setHasMoreEvents(hasMoreEventsRef.current); - } - failed = eventSequenceRef.current.needsCatchup; - failedRead = null; - } catch { - failed = true; failedRead = { requested, catchup }; - if (!cancelled) setEventsError("Could not load events. Try again."); - } - if (cancelled) return; - reading = false; setLoadingMoreEvents(false); setLoading(false); - if (pendingRefresh) void read(false, true); - else if (failed || (isLive && !stream.healthy && !hasMoreEventsRef.current)) { - timer = window.setTimeout(() => void read(requested, catchup || eventSequenceRef.current.needsCatchup), 2000); - } - }; - refreshEventsRef.current = () => { void read(false, wasLiveRef.current); }; - loadMoreEventsRef.current = () => { void read(failedRead?.requested ?? true, failedRead?.catchup ?? false); }; - void read(false, wasLiveRef.current && (stream.healthy || eventsReadCursorRef.current > 0)); - return () => { - cancelled = true; refreshEventsRef.current = null; loadMoreEventsRef.current = null; - abort.abort(); if (timer !== undefined) window.clearTimeout(timer); - }; - }, [visible, run.id, run.companyId, run.status, stream.healthy, stream.epoch]); - - const getScrollContainer = useCallback((): ScrollContainer => { - if (scrollContainerRef.current) return scrollContainerRef.current; - const container = findScrollContainer(logEndRef.current); - scrollContainerRef.current = container; - return container; - }, []); - - const updateFollowingState = useCallback(() => { - const container = getScrollContainer(); - const metrics = readScrollMetrics(container); - lastMetricsRef.current = metrics; - const nearBottom = metrics.distanceFromBottom <= LIVE_SCROLL_BOTTOM_TOLERANCE_PX; - isFollowingRef.current = nearBottom; - setIsFollowing((prev) => (prev === nearBottom ? prev : nearBottom)); - }, [getScrollContainer]); - - useEffect(() => { - scrollContainerRef.current = null; - lastMetricsRef.current = { - scrollHeight: 0, - distanceFromBottom: Number.POSITIVE_INFINITY, - }; - - if (!isLive) { - isFollowingRef.current = false; - setIsFollowing(false); - return; - } - - updateFollowingState(); - }, [isLive, run.id, updateFollowingState]); - - useEffect(() => { - if (!isLive) return; - const container = getScrollContainer(); - updateFollowingState(); - - if (container === window) { - window.addEventListener("scroll", updateFollowingState, { passive: true }); - } else { - container.addEventListener("scroll", updateFollowingState, { passive: true }); - } - window.addEventListener("resize", updateFollowingState); - return () => { - if (container === window) { - window.removeEventListener("scroll", updateFollowingState); - } else { - container.removeEventListener("scroll", updateFollowingState); - } - window.removeEventListener("resize", updateFollowingState); - }; - }, [isLive, run.id, getScrollContainer, updateFollowingState]); - - // Auto-scroll only for live runs when following - useEffect(() => { - if (!isLive || !isFollowingRef.current) return; - - const container = getScrollContainer(); - const previous = lastMetricsRef.current; - const current = readScrollMetrics(container); - const growth = Math.max(0, current.scrollHeight - previous.scrollHeight); - const expectedDistance = previous.distanceFromBottom + growth; - const movedAwayBy = current.distanceFromBottom - expectedDistance; - - // If user moved away from bottom between updates, release auto-follow immediately. - if (movedAwayBy > LIVE_SCROLL_BOTTOM_TOLERANCE_PX) { - isFollowingRef.current = false; - setIsFollowing(false); - lastMetricsRef.current = current; - return; - } - - scrollToContainerBottom(container, "auto"); - const after = readScrollMetrics(container); - lastMetricsRef.current = after; - if (!isFollowingRef.current) { - isFollowingRef.current = true; - } - setIsFollowing((prev) => (prev ? prev : true)); - }, [events.length, logLines.length, isLive, getScrollContainer]); - - // Reset only when the log source changes, never when visibility changes. - useEffect(() => { - pendingLogLineRef.current = ""; - logMergeRefs.current = { seenChunkKeys: new Set(), trimmedSeqFloorByRun: new Map() }; - seenProgressLogLineKeysRef.current = new Set(); - setLogLines([]); - setLogOffset(0); - setHasMoreLog(false); - setLoadingMoreLog(false); - setLogError(null); - }, [run.id, run.logRef, setLogOffset]); - - useEffect(() => { - if (!visible) return; - if (!run.logRef && !shouldPollShellLog) { setLogLoading(false); return; } - let cancelled = false; - let timer: number | undefined; - const abort = new AbortController(); - setLogLoading(true); - let reading = false; - let pendingRefresh = false; - const load = async () => { - if (cancelled || reading) return; - reading = true; pendingRefresh = false; - window.clearTimeout(timer); timer = undefined; - let failed = false; - try { - for (;;) { - const offset = logOffsetRef.current; - const result = await readTranscriptRequest(signal => heartbeatsApi.log(run.id, offset, RUN_LOG_PAGE_BYTES, { signal }), abort.signal); - if (cancelled) return; - const binding = bindLog(result.logRef); - if (binding === "stale") { failed = true; break; } - if (binding === "changed" && offset !== 0) continue; - appendLogContent(result.content, result.nextOffset === undefined); - const next = result.nextOffset ?? offset + new TextEncoder().encode(result.content).byteLength; - setLogOffset(next); setLogError(null); - setHasMoreLog(!wasLiveRef.current && result.nextOffset !== undefined && !!result.content); - if (!wasLiveRef.current || result.nextOffset === undefined || !result.content || next <= offset) break; - } - failed ||= logSequenceRef.current.needsCatchup; - } catch (err) { - if (cancelled) return; - failed = shouldPollShellLog || !isRunLogUnavailable(err); - if (!isRunLogUnavailable(err)) setLogError(err instanceof Error ? err.message : "Failed to load run log"); - } - if (cancelled) return; - reading = false; - setLogLoading(false); - if (pendingRefresh) void load(); - else if (failed || (shouldPollShellLog && !stream.healthy)) timer = window.setTimeout(() => void load(), 2000); - }; - refreshLogRef.current = (force = false) => { if (force) pendingRefresh = true; void load(); }; - void load(); - return () => { cancelled = true; refreshLogRef.current = null; abort.abort(); if (timer !== undefined) window.clearTimeout(timer); }; - }, [visible, run.id, run.companyId, run.logRef, isLive ? null : run.logBytes, shouldPollShellLog, stream.healthy, stream.epoch]); - - async function loadMorePersistedLog() { - if (loadingMoreLog || !hasMoreLog) return; - setLoadingMoreLog(true); - setLogError(null); - try { - const result = await heartbeatsApi.log(run.id, logOffset, RUN_LOG_PAGE_BYTES); - appendLogContent(result.content, result.nextOffset === undefined); - const next = result.nextOffset ?? logOffset + new TextEncoder().encode(result.content).byteLength; - setLogOffset(next); - setHasMoreLog(result.nextOffset !== undefined); - } catch (err) { - setLogError(err instanceof Error ? err.message : "Failed to load more run log"); - } finally { - setLoadingMoreLog(false); - } - } - - const censorUsernameInLogs = useQuery({ - queryKey: queryKeys.instance.generalSettings, - queryFn: () => instanceSettingsApi.getGeneral(), - }).data?.censorUsernameInLogs === true; - - const adapterInvokePayload = useMemo(() => { - const evt = events.find((e) => e.eventType === "adapter.invoke"); - return redactPathValue(asRecord(evt?.payload ?? null), censorUsernameInLogs); - }, [censorUsernameInLogs, events]); - - // NOTE: adapter is NOT memoized because external adapters replace their - // parseStdoutLine asynchronously after dynamic parser loading. Memoizing - // on adapterType alone would stale the transcript with the fallback parser. - // We subscribe to adapter registry changes to force transcript recomputation. - const [parserTick, setParserTick] = useState(0); - const adapter = getUIAdapter(adapterType); - - useEffect(() => { - return onAdapterChange(() => setParserTick((t) => t + 1)); - }, []); - - const transcript = useMemo( - () => buildTranscript(logLines, adapter, { censorUsernameInLogs }), - [adapter, censorUsernameInLogs, logLines, parserTick], - ); - const toolDecisionLookup = useQuery({ - queryKey: queryKeys.tools.runDecisions(run.companyId, run.id), - queryFn: () => toolsApi.getRunDecisionLookup(run.companyId, run.id), - enabled: Boolean(run.companyId && run.id), - refetchInterval: isLive ? 3000 : false, - staleTime: isLive ? 1000 : 30_000, - }); - - useEffect(() => { - setTranscriptMode("nice"); - }, [run.id]); - - if (loading && logLoading) { - return

Loading run logs...

; - } - - if (events.length === 0 && logLines.length === 0 && !logError && !eventsError) { - return

No log events.

; - } - - const levelColors: Record = { - info: "text-foreground", - warn: "text-yellow-600 dark:text-yellow-400", - error: "text-red-600 dark:text-red-400", - }; - - const streamColors: Record = { - stdout: "text-foreground", - stderr: "text-red-600 dark:text-red-300", - system: "text-blue-600 dark:text-blue-300", - }; - - return ( -
- - {adapterInvokePayload && ( - - )} - -
- - Transcript ({transcript.length}) - -
-
- {(["nice", "raw"] as const).map((mode) => ( - - ))} -
- {isLive && !isFollowing && ( - - )} - {isLive && ( - - - - - - Live - - )} -
-
-
- - {hasMoreLog && ( -
- - - Showing the first {Math.round(logOffset / 1024).toLocaleString("en-US")} KB - {typeof run.logBytes === "number" && run.logBytes > 0 - ? ` of ${Math.round(run.logBytes / 1024).toLocaleString("en-US")} KB` - : ""} - -
- )} - {logError && ( -
- {logError} -
- )} -
-
- - {(run.status === "failed" || run.status === "timed_out") && ( -
-
Failure details
- {run.error && ( -
- Error: - {redactPathText(run.error, censorUsernameInLogs)} -
- )} - {run.stderrExcerpt && run.stderrExcerpt.trim() && ( -
-
stderr excerpt
-
-                {redactPathText(run.stderrExcerpt, censorUsernameInLogs)}
-              
-
- )} - {run.resultJson && ( -
-
adapter result JSON
-
-                {JSON.stringify(redactPathValue(run.resultJson, censorUsernameInLogs), null, 2)}
-              
-
- )} - {run.stdoutExcerpt && run.stdoutExcerpt.trim() && !run.resultJson && ( -
-
stdout excerpt
-
-                {redactPathText(run.stdoutExcerpt, censorUsernameInLogs)}
-              
-
- )} -
- )} - - {(events.length > 0 || eventsError) && ( -
-
Events ({events.length})
- {eventsError &&

{eventsError}

} - {(hasMoreEvents || eventsError) && ( - - )} -
- {events.map((evt) => { - const color = evt.color - ?? (evt.level ? levelColors[evt.level] : null) - ?? (evt.stream ? streamColors[evt.stream] : null) - ?? "text-foreground"; - - return ( -
- - {new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })} - - - {evt.stream ? `[${evt.stream}]` : ""} - - - {evt.message - ? redactPathText(evt.message, censorUsernameInLogs) - : evt.payload - ? JSON.stringify(redactPathValue(evt.payload, censorUsernameInLogs)) - : ""} - -
- ); - })} -
-
- )} -
- ); -} - /* ---- Keys Tab ---- */ export function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }) { diff --git a/vitest.config.ts b/vitest.config.ts index d91485c5935..8b1cf431d44 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,9 +2,9 @@ import { configDefaults, defineConfig } from "vitest/config"; export default defineConfig({ test: { - ...(process.env.PAPERCLIP_CI_PROFILE === "true" - ? { reporters: [...configDefaults.reporters, "./scripts/ci-profile/vitest-reporter.mjs"] } - : {}), + reporters: [...configDefaults.reporters, + ...(process.env.GITHUB_ACTIONS === "true" ? ["./scripts/ci/test-timing-reporter.mjs"] : []), + ...(process.env.PAPERCLIP_CI_PROFILE === "true" ? ["./scripts/ci-profile/vitest-reporter.mjs"] : [])], projects: [ "packages/shared", "packages/skills-catalog",