diff --git a/.github/workflows/pr-trusted.yml b/.github/workflows/pr-trusted.yml index 1231280351f..18246ba0de7 100644 --- a/.github/workflows/pr-trusted.yml +++ b/.github/workflows/pr-trusted.yml @@ -446,6 +446,9 @@ jobs: - name: Typecheck workspaces whose build scripts skip TypeScript run: pnpm run typecheck:build-gaps + - name: Typecheck test files that no build reads + run: node scripts/typecheck-test-programs.mjs --low-memory + - name: Typecheck the server in low-memory layers env: PAPERCLIP_TYPECHECK_LOW_MEMORY: "1" diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 054b8317510..5704355c9fc 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -1319,6 +1319,40 @@ that file, and it fails when a referenced config does not exist yet. The program. That program peaks at about 15 GiB at default settings and about 8 GiB on one thread. +#### Typecheck test files outside the build + +Most package tsconfigs exclude tests, and Vitest strips types, so a type error in +one of those tests fails nothing. `scripts/typecheck-test-programs.mjs` reads them +through separate programs, usually a `tsconfig.test.json` beside the package's +`tsconfig.json`: + +```sh +node scripts/typecheck-test-programs.mjs --low-memory +node scripts/typecheck-test-programs.mjs packages/paperclip-runner/tsconfig.test.json +``` + +The first form runs in the required `Typecheck + Release Registry` job. It checks +one program at a time and the largest peaks below 2 GiB. It then fails if a tracked +`*.test.ts(x)` or `*.spec.ts(x)` file is in no program. The compiler's file list +decides that, because `include` silently drops `foo.test.tsx` when `foo.test.ts` +exists. + +To add a test directory, add it to the `include` of the package's +`tsconfig.test.json`. To add a package, add a program to `PROGRAMS` in the script. + +Sandbox providers are outside the root workspace, so the root install has none of +their SDKs. `scripts/typecheck-sandbox-providers` is a small workspace that lists +them and holds one lockfile. Its install replaces any standalone `node_modules` in +those providers. After changing a provider's `package.json`, refresh that lockfile: + +```sh +pnpm -C scripts/typecheck-sandbox-providers install --ignore-scripts --lockfile-only +``` + +Do not commit a lockfile inside a provider. +`scripts/build-standalone-public-packages.mjs` uses a frozen install when one +exists, which changes what a release resolves. + #### Reused Runner build in the server build The server build vendors `packages/paperclip-runner/dist`, so its diff --git a/doc/execution-semantics.md b/doc/execution-semantics.md index 69ca5f0d3f3..c405b8be27f 100644 --- a/doc/execution-semantics.md +++ b/doc/execution-semantics.md @@ -2589,13 +2589,26 @@ Shutdown, process loss, and provider failure use the existing durable failure re If shutdown or a recorded unexpected system interruption stops Paperclip Runner preparation before native execution was selected, recovery verifies that no provider, process launch, native result or coordinator exists. The previous -controller must have settled or lost its lease. +controller must have settled or lost its lease. A failed `process_lost` run also +qualifies when restart recovery committed its explicit bootstrap certificate +that provider work did not start. The error label alone grants no replay. Required workspace recovery must have an exact saved-file receipt and stopped leases before the task can continue. Once these checks pass, the server retires its own unchanged bootstrap-failure block and schedules one bounded retry. Startup also discovers this obligation when file recovery completed before the restart. A pending Retry or saved message owns its continuation instead of -starting a competing automatic attempt. Retry uses the same preparation proof +starting a competing automatic attempt. If a replacement was already withheld +by the workspace gate before execution, continuation retains that cancelled row +and uses its existing predecessor claim and remaining failure budget. +Recovery adopts that withheld row only when it is the source's sole retry child. +Multiple-child history reuses the newest existing child, ordered by creation time +and run ID, without scheduling another replacement. +Clearing the file hold records a pending continuation before enqueue. Restart +reconstructs that obligation until a successor is recorded. Known admission +gates defer it without a polling deadline; eligibility changes wake the existing +recovery worker. An unresolved enqueue or receipt write keeps its shared retry +deadline. Delivery or exhaustion retires that obligation. +Retry uses the same preparation proof and preserves the recovered workspace. Current task ownership, human blocks, pauses, approvals, budgets and the existing retry limit remain effective. Operator Stops retain their explicit continuation requirements. Other ambiguous diff --git a/package.json b/package.json index d05ed21d7ad..8ea83e18e59 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "smoke:notion-generic-live": "node scripts/smoke/notion-generic-live.mjs", "smoke:posthog-live": "node scripts/smoke/posthog-live.mjs", "smoke:terminal-bench-loop-skill": "node scripts/smoke/terminal-bench-loop-skill-smoke.mjs", - "test:release-registry": "node scripts/ci-profile/node-test.mjs scripts/ci-profile/reporters.test.mjs scripts/__tests__/canary-startup-playwright.test.mjs scripts/__tests__/cursor-public-install-sandbox.test.mjs scripts/__tests__/grok-public-install-sandbox.test.mjs scripts/__tests__/public-install-closure.test.mjs scripts/__tests__/public-install-registry.test.mjs scripts/__tests__/prefetch-child.test.mjs scripts/__tests__/stage-bundled-packages.test.mjs scripts/__tests__/pack-staged-packages.test.mjs scripts/__tests__/retain-runner-qualification-packages.test.mjs scripts/__tests__/runner-release-crate-cache.test.mjs scripts/__tests__/runner-debug-crate-cache.test.mjs scripts/__tests__/vitest-worker-isolation.test.mjs scripts/verify-release-registry-state.test.mjs scripts/release-package-map.test.mjs scripts/check-release-package-bootstrap.test.mjs scripts/check-no-git-push.test.mjs scripts/release-lib.test.mjs scripts/release-registry-versions.test.mjs scripts/link-plugin-dev-sdk.test.js scripts/acpx-patch-packaging.test.mjs scripts/service-onboard-smoke.test.mjs scripts/docker-onboard-smoke.test.mjs scripts/preview-artifacts.test.mjs scripts/cloud-migrator-artifacts.test.mjs scripts/__tests__/local-verify.test.mjs", + "test:release-registry": "node scripts/ci-profile/node-test.mjs scripts/ci-profile/reporters.test.mjs scripts/__tests__/canary-startup-playwright.test.mjs scripts/__tests__/cursor-public-install-sandbox.test.mjs scripts/__tests__/grok-public-install-sandbox.test.mjs scripts/__tests__/public-install-closure.test.mjs scripts/__tests__/public-install-registry.test.mjs scripts/__tests__/prefetch-child.test.mjs scripts/__tests__/stage-bundled-packages.test.mjs scripts/__tests__/pack-staged-packages.test.mjs scripts/__tests__/retain-runner-qualification-packages.test.mjs scripts/__tests__/runner-release-crate-cache.test.mjs scripts/__tests__/runner-debug-crate-cache.test.mjs scripts/__tests__/vitest-worker-isolation.test.mjs scripts/verify-release-registry-state.test.mjs scripts/release-package-map.test.mjs scripts/check-release-package-bootstrap.test.mjs scripts/check-no-git-push.test.mjs scripts/release-lib.test.mjs scripts/release-registry-versions.test.mjs scripts/link-plugin-dev-sdk.test.js scripts/acpx-patch-packaging.test.mjs scripts/service-onboard-smoke.test.mjs scripts/docker-onboard-smoke.test.mjs scripts/preview-artifacts.test.mjs scripts/cloud-migrator-artifacts.test.mjs scripts/typecheck-test-programs.test.mjs scripts/__tests__/local-verify.test.mjs", "storybook-visual:baseline": "node scripts/storybook-visual-baseline.mjs", "test:storybook-visual": "node scripts/storybook-visual-baseline.mjs download && node scripts/storybook-visual-baseline.mjs verify && pnpm build-storybook && npx playwright test --config tests/storybook-visual/playwright.config.ts", "test:storybook-visual:update": "node scripts/storybook-visual-baseline.mjs download && pnpm build-storybook && npx playwright test --config tests/storybook-visual/playwright.config.ts --update-snapshots && node scripts/storybook-visual-baseline.mjs pack", diff --git a/packages/db/tsconfig.test.json b/packages/db/tsconfig.test.json new file mode 100644 index 00000000000..0308a876ded --- /dev/null +++ b/packages/db/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["scripts/**/*.test.ts"] +} diff --git a/packages/kv-demo-mcp-server/tsconfig.test.json b/packages/kv-demo-mcp-server/tsconfig.test.json new file mode 100644 index 00000000000..1e36832b11d --- /dev/null +++ b/packages/kv-demo-mcp-server/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "exclude": [] +} diff --git a/packages/paperclip-runner/src/backends/harness-driver-backend.test.ts b/packages/paperclip-runner/src/backends/harness-driver-backend.test.ts index 6791372425a..bc1193167e7 100644 --- a/packages/paperclip-runner/src/backends/harness-driver-backend.test.ts +++ b/packages/paperclip-runner/src/backends/harness-driver-backend.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import type { HarnessDriver, HarnessSession, PersistedHarnessSession } from "../contracts/harness-driver.js"; import { PROVIDER_STATE_INCOMPATIBLE, runnerRecoveryFailureClass } from "../contracts/harness-driver.js"; import type { PrpEvent, PrpStructuredRunResult, PrpTerminalState } from "../protocol/replay-contract.js"; -import { NativeSessionProtocolIntegrityError } from "../contracts/native-session-backend.js"; +import { NativeSessionProtocolIntegrityError, type NativeSession } from "../contracts/native-session-backend.js"; import { HarnessDriverBackend } from "./harness-driver-backend.js"; const result: PrpStructuredRunResult = { @@ -55,8 +55,13 @@ function prpEvent(sourceSeq: number, eventType: PrpEvent["eventType"], payload: const runtimeResolutions: unknown[] = []; +function cancelSession(session: NativeSession, input: Parameters>[0]) { + if (!session.cancel) throw new Error("backend session does not support cancellation"); + return session.cancel(input); +} + class FakeHarnessSession implements HarnessSession { - ids() { return { driverSessionId: "driver-1", providerSessionId: "provider-1" }; } + ids(): ReturnType { return { driverSessionId: "driver-1", providerSessionId: "provider-1" }; } async *events() { yield prpEvent(1, "run.result.proposed", result); yield prpEvent(2, "turn.completed", { status: "completed" }); @@ -942,7 +947,7 @@ describe("HarnessDriverBackend", () => { ); class TerminalThenIntegrityFailure extends FakeHarnessSession { goal = vi.fn(async () => null); - steer = vi.fn(async () => ({ correlationId: "blocked-steer" })); + steer = vi.fn(async () => {}); override async *events() { yield* super.events(); throw fault; @@ -1068,8 +1073,8 @@ describe("HarnessDriverBackend", () => { const session = await backend.openSession({ identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" }, workingDirectory: "/workspace", }); - if (journalOnly) session.revokeTurnPublication(); - else await session.cancel({ reason: "durable governed wait", signal: new AbortController().signal }).cleanup; + if (journalOnly) session.revokeTurnPublication!(); + else await cancelSession(session, { reason: "durable governed wait", signal: new AbortController().signal }).cleanup; const events = []; for await (const event of session.events()) events.push(event); expect(events.filter(event => event.payload.kind === "agentMessage")).toHaveLength(0); @@ -1085,7 +1090,7 @@ describe("HarnessDriverBackend", () => { identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" }, workingDirectory: "/workspace", }); - await session.cancel({ reason: "user stop during startup", signal: new AbortController().signal }).cleanup; + await cancelSession(session, { reason: "user stop during startup", signal: new AbortController().signal }).cleanup; await expect(session.startTurn({ message: { role: "user", text: "cancelled work" } })) .rejects.toThrow("native_session_cancelled"); expect(start).not.toHaveBeenCalled(); @@ -1108,7 +1113,7 @@ describe("HarnessDriverBackend", () => { }); const iterator = session.events()[Symbol.asyncIterator](); await iterator.next(); - await session.cancel({ reason: "operator stop", signal: new AbortController().signal }).cleanup; + await cancelSession(session, { reason: "operator stop", signal: new AbortController().signal }).cleanup; await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "runtime_request.cancelled", sourceSeq: 3 } }); await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "turn.failed", sourceSeq: 4 } }); await expect(iterator.next()).resolves.toMatchObject({ done: true }); @@ -1133,7 +1138,7 @@ describe("HarnessDriverBackend", () => { }); const iterator = session.events()[Symbol.asyncIterator](); await iterator.next(); - await expect(session.cancel({ reason: "operator stop", signal: new AbortController().signal }).cleanup).rejects.toThrow("no active provider turn"); + await expect(cancelSession(session, { reason: "operator stop", signal: new AbortController().signal }).cleanup).rejects.toThrow("no active provider turn"); await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "turn.cancelled", sourceSeq: 2, payload: { status: "cancelled", providerTerminalState: "completed", reason: "cancelled_after_provider_completed" } } }); await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "run.terminal", sourceSeq: 3, @@ -1166,7 +1171,7 @@ describe("HarnessDriverBackend", () => { }); const iterator = session.events()[Symbol.asyncIterator](); await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "runtime_request.created" } }); - await session.cancel({ + await cancelSession(session, { reason: "operator cancelled the run", signal: new AbortController().signal, }).cleanup; diff --git a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts index 1b551e20ccf..a0cf6f0e9b5 100644 --- a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts +++ b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import * as codexSecurity from "../drivers/codex/codex-security-config.js"; -import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js"; +import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInput, type NativeExecutionInputV1, type NativeExecutionInputV2 } from "../contracts/native-execution.js"; import { NATIVE_RUNTIME_ASSET_SCHEMA, PAPERCLIP_EXECUTION_PROMPT, PAPERCLIP_EXECUTION_PROMPT_REVISION, nativeRuntimePromptDigest, canonicalNativeRuntimeContextDigest, composeNativeSystemInstructions } from "../contracts/runtime-context.js"; const contextRoots: string[] = []; @@ -36,12 +36,12 @@ import { createCodexNativeSessionBackend, createRunnerdNativeSessionBackend } fr import { createOpenCodeNativeSessionBackend } from "./opencode-native-backend.js"; function execution( - provider: NativeExecutionInput["provider"] = { + provider: NativeExecutionInputV1["provider"] = { kind: "codex", model: null, approvalPolicy: "never", }, -): NativeExecutionInput { +): NativeExecutionInputV1 { return { schema: "paperclip.native-execution-input.v1", binding: { @@ -88,7 +88,7 @@ function execution( function acpxExecution( agent: "codex" | "pi" | "claude" = "codex", -): NativeExecutionInput { +): NativeExecutionInputV1 { return { ...execution(), session: { @@ -107,7 +107,7 @@ function acpxExecution( }; } -function opencodeExecution(): NativeExecutionInput { +function opencodeExecution(): NativeExecutionInputV1 { return { ...execution(), session: { @@ -124,7 +124,7 @@ function opencodeExecution(): NativeExecutionInput { }; } -function planningExecution(input: NativeExecutionInput): NativeExecutionInput { +function planningExecution(input: NativeExecutionInputV1): NativeExecutionInputV2 { return { ...input, schema: "paperclip.native-execution-input.v2", @@ -143,7 +143,7 @@ function planningExecution(input: NativeExecutionInput): NativeExecutionInput { function managedExecution( kind: "claude_managed" | "aws_agentcore", -): NativeExecutionInput { +): NativeExecutionInputV1 { if (kind === "claude_managed") { return { ...execution(), @@ -223,7 +223,7 @@ describe("native backend factory", () => { }); try { const backend = createNativeSessionBackend(input, { environment: {} }); - await expect(backend.openSession({ identity: { runId: "run", sessionId: "session", companyId: "company", agentId: "agent" }, + await expect(backend.openSession({ identity: { runId: "run", sessionId: "session", companyId: "company", issueId: "issue", agentId: "agent" }, workingDirectory: WORKSPACE })).rejects.toThrow("offline-stop-before-process-spawn"); expect(generated).toBeDefined(); if (webSearch) expect(generated?.slice(-3)).toEqual(["-c", 'web_search="disabled"', "app-server"]); @@ -404,7 +404,7 @@ describe("native backend factory", () => { expect(recovered).toMatchObject({ recovered: true }); expect(factory).toHaveBeenCalledWith(expect.objectContaining({ baseInstructions: retainedSystemInstructions })); expect(transport.calls.some(call => call.method === "thread/start" || call.method === "turn/start")).toBe(false); - if (recovered.recovered) await recovered.session.close({ reason: "test complete" }); + if (recovered.recovered) await recovered.session!.close({ reason: "test complete" }); }); describe("runner-prepared launch working directory", () => { @@ -420,7 +420,7 @@ describe("native backend factory", () => { return response; } } - const identity = { runId: "run", sessionId: "session", companyId: "company", agentId: "agent" }; + const identity = { runId: "run", sessionId: "session", companyId: "company", issueId: "issue", agentId: "agent" }; const providers = [ ["Codex", execution()], ["OpenCode", opencodeExecution()], @@ -452,8 +452,8 @@ describe("native backend factory", () => { }).recoverSession!({ ...snapshot, activeTurnId: null }, { signal: new AbortController().signal }); expect(recovered).toMatchObject({ recovered: true }); if (recovered.recovered) { - expect((await recovered.session.snapshot()).workingDirectory).toBe(cwd); - await recovered.session.close({ reason: "test complete" }); + expect((await recovered.session!.snapshot()).workingDirectory).toBe(cwd); + await recovered.session!.close({ reason: "test complete" }); } }); @@ -769,6 +769,7 @@ describe("native backend factory", () => { await backend.openSession({ identity: { companyId: "company", + issueId: "issue", agentId: "agent", runId: "run", sessionId: "session", @@ -807,7 +808,7 @@ describe("native backend factory", () => { environment: { PAPERCLIP_WORKSPACE_CWD: WORKSPACE }, }); const session = await backend.openSession({ - identity: { companyId: "company", agentId: "agent", runId: "run", sessionId: "session" }, + identity: { companyId: "company", issueId: "issue", agentId: "agent", runId: "run", sessionId: "session" }, workingDirectory: WORKSPACE, }); const first = JSON.stringify(buildNativeModelEnvelope(input)); diff --git a/packages/paperclip-runner/src/backends/native-instruction-measurement.test.ts b/packages/paperclip-runner/src/backends/native-instruction-measurement.test.ts index 2c5592af47e..ec1f00ec4bb 100644 --- a/packages/paperclip-runner/src/backends/native-instruction-measurement.test.ts +++ b/packages/paperclip-runner/src/backends/native-instruction-measurement.test.ts @@ -21,7 +21,7 @@ import { inspectNativeCompletionSourceMetadata } from "../../../../tests/runner- // It measures complete Paperclip-supplied instruction/tool/message projections, // not provider-owned stock prompts, tokenization, billing or model behavior. const tools = PAPERCLIP_SEMANTIC_ACTION_CATALOG - .filter(tool => tool.placement === "core") + .filter(tool => tool.placement === "always") .map(tool => ({ name: tool.operationId, description: tool.description, inputSchema: tool.inputSchema })); const identity = { companyId: "company", agentId: "agent", issueId: "issue", runId: "run", sessionId: "session" }; const { qualificationModel: _qualificationModel, reportedModelId: _reportedModelId, permissionPolicy: _permissionPolicy, ...profile } = resolveQualifiedAcpxProfile("claude", "claude-sonnet-5"); @@ -182,7 +182,7 @@ describe("direct OpenCode HTTP instruction boundary", () => { try { const checkpoint = await session.snapshot(); const send = async (current: typeof session, phase: string, value: NativeExecutionInput) => { - const envelope = buildNativeModelEnvelope(value, { resumedSession: phase === "continuation" }); + const envelope = phase === "continuation" ? buildNativeModelEnvelope(value, { resumedSession: true }) : buildNativeModelEnvelope(value); if ("requestedSkills" in envelope && backend.preparedTaskConstraints) envelope.constraints = [...backend.preparedTaskConstraints]; await current.startTurn({ message: { role: "user", text: JSON.stringify(envelope) } }); for await (const event of current.events()) { if (event.eventType === "turn.completed") break; if (["turn.failed", "turn.cancelled"].includes(event.eventType)) throw new Error(`Fixture turn failed: ${event.eventType}`); } diff --git a/packages/paperclip-runner/src/browser/client.test.ts b/packages/paperclip-runner/src/browser/client.test.ts index 27795f4dd78..99fdef4fead 100644 --- a/packages/paperclip-runner/src/browser/client.test.ts +++ b/packages/paperclip-runner/src/browser/client.test.ts @@ -54,16 +54,20 @@ describe("public runner browser client", () => { fetchImpl: vi.fn() as unknown as typeof fetch, eventSourceFactory: (nextUrl) => { url = nextUrl; return source; }, }); - const event = { + const event: PrpEvent = { schema: "paperclip.prp.event.v1", sourceEventId: "event-9", - sourceKind: "harness", + sourceKind: "runner", sourceInstanceId: "driver", sourceSeq: 9, + runId: "run-1", + normalizedSessionId: "session/id", + schemaVersion: 1, + priority: 2, emittedAt: "2026-08-08T00:00:00.000Z", eventType: "item.delta", payload: { text: "exact" }, - } as PrpEvent; + }; const onEvent = vi.fn(); const onError = vi.fn(); const handle = client.openEventStream("session/id", 8, { onEvent, onError }); diff --git a/packages/paperclip-runner/src/browser/transcript-model.test.ts b/packages/paperclip-runner/src/browser/transcript-model.test.ts index 2de5010907c..b2fd25af8a2 100644 --- a/packages/paperclip-runner/src/browser/transcript-model.test.ts +++ b/packages/paperclip-runner/src/browser/transcript-model.test.ts @@ -1,17 +1,17 @@ import { describe, expect, it } from "vitest"; -import { LiveConsoleScriptedDriver } from "../mock-core/live-console-scripted-driver"; +import { LiveConsoleScriptedDriver } from "../mock-core/live-console-scripted-driver.js"; import { parseHarnessRuntimeRequestResolution, type HarnessSession, -} from "../contracts/harness-driver"; -import type { PrpEvent } from "../protocol/replay-contract"; +} from "../contracts/harness-driver.js"; +import type { PrpEvent } from "../protocol/replay-contract.js"; import { applyPrpEvent, createSessionSnapshotFromMetadata, reduceSessionEvents, type SessionSnapshot, -} from "../reducer/session-reducer"; +} from "../reducer/session-reducer.js"; import type { RunnerCapabilities } from "./protocol.js"; import { buildTranscript, diff --git a/packages/paperclip-runner/src/catalog/protocol-action-contracts.test.ts b/packages/paperclip-runner/src/catalog/protocol-action-contracts.test.ts index 2ae21e152d5..c399e302439 100644 --- a/packages/paperclip-runner/src/catalog/protocol-action-contracts.test.ts +++ b/packages/paperclip-runner/src/catalog/protocol-action-contracts.test.ts @@ -1,4 +1,4 @@ -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { PAPERCLIP_PROTOCOL_ACTIONS } from "../protocol-actions/index.js"; @@ -50,8 +50,9 @@ describe("canonical Paperclip protocol action contracts", () => { ).toEqual([]); if (action.scenario.descriptor.idempotency === "required") { expect(action.examples).toHaveProperty("scenarioCall"); - expect(scenarioCall.idempotencyKey).toEqual(expect.any(String)); - expect(scenarioCall.idempotencyKey).not.toHaveLength(0); + const { idempotencyKey } = scenarioCall as { idempotencyKey?: string }; + expect(idempotencyKey).toEqual(expect.any(String)); + expect(idempotencyKey).not.toHaveLength(0); } else { expect(scenarioCall).not.toHaveProperty("idempotencyKey"); } diff --git a/packages/paperclip-runner/src/catalog/semantic-action-catalog.test.ts b/packages/paperclip-runner/src/catalog/semantic-action-catalog.test.ts index 992cf522183..afec800cdba 100644 --- a/packages/paperclip-runner/src/catalog/semantic-action-catalog.test.ts +++ b/packages/paperclip-runner/src/catalog/semantic-action-catalog.test.ts @@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { createTaskAction } from "../protocol-actions/create-task.js"; import { createProjectAction } from "../protocol-actions/create-project.js"; diff --git a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.test.ts b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.test.ts index 90d49bb600c..676dabac7cb 100644 --- a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.test.ts +++ b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.test.ts @@ -449,7 +449,7 @@ describe("qualified ACPX runtime sidecar", () => { // The receipt hashes the actual emitted input, not a second normalization. await loadActualToolCommands(tools).resolve({ callId: "3", turnId: "test-turn", result: { accepted: true }, error: null }); } - const body = await (await response).json(); + const body = await (await response).json() as ToolCallResponseBody; const receipt = readNativeSemanticReceipt({ contents: body.result.content }); expect(receipt).toMatchObject({ schema: "paperclip.semantic_tool_receipt.v2", inputSha256: semanticInputSha256(raw), normalizedInputSha256: mode === "forwarded" ? semanticInputSha256(emitted[0].input) : null, @@ -490,7 +490,7 @@ describe("qualified ACPX runtime sidecar", () => { expect(validatePrpStructuredRunResult(emitted[1].input).ok).toBe(true); await commands.resolve({ callId: "3", turnId: "test-turn", result: { accepted: true }, error: null }); } - const body = await (await response).json(); + const body = await (await response).json() as ToolCallResponseBody; const receipt = readNativeSemanticReceipt({ contents: body.result.content }); expect(receipt).toMatchObject({ inputSha256: semanticInputSha256(raw), normalizedInputSha256: eligible ? semanticInputSha256(emitted[1].input) : null, outcome: eligible ? "returned" : "error" }); @@ -517,7 +517,7 @@ describe("qualified ACPX runtime sidecar", () => { await vi.waitFor(() => expect(emitted).toHaveLength(1)); await expect(handler(call)).rejects.toThrow("duplicated"); } finally { controller.abort(); } - expect((await result).message).toContain("cancelled"); + expect((await result as Error).message).toContain("cancelled"); expect(capture).not.toHaveBeenCalled(); expect(tools.size).toBe(0); expect(emitted[0].operationId).toBe("get_task_context"); @@ -547,7 +547,7 @@ describe("qualified ACPX runtime sidecar", () => { const bridge = await startRunnerToolBridge({ handler, captureSemanticReceipt: () => () => {} }); try { const body = await (await fetch(bridge.url, { method: "POST", headers: { Authorization: `Bearer ${bridge.secret}`, "Content-Type": "application/json" }, - body: JSON.stringify(request) })).json(); + body: JSON.stringify(request) })).json() as ToolCallResponseBody; expect(body.result.isError).toBe(true); expect(body.result.content[0].text).toBe("ACPX semantic tool call exceeds the sidecar frame limit"); expect(readNativeSemanticReceipt({ contents: body.result.content })).toMatchObject({ outcome: "error", normalizedInputSha256: null }); @@ -561,7 +561,7 @@ describe("qualified ACPX runtime sidecar", () => { } finally { await bridge.close(); } }); - it.each(["backpressure", "throws"])("uses actual writer acceptance, preserving %s semantics", async mode => { + it.each(["backpressure", "throws"] as const)("uses actual writer acceptance, preserving %s semantics", async mode => { const wire = loadActualSidecarWriter(mode); const tools = new Map(), emitted: any[] = [], commit = vi.fn(), capture = vi.fn((_input: unknown) => commit); const raw = { reportedWorkDisposition: "done", summary: "Complete", evidence: [], verification: [], @@ -727,7 +727,7 @@ describe("qualified ACPX runtime sidecar", () => { expect(Buffer.byteLength(JSON.stringify(JSON.parse(wire.writes[0]!).payload))).toBeGreaterThan(256 * 1024); expect(Buffer.byteLength(wire.writes[0]!)).toBeLessThan(ACPX_SIDECAR_MAX_FRAME_BYTES); } - const body = await (await response).json(); + const body = await (await response).json() as ToolCallResponseBody; if (mode === "error") { expect(JSON.stringify(body)).not.toContain(credentialCanary); expect(JSON.stringify(body)).toContain("[REDACTED]"); @@ -820,8 +820,9 @@ describe("qualified ACPX runtime sidecar", () => { const sanitize = new Function("boundedOptionalText", "stableProviderIdentity", `return event => {${source.slice(start, end)} return null;}`)( (value: string) => value, (value: string) => `stable-${value}`, ); - const projection = createPiMessageProjection<{ type: string; text: string; stream: string; messageId: string; meta: Record }>(); - const event = (kind: string) => ({ type: "text_delta", text: "", stream: "output", messageId: `pi-message-${"a".repeat(64)}`, + type PiTextFrame = { type: string; text: string; stream: string; messageId: string; meta: Record }; + const projection = createPiMessageProjection(); + const event = (kind: string): PiTextFrame => ({ type: "text_delta", text: "", stream: "output", messageId: `pi-message-${"a".repeat(64)}`, meta: { origin: "pi-native-assistant", source: "pi-rpc-message-v1", kind, secret: "DROP_ME" } }); const startFrame = sanitize(projection.normalize(event("start"))); const endFrame = sanitize(projection.normalize(event("end:toolUse"))); @@ -830,7 +831,7 @@ describe("qualified ACPX runtime sidecar", () => { expect(startFrame.messageId).toBe(endFrame.messageId); expect(JSON.stringify([startFrame, endFrame])).not.toContain("DROP_ME"); projection.settle(); - const loaded = createPiMessageProjection>(); + const loaded = createPiMessageProjection(); const history = sanitize(loaded.normalize({ ...event("history"), messageId: `pi-history-message-${"b".repeat(64)}`, text: "old reply", meta: { origin: "pi-history-assistant", source: "pi-session-history-v1", kind: "history" } })); expect(history).toMatchObject({ text: "old reply", piMessageHistory: true }); loaded.settle(); @@ -1744,6 +1745,8 @@ class SidecarProcess { } } +type ToolCallResponseBody = { result: { content: Array<{ text: string }>; isError?: boolean } }; + function loadWaitForTool(input: { tools: Map; emitted: unknown[]; diff --git a/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts b/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts index 6ebd3259074..8ad2681f63c 100644 --- a/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts +++ b/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts @@ -54,7 +54,9 @@ describe("ACPX sidecar input sequencing", () => { async () => { events.push(command); }, - () => events.push(`${command}:error`), + () => { + events.push(`${command}:error`); + }, ); } const shutdown = pending.then(() => events.push("shutdown")); @@ -87,7 +89,9 @@ describe("ACPX sidecar input sequencing", () => { async () => { events.push("recovered"); }, - () => events.push("unexpected"), + () => { + events.push("unexpected"); + }, ); await recovered; @@ -113,7 +117,9 @@ describe("ACPX sidecar input sequencing", () => { async () => { events.push("recovered"); }, - () => events.push("unexpected"), + () => { + events.push("unexpected"); + }, ); await expect( diff --git a/packages/paperclip-runner/src/cli/eval-session-contract.test.ts b/packages/paperclip-runner/src/cli/eval-session-contract.test.ts index 5888e1b9328..0f07e2173ac 100644 --- a/packages/paperclip-runner/src/cli/eval-session-contract.test.ts +++ b/packages/paperclip-runner/src/cli/eval-session-contract.test.ts @@ -123,7 +123,7 @@ describe("eval-session request contract", () => { const value = request({ provider: "acpx", acpxAgent: agent, model: "explicit-provider-model" }); expect(() => parseEvalSessionRequest(value)).toThrow("--candidate-profile"); expect(parseEvalSessionRequest(value, { candidateProfile: agent })).toMatchObject({ acpxAgent: agent, model: "explicit-provider-model" }); - expect(() => parseEvalSessionRequest(value, { candidateProfile: agent === "pi" ? "cursor" : "pi" })).toThrow("must match"); + expect(() => parseEvalSessionRequest(value, { candidateProfile: "pi" })).toThrow("must match"); expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, model: "" }), { candidateProfile: agent })).toThrow("request.model"); expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: "codex", session: { acpxAgent: agent } }))).toThrow("session.acpxAgent must match"); expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, candidateProfile: agent }))).toThrow("--candidate-profile"); @@ -525,10 +525,10 @@ describe("eval-session usage", () => { providerReported: { nanodollars: null, recordedAttempts: 0 }, estimated: { nanodollars: 6_200_000, recordedAttempts: 1 }, }); - expect(snapshot.usageLedger[0]?.costNanodollars).toBe(legacyCost); + expect(snapshot.usageLedger![0]?.costNanodollars).toBe(legacyCost); const limitedRequest = parseEvalSessionRequest(request()); limitedRequest.limits.maxEstimatedCostNanodollars = 10_000_000; - const costlySnapshot = { ...snapshot, usageLedger: [{ ...snapshot.usageLedger[0]!, costNanodollars: 10_000_001 }] }; + const costlySnapshot = { ...snapshot, usageLedger: [{ ...snapshot.usageLedger![0]!, costNanodollars: 10_000_001 }] }; expect(() => boundedEvalSessionUsage(limitedRequest, { turnId: "codex-turn", status: "completed", assistantText: "Completed", snapshot: costlySnapshot, })).toThrow("legacy ledger cost limit exceeded"); @@ -609,7 +609,7 @@ describe("eval-session budget settlement", () => { }); const input = request({ provider: "acpx", acpxAgent: agent, model, runnerd: { path: binary, sha256: createHash("sha256").update("unused fake runner").digest("hex") }, - session: { workingDirectory: workspace, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}) }, limits: { turnTimeoutMs: 1000, maxAgentTurns: 2, maxEstimatedCostNanodollars: 100_000_000 }, + session: { workingDirectory: workspace }, limits: { turnTimeoutMs: 1000, maxAgentTurns: 2, maxEstimatedCostNanodollars: 100_000_000 }, }); const requestPath = join(workspace, "request.json"); const outputPath = join(workspace, "output.json"); diff --git a/packages/paperclip-runner/src/cli/opencode-proxy-input.test.ts b/packages/paperclip-runner/src/cli/opencode-proxy-input.test.ts index 47170c7dbf8..7b3a0a27a7b 100644 --- a/packages/paperclip-runner/src/cli/opencode-proxy-input.test.ts +++ b/packages/paperclip-runner/src/cli/opencode-proxy-input.test.ts @@ -17,14 +17,18 @@ describe("OpenCode proxy input sequencing", () => { await firstBlocked; events.push("initialize:complete"); }, - () => events.push("initialize:error"), + () => { + events.push("initialize:error"); + }, ); pending = enqueueOpenCodeProxyInput( pending, async () => { events.push("thread:start"); }, - () => events.push("thread:error"), + () => { + events.push("thread:error"); + }, ); const shutdown = pending.then(() => { events.push("shutdown"); @@ -50,7 +54,9 @@ describe("OpenCode proxy input sequencing", () => { async () => { throw new Error("initialize failed"); }, - (error) => events.push((error as Error).message), + (error) => { + events.push((error as Error).message); + }, ); await pending; diff --git a/packages/paperclip-runner/src/conformance/local-provider-fault-matrix.test.ts b/packages/paperclip-runner/src/conformance/local-provider-fault-matrix.test.ts index 74419455b09..f69d58d8acb 100644 --- a/packages/paperclip-runner/src/conformance/local-provider-fault-matrix.test.ts +++ b/packages/paperclip-runner/src/conformance/local-provider-fault-matrix.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { NativeExecutionInput } from "../contracts/native-execution.js"; +import type { NativeExecutionInput, NativeExecutionInputV1 } from "../contracts/native-execution.js"; import type { NativeRunIdentity, NativeSessionCapabilities, @@ -29,8 +29,8 @@ type ProfileKey = "codex" | "opencode" | "acpx-claude" | "acpx-codex"; interface LocalNativeProfile { key: ProfileKey; - driverKind: NativeExecutionInput["session"]["driverKind"]; - provider: NativeExecutionInput["provider"]; + driverKind: NativeExecutionInputV1["session"]["driverKind"]; + provider: NativeExecutionInputV1["provider"]; } /** diff --git a/packages/paperclip-runner/src/contracts/completion-result.test.ts b/packages/paperclip-runner/src/contracts/completion-result.test.ts index d230ef4ef21..fb166ef36e0 100644 --- a/packages/paperclip-runner/src/contracts/completion-result.test.ts +++ b/packages/paperclip-runner/src/contracts/completion-result.test.ts @@ -1,5 +1,5 @@ import { normalizeLegacyPrpStructuredRunResult } from "../protocol/result-normalization.js"; -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { PRP_BLOCK_RESULT_OUTPUT_SCHEMA, diff --git a/packages/paperclip-runner/src/contracts/provider-trace-file-sink.test.ts b/packages/paperclip-runner/src/contracts/provider-trace-file-sink.test.ts index fc30e120364..f104bc303a3 100644 --- a/packages/paperclip-runner/src/contracts/provider-trace-file-sink.test.ts +++ b/packages/paperclip-runner/src/contracts/provider-trace-file-sink.test.ts @@ -76,5 +76,5 @@ describe("ProviderTraceFileSink", () => { }); function statMode(value: Awaited>): number { - return value.mode & 0o777; + return Number(value.mode) & 0o777; } diff --git a/packages/paperclip-runner/src/contracts/question-set.test.ts b/packages/paperclip-runner/src/contracts/question-set.test.ts index c7860c8bf24..16190c9685d 100644 --- a/packages/paperclip-runner/src/contracts/question-set.test.ts +++ b/packages/paperclip-runner/src/contracts/question-set.test.ts @@ -1,4 +1,4 @@ -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { questionSetSchema } from "../protocol/generated/schema-bundle.js"; import { describe, expect, it } from "vitest"; diff --git a/packages/paperclip-runner/src/control-plane/cursor-cloud-runner-launch.test.ts b/packages/paperclip-runner/src/control-plane/cursor-cloud-runner-launch.test.ts index 14b87373c9c..b38b5099f13 100644 --- a/packages/paperclip-runner/src/control-plane/cursor-cloud-runner-launch.test.ts +++ b/packages/paperclip-runner/src/control-plane/cursor-cloud-runner-launch.test.ts @@ -101,7 +101,7 @@ it("settles a create rejected during real-facade Stop before cold recovery and t PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 60_000) }; const options = { provider: "cursor_cloud" as const, cursorCloud, stateDirectory, runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand, cursorCloudProxyPath: proxyPath, cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`, - lifecyclePolicy: { mode: "per_turn" as const }, environment }; + lifecyclePolicy: { mode: "per_turn" as const, idleTimeoutMs: null }, environment }; const first = createRunnerdCodexTransport({ ...options, prpIdentity: identity }), bundles = [first]; let stop: Promise | undefined; try { @@ -169,7 +169,7 @@ it("opens and closes the real Cloud facade without a provider prompt", async () runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand, cursorCloudProxyPath: proxyPath, cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`, - lifecyclePolicy: { mode: "per_turn" }, + lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, prpIdentity: { runnerInstanceId: "runner-cloud-launch", environmentLeaseId: "lease-cloud-launch", runId: "00000000-0000-4000-8000-000000000761", normalizedSessionId, turnId: "turn-cloud-launch", itemId: "item-cloud-launch" }, environment: { @@ -249,7 +249,7 @@ it("preserves native Cloud tool identity and redacted output through the real Ru provider: "cursor_cloud", cursorCloud, stateDirectory: join(root, "runner-session"), runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand, cursorCloudProxyPath: proxyPath, cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`, - lifecyclePolicy: { mode: "per_turn" }, + lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, prpIdentity: { runnerInstanceId: "runner-cloud-tool", environmentLeaseId: "lease-cloud-tool", runId: "00000000-0000-4000-8000-000000000762", normalizedSessionId, turnId: "turn-cloud-tool", itemId: "item-cloud-tool" }, environment: { @@ -343,7 +343,7 @@ async function cloudQuestionHandoff(terminalDelayMs: number, governed: boolean, provider: "cursor_cloud", cursorCloud, stateDirectory: join(root, "runner-session"), runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand, cursorCloudProxyPath: proxyPath, cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`, - lifecyclePolicy: { mode: "per_turn" }, + lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, prpIdentity: { runnerInstanceId: "runner-cloud-question", environmentLeaseId: "lease-cloud-question", runId: "00000000-0000-4000-8000-000000000763", normalizedSessionId, turnId: "turn-cloud-question", itemId: "item-cloud-question" }, environment: { @@ -405,7 +405,7 @@ async function cloudQuestionHandoff(terminalDelayMs: number, governed: boolean, const execution = executeNativeSession({ input, backend, runnerInstanceId: "runner-cloud-question", controlPlaneInstanceId: "control-cloud-question", timeoutMs: 120_000, controlPlane: { async openRun() {}, async checkpointSession() {}, completeRun, - async appendEvent(event) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async appendEvent(event: PrpEvent) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents(replay) { return { events: history.filter(event => event.sourceInstanceId === replay.sourceInstanceId && event.sourceSeq > replay.afterSourceSeq), highestContiguousSourceSeq: 0 }; }, }, resolveGovernedWait: ({ event }) => event.eventType === "item.completed" && event.payload.kind === "dynamicToolCall" ? yielded : null, @@ -520,7 +520,7 @@ async function cloudColdAuthorityRotation(instructions: string, validShutdown = PAPERCLIP_CURSOR_CLOUD_RELAY_URL: `https://relay.example.test/mcp/native-runs/${runId}`, PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 60_000) }); const options = { provider: "cursor_cloud" as const, cursorCloud, stateDirectory, runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand, cursorCloudProxyPath: proxyPath, cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`, - lifecyclePolicy: { mode: "per_turn" as const } }; + lifecyclePolicy: { mode: "per_turn" as const, idleTimeoutMs: null } }; const mcp = async (token: string, body: Record, sessionId?: string, priorScopeHeader?: string): Promise<{ status: number; sessionId?: string; value: any }> => { const scopeHeader = priorScopeHeader ?? await currentCloudScopeHeader(runtime, normalizedSessionId); return new Promise((accept, reject) => { diff --git a/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts b/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts index 13aa10b34d1..0dd2161ca0b 100644 --- a/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts +++ b/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts @@ -39,6 +39,8 @@ import { inspectRetainedRunInstructions, inspectStagedRunAttachmentHeader, type DurablePrpCheckpoint, + type DurablePrpControlPlaneStore, + type RunnerProcessHandle, durableRecoveryInternals, SemanticToolNotDispatchedError, inspectWarmRunTransition, @@ -48,6 +50,16 @@ import { } from "./durable-prp-control-plane.js"; import type { DurableRecoveryIdentity } from "./prp-transport-types.js"; +type WritableCoreStore = DurablePrpControlPlaneStore & { + save(): void; + commit(candidate: DurablePrpControlPlaneStore["state"]): void; +}; + +function restartRunner(handle: RunnerProcessHandle, ticket: string): void { + if (!handle.restart) throw new Error("Runner handle does not support restart"); + handle.restart(ticket); +} + const identity: DurableRecoveryIdentity = { runnerInstanceId: "runner-test-1", environmentLeaseId: "environment-test-1", @@ -229,7 +241,7 @@ it("recovers only the matching run's admitted instructions from a valid checkpoi expect(inspectRetainedRunInstructions(core.store.state, { ...targetExpected, runtimeContextDigest: digest })).toBeNull(); expect(inspectRetainedRunInstructions(core.store.state, expected)).toBe(template.provider.instructions); const corrupted = structuredClone(core.store.state); - corrupted.stagedRunAttachment!.templateDigest = `sha256:${"f".repeat(64)}`; + corrupted.stagedRunAttachment = { ...corrupted.stagedRunAttachment!, templateDigest: `sha256:${"f".repeat(64)}` }; expect(inspectRetainedRunInstructions(corrupted, targetExpected)).toBeNull(); } finally { await core.stop(); rmSync(root, { recursive: true, force: true }); } }); @@ -545,7 +557,7 @@ it("excludes streamed output from checkpoints even when the live window is full" anchor.eventType = "turn.started"; (anchor.envelope.payload as Record).eventType = "turn.started"; core.store.state.ackedSourceSeq = 4096; - core.store.save(); + (core.store as WritableCoreStore).save(); const snapshots: DurablePrpCheckpoint[] = []; core.setCheckpointPersistence(async saved => { snapshots.push(saved); }); try { @@ -918,7 +930,7 @@ it.each([false, true])("dispatches a fresh semantic input once when recovery ove if (JSON.parse(saved.records.get("cursor")!).ackedSourceSeq > 0) { writeHeld = true; await writeBarrier; } }); const pending: Promise[] = []; - let client: Awaited>; + let client: AuthenticatedClient | null = null; try { await core.start(); client = (await authenticate(core, core.issueBootstrapTicket()))!; @@ -1149,7 +1161,7 @@ it.each(["event", "progress", "lifecycle", "lifecycle_priority_one", "usage", "c expect(() => core.queueCommand("session.snapshot")).toThrow("indeterminate"); } if (mode === "authority_conflict") { - core.store.commit(structuredClone(core.store.state)); + (core.store as WritableCoreStore).commit(structuredClone(core.store.state)); core.queueCommand("session.snapshot", {}, "command_after_authority"); expect(JSON.parse(readFileSync(core.store.path, "utf8")).ackedSourceSeq).toBe(0); expect(core.store.state.ackedSourceSeq).toBe(0); @@ -1817,7 +1829,7 @@ it("pins the ACPX launch profile in runner startup arguments and restarts", () = }, }); - handle.restart("replacement-ticket"); + restartRunner(handle, "replacement-ticket"); expect(launches).toHaveLength(2); for (const launch of launches) { expect(launch.args).toContain("--acpx-launch-authority-digest"); @@ -1869,7 +1881,7 @@ it("pins the OpenCode launch profile in runner startup arguments and restarts", }, }); - handle.restart("replacement-ticket"); + restartRunner(handle, "replacement-ticket"); expect(launches).toHaveLength(2); for (const launch of launches) { expect(launch.args).toEqual( @@ -2057,7 +2069,7 @@ it("preserves only bounded GitHub credential projection at the runner spawn boun it("passes structured inputs to an external owner and preserves its replacement veto", () => { const process = { child: { exitCode: null, signalCode: null, kill: () => true }, completion: Promise.resolve({ code: 0, signal: null, stdout: "", stderr: "" }), restart: null }; - const launcher = vi.fn(() => process); + const launcher = vi.fn((_spec: RunnerProcessLaunchSpec) => process); const options = { connection: { mode: "external" as const }, stateDirectory: "/unused", identity, ticket: "synthetic-ticket", maxOutboxBytes: 262144, p0ReserveBytes: 65536, runnerVersion: expectedRunnerVersion, runnerDigest: expectedRunnerDigest, @@ -2065,7 +2077,7 @@ it("passes structured inputs to an external owner and preserves its replacement expect(() => spawnRunner(options)).toThrow("requires a process owner"); expect(spawnRunner({ ...options, processLauncher: launcher })).toBe(process); expect(launcher).toHaveBeenCalledWith(expect.objectContaining({ nativeRunner: expect.objectContaining(options) })); - expect(launcher.mock.calls[0][0].args).not.toContain("--connect-url"); + expect(launcher.mock.calls[0]![0].args).not.toContain("--connect-url"); }); it("preserves the controller-selected ACPX provider package root", () => { @@ -2147,7 +2159,7 @@ it.each([ completion: Promise.resolve({ code: 0, signal: null, stdout: "", stderr: "" }) }; }, }); - handle.restart("replacement-ticket"); + restartRunner(handle, "replacement-ticket"); for (const launch of launches.splice(0)) { expect(launch.environment[key]).toBe(explicit ? "explicit-fixture-credential" : undefined); const receipt = launch.environment[ACPX_CREDENTIAL_BINDING_ENV]; diff --git a/packages/paperclip-runner/src/devtools/index.test.ts b/packages/paperclip-runner/src/devtools/index.test.ts index 80c591de7cb..f6e118ceb34 100644 --- a/packages/paperclip-runner/src/devtools/index.test.ts +++ b/packages/paperclip-runner/src/devtools/index.test.ts @@ -19,37 +19,41 @@ function snapshot(): CapabilityLiveSessionSnapshot { current.lifecycle = "running"; current.artifacts.push({ id: "artifact-1", - companyId: "company-1", taskId: "task-1", - runId: "run-1", - kind: "text", - name: "result.txt", + filename: "result.txt", + contentType: "text/plain", + byteSize: 6, + sha256: "a".repeat(64), contentRef: "file:///private/result.txt", - metadata: {}, createdAt: "2026-08-09T00:00:01.000Z", }); return { - id: "session-1", + schema: "paperclip.capability.live-session.v1", + sessionId: "session-1", revision: 1, status: "running", activeTurnId: null, providerThreadId: "thread-1", providerSessionId: "provider-session-1", - providerModel: "gpt-test", + providerModel: { id: "gpt-test", provider: "openai" }, config: { seedState: JSON.stringify(seed), workingDirectory: "/private/worktree", - scenario: "hb-baseline", + scenario: { id: "hb-baseline" }, capabilities: ["task:read"], explicitClaims: [], turnTimeoutMs: 1_000, }, authority: { + active: true, companyId: "company-1", actorId: "actor-1", taskId: "task-1", runId: "run-1", sessionId: "session-1", + scenarioId: "hb-baseline", + capabilities: ["task:read"], + explicitClaims: [], }, transcript: [], evidence: [{ @@ -64,8 +68,9 @@ function snapshot(): CapabilityLiveSessionSnapshot { }, }], mockState: JSON.stringify(current), + authorizationRecords: [], process: null, - networkEvidence: [], + networkEvidence: { realPaperclipRequests: 0, childPaperclipEnvironmentKeys: [] }, createdAt: "2026-08-09T00:00:00.000Z", updatedAt: "2026-08-09T00:00:01.000Z", stateHistory: [{ diff --git a/packages/paperclip-runner/src/drivers/acpx/antigravity-acpx-runtime.test.ts b/packages/paperclip-runner/src/drivers/acpx/antigravity-acpx-runtime.test.ts index 93ce3d63825..c8aef50aaa0 100644 --- a/packages/paperclip-runner/src/drivers/acpx/antigravity-acpx-runtime.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/antigravity-acpx-runtime.test.ts @@ -168,5 +168,6 @@ it("rejects an unacknowledged thinking-level change after a native family model const f = await fixture("family-change"), owner = await f.open(); const result = await f.turn(owner); expect(result.status).toBe("failed"); + if (result.status !== "failed") throw new Error("turn did not fail"); expect(result.error?.message).toMatch(/admission/i); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/antigravity-admission.test.ts b/packages/paperclip-runner/src/drivers/acpx/antigravity-admission.test.ts index 728b1fd2a32..343317d0f29 100644 --- a/packages/paperclip-runner/src/drivers/acpx/antigravity-admission.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/antigravity-admission.test.ts @@ -22,7 +22,7 @@ function response(message: ReturnType, changes: Record }, } }; } const prompt = () => ({ id: "turn", method: "session/prompt", params: { sessionId: "native-session", prompt: [{ type: "text", text: "Do the task" }] } }); diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.test.ts b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.test.ts index f49e42294b7..3fbcc41260e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.test.ts @@ -2,7 +2,7 @@ import { appendSemanticToolReceipt } from "../semantic-tool-receipt.js"; import { readFileSync } from "node:fs"; import { createHash } from "node:crypto"; import { cursorToolIdentity } from "./cursor-plan-tool-identity.js"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi, type Mock } from "vitest"; import { AcpxProviderProcessLostError } from "./codex-runtime-adapter.js"; import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js"; @@ -31,7 +31,7 @@ describe("Codex ACPX harness driver", () => { it("captures the direct driver's normalized input only when the exact proposal is retained", async () => { const fixture = driverFixture({ agent: "copilot", model: "explicit-test-model", providerPolicy: { readOnly: true } }, { runtimeEvents: [] }); const session = await fixture.driver.openSession({ runId: "run-normalized-receipt", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); - await session.startTurn({ message: { text: "Complete" } }); + await session.startTurn({ message: { role: "user", text: "Complete" } }); const { schema: _, attentionRequests: __, artifacts: ___, ...raw } = completedResult(); const commit = vi.fn(), capture = vi.fn((value: unknown) => { expect(commit).not.toHaveBeenCalled(); return commit; }); const handler = fixture.hostOptions!.semanticTools!.handler; @@ -58,7 +58,7 @@ describe("Codex ACPX harness driver", () => { runtimeEvents: Array.from({ length: 8 }, (_, n) => ({ type: "text_delta" as const, stream: "output" as const, text: `bounded-${n}` })), }); const session = await fixture.driver.openSession({ runId: "run-normalized-pressure", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); - await session.startTurn({ message: { text: "Complete" } }); + await session.startTurn({ message: { role: "user", text: "Complete" } }); await vi.waitFor(async () => expect((await session.transcript!()).eventCount).toBeGreaterThanOrEqual(4)); const commit = vi.fn(), capture = vi.fn(() => commit); await expect(fixture.hostOptions!.semanticTools!.handler({ tool: PRP_COMPLETION_TOOL_NAME, callId: "finish", arguments: completedResult(), @@ -107,23 +107,23 @@ describe("Codex ACPX harness driver", () => { }, } }; fixture.host.status.mockResolvedValueOnce(before as never).mockResolvedValue(after as never); - await session.startTurn({ message: { text: "fixture" } }); + await session.startTurn({ message: { role: "user", text: "fixture" } }); fixture.finishTurn({ status: "completed", stopReason: "end_turn" }); const events = await collectUntil(session.events(), "turn.completed"); const notices = events.filter(e => e.eventType === "provider.notice.recorded" && e.payload.category === "cursor_native_usage_observed"); expect(notices).toHaveLength(agent === "cursor" ? 1 : 0); - expect(events.filter(e => e.eventType === "usage.updated")).toEqual([]); + expect(events.filter(e => e.eventType === "usage.reported")).toEqual([]); expect(events.at(-1)?.eventType).toBe("turn.completed"); await session.close({ reason: "partial diagnostics verified" }); }); it.each(["tool-1", "tool with spaces", "🧭/plan", "x".repeat(300)])("binds a native Cursor plan request to its actual projected tool identity: %s", async toolCallId => { const fixture = driverFixture({ agent: "cursor", model: "explicit-test-model", mode: "plan", providerPolicy: { readOnly: false } }, { - runtimeEvents: [{ type: "tool_call", tag: "tool_call", toolCallId, title: "arbitrary tool display", kind: "execute", status: "pending" }], + runtimeEvents: [{ type: "tool_call", tag: "tool_call", toolCallId, title: "arbitrary tool display", kind: "execute", status: "pending", text: "arbitrary tool display" }], }); const session = await fixture.driver.openSession({ runId: "run-plan-identity", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); const toolEvents = collectUntil(session.events(), "tool.execution.started"); - const { turnId } = await session.startTurn({ message: { text: "Plan" } }); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Plan" } }); const tool = (await toolEvents).find(e => e.eventType === "tool.execution.started")!; const created = collectUntil(session.events(), "runtime_request.created"); const callback = fixture.host.startTurn.mock.calls[0]![0].onExtensionRequest!; @@ -145,7 +145,7 @@ describe("Codex ACPX harness driver", () => { ]; const fixture = driverFixture({ agent, model: "explicit-model", providerPolicy: { readOnly: true } }, { runtimeEvents }); const session = await fixture.driver.openSession({ runId: "other-provider-identity", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); - const { turnId } = await session.startTurn({ message: { text: "fixture" } }); + const { turnId } = await session.startTurn({ message: { role: "user", text: "fixture" } }); fixture.finishTurn({ status: "completed" }); const events = await collectUntil(session.events(), "turn.completed"); const messages = events.filter(event => event.payload.kind === "agentMessage"); @@ -162,7 +162,7 @@ describe("Codex ACPX harness driver", () => { runtimeEvents.push(chunk(undefined, "Info: native session activity")); const fixture = driverFixture({ agent: "copilot", model: "explicit-model", providerPolicy: { readOnly: true } }, { runtimeEvents }); const session = await fixture.driver.openSession({ runId: "run-copilot-boundaries", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); - await session.startTurn({ message: { text: "Answer exactly" } }); + await session.startTurn({ message: { role: "user", text: "Answer exactly" } }); fixture.finishTurn({ status: scenario === "cancelled" ? "cancelled" : "completed" }); const events = await collectUntil(session.events(), scenario === "cancelled" ? "turn.interrupted" : "turn.completed"); const interim = events.find(event => event.eventType === "item.delta" && event.payload.text === "EXACT_MARKER"); @@ -179,7 +179,7 @@ describe("Codex ACPX harness driver", () => { it("keeps Pi retry-only activity as a rich notice without inventing a final answer", async () => { const fixture = driverFixture({ agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", providerPolicy: { readOnly: true } }, { runtimeEvents: [] }); const session = await fixture.driver.openSession({ runId: "run-pi-notice", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); - await session.startTurn({ message: { text: "Work" } }); + await session.startTurn({ message: { role: "user", text: "Work" } }); const input = fixture.host.startTurn.mock.calls[0]![0]; await input.onExtensionNotification!("paperclip/pi_notice", { sessionId: "backend-1", category: "auto_retry_end", summary: "Retry did not recover.", severity: "warning", details: { attempt: 2, success: false } }); fixture.finishTurn({ status: "completed" }); @@ -201,7 +201,7 @@ describe("Codex ACPX harness driver", () => { } const fixture = driverFixture({ agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", providerPolicy: { readOnly: true } }, { runtimeEvents }); const session = await fixture.driver.openSession({ runId: "run-pi-boundaries", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); - await session.startTurn({ message: { text: "Answer exactly" } }); + await session.startTurn({ message: { role: "user", text: "Answer exactly" } }); fixture.finishTurn({ status: scenario === "cancelled" ? "cancelled" : "completed" }); const events = await collectUntil(session.events(), scenario === "missing-end" ? "turn.failed" : scenario === "cancelled" ? "turn.interrupted" : "turn.completed"); expect(events.some(event => event.eventType === "item.delta" && event.payload.text === "Calling finish.")).toBe(true); @@ -233,7 +233,7 @@ describe("Codex ACPX harness driver", () => { } const fixture = driverFixture({ agent: "kimi", model: "kimi-for-coding", providerPolicy: { readOnly: true } }, { runtimeEvents }); const session = await fixture.driver.openSession({ runId: "run-kimi-boundaries", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); - const { turnId } = await session.startTurn({ message: { text: "Answer exactly" } }); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Answer exactly" } }); fixture.finishTurn({ status: scenario === "cancelled" ? "cancelled" : "completed" }); const events = await collectUntil(session.events(), scenario === "missing-end" ? "turn.failed" : scenario === "cancelled" ? "turn.interrupted" : "turn.completed"); const progress = events.find(event => event.eventType === "item.delta" && event.payload.text === "Calling finish.")!; @@ -256,34 +256,34 @@ describe("Codex ACPX harness driver", () => { const session = await fixture.driver.openSession({ runId: "run-controls", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); expect(fixture.hostOptions?.providerPolicy).toEqual({ readOnly: true }); expect(session.turnControlCapabilities?.()).toEqual({ steering: true, queuedFollowUp: true }); - const { turnId } = await session.startTurn({ message: { text: "Work" } }); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Work" } }); const turnInput = fixture.host.startTurn.mock.calls[0]![0]; const context = { requestId: 0, signal: new AbortController().signal }; await expect(turnInput.onExtensionRequest!("provider/request", { sessionId: "agent-1" }, context)).rejects.toThrow(/session mismatch/); - await session.steer!({ turnId, correlationId: "control-1", message: { text: "Change focus" } }); - await session.steer!({ turnId, correlationId: "control-2", mode: "follow_up", message: { text: "Then validate" } }); + await session.steer!({ turnId, correlationId: "control-1", message: { role: "user", text: "Change focus" } }); + await session.steer!({ turnId, correlationId: "control-2", mode: "follow_up", message: { role: "user", text: "Then validate" } }); expect(fixture.host.steerActiveTurn).toHaveBeenCalledExactlyOnceWith("Change focus", `run-controls:${turnId}`); expect(fixture.host.queueFollowUp).toHaveBeenCalledExactlyOnceWith("Then validate", `run-controls:${turnId}`); expect(fixture.host.interruptActiveTurn).not.toHaveBeenCalled(); - await expect(session.steer!({ turnId, correlationId: "control-2", message: { text: "Duplicate" } })).rejects.toThrow(/already attempted/); - await expect(session.steer!({ turnId: "wrong-turn", message: { text: "Stale" } })).rejects.toThrow(); + await expect(session.steer!({ turnId, correlationId: "control-2", message: { role: "user", text: "Duplicate" } })).rejects.toThrow(/already attempted/); + await expect(session.steer!({ turnId: "wrong-turn", message: { role: "user", text: "Stale" } })).rejects.toThrow(); fixture.finishTurn({ status: "completed" }); const events = await collectUntil(session.events(), "turn.completed"); expect(events.filter(event => event.payload.kind === "steering_acknowledgement").map(event => event.payload.mode)).toEqual(["steer", "follow_up"]); for (const event of events) expect(validatePrpEvent(event).ok).toBe(true); - await expect(session.steer!({ turnId, message: { text: "Already settled" } })).rejects.toThrow(); + await expect(session.steer!({ turnId, message: { role: "user", text: "Already settled" } })).rejects.toThrow(); await session.close({ reason: "controls verified" }); }); it("rejects unnegotiated controls and retains ambiguous delivery attempts", async () => { const fixture = driverFixture({ agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", providerPolicy: { readOnly: true } }, { runtimeEvents: [] }); const session = await fixture.driver.openSession({ runId: "run-controls", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); - const { turnId } = await session.startTurn({ message: { text: "Work" } }); - await expect(session.steer!({ turnId, message: { text: "No handshake" } })).rejects.toThrow(/did not negotiate/); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Work" } }); + await expect(session.steer!({ turnId, message: { role: "user", text: "No handshake" } })).rejects.toThrow(/did not negotiate/); expect(fixture.host.steerActiveTurn).not.toHaveBeenCalled(); fixture.host.steeringCapability.mockReturnValue({ steering: true, queuedFollowUp: false }); fixture.host.steerActiveTurn.mockRejectedValueOnce(new Error("delivery acknowledgement lost")); - const input = { turnId, correlationId: "ambiguous", message: { text: "Maybe delivered" } }; + const input = { turnId, correlationId: "ambiguous", message: { role: "user" as const, text: "Maybe delivered" } }; await expect(session.steer!(input)).rejects.toThrow(/acknowledgement lost/); await expect(session.steer!(input)).rejects.toThrow(/already attempted/); expect(fixture.host.steerActiveTurn).toHaveBeenCalledOnce(); @@ -453,7 +453,7 @@ describe("Codex ACPX harness driver", () => { ], }); const session = await fixture.driver.openSession({ runId: "run-claude-banners", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); - await session.startTurn({ message: { text: "Fix it" } }); + await session.startTurn({ message: { role: "user", text: "Fix it" } }); fixture.finishTurn({ status: "completed" }); const events = await collectUntil(session.events(), "turn.completed"); for (const event of events) validatePrpEvent(event); @@ -713,11 +713,11 @@ describe("Codex ACPX harness driver", () => { signal: new AbortController().signal, }), ).rejects.toThrow("does not match"); - await session.interrupt({ turnId, reason: "user cancelled" }); + await session.interrupt!({ turnId, reason: "user cancelled" }); expect(fixture.host.interruptActiveTurn).toHaveBeenCalledWith( "user cancelled", ); - await expect(session.interrupt({ turnId: "stale-turn" })).rejects.toThrow( + await expect(session.interrupt!({ turnId: "stale-turn" })).rejects.toThrow( "is not the active turn", ); await session.close({ reason: "cancelled" }); @@ -1291,13 +1291,13 @@ describe("Codex ACPX harness driver", () => { const fixture = driverFixture({}, { closeSettlementTimeoutMs: 1 }); const hostA = { ...fixture.host, - close: vi.fn(async () => { + close: vi.fn(async (_input: { reason: string }): Promise => { throw new Error("host A cleanup failed"); }), }; const hostB = { ...fixture.host, - close: vi.fn(async () => { + close: vi.fn(async (_input: { reason: string }): Promise => { throw new Error("host B cleanup failed"); }), }; @@ -1606,7 +1606,7 @@ describe("Codex ACPX harness driver", () => { ).rejects.toThrow("awaiting canonical terminal-event retention"); expect(dynamicToolHandler).not.toHaveBeenCalled(); await expect( - session.interrupt({ turnId, reason: "too late" }), + session.interrupt!({ turnId, reason: "too late" }), ).rejects.toThrow("awaiting canonical terminal-event retention"); expect(fixture.host.interruptActiveTurn).not.toHaveBeenCalled(); await expect(session.snapshot()).resolves.toMatchObject({ @@ -1761,7 +1761,7 @@ describe("Codex ACPX harness driver", () => { const fixture = driverFixture({ agent: "copilot", model: "gpt-5.6-luna", providerPolicy: { readOnly: false } }, { eventStreamFailure: failure.promise }); const session = await fixture.driver.openSession({ runId: "run-provider-death", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); const created = collectUntil(session.events(), "runtime_request.created"); - const { turnId } = await session.startTurn({ message: { text: "Edit the file" } }); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Edit the file" } }); const callback = fixture.host.startTurn.mock.calls[0]![0].onPermissionRequest!; const controller = new AbortController(); const response = callback({ inferredKind: "edit", raw: { @@ -1895,11 +1895,11 @@ describe("Codex ACPX harness driver", () => { it.each(["cursor-tool", "native\u0000tool", "native\u007ftool"].flatMap(toolCallId => ["written", "failed"].map(outcome => ({ toolCallId, outcome }))))("binds Cursor denial evidence to its original tool and response write: $outcome/$toolCallId", async ({ outcome, toolCallId }) => { const command = "printf 'sensitive-value' > /workspace/denied.txt"; const fixture = driverFixture({ agent: "cursor", model: "explicit-test-model", providerPolicy: { readOnly: false } }, { - runtimeEvents: [{ type: "tool_call", tag: "tool_call", toolCallId, title: "Run command", kind: "execute", status: "pending", rawInput: { command } }], + runtimeEvents: [{ type: "tool_call", tag: "tool_call", toolCallId, title: "Run command", kind: "execute", status: "pending", text: "Run command", rawInput: { command } }], }); const session = await fixture.driver.openSession({ runId: "run-cursor-receipt", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); const origin = collectUntil(session.events(), "tool.execution.started"); - const { turnId } = await session.startTurn({ message: { text: "Request the command." } }); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Request the command." } }); const originEvents = await origin; const created = collectUntil(session.events(), "runtime_request.created"); const callback = fixture.host.startTurn.mock.calls[0]![0].onPermissionRequest!; @@ -1942,7 +1942,7 @@ describe("Codex ACPX harness driver", () => { const fixture = driverFixture({ agent, providerPolicy: { readOnly: false } }, { runtimeEvents }); const session = await fixture.driver.openSession({ runId: "run-attached", normalizedSessionId: "session-1", workingDirectory: "/fixture/workspace" }); const completed = collectUntil(session.events(), "turn.completed"); - const { turnId } = await session.startTurn({ message: { text: "Start attached work." } }); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Start attached work." } }); fixture.finishTurn({ status: "completed", stopReason: "end_turn" }); const events = await completed; const evidence = events.filter(event => event.eventType === "provider.notice.recorded" && event.payload.category === "copilot_tool_evidence_v1"); @@ -1976,6 +1976,7 @@ describe("Codex ACPX harness driver", () => { const providerResponse = onElicitation( { mode: "form", + sessionId: "backend-1", message: "Choose deployment settings.", requestedSchema: { type: "object", @@ -2065,6 +2066,8 @@ describe("Codex ACPX harness driver", () => { onElicitation( { mode: "form", + sessionId: "backend-1", + message: "Provide a value.", requestedSchema: { type: "object", properties: { value: { type: "string" } }, @@ -2101,6 +2104,8 @@ describe("Codex ACPX harness driver", () => { const providerResponse = onElicitation( { mode: "form", + sessionId: "backend-1", + message: "Provide a value.", requestedSchema: { type: "object", properties: { value: { type: "string" } }, @@ -2148,6 +2153,8 @@ describe("Codex ACPX harness driver", () => { const providerResponse = onElicitation( { mode: "form", + sessionId: "backend-1", + message: "Provide a value.", requestedSchema: { type: "object", properties: { value: { type: "string" } }, @@ -2194,6 +2201,8 @@ describe("Codex ACPX harness driver", () => { const providerResponse = onElicitation( { mode: "form", + sessionId: "backend-1", + message: "Provide a value.", requestedSchema: { type: "object", properties: { value: { type: "string" } }, @@ -2262,6 +2271,8 @@ describe("Codex ACPX harness driver", () => { const providerResponse = onElicitation( { mode: "form", + sessionId: "backend-1", + message: "Provide a value.", requestedSchema: { type: "object", properties: { value: { type: "string" } }, @@ -3177,12 +3188,12 @@ function driverFixture( function recoveryWorkspaceLease( path = "/workspace", ): AcpxRecoveryWorkspaceLease & { - assertHeld: ReturnType; - close: ReturnType; + assertHeld: Mock<() => void>; + close: Mock<() => Promise>; } { return { path, - assertHeld: vi.fn(), + assertHeld: vi.fn<() => void>(), close: vi.fn(async () => undefined), }; } @@ -3207,6 +3218,7 @@ function fakeHost(createTurn: () => AcpxRuntimeTurn, onClose: () => void) { workspacePath: "/workspace", workspaceDigest: `sha256:${"b".repeat(64)}`, runtimeRoot: "/runtime/acpx/session-1", + commandDigest: `sha256:${"c".repeat(64)}`, profileDigest: `sha256:${"a".repeat(64)}`, requestedModel: "gpt-5.6-sol", effectiveModel: "gpt-5.6-sol", @@ -3225,7 +3237,7 @@ function fakeHost(createTurn: () => AcpxRuntimeTurn, onClose: () => void) { steerActiveTurn: vi.fn(async (_text: string, _requestId?: string) => undefined), queueFollowUp: vi.fn(async (_text: string, _requestId?: string) => undefined), interruptActiveTurn: vi.fn(async () => undefined), - close: vi.fn(async () => { + close: vi.fn(async (_input: { reason: string }) => { onClose(); }), }; diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts index 11c16adc8a5..467f2d11ee9 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts @@ -7,6 +7,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { + AcpPermissionRequest, AcpAgentRegistry, AcpRuntime, AcpRuntimeHandle, @@ -114,7 +115,7 @@ describe("Codex ACPX runtime adapter", () => { if (!restoring) spawnProvider(); return HANDLE; }); - vi.mocked(runtime.setConfigOption).mockImplementation(async input => { + vi.mocked(runtime.setConfigOption!).mockImplementation(async input => { if (restoring) spawnProvider(); const guard = created.protocolGuardFactory!(); guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } }); @@ -546,7 +547,7 @@ describe("Codex ACPX runtime adapter", () => { const options = openOptions(fakeCommand()); options.profile = { ...options.profile, agent: "cursor" }; options.refreshConsumedCommand = vi.fn(async () => undefined); let created!: AcpRuntimeOptions; - vi.mocked(runtime.setConfigOption).mockImplementation(async () => { + vi.mocked(runtime.setConfigOption!).mockImplementation(async () => { const guard = created.protocolGuardFactory!(); const binding = cursorInstructionBinding(options.systemInstructions); guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } }); @@ -574,7 +575,7 @@ describe("Codex ACPX runtime adapter", () => { let created!: AcpRuntimeOptions; const binding = cursorInstructionBinding(options.systemInstructions); const configs = (mode: string) => [{ id: "mode", currentValue: mode }]; - vi.mocked(runtime.setConfigOption).mockImplementation(async input => { + vi.mocked(runtime.setConfigOption!).mockImplementation(async input => { const guard = created.protocolGuardFactory!(); guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } }); guard("inbound", { id: 0, result: { modes: { currentModeId: "agent" }, configOptions: configs("agent"), _meta: { paperclipCursorInstructions: { @@ -586,7 +587,7 @@ describe("Codex ACPX runtime adapter", () => { const port = await openCodexAcpxRuntime(options, { createRegistry: () => registry(), createStore: () => store(), createRuntime: value => { created = value; return runtime; }, }); - expect(vi.mocked(runtime.setConfigOption).mock.calls.map(([call]) => [call.key, call.value])).toEqual([["model", options.profile.reportedModelId], ["mode", selected]]); + expect(vi.mocked(runtime.setConfigOption!).mock.calls.map(([call]) => [call.key, call.value])).toEqual([["model", options.profile.reportedModelId], ["mode", selected]]); expect(options.refreshConsumedCommand).toHaveBeenCalledTimes(2); expect(await port.identity()).toMatchObject({ mode: selected }); expect(runtime.startTurn).not.toHaveBeenCalled(); @@ -652,13 +653,13 @@ describe("Codex ACPX runtime adapter", () => { it("requires the Pi capability handshake and acknowledgments, preserves follow-up order, and never starts a second prompt", async () => { const pending = pendingExtensionTurn("turn-1"); - const runtime = Object.assign(fakeRuntime(), { requestExtension: vi.fn(async () => ({ accepted: true })) }); + const runtime = Object.assign(fakeRuntime(), { requestExtension: vi.fn(async (_input: unknown) => ({ accepted: true })) }); vi.mocked(runtime.startTurn).mockReturnValue(pending.turn); let created!: AcpRuntimeOptions & { onAgentInitialize?: (result: unknown) => void }; const options = openOptions(fakeCommand()); options.profile = { ...options.profile, agent: "pi" }; options.piThinkingLevel = "low"; - vi.mocked(runtime.setConfigOption).mockImplementation(async input => { + vi.mocked(runtime.setConfigOption!).mockImplementation(async input => { const guard = created.protocolGuardFactory!(); guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } }); guard("inbound", { id: 0, result: { modes: { currentModeId: "high" }, configOptions: [{ id: "thought_level", currentValue: "high" }] } }); @@ -734,9 +735,7 @@ describe("Codex ACPX runtime adapter", () => { expect(runtimeOptions!.permissionPolicy).toMatchObject({ defaultAction: "escalate" }); const turn = port.startTurn({ text: "Attempt a write.", requestId: "permission-test" }); try { - await runtimeOptions!.onPermissionRequest!({ - sessionId: "backend-1", inferredKind: "write", raw: {}, - }, { signal: new AbortController().signal }); + await runtimeOptions!.onPermissionRequest!(permissionRequest("backend-1", "edit"), { signal: new AbortController().signal }); expect(signal?.aborted).toBe(true); await expect(turn.result).rejects.toThrow("Approval required"); await expect((async () => { for await (const _event of turn.events) { /* drain */ } })()) @@ -2361,21 +2360,13 @@ describe("Codex ACPX runtime adapter", () => { ]); await expect( runtimeOptions?.onPermissionRequest?.( - { - sessionId: "session-1", - inferredKind: "execute", - raw: { _meta: { is_mcp_tool_approval: true } }, - }, + permissionRequest("session-1", "execute", { _meta: { is_mcp_tool_approval: true } }), { signal: new AbortController().signal }, ), ).resolves.toEqual({ outcome: "reject_once" }); await expect( runtimeOptions?.onPermissionRequest?.( - { - sessionId: "session-1", - inferredKind: "execute", - raw: {}, - }, + permissionRequest("session-1", "execute"), { signal: new AbortController().signal }, ), ).resolves.toEqual({ outcome: "reject_once" }); @@ -2399,11 +2390,7 @@ describe("Codex ACPX runtime adapter", () => { await expect( runtimeOptions?.onPermissionRequest?.( - { - sessionId: "session-1", - inferredKind: "write", - raw: {}, - }, + permissionRequest("session-1", "edit"), { signal: new AbortController().signal }, ), ).resolves.toEqual({ outcome: "reject_once" }); @@ -2438,11 +2425,7 @@ describe("Codex ACPX runtime adapter", () => { expect(runtimeOptions?.nonInteractivePermissions).toBe("fail"); await expect( runtimeOptions?.onPermissionRequest?.( - { - sessionId: "backend-1", - inferredKind: "write", - raw: {}, - }, + permissionRequest("backend-1", "edit"), { signal: new AbortController().signal }, ), ).resolves.toEqual(expected); @@ -3618,8 +3601,18 @@ function fakeCommand(): VerifiedAcpxCommandLease { return { spawn: vi.fn(), close: vi.fn() }; } -function fakeChild(): ChildProcess { - const child = new EventEmitter() as ChildProcess; +type FakeChildProcess = ChildProcess & { exitCode: number | null; signalCode: NodeJS.Signals | null }; + +function permissionRequest( + sessionId: string, + inferredKind: AcpPermissionRequest["inferredKind"], + raw: Record = {}, +): AcpPermissionRequest { + return { sessionId, inferredKind, raw: raw as unknown as AcpPermissionRequest["raw"] }; +} + +function fakeChild(): FakeChildProcess { + const child = new EventEmitter() as FakeChildProcess; Object.defineProperties(child, { exitCode: { value: null, writable: true }, signalCode: { value: null, writable: true }, @@ -3633,10 +3626,10 @@ function fakeChild(): ChildProcess { } function failingSignalChild(): { - child: ChildProcess; + child: FakeChildProcess; errors: [Error, Error]; } { - const child = new EventEmitter() as ChildProcess; + const child = new EventEmitter() as FakeChildProcess; const errors: [Error, Error] = [ new Error("SIGTERM delivery failed"), new Error("SIGKILL delivery failed"), @@ -3654,8 +3647,8 @@ function failingSignalChild(): { return { child, errors }; } -function childThatExitsAfterSignalCalls(exitAfter: number): ChildProcess { - const child = new EventEmitter() as ChildProcess; +function childThatExitsAfterSignalCalls(exitAfter: number): FakeChildProcess { + const child = new EventEmitter() as FakeChildProcess; Object.defineProperties(child, { exitCode: { value: null, writable: true }, signalCode: { value: null, writable: true }, @@ -3676,8 +3669,8 @@ function childThatExitsAfterSignalCalls(exitAfter: number): ChildProcess { return child; } -function stubbornChild(): ChildProcess { - const child = new EventEmitter() as ChildProcess; +function stubbornChild(): FakeChildProcess { + const child = new EventEmitter() as FakeChildProcess; Object.defineProperties(child, { pid: { value: 71_001 }, exitCode: { value: null, writable: true }, @@ -3691,8 +3684,8 @@ function stubbornChild(): ChildProcess { return child; } -function childThatExitsOnKill(): ChildProcess { - const child = new EventEmitter() as ChildProcess; +function childThatExitsOnKill(): FakeChildProcess { + const child = new EventEmitter() as FakeChildProcess; Object.defineProperties(child, { pid: { value: 71_002 }, exitCode: { value: null, writable: true }, diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-runtime-host.test.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-runtime-host.test.ts index 536220811fa..cb2cf018190 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-runtime-host.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-runtime-host.test.ts @@ -20,7 +20,7 @@ it("reports missing Copilot authentication before spawn and releases admission o agentServerPackageJsonPath: join(root, "provider/package.json"), agentRuntimePackageJsonPath: null, openCommand }), openRuntime, reportRetainedCleanupFailure: () => {}, }; - const options = { agent: "copilot" as const, model: "explicit-model", normalizedSessionId: "copilot-auth-test", + const options = { agent: "copilot" as const, model: "explicit-model", permissionMode: "approve-reads" as const, normalizedSessionId: "copilot-auth-test", runtimeDirectory, workingDirectory, providerPolicy: { readOnly: false } }; try { await expect(AcpxRuntimeHost.open({ ...options, environment: { GH_TOKEN: "ambient-forbidden", GITHUB_TOKEN: "ambient-forbidden" } }, dependencies)) diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts index 1e77f46018f..a921c5cc396 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts @@ -1,15 +1,16 @@ +/// import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { validateAcpxRichEvent } from "./profile-extensions.js"; -import { PAPERCLIP_QUESTION_RESPONSE_SCHEMA } from "../../contracts/question-set.js"; +import { PAPERCLIP_QUESTION_RESPONSE_SCHEMA, type PaperclipQuestionAnswer } from "../../contracts/question-set.js"; import { createCursorNotificationNormalizer, createCursorProfileExtensionAdapter, createCursorSubagentNormalizer, cursorWorkspaceArtifactReference, normalizeCursorPlanRequest, normalizeCursorQuestionRequest, normalizeCursorSubagentUpdate, } from "./cursor-extensions.js"; -const response = (answers: Record) => ({ schema: PAPERCLIP_QUESTION_RESPONSE_SCHEMA, answers }); +const response = (answers: Record) => ({ schema: PAPERCLIP_QUESTION_RESPONSE_SCHEMA, answers }); const roots: string[] = []; afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); }); async function workspace() { const root = await mkdtemp(join(tmpdir(), "cursor-extension-")); roots.push(root); return root; } diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-permission-identity.test.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-permission-identity.test.ts index 57462493056..4c95ad21263 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-permission-identity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-permission-identity.test.ts @@ -1,3 +1,4 @@ +/// import { spawn, type ChildProcess } from "node:child_process"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -43,7 +44,7 @@ it.each(["tool-first", "permission-first"])("real ACPX streams Cursor tool origi const evidence = createCursorToolEvidence({ sessionId: "native", turnId: "turn", workingDirectory: cwd, active: () => true, emit: event => { validateAcpxRichEvent(event); fields.push(Object.fromEntries((event.payload.details as Array<{ name: string; value: string }>).map(field => [field.name, field.value]))); }, }); - const runtime = createAcpRuntime({ cwd, agentRegistry: createAgentRegistry({ overrides: { cursor: "fixture" } }), sessionStore: createRuntimeStore({ stateDir: join(cwd, "state") }), permissionMode: "approve-reads", authPolicy: "skip", + const runtime = createAcpRuntime({ cwd, agentRegistry: createAgentRegistry({ overrides: { cursor: "fixture" } }), sessionStore: createRuntimeStore({ stateDir: join(cwd, "state") }), permissionMode: "approve-reads", spawnAgent: () => { const child = spawn(process.execPath, ["-e", peer], { cwd, env: { ORDER: order }, stdio: ["pipe", "pipe", "pipe"] }); children.push(child); return child; }, onAcpMessage: (direction, value) => { const message = value as { id?: number; result?: { outcome?: unknown } }; diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-sdk-interactions.test.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-sdk-interactions.test.ts index 1566e01cd7d..8bf956f9af7 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-sdk-interactions.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-sdk-interactions.test.ts @@ -1,3 +1,4 @@ +/// import { spawn } from "node:child_process"; import { createRequire } from "node:module"; import { mkdtemp, rm } from "node:fs/promises"; diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.test.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.test.ts index 0c208001bc6..5410fced811 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.test.ts @@ -34,7 +34,7 @@ it.each(["tool-first", "permission-first"])("correlates opaque C0/C1/DEL native expect(expectedIds.at(-1)).toBe("safe-tool-1"); for (const [index, toolCallId] of rawIds.entries()) { const native = { sessionId: "session", inferredKind: "execute" as const, raw: { - sessionId: "session", toolCall: { toolCallId, kind: "execute" }, + sessionId: "session", toolCall: { toolCallId, kind: "execute" as const }, options: [{ kind: "reject_once" as const, optionId: `native-denial-${index}`, name: "Deny" }], } }; const before = structuredClone(native); @@ -99,7 +99,7 @@ it("ignores inactive turns and never lets observation errors undo a delivered de }); it("bounds retained tool origins", () => { const s = setup(); for (let i = 0; i < 257; i++) s.p.tool({ ...initial, toolCallId: `tool-${i}` }); - expect(s.fields().at(-1).stage).toBe("evidence_incomplete"); expect(s.events).toHaveLength(257); + expect(s.fields().at(-1)!.stage).toBe("evidence_incomplete"); expect(s.events).toHaveLength(257); }); it("rejects commands missing from their original frame and bounded oversized input", () => { @@ -159,5 +159,5 @@ it("preserves execute denial evidence after valid edit and read permission input it.each(["edit", "read", "unknown-native-kind"])("still rejects a contradictory permission kind %s", kind => { const s = setup(); s.p.tool(initial); s.p.permission({ raw: { sessionId: "session", toolCall: { toolCallId: "tool", kind, rawInput: { path: "/fixture/file" } } } }, "request", ["decline"]); - expect(s.fields().at(-1).stage).toBe("evidence_incomplete"); expect(s.unavailable()).toBe(true); + expect(s.fields().at(-1)!.stage).toBe("evidence_incomplete"); expect(s.unavailable()).toBe(true); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/grok-native-session-options.test.ts b/packages/paperclip-runner/src/drivers/acpx/grok-native-session-options.test.ts index 2b2e8798eec..317feaf02cd 100644 --- a/packages/paperclip-runner/src/drivers/acpx/grok-native-session-options.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/grok-native-session-options.test.ts @@ -7,6 +7,11 @@ import { expect, it } from "vitest"; import { createGrokEffortAdmission } from "./grok-effort.js"; import { acpxSidecarErrorCode } from "../../cli/acpx-sidecar-input.js"; +// The patched acpx runtime implements this method without declaring it. +interface ExtensionRuntime { + requestExtension(input: { handle: AcpRuntimeHandle; method: string; params: Record; sessionMode?: "persistent" | "oneshot" }): Promise>; +} + it("retains a native Grok acknowledgement failure through the real ACPX connection", async () => { const root = await mkdtemp(join(tmpdir(), "grok-rejected-admission-")); const fixture = join(root, "grok.cjs"); @@ -94,7 +99,7 @@ it.each(["low", "high"] as const)("transports and restores native Grok %s effort handle = await runtime.ensureSession(input); expect(handle.backendSessionId).toBe(originalSession); expect(admission.isReady()).toBe(false); - await runtime.requestExtension({ handle, method: "_x.ai/session/info", params: { sessionId: handle.backendSessionId }, sessionMode: "persistent" }); + await (runtime as typeof runtime & ExtensionRuntime).requestExtension({ handle, method: "_x.ai/session/info", params: { sessionId: handle.backendSessionId }, sessionMode: "persistent" }); admission.assertReady(); const turn = runtime.startTurn({ handle, text: "Verify the restored fixture", mode: "prompt", requestId: "grok-cold-turn", timeoutMs: 5_000 }); const output: string[] = []; diff --git a/packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.test.ts b/packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.test.ts index 397cd56a238..61cd382955b 100644 --- a/packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.test.ts @@ -686,7 +686,7 @@ describe("native ACPX execution closure", () => { const fences = await Promise.all([listen(), listen()]); const fds = fences.map(server => (server as Server & { _handle?: { fd?: number } })._handle!.fd!); const installation = await verifyNativeAcpxInstallation(declaration); - const owner = createAcpxCommandLeaseOwner(await installation.openCommand(), installation.openCommand); + const owner = createAcpxCommandLeaseOwner(await installation.openCommand(), signal => installation.openCommand({ signal })); const pidFile = join(declaration.distributionRoot, "native-pid"); let port: Awaited> | undefined; let deadline: ReturnType | undefined; diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-runtime-extension.test.ts b/packages/paperclip-runner/src/drivers/acpx/pi-runtime-extension.test.ts index c11999afac8..d5e4ccca82c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-runtime-extension.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-runtime-extension.test.ts @@ -4,7 +4,7 @@ import { mkdtemp, mkdir, realpath, rename, rm, symlink, writeFile } from "node:f import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import Ajv from "ajv"; +import { Ajv } from "ajv"; import { checkPiNativeTool, installPiRuntimeExtension, piMcpRequest, readPiRuntimeConfiguration, PI_NATIVE_QUESTION_TOOL, PI_PERMISSION_TITLE_PREFIX, type PiExtensionApi, type PiRuntimeConfiguration, type PiToolDefinition, @@ -12,7 +12,7 @@ import { const directories: string[] = []; afterEach(async () => { for (const path of directories.splice(0)) await rm(path, { recursive: true, force: true }); }); -async function workspace() { +async function workspace(): Promise<{ root: string; config: PiRuntimeConfiguration }> { const root = await mkdtemp(join(tmpdir(), "paperclip-pi-extension-")); directories.push(root); await mkdir(join(root, "workspace")); await mkdir(join(root, "skills")); await mkdir(join(root, "private")); return { root, config: { @@ -20,7 +20,7 @@ async function workspace() { workspace: join(root, "workspace"), readOnly: false, readRoots: [join(root, "skills")], protectedRoots: [join(root, "private")], instructions: "Use the assigned Paperclip tools", servers: [], - } satisfies PiRuntimeConfiguration }; + } }; } function harness() { const handlers = new Map any>(); @@ -241,7 +241,7 @@ describe("owned Pi runtime extension", () => { it("revalidates paths after a human wait and never treats cancellation as approval", async () => { const { root, config } = await workspace(); const h = harness(); await installPiRuntimeExtension(h.api, config); h.handlers.get("turn_start")!(); - const context = { cwd: config.workspace, ui: { select: vi.fn(async () => { + const context = { cwd: config.workspace, ui: { select: vi.fn(async (_title: string) => { await symlink(join(root, "private"), join(config.workspace, "target")); return "Allow once"; }) } }; diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts index b2edd4c6367..cb38eec8b12 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts @@ -68,7 +68,7 @@ describe("historical ACPX profile decoding", () => { ["copilot", "copilot-profile-v85-cursor-cloud-qualification-historical-identity.json", "977d68b327f3964e8337ef8ba00230eb1b1697017db21f8010f664fee3d3c70a", "private Cloud qualification"], ["grok", "grok-profile-v14-identity.json", "08294ec6bdcbdbe46c8c8b711d8b368558a05f0d84d25c9f5fd0c3b67b79cd17", "published ed8"], ["grok", "grok-profile-v14-cursor-cloud-qualification-historical-identity.json", "fa34ba395ff51affed7e7195dc44f4e33a3c6718ada1c80e8e36852cf7af40ac", "private Cloud qualification"], - ] as const)("keeps %s identity %s immutable and decode-only", (agent, filename, checksum) => { + ] as const)("keeps %s identity %s immutable and decode-only", (agent, filename, checksum, _source) => { const bytes = readFileSync(new URL(`../../../test/fixtures/${filename}`, import.meta.url)); expect(createHash("sha256").update(bytes).digest("hex")).toBe(checksum); const identity = JSON.parse(bytes.toString("utf8")); diff --git a/packages/paperclip-runner/src/drivers/acpx/response-delivery.test.ts b/packages/paperclip-runner/src/drivers/acpx/response-delivery.test.ts index 8e2073ec064..06f721541a7 100644 --- a/packages/paperclip-runner/src/drivers/acpx/response-delivery.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/response-delivery.test.ts @@ -1,3 +1,4 @@ +/// import { describe, expect, it, vi } from "vitest"; import { deliverAcpxResponse, requireAcpxResponseDelivery } from "./response-delivery.js"; diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.test.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.test.ts index 3c663b40447..793aadce056 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.test.ts @@ -272,7 +272,7 @@ describe("ACPX runtime host", () => { it("rejects preconfigured Codex before admission resources or runtime launch", async () => { const fixture = await hostFixture(); const openRuntime = vi.fn(), prepareSandbox = vi.fn(); - await expect(AcpxRuntimeHost.open({ ...fixture.options, agent: "codex", permissionMode: "approve-all", + await expect(AcpxRuntimeHost.open({ ...fixture.options, agent: "codex", model: "gpt-5.6-sol", permissionMode: "approve-all", environment: { HOME: fixture.root, PAPERCLIP_RUNNER_HOME_MODE: "preconfigured" }, }, fixture.dependencies({ openRuntime, prepareSandbox }))).rejects.toThrow("cannot protect Runner state"); expect(prepareSandbox).not.toHaveBeenCalled(); @@ -282,7 +282,7 @@ describe("ACPX runtime host", () => { it.each(["claude", "codex"] as const)("rejects preconfigured read-only %s before provider admission", async agent => { const fixture = await hostFixture(); const openRuntime = vi.fn(); - await expect(AcpxRuntimeHost.open({ ...fixture.options, agent, permissionMode: "approve-all", + await expect(AcpxRuntimeHost.open({ ...fixture.options, agent, model: "explicit-test-model", permissionMode: "approve-all", providerPolicy: { readOnly: true }, environment: { HOME: fixture.root, PAPERCLIP_RUNNER_HOME_MODE: "preconfigured" }, }, fixture.dependencies({ openRuntime }))).rejects.toThrow("preconfigured ACPX read-only execution is not supported"); expect(openRuntime).not.toHaveBeenCalled(); @@ -291,7 +291,7 @@ describe("ACPX runtime host", () => { it.each(["approve-reads", "approve-paperclip", "deny-all"] as const)("rejects unqualified preconfigured permission mode %s before admission", async permissionMode => { const fixture = await hostFixture(); const openRuntime = vi.fn(); - await expect(AcpxRuntimeHost.open({ ...fixture.options, permissionMode, + await expect(AcpxRuntimeHost.open({ ...fixture.options, agent: "claude", model: "claude-sonnet-5", permissionMode, environment: { HOME: fixture.root, PAPERCLIP_RUNNER_HOME_MODE: "preconfigured" }, }, fixture.dependencies({ openRuntime }))).rejects.toThrow("requires ACPX approve-all"); expect(openRuntime).not.toHaveBeenCalled(); @@ -379,7 +379,7 @@ describe("ACPX runtime host", () => { ])("rejects invalid assigned connection bindings before runtime launch", async (environment) => { const fixture = await hostFixture(); const openRuntime = vi.fn(); - await expect(AcpxRuntimeHost.open({ ...fixture.options, environment }, fixture.dependencies({ openRuntime }))).rejects.toThrow(/assigned native MCP/); + await expect(AcpxRuntimeHost.open({ ...fixture.options, agent: "claude", model: "claude-sonnet-5", permissionMode: "approve-reads", environment }, fixture.dependencies({ openRuntime }))).rejects.toThrow(/assigned native MCP/); expect(openRuntime).not.toHaveBeenCalled(); }); @@ -491,7 +491,8 @@ describe("ACPX runtime host", () => { const host = await AcpxRuntimeHost.open({ ...fixture.options, agent: "claude", model: "claude-sonnet-5", - environment: { ...fixture.options.environment, + permissionMode: "approve-reads", + environment: { PAPERCLIP_NATIVE_MCP_NAME: "paperclip-assigned", PAPERCLIP_NATIVE_MCP_URL: "http://127.0.0.1:3211/mcp/gateway", PAPERCLIP_NATIVE_MCP_TOKEN: "fixture-gateway-token-".repeat(3), @@ -518,7 +519,7 @@ describe("ACPX runtime host", () => { let servers: AcpxRuntimePortOpenOptions["mcpServers"] = []; const host = await AcpxRuntimeHost.open({ ...fixture.options, agent: "claude", model: "claude-sonnet-5", permissionMode: "approve-reads", providerPolicy: { readOnly }, - environment: { ...fixture.options.environment, PAPERCLIP_NATIVE_MCP_STDIO: JSON.stringify([binding]), PAPERCLIP_NATIVE_MCP_PROXY: "/verified/runnerd" }, + environment: { PAPERCLIP_NATIVE_MCP_STDIO: JSON.stringify([binding]), PAPERCLIP_NATIVE_MCP_PROXY: "/verified/runnerd" }, }, fixture.dependencies({ openRuntime: async options => { servers = options.mcpServers; return runtimePort({ getStatus: async () => ({ models: { currentModelId: "claude-sonnet-5" } }) }); @@ -1042,12 +1043,12 @@ describe("ACPX runtime host", () => { const events: CanonicalProviderEvent[] = []; const evidence = createCopilotToolEvidence({ sessionId: "backend-1", turnId: "turn-1", workingDirectory: fixture.options.workingDirectory, active: () => true, emit: event => { validateAcpxRichEvent(event); events.push(JSON.parse(JSON.stringify(event))); } }); - let bridge: AcpxRuntimePortOpenOptions["mcpServers"][number] | undefined; + let bridge: Extract | undefined; const host = await AcpxRuntimeHost.open({ ...fixture.options, agent: "copilot", model: "gpt-5.6-sol", permissionMode: "deny-all", environment: { COPILOT_GITHUB_TOKEN: "fixture-not-a-real-credential" }, semanticTools: { tools: [{ name: "get_task_context", inputSchema: { type: "object", properties: {}, additionalProperties: false } }], handler: async () => ({ accepted: false }), captureSemanticReceipt: () => evidence.captureSemanticReceipt() }, - }, fixture.dependencies({ openRuntime: async options => { bridge = options.mcpServers[0]; return runtimePort(); } })); + }, fixture.dependencies({ openRuntime: async options => { bridge = urlBridge(options.mcpServers[0]); return runtimePort(); } })); try { const response = await fetch(bridge!.url, { method: "POST", headers: { Authorization: `Bearer ${bridge!.bearerToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "get_task_context", arguments: {} } }) }); @@ -1092,7 +1093,7 @@ describe("ACPX runtime host", () => { }, fixture.dependencies({ openRuntime: async (options) => { - bridge = options.mcpServers[0]; + bridge = urlBridge(options.mcpServers[0]); return runtimePort(); }, }), @@ -1990,10 +1991,10 @@ describe("ACPX runtime host", () => { it("retains a settled turn when shutdown cleanup must be retried", async () => { const fixture = await hostFixture(); const credential = credentialLeaseFixture(fixture.root); - let settleTurn!: (value: { stopReason: string }) => void; + let settleTurn!: (value: Awaited) => void; const turn = { ...runtimeTurn(), - result: new Promise<{ stopReason: string }>((resolve) => { + result: new Promise>((resolve) => { settleTurn = resolve; }), }; @@ -2019,7 +2020,7 @@ describe("ACPX runtime host", () => { host.startTurn({ text: "Complete the task.", requestId: "turn-1" }); const firstClose = host.close({ reason: "first shutdown" }); - settleTurn({ stopReason: "end_turn" }); + settleTurn({ status: "completed", stopReason: "end_turn" }); await expect(firstClose).rejects.toThrow(/cleanup failed/); expect(turn.cancel).toHaveBeenCalledOnce(); expect(credential.close).not.toHaveBeenCalled(); @@ -2332,6 +2333,11 @@ function credentialLeaseFixture(root: string) { }; } +function urlBridge(server: AcpxRuntimePortOpenOptions["mcpServers"][number] | undefined) { + if (!server || !("url" in server)) throw new Error("missing tool bridge"); + return server; +} + function runtimePort( input: { identity?: AcpxRuntimePort["identity"]; @@ -2402,7 +2408,7 @@ async function hostFixture() { dependencies( input: Pick & Partial< - Pick + Pick > & { // Opt-in only. When true, `prepareSandbox` runs the real // preparation once, then returns that same sandbox for every @@ -2413,19 +2419,19 @@ async function hostFixture() { ): AcpxRuntimeHostDependencies { let reusedSandbox: AcpxRuntimeSandbox | null = null; return { - verifyInstallation: async (profile) => + verifyInstallation: input.verifyInstallation ?? (async (profile) => ({ commandDigest: profile.commandDigest, agentServerPackageJsonPath: join(root, "package.json"), agentRuntimePackageJsonPath: null, openCommand: async () => command, - }) satisfies VerifiedAcpxInstallation, + }) satisfies VerifiedAcpxInstallation), openRuntime: input.openRuntime, // Record the skills home the instant the sandbox exists, ahead of // the materialize call that seals it. A test that overrides // `prepareSandbox` for a non-Claude agent replaces this wrapper, but // those agents never materialize skills, so nothing is lost. - prepareSandbox: async (sandboxInput) => { + prepareSandbox: input.prepareSandbox ?? (async (sandboxInput) => { if (input.reuseSandbox && reusedSandbox) return reusedSandbox; const sandbox = await prepareAcpxRuntimeSandbox(sandboxInput); if (sandboxInput.agent === "claude") { @@ -2433,7 +2439,7 @@ async function hostFixture() { } if (input.reuseSandbox) reusedSandbox = sandbox; return sandbox; - }, + }), retainAdmissionCleanup: trackAdmissionCleanup, reportRetainedCleanupFailure: input.reportRetainedCleanupFailure ?? vi.fn(), diff --git a/packages/paperclip-runner/src/drivers/codex/app-server-transport.test.ts b/packages/paperclip-runner/src/drivers/codex/app-server-transport.test.ts index ddeef5fc806..b42c13323d0 100644 --- a/packages/paperclip-runner/src/drivers/codex/app-server-transport.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/app-server-transport.test.ts @@ -128,10 +128,11 @@ describe("Codex app-server transport limits", () => { }); it("installs cleanup handlers before synchronously reporting process ownership", async () => { - const originalSpawn = ChildProcess.prototype.spawn; + const childProcessPrototype = ChildProcess.prototype as ChildProcess & { spawn(...args: unknown[]): unknown }; + const originalSpawn = childProcessPrototype.spawn; let spawnedProcess: ChildProcess | undefined; const spawnSpy = vi - .spyOn(ChildProcess.prototype, "spawn") + .spyOn(childProcessPrototype, "spawn") .mockImplementation(function (this: ChildProcess, ...args) { spawnedProcess = this; return originalSpawn.apply(this, args); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts index 719c7035661..13e83500aba 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts @@ -660,7 +660,7 @@ describe("Codex app-server Codex driver", () => { ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); for (const action of ["set", "pause", "resume", "clear"] as const) { await expect( - unsupported.goal?.({ action, ...(action === "set" ? { objective: "Must not reach the provider" } : {}) }), + unsupported.goal?.(action === "set" ? { action, objective: "Must not reach the provider" } : { action }), ).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError); } expect(unsupportedTransport.calls.filter(({ method }) => diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.events.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.events.test.ts index 142955c787c..19e218768af 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.events.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.events.test.ts @@ -401,7 +401,7 @@ describe("Codex app-server Codex driver", () => { { runDelta: first, runDeltaComplete: true }, { runDelta: { ...first, ...second }, runDeltaComplete: false }, ]); - expect(await session.usage()).toMatchObject({ runDeltaComplete: false }); + expect(await session.usage!()).toMatchObject({ runDeltaComplete: false }); }); it("keeps usage reported between runs in the next run's delta so totals stay exact", async () => { @@ -478,7 +478,7 @@ describe("Codex app-server Codex driver", () => { expect(events.some((event) => event.eventType === "session.failed")).toBe( false, ); - expect(await session.usage()).toMatchObject({ + expect(await session.usage!()).toMatchObject({ total: { inputTokens: 10, outputTokens: 4 }, }); }); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts index 4cb21eaa922..a0baee374a6 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.lifecycle.test.ts @@ -785,6 +785,7 @@ describe("Codex app-server Codex driver", () => { runId: "run-1", environmentLeaseId: "lease-1", runnerInstanceId: "runner-codex", + driverSessionId: "thread-1", normalizedSessionId: "normalized-1", }, capabilities, @@ -819,7 +820,7 @@ it("sends explicit skill input on every requested turn", async () => { await collectUntilTerminal(session.events()); transport.turnStartResponse = Promise.resolve({ turn: { id: "turn-2", status: "inProgress", items: [] } }); } - await session.close(); + await session.close({ reason: "fixture complete" }); }); @@ -830,7 +831,7 @@ it("reapplies explicit skills after recovery without persisting them into ordina runId: "initial", normalizedSessionId: "skill-recovery", workingDirectory: WORKSPACE, }); const snapshot = await original.snapshot(); - await original.close(); + await original.close({ reason: "fixture complete" }); for (const selected of [skillInputs, []]) { const transport = new FakeCodexTransport(); const recovery = await makeDriver([transport], { skillInputs: selected }).recoverSession(snapshot); @@ -840,6 +841,6 @@ it("reapplies explicit skills after recovery without persisting them into ordina await session.startTurn({ message: { role: "user", text: "Continue" } }); const items = transport.calls.find((call) => call.method === "turn/start")?.params.input as Array<{type:string}>; expect(items.filter((item) => item.type === "skill")).toEqual(selected); - } finally { await session.close(); } + } finally { await session.close({ reason: "fixture complete" }); } } }); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.runtime-requests.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.runtime-requests.test.ts index ada36dcf876..c3b718401ea 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.runtime-requests.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.runtime-requests.test.ts @@ -76,7 +76,7 @@ it("preserves runnerd permission expiry before a failed provider terminal and re expect(resolve).not.toHaveBeenCalled(); transport.push("turn/completed", { threadId: "thread-1", turn: { id: turnId, status: "failed", error: { code: "ACPX_PROVIDER_PROCESS_LOST", message: "Provider died" } } }); await vi.waitFor(() => expect(events.some(event => event.eventType === "turn.failed")).toBe(true)); - await session.close(); await consume; + await session.close({ reason: "fixture complete" }); await consume; const settlements = events.filter(event => ["runtime_request.expired", "runtime_request.cancelled", "runtime_request.resolved"].includes(event.eventType)); expect(settlements).toHaveLength(1); expect(settlements[0]).toMatchObject({ eventType: "runtime_request.expired", turnId, itemId: "native-edit", payload: { ...payload, turnId, itemId: "native-edit" } }); @@ -105,7 +105,7 @@ it("preserves canonical question expiry after provider loss and refuses late ans await expect(session.resolveRuntimeRequest?.({ requestId: "native-question", turnId, resolution: { action: "accept" } })).rejects.toThrow(); transport.push("turn/completed", { threadId: "thread-1", turn: { id: turnId, status: "failed", error: { code: "ACPX_PROVIDER_PROCESS_LOST", message: "Provider died" } } }); await vi.waitFor(() => expect(events.some(event => event.eventType === "turn.failed" || event.eventType === "session.failed")).toBe(true)); - await session.close(); await consume; + await session.close({ reason: "fixture complete" }); await consume; expect(events.filter(event => event.eventType === "runtime_request.expired")).toHaveLength(1); expect(events.find(event => event.eventType === "runtime_request.expired")).toMatchObject({ turnId, itemId: "question-item", payload }); expect(events.some(event => event.eventType === "session.failed" && event.payload.code === "turn_binding_mismatch")).toBe(false); @@ -127,7 +127,7 @@ it.each(["turn", "kind", "type", "replay"])("refuses canonical permission expiry requestType: kind === "type" ? "input" : "permission", replayAllowed: kind === "replay", reason: "provider_process_lost" }; const notification = runnerdRuntimeRequestExpiryNotification("runtime_request.expired", payload, "thread-1", kind === "turn" ? "foreign-turn" : turnId)!; transport.push(notification.method, notification.params); - await pending; await consume; await session.close(); + await pending; await consume; await session.close({ reason: "fixture complete" }); expect(events.some(e => e.eventType === "runtime_request.expired")).toBe(false); expect(events.some(e => e.eventType === "session.failed" && e.payload.code === "turn_binding_mismatch")).toBe(true); }); @@ -159,7 +159,7 @@ describe("Codex app-server Codex driver", () => { }); expect(await response).toEqual({ action: "accept", content: null, _meta: null }); expect(session.pendingRuntimeRequests?.()).toHaveLength(0); - await session.close(); + await session.close({ reason: "fixture complete" }); }); it("validates runtime request resolutions against the kind of request they answer", async () => { diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.semantic-results.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.semantic-results.test.ts index 8982c48fd5e..7c470f79972 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.semantic-results.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.semantic-results.test.ts @@ -68,7 +68,7 @@ describe("Codex app-server Codex driver", () => { threadId: "thread-1", turnId: "turn-1", arguments: {} }); expect(feedback).toHaveBeenCalledExactlyOnceWith({ ...report, continuation }); expect((await session.snapshot()).semanticResult?.result).toEqual({ ...report, continuation }); - } finally { await session.close(); } + } finally { await session.close({ reason: "fixture complete" }); } }); it.each([ @@ -92,7 +92,7 @@ describe("Codex app-server Codex driver", () => { expect(response).toMatchObject({ success: false }); expect(feedback).not.toHaveBeenCalled(); expect((await session.snapshot()).semanticResult).toBeNull(); - } finally { await session.close(); } + } finally { await session.close({ reason: "fixture complete" }); } }); it("returns current approval feedback and permits correcting a rejected completion report", async () => { @@ -111,7 +111,7 @@ describe("Codex app-server Codex driver", () => { expect(await call("corrected")).toMatchObject({ success: true, contentItems: [{ type: "inputText", text: expect.stringContaining("/approvals/approval-1") }] }); expect((await session.snapshot()).semanticResult?.result).toEqual(result); - await session.close(); + await session.close({ reason: "fixture complete" }); }); it("accepts an explicit response-wake yield through paperclip_finish", async () => { @@ -194,7 +194,7 @@ describe("Codex app-server Codex driver", () => { }, }, })).toMatchObject({ success: false }); - await otherSession.close(); + await otherSession.close({ reason: "fixture complete" }); }); it("makes duplicate semantic completion idempotent and rejects changed payloads", async () => { diff --git a/packages/paperclip-runner/src/drivers/codex/codex-protocol-integrity.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-protocol-integrity.test.ts index 20dbb749a2a..080b2717bbd 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-protocol-integrity.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-protocol-integrity.test.ts @@ -16,8 +16,8 @@ import { NativeSessionProtocolIntegrityError } from "../../contracts/native-sess import { DurablePrpControlPlane, durableRecoveryInternals, - type DurableRecoveryIdentity, } from "../../control-plane/durable-prp-control-plane.js"; +import type { DurableRecoveryIdentity } from "../../control-plane/prp-transport-types.js"; import { createCapabilityRunnerdCodexTransport } from "../../live/runnerd-codex-transport.js"; import { executeNativeSession } from "../../native-session-runtime.js"; import { CodexAppServerDriver } from "./codex-app-server-driver.js"; @@ -44,7 +44,7 @@ async function authenticatedRunner( runnerBinary: string, ) { const framed = (domain: string, parts: Buffer[]) => { - const values = [Buffer.from(domain), Buffer.from([0])]; + const values: Buffer[] = [Buffer.from(domain), Buffer.from([0])]; for (const part of parts) { const length = Buffer.alloc(8); length.writeBigUInt64BE(BigInt(part.length)); @@ -476,6 +476,7 @@ describe("Codex protocol integrity propagation", () => { normalizedSessionId: identity.normalizedSessionId, driverKind: "codex_app_server", protocolVersion: 1, + lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, }, provider: { kind: "codex", model: null }, completionContract: { diff --git a/packages/paperclip-runner/src/drivers/codex/codex-question-adapter.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-question-adapter.test.ts index 9297dbd2c0f..c38675e3186 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-question-adapter.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-question-adapter.test.ts @@ -335,7 +335,7 @@ describe("Codex structured question adapter", () => { questions: [{ id: "form", question: "Choose one" }], }); expect(safeForm?.description).toContain("Bearer [REDACTED]"); - expect(safeForm?.description.endsWith(preservedTail)).toBe(true); + expect(safeForm?.description?.endsWith(preservedTail)).toBe(true); expect(() => normalizeCodexQuestionSet("mcpServer/elicitation/request", { requestedSchema: { diff --git a/packages/paperclip-runner/src/drivers/codex/codex-resume-usage.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-resume-usage.test.ts index 7e986c46baf..54d325146fb 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-resume-usage.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-resume-usage.test.ts @@ -35,7 +35,7 @@ describe("Codex resume accounting through the production driver", () => { turnId: string, ) => { const saved = JSON.parse(JSON.stringify(await session.snapshot!())); - await session.close(); + await session.close({ reason: "test complete" }); transport.readResponse = { thread: { id: "thread-1", sessionId: "provider-session-1", cwd: WORKSPACE, turns: [] }, }; @@ -60,7 +60,7 @@ describe("Codex resume accounting through the production driver", () => { session = await coldRestart(session, transports[2]!, "third", "turn-3"); await expect(runTurn(session, transports[2]!, "turn-3", { inputTokens: 170, outputTokens: 20 })) .resolves.toEqual({ inputTokens: 30, outputTokens: 4 }); - await session.close(); + await session.close({ reason: "test complete" }); }); it.each(["thread", "snapshot"] as const)( @@ -137,7 +137,7 @@ describe("Codex resume accounting through the production driver", () => { expect(await session.usage!()).toMatchObject({ runDelta: { inputTokens: 40, outputTokens: 6 }, }); - await session.close(); + await session.close({ reason: "fixture complete" }); second.readResponse = readResponse(140, 16); const recovery = await driver.recoverSession(persisted); expect(recovery.recovered).toBe(true); @@ -187,7 +187,7 @@ describe("Codex resume accounting through the production driver", () => { inputTokens: 140, outputTokens: 16, }); - await recoveredSession.close(); + await recoveredSession.close({ reason: "fixture complete" }); third.readResponse = readResponse(150, 19); const secondRecovery = await driver.recoverSession(advanced); expect(secondRecovery.recovered).toBe(true); @@ -205,8 +205,8 @@ describe("Codex resume accounting through the production driver", () => { ), ).toEqual([]); } finally { - await recoveredSession?.close(); - await session.close(); + await recoveredSession?.close({ reason: "fixture complete" }); + await session.close({ reason: "fixture complete" }); } }, ); @@ -245,7 +245,7 @@ describe("Codex resume accounting through the production driver", () => { baseline: { inputTokens: 100 }, latest: { inputTokens: 140 }, }; - await session.close(); + await session.close({ reason: "fixture complete" }); const tokenUsage = { total: { inputTokens: 150 }, last: { inputTokens: 10 }, @@ -270,8 +270,8 @@ describe("Codex resume accounting through the production driver", () => { second.calls.filter((call) => call.method === "turn/start"), ).toEqual([]); } finally { - await recoveredSession?.close(); - await session.close(); + await recoveredSession?.close({ reason: "fixture complete" }); + await session.close({ reason: "fixture complete" }); } }, ); @@ -300,7 +300,7 @@ describe("Codex resume accounting through the production driver", () => { }); await collectUntilTerminal(session.events()); const persisted = JSON.parse(JSON.stringify(await session.snapshot!())); - await session.close(); + await session.close({ reason: "fixture complete" }); second.readResponse = { thread: { id: "thread-1", @@ -384,7 +384,7 @@ describe("Codex resume accounting through the production driver", () => { .filter((call) => call.method === "thread/read") .every((call) => call.params.includeTurns === false), ).toBe(true); - await session.close(); + await session.close({ reason: "fixture complete" }); }); it("keeps usage reported between runs in the next run's delta after a restore", async () => { @@ -401,7 +401,7 @@ describe("Codex resume accounting through the production driver", () => { await new Promise(resolve => setImmediate(resolve)); const persisted = JSON.parse(JSON.stringify(await session.snapshot!())); expect(persisted.codexUsageBaseline).toMatchObject({ latest: { inputTokens: 130 }, accounted: { inputTokens: 100 } }); - await session.close(); + await session.close({ reason: "test complete" }); second.readResponse = { thread: { id: "thread-1", sessionId: "provider-session-1", cwd: WORKSPACE, turns: [{ id: "turn-1", status: "completed", items: [] }], tokenUsage: { total: { inputTokens: 130, outputTokens: 10 } } } }; const recovery = await driver.recoverSession(persisted); @@ -413,7 +413,7 @@ describe("Codex resume accounting through the production driver", () => { second.push("thread/tokenUsage/updated", { threadId: "thread-1", turnId: "turn-2", tokenUsage: { total: { inputTokens: 150, outputTokens: 16 } } }); for await (const event of recovered.events()) if (event.payload.kind === "usage") break; expect(await recovered.usage!()).toMatchObject({ runDelta: { inputTokens: 50, outputTokens: 6 } }); - await recovered.close(); + await recovered.close({ reason: "test complete" }); }); it("keeps the startup external sandbox profile on subsequent turns", async () => { @@ -439,6 +439,6 @@ describe("Codex resume accounting through the production driver", () => { transport.calls.find((call) => call.method === "turn/start")?.params .permissions, ).toBe(start.params.permissions); - await session.close(); + await session.close({ reason: "fixture complete" }); }); }); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-turn-diff.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-turn-diff.test.ts index c7cc22e1663..79849db1dba 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-turn-diff.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-turn-diff.test.ts @@ -1,17 +1,19 @@ import { describe, expect, it } from "vitest"; -import { parseCodexTurnDiff, summarizeCodexTurnDiff } from "./codex-turn-diff.js"; +import { parseCodexTurnDiff, summarizeCodexTurnDiff, type ParsedCodexTurnDiffFile } from "./codex-turn-diff.js"; describe("Codex turn diff parser", () => { it("summarizes parsed files for work-product metadata", () => { - expect(summarizeCodexTurnDiff([ + const textFiles: ParsedCodexTurnDiffFile[] = [ { path: "a.ts", operation: "modify", previousPath: null, additions: 4, deletions: 2, binary: false, diff: "patch" }, { path: "b.ts", operation: "create", previousPath: null, additions: 3, deletions: 0, binary: false, diff: "patch" }, - ])).toEqual({ files: 2, additions: 7, deletions: 2 }); + ]; + expect(summarizeCodexTurnDiff(textFiles)).toEqual({ files: 2, additions: 7, deletions: 2 }); - expect(summarizeCodexTurnDiff([ + const binaryFiles: ParsedCodexTurnDiffFile[] = [ { path: "logo.png", operation: "modify", previousPath: null, additions: null, deletions: null, binary: true, diff: null }, - ])).toEqual({ files: 1, additions: null, deletions: null }); + ]; + expect(summarizeCodexTurnDiff(binaryFiles)).toEqual({ files: 1, additions: null, deletions: null }); }); it("parses a complete snapshot with bounded file statistics", () => { diff --git a/packages/paperclip-runner/src/drivers/codex/prepared-context-transport.test.ts b/packages/paperclip-runner/src/drivers/codex/prepared-context-transport.test.ts index dadddbad2b6..5a4a522dc0e 100644 --- a/packages/paperclip-runner/src/drivers/codex/prepared-context-transport.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/prepared-context-transport.test.ts @@ -91,7 +91,7 @@ describe("prepared native context transport", () => { text: expect.stringContaining('"task"'), }), ]); - expect((input?.[0] as { text: string }).text).toContain( + expect((input as Array<{ text: string }>)[0]!.text).toContain( '"message":"ordinary standalone task"', ); }); diff --git a/packages/paperclip-runner/src/drivers/cursor-cloud/app-server-proxy.test.ts b/packages/paperclip-runner/src/drivers/cursor-cloud/app-server-proxy.test.ts index 72fe00b3ec6..2cffbfa7c91 100644 --- a/packages/paperclip-runner/src/drivers/cursor-cloud/app-server-proxy.test.ts +++ b/packages/paperclip-runner/src/drivers/cursor-cloud/app-server-proxy.test.ts @@ -87,7 +87,7 @@ describe("Cursor Cloud bounded stdio proxy", () => { const terminalGate = new Promise(resolve => { releaseTerminalWrites = resolve; }); let pausedTerminalWrites = 0; const write = CursorCloudCheckpointStore.prototype.write; - vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(async function(value) { + vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(async function(this: CursorCloudCheckpointStore, value) { if (value.phase === "terminal" && value.pendingTerminalDelivery) { pausedTerminalWrites += 1; await terminalGate; @@ -151,7 +151,7 @@ describe("Cursor Cloud bounded stdio proxy", () => { }); let openedSession!: CursorCloudSessionHandle; const openSession = CursorCloudDriver.prototype.openSession; - vi.spyOn(CursorCloudDriver.prototype, "openSession").mockImplementation(async function(input) { + vi.spyOn(CursorCloudDriver.prototype, "openSession").mockImplementation(async function(this: CursorCloudDriver, input) { openedSession = await openSession.call(this, input); return openedSession; }); diff --git a/packages/paperclip-runner/src/drivers/cursor-cloud/cloud-client.test.ts b/packages/paperclip-runner/src/drivers/cursor-cloud/cloud-client.test.ts index 404986ecec0..7476a828047 100644 --- a/packages/paperclip-runner/src/drivers/cursor-cloud/cloud-client.test.ts +++ b/packages/paperclip-runner/src/drivers/cursor-cloud/cloud-client.test.ts @@ -139,7 +139,7 @@ describe("Cursor Cloud v1 HTTP client", () => { if (request.url?.includes("/artifacts/download?")) return json(response, { url: `https://${host}/proof?private-signature`, expiresAt: TIME }); response.end(bytes); }); - const externalHeaders: HeadersInit[] = []; + const externalHeaders: Array> = []; let unexpectedTargets = 0; const transport: typeof fetch = (input, init) => { const url = String(input); diff --git a/packages/paperclip-runner/src/drivers/cursor-cloud/cursor-cloud-driver.test.ts b/packages/paperclip-runner/src/drivers/cursor-cloud/cursor-cloud-driver.test.ts index 219f45cd95d..f871999474b 100644 --- a/packages/paperclip-runner/src/drivers/cursor-cloud/cursor-cloud-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/cursor-cloud/cursor-cloud-driver.test.ts @@ -71,7 +71,7 @@ function maintainedMutations(peer: Peer, maxResponseBytes?: number) { async function fixture(peer = new Peer(), extra: Partial = {}) { const root = await mkdtemp("/tmp/cc-"); roots.push(root); - const handler = vi.fn(async () => ({ success: true, contentItems: [{ type: "inputText", text: "accepted" }] })); + const handler = vi.fn(async (_call: Parameters[0]) => ({ success: true, contentItems: [{ type: "inputText", text: "accepted" }] })); const options: CursorCloudDriverOptions = { model: "explicit-model", config: { schema: "paperclip.cursor-cloud.config.v1", repos: [{ url: "https://github.com/example/repo", startingRef: "main" }] }, binding: { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId: "session" }, runtimeDirectory: root, relay: { url: "https://relay.example.test/mcp", token: "private-relay-token", socketPath: join(root, "mcp.sock"), expiresAt: Date.now() + 600_000 }, instructions: "Follow these exact instructions.", @@ -111,7 +111,11 @@ async function nextAuthority(f: Awaited>) { return { next, checkpoint: await savedCheckpoint(f.root, f.session), eventCount: f.events.length }; } -function relayHeaders(peer: Peer): Record { return structuredClone(peer.request!.mcpServers[0]!.headers!); } +function relayHeaders(peer: Peer): Record { + const server = peer.request!.mcpServers![0]!; + if (!("headers" in server)) throw new Error("Expected an HTTP relay MCP server"); + return structuredClone(server.headers!); +} async function rpc(path: string, body: unknown, token: string | Record = "private-relay-token", session?: string): Promise<{ status: number; value: any; session?: string }> { return new Promise((accept, reject) => { @@ -206,7 +210,7 @@ describe("Cursor Cloud native driver", () => { await f.session.startTurn({ message: { role: "user", text: "Stop an observation in flight" } }); await reading.promise; try { - await f.session.interrupt!(); + await f.session.interrupt!({}); await vi.waitFor(() => expect(readAborted).toBe(true), { timeout: 100 }); expect((await f.session.read()).phase).toBe("terminal"); } finally { blocked.resolve(await original()); } @@ -257,7 +261,7 @@ describe("Cursor Cloud native driver", () => { await f.session.startTurn({ message: { role: "user", text: "Stop during a provider retry delay" } }); await waitFor(() => f.events.some(event => (event.payload as { code?: string }).code === "cursor_cloud_observation_retry")); const signal = ((peer.getRun.mock.calls[0] as unknown[])[2] as { signal: AbortSignal }).signal; - await f.session.interrupt!(); + await f.session.interrupt!({}); expect(signal.aborted).toBe(true); expect((await f.session.read()).phase).toBe("terminal"); expect(peer.createAgent).toHaveBeenCalledTimes(1); expect(peer.createRun).not.toHaveBeenCalled(); @@ -354,7 +358,7 @@ describe("Cursor Cloud native driver", () => { } } await reading.promise; - await session.interrupt!(); + await session.interrupt!({}); await acknowledgeTerminal(session); await session.attachRun!({ runId: "paperclip-followup" }); const next = await session.startTurn({ message: { role: "user", text: "Second task" } }); @@ -370,7 +374,7 @@ describe("Cursor Cloud native driver", () => { expect(JSON.parse(await readFile(checkpointPath, "utf8"))).toEqual(before); expect(await session.read()).toMatchObject({ agentId: previous.agentId, runId: followupRunId, turnId: next.turnId, phase: "running", pendingTerminalDelivery: null }); expect(events.slice(eventCount)).toEqual([]); - await session.interrupt!(); + await session.interrupt!({}); expect(await session.read()).toMatchObject({ runId: followupRunId, phase: "terminal", pendingTerminalDelivery: { providerRunId: followupRunId, turnId: next.turnId, status: "CANCELLED" } }); expect(events.filter(event => event.turnId === next.turnId && event.eventType === "turn.interrupted")).toHaveLength(1); }); @@ -393,14 +397,14 @@ describe("Cursor Cloud native driver", () => { }; const f = await fixture(peer); await f.session.startTurn({ message: { role: "user", text: "First task" } }); await waiting.promise; - await f.session.interrupt!(); + await f.session.interrupt!({}); const next = await nextAuthority(f); release.resolve(); await new Promise(accept => setImmediate(accept)); expect(await savedCheckpoint(f.root, f.session)).toEqual(next.checkpoint); expect(f.events.slice(next.eventCount)).toEqual([]); expect(streams.map(stream => stream.runId)).toEqual([runId, peer.native!.id]); expect(streams[1]!.signal!.aborted).toBe(false); - await f.session.interrupt!(); + await f.session.interrupt!({}); expect((await f.session.read()).pendingTerminalDelivery).toMatchObject({ providerRunId: peer.native!.id, turnId: next.next.turnId }); }); @@ -421,7 +425,7 @@ describe("Cursor Cloud native driver", () => { released.resolve(); await new Promise(accept => setImmediate(accept)); expect(await savedCheckpoint(f.root, f.session)).toEqual(next.checkpoint); expect(f.events.slice(next.eventCount)).toEqual([]); - await f.session.interrupt!(); + await f.session.interrupt!({}); expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { providerRunId: peer.native!.id, turnId: next.next.turnId } }); }); @@ -441,7 +445,7 @@ describe("Cursor Cloud native driver", () => { released.resolve(oldAgent); await oldReconcile; expect(await savedCheckpoint(f.root, f.session)).toEqual(next.checkpoint); expect(f.events.slice(next.eventCount)).toEqual([]); - await f.session.interrupt!(); + await f.session.interrupt!({}); expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { providerRunId: peer.native!.id, turnId: next.next.turnId } }); }); @@ -453,19 +457,19 @@ describe("Cursor Cloud native driver", () => { const previous = { ...peer.native! }; const oldRead = deferred(), reading = deferred(); peer.getRun.mockImplementationOnce(async () => { reading.resolve(); return oldRead.promise; }); - const oldStop = f.session.interrupt!(); await reading.promise; + const oldStop = f.session.interrupt!({}); await reading.promise; peer.getRun.mockResolvedValueOnce({ ...previous, status: "FINISHED" }); await f.session.reconcile!(); const next = await nextAuthority(f); const currentRead = deferred(), currentReading = deferred(); peer.getRun.mockImplementationOnce(async () => { currentReading.resolve(); return currentRead.promise; }); - const currentStop = f.session.interrupt!(); await currentReading.promise; + const currentStop = f.session.interrupt!({}); await currentReading.promise; const before = await savedCheckpoint(f.root, f.session); oldRead.resolve(previous); await oldStop; expect(await savedCheckpoint(f.root, f.session)).toEqual(before); expect(peer.cancelRun).not.toHaveBeenCalled(); const readCount = peer.getRun.mock.calls.length; - const duplicateStop = f.session.interrupt!(); + const duplicateStop = f.session.interrupt!({}); await new Promise(accept => setImmediate(accept)); expect(peer.getRun).toHaveBeenCalledTimes(readCount); currentRead.resolve({ ...peer.native! }); await Promise.all([currentStop, duplicateStop]); @@ -480,7 +484,7 @@ describe("Cursor Cloud native driver", () => { if (phase === "follow-up") { await f.session.startTurn({ message: { role: "user", text: "First task" } }); await waitFor(() => f.events.some(event => event.eventType === "item.delta")); - await f.session.interrupt!(); await acknowledgeTerminal(f.session); + await f.session.interrupt!({}); await acknowledgeTerminal(f.session); await f.session.attachRun!({ runId: "paperclip-followup" }); } const before = await savedCheckpoint(f.root, f.session); @@ -498,7 +502,7 @@ describe("Cursor Cloud native driver", () => { expect(await savedCheckpoint(f.root, f.session)).toEqual(before); expect(f.events.slice(eventCount)).toEqual([]); const reconciliations = peer.getAgent.mock.calls.length, cancellations = peer.cancelRun.mock.calls.length; - await f.session.interrupt!(); + await f.session.interrupt!({}); expect(peer.getAgent).toHaveBeenCalledTimes(reconciliations); expect(peer.cancelRun).toHaveBeenCalledTimes(cancellations); const snapshot = await f.session.snapshot(); await f.session.close({ reason: "storage_recovered" }); await f.pump; expect(peer.getAgent).toHaveBeenCalledTimes(reconciliations); expect(peer.cancelRun).toHaveBeenCalledTimes(cancellations); @@ -507,7 +511,7 @@ describe("Cursor Cloud native driver", () => { expect(await savedCheckpoint(f.root, recovered)).toEqual({ ...before, relayTurnToken: before.relayTurnToken ?? null }); await recovered.startTurn({ message: { role: "user", text: "Retry after storage recovery" } }); await waitFor(() => peer.createAgent.mock.calls.length === 1 && peer.createRun.mock.calls.length === (phase === "fresh" ? 0 : 1)); - await recovered.interrupt!(); + await recovered.interrupt!({}); }); it("retains accepted provider work when its confirmed-identity checkpoint write fails", async () => { @@ -531,7 +535,7 @@ describe("Cursor Cloud native driver", () => { await rm(checkpointPath, { recursive: true }); await rename(savedPath, checkpointPath); } expect(await savedCheckpoint(f.root, f.session)).toMatchObject({ agentId: peer.native!.agentId, runId, phase: "running", pendingOperation: null }); - await f.session.interrupt!(); + await f.session.interrupt!({}); expect(peer.createAgent).toHaveBeenCalledTimes(1); expect(peer.cancelRun).toHaveBeenCalledTimes(1); expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { status: "CANCELLED", providerRunId: runId } }); }); @@ -543,7 +547,7 @@ describe("Cursor Cloud native driver", () => { const failure = new Error("controlled post-publication persistence failure"); const originalWrite = CursorCloudCheckpointStore.prototype.write; let intercepted = false; - const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) { + const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) { const persisted = originalWrite.call(this, value); if (value.phase === "creating" && !intercepted) { intercepted = true; @@ -556,13 +560,13 @@ describe("Cursor Cloud native driver", () => { const started = f.session.startTurn({ message: { role: "user", text: "Stop before unsent launch" } }); const rejected = expect(started).rejects.toBe(failure); await held.promise; - const stopped = f.session.interrupt!(); await stoppingWritten.promise; + const stopped = f.session.interrupt!({}); await stoppingWritten.promise; expect(f.peer.createAgent).not.toHaveBeenCalled(); release.resolve(); await rejected; await stopped; expect(await savedCheckpoint(f.root, f.session)).toEqual(before); expect(f.peer.createAgent).not.toHaveBeenCalled(); expect(f.peer.getAgent).not.toHaveBeenCalled(); expect(f.peer.cancelRun).not.toHaveBeenCalled(); await f.session.startTurn({ message: { role: "user", text: "Next admitted task" } }); await f.peer.created.promise; - await f.session.interrupt!(); + await f.session.interrupt!({}); } finally { release.resolve(); write.mockRestore(); } }); @@ -571,10 +575,10 @@ describe("Cursor Cloud native driver", () => { const f = await fixture(peer); await f.session.startTurn({ message: { role: "user", text: "First task" } }); await peer.created.promise; await waitFor(() => f.events.some(event => event.eventType === "item.delta")); - await f.session.interrupt!(); await acknowledgeTerminal(f.session); + await f.session.interrupt!({}); await acknowledgeTerminal(f.session); const held = deferred(), release = deferred(), stoppingWritten = deferred(); const originalWrite = CursorCloudCheckpointStore.prototype.write; - const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) { + const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) { const persisted = originalWrite.call(this, value); if (value.phase === "creating") return persisted.then(async () => { held.resolve(); await release.promise; }); if (value.phase === "stopping") void persisted.then(() => stoppingWritten.resolve()); @@ -582,7 +586,7 @@ describe("Cursor Cloud native driver", () => { }); try { const started = f.session.startTurn({ message: { role: "user", text: "Second task" } }); await held.promise; - const stopped = f.session.interrupt!(); await stoppingWritten.promise; + const stopped = f.session.interrupt!({}); await stoppingWritten.promise; expect(peer.createRun).not.toHaveBeenCalled(); release.resolve(); const next = await started; await stopped; @@ -605,7 +609,7 @@ describe("Cursor Cloud native driver", () => { const after = await savedCheckpoint(f.root, f.session); expect(after).toEqual({ ...before, sourceSequence: before.sourceSequence + 1 }); expect(f.events.some(event => event.eventType === "turn.failed")).toBe(false); - await f.session.interrupt!(); + await f.session.interrupt!({}); expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { providerRunId: runId } }); }); @@ -617,7 +621,7 @@ describe("Cursor Cloud native driver", () => { f.peer.getRun.mockResolvedValueOnce({ ...f.peer.native!, status: "FINISHED", ...(identity === "agent" ? { agentId: "bc-different" } : { id: "run-22222222-2222-4222-8222-222222222222" }) }); await expect(f.session.reconcile!()).rejects.toThrow("confirmed Agent/Run identity"); expect(await savedCheckpoint(f.root, f.session)).toEqual(before); - await f.session.interrupt!(); + await f.session.interrupt!({}); expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { providerRunId: runId } }); }); @@ -660,7 +664,7 @@ describe("Cursor Cloud native driver", () => { const activeSnapshot = await restored.snapshot(); await restored.detachControllerForRestart!(); await expect(new CursorCloudDriver(f.options).recoverSession(activeSnapshot)).rejects.toThrow("instructions changed"); const final = (await new CursorCloudDriver(revised).recoverSession(activeSnapshot)).session!; sessions.push(final); - await final.interrupt!(); + await final.interrupt!({}); await acknowledgeTerminal(final); await final.startTurn({ message: { role: "user", text: "One more task" } }); await waitFor(() => f.peer.createRun.mock.calls.length === 2); @@ -702,7 +706,7 @@ describe("Cursor Cloud native driver", () => { expect(f.handler).toHaveBeenCalledWith(expect.objectContaining({ tool: "write_note", arguments: { note: "first" }, turnId: expect.stringMatching(/^turn-/) })); await rpc(path, { id: "missing", method: "tools/call", params: { name: "not_admitted", arguments: {} } }, relayHeaders(f.peer), initialized.session); expect(f.handler).toHaveBeenCalledTimes(1); - await f.session.interrupt!(); + await f.session.interrupt!({}); expect(f.peer.cancelRun).toHaveBeenCalledTimes(1); expect(f.peer.getRun.mock.calls.length).toBeGreaterThanOrEqual(2); const late = await rpc(path, { id: "late", method: "tools/call", params: { name: "write_note", arguments: { note: "late" } } }, relayHeaders(f.peer), initialized.session); @@ -849,7 +853,7 @@ describe("Cursor Cloud native driver", () => { await waitFor(() => peer.getAgent.mock.calls.length > 0); expect(peer.createAgent).toHaveBeenCalledTimes(1); expect((await restored.read?.())?.effectiveModelAcknowledged).toBe(false); - await restored.interrupt!(); + await restored.interrupt!({}); const restoredSnapshot = await restored.snapshot(); await restored.close({ reason: "done" }); await expect(new CursorCloudDriver({ ...f.options, model: "different-model" }).recoverSession(restoredSnapshot)).rejects.toThrow("changed"); }); @@ -873,7 +877,7 @@ describe("Cursor Cloud native driver", () => { expect(after.requestedAgentId).toMatch(/^bc-/); await expect(restored.startTurn({ message: { role: "user", text: "Must not replace uncertain work" } })).rejects.toThrow("unresolved remote work"); expect(peer.createAgent).toHaveBeenCalledTimes(1); expect(peer.createRun).not.toHaveBeenCalled(); - peer.getAgent.mockImplementation(getAgent); await restored.interrupt!(); + peer.getAgent.mockImplementation(getAgent); await restored.interrupt!({}); }); it("allows a definitive direct mutation rejection to settle without pretending work was accepted", async () => { @@ -893,7 +897,7 @@ describe("Cursor Cloud native driver", () => { if (phase === "follow-up") { await f.session.startTurn({ message: { role: "user", text: "Previous accepted task" } }); await peer.created.promise; - await f.session.interrupt!(); await acknowledgeTerminal(f.session); + await f.session.interrupt!({}); await acknowledgeTerminal(f.session); } const before = { fetch: fetch.mock.calls.length, getAgent: peer.getAgent.mock.calls.length, getRun: peer.getRun.mock.calls.length, cancel: peer.cancelRun.mock.calls.length }; const text = "\u0001".repeat(400_000); @@ -905,11 +909,11 @@ describe("Cursor Cloud native driver", () => { expect(fetch).toHaveBeenCalledTimes(before.fetch); expect(await f.session.read()).toMatchObject({ phase: "idle", providerStatus: "REJECTED" }); expect(f.events.some(event => event.turnId === turn.turnId && event.eventType === "turn.failed" && event.payload.remoteWorkAccepted === false)).toBe(true); - await f.session.interrupt!(); + await f.session.interrupt!({}); expect(peer.getAgent).toHaveBeenCalledTimes(before.getAgent); expect(peer.getRun).toHaveBeenCalledTimes(before.getRun); expect(peer.cancelRun).toHaveBeenCalledTimes(before.cancel); - const snapshot = await f.session.snapshot(); await f.session.detachControllerForRestart(); await f.pump; + const snapshot = await f.session.snapshot(); await f.session.detachControllerForRestart!(); await f.pump; const recovered = await new CursorCloudDriver(f.options).recoverSession(snapshot); expect(recovered.recovered).toBe(true); const restored = recovered.session!; sessions.push(restored); @@ -919,7 +923,7 @@ describe("Cursor Cloud native driver", () => { const next = await restored.startTurn({ message: { role: "user", text: "A smaller admitted task" } }); await vi.waitFor(async () => expect(await restored.read()).toMatchObject({ phase: "running", turnId: next.turnId })); expect(fetch).toHaveBeenCalledTimes(before.fetch + 1); - await restored.interrupt!(); + await restored.interrupt!({}); }); it.each(["fresh", "follow-up"] as const)("keeps a %s oversized response fenced after the mutation was sent", async phase => { @@ -928,7 +932,7 @@ describe("Cursor Cloud native driver", () => { const f = await fixture(peer, { client: mutations.client }); if (phase === "follow-up") { await f.session.startTurn({ message: { role: "user", text: "Previous accepted task" } }); - await peer.created.promise; await f.session.interrupt!(); await acknowledgeTerminal(f.session); + await peer.created.promise; await f.session.interrupt!({}); await acknowledgeTerminal(f.session); } const before = mutations.fetch.mock.calls.length; mutations.fetch.mockImplementationOnce(async (url, init) => { @@ -954,7 +958,7 @@ describe("Cursor Cloud native driver", () => { if (phase === "follow-up") { await f.session.startTurn({ message: { role: "user", text: "Previous accepted task" } }); await peer.created.promise; - await f.session.interrupt!(); + await f.session.interrupt!({}); await acknowledgeTerminal(f.session); } const entered = deferred(), rejectCreate = deferred(), stopHeld = deferred(), releaseStop = deferred(); @@ -975,7 +979,7 @@ describe("Cursor Cloud native driver", () => { const before = { getAgent: peer.getAgent.mock.calls.length, getRun: peer.getRun.mock.calls.length, cancel: peer.cancelRun.mock.calls.length }; const originalWrite = CursorCloudCheckpointStore.prototype.write; let pausedStop = false, pausedRejection = false; - const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) { + const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) { if (value.phase === "idle" && value.terminalStatus === "REJECTED" && !pausedRejection) { pausedRejection = true; rejectionHeld.resolve(); return releaseRejection.promise.then(() => originalWrite.call(this, value)); @@ -989,7 +993,7 @@ describe("Cursor Cloud native driver", () => { }); let stopDone = false; let stopped: Promise<{ error: unknown | null }> | undefined; - const stop = () => stopped = f.session.interrupt!().then(() => ({ error: null }), error => ({ error })) + const stop = () => stopped = f.session.interrupt!({}).then(() => ({ error: null }), error => ({ error })) .finally(() => { stopDone = true; }); try { const turn = await f.session.startTurn({ message: { role: "user", text: rejection === "unsent_payload" ? "\u0001".repeat(400_000) : "Rejected task" } }); @@ -1029,7 +1033,7 @@ describe("Cursor Cloud native driver", () => { expect(peer.cancelRun).toHaveBeenCalledTimes(before.cancel); if (mutations) expect(mutations.fetch).toHaveBeenCalledTimes(fetchesBefore!); const snapshot = await f.session.snapshot(); - await f.session.detachControllerForRestart(); await f.pump; + await f.session.detachControllerForRestart!(); await f.pump; const recovered = await new CursorCloudDriver(f.options).recoverSession(snapshot); expect(recovered.recovered).toBe(true); const restored = recovered.session!; sessions.push(restored); @@ -1037,7 +1041,7 @@ describe("Cursor Cloud native driver", () => { distinctFollowup(peer); peer.release = deferred(); const next = await restored.startTurn({ message: { role: "user", text: "Next admitted task" } }); await vi.waitFor(async () => expect(await restored.read()).toMatchObject({ phase: "running", turnId: next.turnId })); - await restored.interrupt!(); + await restored.interrupt!({}); expect(await restored.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { turnId: next.turnId } }); } finally { rejectCreate.resolve(); releaseStop.resolve(); releaseRejection.resolve(); @@ -1051,7 +1055,7 @@ describe("Cursor Cloud native driver", () => { const f = await fixture(peer), held = deferred(), release = deferred(); const originalWrite = CursorCloudCheckpointStore.prototype.write; let paused = false; - const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) { + const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) { if (value.phase === "idle" && value.terminalStatus === "REJECTED" && !paused) { paused = true; held.resolve(); return release.promise.then(() => originalWrite.call(this, value)); } @@ -1082,7 +1086,7 @@ describe("Cursor Cloud native driver", () => { await f.session.reconcile!(); await acknowledgeTerminal(f.session); await f.session.attachRun!({ runId: "current-owner" }); peer.createRun.mockRejectedValue(new CursorCloudClientError("http_error", { status: 422 })); const held = deferred(), release = deferred(), originalWrite = CursorCloudCheckpointStore.prototype.write; - const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) { + const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) { if (value.phase === "idle" && value.terminalStatus === "REJECTED") { held.resolve(); return release.promise.then(() => originalWrite.call(this, value)); } return originalWrite.call(this, value); }); @@ -1105,7 +1109,7 @@ describe("Cursor Cloud native driver", () => { const f = await fixture(peer, mutations ? { client: mutations.client } : {}); if (phase === "follow-up") { await f.session.startTurn({ message: { role: "user", text: "Previous accepted task" } }); await peer.created.promise; - await f.session.interrupt!(); await acknowledgeTerminal(f.session); + await f.session.interrupt!({}); await acknowledgeTerminal(f.session); } const entered = deferred(), rejected = deferred(); if (mutations) { @@ -1119,14 +1123,14 @@ describe("Cursor Cloud native driver", () => { }); peer.getAgent.mockRejectedValue(new CursorCloudClientError("http_error", { status: 404 })); const originalWrite = CursorCloudCheckpointStore.prototype.write; - const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) { + const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) { return value.phase === "idle" && value.terminalStatus === "REJECTED" ? Promise.reject(new Error("injected rejection checkpoint failure")) : originalWrite.call(this, value); }); try { const before = mutations?.fetch.mock.calls.length; const turn = await f.session.startTurn({ message: { role: "user", text: mutations ? "\u0001".repeat(400_000) : "Rejected task" } }); await entered.promise; - const stopped = f.session.interrupt!().then(() => null, error => error); + const stopped = f.session.interrupt!({}).then(() => null, error => error); rejected.resolve(); expect(await stopped).toMatchObject({ message: expect.stringContaining("remains fenced") }); if (mutations) expect(mutations.fetch).toHaveBeenCalledTimes(before!); @@ -1147,7 +1151,7 @@ describe("Cursor Cloud native driver", () => { peer.getAgent.mockRejectedValue(new CursorCloudClientError("http_error", { status: 404 })); const f = await fixture(peer), turn = await f.session.startTurn({ message: { role: "user", text: "Unknown create" } }); await entered.promise; - const stopped = f.session.interrupt!().then(() => null, error => error); released.resolve(); + const stopped = f.session.interrupt!({}).then(() => null, error => error); released.resolve(); expect(await stopped).toMatchObject({ message: expect.stringContaining("remains fenced") }); expect(await savedCheckpoint(f.root, f.session)).toMatchObject({ phase: "stopping", pendingOperation: "create_agent", terminalStatus: null }); expect(f.events.some(event => event.turnId === turn.turnId && event.payload.remoteWorkAccepted === false)).toBe(false); @@ -1159,7 +1163,7 @@ describe("Cursor Cloud native driver", () => { const f = await fixture(peer, { cancelTimeoutMs: 120 }); await f.session.startTurn({ message: { role: "user", text: "Keep running" } }); await peer.created.promise; await waitFor(() => f.events.some(event => event.eventType === "item.delta")); - await expect(f.session.interrupt!()).rejects.toThrow("did not confirm"); + await expect(f.session.interrupt!({})).rejects.toThrow("did not confirm"); expect(f.events.some(event => event.eventType === "turn.interrupted" || event.eventType === "turn.completed")).toBe(false); expect((await f.session.read?.())?.phase).toBe("stopping"); await expect(f.session.startTurn({ message: { role: "user", text: "New work" } })).rejects.toThrow("unresolved"); @@ -1204,7 +1208,7 @@ describe("Cursor Cloud native driver", () => { const originalWrite = CursorCloudCheckpointStore.prototype.write; const attempts: Array> = []; let injected = false; - const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(async function(value) { + const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(async function(this: CursorCloudCheckpointStore, value) { if (this.path !== checkpointPath || value.phase !== "terminal") return originalWrite.call(this, value); attempts.push(structuredClone(value.pendingTerminalDelivery)); if (injected) { await releaseRetry.promise; return originalWrite.call(this, value); } @@ -1248,7 +1252,7 @@ describe("Cursor Cloud native driver", () => { const held = deferred(), release = deferred(); const originalWrite = CursorCloudCheckpointStore.prototype.write; let paused = false; - const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) { + const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) { if (this.path === checkpointPath && value.phase === "terminal" && !paused) { paused = true; held.resolve(); return release.promise.then(() => originalWrite.call(this, value)); } @@ -1261,7 +1265,7 @@ describe("Cursor Cloud native driver", () => { const late = await rpc(f.options.relay.socketPath, { id: "late", method: "tools/call", params: { name: "write_note", arguments: { note: "No authority after provider termination" } } }, relayHeaders(f.peer), initialized.session); expect(late.status).toBe(403); expect(f.handler).not.toHaveBeenCalled(); let stopped = false, reconciled = false, snapshotted = false; - const stop = f.session.interrupt!().finally(() => { stopped = true; }); + const stop = f.session.interrupt!({}).finally(() => { stopped = true; }); const reconcile = f.session.reconcile!().finally(() => { reconciled = true; }); const snapshot = f.session.snapshot().finally(() => { snapshotted = true; }); await new Promise(accept => setImmediate(accept)); @@ -1290,7 +1294,7 @@ describe("Cursor Cloud native driver", () => { const originalWrite = CursorCloudCheckpointStore.prototype.write; const deliveries: unknown[] = []; let unavailable = true; - const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) { + const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) { if (this.path === checkpointPath && value.phase === "terminal") { deliveries.push(structuredClone(value.pendingTerminalDelivery)); if (unavailable) return Promise.reject(Object.assign(new Error("controlled unavailable checkpoint storage"), { code: "ENOSPC" })); @@ -1363,7 +1367,7 @@ describe("Cursor Cloud native driver", () => { await waitFor(() => stream.mock.calls.length > 0); expect(peer.createAgent).toHaveBeenCalledTimes(1); expect(peer.createRun).not.toHaveBeenCalled(); expect(JSON.parse(await readFile(checkpointPath, "utf8")).deadlineAt).toBe(deadlineAt); - await restored.interrupt!(); + await restored.interrupt!({}); }); it("enforces the saved deadline while the first recovery read remains pending", async () => { diff --git a/packages/paperclip-runner/src/drivers/dot/dot-harness-driver.test.ts b/packages/paperclip-runner/src/drivers/dot/dot-harness-driver.test.ts index 15fd39f32a2..05f174cc754 100644 --- a/packages/paperclip-runner/src/drivers/dot/dot-harness-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/dot/dot-harness-driver.test.ts @@ -11,7 +11,7 @@ const result = { schema: "paperclip.run_result.v1", reportedWorkDisposition: "do async function fixture(overrides: Partial = {}) { const identity = { companyId: randomUUID(), agentId: randomUUID(), issueId: randomUUID(), runId: randomUUID(), sessionId: randomUUID() }; const principal = { companyId: identity.companyId, grantId: randomUUID() }; - const publish = vi.fn(async () => {}); + const publish = vi.fn(async () => {}); const executeTool = vi.fn(async () => ({ saved: true })); const assertAuthority = vi.fn(async () => {}); const driver = new DotHarnessDriver({ identity, principal, expiresAt: Date.now() + 60_000, diff --git a/packages/paperclip-runner/src/drivers/dot/runnerd-dot-driver.test.ts b/packages/paperclip-runner/src/drivers/dot/runnerd-dot-driver.test.ts index 9e14a1bdd2b..6758f8769b6 100644 --- a/packages/paperclip-runner/src/drivers/dot/runnerd-dot-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/dot/runnerd-dot-driver.test.ts @@ -96,7 +96,7 @@ it.each([ return handle; }); const getCommand = controlPlane.DurablePrpControlPlane.prototype.getCommand; - const commandSpy = vi.spyOn(controlPlane.DurablePrpControlPlane.prototype, "getCommand").mockImplementation(function (id) { + const commandSpy = vi.spyOn(controlPlane.DurablePrpControlPlane.prototype, "getCommand").mockImplementation(function (this: controlPlane.DurablePrpControlPlane, id) { const command = getCommand.call(this, id); // Hold the SDK poll until the real process has exited. The authenticated // shutdown receipt and its ACK remain committed in the transport journal. @@ -112,7 +112,7 @@ it.each([ }); let session: Awaited> | undefined; try { - session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! }); + session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root }); if (kind === "shutdown") { await expect(session.close({ reason: "Test complete" })).resolves.toBeUndefined(); expect(exited).toBe(true); @@ -152,7 +152,7 @@ it.each(["local", "local pending finish", "custom local directory", "external la identity: { runnerInstanceId: randomUUID(), environmentLeaseId: randomUUID(), runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId }, runnerBinary: resolve("runner/target/debug/paperclip-runnerd"), - onSpawn: process => { spawned = process; }, + onSpawn: async process => { spawned = process; }, controlPlaneRegistration: async authority => { if (emptyController) { authority.setCheckpointPersistence(async saved => { checkpoint = controlPlane.decodeDurablePrpCheckpoint(new Map(saved.records)); }); @@ -190,7 +190,7 @@ it.each(["local", "local pending finish", "custom local directory", "external la }; } const driver = new RunnerdDotDriver(options); - let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! }); + let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root }); const operation = (action: ExternalProviderOperation["action"], args: Record): ExternalProviderOperation => ({ requestId: randomUUID(), bindingId: input.provider.binding.bindingId, bindingGeneration: 1, runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, turnId: options.identity.turnId, assignmentRevision: 1, diff --git a/packages/paperclip-runner/src/drivers/muse/runnerd-muse-driver.test.ts b/packages/paperclip-runner/src/drivers/muse/runnerd-muse-driver.test.ts index d0b4e6f6527..9e8da75fd51 100644 --- a/packages/paperclip-runner/src/drivers/muse/runnerd-muse-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/muse/runnerd-muse-driver.test.ts @@ -82,7 +82,7 @@ it("uses the native request, exact question command ID and durable answer receip execution: input, stateDirectory: join(root, "state"), runnerBinary: resolve("runner/target/debug/paperclip-runnerd"), identity: { runnerInstanceId: randomUUID(), environmentLeaseId: randomUUID(), runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId }, - onSpawn: process => { spawned = process; }, + onSpawn: async process => { spawned = process; }, controlPlaneRegistration: async authority => { await authority.start(controllerPort); controllerPort = Number(new URL(authority.connectUrl).port); return { connectUrl: authority.connectUrl, release: () => authority.stop() }; }, port: { dispatch: async () => {}, settle: async event => { settlements.set(String(event.payload.requestId), event.payload.outcome); }, @@ -95,7 +95,7 @@ it("uses the native request, exact question command ID and durable answer receip }); const commandSpy = vi.spyOn(controlPlane.DurablePrpControlPlane.prototype, "queueCommand"); let driver = new RunnerdMuseDriver(options); - let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! }); + let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root }); try { const prepared = commandSpy.mock.calls.find(call => call[0] === "run.prepare"); expect(JSON.stringify(prepared?.[1])).not.toContain("connections_search"); @@ -166,7 +166,7 @@ it("bounds default Muse question answer command IDs at the native queue boundary execution: input, stateDirectory: join(root, "state"), runnerBinary: resolve("runner/target/debug/paperclip-runnerd"), identity: { runnerInstanceId: randomUUID(), environmentLeaseId: randomUUID(), runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId }, - onSpawn: process => { spawned = process; }, + onSpawn: async process => { spawned = process; }, controlPlaneRegistration: async authority => { await authority.start(controllerPort); controllerPort = Number(new URL(authority.connectUrl).port); return { connectUrl: authority.connectUrl, release: () => authority.stop() }; }, port: { dispatch: async () => {}, settle: async event => { settlements.set(String(event.payload.requestId), event.payload.outcome); }, @@ -182,7 +182,7 @@ it("bounds default Muse question answer command IDs at the native queue boundary const response = { schema: "paperclip.question_response.v1" as const, answers: { environment: { selectedOptionIds: ["test"] } } }; const commandSpy = vi.spyOn(controlPlane.DurablePrpControlPlane.prototype, "queueCommand"); let driver = new RunnerdMuseDriver(options); - let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! }); + let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root }); try { await session.startTurn({ message: { role: "user", text: "Answer boundary questions." } }); await send!(operation("accept", {})); @@ -253,7 +253,7 @@ it("rejects bridge-unsupported questions at native admission without a stored re execution: input, stateDirectory: join(root, "state"), runnerBinary: resolve("runner/target/debug/paperclip-runnerd"), identity: { runnerInstanceId: randomUUID(), environmentLeaseId: randomUUID(), runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId }, - onSpawn: process => { spawned = process; }, + onSpawn: async process => { spawned = process; }, port: { dispatch: async () => {}, settle: async () => {}, inputAvailable: async () => {}, attach: async callback => { send = callback; return async () => { send = undefined; }; } }, }; @@ -263,7 +263,7 @@ it("rejects bridge-unsupported questions at native admission without a stored re action: "request_user_input", input: args, digest: externalOperationDigest("request_user_input", args), }); const driver = new RunnerdMuseDriver(options); - const session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! }); + const session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root }); const events: PrpEvent[] = []; let eventReadFailure: unknown; const reading = (async () => { for await (const event of session.events()) events.push(event); })() @@ -315,8 +315,8 @@ it("keeps the exact Muse question and assignment alive past the native live wind await writeFile(join(root, "AGENTS.md"), "Use only first-party tools."); const input = execution(root); const events: PrpEvent[] = []; - const dispatches: PrpEvent[] = []; - const deliveries: PrpEvent[] = []; + const dispatches: Array<{ sourceEventId: string; payload: Record }> = []; + const deliveries: Array<{ sourceEventId: string; payload: Record }> = []; const settlements = new Map>(); let send: ((operation: ExternalProviderOperation) => Promise) | undefined; let spawned: { pid: number; processGroupId: number | null; startedAt: string } | undefined; @@ -327,7 +327,7 @@ it("keeps the exact Muse question and assignment alive past the native live wind normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId }; const driver = new RunnerdMuseDriver({ execution: input, stateDirectory: join(root, "state"), identity, - runnerBinary: resolve("runner/target/debug/paperclip-runnerd"), onSpawn: process => { spawned = process; }, + runnerBinary: resolve("runner/target/debug/paperclip-runnerd"), onSpawn: async process => { spawned = process; }, port: { dispatch: async event => { dispatches.push(event); }, inputAvailable: async event => { deliveries.push(event); }, settle: async event => { settlements.set(String(event.payload.requestId), event.payload.outcome as Record); }, attach: async callback => { send = callback; return async () => { send = undefined; }; } }, diff --git a/packages/paperclip-runner/src/drivers/opencode/mcp-bridge-diagnostics.test.ts b/packages/paperclip-runner/src/drivers/opencode/mcp-bridge-diagnostics.test.ts index 4c3b253a83c..59844a49dc7 100644 --- a/packages/paperclip-runner/src/drivers/opencode/mcp-bridge-diagnostics.test.ts +++ b/packages/paperclip-runner/src/drivers/opencode/mcp-bridge-diagnostics.test.ts @@ -12,10 +12,14 @@ const report = { evidence: [], verification: [], }; +type RpcResponse = { + result: { isError?: boolean; tools: Array<{ name: string; inputSchema: unknown }> }; +}; + async function rpc(bridge: OpenCodeMcpBridge, id: number, method: string, params?: unknown) { return (await fetch(bridge.url, { method: "POST", headers: { Authorization: `Bearer ${bridge.secret}`, "Content-Type": "application/json", - }, body: JSON.stringify({ jsonrpc: "2.0", id, method, params }) })).json(); + }, body: JSON.stringify({ jsonrpc: "2.0", id, method, params }) })).json() as Promise; } it("binds closed rejection shapes to the actual offered finish schema without recording values", async () => { @@ -23,7 +27,7 @@ it("binds closed rejection shapes to the actual offered finish schema without re const bridge = await startOpenCodeMcpBridge({ handler, onDiagnostic: message => messages.push(message) }); bridges.push(bridge); const listed = await rpc(bridge, 1, "tools/list"); - const schema = listed.result.tools.find((tool: { name: string }) => tool.name === "paperclip_finish").inputSchema; + const schema = listed.result.tools.find((tool: { name: string }) => tool.name === "paperclip_finish")!.inputSchema; const digest = createHash("sha256").update(JSON.stringify(schema)).digest("hex"); for (const [index, input] of [report, { ...report, continuation: null }].entries()) { const result = await rpc(bridge, index + 2, "tools/call", { name: "paperclip_finish", arguments: input }); diff --git a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts index 5a2c95e2ad0..0613569216a 100644 --- a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts @@ -241,7 +241,7 @@ describe("OpenCodeServerDriver", () => { }); const session = await driver.openSession({ runId: "empty", normalizedSessionId: "empty", workingDirectory: root }); try { - expect(await session.read()).toMatchObject({ messages: [] }); + expect(await session.read!()).toMatchObject({ messages: [] }); } finally { await session.close({ reason: "test" }); } @@ -338,7 +338,7 @@ describe("OpenCodeServerDriver", () => { }); const descriptor = await driver.descriptor(); expect( - descriptor.capabilities.typedEventFamilies.find( + descriptor.capabilities.typedEventFamilies?.find( (family) => family.family === "plan", ), ).toMatchObject({ availability: "unsupported" }); @@ -751,7 +751,7 @@ describe("OpenCodeServerDriver", () => { // run.terminal is owned by the native-session layer above this driver. // Keep both absences explicit instead of synthesizing Codex-shaped input. expect( - (await driver.descriptor()).capabilities.typedEventFamilies.find( + (await driver.descriptor()).capabilities.typedEventFamilies?.find( (family) => family.family === "plan", ), ).toMatchObject({ availability: "unsupported" }); @@ -1105,7 +1105,7 @@ describe("OpenCodeServerDriver", () => { expect(JSON.stringify(events)).not.toContain( "submitted prompt must not become assistant text", ); - expect(await session.usage()).toMatchObject({ + expect(await session.usage!()).toMatchObject({ input: 3, output: 2, costUsd: 0.001, @@ -1772,7 +1772,7 @@ describe("OpenCodeServerDriver", () => { // A prompt tools map replaces OpenCode's session permissions. Question is // enabled in config without overwriting the allow/ask/deny or path policy. expect(submittedPrompt).not.toHaveProperty("tools"); - expect(JSON.stringify(submittedPrompt?.parts ?? null)).not.toContain( + expect(JSON.stringify((submittedPrompt as Record | null)?.parts ?? null)).not.toContain( PAPERCLIP_EXECUTION_PROMPT, ); const sessionRoot = join(root, "context"); @@ -2871,13 +2871,13 @@ describe("OpenCodeServerDriver", () => { await session.startTurn({ message: { role: "user", text: "finish" } }); await started; let stopped = false; - const stop = session[operation]({ reason: "fixture stop" }).then(() => { stopped = true; }); + const stop = session[operation]!({ reason: "fixture stop" }).then(() => { stopped = true; }); await new Promise(resolve => setImmediate(resolve)); expect(stopped).toBe(false); releaseFeedback(); await stop; expect((await session.snapshot()).semanticResult).not.toBeNull(); - const transcript = await session.transcript(); + const transcript = await session.transcript!(); expect(transcript.events.filter(event => event.eventType === "run.result.proposed")).toHaveLength(1); expect(transcript.events.some(event => event.eventType === "item.completed" && (event.payload.item as { is_error?: boolean })?.is_error)).toBe(false); } finally { diff --git a/packages/paperclip-runner/src/drivers/opencode/reasoning-mode.test.ts b/packages/paperclip-runner/src/drivers/opencode/reasoning-mode.test.ts index d6d9263046f..db3a875e4d2 100644 --- a/packages/paperclip-runner/src/drivers/opencode/reasoning-mode.test.ts +++ b/packages/paperclip-runner/src/drivers/opencode/reasoning-mode.test.ts @@ -57,7 +57,7 @@ describe("per-turn OpenCode reasoning", () => { }); expect(config.provider.openrouter.models["deepseek/deepseek-v4-flash-0731"].options).toBeUndefined(); } finally { - try { await session?.close(); } + try { await session?.close({ reason: "fixture complete" }); } finally { await rm(root, { recursive: true, force: true }); } } }); diff --git a/packages/paperclip-runner/src/drivers/runner-tool-bridge.test.ts b/packages/paperclip-runner/src/drivers/runner-tool-bridge.test.ts index c9818c65076..208a738d697 100644 --- a/packages/paperclip-runner/src/drivers/runner-tool-bridge.test.ts +++ b/packages/paperclip-runner/src/drivers/runner-tool-bridge.test.ts @@ -23,6 +23,8 @@ afterEach(async () => { await Promise.all(bridges.splice(0).map((bridge) => bridge.close())); }); +type ToolCallBody = { result: { content: unknown } }; + async function rpc( bridge: RunnerToolBridge, body: Record, @@ -563,7 +565,7 @@ describe("Copilot semantic receipt opt-in", () => { } }); bridges.push(bridge); const request = { id: 3, method: "tools/call", params: { name: "paperclip_finish", arguments: rawFinish } }; - const body = await (await rpc(bridge, request)).json(); + const body = await (await rpc(bridge, request)).json() as ToolCallBody; expect(receipts).toHaveLength(1); expect(readNativeSemanticReceipt({ contents: body.result.content })).toEqual(receipts[0]); expect(receipts[0]).toMatchObject({ schema: "paperclip.semantic_tool_receipt.v2", inputSha256: semanticInputSha256(rawFinish), @@ -587,11 +589,11 @@ describe("Copilot semantic receipt opt-in", () => { const normalizedFirst = validatePrpStructuredRunResult(rawFinish); const normalizedSecond = validatePrpStructuredRunResult(secondInput); if (!normalizedFirst.ok || !normalizedSecond.ok) throw new Error("Invalid fixture"); pending.get("second")!.capture(normalizedSecond.result)(); pending.get("second")!.settle(); - const secondReceipt = readNativeSemanticReceipt({ contents: (await (await second).json()).result.content }); + const secondReceipt = readNativeSemanticReceipt({ contents: (await (await second).json() as ToolCallBody).result.content }); expect(secondReceipt).toMatchObject({ inputSha256: semanticInputSha256(secondInput), normalizedInputSha256: semanticInputSha256(normalizedSecond.result) }); pending.get("second")!.capture(normalizedFirst.result)(); pending.get("first")!.capture(normalizedFirst.result)(); pending.get("first")!.settle(); - const firstReceipt = readNativeSemanticReceipt({ contents: (await (await first).json()).result.content }); + const firstReceipt = readNativeSemanticReceipt({ contents: (await (await first).json() as ToolCallBody).result.content }); expect(firstReceipt).toMatchObject({ inputSha256: semanticInputSha256(rawFinish), normalizedInputSha256: semanticInputSha256(normalizedFirst.result) }); expect(receipts).toEqual([secondReceipt, firstReceipt]); }); @@ -600,10 +602,10 @@ describe("Copilot semantic receipt opt-in", () => { const withEvidence = await startRunnerToolBridge({ tools: [tool("documents.read")], captureSemanticReceipt: () => () => {}, handler: async call => { expect(call.captureNormalizedInput).toBeUndefined(); return modelMetadata; } }); bridges.push(withEvidence); - const body = await (await rpc(withEvidence, { id: 1, method: "tools/call", params: { name: "documents.read", arguments: modelMetadata } })).json(); + const body = await (await rpc(withEvidence, { id: 1, method: "tools/call", params: { name: "documents.read", arguments: modelMetadata } })).json() as ToolCallBody; expect(readNativeSemanticReceipt({ contents: body.result.content })).toHaveProperty("normalizedInputSha256", null); const plain = await startRunnerToolBridge({ handler: async call => { expect(call.captureNormalizedInput).toBeUndefined(); return { accepted: true }; } }); bridges.push(plain); - const plainBody = await (await rpc(plain, { id: 2, method: "tools/call", params: { name: "paperclip_finish", arguments: rawFinish } })).json(); + const plainBody = await (await rpc(plain, { id: 2, method: "tools/call", params: { name: "paperclip_finish", arguments: rawFinish } })).json() as ToolCallBody; expect(readNativeSemanticReceipt({ contents: plainBody.result.content })).toBeNull(); }); it.each(["small", "chunked", "tagged", "error"])("preserves %s result encoding and captures scope before dispatch", async encoding => { diff --git a/packages/paperclip-runner/src/eval/workflow-evals.test.ts b/packages/paperclip-runner/src/eval/workflow-evals.test.ts index b4df89c143a..5c7a399fe1f 100644 --- a/packages/paperclip-runner/src/eval/workflow-evals.test.ts +++ b/packages/paperclip-runner/src/eval/workflow-evals.test.ts @@ -1,7 +1,7 @@ import { readFile, stat } from "node:fs/promises"; import { resolve } from "node:path"; -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { diff --git a/packages/paperclip-runner/src/evals/native-execution.test.ts b/packages/paperclip-runner/src/evals/native-execution.test.ts index 9700ae81b00..f241b35883b 100644 --- a/packages/paperclip-runner/src/evals/native-execution.test.ts +++ b/packages/paperclip-runner/src/evals/native-execution.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { prpSchemaBundle } from "../protocol/generated/schema-bundle.js"; diff --git a/packages/paperclip-runner/src/live/acpx-request-recovery.test.ts b/packages/paperclip-runner/src/live/acpx-request-recovery.test.ts index 4eb952ed63d..c7ed7f36d3b 100644 --- a/packages/paperclip-runner/src/live/acpx-request-recovery.test.ts +++ b/packages/paperclip-runner/src/live/acpx-request-recovery.test.ts @@ -76,7 +76,7 @@ describe("live ACP request recovery", () => { recovered = await original.snapshot(); expect(recovered.pendingRuntimeRequests).toHaveLength(2); } finally { - await original.close(); + await original.close({ reason: "test complete" }); } class AdoptedTransport extends FakeCodexTransport { takeRestoredRuntimeRequests(): CodexRpcServerRequest[] { return requests; } @@ -99,7 +99,7 @@ describe("live ACP request recovery", () => { await expect(session.resolveRuntimeRequest?.({ requestId, turnId: "turn-1", resolution })).rejects.toThrow("no longer pending"); expect(deliver).toHaveBeenCalledTimes(1); } finally { - await session.close(); await drained; + await session.close({ reason: "test complete" }); await drained; expect(recoveredEvents.map(event => event.eventType)).not.toContain("runtime_request.created"); expect(recoveredEvents.filter(event => event.eventType === "runtime_request.expired").map(event => event.payload.requestId)).not.toContain(requestId); } diff --git a/packages/paperclip-runner/src/live/antigravity-candidate-startup.test.ts b/packages/paperclip-runner/src/live/antigravity-candidate-startup.test.ts index 80389f03b96..3776ad229ef 100644 --- a/packages/paperclip-runner/src/live/antigravity-candidate-startup.test.ts +++ b/packages/paperclip-runner/src/live/antigravity-candidate-startup.test.ts @@ -63,7 +63,9 @@ describe("Antigravity candidate trusted startup", () => { const root = await mkdtemp(join(tmpdir(), "antigravity-candidate-continuation-")); const admission = candidate(); const retained = { driverSessionId: "driver-retained", providerSessionId: "native-retained", providerIdentity: { - kind: "acpx", normalizedSessionId: identity.normalizedSessionId, + kind: "acpx" as const, normalizedSessionId: identity.normalizedSessionId, + acpxRecordId: "acpx-retained", backendSessionId: "backend-retained", agentSessionId: "native-retained", + workspaceDigest: `sha256:${"e".repeat(64)}`, providerLifetimeFenceCandidates: [1, 2, 3] as const, // The recovery binding digest is distinct from the release command digest. profileDigest: `sha256:${"a".repeat(64)}`, requestedModel: admission.model, effectiveModel: admission.model, } }; diff --git a/packages/paperclip-runner/src/live/clean-room-server.test.ts b/packages/paperclip-runner/src/live/clean-room-server.test.ts index 65b0872adab..f9a5df8b05c 100644 --- a/packages/paperclip-runner/src/live/clean-room-server.test.ts +++ b/packages/paperclip-runner/src/live/clean-room-server.test.ts @@ -15,6 +15,7 @@ import type { } from "../drivers/codex/app-server-transport.js"; import type { CapabilityIssueThreadSnapshot, CapabilityThreadItem } from "../issue-thread/types.js"; import type { CapabilityLiveTransportFactory } from "./live-session.js"; +import type { CapabilityRunnerdProcessEvidence } from "./runnerd-codex-transport.js"; import { createCapabilityTurnStreamDecoder, readCapabilityTurnStream, @@ -343,10 +344,25 @@ class FakeCleanRoomTransport implements CodexAppServerTransport { function fakeTransportFactory(state: FakeProviderState): CapabilityLiveTransportFactory { return (options) => { - const evidence = { + const evidence: CapabilityRunnerdProcessEvidence = { runnerPid: 7100, runnerProcessGroupId: 7100, + providerPid: null, + providerProcessStartedAt: null, codexPid: 7200, + codexProcessStartedAt: null, + sidecarPid: null, + sidecarProcessStartedAt: null, + agentPid: null, + agentProcessStartedAt: null, + providerDriver: null, + providerVersion: null, + acpxAgent: null, + agentServerVersion: null, + agentRuntimeVersion: null, + acpProtocolVersion: null, + providerExecutionKind: null, + providerService: null, runnerExited: false, runnerExitCode: null, runnerSignal: null, @@ -359,7 +375,7 @@ function fakeTransportFactory(state: FakeProviderState): CapabilityLiveTransport options.onEvidence?.(evidence); }); options.onEvidence?.(evidence); - return { transport, evidence: () => ({ ...evidence }) }; + return { transport, evidence: () => ({ ...evidence }), detachControllerForRestart: async () => {} }; }; } @@ -369,6 +385,8 @@ interface CleanRoomPayload { identity: { token: string; companyId: string; actorId: string; taskId: string; identifier: string }; limits: { maxTurns: number; maxMessageBytes: number }; view: CapabilityIssueThreadSnapshot; + toolResult?: unknown; + toolTurnId?: string | null; configuration: { provider: "codex" | "opencode"; model: string | null; @@ -801,9 +819,9 @@ describe("Capability clean-room chat server", () => { expect(after.toolTurnId).toMatch(/^turn-/); const turn = after.view.turns.find((candidate: { id: string }) => candidate.id === after.toolTurnId); expect(turn).toBeDefined(); - expect(turn.items.some((item: CapabilityThreadItem) => item.kind === "user_message")).toBe(true); - expect(turn.items.some((item: CapabilityThreadItem) => item.kind === "tool_activity" && item.operationId === "report_progress")).toBe(true); - expect(turn.items.some((item: CapabilityThreadItem) => item.kind === "durable_comment" && item.body === "Progress entered manually from DevTools.")).toBe(true); + expect(turn!.items.some((item: CapabilityThreadItem) => item.kind === "user_message")).toBe(true); + expect(turn!.items.some((item: CapabilityThreadItem) => item.kind === "tool_activity" && item.operationId === "report_progress")).toBe(true); + expect(turn!.items.some((item: CapabilityThreadItem) => item.kind === "durable_comment" && item.body === "Progress entered manually from DevTools.")).toBe(true); expect(after.view.evidence.state.at(-1)?.toRevision).toBeGreaterThan(0); }); diff --git a/packages/paperclip-runner/src/live/live-session.test.ts b/packages/paperclip-runner/src/live/live-session.test.ts index c3d4eb9abac..ec35ad7c5e8 100644 --- a/packages/paperclip-runner/src/live/live-session.test.ts +++ b/packages/paperclip-runner/src/live/live-session.test.ts @@ -24,7 +24,7 @@ import { type CapabilityLiveTurnEvent, } from "./live-session.js"; import { DurableCapabilityLiveSessionStore } from "./durable-live-session-store.js"; -import { defaultCapabilityRunnerdBinary } from "./runnerd-codex-transport.js"; +import { defaultCapabilityRunnerdBinary, type CapabilityRunnerdProcessEvidence } from "./runnerd-codex-transport.js"; import { persistedCursorUsageNotice } from "../drivers/acpx/usage-accounting.js"; import { captureTurnRejection } from "../../test/capture-turn-rejection.js"; import * as workspaceDiff from "./workspace-diff.js"; @@ -45,7 +45,7 @@ it("admits Pi live sessions without candidate opt-in while preserving the exact const session = await service.create({ provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low", requestedModel: "openrouter/deepseek/deepseek-v4-flash-0731" }); try { expect(session.snapshot().config.acpxProfile).toMatchObject({ - agent: "pi", ...resolveQualifiedAcpxProfile("pi", "openrouter/deepseek/deepseek-v4-flash-0731"), + ...resolveQualifiedAcpxProfile("pi", "openrouter/deepseek/deepseek-v4-flash-0731"), agent: "pi", }); const custom = await service.create({ provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low", requestedModel: "another-model" }); expect(custom.snapshot().config.acpxProfile).toMatchObject({ qualificationModel: "another-model", reportedModelId: "another-model" }); @@ -322,10 +322,25 @@ class FakeCapabilityCodexTransport implements CodexAppServerTransport { function fakeTransportFactory(state: FakeProviderState): CapabilityLiveTransportFactory { return (options) => { - const evidence = { + const evidence: CapabilityRunnerdProcessEvidence = { runnerPid: 4100 + state.transports.length, runnerProcessGroupId: 4100 + state.transports.length, + providerPid: null, + providerProcessStartedAt: null, codexPid: 5100 + state.transports.length, + codexProcessStartedAt: null, + sidecarPid: null, + sidecarProcessStartedAt: null, + agentPid: null, + agentProcessStartedAt: null, + providerDriver: null, + providerVersion: null, + acpxAgent: null, + agentServerVersion: null, + agentRuntimeVersion: null, + acpProtocolVersion: null, + providerExecutionKind: null, + providerService: null, runnerExited: false, runnerExitCode: null, runnerSignal: null, @@ -337,7 +352,7 @@ function fakeTransportFactory(state: FakeProviderState): CapabilityLiveTransport options.onEvidence?.(evidence); }); options.onEvidence?.(evidence); - return { transport, evidence: () => ({ ...evidence, runnerExited: transport.processInfo().exited }) }; + return { transport, evidence: () => ({ ...evidence, runnerExited: transport.processInfo().exited }), detachControllerForRestart: async () => {} }; }; } @@ -394,7 +409,7 @@ class TransientFailureLiveSessionStore implements CapabilityLiveSessionStore { throw new Error("transient live-session store failure"); } if ( - snapshot.terminalTurns.length > 0 && + snapshot.terminalTurns!.length > 0 && (this.#rejectTerminalSaves || this.#terminalSaveFailuresRemaining > 0) ) { if (this.#terminalSaveFailuresRemaining > 0) { @@ -647,7 +662,7 @@ describe("Capability live runnerd and Codex session", () => { outputTokens: 4, })).resolves.toBe("committed"); - expect((await store.load(session.id))?.usageLedger.map((receipt) => receipt.receiptId)).toEqual([ + expect((await store.load(session.id))?.usageLedger!.map((receipt) => receipt.receiptId)).toEqual([ "usage-before-recovery", "usage-after-recovery", ]); @@ -1159,7 +1174,7 @@ describe("Capability live runnerd and Codex session", () => { expect(first.snapshot.providerThreadId).toBe(second.snapshot.providerThreadId); expect(state.attachments).toHaveLength(1); expect(second.snapshot.providerRunBinding).toEqual(state.attachments[0]); - expect(second.snapshot.usageLedger.slice(-2)).toMatchObject([ + expect(second.snapshot.usageLedger!.slice(-2)).toMatchObject([ { providerRequests: 1, inputTokens: 100, outputTokens: 10 }, { providerRequests: 1, inputTokens: 100, outputTokens: 10 }, ]); @@ -1497,7 +1512,7 @@ describe("Capability live runnerd and Codex session", () => { const result = await session.sendMessage("report overlapping usage"); expect(result.snapshot.usageLedger).toHaveLength(1); - expect(result.snapshot.usageLedger[0]).toMatchObject({ + expect(result.snapshot.usageLedger![0]).toMatchObject({ providerRequests: 1, inputTokens: 100, outputTokens: 10, @@ -1596,7 +1611,7 @@ describe("Capability live runnerd and Codex session", () => { const result = await session.sendMessage("report late usage"); expect(result.snapshot.usageLedger).toHaveLength(1); - expect(result.snapshot.usageLedger[0]).toMatchObject({ + expect(result.snapshot.usageLedger![0]).toMatchObject({ providerRequests: 2, inputTokens: 100, outputTokens: 10, diff --git a/packages/paperclip-runner/src/live/runner-prepared-launch-workspace.test.ts b/packages/paperclip-runner/src/live/runner-prepared-launch-workspace.test.ts index 943871742f0..6112d7f87bb 100644 --- a/packages/paperclip-runner/src/live/runner-prepared-launch-workspace.test.ts +++ b/packages/paperclip-runner/src/live/runner-prepared-launch-workspace.test.ts @@ -106,10 +106,10 @@ it.each([ const recovered = await restarted.backend.recoverSession!(snapshot, { signal: new AbortController().signal }); expect(recovered).toMatchObject({ recovered: true }); if (!recovered.recovered) throw new Error(recovered.reason); - expect((await recovered.session.snapshot()).workingDirectory).toBe(cwd); + expect((await recovered.session!.snapshot()).workingDirectory).toBe(cwd); // Recovery reuses the saved preparation instead of resolving a new directory. expect(restarted.prepareLaunch.mock.calls[0]![1]).toMatchObject({ configurationId: "plugin-run" }); - await recovered.session.close({ reason: "test complete" }); + await recovered.session!.close({ reason: "test complete" }); const moved = await pluginBackend(root, join(root, "unused"), "runner_prepared_launch"); await expect(moved.backend.recoverSession!({ ...snapshot, workingDirectory: join(root, "elsewhere") }, diff --git a/packages/paperclip-runner/src/live/runnerd-acpx-questions.test.ts b/packages/paperclip-runner/src/live/runnerd-acpx-questions.test.ts index 4d208fc1652..e5fe9de7647 100644 --- a/packages/paperclip-runner/src/live/runnerd-acpx-questions.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-acpx-questions.test.ts @@ -24,7 +24,7 @@ describe("ACPX questions through the shared runner transport", () => { [timing!.id]: { selectedOptionIds: [timing!.options![1]!.id] }, } }; expect(runtimeRequestResponse({ requestId: "question-1", requestKind: "elicitation", method: origin.method, - turnId: "turn", itemId: "item", status: "pending", prompt: "Interview", input: shown }, { action: "submit", response }, context)).toEqual({ action: "submit", response }); + turnId: "turn", itemId: "item", status: "pending", prompt: "Interview", details: {}, input: shown }, { action: "submit", response }, context)).toEqual({ action: "submit", response }); expect(form.accept(response)).toEqual({ action: "accept", content: { organization: "Garden club", goal: "Welcome note", timing: "later" } }); }); it("does not admit an invalid canonical question set", () => { diff --git a/packages/paperclip-runner/src/live/runnerd-acpx-source-reconciliation.test.ts b/packages/paperclip-runner/src/live/runnerd-acpx-source-reconciliation.test.ts index bb26e9428b5..34263fd42a7 100644 --- a/packages/paperclip-runner/src/live/runnerd-acpx-source-reconciliation.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-acpx-source-reconciliation.test.ts @@ -47,7 +47,7 @@ async function fixture(agent: "codex" | "claude" = "codex", nativeSource = false binding: { companyId: nativeIdentity.companyId, agentId: nativeIdentity.agentId, issueId: nativeIdentity.issueId, runId: identity.runId, executionWorkspaceId: "workspace" }, task: { identifier: "TASK-1", title: "Complete fake work", description: null, prompt: "Complete fake work", workMode: "standard" }, workspace: { cwd: root, repoUrl: null, repoRef: null, branchName: null }, - session: { normalizedSessionId: identity.normalizedSessionId, driverKind: "acpx_runtime", protocolVersion: 1 }, + session: { normalizedSessionId: identity.normalizedSessionId, driverKind: "acpx_runtime", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } }, provider: { kind: "acpx", agent, model: "explicit-source-model", permissionPolicy: "interactive", profile: resolveQualifiedAcpxProfile(agent, "explicit-source-model") }, completionContract: { id: "contract", sha256: sha("contract"), schemaVersion: "paperclip.completion-contract.v1", contract: { revision: "1", objective: "Complete fake work", criteria: [{ id: "objective", requirement: "Complete fake work" }] } }, interactionResponses: [], credentialBindings: [] }; @@ -147,7 +147,7 @@ it.skipIf(process.platform === "win32")("recovers and adopts ACPX through the re environment: { PAPERCLIP_WORKSPACE_CWD: f.root }, dynamicTools: [] }); let recovered = await backend().recoverSession!(f.nativeSnapshot!, { signal: new AbortController().signal }); expect(recovered.recovered).toBe(true); - const pid = source.evidence().runnerPid, startedAt = source.transport.processInfo().startedAt; + const pid = source.evidence().runnerPid, startedAt = source.transport.processInfo!().startedAt; expect(await f.count("turn.start")).toBe(1); expect(await f.count("processStart")).toBe(2); await recovered.session!.detachControllerForRestart!(); const launch = vi.fn(() => { throw new Error("duplicate source launch"); }); @@ -197,7 +197,7 @@ it.skipIf(process.platform === "win32").each(["codex", "claude", "adopt", "pause await expect(source.transport.request("turn/start", {})).rejects.toThrow("source_attachment_pending"); const pid = source.evidence().runnerPid; if (mode === "adopt") { - const startedAt = source.transport.processInfo().startedAt; + const startedAt = source.transport.processInfo!().startedAt; await source.detachControllerForRestart(); const launch = vi.fn(() => { throw new Error("duplicate source launch"); }); source = f.recover({ runnerProcessLauncher: launch, authorizeRunnerAdoption: async () => { throw new Error("old source configuration"); }, diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport-adoption.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport-adoption.test.ts index 424db66444b..664f7db4a44 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport-adoption.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport-adoption.test.ts @@ -38,7 +38,7 @@ async function verifyLiveRunnerAdoption( evidence: [], verification: [], attentionRequests: [], artifacts: [], })); const server = createServer(); - let authority: DurablePrpControlPlane | null = null; + let authority = null as DurablePrpControlPlane | null; const checkpoint = vi.fn(async () => undefined); server.on("upgrade", (request, socket, head) => { if (!authority) { @@ -136,7 +136,7 @@ async function verifyLiveRunnerAdoption( ); } runnerPid = first.evidence().runnerPid; - runnerStartedAt = first.transport.processInfo().startedAt; + runnerStartedAt = first.transport.processInfo!().startedAt; expect(runnerPid).toEqual(expect.any(Number)); if (goalMidTurn) { @@ -404,7 +404,7 @@ async function verifyLiveRunnerAdoption( } else expect(redispatchedTool).not.toHaveBeenCalled(); } expect(adopted.evidence().runnerPid).toBe(runnerPid); - expect(adopted.transport.processInfo().startedAt).toBe(runnerStartedAt); + expect(adopted.transport.processInfo!().startedAt).toBe(runnerStartedAt); if (startWithoutCheckpoint) { const retained = JSON.parse(await readFile(controlPlaneStatePath, "utf8")); for (const type of ["run.prepare", "session.open"]) { @@ -448,7 +448,7 @@ async function verifyLiveRunnerAdoption( expect(await Promise.race([notificationsClosed, new Promise(resolve => { queueDeadline = setTimeout(() => resolve(false), 250); })])).toBe(true); } finally { clearTimeout(queueDeadline); } await expect(adopted.transport.close()).rejects.toBe(probeError); - expect(adopted.transport.processInfo()).toMatchObject({ exited: false }); + expect(adopted.transport.processInfo!()).toMatchObject({ exited: false }); expect(() => process.kill(runnerPid!, 0)).not.toThrow(); expect(signal).not.toHaveBeenCalled(); expect(checkpoint).not.toHaveBeenCalled(); diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport-attachment-recovery.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport-attachment-recovery.test.ts index d672d9bb2e3..e81f64320c7 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport-attachment-recovery.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport-attachment-recovery.test.ts @@ -14,9 +14,18 @@ import { expect, it, vi } from "vitest"; import { DurablePrpControlPlane } from "../control-plane/durable-prp-control-plane.js"; import * as durableControlPlane from "../control-plane/durable-prp-control-plane.js"; import { createCapabilityRunnerdCodexTransport, readRunnerdArtifactBinding } from "./runnerd-codex-transport.js"; -import { defaultCapabilityRunnerdBinary, fakeCodex, fakeCodexArgs } from "../../test/support/runnerd-transport.js"; +import { committableStore, defaultCapabilityRunnerdBinary, fakeCodex, fakeCodexArgs } from "../../test/support/runnerd-transport.js"; -it.each([ +type WarmAttachRecoveryCase = { + lossPoint: string; + routed: boolean; + recoveryFault: string; + executionPrepared?: boolean; + externalArtifact?: boolean; + ordinaryFollowup?: boolean | string; +}; + +it.each([ ...[ "before-result", "after-result", @@ -478,14 +487,15 @@ it.each([ ).toBe(false); return; } - const commit = core.store.commit.bind(core.store); + const store = committableStore(core); + const commit = store.commit.bind(store); let lossObserved = false; let signalLoss!: () => void; const loss = new Promise((resolveLoss) => { signalLoss = resolveLoss; }); commitSpy = vi - .spyOn(core.store, "commit") + .spyOn(store, "commit") .mockImplementation((candidate) => { // The selected process-crash boundary stays unavailable until the // exact owned runner is joined. A later replay must not silently diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport-deadlines.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport-deadlines.test.ts index 1f0fbaef19f..06cd0ae3735 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport-deadlines.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport-deadlines.test.ts @@ -67,7 +67,7 @@ it("keeps a quiet active Codex turn in the same process across connection lease expect(calls).not.toContain("turn/interrupt"); expect(calls).not.toContain("thread/resume"); const state = JSON.parse(await readFile(join(stateDirectory, "fake-codex-state.json"), "utf8")); - expect(state.threadId).toBe(opened.thread.id); + expect(state.threadId).toBe((opened.thread as { id: string }).id); expect(state.activeTurnId).toBe("provider-turn-1"); } finally { await bundle.transport.close(); diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport-settlement.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport-settlement.test.ts index 9c97cd48540..f06b0a3e8bf 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport-settlement.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport-settlement.test.ts @@ -286,7 +286,7 @@ it.each([ // output plus another full provider-owned prefix behind the old suspend. const runnerFile = join(original, "runner/runner-state.json"); const runnerBefore = JSON.parse(await readFile(runnerFile, "utf8")); - const template = builder.store.state.committedEvents[0]!.envelope.payload; + const template = builder.store.state.committedEvents[0]!.envelope.payload as Record; for (let index = 0; index < 90; index++) { const sourceSeq = runnerBefore.nextSourceSeq++; const event = { @@ -388,7 +388,7 @@ it.each([ sessionId: identity.normalizedSessionId, }, backend: { - kind: "codex", + kind: "runner" as const, name: maintenanceFixtureBackendName(fixtureId), }, identity, @@ -406,7 +406,7 @@ it.each([ recordEpoch, signal: maintenanceAbort.signal, }; - const close = vi.fn(async () => { + const close = vi.fn(async (): Promise => { throw new NativeSessionCloseUnrecoverableError(); }); const start = vi.fn(async () => { @@ -424,9 +424,10 @@ it.each([ capabilities: async () => capabilities, events: async function* () {}, startTurn: start, + result: async () => { throw new Error("fixture never produces a result"); }, close, snapshot: async () => ({ - backendKind: "codex", + backendKind: "runner", sessionId: input.binding.sessionId, identity: input.binding, providerSessionId: thread.thread.id, @@ -471,6 +472,7 @@ it.each([ normalizedSessionId: input.binding.sessionId, driverKind: "codex_app_server", protocolVersion: 1, + lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, }, completionContract: { id: "contract-maintenance", diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport-suspension.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport-suspension.test.ts index dba8ec470b3..004453760bd 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport-suspension.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport-suspension.test.ts @@ -211,6 +211,7 @@ it("does not retry a real memoized transport close whose suspension proof is una normalizedSessionId: identity.sessionId, driverKind: "codex_app_server", protocolVersion: 1, + lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, }, provider: { kind: "codex", model: null }, completionContract: { @@ -499,7 +500,7 @@ it.each([ message: { role: "user", text: "Keep working until interrupted." }, }); if (label === "interrupted") { - await session.interrupt({ + await session.interrupt!({ turnId: turn.turnId, reason: "Stopped by the user", }); @@ -525,7 +526,7 @@ it.each([ }, }); } finally { - await session.close().catch(() => undefined); + await session.close({ reason: "test complete" }).catch(() => undefined); await rm(stateDirectory, { recursive: true, force: true, diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport-warm-attachment.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport-warm-attachment.test.ts index 7739d4e2851..083290d5fae 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport-warm-attachment.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport-warm-attachment.test.ts @@ -13,6 +13,7 @@ import { codexSemanticToolSpecs } from "../drivers/codex/codex-app-server-driver import { releaseMaterializedNativeRuntimeSkills } from "../drivers/runtime-context-materializer.js"; import { createCapabilityRunnerdCodexTransport, readRunnerdArtifactBinding } from "./runnerd-codex-transport.js"; import { + committableStore, defaultCapabilityRunnerdBinary, fakeCodex, fakeCodexArgs, @@ -163,8 +164,9 @@ it.each([ providerPid = bundle.evidence().codexPid; const rotations: (typeof core.store.state)[] = []; const preparedStates: (typeof core.store.state)[] = []; - const commit = core.store.commit.bind(core.store); - vi.spyOn(core.store, "commit").mockImplementation((candidate) => { + const store = committableStore(core); + const commit = store.commit.bind(store); + vi.spyOn(store, "commit").mockImplementation((candidate) => { commit(candidate); // Capture the durable old epoch at publication. The authenticated // successor may activate before attachRun observes the completed command. @@ -716,7 +718,7 @@ it("recovers an active second warm business run after deleting its controller co await writeFile(join(skillRoot, "SKILL.md"), "# Pinned skill\n"); await writeFile(join(instructionRoot, "AGENTS.md"), "Pinned instructions\n"); const server = createServer(); - let authority: DurablePrpControlPlane | null = null, generation = 0; + let authority = null as DurablePrpControlPlane | null, generation = 0; let checkpoint: durableControlPlane.DurablePrpCheckpoint | undefined; const registered = new WeakSet(); server.on("upgrade", (request, socket, head) => authority @@ -760,7 +762,7 @@ it("recovers an active second warm business run after deleting its controller co providerPid = first.evidence().codexPid; expect(providerPid).toEqual(expect.any(Number)); const providerStart = execFileSync("ps", ["-p", String(providerPid), "-o", "lstart="], { encoding: "utf8" }).trim(); - const startedAt = first.transport.processInfo().startedAt; + const startedAt = first.transport.processInfo!().startedAt; const firstCompleted = waitComplete(first.transport); await within("first turn/start", first.transport.request("turn/start", { input: [{ type: "text", text: "first healthy business run" }] })); await within("healthy first turn completion", firstCompleted); @@ -805,7 +807,7 @@ it("recovers an active second warm business run after deleting its controller co expect(execFileSync("ps", ["-p", String(providerPid), "-o", "lstart="], { encoding: "utf8" }).trim()).toBe(providerStart); expect(calls).toBe(2); expect(authority!.store.state.commands.filter(command => command.type === "semantic_tool.result")).toHaveLength(0); - expect(recovered.transport.processInfo().startedAt).toBe(startedAt); + expect(recovered.transport.processInfo!().startedAt).toBe(startedAt); expect(authority!.store.state.identity).toEqual(savedIdentity); expect(authority!.store.state.commands.filter(command => command.type === "turn.start")).toHaveLength(1); expect(noSpawn).not.toHaveBeenCalled(); diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts index 1c2b35e6f7c..87253a57f48 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts @@ -283,7 +283,7 @@ it.each(["resolved", "rejected"] as const)( expect(error).toMatchObject({ message: expect.stringContaining("runner_process_termination_unconfirmed") }); if (outcome === "rejected") expect(error).toHaveProperty("cause", probeError); expect(children).toHaveLength(1); - expect(bundle.transport.processInfo()).toMatchObject({ exited: false }); + expect(bundle.transport.processInfo!()).toMatchObject({ exited: false }); expect(() => process.kill(children[0]!.pid!, 0)).not.toThrow(); } finally { const providerPid = bundle.evidence().codexPid; @@ -358,7 +358,7 @@ it("preserves the cause when a replacement runner's health cannot be verified", if (child) return { child: { pid: 42, exitCode: null, kill: () => false }, completion: Promise.reject(probeError), - isExitConfirmed: () => false, + isExitConfirmed: (): boolean => false, }; child = spawn(spec.command, [...spec.args], { cwd: spec.cwd, env: spec.environment, stdio: "ignore" }); const ownedChild = child; @@ -390,7 +390,7 @@ it("preserves the cause when a replacement runner's health cannot be verified", }); expect(error).toHaveProperty("message", expect.stringContaining("probe_timeout")); expect(launch).toHaveBeenCalledTimes(2); - expect(bundle.transport.processInfo()).toMatchObject({ exited: false }); + expect(bundle.transport.processInfo!()).toMatchObject({ exited: false }); } finally { const providerPid = bundle.evidence().codexPid; if (providerPid) { @@ -415,7 +415,7 @@ it("retries a replacement whose staging failed before any process launched", asy if (launch.mock.calls.length === 2) return { child: { pid: undefined, exitCode: null, kill: () => false }, completion: Promise.reject(stagingError), - isExitConfirmed: () => true, + isExitConfirmed: (): boolean => true, }; const child = spawn(spec.command, [...spec.args], { cwd: spec.cwd, env: spec.environment, stdio: "ignore" }); children.push(child); @@ -2659,7 +2659,7 @@ it.each(["reuse", "large-instructions", "rotated-recovery", "removed-context", " resolved.skills[0]!.bundle.rootPath = "/guest/.agents/skills/assigned"; const materialize = vi.spyOn(runtimeMaterializer, "prepareIsolatedCodexHome").mockRejectedValue(new Error("unexpected Core materialization")); const discover = vi.spyOn(codexCommandRuntime, "resolveCodexCommand").mockImplementation(() => { throw new Error("unexpected Core discovery"); }); - let configuration: Record | undefined; + let configuration: Record | null | undefined; let recovered = false; let requestedRun = ""; let requestedInstructions = ""; @@ -3574,7 +3574,7 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper let template: Record | null = null; let registrationStarted!: () => void; const started = new Promise(resolve => { registrationStarted = resolve; }); - let returnRegistration!: (registration: { connection: { mode: "listen"; listenAddress: string; listenPort: number; listenPath: string }; release: () => Promise }) => void; + let returnRegistration!: (registration: { connection: { mode: "listen"; listenAddress: "0.0.0.0"; listenPort: number; listenPath: string }; release: () => Promise }) => void; const registration = new Promise[0]>(resolve => { returnRegistration = resolve; }); let finishRelease!: () => void; const releasing = new Promise(resolve => { finishRelease = resolve; }); @@ -3585,7 +3585,7 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper if (releaseFails) throw releaseError; }); const registered = { - connection: { mode: "listen" as const, listenAddress: "127.0.0.1", listenPort: 43127, listenPath: "/setup-probe" }, + connection: { mode: "listen" as const, listenAddress: "0.0.0.0" as const, listenPort: 43127, listenPath: "/setup-probe" }, release, }; let registrationSignal: AbortSignal | undefined; @@ -4037,7 +4037,7 @@ it("runs the lab provider boundary through authenticated durable PRP", async () } expect(methods).toContain("turn/completed"); expect(terminalParams).toMatchObject({ - threadId: opened.thread.id, + threadId: (opened.thread as { id: string }).id, turnId: "provider-turn-1", }); expect(bundle.evidence().diagnostics).toContain( @@ -4081,7 +4081,7 @@ it("controls a Codex session goal end to end through durable PRP v2", async () = }, ], }); - const threadId = opened.thread.id; + const threadId = (opened.thread as { id: string }).id; await expect( bundle.transport.request("thread/goal/set", { @@ -4236,7 +4236,7 @@ it.each([false, true])("binds goal turns through the full Codex harness (autonom }); expect(observed.some((event) => event.eventType === "session.failed")).toBe(false); } finally { - await session?.close(); + await session?.close({ reason: "test complete" }); await bundle.transport.close(); await rm(stateDirectory, { recursive: true, force: true }); } @@ -4882,7 +4882,7 @@ it("binds an immediately failed durable turn before exposing its terminal", asyn ], }); } finally { - await session.close(); + await session.close({ reason: "test complete" }); await rm(stateDirectory, { recursive: true, force: true, @@ -5089,9 +5089,10 @@ it("captures exact provider frames and correlates Rust and TypeScript interpreta if (event.eventType === "turn.completed") break; } } finally { - await session.close(); + await session.close({ reason: "test complete" }); } const snapshot = await session.snapshot(); + if (snapshot.lastSourceSequence === undefined) throw new Error("Expected a source sequence in the session snapshot"); for ( let sourceSeq = 1; sourceSeq <= snapshot.lastSourceSequence; @@ -5791,7 +5792,7 @@ it.each([ expect(outcomes[1].result).toMatchObject({ content: [{ text: expect.stringContaining(feedback) }] }); if (adoptAfterTurn) { const snapshot = await session.snapshot(); - const runnerStartedAt = bundle.transport.processInfo().startedAt; + const runnerStartedAt = bundle.transport.processInfo!().startedAt; survivingPid = bundle.evidence().runnerPid; expect(survivingPid).toEqual(expect.any(Number)); const artifact = readRunnerdArtifactBinding(defaultCapabilityRunnerdBinary()); @@ -5833,7 +5834,7 @@ it.each([ expect(recovery.session).toBeDefined(); session = recovery.session!; expect(adopted.evidence().runnerPid).toBe(survivingPid); - expect(adopted.transport.processInfo().startedAt).toBe(runnerStartedAt); + expect(adopted.transport.processInfo!().startedAt).toBe(runnerStartedAt); expect((await session.snapshot()).providerSessionId).toBe(snapshot.providerSessionId); expect(duplicateLauncher).not.toHaveBeenCalled(); } @@ -5843,7 +5844,7 @@ it.each([ expect(allRequests.map(request => request.variant)).toEqual(supportsReasoning ? ["paperclip-no-reasoning", "paperclip-default"] : [undefined, undefined]); }, - closeSession: async () => { await session?.close(); }, + closeSession: async () => { await session?.close({ reason: "test complete" }); }, closeTransport: async () => { try { await adopted?.transport.close(); } finally { diff --git a/packages/paperclip-runner/src/live/runnerd-detach-durability.test.ts b/packages/paperclip-runner/src/live/runnerd-detach-durability.test.ts index d49672cf964..add7ff63fa8 100644 --- a/packages/paperclip-runner/src/live/runnerd-detach-durability.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-detach-durability.test.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { expect, it, vi } from "vitest"; -import { DurablePrpControlPlane } from "../control-plane/durable-prp-control-plane.js"; +import { DurablePrpControlPlane, type RunnerProcessResult } from "../control-plane/durable-prp-control-plane.js"; import { createCapabilityRunnerdCodexTransport, defaultCapabilityRunnerdBinary, readRunnerdArtifactBinding, runnerdRecoveryInternals } from "./runnerd-codex-transport.js"; it.each(["resolved", "rejected"] as const)("keeps the checkpoint behind controller retirement (%s)", async outcome => { @@ -39,7 +39,7 @@ it.each(["drain", "timeout", "external_close"] as const)("joins an output flush const artifact = readRunnerdArtifactBinding(binary); let core!: DurablePrpControlPlane; const start = DurablePrpControlPlane.prototype.start; - const startSpy = vi.spyOn(DurablePrpControlPlane.prototype, "start").mockImplementation(function (port) { + const startSpy = vi.spyOn(DurablePrpControlPlane.prototype, "start").mockImplementation(function (this: DurablePrpControlPlane, port) { core = this; return start.call(this, port); }); @@ -81,7 +81,7 @@ it.each(["drain", "timeout", "external_close"] as const)("joins an output flush let stderr = ""; owned.stderr?.on("data", chunk => { stderr += String(chunk); }); exit = new Promise(resolveExit => owned.once("exit", () => resolveExit())); - const completion = new Promise<{ code: number | null; signal: string | null; stdout: string; stderr: string }>((resolveExit, reject) => { + const completion = new Promise((resolveExit, reject) => { owned.once("error", reject); owned.once("exit", (code, signal) => resolveExit({ code, signal, stdout: "", stderr })); }); diff --git a/packages/paperclip-runner/src/live/runnerd-opencode-streaming-tools.test.ts b/packages/paperclip-runner/src/live/runnerd-opencode-streaming-tools.test.ts index 604cc561a98..1809b0cfc53 100644 --- a/packages/paperclip-runner/src/live/runnerd-opencode-streaming-tools.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-opencode-streaming-tools.test.ts @@ -76,14 +76,14 @@ it.skipIf(!compilerAvailable || !existsSync(runner)).each(["discovery", "complet expect(seen).toEqual(tool !== "completion" ? ["paperclip_search_assigned_tools", "paperclip_finish"] : ["paperclip_finish"]); expect(text.join("")).toBe("Streaming progress. ".repeat(800)); if (tool === "durable") { - const runnerPid = transport.processInfo().pid; + const runnerPid = transport.processInfo!().pid; await transport.attachRun!({ runId: "next-settled-run", turnId: "next-turn", itemId: "next-item" }); completed = (async () => { for await (const event of transport.notifications()) if (event.method === "turn/completed") return; })(); await transport.request("turn/start", { input: [{ type: "text", text: "stream-backlog-discovery" }] }); await completed; - expect(transport.processInfo().pid).toBe(runnerPid); + expect(transport.processInfo!().pid).toBe(runnerPid); expect(seen).toEqual(["paperclip_search_assigned_tools", "paperclip_finish", "paperclip_search_assigned_tools", "paperclip_finish"]); const provider = JSON.parse(await readFile(join(root, "state", "runner", "codex-provider-state.json"), "utf8")); expect(provider.toolBridge.pending).toEqual({}); diff --git a/packages/paperclip-runner/src/live/runnerd-restart-continuation.test.ts b/packages/paperclip-runner/src/live/runnerd-restart-continuation.test.ts index e1e10951657..efc2e95833b 100644 --- a/packages/paperclip-runner/src/live/runnerd-restart-continuation.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-restart-continuation.test.ts @@ -15,7 +15,7 @@ it.each(["none", "before-start", "after-start"] as const)("completes after the r const execution: NativeExecutionInputV1 = { schema: "paperclip.native-execution-input.v1", binding: { runId: identity.runId, companyId: identity.companyId, issueId: identity.issueId, agentId: identity.agentId, executionWorkspaceId: "workspace-restart" }, - session: { normalizedSessionId: identity.sessionId, driverKind: "codex_app_server", protocolVersion: 1 }, + session: { normalizedSessionId: identity.sessionId, driverKind: "codex_app_server", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } }, task: { identifier: "TEST-1", title: "Finish checks", description: null, prompt: "Watch the already running checks and finish the request.", workMode: "standard" }, workspace: { cwd: root, repoUrl: null, repoRef: null, branchName: null }, provider: { kind: "codex", model: null }, @@ -88,7 +88,7 @@ it.each(["none", "before-start", "after-start"] as const)("completes after the r throw interruptedController; } }, - async appendEvent(event) { events.push(event); durableEvents.push(event); return { cursor: durableEvents.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async appendEvent(event) { if (!("sourceSeq" in event)) throw new Error("Expected a durable PRP event"); events.push(event); durableEvents.push(event); return { cursor: durableEvents.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents(request) { const source = durableEvents.filter(event => event.sourceInstanceId === request.sourceInstanceId); return { events: source.filter(event => event.sourceSeq > request.afterSourceSeq), highestContiguousSourceSeq: source.at(-1)?.sourceSeq ?? 0 }; diff --git a/packages/paperclip-runner/src/live/runnerd-runtime-activity.test.ts b/packages/paperclip-runner/src/live/runnerd-runtime-activity.test.ts index 96bec323f05..6e2e3c46e14 100644 --- a/packages/paperclip-runner/src/live/runnerd-runtime-activity.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-runtime-activity.test.ts @@ -63,7 +63,7 @@ it("delivers authenticated runtime activity through the real Runner bridge acros const firstContext = (await helper(["notification-context"])).stdout.trim(); for (const runId of ["activity-first", "activity-next"]) { if (runId === "activity-next") { - attaching = session.attachRun!({ runId }); + attaching = Promise.resolve(session.attachRun!({ runId })); await releasing; await vi.waitFor(async () => { expect((await helper(["notification-context"])).stdout.trim()).not.toBe(firstContext); @@ -93,7 +93,7 @@ it("delivers authenticated runtime activity through the real Runner bridge acros } finally { releasePrevious(); await attaching?.catch(() => undefined); - await session?.close(); + await session?.close({ reason: "test complete" }); await bundle.transport.close(); await rm(root, { recursive: true, force: true }); } diff --git a/packages/paperclip-runner/src/live/runnerd-saved-continuation.test.ts b/packages/paperclip-runner/src/live/runnerd-saved-continuation.test.ts index 8f750ea11e7..72fcb3c100c 100644 --- a/packages/paperclip-runner/src/live/runnerd-saved-continuation.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-saved-continuation.test.ts @@ -49,7 +49,7 @@ it.each(["omitted", "null", "resolved"] as const)("retains a server-owned worksp expect(terminals).toHaveLength(1); expect(terminals[0].envelope.payload).toMatchObject({ runId, eventType: "run.terminal", payload: { runTerminalState: "succeeded", reportedWorkDisposition: "yielded" } }); } finally { - await session?.close(); + await session?.close({ reason: "test complete" }); await bundle.transport.close(); await rm(root, { recursive: true, force: true }); } diff --git a/packages/paperclip-runner/src/live/runnerd-source-reconciliation.test.ts b/packages/paperclip-runner/src/live/runnerd-source-reconciliation.test.ts index 0728c1b8832..6ef8adc0f4b 100644 --- a/packages/paperclip-runner/src/live/runnerd-source-reconciliation.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-source-reconciliation.test.ts @@ -61,7 +61,7 @@ async function sourceFixture(registration?: CapabilityRunnerdCodexTransportOptio runId: identity.runId, executionWorkspaceId: "workspace" }, task: { identifier: "TASK-1", title: "Complete fake work", description: null, prompt: "Complete fake work", workMode: "standard" }, workspace: { cwd: root, repoUrl: null, repoRef: null, branchName: null }, - session: { normalizedSessionId: identity.normalizedSessionId, driverKind: "codex_app_server", protocolVersion: 1 }, + session: { normalizedSessionId: identity.normalizedSessionId, driverKind: "codex_app_server", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } }, provider: { kind: "codex", model: null, approvalPolicy: "never" }, completionContract: { id: "contract", sha256: `sha256:${"a".repeat(64)}`, schemaVersion: "paperclip.completion-contract.v1", contract: { revision: "1", objective: "Complete fake work", criteria: [{ id: "objective", requirement: "Complete fake work" }] } }, interactionResponses: [], credentialBindings: [] }; @@ -231,7 +231,7 @@ it.each([["launch", "default"], ["adopt", "default"], ["adopt-denied", "default" expect((await read(fixture.coreFile)).preparedLaunchConfigurations).toEqual(before.preparedLaunchConfigurations); await expect(readFile(join(fixture.root, "launch-preparation.json"))).rejects.toHaveProperty("code", "ENOENT"); if (mode !== "launch") { - const evidence = resumed.evidence(), startedAt = resumed.transport.processInfo().startedAt; + const evidence = resumed.evidence(), startedAt = resumed.transport.processInfo!().startedAt; await resumed.detachControllerForRestart(); prepare.mockClear(); if (mode === "adopt-denied") prepare.mockRejectedValueOnce(new Error("source preparation authorization revoked")); diff --git a/packages/paperclip-runner/src/live/runnerd-staged-handoff.test.ts b/packages/paperclip-runner/src/live/runnerd-staged-handoff.test.ts index 4e16fd7a3b7..bdad642b3d5 100644 --- a/packages/paperclip-runner/src/live/runnerd-staged-handoff.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-staged-handoff.test.ts @@ -205,7 +205,7 @@ it.each([["connect", "owned"], ["listen", "owned"], ["external", "owned"], ["con try { const opened = await within(first.transport.request("thread/start", { cwd: root, dynamicTools: [] }), "source open"); runnerPid = first.evidence().runnerPid; providerPid = first.evidence().providerPid; - const startedAt = first.transport.processInfo().startedAt; + const startedAt = first.transport.processInfo!().startedAt; const providerStart = execFileSync("ps", ["-p", String(providerPid), "-o", "lstart="], { encoding: "utf8" }).trim(); expect(runnerPid).toBeGreaterThan(0); expect(providerPid).toBeGreaterThan(0); const attach = first.transport.attachRun!({ runId: target.runId, turnId: target.turnId, itemId: target.itemId }); diff --git a/packages/paperclip-runner/src/live/runnerd-trace-frame-index.test.ts b/packages/paperclip-runner/src/live/runnerd-trace-frame-index.test.ts index 2c1731e7ee2..63cb83f5fcb 100644 --- a/packages/paperclip-runner/src/live/runnerd-trace-frame-index.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-trace-frame-index.test.ts @@ -9,6 +9,9 @@ vi.mock("node:fs", async (importOriginal) => { return { ...fs, readSync: vi.fn(fs.readSync) }; }); +type PositionalRead = [fd: number, buffer: NodeJS.ArrayBufferView, offset: number, length: number, position: number | null]; +const readPosition = (call: number) => (vi.mocked(readSync).mock.calls[call] as unknown as PositionalRead)[4]; + const interpretation = (frameId: number, ...emittedEventIds: string[]) => `${JSON.stringify({ kind: "interpretation", frameId, emittedEventIds })}\n`; const settled = `${JSON.stringify({ kind: "trace_status", debugChannel: "rust_native", status: "complete" })}\n`; @@ -41,7 +44,7 @@ describe("runnerd trace frame index", () => { expect(index.locate(path, "pending-1")).toEqual({ frameId: 2, nativeChannelSettled: true }); expect(index.locate(path, "missing")).toEqual({ frameId: null, nativeChannelSettled: true }); expect(readSync).toHaveBeenCalledTimes(2); - expect(vi.mocked(readSync).mock.calls[1]![4]).toBe(Buffer.byteLength(first)); + expect(readPosition(1)).toBe(Buffer.byteLength(first)); }); it("retains partial lines and split UTF-8 until the append is complete", () => { @@ -87,7 +90,7 @@ describe("runnerd trace frame index", () => { expect(index.locate(path, "last").frameId).toBeNull(); expect(index.locate(path, "last").frameId).toBeNull(); expect(index.locate(path, "last").frameId).toBe(4); - for (const call of vi.mocked(readSync).mock.calls) expect(call[3]).toBeLessThanOrEqual(1024 * 1024); + for (const call of vi.mocked(readSync).mock.calls as unknown as PositionalRead[]) expect(call[3]).toBeLessThanOrEqual(1024 * 1024); const calls = vi.mocked(readSync).mock.calls.length; for (let i = 0; i < 1000; i++) index.locate(path, "missing"); expect(readSync).toHaveBeenCalledTimes(calls); @@ -124,8 +127,8 @@ describe("runnerd trace frame index", () => { } expect(readSync).toHaveBeenCalledTimes(72); for (let i = 0; i < 24; i++) { - expect(vi.mocked(readSync).mock.calls[24 + i]![4]).toBe(1024 * 1024); - expect(vi.mocked(readSync).mock.calls[48 + i]![4]).toBe(2 * 1024 * 1024); + expect(readPosition(24 + i)).toBe(1024 * 1024); + expect(readPosition(48 + i)).toBe(2 * 1024 * 1024); } }); diff --git a/packages/paperclip-runner/src/live/runnerd-welcome.integration.test.ts b/packages/paperclip-runner/src/live/runnerd-welcome.integration.test.ts index 49d44895434..933dc68564f 100644 --- a/packages/paperclip-runner/src/live/runnerd-welcome.integration.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-welcome.integration.test.ts @@ -39,7 +39,7 @@ it("starts one provider after delayed bootstrap admission and retains it for a s await bundle.transport.request("turn/start", { input: [{ type: "text", text }] }); for await (const event of bundle.transport.notifications()) { if (event.method === "turn/completed") { - expect(event.params).toMatchObject({ threadId: opened.thread.id }); + expect(event.params).toMatchObject({ threadId: (opened.thread as { id: string }).id }); terminalTurns.push(event.params); break; } diff --git a/packages/paperclip-runner/src/live/scenario-chat-demo-server.test.ts b/packages/paperclip-runner/src/live/scenario-chat-demo-server.test.ts index 59c88dfdd32..9b22f8f769d 100644 --- a/packages/paperclip-runner/src/live/scenario-chat-demo-server.test.ts +++ b/packages/paperclip-runner/src/live/scenario-chat-demo-server.test.ts @@ -136,7 +136,7 @@ describe("Scenario chat deployment boundary", () => { it("requires exact Origin and Fetch Metadata on every mutation", async () => { await withHttpRuntime(async ({ request }) => { const body = JSON.stringify({ scenarioId: SCENARIO_ID }); - const invalidHeaders = [ + const invalidHeaders: Record[] = [ { origin: `${PUBLIC_ORIGIN}/` }, { "sec-fetch-site": "none" }, { "sec-fetch-site": "cross-site" }, diff --git a/packages/paperclip-runner/src/live/turn-stream.test.ts b/packages/paperclip-runner/src/live/turn-stream.test.ts index e91f6e83527..f3547736500 100644 --- a/packages/paperclip-runner/src/live/turn-stream.test.ts +++ b/packages/paperclip-runner/src/live/turn-stream.test.ts @@ -14,6 +14,7 @@ import { type CapabilityLiveTransportFactory, type CapabilityLiveTurnEvent, } from "./live-session.js"; +import type { CapabilityRunnerdProcessEvidence } from "./runnerd-codex-transport.js"; import { createCapabilityTurnStreamDecoder, encodeCapabilityTurnStreamFrame, @@ -161,10 +162,25 @@ class GatedTransport implements CodexAppServerTransport { function gatedTransportFactory(gate: Gate): CapabilityLiveTransportFactory { return (options) => { - const evidence = { + const evidence: CapabilityRunnerdProcessEvidence = { runnerPid: 7100, runnerProcessGroupId: 7100, + providerPid: null, + providerProcessStartedAt: null, codexPid: 7200, + codexProcessStartedAt: null, + sidecarPid: null, + sidecarProcessStartedAt: null, + agentPid: null, + agentProcessStartedAt: null, + providerDriver: null, + providerVersion: null, + acpxAgent: null, + agentServerVersion: null, + agentRuntimeVersion: null, + acpProtocolVersion: null, + providerExecutionKind: null, + providerService: null, runnerExited: false, runnerExitCode: null, runnerSignal: null, @@ -172,7 +188,7 @@ function gatedTransportFactory(gate: Gate): CapabilityLiveTransportFactory { diagnostics: [], }; options.onEvidence?.(evidence); - return { transport: new GatedTransport(gate), evidence: () => ({ ...evidence }) }; + return { transport: new GatedTransport(gate), evidence: () => ({ ...evidence }), detachControllerForRestart: async () => {} }; }; } diff --git a/packages/paperclip-runner/src/mock-core/capability-mock-control-plane-adapter.test.ts b/packages/paperclip-runner/src/mock-core/capability-mock-control-plane-adapter.test.ts index a21225b63c8..80e19a86926 100644 --- a/packages/paperclip-runner/src/mock-core/capability-mock-control-plane-adapter.test.ts +++ b/packages/paperclip-runner/src/mock-core/capability-mock-control-plane-adapter.test.ts @@ -584,6 +584,7 @@ describe("CapabilityMockControlPlaneAdapter", () => { commandKind: "record_budget_usage", effect: "lost_ack", remaining: 1, + code: "budget_ack_lost", }], }); await adapter.start(); diff --git a/packages/paperclip-runner/src/mock-core/live-console-demo-server.test.ts b/packages/paperclip-runner/src/mock-core/live-console-demo-server.test.ts index c370aaf6e1b..56cfb03309a 100644 --- a/packages/paperclip-runner/src/mock-core/live-console-demo-server.test.ts +++ b/packages/paperclip-runner/src/mock-core/live-console-demo-server.test.ts @@ -333,7 +333,7 @@ class StubDriver implements HarnessDriver { const servers: LiveConsoleDemoServer[] = []; -function browserHeaders(url: string, headers: HeadersInit = {}): Headers { +function browserHeaders(url: string, headers: ConstructorParameters[0] = {}): Headers { const result = new Headers(headers); result.set("origin", new URL(url).origin); result.set("sec-fetch-site", "same-origin"); diff --git a/packages/paperclip-runner/src/mock-core/live-console-scripted-driver.test.ts b/packages/paperclip-runner/src/mock-core/live-console-scripted-driver.test.ts index 42010232374..9fa24ed1a2c 100644 --- a/packages/paperclip-runner/src/mock-core/live-console-scripted-driver.test.ts +++ b/packages/paperclip-runner/src/mock-core/live-console-scripted-driver.test.ts @@ -82,6 +82,7 @@ function reduce(events: readonly PrpEvent[]): SessionSnapshot { environmentLeaseId: "package-local", runnerInstanceId: "live-console-demo-server", normalizedSessionId: events[0]?.normalizedSessionId ?? "session", + driverSessionId: "live-console-demo", }, capabilities: { schema: "paperclip.prp.capabilities.v1", diff --git a/packages/paperclip-runner/src/mock-core/local-runner.test.ts b/packages/paperclip-runner/src/mock-core/local-runner.test.ts index 3fe96dcdfe0..d60b3296f60 100644 --- a/packages/paperclip-runner/src/mock-core/local-runner.test.ts +++ b/packages/paperclip-runner/src/mock-core/local-runner.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; +import type { LocalRunnerRunMetadata, LocalRunnerRunTrace } from "../contracts/local-runner.js"; import { applyLocalRunnerLiveEvent, createLocalRunnerLiveSnapshot, @@ -13,6 +14,16 @@ import { startLocalRunnerScenario, } from "./local-runner.js"; +function traceMetadata(trace: LocalRunnerRunTrace): LocalRunnerRunMetadata { + return { + schema: "paperclip.runner.local-runner.metadata.v1", + scenario: trace.scenario, + fixtureName: trace.fixtureName, + identity: trace.identity, + capabilities: trace.capabilities, + }; +} + describe("Local runner and fake harness", () => { it("runs lifecycle, tool, file, structured-result, and process-exit events", async () => { const trace = await runLocalRunnerScenario({ scenario: "happy-path", delayMs: 1 }); @@ -108,7 +119,7 @@ describe("Local runner and fake harness", () => { ), ).toBe(true); expect( - replayLocalRunnerEvents(trace, trace.events).timeline.find( + replayLocalRunnerEvents(traceMetadata(trace), trace.events).timeline.find( (event) => event.eventType === "harness.diagnostic", )?.summary, ).toBe( @@ -160,7 +171,7 @@ describe("Local runner and fake harness", () => { it("produces the same snapshot for validated live events and replay", async () => { const trace = await runLocalRunnerScenario({ scenario: "happy-path", delayMs: 1 }); - let live = createLocalRunnerLiveSnapshot(trace); + let live = createLocalRunnerLiveSnapshot(traceMetadata(trace)); for (const event of trace.events) { const next = applyLocalRunnerLiveEvent(live, event); expect(next.ok).toBe(true); @@ -168,7 +179,7 @@ describe("Local runner and fake harness", () => { live = next.snapshot; } } - const replay = replayLocalRunnerEvents(trace, trace.events); + const replay = replayLocalRunnerEvents(traceMetadata(trace), trace.events); expect(localRunnerSnapshotsMatch(live, replay)).toBe(true); }); diff --git a/packages/paperclip-runner/src/native-session-runtime.test.ts b/packages/paperclip-runner/src/native-session-runtime.test.ts index 2f9fc42456c..de63070094e 100644 --- a/packages/paperclip-runner/src/native-session-runtime.test.ts +++ b/packages/paperclip-runner/src/native-session-runtime.test.ts @@ -33,6 +33,7 @@ import { PAPERCLIP_EXECUTION_PROMPT_REVISION, canonicalNativeRuntimeContextDigest, nativeRuntimePromptDigest, + type NativeRuntimeContextSnapshot, } from "./contracts/runtime-context.js"; import { executeNativeSession, @@ -129,6 +130,7 @@ const input: NativeExecutionInputV1 = { normalizedSessionId: identity.sessionId, driverKind: "codex_app_server", protocolVersion: 1, + lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, }, provider: { kind: "codex", model: null }, completionContract: { @@ -145,13 +147,15 @@ const input: NativeExecutionInputV1 = { credentialBindings: [], }; +type RuntimeContextFields = Omit; + function preparedInput(): NativeExecutionInputV5 { const digest = "0".repeat(64); const context = { prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() }, instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } }, skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null }, - } as const; + } satisfies RuntimeContextFields; const requirement = input.completionContract.contract.criteria[0]!.requirement; const prompt = `${input.task.prompt}\n\n${requirement}`; return { @@ -256,7 +260,7 @@ describe("executeNativeSession recovery", () => { const completeRun = vi.fn(async () => undefined); const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { committed.push(structuredClone(event)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async appendEvent(event: PrpEvent) { committed.push(structuredClone(event)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents(query) { const events = committed.filter(event => event.sourceInstanceId === query.sourceInstanceId); return { events: events.filter(event => event.sourceSeq > query.afterSourceSeq).slice(0, query.limit), highestContiguousSourceSeq: highestContiguous(events) }; }, completeRun, }; @@ -318,7 +322,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, async loadSessionCheckpoint() { return recovered ? structuredClone(snapshot) : null; }, - async appendEvent(event) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async appendEvent(event: PrpEvent) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, async completeRun() {}, }; @@ -390,7 +394,7 @@ describe("executeNativeSession recovery", () => { async openRun() {}, async loadSessionCheckpoint() { return structuredClone(checkpoint); }, async checkpointSession(value) { checkpoints.push(structuredClone(value)); }, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { const existing = journal.find(value => value.sourceEventId === event.sourceEventId); if (existing) expect(event).toEqual(existing); else journal.push(structuredClone(event as PrpEvent)); @@ -479,7 +483,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession(snapshot) { checkpoints.push(structuredClone(snapshot)); }, - async appendEvent(event) { events.push(event); return { cursor: events.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async appendEvent(event: PrpEvent) { events.push(event); return { cursor: events.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, async completeRun() {}, }; @@ -533,7 +537,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async appendEvent(event: PrpEvent) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, async completeRun() {}, }; @@ -555,7 +559,7 @@ describe("executeNativeSession recovery", () => { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true, }; - const cancel = vi.fn(async () => { finish(); }); + const cancel = vi.fn(() => { finish(); return { cleanup: Promise.resolve() }; }); const close = vi.fn(async () => { finish(); }); const session: NativeSession = { identity: () => identity, @@ -574,7 +578,7 @@ describe("executeNativeSession recovery", () => { }, cancel, close, }; - const appendEvent = vi.fn(async event => ({ + const appendEvent = vi.fn(async (event: PrpEvent) => ({ cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed", })); @@ -728,7 +732,7 @@ describe("executeNativeSession recovery", () => { prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() }, instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } }, skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null }, - } as const; + } satisfies RuntimeContextFields; const completed = await executeNativeSession({ input: snapshotBeforeUpdate ? { ...input, schema: "paperclip.native-execution-input.v4", executionMode: "default", planningContext: null, @@ -748,7 +752,7 @@ describe("executeNativeSession recovery", () => { controlPlane: { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { appended.push(event); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, @@ -885,7 +889,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { appended.push(event); return { cursor: event.sourceSeq, @@ -1034,7 +1038,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { appended.push(event); return { cursor: event.sourceSeq, @@ -1185,7 +1189,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, @@ -1241,7 +1245,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents() { @@ -1574,7 +1578,7 @@ describe("executeNativeSession recovery", () => { }, controlPlane: { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { appended.push(event); return { + async appendEvent(event: PrpEvent) { appended.push(event); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed", }; }, async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, @@ -2568,12 +2572,12 @@ describe("executeNativeSession recovery", () => { }, skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null }, - } as const; + } satisfies RuntimeContextFields; const openSession = vi.fn(); const backend: NativeSessionBackend = { async descriptor() { return { - kind: "future-provider", + kind: "mock", name: "future-provider", version: "1", capabilities: { @@ -2741,6 +2745,7 @@ describe("executeNativeSession recovery", () => { typedEvents: true, steering: false, interruption: true, + structuredResult: true, }; }, async *events() {}, @@ -2854,7 +2859,7 @@ describe("executeNativeSession recovery", () => { const snapshot = vi.fn(async () => { throw snapshotFailure; }); const session: NativeSession = { identity: () => identity, - async capabilities() { return { resume: false, typedEvents: true, steering: false, interruption: true }; }, + async capabilities() { return { resume: false, typedEvents: true, steering: false, interruption: true, structuredResult: true }; }, async *events() {}, async startTurn() { throw new Error("unexpected turn"); }, async result() { return null; }, @@ -2886,6 +2891,7 @@ describe("executeNativeSession recovery", () => { typedEvents: true, steering: false, interruption: true, + structuredResult: true, }; }, async *events() {}, @@ -3509,7 +3515,7 @@ describe("executeNativeSession recovery", () => { } }, ); - const checkpointSession = vi.fn( + const checkpointSession = vi.fn>( async () => undefined, ); const port: ControlPlanePort = { @@ -3744,8 +3750,9 @@ describe("executeNativeSession recovery", () => { timeoutMs: 10, keepSessionOpen: true, onSession: (current) => retainedSessions.push(current), - onPostCompletionEnrichmentFailure: ({ stage }) => - enrichmentFailures.push(stage), + onPostCompletionEnrichmentFailure: ({ stage }) => { + enrichmentFailures.push(stage); + }, }); await enrichmentStalled; if (stalledSignal !== undefined) @@ -4650,12 +4657,12 @@ describe("executeNativeSession recovery", () => { async *events() { throw new Error("provider failed after editing instructions"); }, startTurn: async () => ({ turnId: "instruction-turn" }), result: async () => null, - snapshot: async () => ({ backendKind: "local", sessionId: "instruction-session", identity: scopedIdentity, + snapshot: async () => ({ backendKind: "runner", sessionId: "instruction-session", identity: scopedIdentity, providerSessionId: "instruction-provider", cursor: null, activeTurnId: null, pendingRuntimeRequests: [], lineage: [] }), close: async () => { order.push("closing"); await closed; order.push("stopped"); }, }; const backend: NativeSessionBackend = { - descriptor: async () => ({ kind: "local", name: "instruction-close-test", version: "1", capabilities }), + descriptor: async () => ({ kind: "runner", name: "instruction-close-test", version: "1", capabilities }), openSession: async () => session, }; const controlPlane: ControlPlanePort = { @@ -4747,7 +4754,7 @@ describe("executeNativeSession recovery", () => { close: vi.fn(async () => { throw failure; }), }; const backend: NativeSessionBackend = { - descriptor: async () => ({ kind: "local", name: "local-stop-test", version: "1", capabilities }), + descriptor: async () => ({ kind: "runner", name: "local-stop-test", version: "1", capabilities }), openSession: vi.fn(async () => session), }; const controlPlane: ControlPlanePort = { @@ -4982,7 +4989,7 @@ describe("executeNativeSession recovery", () => { runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", timeoutMs: 5_000, controlPlane: { async openRun() {}, async checkpointSession() {}, completeRun, - async appendEvent(event, options) { + async appendEvent(event: PrpEvent, options) { if (options?.deferPostCommitObservers) deferredEventTypes.push(event.eventType); events.push(structuredClone(event)); return { cursor: events.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed", @@ -5080,7 +5087,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { events.push(structuredClone(event as PrpEvent)); return { cursor: events.length, @@ -5199,7 +5206,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { events.push(structuredClone(event as PrpEvent)); const sourceEvents = events.filter( (candidate) => candidate.sourceInstanceId === event.sourceInstanceId, @@ -5417,7 +5424,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { if (event.eventType === "item.completed") { persistingAnswer = true; await persistence; @@ -5925,7 +5932,7 @@ describe("executeNativeSession recovery", () => { const openReplacementSession = vi.fn(); const backend: NativeSessionBackend = { descriptor: async () => ({ kind: "mock", name: "incompatible", version: "1", - capabilities: { resume: true, typedEvents: true, steering: false, interruption: true } }), + capabilities: { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true } }), openSession, openReplacementSession, recoverSession: async () => ({ recovered: false, reason: "saved launch profile changed", failureClass: PROVIDER_STATE_INCOMPATIBLE }), @@ -6093,6 +6100,7 @@ describe("executeNativeSession recovery", () => { typedEvents: true, steering: false, interruption: true, + structuredResult: true, }; }, async *events() {}, @@ -6136,6 +6144,7 @@ describe("executeNativeSession recovery", () => { typedEvents: true, steering: false, interruption: true, + structuredResult: true, }, }; }, @@ -6224,6 +6233,7 @@ describe("executeNativeSession recovery", () => { typedEvents: true, steering: false, interruption: true, + structuredResult: true, }; }, async *events() {}, @@ -6268,6 +6278,7 @@ describe("executeNativeSession recovery", () => { typedEvents: true, steering: false, interruption: true, + structuredResult: true, }, }; }, @@ -6375,7 +6386,7 @@ describe("executeNativeSession recovery", () => { }, async replayEvents(replay) { return { - events: replay.afterSourceSeq === 0 ? [runnerEvent(1)] : [], + events: replay.afterSourceSeq === 0 ? [runnerEvent(1, "turn.started")] : [], highestContiguousSourceSeq: 1, }; }, @@ -6479,7 +6490,7 @@ describe("executeNativeSession recovery", () => { }, async replayEvents(replay) { return { - events: replay.afterSourceSeq === 0 ? [runnerEvent(1)] : [], + events: replay.afterSourceSeq === 0 ? [runnerEvent(1, "turn.started")] : [], highestContiguousSourceSeq: 1, }; }, @@ -6530,7 +6541,7 @@ describe("executeNativeSession recovery", () => { sourceEventId: "provider-recovery:1", sourceSeq: 1, sourceInstanceId: "provider-recovery", - sourceKind: "provider", + sourceKind: "runner", runId: identity.runId, normalizedSessionId: identity.sessionId, turnId: "turn-recovery", @@ -6594,7 +6605,7 @@ describe("executeNativeSession recovery", () => { return structuredClone(checkpoint); }, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { const list = bySource.get(event.sourceInstanceId) ?? []; list.push(structuredClone(event)); bySource.set(event.sourceInstanceId, list); @@ -6718,7 +6729,7 @@ describe("executeNativeSession recovery", () => { async checkpointSession(snapshot) { checkpoints.push(structuredClone(snapshot)); }, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { const target = event.sourceInstanceId === "runner-recovery" ? runnerEvents @@ -6852,7 +6863,7 @@ describe("executeNativeSession recovery", () => { return structuredClone(checkpoint); }, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { events.push(structuredClone(event)); return { cursor: events.length, @@ -6989,8 +7000,8 @@ describe("executeNativeSession recovery", () => { instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } }, skills: [skill], mcp: { assignmentSetId: "none", digest, bindingId: null }, - } as const; - const legacyContext = { ...context, skills: [] as const }; + } satisfies RuntimeContextFields; + const legacyContext = { ...context, skills: [] } satisfies RuntimeContextFields; const prepared = preparedInput(); const executionInput = preparedMode ? { @@ -7049,7 +7060,7 @@ describe("executeNativeSession recovery", () => { terminal: null, terminalTurns: [], }; - const startTurn = vi.fn(async () => ({ turnId: "turn-replacement" })); + const startTurn = vi.fn(async () => ({ turnId: "turn-replacement" })); const replacementSession: NativeSession = { identity: () => identity, async capabilities() { @@ -7108,7 +7119,7 @@ describe("executeNativeSession recovery", () => { return structuredClone(checkpoint); }, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { events.push(structuredClone(event)); return { cursor: events.length, @@ -7194,7 +7205,7 @@ describe("executeNativeSession recovery", () => { sourceEventId: "provider-recovery:2", sourceSeq: 2, sourceInstanceId: "provider-recovery", - sourceKind: "provider", + sourceKind: "runner", runId: identity.runId, normalizedSessionId: identity.sessionId, turnId: "turn-continuation", @@ -7204,7 +7215,7 @@ describe("executeNativeSession recovery", () => { emittedAt: "2026-08-09T00:00:01.000Z", payload: {}, }; - const startTurn = vi.fn(async () => ({ turnId: "turn-continuation" })); + const startTurn = vi.fn(async () => ({ turnId: "turn-continuation" })); const session: NativeSession = { identity: () => identity, async capabilities() { @@ -7273,7 +7284,7 @@ describe("executeNativeSession recovery", () => { return structuredClone(checkpoint); }, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { const list = bySource.get(event.sourceInstanceId) ?? []; list.push(structuredClone(event)); bySource.set(event.sourceInstanceId, list); @@ -7490,7 +7501,7 @@ describe("executeNativeSession recovery", () => { sourceEventId: "provider-recovery:2", sourceSeq: 2, sourceInstanceId: "provider-recovery", - sourceKind: "provider", + sourceKind: "runner", runId: identity.runId, normalizedSessionId: identity.sessionId, turnId: "turn-disposition", @@ -7571,7 +7582,7 @@ describe("executeNativeSession recovery", () => { prematureDispositionCheckpoint = true; durableCheckpoint = structuredClone(snapshot); }, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { if (event.eventType === terminalType && failNextAppend) { failNextAppend = false; throw appendFailure; @@ -7674,7 +7685,7 @@ describe("executeNativeSession recovery", () => { sourceEventId: "runner-recovery:run-native:4", sourceSeq: 4, sourceInstanceId: "runner-recovery", - sourceKind: "provider", + sourceKind: "runner", runId: identity.runId, normalizedSessionId: identity.sessionId, turnId: "turn-disposition", @@ -7784,7 +7795,7 @@ describe("executeNativeSession recovery", () => { return structuredClone(checkpoint); }, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { const list = bySource.get(event.sourceInstanceId) ?? []; list.push(structuredClone(event)); bySource.set(event.sourceInstanceId, list); @@ -7935,7 +7946,7 @@ describe("executeNativeSession recovery", () => { return structuredClone(checkpoint); }, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { const list = bySource.get(event.sourceInstanceId) ?? []; list.push(structuredClone(event)); bySource.set(event.sourceInstanceId, list); @@ -8085,7 +8096,7 @@ describe("executeNativeSession recovery", () => { pendingRuntimeRequests: [], lineage: [], }; - const events = [ + const events: PrpEvent[] = [ { ...controlEvent(1, "run.result.accepted", { result }), turnId: "turn-with-result", @@ -8223,7 +8234,7 @@ describe("executeNativeSession recovery", () => { backend: { async descriptor() { return { kind: "mock", name: "missing-current-completion", version: "1", capabilities }; }, async openSession() { return session; } }, controlPlane: { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { events.push(structuredClone(event as PrpEvent)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async appendEvent(event: PrpEvent) { events.push(structuredClone(event as PrpEvent)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, completeRun, }, runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", timeoutMs: 1000, resolveMissingResult: async () => null, @@ -8239,7 +8250,7 @@ describe("executeNativeSession recovery", () => { sourceEventId: "provider-recovery:1", sourceSeq: 1, sourceInstanceId: "provider-recovery", - sourceKind: "provider", + sourceKind: "runner", runId: identity.runId, normalizedSessionId: identity.sessionId, turnId: "turn-waiting", @@ -8420,7 +8431,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, completeRun: complete, async checkpointSession(value) { checkpoint = structuredClone(value); }, - async appendEvent(event) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async appendEvent(event: PrpEvent) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents(request) { return { events: history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq), highestContiguousSourceSeq: 0 }; }, }; const outcome = expect(executeNativeSession({ input, backend, controlPlane: port, timeoutMs: 1_000, @@ -8475,7 +8486,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, completeRun: complete, async checkpointSession(value) { checkpoint = structuredClone(value); }, - async appendEvent(event) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, + async appendEvent(event: PrpEvent) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, async replayEvents(request) { return { events: history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq), highestContiguousSourceSeq: 0 }; }, }; const execution = executeNativeSession({ input, backend, controlPlane: port, timeoutMs: operationMs ?? 120_000, @@ -8523,7 +8534,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, completeRun: complete, async checkpointSession(value) { checkpoint = structuredClone(value); }, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { const duplicate = history.some(row => row.sourceEventId === event.sourceEventId); if (!duplicate) history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: duplicate ? "duplicate" : "committed" }; @@ -8585,7 +8596,7 @@ describe("executeNativeSession recovery", () => { throw new Error("controller lost after governed checkpoint"); } }, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { if (!history.some(row => row.sourceEventId === event.sourceEventId)) history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; }, @@ -8673,14 +8684,14 @@ describe("executeNativeSession recovery", () => { }), sourceEventId: "provider-recovery:1", sourceInstanceId: "provider-recovery", - sourceKind: "provider", + sourceKind: "runner", turnId: "turn-waiting", }; const turnInterrupted: PrpEvent = { ...controlEvent(3, "turn.interrupted", { reason: "governed_wait" }), sourceEventId: "provider-recovery:3", sourceInstanceId: "provider-recovery", - sourceKind: "provider", + sourceKind: "runner", turnId: "turn-waiting", }; let releaseCancelled!: () => void; @@ -8945,7 +8956,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { events.push(structuredClone(event as PrpEvent)); if (event.eventType === "runtime_request.created") releaseCreated(); const sourceEvents = events.filter( @@ -9491,7 +9502,7 @@ describe("executeNativeSession recovery", () => { const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, - async appendEvent(event) { + async appendEvent(event: PrpEvent) { if (event.eventType === "runtime_request.resolved") { markSettlementAppendStarted(); await settlementAppendReleased; @@ -9521,11 +9532,11 @@ describe("executeNativeSession recovery", () => { }; const originalSetTimeout = globalThis.setTimeout; - let queuedHandoffCallback: (() => void) | null = null; + let queuedHandoffCallback = null as (() => void) | null; const timeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation((( - callback, - delay, - ...args + callback: (...args: unknown[]) => void, + delay?: number, + ...args: unknown[] ) => { if (delay === 123_456) { queuedHandoffCallback = () => callback(...args); @@ -9576,7 +9587,7 @@ it("fails preconfigured recovery without replacing a missing provider session", const backend: NativeSessionBackend = { descriptor: async () => ({ kind: "mock", name: "preconfigured", version: "1", runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" }, - capabilities: { resume: true, typedEvents: true, steering: false, interruption: true } }), + capabilities: { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true } }), openSession, openReplacementSession, recoverSession: async () => ({ recovered: false, reason: "provider history is missing" }), }; @@ -9586,7 +9597,7 @@ it("fails preconfigured recovery without replacing a missing provider session", }; await expect(executeNativeSession({ input: { ...preparedInput(), schema: "paperclip.native-execution-input.v7", homeMode: "preconfigured" }, - backend, controlPlane: port, runnerInstanceId: "runner-preconfigured", persistedSession: checkpoint, + backend, controlPlane: port, runnerInstanceId: "runner-preconfigured", controlPlaneInstanceId: "control-preconfigured", persistedSession: checkpoint, })).rejects.toThrow("native_session_recovery_failed: provider history is missing"); expect(openSession).not.toHaveBeenCalled(); expect(openReplacementSession).not.toHaveBeenCalled(); diff --git a/packages/paperclip-runner/src/protocol-actions/case-data.test.ts b/packages/paperclip-runner/src/protocol-actions/case-data.test.ts index ea569d9472d..d2459f92eac 100644 --- a/packages/paperclip-runner/src/protocol-actions/case-data.test.ts +++ b/packages/paperclip-runner/src/protocol-actions/case-data.test.ts @@ -1,4 +1,4 @@ -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { paperclipSemanticAction } from "../catalog/semantic-action-catalog.js"; import { paperclipSemanticActionNamespace } from "../semantic-tools/paperclip-discovery.js"; diff --git a/packages/paperclip-runner/src/protocol-actions/collaboration-contracts.test.ts b/packages/paperclip-runner/src/protocol-actions/collaboration-contracts.test.ts index 0458475389d..03758fe075b 100644 --- a/packages/paperclip-runner/src/protocol-actions/collaboration-contracts.test.ts +++ b/packages/paperclip-runner/src/protocol-actions/collaboration-contracts.test.ts @@ -1,4 +1,4 @@ -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { PAPERCLIP_COLLABORATION_PROTOCOL_ACTIONS } from "./collaboration.js"; diff --git a/packages/paperclip-runner/src/protocol-actions/core-contracts.test.ts b/packages/paperclip-runner/src/protocol-actions/core-contracts.test.ts index 8a76a6f2659..fe06c2f98d5 100644 --- a/packages/paperclip-runner/src/protocol-actions/core-contracts.test.ts +++ b/packages/paperclip-runner/src/protocol-actions/core-contracts.test.ts @@ -1,4 +1,4 @@ -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { PAPERCLIP_CORE_PROTOCOL_ACTIONS } from "./core.js"; diff --git a/packages/paperclip-runner/src/protocol/permission-request.test.ts b/packages/paperclip-runner/src/protocol/permission-request.test.ts index 2f5a18d2fa3..f0ac44e6fa4 100644 --- a/packages/paperclip-runner/src/protocol/permission-request.test.ts +++ b/packages/paperclip-runner/src/protocol/permission-request.test.ts @@ -1,4 +1,4 @@ -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { questionSetSchema, requestSchema } from "./generated/schema-bundle.js"; diff --git a/packages/paperclip-runner/src/protocol/provider-descriptor.test.ts b/packages/paperclip-runner/src/protocol/provider-descriptor.test.ts index 891fb68a5f5..d6d1f3d5b6e 100644 --- a/packages/paperclip-runner/src/protocol/provider-descriptor.test.ts +++ b/packages/paperclip-runner/src/protocol/provider-descriptor.test.ts @@ -1,4 +1,4 @@ -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { providerDescriptorSchema } from "./generated/schema-bundle.js"; @@ -19,7 +19,7 @@ describe("provider runtime descriptor", () => { } const event: PrpEvent = { schema: "paperclip.prp.event.v2", schemaVersion: 2, sourceEventId: "controls", sourceSeq: 1, - sourceInstanceId: "runner", sourceKind: "runner", runId: "run", eventType: "session.capabilities.updated", + sourceInstanceId: "runner", sourceKind: "runner", runId: "run", normalizedSessionId: "session", eventType: "session.capabilities.updated", priority: 0, emittedAt: "2026-09-28T12:00:00.000Z", payload: { sessionGoals: { availability: "unsupported", reason: "provider has no native goals", actions: [], autonomousUpdates: false, persistentAcrossResume: false, maxObjectiveChars: 4000, diff --git a/packages/paperclip-runner/src/protocol/prp-v1-expressiveness-crosswalk.test.ts b/packages/paperclip-runner/src/protocol/prp-v1-expressiveness-crosswalk.test.ts index 2909598d388..e190ea1fa3e 100644 --- a/packages/paperclip-runner/src/protocol/prp-v1-expressiveness-crosswalk.test.ts +++ b/packages/paperclip-runner/src/protocol/prp-v1-expressiveness-crosswalk.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; -import Ajv2020 from "ajv/dist/2020"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; type Classification = diff --git a/packages/paperclip-runner/src/protocol/replay-contract.test.ts b/packages/paperclip-runner/src/protocol/replay-contract.test.ts index bad4d2536ee..2b98ffe4acb 100644 --- a/packages/paperclip-runner/src/protocol/replay-contract.test.ts +++ b/packages/paperclip-runner/src/protocol/replay-contract.test.ts @@ -1,4 +1,4 @@ -import Ajv2020 from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { usageSchema } from "./generated/schema-bundle.js"; import { readFile } from "node:fs/promises"; @@ -77,7 +77,7 @@ describe("PRP v1 JSON Schema contract", () => { expect(result.fixture.futureFixtureHint).toEqual({ producerVersion: "1.1-preview", }); - expect(result.fixture.events[0]?.futureEnvelopeField).toBe(42); + expect((result.fixture.events[0] as unknown as Record).futureEnvelopeField).toBe(42); } }); @@ -269,7 +269,7 @@ describe("PRP v1 JSON Schema contract", () => { expect(result.ok).toBe(true); if (!result.ok) return; expect( - result.fixture.events[0]?.payload.semantic_tool?.futureEnvelopeHint, + (result.fixture.events[0]?.payload.semantic_tool as { futureEnvelopeHint?: unknown } | undefined)?.futureEnvelopeHint, ).toEqual({ version: "1.1-preview" }); const withoutEnvelope = structuredClone(result.fixture); diff --git a/packages/paperclip-runner/src/protocol/workspace-diff-events.test.ts b/packages/paperclip-runner/src/protocol/workspace-diff-events.test.ts index 9a25aec4bb3..a6fcdd91d81 100644 --- a/packages/paperclip-runner/src/protocol/workspace-diff-events.test.ts +++ b/packages/paperclip-runner/src/protocol/workspace-diff-events.test.ts @@ -10,6 +10,7 @@ function workspaceEvent(eventType: "workspace.change.updated" | "workspace.diff. sourceInstanceId: "runner-1", sourceKind: "runner", runId: "run-1", + normalizedSessionId: "session-1", turnId: "turn-1", eventType, schemaVersion: 1, @@ -44,7 +45,7 @@ describe("PRP workspace change events", () => { it("rejects traversal paths and incomplete recorded diffs", () => { const traversal = workspaceEvent("workspace.diff.recorded"); - traversal.payload.files[0]!.path = "../secret"; + (traversal.payload.files as Array<{ path: string }>)[0]!.path = "../secret"; expect(validatePrpEvent(traversal).ok).toBe(false); const incomplete = workspaceEvent("workspace.diff.recorded"); incomplete.payload.complete = false; @@ -59,6 +60,7 @@ describe("PRP workspace change events", () => { sourceInstanceId: "runner-1", sourceKind: "runner", runId: "run-1", + normalizedSessionId: "session-1", turnId: "turn-1", eventType: "workspace.file.referenced", schemaVersion: 1, diff --git a/packages/paperclip-runner/src/react/public-contract.test.tsx b/packages/paperclip-runner/src/react/public-contract.test.tsx index cae0aa98f6a..3f60ddadd1c 100644 --- a/packages/paperclip-runner/src/react/public-contract.test.tsx +++ b/packages/paperclip-runner/src/react/public-contract.test.tsx @@ -3,7 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import type { SessionItemSnapshot } from "../reducer/session-reducer.js"; -import type { TranscriptEntry } from "../browser/transcript-model.js"; +import type { TranscriptEntry, TranscriptItemEntry } from "../browser/transcript-model.js"; import { Composer } from "./components/composer.js"; import { Conversation } from "./components/conversation.js"; import { parseStructuredResponse } from "./components/message.js"; @@ -17,7 +17,7 @@ const item: SessionItemSnapshot = { text: "Canonical answer", }; -const entries: TranscriptEntry[] = [ +const entries: TranscriptItemEntry[] = [ { kind: "item", key: "item:assistant-1", diff --git a/packages/paperclip-runner/src/tools/capability-semantic-tools.test.ts b/packages/paperclip-runner/src/tools/capability-semantic-tools.test.ts index 5a4685a670f..9d4b010b046 100644 --- a/packages/paperclip-runner/src/tools/capability-semantic-tools.test.ts +++ b/packages/paperclip-runner/src/tools/capability-semantic-tools.test.ts @@ -93,6 +93,11 @@ async function runtimeFor(options: { }; } +function pendingLeaseExpiry(snapshot: CapabilitySemanticToolRuntimeSnapshot | null): number | undefined { + const extension = snapshot?.extensions[0]; + return extension?.status === "pending" ? extension.leaseExpiresAtMs : undefined; +} + describe("Capability semantic tool catalog", () => { it("defines a unique versioned provider-neutral catalog and every optional group", () => { const operationIds = CAPABILITY_SEMANTIC_TOOL_CATALOG.map((tool) => tool.operationId); @@ -441,7 +446,7 @@ describe("Capability exposure and authorization", () => { }); it("retries the completion merge after a concurrent lease heartbeat", async () => { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; let injectedHeartbeats = 0; let completionAttempts = 0; const durableStore: CapabilitySemanticToolRuntimeStore = { @@ -494,7 +499,7 @@ describe("Capability exposure and authorization", () => { it("bounds completion contention and retries the fulfilled execution without rerunning it", async () => { vi.useFakeTimers({ now: 0 }); try { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; let rejectCompletion = true; let completionAttempts = 0; const durableStore: CapabilitySemanticToolRuntimeStore = { @@ -574,7 +579,7 @@ describe("Capability exposure and authorization", () => { vi.useFakeTimers({ now: 0 }); try { let now = 0; - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; let rejectCompletion = true; const durableStore: CapabilitySemanticToolRuntimeStore = { load: () => durableSnapshot === null ? null : structuredClone(durableSnapshot), @@ -704,7 +709,7 @@ describe("Capability exposure and authorization", () => { it("retains a failed lease until cleanup contention clears", async () => { vi.useFakeTimers({ now: 0 }); try { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; let failExecutionAllocation = true; let cleanupAttempts = 0; const durableStore: CapabilitySemanticToolRuntimeStore = { @@ -764,7 +769,7 @@ describe("Capability exposure and authorization", () => { }); it("allocates read result ids from the latest shared durable sequence", async () => { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; const durableStore: CapabilitySemanticToolRuntimeStore = { load: () => durableSnapshot === null ? null : structuredClone(durableSnapshot), save: (_runId, snapshot) => { @@ -800,7 +805,7 @@ describe("Capability exposure and authorization", () => { }); it("retries extension acquisition after an unrelated snapshot update", async () => { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; let injectedContention = false; const durableStore: CapabilitySemanticToolRuntimeStore = { load: () => durableSnapshot === null ? null : structuredClone(durableSnapshot), @@ -903,7 +908,7 @@ describe("Capability exposure and authorization", () => { }); it("does not revive a serialized pending extension when the durable store is empty", async () => { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; const durableStore: CapabilitySemanticToolRuntimeStore = { load: () => durableSnapshot === null ? null : structuredClone(durableSnapshot), save: (_runId, snapshot) => { @@ -928,7 +933,7 @@ describe("Capability exposure and authorization", () => { const inFlight = first.runtime.invoke(invocation); const serializedWhilePending = first.adapter.serialize(); await expect(inFlight).resolves.toMatchObject({ ok: true }); - durableSnapshot = null; + durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; const restoredAdapter = CapabilityMockControlPlaneAdapter.restore( serializedWhilePending, @@ -952,7 +957,7 @@ describe("Capability exposure and authorization", () => { }); it("does not revive serialized completed extensions when the durable store is empty", async () => { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; const durableStore: CapabilitySemanticToolRuntimeStore = { load: () => durableSnapshot === null ? null : structuredClone(durableSnapshot), save: (_runId, snapshot) => { @@ -974,7 +979,7 @@ describe("Capability exposure and authorization", () => { idempotencyKey: "stale-completed", })).resolves.toMatchObject({ ok: true }); const serializedWithCompleted = first.adapter.serialize(); - durableSnapshot = null; + durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; const restoredAdapter = CapabilityMockControlPlaneAdapter.restore( serializedWithCompleted, @@ -1004,7 +1009,7 @@ describe("Capability exposure and authorization", () => { it("renews a live extension lease before another runtime can reclaim it", async () => { vi.useFakeTimers({ now: 0 }); try { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; const durableStore: CapabilitySemanticToolRuntimeStore = { load: () => durableSnapshot === null ? null : structuredClone(durableSnapshot), @@ -1066,7 +1071,7 @@ describe("Capability exposure and authorization", () => { it("retries a lease heartbeat after a transient durable-store failure", async () => { vi.useFakeTimers({ now: 0 }); try { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; let failNextLoad = false; const durableStore: CapabilitySemanticToolRuntimeStore = { load: () => { @@ -1099,9 +1104,9 @@ describe("Capability exposure and authorization", () => { failNextLoad = true; expect(() => vi.advanceTimersByTime(10_000)).not.toThrow(); - expect(durableSnapshot?.extensions[0]?.leaseExpiresAtMs).toBe(30_000); + expect(pendingLeaseExpiry(durableSnapshot)).toBe(30_000); vi.advanceTimersByTime(10_000); - expect(durableSnapshot?.extensions[0]?.leaseExpiresAtMs).toBe(50_000); + expect(pendingLeaseExpiry(durableSnapshot)).toBe(50_000); await expect(inFlight).resolves.toMatchObject({ ok: true }); } finally { vi.useRealTimers(); @@ -1111,7 +1116,7 @@ describe("Capability exposure and authorization", () => { it("recovers a prepared extension after repeated heartbeat failures", async () => { vi.useFakeTimers({ now: 0 }); try { - let durableSnapshot: CapabilitySemanticToolRuntimeSnapshot | null = null; + let durableSnapshot = null as CapabilitySemanticToolRuntimeSnapshot | null; let failHeartbeatLoads = false; const durableStore: CapabilitySemanticToolRuntimeStore = { load: () => { diff --git a/packages/paperclip-runner/test/support/runnerd-transport.ts b/packages/paperclip-runner/test/support/runnerd-transport.ts index 171414f2415..4fb1359f567 100644 --- a/packages/paperclip-runner/test/support/runnerd-transport.ts +++ b/packages/paperclip-runner/test/support/runnerd-transport.ts @@ -8,6 +8,7 @@ import { nativeRuntimePromptDigest, type NativeRuntimeContextSnapshot, } from "../../src/contracts/runtime-context.js"; +import type { DurablePrpControlPlane, DurablePrpControlPlaneStore } from "../../src/control-plane/durable-prp-control-plane.js"; import { defaultCapabilityRunnerdBinary as qualifiedCapabilityRunnerdBinary } from "../../src/live/runnerd-codex-transport.js"; export const defaultCapabilityRunnerdBinary = () => @@ -86,3 +87,11 @@ export function assignedRuntimeContext( aggregateDigest: canonicalNativeRuntimeContextDigest(value), }; } + +export type CommittableStore = DurablePrpControlPlaneStore & { + commit(candidate: DurablePrpControlPlaneStore["state"], checkpointRequired?: boolean): void; +}; + +export function committableStore(authority: DurablePrpControlPlane): CommittableStore { + return authority.store as CommittableStore; +} diff --git a/packages/paperclip-runner/tsconfig.browser.json b/packages/paperclip-runner/tsconfig.browser.json index 624c92fc56c..f97c6806310 100644 --- a/packages/paperclip-runner/tsconfig.browser.json +++ b/packages/paperclip-runner/tsconfig.browser.json @@ -14,22 +14,21 @@ "vite/client", "node" ], - "baseUrl": ".", "paths": { "@paperclipai/paperclip-runner/browser": [ - "src/browser/index.ts" + "./src/browser/index.ts" ], "@paperclipai/paperclip-runner/react": [ - "src/react/index.ts" + "./src/react/index.ts" ], "@paperclipai/paperclip-runner/standalone": [ - "src/standalone/index.ts" + "./src/standalone/index.ts" ], "@paperclipai/paperclip-runner/styles.css": [ - "styles.css" + "./styles.css" ], "@paperclip-runner-local/capability": [ - "src/scenarios/index.ts" + "./src/scenarios/index.ts" ] } }, diff --git a/packages/paperclip-runner/tsconfig.browser.test.json b/packages/paperclip-runner/tsconfig.browser.test.json new file mode 100644 index 00000000000..4d74f669f27 --- /dev/null +++ b/packages/paperclip-runner/tsconfig.browser.test.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.browser.json", + "compilerOptions": { + "noEmit": true + }, + "include": [ + "devtools/browser/**/*.ts", + "devtools/browser/**/*.tsx", + "devtools/issue-thread/**/*.ts", + "devtools/issue-thread/**/*.tsx", + "examples/**/*.ts", + "examples/**/*.tsx" + ], + "exclude": ["**/dist/**", "**/vite.config.ts"] +} diff --git a/packages/paperclip-runner/tsconfig.surfaces.test.json b/packages/paperclip-runner/tsconfig.surfaces.test.json new file mode 100644 index 00000000000..b6b27ea0e3a --- /dev/null +++ b/packages/paperclip-runner/tsconfig.surfaces.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.surfaces.json", + "compilerOptions": { + "allowJs": true, + "noEmit": true, + "rootDir": "." + }, + "exclude": [] +} diff --git a/packages/paperclip-runner/tsconfig.test.json b/packages/paperclip-runner/tsconfig.test.json new file mode 100644 index 00000000000..1b05e128951 --- /dev/null +++ b/packages/paperclip-runner/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": true, + "noEmit": true, + "rootDir": ".." + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": [ + "src/browser/**", + "src/devtools/**", + "src/issue-thread/**", + "src/react/**", + "src/scenarios/**", + "src/standalone/**" + ] +} diff --git a/packages/plugins/examples/plugin-orchestration-smoke-example/tests/plugin.spec.ts b/packages/plugins/examples/plugin-orchestration-smoke-example/tests/plugin.spec.ts index dd879eeef07..a583fb4080a 100644 --- a/packages/plugins/examples/plugin-orchestration-smoke-example/tests/plugin.spec.ts +++ b/packages/plugins/examples/plugin-orchestration-smoke-example/tests/plugin.spec.ts @@ -20,8 +20,11 @@ function issue(input: Partial & Pick title, description: null, status: "todo", + workMode: "standard", priority: "medium", + reviewPolicy: null, assigneeAgentId: null, + responsibleUserId: null, assigneeUserId: null, checkoutRunId: null, executionRunId: null, diff --git a/packages/plugins/examples/plugin-orchestration-smoke-example/tsconfig.test.json b/packages/plugins/examples/plugin-orchestration-smoke-example/tsconfig.test.json new file mode 100644 index 00000000000..f20d34cbaee --- /dev/null +++ b/packages/plugins/examples/plugin-orchestration-smoke-example/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "paths": { + "@paperclipai/shared": ["../../../shared/dist/index.d.ts"] + }, + "types": ["node"], + "typeRoots": ["../../../../server/node_modules/@types"] + }, + "include": ["tests"] +} diff --git a/packages/plugins/sandbox-providers/boxd/tsconfig.test.json b/packages/plugins/sandbox-providers/boxd/tsconfig.test.json new file mode 100644 index 00000000000..90040f25e53 --- /dev/null +++ b/packages/plugins/sandbox-providers/boxd/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/packages/plugins/sandbox-providers/cloudflare/bridge-template/tsconfig.test.json b/packages/plugins/sandbox-providers/cloudflare/bridge-template/tsconfig.test.json new file mode 100644 index 00000000000..a8d4317b49c --- /dev/null +++ b/packages/plugins/sandbox-providers/cloudflare/bridge-template/tsconfig.test.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": [] +} diff --git a/packages/plugins/sandbox-providers/cloudflare/tsconfig.test.json b/packages/plugins/sandbox-providers/cloudflare/tsconfig.test.json new file mode 100644 index 00000000000..90040f25e53 --- /dev/null +++ b/packages/plugins/sandbox-providers/cloudflare/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/packages/plugins/sandbox-providers/createos/src/file-sync.test.ts b/packages/plugins/sandbox-providers/createos/src/file-sync.test.ts index 4ac98c98907..259734a1f80 100644 --- a/packages/plugins/sandbox-providers/createos/src/file-sync.test.ts +++ b/packages/plugins/sandbox-providers/createos/src/file-sync.test.ts @@ -2,7 +2,7 @@ import path from "node:path"; import os from "node:os"; import { promises as fs } from "node:fs"; import * as tar from "tar"; -import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, expect, it, vi, type Mock } from "vitest"; import { CreateosClient } from "./client.js"; import { parseConfig } from "./config.js"; import { assertRemotePath, syncFiles, validateArchive } from "./file-sync.js"; @@ -16,7 +16,7 @@ const params = { driverKey: "createos", companyId: "c", environmentId: "e", conf let temp: string; let downloads: Buffer; let uploads: Buffer[]; -let fetchMock: ReturnType; +let fetchMock: Mock<(url: string, init?: RequestInit) => Promise>; beforeEach(async () => { temp = await fs.mkdtemp(path.join(os.tmpdir(), "createos-sync-test-")); diff --git a/packages/plugins/sandbox-providers/createos/tsconfig.test.json b/packages/plugins/sandbox-providers/createos/tsconfig.test.json new file mode 100644 index 00000000000..90040f25e53 --- /dev/null +++ b/packages/plugins/sandbox-providers/createos/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/packages/plugins/sandbox-providers/daytona/tsconfig.test.json b/packages/plugins/sandbox-providers/daytona/tsconfig.test.json new file mode 100644 index 00000000000..90040f25e53 --- /dev/null +++ b/packages/plugins/sandbox-providers/daytona/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/packages/plugins/sandbox-providers/e2b/src/plugin.test.ts b/packages/plugins/sandbox-providers/e2b/src/plugin.test.ts index 851807e98b1..e74c4b92beb 100644 --- a/packages/plugins/sandbox-providers/e2b/src/plugin.test.ts +++ b/packages/plugins/sandbox-providers/e2b/src/plugin.test.ts @@ -308,7 +308,7 @@ describe("E2B sandbox provider plugin", () => { expect(mockConnect).toHaveBeenCalledWith("sandbox-123", expect.objectContaining({ apiKey: "resolved-key" })); expect(sandbox.files.write).toHaveBeenCalledWith(expect.stringMatching(/^\/tmp\/paperclip-stdin-/), "input"); - const stdinCall = sandbox.commands.run.mock.calls.find(([cmd]: [string]) => cmd.includes("'printf'")); + const stdinCall = sandbox.commands.run.mock.calls.find(([cmd]) => cmd.includes("'printf'")); expect(stdinCall).toBeDefined(); if (!stdinCall) throw new Error("stdinCall not found"); expect(stdinCall[0]).toMatch(/\.profile/); @@ -365,7 +365,7 @@ describe("E2B sandbox provider plugin", () => { timeoutMs: 1000, }); - const fgCall = sandbox.commands.run.mock.calls.find(([cmd]: [string]) => cmd.includes("'printf'")); + const fgCall = sandbox.commands.run.mock.calls.find(([cmd]) => cmd.includes("'printf'")); expect(fgCall).toBeDefined(); if (!fgCall) throw new Error("fgCall not found"); expect(fgCall[0]).toMatch(/\.profile/); diff --git a/packages/plugins/sandbox-providers/e2b/tsconfig.test.json b/packages/plugins/sandbox-providers/e2b/tsconfig.test.json new file mode 100644 index 00000000000..90040f25e53 --- /dev/null +++ b/packages/plugins/sandbox-providers/e2b/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/packages/plugins/sandbox-providers/exe-dev/tsconfig.test.json b/packages/plugins/sandbox-providers/exe-dev/tsconfig.test.json new file mode 100644 index 00000000000..90040f25e53 --- /dev/null +++ b/packages/plugins/sandbox-providers/exe-dev/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/packages/plugins/sandbox-providers/kubernetes/package.json b/packages/plugins/sandbox-providers/kubernetes/package.json index 057f6685b8b..efb51c2a4f9 100644 --- a/packages/plugins/sandbox-providers/kubernetes/package.json +++ b/packages/plugins/sandbox-providers/kubernetes/package.json @@ -49,12 +49,12 @@ }, "dependencies": { "@kubernetes/client-node": "^1.0.0", - "zod": "^3.24.2" + "zod": "^4.6.6" }, "devDependencies": { "@types/node": "^24.0.0", "typescript": "^5.7.3", - "vitest": "^3.2.4" + "vitest": "^4.1.8" }, "engines": { "node": ">=24.11.0" diff --git a/packages/plugins/sandbox-providers/kubernetes/src/adapter-registry.ts b/packages/plugins/sandbox-providers/kubernetes/src/adapter-registry.ts index ce732b57db5..0e71c6035ce 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/adapter-registry.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/adapter-registry.ts @@ -19,7 +19,7 @@ export const adapterRegistryEntrySchema = z envKeys: z.array(z.string()).optional(), allowFqdns: z.array(z.string()).optional(), probeCommand: z.array(z.string()).optional(), - defaultEnv: z.record(z.string()).optional(), + defaultEnv: z.record(z.string(), z.string()).optional(), }) .strict(); diff --git a/packages/plugins/sandbox-providers/kubernetes/src/types.ts b/packages/plugins/sandbox-providers/kubernetes/src/types.ts index 5626daa9384..e39ccac78ed 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/types.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/types.ts @@ -28,7 +28,7 @@ export const kubernetesProviderConfigSchema = z .optional(), runtimeClassName: z.string().optional(), - serviceAccountAnnotations: z.record(z.string()).default({}), + serviceAccountAnnotations: z.record(z.string(), z.string()).default({}), jobTtlSecondsAfterFinished: z.number().int().nonnegative().default(900), podActivityDeadlineSec: z.number().int().positive().default(3600), diff --git a/packages/plugins/sandbox-providers/kubernetes/test/integration/end-to-end-run.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/integration/end-to-end-run.test.ts index 8fb64270bcf..44f5772894b 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/integration/end-to-end-run.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/integration/end-to-end-run.test.ts @@ -65,6 +65,7 @@ describe("plugin-kubernetes end-to-end", () => { }); expect(lease.providerLeaseId).toMatch(/^pc-/); + const providerLeaseId = lease.providerLeaseId!; // Verify the Job exists in the tenant namespace const jobs = kubectl(`get jobs -n ${NAMESPACE} -o name`); @@ -137,6 +138,7 @@ describe("plugin-kubernetes end-to-end", () => { }); expect(lease.providerLeaseId).toMatch(/^pc-/); + const providerLeaseId = lease.providerLeaseId!; // Verify the Sandbox CR exists in the tenant namespace const sandboxes = kubectl( @@ -156,7 +158,7 @@ describe("plugin-kubernetes end-to-end", () => { await sandboxCrOrchestrator.waitForCompletion( clients, NAMESPACE, - lease.providerLeaseId, + providerLeaseId, { timeoutMs: 90_000, pollMs: 3000 }, ); @@ -164,7 +166,7 @@ describe("plugin-kubernetes end-to-end", () => { const podName = await sandboxCrOrchestrator.findPod( clients, NAMESPACE, - lease.providerLeaseId, + providerLeaseId, ); expect(podName).toBeTruthy(); diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/adapter-defaults.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/adapter-defaults.test.ts index 513c2506809..f7f1b78037e 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/adapter-defaults.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/adapter-defaults.test.ts @@ -75,6 +75,7 @@ describe("getAdapterDefaults", () => { const registry: AdapterRegistryEntry[] = [ { adapterType: "opencode_local", + enabled: true, runtimeImage: "x", envKeys: [], allowFqdns: [], @@ -88,7 +89,7 @@ describe("getAdapterDefaults", () => { it("throws when a supplied registry entry is missing runtimeImage", () => { const registry: AdapterRegistryEntry[] = [ - { adapterType: "opencode_local", envKeys: [], allowFqdns: [], probeCommand: ["x"] }, + { adapterType: "opencode_local", enabled: true, envKeys: [], allowFqdns: [], probeCommand: ["x"] }, ]; expect(() => getAdapterDefaults("opencode_local", registry)).toThrow( /missing required runtime field: runtimeImage/, @@ -97,7 +98,7 @@ describe("getAdapterDefaults", () => { it("defaults the optional array fields to [] when the registry omits them", () => { const registry: AdapterRegistryEntry[] = [ - { adapterType: "opencode_local", runtimeImage: "img" }, + { adapterType: "opencode_local", enabled: true, runtimeImage: "img" }, ]; const d = getAdapterDefaults("opencode_local", registry); expect(d.envKeys).toEqual([]); diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/cilium-network-policy.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/cilium-network-policy.test.ts index 419f4b1b33c..4cb6faa8693 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/cilium-network-policy.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/cilium-network-policy.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { buildCiliumNetworkPolicyManifest } from "../../src/cilium-network-policy.js"; +import { buildCiliumNetworkPolicyManifest as buildRawCiliumNetworkPolicyManifest, type BuildCiliumNetworkPolicyInput } from "../../src/cilium-network-policy.js"; +import type { CiliumNetworkPolicyView } from "./manifest-views.js"; + +const buildCiliumNetworkPolicyManifest = (input: BuildCiliumNetworkPolicyInput) => + buildRawCiliumNetworkPolicyManifest(input) as unknown as CiliumNetworkPolicyView; describe("buildCiliumNetworkPolicyManifest", () => { const baseInput = { @@ -27,7 +31,7 @@ describe("buildCiliumNetworkPolicyManifest", () => { }); const fqdnRule = cnp.spec.egress.find((e: { toFQDNs?: { matchName: string }[] }) => e.toFQDNs); expect(fqdnRule).toBeDefined(); - expect(fqdnRule.toFQDNs.map((f: { matchName: string }) => f.matchName).sort()).toEqual([ + expect(fqdnRule!.toFQDNs!.map((f: { matchName: string }) => f.matchName).sort()).toEqual([ "api.anthropic.com", "api.openai.com", ]); @@ -55,7 +59,7 @@ describe("buildCiliumNetworkPolicyManifest", () => { egressAllowCidrs: ["10.0.0.0/8"], }); const cidrRule = cnp.spec.egress.find((e: { toCIDRSet?: { cidr: string }[] }) => e.toCIDRSet); - expect(cidrRule.toCIDRSet[0].cidr).toBe("10.0.0.0/8"); + expect(cidrRule?.toCIDRSet?.[0]?.cidr).toBe("10.0.0.0/8"); }); it("targets only the granted run when building a scoped policy", () => { diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/manifest-views.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/manifest-views.ts new file mode 100644 index 00000000000..a0314c5096e --- /dev/null +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/manifest-views.ts @@ -0,0 +1,98 @@ +// The manifest builders return Record because the manifests go +// straight to the Kubernetes API. These views name only the fields the tests read. + +export interface PodTemplateView { + metadata: { labels: Record }; + spec: { + automountServiceAccountToken: boolean; + restartPolicy: string; + runtimeClassName?: string; + imagePullSecrets?: { name: string }[]; + securityContext: { + runAsNonRoot: boolean; + runAsUser: number; + fsGroupChangePolicy: string; + seccompProfile: { type: string }; + }; + containers: { + command: string[]; + envFrom: { secretRef: { name: string } }[]; + securityContext: { + runAsNonRoot: boolean; + readOnlyRootFilesystem: boolean; + allowPrivilegeEscalation: boolean; + capabilities: { drop: string[] }; + }; + volumeMounts: { mountPath: string }[]; + }[]; + volumes: { emptyDir?: unknown }[]; + }; +} + +export interface JobManifestView { + apiVersion: string; + kind: string; + metadata: { labels: Record }; + spec: { + backoffLimit: number; + ttlSecondsAfterFinished: number; + activeDeadlineSeconds: number; + template: PodTemplateView; + }; +} + +export interface SandboxCrManifestView { + apiVersion: string; + kind: string; + metadata: { + name: string; + namespace: string; + labels: Record; + ownerReferences?: unknown[]; + }; + spec: { podTemplate: PodTemplateView }; +} + +export interface NetworkPolicyEgressRuleView { + to: { + namespaceSelector?: { matchLabels?: Record }; + podSelector?: { matchLabels?: Record }; + ipBlock?: { cidr: string; except?: string[] }; + }[]; + ports?: { protocol: string; port: number }[]; +} + +export interface DenyAllPolicyView { + metadata: { name: string }; + spec: { + podSelector: Record; + policyTypes: string[]; + ingress?: unknown[]; + egress?: unknown[]; + }; +} + +export interface EgressAllowPolicyView { + metadata: { name: string; ownerReferences?: unknown[] }; + spec: { + podSelector: { matchLabels: Record }; + egress: NetworkPolicyEgressRuleView[]; + }; +} + +export interface CiliumEgressRuleView { + toEndpoints?: { matchLabels: Record }[]; + toPorts?: { ports: { port: string }[] }[]; + toFQDNs?: { matchName: string }[]; + toCIDRSet?: { cidr: string }[]; +} + +export interface CiliumNetworkPolicyView { + apiVersion: string; + kind: string; + metadata: { name: string; ownerReferences?: unknown[] }; + spec: { + endpointSelector: { matchLabels: Record }; + egress: CiliumEgressRuleView[]; + }; +} diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/network-policy.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/network-policy.test.ts index 80338e70129..5562d8ed6e4 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/network-policy.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/network-policy.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { buildNetworkPolicyManifests } from "../../src/network-policy.js"; +import { buildNetworkPolicyManifests as buildRawNetworkPolicyManifests, type BuildNetworkPolicyInput } from "../../src/network-policy.js"; +import type { DenyAllPolicyView, EgressAllowPolicyView } from "./manifest-views.js"; + +const buildNetworkPolicyManifests = (input: BuildNetworkPolicyInput) => + buildRawNetworkPolicyManifests(input) as unknown as [DenyAllPolicyView, EgressAllowPolicyView]; describe("buildNetworkPolicyManifests", () => { const baseInput = { @@ -50,7 +54,7 @@ describe("buildNetworkPolicyManifests", () => { r.to.some((t) => t.podSelector?.matchLabels?.app === "paperclip-server"), ); expect(callbackRule).toBeDefined(); - expect(callbackRule.ports[0].port).toBe(3100); + expect(callbackRule!.ports![0].port).toBe(3100); }); it("adds public-IPv4 fallback (with private/link-local excluded) when FQDNs are configured and no CIDRs are supplied", () => { @@ -62,10 +66,10 @@ describe("buildNetworkPolicyManifests", () => { r.to.some((t) => t.ipBlock?.cidr === "0.0.0.0/0"), ); expect(fallback).toBeDefined(); - expect(fallback.to[0].ipBlock.except).toEqual( + expect(fallback!.to[0].ipBlock!.except).toEqual( expect.arrayContaining(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"]), ); - expect(fallback.ports).toEqual( + expect(fallback!.ports).toEqual( expect.arrayContaining([ { protocol: "TCP", port: 443 }, { protocol: "TCP", port: 80 }, @@ -107,6 +111,6 @@ describe("buildNetworkPolicyManifests", () => { expect(egress.metadata.ownerReferences).toHaveLength(1); expect(egress.spec.podSelector.matchLabels).toEqual({ "paperclip.io/run-id": "run-123" }); expect(egress.spec.egress).toHaveLength(1); - expect(egress.spec.egress[0].to[0].ipBlock.cidr).toBe("0.0.0.0/0"); + expect(egress.spec.egress[0].to[0].ipBlock?.cidr).toBe("0.0.0.0/0"); }); }); diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-spec-builder.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-spec-builder.test.ts index 75d1da3cc92..593dca7df74 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-spec-builder.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-spec-builder.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { buildJobManifest } from "../../src/pod-spec-builder.js"; +import { buildJobManifest as buildRawJobManifest, type BuildJobManifestInput } from "../../src/pod-spec-builder.js"; +import type { JobManifestView } from "./manifest-views.js"; + +const buildJobManifest = (input: BuildJobManifestInput) => + buildRawJobManifest(input) as unknown as JobManifestView; const baseInput = { namespace: "paperclip-acme", diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/sandbox-cr-builder.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/sandbox-cr-builder.test.ts index 97beb8e9a53..01e68d1f59e 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/sandbox-cr-builder.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/sandbox-cr-builder.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { buildSandboxCrManifest } from "../../src/sandbox-cr-builder.js"; +import { buildSandboxCrManifest as buildRawSandboxCrManifest, type BuildSandboxCrManifestInput } from "../../src/sandbox-cr-builder.js"; +import type { SandboxCrManifestView } from "./manifest-views.js"; + +const buildSandboxCrManifest = (input: BuildSandboxCrManifestInput) => + buildRawSandboxCrManifest(input) as unknown as SandboxCrManifestView; const baseInput = { namespace: "paperclip-acme", diff --git a/packages/plugins/sandbox-providers/kubernetes/tsconfig.test.json b/packages/plugins/sandbox-providers/kubernetes/tsconfig.test.json new file mode 100644 index 00000000000..8305c630843 --- /dev/null +++ b/packages/plugins/sandbox-providers/kubernetes/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src", "test"], + "exclude": [] +} diff --git a/packages/plugins/sandbox-providers/modal/tsconfig.test.json b/packages/plugins/sandbox-providers/modal/tsconfig.test.json new file mode 100644 index 00000000000..90040f25e53 --- /dev/null +++ b/packages/plugins/sandbox-providers/modal/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/packages/plugins/sandbox-providers/novita/src/plugin.test.ts b/packages/plugins/sandbox-providers/novita/src/plugin.test.ts index 29a3fe22514..6f6b9b7898f 100644 --- a/packages/plugins/sandbox-providers/novita/src/plugin.test.ts +++ b/packages/plugins/sandbox-providers/novita/src/plugin.test.ts @@ -120,10 +120,10 @@ describe("Novita sandbox provider plugin", () => { mockSandboxConnect.mockRejectedValueOnce(new Error("sandbox not found")); const result = await plugin.definition.onEnvironmentExecute?.({ + driverKey: "novita", companyId: "company-1", environmentId: "env-1", issueId: "issue-1", - runId: "run-1", config: { apiKey: "sk-test" }, lease: { providerLeaseId: "sb-expired", metadata: {} }, command: "true", diff --git a/packages/plugins/sandbox-providers/novita/tsconfig.test.json b/packages/plugins/sandbox-providers/novita/tsconfig.test.json new file mode 100644 index 00000000000..90040f25e53 --- /dev/null +++ b/packages/plugins/sandbox-providers/novita/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src"], + "exclude": [] +} diff --git a/packages/plugins/sdk/tests/testing-actions.test.ts b/packages/plugins/sdk/tests/testing-actions.test.ts index ff5eeaf9cef..203a9851d9d 100644 --- a/packages/plugins/sdk/tests/testing-actions.test.ts +++ b/packages/plugins/sdk/tests/testing-actions.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { createTestHarness } from "../src/testing.js"; -import type { PaperclipPluginManifestV1 } from "../src/types.js"; +import type { PaperclipPluginManifestV1, PluginIssuesClient } from "../src/types.js"; const manifest = { id: "paperclip.test-actions", @@ -12,7 +12,7 @@ const manifest = { author: "Paperclip", categories: ["automation"], capabilities: [], - entrypoints: {}, + entrypoints: { worker: "./worker.js" }, } satisfies PaperclipPluginManifestV1; describe("createTestHarness action context", () => { @@ -136,7 +136,8 @@ describe("createTestHarness issue interactions", () => { minSelected: 1, maxSelected: 2, }, - }, + // Omits the fields the schema defaults, so the assertions below cover those defaults. + } as Parameters[1], "company-1", { authorAgentId: "agent-1" }, ); diff --git a/packages/plugins/sdk/tests/testing-lifecycle.test.ts b/packages/plugins/sdk/tests/testing-lifecycle.test.ts index dc8963f6bfa..53b794b33e5 100644 --- a/packages/plugins/sdk/tests/testing-lifecycle.test.ts +++ b/packages/plugins/sdk/tests/testing-lifecycle.test.ts @@ -4,7 +4,7 @@ import type { PaperclipPluginManifestV1, ResourceLifecycleEvent } from "../src/t const manifest: PaperclipPluginManifestV1 = { id: "fixture.lifecycle", apiVersion: 1, version: "1.0.0", displayName: "Lifecycle", description: "Fixture", - author: "Tests", categories: ["automation"], capabilities: ["events.subscribe"], entrypoints: {}, + author: "Tests", categories: ["automation"], capabilities: ["events.subscribe"], entrypoints: { worker: "./worker.js" }, }; describe("Lifecycle inbox test harness", () => { diff --git a/packages/plugins/sdk/tests/worker-rpc-host.test.ts b/packages/plugins/sdk/tests/worker-rpc-host.test.ts index ae409e00ef6..9aafcf5a26b 100644 --- a/packages/plugins/sdk/tests/worker-rpc-host.test.ts +++ b/packages/plugins/sdk/tests/worker-rpc-host.test.ts @@ -874,8 +874,8 @@ describe("worker setup-token pseudo-terminal dispatch", () => { let nextRequestId = 1; // The fake session the opener returns. The test drives its output and exit. - let emitOutput: ((chunk: string) => void) | null = null; - let resolveWait: ((value: { exitCode: number | null }) => void) | null = null; + let emitOutput = null as ((chunk: string) => void) | null; + let resolveWait = null as ((value: { exitCode: number | null }) => void) | null; const inputs: string[] = []; let killed = 0; let closed = 0; @@ -1029,16 +1029,16 @@ describe("worker duplex channel dispatch", () => { let stopped = 0; let closed = 0; - // The plugin declares the four duplex channel handlers. The open returns a - // worker session id. The close keys on the host route id and returns a bound - // acknowledgement. + // The plugin declares the four duplex channel handlers. The open echoes the + // host route id and returns a worker session id. The close keys on the host + // route id and returns a bound acknowledgement. const controllablePlugin = definePlugin({ async setup() {}, async onDuplexChannelOpen(params) { expect(params.hostRouteId).toBe("route-1"); expect(params.command).toEqual(["paperclip-bridge"]); expect(params.providerLeaseId).toBe("lease-1"); - return { workerSessionId: "ws-1" }; + return { hostRouteId: params.hostRouteId, workerSessionId: "ws-1" }; }, async onDuplexChannelWrite(params) { writes.push(params.data); @@ -1117,7 +1117,7 @@ describe("worker duplex channel dispatch", () => { providerLeaseId: "lease-1", command: ["paperclip-bridge"], }), - ).resolves.toEqual({ workerSessionId: "ws-1" }); + ).resolves.toEqual({ hostRouteId: "route-1", workerSessionId: "ws-1" }); await callWorker("duplexChannelWrite", { workerSessionId: "ws-1", data: "payload" }); await callWorker("duplexChannelStop", { workerSessionId: "ws-1" }); @@ -1212,7 +1212,7 @@ describe("AI connection router RPC", () => { const reader = createInterface({ input: output }); const request = { companyId: "company", taskKey: "task", candidates: [], memberOrder: [] }; let received: unknown; - const plugin = definePlugin({ async setup() {}, onRouteAiConnection(params) { received = params; return { kind: "selected", memberId: "authorized-member" }; } }); + const plugin = definePlugin({ async setup() {}, async onRouteAiConnection(params) { received = params; return { kind: "selected", memberId: "authorized-member" }; } }); const worker = startWorkerRpcHost({ plugin, stdin: input, stdout: output }); let sequence = 0; const pending = new Map void>(); @@ -1235,9 +1235,10 @@ describe("Durable lifecycle inbox RPC", () => { const event = { id: "7", companyId: "company-a", resourceType: "agent", resourceId: "agent-a", action: "pause", createdAt: new Date(0).toISOString() }; const plugin = definePlugin({ async setup(ctx) { ctx.data.register("drain", async ({ companyId }) => { - const events = await ctx.events.listLifecycle(companyId!, 10, "6"); - await ctx.events.acknowledgeLifecycle(companyId!, events[0].id); - await ctx.agents.getRuntime("agent-a", companyId!, "environment-a", true); + if (typeof companyId !== "string") throw new Error("companyId is required"); + const events = await ctx.events.listLifecycle(companyId, 10, "6"); + await ctx.events.acknowledgeLifecycle(companyId, events[0].id); + await ctx.agents.getRuntime("agent-a", companyId, "environment-a", true); return events; }); } }); diff --git a/packages/plugins/sdk/tsconfig.test.json b/packages/plugins/sdk/tsconfig.test.json new file mode 100644 index 00000000000..508201b3b8e --- /dev/null +++ b/packages/plugins/sdk/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src", "tests"] +} diff --git a/scripts/typecheck-sandbox-providers/package.json b/scripts/typecheck-sandbox-providers/package.json new file mode 100644 index 00000000000..c26c721beb8 --- /dev/null +++ b/scripts/typecheck-sandbox-providers/package.json @@ -0,0 +1,8 @@ +{ + "name": "@paperclipai/typecheck-sandbox-providers", + "private": true, + "description": "Installs sandbox provider dependencies for scripts/typecheck-test-programs.mjs. See doc/DEVELOPING.md.", + "engines": { + "node": ">=24.11.0" + } +} diff --git a/scripts/typecheck-sandbox-providers/pnpm-lock.yaml b/scripts/typecheck-sandbox-providers/pnpm-lock.yaml new file mode 100644 index 00000000000..b3c4b347403 --- /dev/null +++ b/scripts/typecheck-sandbox-providers/pnpm-lock.yaml @@ -0,0 +1,4206 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + ../../packages/plugins/sandbox-providers/boxd: + dependencies: + '@boxd-sh/sdk': + specifier: ^0.2.20 + version: 0.2.22 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.19.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + + ../../packages/plugins/sandbox-providers/cloudflare: + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.19.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + + ../../packages/plugins/sandbox-providers/cloudflare/bridge-template: + dependencies: + '@cloudflare/sandbox': + specifier: ^0.7.0 + version: 0.7.21 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20260501.0 + version: 4.20260702.1 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + wrangler: + specifier: ^4.15.0 + version: 4.156.2(@cloudflare/workers-types@4.20260702.1)(@types/node@24.19.2) + + ../../packages/plugins/sandbox-providers/createos: + dependencies: + tar: + specifier: ^7.5.22 + version: 7.5.22 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.19.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + + ../../packages/plugins/sandbox-providers/daytona: + dependencies: + '@daytonaio/sdk': + specifier: 0.203.0 + version: 0.203.0 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.19.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + + ../../packages/plugins/sandbox-providers/e2b: + dependencies: + e2b: + specifier: ^2.19.0 + version: 2.62.1 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.19.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + + ../../packages/plugins/sandbox-providers/exe-dev: + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.19.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + + ../../packages/plugins/sandbox-providers/kubernetes: + dependencies: + '@kubernetes/client-node': + specifier: ^1.0.0 + version: 1.4.0 + zod: + specifier: ^4.6.6 + version: 4.6.7 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.19.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + + ../../packages/plugins/sandbox-providers/modal: + dependencies: + modal: + specifier: ^0.7.4 + version: 0.7.6 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.19.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + + ../../packages/plugins/sandbox-providers/novita: + dependencies: + novita-sandbox: + specifier: ^1.0.3-b2 + version: 1.0.3-b2 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.19.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.8 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + +packages: + + '@aws-sdk/checksums@3.1002.1': + resolution: {integrity: sha512-CXNESwkQC9jdrUG6cKXBfudVt1frGcYQykcCYbNclVrsewuGMbo3PlFjRteMe4OftNAqhyc3xC2jTz7SUqLasQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1161.0': + resolution: {integrity: sha512-iRZGMsJnYRrI98O+U6o5fHeLPQTP+Dc/w0mWLcBLQcEQ10DA06+XQj0WmdRiCM7vIso7QEtN1fsj9Vzo5/R3LQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.979.1': + resolution: {integrity: sha512-ayOlV1VhLNMiq8PW9ZkVobG4gkldnUoHjG69yy2b8j+VYFlhz/uAL7ZdYQAoQ+eShtGUdNDnnG5rxDg6yUJrOg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.80': + resolution: {integrity: sha512-uO5TiSvb6j1w9RTe5z28OaOmsrOxVQUPaC5+wZl50RfOXcrYD/KgnS8I9lfyGXc7sGbkCopK6VnhNIAoLqfewQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.82': + resolution: {integrity: sha512-vQzlDHlC24Yz42p+rT4NBoP/5VdetkBoujbOLq9h6QYWC8THuZSPYnXIR9OP3XcOrdMgPBBipOrdssfxnIiWUg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.25': + resolution: {integrity: sha512-b5xl71uCRdWsu7P1VeGpiGuCf6zfYETsSNOv8AmYVTrsgbvTJRXdmmOPVfxkvGGVNsiKkGctClQW3jAn0SZLug==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.87': + resolution: {integrity: sha512-C4ZFmcwM+Zj14Z8huVUk54Gv4uzT1ZdzG9cY3Gdh8FePakq7Bss1OFsgZjAhWFOkFOGpBvFBeu0OWjWf8HNKRQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.92': + resolution: {integrity: sha512-jC3zmOo7Qxlk+oBivvSsS1Ph7sFGm0QfuhqDGbqgRXi4cxUYMG+dWtBwS2XYYXgc8TIVXJiwgQ65sbNpbYupTA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.80': + resolution: {integrity: sha512-xqfixO5aclje7tbQJDias+iNND5wp/3QZixCdZcVQpgWPTZ0V5DfTW9A804A5Z7ezu7sH3WRO+vzUjtiJBA8yg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.24': + resolution: {integrity: sha512-nlg3BKG03Cgzvv/dMnhxvDJPhAhQSpaN0RUdwgU7ziwkasoev7iI8el8Xy4Rd5sqzgaDUbV269fOXck5SargqA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.86': + resolution: {integrity: sha512-zKTyLJBEyfQBVlYqT6Jt+mYV4r8kGbWM7dn6v4Y4MRARizhYafjDXP/iMu76UMRhToSPNmIKX1XAb24gtpBFIA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/lib-storage@3.1161.0': + resolution: {integrity: sha512-ROSytGylVRtkUsp64mpzryv1Puvgo6jAm4RWTW2m3kC4SSsjrvhqZa5uPKfz116a0U1kk/hBz9S1eBItkyd++w==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@aws-sdk/client-s3': ^3.1161.0 + + '@aws-sdk/middleware-sdk-s3@3.972.85': + resolution: {integrity: sha512-Kyl2G6zhIS9qFoeLdlxvXFrWyc6g9D9ZCIRJnGkkfX6JZhxCZB5ebXBc7GKEeetrnhXcail6ALZT0lKbojcaNA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.54': + resolution: {integrity: sha512-/OczlavsS7U5sOodRO10Ug4R/AKFVZiXTstKipWe68EfjPkmdDFNXNl9FCJyXWkg1DG6RBm3qAuVB8FNW9O8KQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.51': + resolution: {integrity: sha512-+aHvUugVBuMPXxgkPfWnTMyGdE/yWS8GHi5+qQ37HowS0YqzrbuaDLznj+Dtmtu5jgCdqz36gXmujOToUhNNWw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1161.0': + resolution: {integrity: sha512-ypbXSofNcSHwf/KA40bFCXJb0MLaAKrfT7sot4dFaA+2lsuXNah4zLRJIAU7Qmvs9PFJ6rnY0bZFEC+mE7attA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.975.0': + resolution: {integrity: sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.42': + resolution: {integrity: sha512-smX8/gNP8Kjq+CWTBMZQLQ912vOQjiWExzwzvMO+eujPB5RY8IbtF0AGdUUXHKN7KoDWK9dIcuW0WG269kiP6w==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + + '@boxd-sh/sdk@0.2.22': + resolution: {integrity: sha512-NvvYy5nblUPUM+F3EUcjgIH7uzu1ebMiEuL7tf0+4D/2CaszleSAd93S3Pn7py5ywrVLXfB5Rzj/GuABj67Z5g==} + engines: {node: '>=20'} + + '@bufbuild/protobuf@2.16.0': + resolution: {integrity: sha512-FWa0sPlqYGJgpTs6OxBRcL/AW4JT0OIO+W1ajerluoTVh8CV0B7XM1qsNCRgnkSCkRCgOICKHRQlY17jrsm0+Q==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.3': + resolution: {integrity: sha512-qpUy9Yoisi6a7QHOVa+AlnqfN7eVstSRhpLwiLnwi6i3SNLkM5D2Z7MXl4LC5nUGA77mZEt479cPH3u8GD163Q==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.3': + resolution: {integrity: sha512-6bZbK7dyKn8BUNmG3UfzSnM4L/OzJ8xGAtBHYycprl+bgvtZmZoQC7CucWXuF/oHJP5skzqkqFr6BLSSGIkoWw==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.3': + resolution: {integrity: sha512-QAuFuQkOw0cm4MNc+QwxH9aiMxmrczESnVEu33B8m2Du9fBuH/1AvJfmO8YajpeWLoHqt2iUG3d1/gIWAKY8IQ==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.3': + resolution: {integrity: sha512-joCovTc7uEYCL9XxdAJJpGIH7Vvc31Fi7vh3HnlY5zgkLSU/64WxYl5iUi/puE21qT3bmxMfOZEZUbnqVKeJTQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.3': + resolution: {integrity: sha512-IQMPVLulOvFv9Had7J1idJyfQzsFamDVdjH37qW7suHI7mgO4w1Oo3MisL7AjnWARxo0/ZmEEQ3i81WA7y34Ug==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.3': + resolution: {integrity: sha512-fGFfq6F3l0GCoKOZeyjjm3q+NHO7jo8ILtQ93U3rYJOgG2BGLS/QmBdoFLNEsoWPjyxAS8svWhTyo73MTDawog==} + cpu: [x64] + os: [win32] + + '@cloudflare/containers@0.1.1': + resolution: {integrity: sha512-YTdobRTnTlUOUPMFemufH367A9Z8pDfZ+UboYMLbGpO0VlvEXZDiioSmXPQMHld2vRtkL31mcRii3bcbQU6fdw==} + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/sandbox@0.7.21': + resolution: {integrity: sha512-BlfbhIWF83NkmixxGVpE9zrcR0Ix5fVgV+zzfJByxyRnXUS+g2aUeIXK2opBcnX8WWlk6v5+RYTKkSz1XKQ4eg==} + peerDependencies: + '@openai/agents': ^0.3.3 + '@opencode-ai/sdk': ^1.1.40 + '@xterm/xterm': '>=5.0.0' + peerDependenciesMeta: + '@openai/agents': + optional: true + '@opencode-ai/sdk': + optional: true + '@xterm/xterm': + optional: true + + '@cloudflare/unenv-preset@2.16.2': + resolution: {integrity: sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20261102.1': + resolution: {integrity: sha512-5vxE27qqftDMv9H0mCZt0B1cr+EvszT/lhpoFmdKqtS2Uu+Jo2PMIUYGepLe6yxbdV1hPKkgLBD2scnW3lXe/Q==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20261102.1': + resolution: {integrity: sha512-VanKNl3pLP2k8j9nEvETp/l3+dvfGvdWKI6bNJByKVz5szVVIMnGvDvrr+JmotreUZHWTDqfLh2RzPGRq1cZzg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20261102.1': + resolution: {integrity: sha512-hQdSdaHLS4UFGZRoue9TFVOO9UPnOAGWw3ZTZnUtXoUGWf4THwatq0l55F7IbOQYcOJfeKpHoFcMoGEH/88GCA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20261102.1': + resolution: {integrity: sha512-cqMnRpvDT6v6K80YClFs7XnZMq2RC5H6aHawlrVsIHrmVdSEXUm7OEVMQ9lY5nx3EI4JftiTeFpkLHFZqTSNVg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20261102.1': + resolution: {integrity: sha512-s9iKBbAdNIdVjpMUHh48zbCfCHw5wbLl8I2aIW74H3OtxpWZP/cHeEwZZMq3xAz5kt4MmvwwO4hDbNYoAD4yhw==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260702.1': + resolution: {integrity: sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==} + + '@connectrpc/connect-web@2.0.0-rc.3': + resolution: {integrity: sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + '@connectrpc/connect': 2.0.0-rc.3 + + '@connectrpc/connect-web@2.3.0': + resolution: {integrity: sha512-JPXOiWaVsj+ZwaXOQjwvhc0ORnkl0a1w36Qyl3j1WTuak6/VYyuWGl1Aq8I60gQfQRjsjLLFGr64tO0sjwstcg==} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + '@connectrpc/connect': 2.3.0 + + '@connectrpc/connect@2.0.0-rc.3': + resolution: {integrity: sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + + '@connectrpc/connect@2.3.0': + resolution: {integrity: sha512-rjAn8L9KbAZrkwiB8m1qzDsLs74hkYmwwM2iSWuxTPVOTRm/EZw31smKbQqPU50AXqJC5eKKpEnA6Uf/swQOgQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@daytona/analytics-api-client@0.203.0': + resolution: {integrity: sha512-sUELDOmSJW73kG8xsEOUVAE+SGAhPIaLThIBJc6Zbp+AnEMObeUNwXFh9r9ATCdYULoDkTVVA8mMqzP46io9LQ==} + + '@daytona/api-client@0.203.0': + resolution: {integrity: sha512-d72iv8+FECQo0h4G9fOIAr7iVuJaa/c1KJXfG2LeeI3uG+uCWh+E0d8pfgW9nAMQS4wD1ZoTuuzPPUedSn7g7Q==} + + '@daytona/toolbox-api-client@0.203.0': + resolution: {integrity: sha512-J3X72PLZOrzR4KUQj6EHdNU4ALZnN8FBPt49h7Q7UqGWdeq21ZmgQ69acOcGZyDq/rc6xPlHKgfQ5W5YLYHdpg==} + + '@daytonaio/sdk@0.203.0': + resolution: {integrity: sha512-wrCfeZFm36l/oVVyOVJsbPwmcaxNAkzH/z00UKBaEjoda5PYz9gqTqJF9e8vgSpkDT28ZMz3CaYnyl6AO/5Ucw==} + deprecated: 'Moved to @daytona/sdk, same API, no breaking changes. Please update: npm uninstall @daytonaio/sdk && npm i @daytona/sdk' + + '@e2b/dockerfile-utils@0.1.1': + resolution: {integrity: sha512-eDmlWRMrJNzSUbBOf6Y/qTChd1Ys/o3YY8PK6tRQmwkV2u7z3ZS2cak/YJB07odHMT/Z6HJKoUElR/b0N6bHUw==} + engines: {node: '>=22'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@grpc/grpc-js@1.14.7': + resolution: {integrity: sha512-W4W/fcBMGRVtG6dW2c7cw2mnpS3hvbBd+oQXjh2iYFTZDk1JJmcNqkM5KSaPubQpamlUUurrlb7G1s9NJeERmQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + + '@iarna/toml@2.2.5': + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@isaacs/nonrecursive-glob-match@1.0.3': + resolution: {integrity: sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@jsep-plugin/assignment@1.4.0': + resolution: {integrity: sha512-nw2Px23Eirx0PFTVoPrb3FEq9Wkmg6vxcDtt38MtBCf1G0tjLCB9cK+3seOXyiw+6+aalgYhvJabQbWwMVKlEQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.1.0': + resolution: {integrity: sha512-2Vkbda+XRjK6VTI6Lrh0sNBuMT8mZ51EhqMvO+V2rSdmqjs7NAGssxPZfkL23EFzYfSrvxyd1ZjOdqPQMM50og==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@kubernetes/client-node@1.4.0': + resolution: {integrity: sha512-Zge3YvF7DJi264dU1b3wb/GmzR99JhUpqTvp+VGHfwZT+g7EOOYNScDJNZwXy9cszyIGPIs0VHr+kk8e95qqrA==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@opentelemetry/api-logs@0.220.0': + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.10.0': + resolution: {integrity: sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/configuration@0.220.0': + resolution: {integrity: sha512-glfIVKnZevRin8fY/9uES/mhRtMT1lGINLHc9MIo5fTQZXswEEHamJtgjv4MTtzgnhHGC92mIS/0lzAUZMyE0w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/context-async-hooks@2.9.0': + resolution: {integrity: sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.12.0': + resolution: {integrity: sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.9.0': + resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.220.0': + resolution: {integrity: sha512-s0sRPCSlXYqlgObOpCftomJllp3LfUL9FobQ5csg2172ydVhSEnu1ptpsVBJadazs5nUNp7vDuLE03FAFWTLOQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.220.0': + resolution: {integrity: sha512-8186thl+pTw64iz/qEEen5oJZoZ/gO73XruChdaGlYdWOdBIQ42r+vHLf6a7vIDqTD4b8ZOoMlyxptanECaI9A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.220.0': + resolution: {integrity: sha512-8LZAxdJ0ENDAFwr4j0oY35mHBltiSzvlhdQAPGiC7p9VnxtuSq4SW1gfBAdW6t6hiQG6OwUl8w7KHaOdJPKHWg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.220.0': + resolution: {integrity: sha512-U128izvJfX/dW9jRGP0gIfadR1Hg7ft3UEGIeRxLFK70m2BWw6AtNCOnsUygpw2zCgR/ygdWbGpcL6TmhW0ZGw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.220.0': + resolution: {integrity: sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.220.0': + resolution: {integrity: sha512-lyO+IQBdSvqHN/ZOW/OzrSWemtfD+HgWngn+HBNLhjy0YrCQQTz0OE/kSekH2Pl340dn9DWzhqHdz5Eftr+HLA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.220.0': + resolution: {integrity: sha512-JZD5DL/NBpVd2BHefvYosm3G40UZ/KzExLv5tc0eZe0CtrsHHtcOk3YPUxR2EINmUeBf8+w5UReTV8fFPn95lA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.220.0': + resolution: {integrity: sha512-bv1xmNhmNwIM6MdUBw4yYuJeVcEViVLk3uD69vOQMwueHBnfyl/u0HnBlB1FNY/Te0UOzJzvcbyR8wN6b+iGbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.220.0': + resolution: {integrity: sha512-/+ExB3lRkf+erv4PnoywyL7RHKITidxtUpUTS55k7OQ0dB42S7gEF1gry7swb9MSm1hYLUhJg4QQh9W8SpwwqA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.220.0': + resolution: {integrity: sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@2.9.0': + resolution: {integrity: sha512-RwINoce2BH8T4obT5pMcAla2sWma1YZvYuaktWmTluQ0PkQdvv5D060rWI1+kawX+J2qBRcMbwrZJJNcMJUauQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-http@0.220.0': + resolution: {integrity: sha512-Szt4dO2Boz2CDr38DaSw/lnqwhwKl+IAdgNGEGgSm2Anb+fwPtIAGmIwkhsLLN69QQZQE96JxjMKYY4rlRkYKw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.220.0': + resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.220.0': + resolution: {integrity: sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.220.0': + resolution: {integrity: sha512-/eIkBPMBTIvM3x/0mDX4aJeSkYifYClnBPr68PL1h5LV4VQv4+SV6CGrpiZ4fIWDnobVmhTWCm1J/QRdAWUfvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.220.0': + resolution: {integrity: sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@2.9.0': + resolution: {integrity: sha512-WrOT1WsOUG+B7hstD2RYoMPIOK76G8E9AQHhMjUvrQaGx/oA7rPWQvvr1Rqv7+yy4R0ZMVwWLC4vW2xnkgWPAQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@2.9.0': + resolution: {integrity: sha512-4mYGty27rYvSM0jtp1ZUOqd3LfVRCYg9H5G9OFzSx5HViYToU21MFhWfco7x1HwXr7ER8yGOiCIHZUwjPksc0Q==} + engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Package no longer supported. See https://github.com/open-telemetry/opentelemetry-js/pull/7077 + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@2.12.0': + resolution: {integrity: sha512-+rSRoOdln6NhXTVcBdHSlZxJDNzwxadeweK2ltZO3tVtfp6aklABc06xclDk6ovRR+EiGgIcSbh/6NgjFhnb3A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.9.0': + resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.220.0': + resolution: {integrity: sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.9.0': + resolution: {integrity: sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.220.0': + resolution: {integrity: sha512-wHtGyHhSKHNH3fym33xRu4Ef/HXTFvX8eQ42xdQdEO9LYx9Y2qNyBDJytyqVlvmo6abWZlNYTUthuAGUMYqYnQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.12.0': + resolution: {integrity: sha512-JOERgZCxRM06FEJ5kEWxlSRChah6MmXDx3KwCruqEv/iEyLkTokCSemf2y1w8oIHtfrr8EVTpzebq/31vn2Cpg==} + engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Use @opentelemetry/sdk-trace package + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.9.0': + resolution: {integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==} + engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Use @opentelemetry/sdk-trace package + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.9.0': + resolution: {integrity: sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ==} + engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Use @opentelemetry/sdk-trace package + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.12.0': + resolution: {integrity: sha512-ImVuRQ6faOsTi1Wna88s2avgrrf/iDgXPigIeynkUhjstMuoQUDOJXgkez2j1mlU4btjXcXBGa+q7qKFYR3GsA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.9.0': + resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.44.0': + resolution: {integrity: sha512-E3JkOlMBi3N8OGGkbl7tFyqk2MAS6/lySRZBK3Mu7AzjF1fT8oIChH9ChE48SUWzDn9Kdwta+G/p5LJ4xW4CLQ==} + engines: {node: '>=14'} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@rollup/rollup-android-arm-eabi@4.67.4': + resolution: {integrity: sha512-/v/pUAPOcwp+r1Hjjdg0cBV1TE89CzGKrKh2FgkIQw2QBcIMmVPOCSfuOZStCn4Xh/98HXfo/ht6ztcssctPNg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.67.4': + resolution: {integrity: sha512-6tmmB/P52Vt167FXXEw5auiEUgTtZz7aqUgWHqYXToIUE0HFDY6fTdAyeekjjsNk6O/8dtpSKtClxSj1mMneKQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-android-x64@4.67.4': + resolution: {integrity: sha512-8Z9Q4RmotVmuL4llOvqX814JNiOK1oqoBv+glG5KbpRfUiypqzz2/fxIzK4Pq129l4dT5a8Gszmpc0/XPYoFuQ==} + cpu: [x64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.67.4': + resolution: {integrity: sha512-pnq6hssxTtwuRAl6Kdi/+IDY+b1IhkUetjhUkdLAAJCssjfqtL2Oqy/FXJVJFtAWOu1oRlArIvf0Yk1O92x2HQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.67.4': + resolution: {integrity: sha512-BeIrGZvHMVdJg/N+ohZfFywG/gF41684YhDj2wBvouj7m9ziP1tZDUNwzXRiDY+3WIKngaC4hiUBqVtBPGP5og==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.67.4': + resolution: {integrity: sha512-fRFX/oqNg/g780DKoS8RIfYcZD59Ru5O/bSeK1N7gQljBFrTxJY/q5EX4eMs5SrJf32VgVBVVYCdfPpzAwPPnQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.67.4': + resolution: {integrity: sha512-bSrBRopG1q+0RuOsW0NtYTtIODlvNA3T3ebx5WnQVdSxWIoXFmj+rZdHUHdSLgFdZ5M6VGmekYE7r4RMGQXjfg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.67.4': + resolution: {integrity: sha512-L8IvdrJVtl65g1ozwDSyjGPG+FmibhCdFUwmqTItJLw0S3+Oj4QzFs7rfgTUiK7rsPjkCLdG1/GApFxoynXFJQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.67.4': + resolution: {integrity: sha512-h7PcoQOzC31jshNpQ48J0akB9tsghjkSjSl5AZ6XsVGTxVsln7t7C4SQyw62tNVSaz2SeHgk4VT2ELbL17BSHQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.67.4': + resolution: {integrity: sha512-KSPuOBNHoOl7AsVjeY1KZci8+0up0U4TFAHU5IzBYN8P+Z6cB9DZq28+JQycZW7f6WLLa/tSzhV7Rdy95bVi+g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.67.4': + resolution: {integrity: sha512-0E/mKmVk1PlQeQYygIwQm0jlwjANTzGSiNBMPrFx+rdFGFj/2Axh4p6XwcrystQIqXZVudTaGsdbDpzYgJ4wSA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.67.4': + resolution: {integrity: sha512-Fvw7dOSc2Sa9UOipRZVI0zOgdLCzsjVyD/6JuHXzUHTXFu7mMgmeX/0/bCVBIut1dz+nWz3pV4WpS/Qg0XVnxw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.67.4': + resolution: {integrity: sha512-jzxbVHaE7V3F+P0wEK7UydQtvv7Twfdtx58jI5/3VRx1jJxyrn+qFwfXmqjVFxPhBl+zjaRs+ZJMFJw7vdHaQA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.67.4': + resolution: {integrity: sha512-fhtrzp9p4GjEJVaigaap9g6X9nP6xgJtKiLLXSDgpE18HXufA5aqfltGn+KAqMtVZReJZfOrXm//y5YSA9BWbA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.67.4': + resolution: {integrity: sha512-uynaF7IUHwAckV0S+yC1f5esy9gB+aqCBsPH5BOck32TYdCuAn2lVG5tvUcFnEsrviGEmRZxE1NHSOieUPYJEg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.67.4': + resolution: {integrity: sha512-uaqRFdfb8eIa2bVHN/FE3ndCvR0zV2xv0AwjCfxrqSu6l6fp/54VhjfhslKlf2RIik9L1jwlt4E56PeVX6tQtQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.67.4': + resolution: {integrity: sha512-JZRow7kmxUDEASnxHuVG5itTm+nTXi9BQUABcVUnUEngC+OPwMrpx1uJ9hIA/v+IcAEBL52ktCSPXk5OJn9Jqg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.67.4': + resolution: {integrity: sha512-rdrh4e+Lq9fCNKoDPaCqeHXbQCe7yl77Ukl45tlSClwYdZ1gpFGZz1XLJdYtY++FRlfL5aHvb6/0n3GDW/Nxsw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.67.4': + resolution: {integrity: sha512-wL7MS4r0fYcxluF3ne2plofHZiB/LsqNCnyaUlyiKpAJLb6agPSFz7DEFCfkonWAZXMrLwCmC08b2ZEnEdJkog==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.67.4': + resolution: {integrity: sha512-4H0XHfd2VufGjX7NjUyeue78sD0bRIoKSGjGqO+F5nSvL6jN48NAU7esdmtFEYxZXwObLtt3MJ/kHS0jASMW3g==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.67.4': + resolution: {integrity: sha512-osRHQbE55pZpCLetpMsFmhkapyZnAUH73qRKMh+npi3boUom6u6hdaTTPSxlInXxwJHsn9r2uDhdz0V7Yoxx2A==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.67.4': + resolution: {integrity: sha512-95+exYEyOwrZePsE0czi4q7X5lxKPa21+lmyAZlwbYENITQ0Unk4HAQAFp/p+KhXBHBx9mUABXRYjOC5A1wY3Q==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.67.4': + resolution: {integrity: sha512-FOXOcKhNQpO4Po+Ynan6IbtM67SrXZbgpRJWVYwz53CT5veI7sVzDwVBmWS65N5Imv+wK3SVhIPAl+C5DXXNCg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.67.4': + resolution: {integrity: sha512-cklKh0gJOkAZQcWziQa3hrjxZKN+PCHZ7SPTyGJDAiBVr05ZXlEjGFCG7zFJMaVibxsmzAe1FimN4u/HBns9+Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.67.4': + resolution: {integrity: sha512-AegwxJPc9bvUp8fQtlsUHkFMOMkGylWU7glyL+YD4vwG+QaYoGXYLSsQoeTD7kvoLZrVlMWNVhTg1dedY5C1VQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.67.4': + resolution: {integrity: sha512-RJZnrwTWj7Iq2ooBwoGbDdC65tDwYawneGcAfFMJqVUrRZMcskka6nTkBpXR/47G7kzn3EW2qupExnaBy11vwg==} + cpu: [x64] + os: [win32] + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@smithy/core@3.38.1': + resolution: {integrity: sha512-U/mB2XiwTLaf+NSgLSWwJfptsMT/H8aBi0aTAA/E5e2/TagFeGCqAyhKGG0w0A9IWTkm4qYEmLoD/HB6aYfnRA==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.8.0': + resolution: {integrity: sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.14.1': + resolution: {integrity: sha512-/T6E1jzCL0fErS9uq8JqES/5yYQQ4ppVmmYHwXw2nlov8KFzLb+3QqHR5iAun1BsuAB50IPxZliV8YMgghLwhA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.5': + resolution: {integrity: sha512-WPjdr5oWOk1LArpI4pavu+fSo+/evi0Re1olxGvqwuZopd4gYR1UGCi+Soyn/IjmafqbWs+ZC+3AKxJS3ZpwUg==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.19.0': + resolution: {integrity: sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==} + engines: {node: '>=18.0.0'} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@speed-highlight/core@1.2.24': + resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.10': + resolution: {integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@24.19.2': + resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + + '@types/stream-buffers@3.0.8': + resolution: {integrity: sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==} + + '@vitest/expect@4.1.12': + resolution: {integrity: sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==} + + '@vitest/mocker@4.1.12': + resolution: {integrity: sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.12': + resolution: {integrity: sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==} + + '@vitest/runner@4.1.12': + resolution: {integrity: sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==} + + '@vitest/snapshot@4.1.12': + resolution: {integrity: sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==} + + '@vitest/spy@4.1.12': + resolution: {integrity: sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==} + + '@vitest/utils@4.1.12': + resolution: {integrity: sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==} + + abort-controller-x@0.5.0: + resolution: {integrity: sha512-yTt9CI0x+nRfX6BFMenEGP8ooPvErGH6AbFz20C2IeOLIlDsrw/VHpgne3GsCEuTA410IiFiaLVFKmgM4bKEPQ==} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + + axios@1.21.0: + resolution: {integrity: sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==} + + b4a@1.9.0: + resolution: {integrity: sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.8.4: + resolution: {integrity: sha512-MPC2eyrpbk3A873sK4y6rk+Iv4o2enxTjHZ8zH3HO0mjrpm5XjnguY2XmfidIsDV6vnjpbMBKUAJpV0ISD2SsQ==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.2: + resolution: {integrity: sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==} + + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.5.5: + resolution: {integrity: sha512-iM9qJCruvpt2rPaLsrumVDru9pogI7PHziHIa9kTNRFt8C8HuSa+kI9kFezK+NvHEFnnz71rKGZ1EAilq/3V/Q==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + bowser@2.15.0: + resolution: {integrity: sha512-xic6KSZglg/I4ymp45fdM4GWfYpBK79Fxd/TOvJktlaPgQqqDxvmGwOPZl2V0dyeS3Lj5c180bLhXF43iyAjZQ==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer@5.6.0: + resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + cbor-extract@2.2.3: + resolution: {integrity: sha512-zTOCEYwiQDChj5DV/sUqccv3jhdZqbhymztwcrW2LncIG2fPPkb1ceK5rmr0bdtxzJCXoWzZ95shA+uaSH8cKw==} + hasBin: true + + cbor-x@1.6.9: + resolution: {integrity: sha512-TCT9XHLb4iPGQ99fpQjcTRncbus6cXHvSD/RY+bPrDACBCJKiQLmAM6r7/BgQcI5i+8h4gwp4vjfb6S6T8bPGw==} + + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cjs-module-lexer@2.3.0: + resolution: {integrity: sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + e2b@2.62.1: + resolution: {integrity: sha512-8TxPojt1659LmX9SX8zSet6fdefWxeSZoh+obad8DuC5wdRQdMbzN6ahEKNGzjrCNJFR26YeRZWOboXsy9UDYA==} + engines: {node: '>=22.19.0'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-client@6.6.7: + resolution: {integrity: sha512-GyAvmPA7gnRCpDQbYACAy79Lg4ewevrmaPzySZz0OVvgyZc3gEfLLqPTYk8wVBScjOEpM9ZO4KryyFpVItjGEA==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + es-module-lexer@3.0.4: + resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + follow-redirects@1.16.2: + resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@14.0.3: + resolution: {integrity: sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==} + engines: {node: 18 || 20 || >=22} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + import-in-the-middle@3.5.3: + resolution: {integrity: sha512-usm/IiY63VtOyx/dm7uEoZV+EsanCDmwRUroQZIkFLGu1NVXZ8mZqIOhaN4QIJ7EsbMTuQbJXwCySCx+ozMA8g==} + engines: {node: '>=18'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.7.5: + resolution: {integrity: sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==} + engines: {node: '>= 12'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + jose@6.2.15: + resolution: {integrity: sha512-qdF6ewcZARtef63fJ9AKKZPgxw+t7USH6HLXYcD4CrdmSvDp79rpJSPFEKc90QBeGS63u1KQtN2MZQHC0yDIzA==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsep@1.6.0: + resolution: {integrity: sha512-Y8Tr54CX7B5YGAwo7/n8eumidEX3Opx98RyyQVnlHqUaZcPA8uUzL2v1rshr9ypW9Q6hNjfJtg3sL+8kaNE7TA==} + engines: {node: '>= 20.16.0'} + + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + miniflare@5.20261102.0-beta: + resolution: {integrity: sha512-FGqoi2BF7Vq6v8aSraq9Tmhg8ORpUBC5RkNGhfa11J597cWlLJ5QP9vJwDMOzwJx+W4U+IuHWSidVceBK6VRZQ==} + engines: {node: '>=22.0.0'} + + minimatch@11.0.4: + resolution: {integrity: sha512-/UJ1dEoVQTjPNDh6SGlUo+ueLMmoHTas4q+8yaAR/5t6nLAIiBDJtoYUh20IYleix7ehiQkgczb84PE3uzHkxg==} + engines: {node: 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + modal@0.7.6: + resolution: {integrity: sha512-AOFRO/eGl4fcNKxHkNLx55+tL318IeAiTDJCMh/Q1ZXhoaZFnpmlirVV2J5BhO32XLA7AiH6KYGA7gRBGA09lQ==} + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nice-grpc-common@2.0.4: + resolution: {integrity: sha512-gOEXlD6ShXZMZ8k+49wm/bA2j1+3IKbEFV9WYREJcvZV4kM5L1KkILYTX9VYBzZF2OuYCe1wp8c82bQkvR0fHw==} + + nice-grpc@2.1.18: + resolution: {integrity: sha512-m1if7HuVIhhVo/obvLgoSco2tj46zfeIEKkavlAnAsjklBzmFthvYX/uoKZmMKIwpVJllYpj+HVFRb+qGxibrA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + novita-sandbox@1.0.3-b2: + resolution: {integrity: sha512-W0TbVfacreckxdbw1Tbh9dPYVZKjr4e0qRnMj/oxNj+gw01CRKA5Nf8uwSPaMI5daRazyo+ZVnV7WZchKhHbNQ==} + engines: {node: '>=16.0.0'} + + oauth4webapi@3.8.8: + resolution: {integrity: sha512-8N28E+a/oxfXWBgOMt+ZP/JUf/XR+IFbvkAEPP3gznXOMv9BpAAwiIj0TFNz3tGTPc0ZQ8zmWBNgN1nAys0gng==} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openapi-fetch@0.17.0: + resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==} + + openapi-fetch@0.9.8: + resolution: {integrity: sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==} + + openapi-typescript-helpers@0.0.8: + resolution: {integrity: sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g==} + + openapi-typescript-helpers@0.1.0: + resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==} + + openid-client@6.8.9: + resolution: {integrity: sha512-6mXMRNJxDOf6BflEWLXul1UwzLFz++9Ofz1PYwPwwsJsyXBsbfkO+Jo255SW1qnFNLYz4EJWHz42X4OJJrya/g==} + + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + + path-scurry@3.0.0: + resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + + postcss@8.5.32: + resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} + engines: {node: ^10 || ^12 || >=14} + + protobufjs@7.6.6: + resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} + engines: {node: '>=12.0.0'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfc4648@1.5.4: + resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} + + rollup@4.67.4: + resolution: {integrity: sha512-Lsv3oKcz5jgOQJej/p7nfwbVEdAbDT+z6OPf/SCdI+nF+q2ENp0pTKT35ZwXAmH8g45mfrIiJeEyAqKYSYoL9Q==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@7.8.6: + resolution: {integrity: sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + shell-quote@1.12.0: + resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + smol-toml@1.9.1: + resolution: {integrity: sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==} + engines: {node: '>= 18'} + + socket.io-client@4.8.4: + resolution: {integrity: sha512-7wjKAvVXYOJsbmUgkWbnhPD4bFpkqrUR0++XOjpGFmqDqsqV1CI93VTraeExLxpjxEqrWgFoSRZu1ihjLWfkAA==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.7: + resolution: {integrity: sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==} + engines: {node: '>=10.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.10: + resolution: {integrity: sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + + stream-buffers@3.0.3: + resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} + engines: {node: '>= 0.10.0'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tar-fs@3.1.3: + resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} + + tar-stream@3.2.2: + resolution: {integrity: sha512-+8NeqHRjQWH9nYlwo2gamAMImZCVzI4UoEgDpWorBt9OEfppiZn+uSkskzQKPWIZyji/C8fpWO7u69G0DX0tbg==} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-error@1.0.6: + resolution: {integrity: sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tslib@2.9.1: + resolution: {integrity: sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + undici@7.30.1: + resolution: {integrity: sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==} + engines: {node: '>=20.18.1'} + + undici@8.12.3: + resolution: {integrity: sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==} + engines: {node: '>=22.19.0'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + vite@7.3.7: + resolution: {integrity: sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.12: + resolution: {integrity: sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.12 + '@vitest/browser-preview': 4.1.12 + '@vitest/browser-webdriverio': ^4.1.11 + '@vitest/coverage-istanbul': 4.1.12 + '@vitest/coverage-v8': 4.1.12 + '@vitest/ui': 4.1.12 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + workerd@1.20261102.1: + resolution: {integrity: sha512-5A+/vm0M5oc98pB3aR+DZ5cIOvRMhVWVS9tg+CrjUap7PIo8tqAnvNdvuiHBIE47/wkxtNM3jToAaU38dBsZTw==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.156.2: + resolution: {integrity: sha512-3RKI10rRqDHlqLFeCgCOmuPzoXw8npyGZbixYB5Vj5RW9jWPoSrupo8CzxCSkw1nVdvpzlFaRrHFjh4+AMt8Cw==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20261102.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.22.1: + resolution: {integrity: sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + +snapshots: + + '@aws-sdk/checksums@3.1002.1': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1161.0': + dependencies: + '@aws-sdk/checksums': 3.1002.1 + '@aws-sdk/core': 3.979.1 + '@aws-sdk/credential-provider-node': 3.972.92 + '@aws-sdk/middleware-sdk-s3': 3.972.85 + '@aws-sdk/signature-v4-multi-region': 3.996.51 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.14.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.979.1': + dependencies: + '@aws-sdk/types': 3.975.0 + '@aws-sdk/xml-builder': 3.972.42 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.38.1 + '@smithy/signature-v4': 5.7.5 + '@smithy/types': 4.19.0 + bowser: 2.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.80': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.82': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.14.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.25': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/credential-provider-env': 3.972.80 + '@aws-sdk/credential-provider-http': 3.972.82 + '@aws-sdk/credential-provider-login': 3.972.87 + '@aws-sdk/credential-provider-process': 3.972.80 + '@aws-sdk/credential-provider-sso': 3.973.24 + '@aws-sdk/credential-provider-web-identity': 3.972.86 + '@aws-sdk/nested-clients': 3.997.54 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.87': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/nested-clients': 3.997.54 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.92': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.80 + '@aws-sdk/credential-provider-http': 3.972.82 + '@aws-sdk/credential-provider-ini': 3.973.25 + '@aws-sdk/credential-provider-process': 3.972.80 + '@aws-sdk/credential-provider-sso': 3.973.24 + '@aws-sdk/credential-provider-web-identity': 3.972.86 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.80': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.24': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/nested-clients': 3.997.54 + '@aws-sdk/token-providers': 3.1161.0 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.86': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/nested-clients': 3.997.54 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/lib-storage@3.1161.0(@aws-sdk/client-s3@3.1161.0)': + dependencies: + '@aws-sdk/client-s3': 3.1161.0 + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + buffer: 5.6.0 + events: 3.3.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.85': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/signature-v4-multi-region': 3.996.51 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.54': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/signature-v4-multi-region': 3.996.51 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.14.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.51': + dependencies: + '@aws-sdk/types': 3.975.0 + '@smithy/signature-v4': 5.7.5 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1161.0': + dependencies: + '@aws-sdk/core': 3.979.1 + '@aws-sdk/nested-clients': 3.997.54 + '@aws-sdk/types': 3.975.0 + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.975.0': + dependencies: + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.42': + dependencies: + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + + '@boxd-sh/sdk@0.2.22': + dependencies: + '@bufbuild/protobuf': 2.16.0 + '@grpc/grpc-js': 1.14.7 + + '@bufbuild/protobuf@2.16.0': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.3': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.3': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.3': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.3': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.3': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.3': + optional: true + + '@cloudflare/containers@0.1.1': {} + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/sandbox@0.7.21': + dependencies: + '@cloudflare/containers': 0.1.1 + aws4fetch: 1.0.20 + + '@cloudflare/unenv-preset@2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261102.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20261102.1 + + '@cloudflare/workerd-darwin-64@1.20261102.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20261102.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20261102.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20261102.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20261102.1': + optional: true + + '@cloudflare/workers-types@4.20260702.1': {} + + '@connectrpc/connect-web@2.0.0-rc.3(@bufbuild/protobuf@2.16.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.16.0))': + dependencies: + '@bufbuild/protobuf': 2.16.0 + '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.16.0) + + '@connectrpc/connect-web@2.3.0(@bufbuild/protobuf@2.16.0)(@connectrpc/connect@2.3.0(@bufbuild/protobuf@2.16.0))': + dependencies: + '@bufbuild/protobuf': 2.16.0 + '@connectrpc/connect': 2.3.0(@bufbuild/protobuf@2.16.0) + + '@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.16.0)': + dependencies: + '@bufbuild/protobuf': 2.16.0 + + '@connectrpc/connect@2.3.0(@bufbuild/protobuf@2.16.0)': + dependencies: + '@bufbuild/protobuf': 2.16.0 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@daytona/analytics-api-client@0.203.0': + dependencies: + axios: 1.21.0 + transitivePeerDependencies: + - debug + - supports-color + + '@daytona/api-client@0.203.0': + dependencies: + axios: 1.21.0 + transitivePeerDependencies: + - debug + - supports-color + + '@daytona/toolbox-api-client@0.203.0': + dependencies: + axios: 1.21.0 + transitivePeerDependencies: + - debug + - supports-color + + '@daytonaio/sdk@0.203.0': + dependencies: + '@aws-sdk/client-s3': 3.1161.0 + '@aws-sdk/lib-storage': 3.1161.0(@aws-sdk/client-s3@3.1161.0) + '@daytona/analytics-api-client': 0.203.0 + '@daytona/api-client': 0.203.0 + '@daytona/toolbox-api-client': 0.203.0 + '@iarna/toml': 2.2.5 + '@opentelemetry/api': 1.10.0 + '@opentelemetry/exporter-trace-otlp-http': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/instrumentation-http': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-node': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace-base': 2.12.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + axios: 1.21.0 + busboy: 1.6.0 + dotenv: 17.4.2 + expand-tilde: 2.0.2 + fast-glob: 3.3.3 + form-data: 4.0.6 + isomorphic-ws: 5.0.0(ws@8.22.1) + pathe: 2.0.3 + shell-quote: 1.12.0 + socket.io-client: 4.8.4 + tar: 7.5.22 + tslib: 2.8.1 + ws: 8.22.1 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + '@e2b/dockerfile-utils@0.1.1': {} + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.9.1 + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@grpc/grpc-js@1.14.7': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.6 + yargs: 17.7.3 + + '@iarna/toml@2.2.5': {} + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@isaacs/nonrecursive-glob-match@1.0.3': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@jsep-plugin/assignment@1.4.0(jsep@1.6.0)': + dependencies: + jsep: 1.6.0 + + '@jsep-plugin/regex@1.1.0(jsep@1.6.0)': + dependencies: + jsep: 1.6.0 + + '@kubernetes/client-node@1.4.0': + dependencies: + '@types/js-yaml': 4.0.9 + '@types/node': 24.19.2 + '@types/node-fetch': 2.6.13 + '@types/stream-buffers': 3.0.8 + form-data: 4.0.6 + hpagent: 1.2.0 + isomorphic-ws: 5.0.0(ws@8.22.1) + js-yaml: 4.3.2 + jsonpath-plus: 10.4.0 + node-fetch: 2.7.0 + openid-client: 6.8.9 + rfc4648: 1.5.4 + socks-proxy-agent: 8.0.5 + stream-buffers: 3.0.3 + tar-fs: 3.1.3 + ws: 8.22.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - supports-color + - utf-8-validate + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@opentelemetry/api-logs@0.220.0': + dependencies: + '@opentelemetry/api': 1.10.0 + + '@opentelemetry/api@1.10.0': {} + + '@opentelemetry/configuration@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + yaml: 2.9.1 + + '@opentelemetry/context-async-hooks@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + + '@opentelemetry/core@2.12.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/core@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@grpc/grpc-js': 1.14.7 + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/exporter-logs-otlp-http@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@grpc/grpc-js': 1.14.7 + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/exporter-prometheus@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@grpc/grpc-js': 1.14.7 + '@opentelemetry/api': 1.10.0 + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/exporter-trace-otlp-http@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/exporter-zipkin@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/instrumentation-http@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/api-logs': 0.220.0 + import-in-the-middle: 3.5.3 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@grpc/grpc-js': 1.14.7 + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/otlp-transformer@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/propagator-b3@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/propagator-jaeger@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/resources@2.12.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/sdk-node@0.220.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/configuration': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-logs-otlp-http': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-prometheus': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-trace-otlp-http': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/exporter-zipkin': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/propagator-b3': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/propagator-jaeger': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace-node': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@2.12.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.12.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace': 2.12.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.10.0) + + '@opentelemetry/sdk-trace@2.12.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.12.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.10.0) + '@opentelemetry/semantic-conventions': 1.44.0 + + '@opentelemetry/semantic-conventions@1.44.0': {} + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + + '@rollup/rollup-android-arm-eabi@4.67.4': + optional: true + + '@rollup/rollup-android-arm64@4.67.4': + optional: true + + '@rollup/rollup-android-x64@4.67.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.67.4': + optional: true + + '@rollup/rollup-darwin-x64@4.67.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.67.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.67.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.67.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.67.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.67.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.67.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.67.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.67.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.67.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.67.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.67.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.67.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.67.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.67.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.67.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.67.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.67.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.67.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.67.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.67.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.67.4': + optional: true + + '@sindresorhus/is@7.2.0': {} + + '@smithy/core@3.38.1': + dependencies: + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.8.0': + dependencies: + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.14.1': + dependencies: + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.5': + dependencies: + '@smithy/core': 3.38.1 + '@smithy/types': 4.19.0 + tslib: 2.8.1 + + '@smithy/types@4.19.0': + dependencies: + tslib: 2.8.1 + + '@socket.io/component-emitter@3.1.2': {} + + '@speed-highlight/core@1.2.24': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.10': {} + + '@types/js-yaml@4.0.9': {} + + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 24.19.2 + form-data: 4.0.6 + + '@types/node@24.19.2': + dependencies: + undici-types: 7.24.6 + + '@types/stream-buffers@3.0.8': + dependencies: + '@types/node': 24.19.2 + + '@vitest/expect@4.1.12': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 + chai: 6.3.0 + tinyrainbow: 3.2.0 + + '@vitest/mocker@4.1.12(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1))': + dependencies: + '@vitest/spy': 4.1.12 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.7(@types/node@24.19.2)(yaml@2.9.1) + + '@vitest/pretty-format@4.1.12': + dependencies: + tinyrainbow: 3.2.0 + + '@vitest/runner@4.1.12': + dependencies: + '@vitest/utils': 4.1.12 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.12': + dependencies: + '@vitest/pretty-format': 4.1.12 + '@vitest/utils': 4.1.12 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.12': {} + + '@vitest/utils@4.1.12': + dependencies: + '@vitest/pretty-format': 4.1.12 + convert-source-map: 2.0.0 + tinyrainbow: 3.2.0 + + abort-controller-x@0.5.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + aws4fetch@1.0.20: {} + + axios@1.21.0: + dependencies: + follow-redirects: 1.16.2 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + b4a@1.9.0: {} + + balanced-match@4.0.4: {} + + bare-events@2.9.2: {} + + bare-fs@4.8.4: + dependencies: + bare-events: 2.9.2 + bare-path: 3.1.2 + bare-stream: 2.13.4(bare-events@2.9.2) + bare-url: 2.5.5 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-path@3.1.2: {} + + bare-stream@2.13.4(bare-events@2.9.2): + dependencies: + b4a: 1.9.0 + streamx: 2.28.1 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.5.5: + dependencies: + bare-path: 3.1.2 + + base64-js@1.5.1: {} + + blake3-wasm@2.1.5: {} + + bowser@2.15.0: {} + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer@5.6.0: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + cbor-extract@2.2.3: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.3 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.3 + '@cbor-extract/cbor-extract-linux-arm': 2.2.3 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.3 + '@cbor-extract/cbor-extract-linux-x64': 2.2.3 + '@cbor-extract/cbor-extract-win32-x64': 2.2.3 + optional: true + + cbor-x@1.6.9: + optionalDependencies: + cbor-extract: 2.2.3 + + chai@6.3.0: {} + + chalk@5.6.2: {} + + chownr@3.0.0: {} + + cjs-module-lexer@2.3.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + compare-versions@6.1.1: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + e2b@2.62.1: + dependencies: + '@bufbuild/protobuf': 2.16.0 + '@connectrpc/connect': 2.3.0(@bufbuild/protobuf@2.16.0) + '@connectrpc/connect-web': 2.3.0(@bufbuild/protobuf@2.16.0)(@connectrpc/connect@2.3.0(@bufbuild/protobuf@2.16.0)) + '@e2b/dockerfile-utils': 0.1.1 + chalk: 5.6.2 + compare-versions: 6.1.1 + glob: 14.0.3 + openapi-fetch: 0.17.0 + platform: 1.3.6 + tar: 7.5.22 + undici: 8.12.3 + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-client@6.6.7: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.21.3 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + error-stack-parser-es@1.0.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.2: {} + + es-module-lexer@3.0.4: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.10 + + events-universal@1.0.1: + dependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + expect-type@1.4.0: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + follow-redirects@1.16.2: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + forwarded-parse@2.1.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@14.0.3: + dependencies: + minimatch: 11.0.4 + minipass: 7.1.3 + path-scurry: 3.0.0 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hpagent@1.2.0: {} + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ieee754@1.2.1: {} + + import-in-the-middle@3.5.3: + dependencies: + cjs-module-lexer: 2.3.0 + es-module-lexer: 3.0.4 + module-details-from-path: 1.0.4 + + inherits@2.0.4: {} + + ip-address@10.7.5: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isomorphic-ws@5.0.0(ws@8.22.1): + dependencies: + ws: 8.22.1 + + jose@6.2.15: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsep@1.6.0: {} + + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.4.0(jsep@1.6.0) + '@jsep-plugin/regex': 1.1.0(jsep@1.6.0) + jsep: 1.6.0 + + kleur@4.1.5: {} + + lodash.camelcase@4.3.0: {} + + long@5.3.2: {} + + lru-cache@11.5.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + miniflare@5.20261102.0-beta(@types/node@24.19.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.5(@types/node@24.19.2) + undici: 7.30.1 + workerd: 1.20261102.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + minimatch@11.0.4: + dependencies: + '@isaacs/nonrecursive-glob-match': 1.0.3 + brace-expansion: 5.0.12 + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + modal@0.7.6: + dependencies: + cbor-x: 1.6.9 + long: 5.3.2 + nice-grpc: 2.1.18 + protobufjs: 7.6.6 + smol-toml: 1.9.1 + uuid: 11.1.1 + + module-details-from-path@1.0.4: {} + + ms@2.1.3: {} + + nanoid@3.3.20: {} + + nice-grpc-common@2.0.4: + dependencies: + ts-error: 1.0.6 + + nice-grpc@2.1.18: + dependencies: + '@grpc/grpc-js': 1.14.7 + abort-controller-x: 0.5.0 + nice-grpc-common: 2.0.4 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + novita-sandbox@1.0.3-b2: + dependencies: + '@bufbuild/protobuf': 2.16.0 + '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.16.0) + '@connectrpc/connect-web': 2.0.0-rc.3(@bufbuild/protobuf@2.16.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.16.0)) + compare-versions: 6.1.1 + openapi-fetch: 0.9.8 + platform: 1.3.6 + + oauth4webapi@3.8.8: {} + + obug@2.2.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openapi-fetch@0.17.0: + dependencies: + openapi-typescript-helpers: 0.1.0 + + openapi-fetch@0.9.8: + dependencies: + openapi-typescript-helpers: 0.0.8 + + openapi-typescript-helpers@0.0.8: {} + + openapi-typescript-helpers@0.1.0: {} + + openid-client@6.8.9: + dependencies: + jose: 6.2.15 + oauth4webapi: 3.8.8 + + parse-passwd@1.0.0: {} + + path-scurry@3.0.0: + dependencies: + lru-cache: 11.5.3 + minipass: 7.1.3 + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + platform@1.3.6: {} + + postcss@8.5.32: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + protobufjs@7.6.6: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 24.19.2 + long: 5.3.2 + + proxy-from-env@2.1.0: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + queue-microtask@1.2.3: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + require-directory@2.1.1: {} + + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + + reusify@1.1.0: {} + + rfc4648@1.5.4: {} + + rollup@4.67.4: + dependencies: + '@types/estree': 1.0.10 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.67.4 + '@rollup/rollup-android-arm64': 4.67.4 + '@rollup/rollup-android-x64': 4.67.4 + '@rollup/rollup-darwin-arm64': 4.67.4 + '@rollup/rollup-darwin-x64': 4.67.4 + '@rollup/rollup-freebsd-arm64': 4.67.4 + '@rollup/rollup-freebsd-x64': 4.67.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.67.4 + '@rollup/rollup-linux-arm-musleabihf': 4.67.4 + '@rollup/rollup-linux-arm64-gnu': 4.67.4 + '@rollup/rollup-linux-arm64-musl': 4.67.4 + '@rollup/rollup-linux-loong64-gnu': 4.67.4 + '@rollup/rollup-linux-loong64-musl': 4.67.4 + '@rollup/rollup-linux-ppc64-gnu': 4.67.4 + '@rollup/rollup-linux-ppc64-musl': 4.67.4 + '@rollup/rollup-linux-riscv64-gnu': 4.67.4 + '@rollup/rollup-linux-riscv64-musl': 4.67.4 + '@rollup/rollup-linux-s390x-gnu': 4.67.4 + '@rollup/rollup-linux-x64-gnu': 4.67.4 + '@rollup/rollup-linux-x64-musl': 4.67.4 + '@rollup/rollup-openbsd-x64': 4.67.4 + '@rollup/rollup-openharmony-arm64': 4.67.4 + '@rollup/rollup-win32-arm64-msvc': 4.67.4 + '@rollup/rollup-win32-ia32-msvc': 4.67.4 + '@rollup/rollup-win32-x64-gnu': 4.67.4 + '@rollup/rollup-win32-x64-msvc': 4.67.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + semver@7.8.6: {} + + sharp@0.35.5(@types/node@24.19.2): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.6 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 24.19.2 + + shell-quote@1.12.0: {} + + siginfo@2.0.0: {} + + smart-buffer@4.2.0: {} + + smol-toml@1.9.1: {} + + socket.io-client@4.8.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-client: 6.6.7 + socket.io-parser: 4.2.7 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.7: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.10 + transitivePeerDependencies: + - supports-color + + socks@2.8.10: + dependencies: + ip-address: 10.7.5 + smart-buffer: 4.2.0 + + source-map-js@1.2.2: {} + + stackback@0.0.2: {} + + std-env@4.3.0: {} + + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + stream-buffers@3.0.3: {} + + streamsearch@1.1.0: {} + + streamx@2.28.1: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + supports-color@10.2.2: {} + + tar-fs@3.1.3: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.2 + optionalDependencies: + bare-fs: 4.8.4 + bare-path: 3.1.2 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@3.2.2: + dependencies: + b4a: 1.9.0 + bare-fs: 4.8.4 + fast-fifo: 1.3.2 + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + teex@1.0.1: + dependencies: + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + text-decoder@1.2.7: + dependencies: + b4a: 1.9.0 + transitivePeerDependencies: + - react-native-b4a + + tinybench@2.9.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.2.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-error@1.0.6: {} + + tslib@2.8.1: {} + + tslib@2.9.1: + optional: true + + typescript@5.9.3: {} + + undici-types@7.24.6: {} + + undici@7.30.1: {} + + undici@8.12.3: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + util-deprecate@1.0.2: {} + + uuid@11.1.1: {} + + vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.32 + rollup: 4.67.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.19.2 + fsevents: 2.3.3 + yaml: 2.9.1 + + vitest@4.1.12(@opentelemetry/api@1.10.0)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)): + dependencies: + '@vitest/expect': 4.1.12 + '@vitest/mocker': 4.1.12(vite@7.3.7(@types/node@24.19.2)(yaml@2.9.1)) + '@vitest/pretty-format': 4.1.12 + '@vitest/runner': 4.1.12 + '@vitest/snapshot': 4.1.12 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.2.0 + vite: 7.3.7(@types/node@24.19.2)(yaml@2.9.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.10.0 + '@types/node': 24.19.2 + transitivePeerDependencies: + - msw + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + workerd@1.20261102.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20261102.1 + '@cloudflare/workerd-darwin-arm64': 1.20261102.1 + '@cloudflare/workerd-linux-64': 1.20261102.1 + '@cloudflare/workerd-linux-arm64': 1.20261102.1 + '@cloudflare/workerd-windows-64': 1.20261102.1 + + wrangler@4.156.2(@cloudflare/workers-types@4.20260702.1)(@types/node@24.19.2): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261102.1) + '@cspotcode/source-map-support': 0.8.1 + blake3-wasm: 2.1.5 + esbuild: 0.28.2 + miniflare: 5.20261102.0-beta(@types/node@24.19.2) + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20261102.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260702.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.21.0: {} + + ws@8.21.3: {} + + ws@8.22.1: {} + + xmlhttprequest-ssl@2.1.2: {} + + y18n@5.0.8: {} + + yallist@5.0.0: {} + + yaml@2.9.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.24 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod@4.6.7: {} diff --git a/scripts/typecheck-sandbox-providers/pnpm-workspace.yaml b/scripts/typecheck-sandbox-providers/pnpm-workspace.yaml new file mode 100644 index 00000000000..8a7199ba45e --- /dev/null +++ b/scripts/typecheck-sandbox-providers/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +# Sandbox providers stay out of the root workspace, and most of them must not +# commit a lockfile: scripts/build-standalone-public-packages.mjs uses a frozen +# install when one exists. This workspace pins their dependencies in one lockfile +# for scripts/typecheck-test-programs.mjs, without changing how they are released. +packages: + - ../../packages/plugins/sandbox-providers/* + - ../../packages/plugins/sandbox-providers/cloudflare/bridge-template diff --git a/scripts/typecheck-test-programs.mjs b/scripts/typecheck-test-programs.mjs new file mode 100644 index 00000000000..562efef0939 --- /dev/null +++ b/scripts/typecheck-test-programs.mjs @@ -0,0 +1,220 @@ +#!/usr/bin/env node +// Typechecks the test files that no build or package typecheck reads. +// +// Most package tsconfigs exclude tests so that `tsc` does not emit them, and +// Vitest strips types, so a type error in those tests fails nothing. Each program +// in PROGRAMS reads them without emitting. Sandbox providers are outside the +// workspace, so the root install has none of their dependencies. They install from +// typecheck-sandbox-providers/pnpm-lock.yaml here, because their SDK types are part +// of what a test checks. +// +// The script then fails if a tracked test file is in no program. tsc decides that, +// not a glob: `include` silently drops foo.test.tsx when foo.test.ts exists. +// +// Usage: node scripts/typecheck-test-programs.mjs [--low-memory] [tsconfig ...] +// Naming a tsconfig runs only that program and skips the coverage check. + +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { linkExcludedPlugins } from "./link-plugin-dev-sdk.mjs"; +import { LOW_MEMORY_VARIABLE, runTsc } from "./run-tsc.mjs"; +import { PROJECTS } from "./typecheck-test-projects.mjs"; + +export const PROVIDER_WORKSPACE = "scripts/typecheck-sandbox-providers"; + +export const SANDBOX_PROVIDERS = [ + "packages/plugins/sandbox-providers/boxd", + "packages/plugins/sandbox-providers/cloudflare", + "packages/plugins/sandbox-providers/cloudflare/bridge-template", + "packages/plugins/sandbox-providers/createos", + "packages/plugins/sandbox-providers/daytona", + "packages/plugins/sandbox-providers/e2b", + "packages/plugins/sandbox-providers/exe-dev", + "packages/plugins/sandbox-providers/kubernetes", + "packages/plugins/sandbox-providers/modal", + "packages/plugins/sandbox-providers/novita", +]; + +export const PROGRAMS = [ + "packages/db/tsconfig.test.json", + "packages/kv-demo-mcp-server/tsconfig.test.json", + "packages/paperclip-runner/tsconfig.browser.test.json", + "packages/paperclip-runner/tsconfig.surfaces.test.json", + "packages/paperclip-runner/tsconfig.test.json", + "packages/plugins/examples/plugin-orchestration-smoke-example/tsconfig.test.json", + "packages/plugins/plugin-workspace-diff/tsconfig.test.json", + "packages/plugins/sdk/tsconfig.test.json", + ...SANDBOX_PROVIDERS.map((directory) => `${directory}/tsconfig.test.json`), + "tests/runner-acceptance/tsconfig.json", + "tests/tsconfig.playwright.json", +]; + +// server/scripts/typecheck-layers.mjs reads this tree. Everything else must be in a program. +export const CHECKED_ELSEWHERE = ["server/src/"]; + +const PREPARE_STEPS = [ + ["--filter", "@paperclipai/plugin-sdk", "ensure-build-deps"], + ["--filter", "@paperclipai/paperclip-runner", "run", "ensure:eval-build-deps"], +]; +// Replacing a provider's standalone node_modules would otherwise ask for confirmation. +const INSTALL_ARGS = ["install", "--frozen-lockfile", "--ignore-scripts", "--prefer-offline", "--config.confirmModulesPurge=false"]; + +export function findUncheckedTests({ trackedFiles, checkedFiles, checkedElsewhere = CHECKED_ELSEWHERE }) { + const checked = new Set(checkedFiles); + return trackedFiles.filter( + (file) => + /\.(test|spec)\.tsx?$/.test(file) && + !checked.has(file) && + !checkedElsewhere.some((prefix) => file.startsWith(prefix)), + ); +} + +function runPnpm(args, { cwd, env }) { + const result = spawnSync("pnpm", args, { + cwd, + env, + stdio: "inherit", + // A bare `pnpm` on Windows is pnpm.cmd, which needs the shell to resolve. + shell: process.platform === "win32", + }); + if (result.error) throw result.error; + return result.status ?? 1; +} + +function listProgramFiles(config, { cwd, env }) { + const tsc = path.join(cwd, "node_modules", "typescript", "bin", "tsc"); + const result = spawnSync(process.execPath, [tsc, "-p", config, "--listFilesOnly"], { + cwd, + env, + encoding: "utf8", + maxBuffer: 1 << 28, + }); + if (result.error) throw result.error; + return { + status: result.status ?? 1, + output: result.stdout, + files: result.stdout + .split("\n") + .filter(Boolean) + .map((file) => path.relative(cwd, file).split(path.sep).join("/")), + }; +} + +function listWorkspaceConfigs(root) { + const result = spawnSync("pnpm", ["ls", "-r", "--depth", "-1", "--json"], { + cwd: root, + encoding: "utf8", + maxBuffer: 1 << 26, + shell: process.platform === "win32", + }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error("Unable to list pnpm workspace packages."); + return JSON.parse(result.stdout) + .map((workspace) => path.relative(root, path.join(workspace.path, "tsconfig.json")).split(path.sep).join("/")) + .filter((config) => config !== "server/tsconfig.json" && existsSync(path.join(root, config))); +} + +function listTrackedFiles(root) { + const result = spawnSync("git", ["ls-files", "-z"], { cwd: root, encoding: "utf8", maxBuffer: 1 << 28 }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error("Unable to list tracked files."); + return result.stdout.split("\0").filter(Boolean); +} + +/** Returns the test files in no program, or null if a program could not be listed. */ +export function checkCoverage({ + root, + env = process.env, + programs = PROGRAMS, + workspaceConfigs = listWorkspaceConfigs, + trackedFiles = listTrackedFiles, + listFiles = listProgramFiles, + logError = console.error, +}) { + const checkedFiles = []; + // `pnpm build` and `typecheck:build-gaps` check each workspace's tsconfig.json. + // scripts/typecheck-test-projects.mjs checks these same files through tsconfig.layers.json. + const projectConfigs = PROJECTS.map((project) => `${project}/tsconfig.json`); + for (const config of [...workspaceConfigs(root), ...projectConfigs, ...programs]) { + const listing = listFiles(config, { cwd: root, env }); + if (listing.status !== 0) { + logError(`typecheck:test-programs: could not list the files of ${config}\n${listing.output ?? ""}`); + return null; + } + checkedFiles.push(...listing.files); + } + return findUncheckedTests({ trackedFiles: trackedFiles(root), checkedFiles }); +} + +export function typecheckTestPrograms({ + root, + env = process.env, + programs = PROGRAMS, + coverage = true, + pnpm = runPnpm, + linkPlugins = linkExcludedPlugins, + run = runTsc, + checkFiles = checkCoverage, + log = console.log, + logError = console.error, +}) { + for (const args of PREPARE_STEPS) { + const status = pnpm(args, { cwd: root, env }); + if (status !== 0) return status; + } + const installed = pnpm(INSTALL_ARGS, { cwd: path.join(root, PROVIDER_WORKSPACE), env }); + if (installed !== 0) { + logError( + `typecheck:test-programs: if a provider's package.json changed, run ` + + `\`pnpm install --ignore-scripts --lockfile-only\` in ${PROVIDER_WORKSPACE} and commit its pnpm-lock.yaml`, + ); + return installed; + } + // The install removes the SDK link from each provider's node_modules. + linkPlugins(); + + let exitCode = 0; + for (const config of programs) { + log(`typecheck:test-programs: ${config}`); + const { status, signal } = run(["-p", config], { cwd: root, env }); + if (signal) return status || 1; + if (status !== 0) exitCode = status; + } + if (!coverage) return exitCode; + + const unchecked = checkFiles({ root, env, logError }); + if (unchecked === null) return exitCode || 1; + if (unchecked.length > 0) { + logError( + `typecheck:test-programs: ${unchecked.length} test files are in no typechecked program. ` + + `Add them to a program in scripts/typecheck-test-programs.mjs. A foo.test.tsx beside a foo.test.ts needs a new name.\n` + + unchecked.map((file) => ` ${file}`).join("\n"), + ); + return exitCode || 1; + } + return exitCode; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const args = process.argv.slice(2); + const flags = args.filter((arg) => arg.startsWith("--")); + const programs = args.filter((arg) => !arg.startsWith("--")); + const unknownFlags = flags.filter((flag) => flag !== "--low-memory"); + const unknown = programs.filter((program) => !PROGRAMS.includes(program)); + if (unknownFlags.length > 0) { + console.error(`Unknown option: ${unknownFlags.join(", ")}`); + process.exitCode = 2; + } else if (unknown.length > 0) { + console.error(`Unknown program: ${unknown.join(", ")}. Known programs:\n ${PROGRAMS.join("\n ")}`); + process.exitCode = 2; + } else { + process.exitCode = typecheckTestPrograms({ + root: path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), + env: flags.includes("--low-memory") ? { ...process.env, [LOW_MEMORY_VARIABLE]: "1" } : process.env, + programs: programs.length > 0 ? programs : PROGRAMS, + coverage: programs.length === 0, + }); + } +} diff --git a/scripts/typecheck-test-programs.test.mjs b/scripts/typecheck-test-programs.test.mjs new file mode 100644 index 00000000000..52816e4fbdb --- /dev/null +++ b/scripts/typecheck-test-programs.test.mjs @@ -0,0 +1,170 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; +import { + checkCoverage, + findUncheckedTests, + PROGRAMS, + PROVIDER_WORKSPACE, + SANDBOX_PROVIDERS, + typecheckTestPrograms, +} from "./typecheck-test-programs.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const tsc = path.join(root, "node_modules", "typescript", "bin", "tsc"); + +const harness = (overrides = {}) => { + const calls = { pnpm: [], tsc: [], linked: 0, errors: [] }; + return { + calls, + options: { + root: "/repo", + programs: ["a/tsconfig.test.json", "b/tsconfig.test.json"], + pnpm: (args, { cwd }) => { + calls.pnpm.push({ args, cwd }); + return 0; + }, + linkPlugins: () => { + calls.linked += 1; + }, + run: (args) => { + calls.tsc.push(args); + return { status: 0 }; + }, + checkFiles: () => [], + log: () => {}, + logError: (message) => calls.errors.push(message), + ...overrides, + }, + }; +}; + +test("findUncheckedTests reports only test files outside programs and other checks", () => { + assert.deepEqual( + findUncheckedTests({ + trackedFiles: ["a/x.test.ts", "a/y.spec.tsx", "a/z.test.tsx", "a/helper.ts", "a/n.test.mjs", "server/src/s.test.ts"], + checkedFiles: ["a/x.test.ts", "a/y.spec.tsx"], + checkedElsewhere: ["server/src/"], + }), + ["a/z.test.tsx"], + ); +}); + +test("every program exists, and the provider list matches the tracked provider packages", () => { + for (const program of PROGRAMS) assert.ok(existsSync(path.join(root, program)), program); + assert.ok(existsSync(path.join(root, PROVIDER_WORKSPACE, "pnpm-lock.yaml"))); + const tracked = execFileSync("git", ["ls-files", "packages/plugins/sandbox-providers"], { cwd: root, encoding: "utf8" }); + const manifests = tracked.split("\n").filter((file) => file.endsWith("/package.json")); + assert.deepEqual(manifests.map((manifest) => path.dirname(manifest)).sort(), [...SANDBOX_PROVIDERS].sort()); +}); + +test("no provider commits a lockfile, except the one Daytona already has", () => { + const tracked = execFileSync("git", ["ls-files", "packages/plugins/sandbox-providers"], { cwd: root, encoding: "utf8" }); + assert.deepEqual( + tracked.split("\n").filter((file) => file.endsWith("/pnpm-lock.yaml")), + ["packages/plugins/sandbox-providers/daytona/pnpm-lock.yaml"], + ); +}); + +test("a type error in one program still checks the rest and fails the run", () => { + const { options, calls } = harness({ + run: (args) => { + calls.tsc.push(args); + return { status: args[1].startsWith("a/") ? 2 : 0 }; + }, + }); + assert.equal(typecheckTestPrograms(options), 2); + assert.deepEqual(calls.tsc, [ + ["-p", "a/tsconfig.test.json"], + ["-p", "b/tsconfig.test.json"], + ]); +}); + +test("a killed compiler stops the run", () => { + const { options, calls } = harness({ + run: (args) => { + calls.tsc.push(args); + return { status: null, signal: "SIGKILL" }; + }, + }); + assert.equal(typecheckTestPrograms(options), 1); + assert.equal(calls.tsc.length, 1); +}); + +test("providers install frozen without scripts, then relink the SDK", () => { + const { options, calls } = harness(); + assert.equal(typecheckTestPrograms(options), 0); + const install = calls.pnpm.find(({ cwd }) => cwd === path.join("/repo", PROVIDER_WORKSPACE)); + assert.ok(install.args.includes("--frozen-lockfile")); + assert.ok(install.args.includes("--ignore-scripts")); + assert.equal(calls.linked, 1); +}); + +test("a stale provider lockfile fails before any program runs and names the fix", () => { + const { options, calls } = harness({ + pnpm: (args, { cwd }) => (cwd === path.join("/repo", PROVIDER_WORKSPACE) ? 1 : 0), + }); + assert.equal(typecheckTestPrograms(options), 1); + assert.equal(calls.tsc.length, 0); + assert.match(calls.errors[0], /pnpm-lock\.yaml/); +}); + +test("an uncovered test file fails the run and is named", () => { + const { options, calls } = harness({ checkFiles: () => ["a/z.test.tsx"] }); + assert.equal(typecheckTestPrograms(options), 1); + assert.match(calls.errors[0], /a\/z\.test\.tsx/); +}); + +test("the coverage check fails closed when a program cannot be listed", () => { + const { options } = harness({ checkFiles: () => null }); + assert.equal(typecheckTestPrograms(options), 1); +}); + +test("naming programs skips the coverage check", () => { + let checked = false; + const { options } = harness({ + coverage: false, + checkFiles: () => { + checked = true; + return []; + }, + }); + assert.equal(typecheckTestPrograms(options), 0); + assert.equal(checked, false); +}); + +test("checkCoverage uses the compiler's file list, so a dropped sibling is caught", () => { + const directory = mkdtempSync(path.join(root, "node_modules", ".typecheck-test-programs-")); + try { + const relative = path.relative(root, directory).split(path.sep).join("/"); + writeFileSync(path.join(directory, "foo.test.ts"), "export {};\n"); + writeFileSync(path.join(directory, "foo.test.tsx"), "export {};\n"); + writeFileSync(path.join(directory, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true, jsx: "preserve" }, include: ["*"] })); + const unchecked = checkCoverage({ + root, + programs: [`${relative}/tsconfig.json`], + workspaceConfigs: () => [], + trackedFiles: () => [`${relative}/foo.test.ts`, `${relative}/foo.test.tsx`], + }); + assert.deepEqual(unchecked, [`${relative}/foo.test.tsx`]); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test("a real type error in a test-style program makes tsc exit non-zero", () => { + const directory = mkdtempSync(path.join(tmpdir(), "typecheck-test-programs-")); + try { + writeFileSync(path.join(directory, "bad.test.ts"), "export const value: number = 'text';\n"); + writeFileSync(path.join(directory, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true, types: [] }, include: ["*.ts"] })); + const { status, stdout } = spawnSync(process.execPath, [tsc, "-p", directory, "--pretty", "false"], { encoding: "utf8" }); + assert.notEqual(status, 0); + assert.match(stdout, /bad\.test\.ts.*TS2322/); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/scripts/typecheck-test-projects.mjs b/scripts/typecheck-test-projects.mjs index 8fd8cef014f..0c6d537b216 100644 --- a/scripts/typecheck-test-projects.mjs +++ b/scripts/typecheck-test-projects.mjs @@ -22,6 +22,7 @@ import { isTestFile, typecheckLayers } from "../server/scripts/typecheck-layers. export const PROJECTS = [ "server/src/__tests__/helpers/maintenance", + "tests/e2e", "tests/lifecycle-baseline", "tests/runner-e2e", "tests/speko-native", diff --git a/server/src/__tests__/issue-recovery-actions.test.ts b/server/src/__tests__/issue-recovery-actions.test.ts index e8e4364fe22..b6896998808 100644 --- a/server/src/__tests__/issue-recovery-actions.test.ts +++ b/server/src/__tests__/issue-recovery-actions.test.ts @@ -99,6 +99,7 @@ describe("issueRecoveryActionService", () => { limit() { return Promise.resolve([]); } }; const fakeDb = { async transaction(fn: (tx: unknown) => Promise): Promise { return fn(fakeDb); }, + execute: vi.fn(async () => [{ pendingPreparationContinuation: false }]), select: vi.fn(() => query), insert: vi.fn(() => ({ values: () => ({ returning: async () => { throw error; } }) })), }; @@ -130,6 +131,7 @@ describe("issueRecoveryActionService", () => { const fakeDb = { async transaction(fn: (tx: unknown) => Promise): Promise { return fn(fakeDb); }, + execute: vi.fn(async () => [{ pendingPreparationContinuation: false }]), select: vi.fn(() => makeSelectQuery(selectResults.shift() ?? [])), update: vi.fn(() => ({ set: vi.fn(() => ({ diff --git a/server/src/modules/run-dispatch/adapters/postgres.test.ts b/server/src/modules/run-dispatch/adapters/postgres.test.ts index 6310be9020f..6e99a1aeb98 100644 --- a/server/src/modules/run-dispatch/adapters/postgres.test.ts +++ b/server/src/modules/run-dispatch/adapters/postgres.test.ts @@ -20,6 +20,7 @@ import { environmentLeases, heartbeatRunEvents, heartbeatRuns, + issueApprovals, issueDocuments, issueRelations, issueRecoveryActions, @@ -379,6 +380,33 @@ describeEmbeddedPostgres("run-dispatch postgres adapter", () => { expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]!.status).toBe("blocked"); }); + it("keeps recovered preparation behind approvals added after scheduling", async () => { + const { companyId, agentId } = await seedCompanyAndAgent(); + const issueId = randomUUID(); + await seedIssue({ companyId, issueId, assigneeAgentId: agentId, status: "in_progress" }); + const retryReason = "interrupted_preparation"; + const replacementId = await seedRun({ companyId, agentId, status: "scheduled_retry", + contextSnapshot: { issueId, wakeReason: retryReason, retryReason, forceFreshSession: true } }); + const adapter = createPostgresRunDispatchAdapter(db); + expect(await adapter.evaluateScheduledRetryGate({ companyId, runId: replacementId, retryReasonOverride: retryReason, now: new Date() })) + .toMatchObject({ allowed: true }); + const approvalId = randomUUID(); + await db.insert(approvals).values({ id: approvalId, companyId, type: "hire_agent", status: "pending", payload: {} }); + await db.insert(issueApprovals).values({ companyId, issueId, approvalId }); + expect(await adapter.evaluateScheduledRetryGate({ companyId, runId: replacementId, retryReasonOverride: retryReason, now: new Date() })) + .toMatchObject({ allowed: false, errorCode: "issue_waiting_for_response" }); + await db.update(heartbeatRuns).set({ status: "queued" }).where(eq(heartbeatRuns.id, replacementId)); + const reservation = await reserveRunBudget(db, companyId, replacementId, null); + expect(await adapter.cancelStaleQueuedRun({ companyId, runId: replacementId, expectedStatus: "queued", now: new Date() })) + .toMatchObject({ outcome: "cancelled", errorCode: "issue_waiting_for_response" }); + await db.update(heartbeatRuns).set({ status: "running" }).where(eq(heartbeatRuns.id, replacementId)); + const dispatch = vi.fn(async () => undefined); + expect(await adapter.dispatchResolvedInteractionIfCurrent({ companyId, runId: replacementId, + reservationId: reservation.id, expectedStatus: "running", now: new Date(), dispatch })) + .toMatchObject({ dispatched: false, cancellation: { outcome: "cancelled" } }); + expect(dispatch).not.toHaveBeenCalled(); + }); + it.each(["queued", "final", "resolved"] as const)("rechecks late native replacement dependencies at %s dispatch", async mode => { const { companyId, agentId } = await seedCompanyAndAgent(); const issueId = randomUUID(), blockerId = randomUUID(); diff --git a/server/src/modules/run-dispatch/adapters/postgres.ts b/server/src/modules/run-dispatch/adapters/postgres.ts index d3a038b7468..076fc046037 100644 --- a/server/src/modules/run-dispatch/adapters/postgres.ts +++ b/server/src/modules/run-dispatch/adapters/postgres.ts @@ -417,7 +417,7 @@ export function createPostgresRunDispatchAdapter( facts.issueAssigneeAgentId = issue.assigneeAgentId; facts.issueExecutionRunId = issue.executionRunId; facts.issueCheckoutRunId = issue.checkoutRunId; - if (input.conversationContinuation) { + if (input.conversationContinuation || retryReason === "interrupted_preparation") { facts.pendingResponse = await connectionContinuationPendingResponse(dbOrTx as unknown as Db, issue, input.responsibleUserId, input.runId); } @@ -657,7 +657,7 @@ export function createPostgresRunDispatchAdapter( unresolvedBlockerIssueIds: readiness.unresolvedBlockerIssueIds, unresolvedBlockerCount: readiness.unresolvedBlockerCount, } : null, - pendingResponse: issue && (input.conversationContinuation || context.connectionIntentResolution === "existing_connection") + pendingResponse: issue && (input.conversationContinuation || retryReason === "interrupted_preparation" || context.connectionIntentResolution === "existing_connection") ? await connectionContinuationPendingResponse(dbOrTx as unknown as Db, issue, input.responsibleUserId, input.runId) : null, issueFound: issue !== null, diff --git a/server/src/services/cancelled-native-startup.ts b/server/src/services/cancelled-native-startup.ts index 9b147c04dc3..d151d2020b5 100644 --- a/server/src/services/cancelled-native-startup.ts +++ b/server/src/services/cancelled-native-startup.ts @@ -103,14 +103,18 @@ function hasOnlyPreparationAccountingContext(usage: unknown, issueId: unknown): * stopped leases, and any saved-file obligation below. */ export function isInterruptedNativePreparationCandidate(run: Run): boolean { const cancellation = readRunCancellation(run.resultJson); + const recovery = run.resultJson?.executionRecovery as Record | undefined; + // Restart recovery records this certificate only after revoking a preparing + // controller. A generic process-lost error cannot establish safe replay. + const lostPreparation = run.status === "failed" && run.errorCode === "process_lost" + && recovery?.kind === "bootstrap" && recovery.providerWorkStarted === false; // Older shutdown records predate structured cancellation. Current records // identify unexpected system interruption without relying on an error label. const systemInterruption = cancellation ? cancellation.initiator.type === "system" && cancellation.source !== "operator" && (cancellation.source === "shutdown" || cancellation.expected === false) - : run.errorCode === "server_shutdown_interrupted"; - const recovery = run.resultJson?.executionRecovery as Record | undefined; - return Boolean(systemInterruption && run.status === "interrupted" && run.finishedAt + : run.errorCode === "server_shutdown_interrupted" || lostPreparation; + return Boolean(systemInterruption && (run.status === "interrupted" || lostPreparation) && run.finishedAt && run.runtimeMode === "legacy" && !run.runtimeModeResolvedAt && run.executionStage === "preparing" && claimedAdapterType(run) === "paperclip_runner" && !run.processPid && !run.processGroupId && !run.processStartedAt && !run.sessionIdAfter && !run.nativeIssueId && !run.nativeSessionId diff --git a/server/src/services/deferred-execution-notifications.ts b/server/src/services/deferred-execution-notifications.ts index 89f7027e9fb..1207e59b63a 100644 --- a/server/src/services/deferred-execution-notifications.ts +++ b/server/src/services/deferred-execution-notifications.ts @@ -1,3 +1,5 @@ +import type { Db } from "@paperclipai/db"; +import { sql } from "drizzle-orm"; import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notifications.js"; export type DeferredExecutionScope = { companyId: string; requestId?: string }; @@ -24,6 +26,17 @@ export async function notifyTaskRetrySettlement(tx: object, scope: DeferredExecu } export async function notifyDeferredExecutionEligibility(tx: object, scope?: { companyId: string }) { + // Only an actual continuation obligation may wake this passive worker. + // Empty eligibility hints during idle drain must not keep maintenance active. + const pending = await (tx as Pick).execute<{ pendingPreparationContinuation: boolean }>(sql` + select exists (select 1 from issue_recovery_actions + where evidence ? 'systemRecovery' and evidence#>>'{systemRecovery,state}' = 'complete' + and evidence->>'bootstrapPreparationSettledAt' is not null + and coalesce(evidence->>'bootstrapPreparationContinuation', 'pending') in ('pending', 'deferred') + ${scope ? sql`and company_id = ${scope.companyId}::uuid` : sql``} + ) as "pendingPreparationContinuation"`); + if (pending[0]?.pendingPreparationContinuation === true) + await notifyDeliveryWork(tx, DELIVERY_QUEUES.workspaceRecovery, scope ? JSON.stringify({ companyId: scope.companyId }) : "*"); await notifyDeliveryWork(tx, DELIVERY_QUEUES.nativeProcessSessions, scope ? JSON.stringify({ companyId: scope.companyId }) : "*"); if (scope) { await notifyDeferredExecutionWork(tx, scope); diff --git a/server/src/services/heartbeat/retries.ts b/server/src/services/heartbeat/retries.ts index 8ca56a7a8ba..5f7c5dcde03 100644 --- a/server/src/services/heartbeat/retries.ts +++ b/server/src/services/heartbeat/retries.ts @@ -1,3 +1,5 @@ +import { isInterruptedNativePreparation } from "../cancelled-native-startup.js"; +import { isPreDispatchExecutionWaitVerified } from "../pre-dispatch-execution-wait.js"; import { notifyAttentionRetentionWork } from "../retention-work-notifications.js"; import { normalizeAgentNameKey } from "../agent-name-key.js"; import { notifyStatusCardIssueChange } from "../status-card-notifications.js"; @@ -430,6 +432,20 @@ export function createHeartbeatRetries(db: Db, dependencies: HeartbeatRetryDepen delayMs?: number; }, ) { + const preparation = parseObject(run.resultJson?.executionRecovery); + if (typeof preparation.preparationSettledAt === "string" && await isInterruptedNativePreparation(db, run)) { + const children = await db.select().from(heartbeatRuns).where(and( + eq(heartbeatRuns.companyId, run.companyId), eq(heartbeatRuns.agentId, run.agentId), + eq(heartbeatRuns.retryOfRunId, run.id), + )).orderBy(desc(heartbeatRuns.createdAt), desc(heartbeatRuns.id)).limit(2); + const withheld = children.length === 1 ? children[0] : undefined; + if (withheld && await isPreDispatchExecutionWaitVerified(db, withheld)) { + // The first replacement was withheld before execution while files were + // recovered. Preserve its history, budget and predecessor claim. + run = withheld; + opts = { ...opts, retryReason: opts?.retryReason ?? "interrupted_preparation" }; + } + } if (!opts?.retryReason && run.errorCode === RUNNER_REVISION_MISMATCH_CODE) opts = { ...RUNNER_REVISION_MISMATCH_RETRY, ...opts }; if (await hasCaseHandoffForRun(run)) return { outcome: "not_scheduled" as const, reason: "The durable case handoff owns successor work.", issueId: readNonEmptyString(run.contextSnapshot?.issueId) }; @@ -449,8 +465,9 @@ export function createHeartbeatRetries(db: Db, dependencies: HeartbeatRetryDepen } const now = opts?.now ?? new Date(); const preparationRecovery = parseObject(run.resultJson?.executionRecovery); - const interruptedPreparation = run.status === "interrupted" && - preparationRecovery.kind === "bootstrap" && preparationRecovery.providerWorkStarted === false; + const interruptedPreparation = ["interrupted", "failed"].includes(run.status) && + preparationRecovery.kind === "bootstrap" && preparationRecovery.providerWorkStarted === false + && (run.status === "interrupted" || await isInterruptedNativePreparation(db, run)); const retryReason = opts?.retryReason ?? (interruptedPreparation ? "interrupted_preparation" : BOUNDED_TRANSIENT_HEARTBEAT_RETRY_REASON); const wakeReason = @@ -590,7 +607,7 @@ export function createHeartbeatRetries(db: Db, dependencies: HeartbeatRetryDepen : baseSchedule; const requiresIssueGate = - interruptedPreparation || + interruptedPreparation || retryReason === "interrupted_preparation" || (run.status === "interrupted" && run.errorCode === "server_shutdown_interrupted") || isTransientWorkspacePreparationCode(run.errorCode) || hasConversationContinuationPolicy(run.resultJson) || @@ -766,6 +783,7 @@ export function createHeartbeatRetries(db: Db, dependencies: HeartbeatRetryDepen eq(heartbeatRuns.retryOfRunId, run.id), ), ) + .orderBy(desc(heartbeatRuns.createdAt), desc(heartbeatRuns.id)) .limit(1); if (existing) return { diff --git a/server/src/services/heartbeat/run-control.ts b/server/src/services/heartbeat/run-control.ts index 8c61d48fbbd..7bacf12d149 100644 --- a/server/src/services/heartbeat/run-control.ts +++ b/server/src/services/heartbeat/run-control.ts @@ -55,7 +55,7 @@ import { undeliveredLegacyUserCommentIds, } from "../explicit-native-continuation.js"; import { isCancelledNativeStartup, isInterruptedNativePreparation } from "../cancelled-native-startup.js"; -import { settleInterruptedNativeBootstrap } from "../legacy-execution-recovery.js"; +import { settleInterruptedNativeBootstrap, recordInterruptedPreparationContinuation } from "../legacy-execution-recovery.js"; import { executionBlockerPredicate, getExecutionBlocker, @@ -573,7 +573,7 @@ export function createHeartbeatRunControl(db: Db, dependencies: HeartbeatRunCont async function resumeRemoteStopComments(run: NonNullable>>, requestId?: string) { if (!isHeartbeatRunTerminalStatus(run.status) || adapterExecutionControls.has(run.id)) return; let interruptedPreparation = false; - if (run.status === "interrupted" && run.runtimeMode === "legacy" && + if (["interrupted", "failed"].includes(run.status) && run.runtimeMode === "legacy" && !run.runtimeModeResolvedAt && run.executionStage === "preparing") { const current = await getRun(run.id, { includeExecutionEvidence: true }); if (current && await isInterruptedNativePreparation(db, current)) { @@ -617,12 +617,20 @@ export function createHeartbeatRunControl(db: Db, dependencies: HeartbeatRunCont requestId ? eq(agentWakeupRequests.id, requestId) : undefined, sql`${agentWakeupRequests.payload}->>'issueId' = ${issueId}`, )).orderBy(asc(agentWakeupRequests.requestedAt)).limit(50); - if (interruptedPreparation && !pending.length && !requestId && !(await getSchedulingSuppression()).suppressed) { - const [task] = await db.select({ status: issues.status, assigneeAgentId: issues.assigneeAgentId }).from(issues).where(and( - eq(issues.companyId, run.companyId), eq(issues.id, issueId), - )); - if (task?.assigneeAgentId === run.agentId && ["todo", "in_progress", "in_review"].includes(task.status)) { - await dependencies.scheduleBoundedRetry(run.id); + if (interruptedPreparation) { + if (pending.length || requestId) await recordInterruptedPreparationContinuation(db, run, "delegated"); + else { + const [task] = await db.select({ status: issues.status, assigneeAgentId: issues.assigneeAgentId }).from(issues).where(and( + eq(issues.companyId, run.companyId), eq(issues.id, issueId), + )); + if (!(await getSchedulingSuppression()).suppressed && task?.assigneeAgentId === run.agentId && + ["todo", "in_progress", "in_review"].includes(task.status)) { + const retry = await dependencies.scheduleBoundedRetry(run.id); + await recordInterruptedPreparationContinuation(db, run, + retry.outcome === "scheduled" ? retry.run.status === "cancelled" ? "blocked" : "delivered" + : retry.outcome === "retry_exhausted" ? "exhausted" : "deferred", + retry.outcome === "scheduled" ? retry.run.id : undefined); + } else await recordInterruptedPreparationContinuation(db, run, "deferred"); } } for (const wake of pending) { diff --git a/server/src/services/legacy-execution-recovery.ts b/server/src/services/legacy-execution-recovery.ts index 2fa69a533d3..32698e7d6a8 100644 --- a/server/src/services/legacy-execution-recovery.ts +++ b/server/src/services/legacy-execution-recovery.ts @@ -1,3 +1,4 @@ +import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notifications.js"; import { notifyNativeStatusReassessment } from "./native-status-reassessment-notifications.js"; import { notifyUnsafeWorkspaceExportRecovery } from "./unsafe-workspace-export-notifications.js"; import { notifyInstructionCollection } from "./instruction-collection-notifications.js"; @@ -96,7 +97,7 @@ export function legacyExecutionNeedsReconciliation( /** Admission-wait receipts are only exempt after retained execution evidence agrees. * The synchronous classifier stays conservative for callers without a DB proof. */ export async function legacyExecutionNeedsReconciliationWithEvidence(db: Db, run: Run): Promise { - if (run.status === "interrupted" && await isInterruptedNativePreparation(db, run)) return false; + if (await isInterruptedNativePreparation(db, run)) return false; return await hasUnrestoredRemoteWorkspace(db, run) || (legacyExecutionNeedsReconciliation(run) && !(await isPreDispatchReviewWaitVerified(db, run)) && !(await isPreDispatchExecutionWaitVerified(db, run))); @@ -369,14 +370,17 @@ export async function settleInterruptedNativeBootstrap( const [settled] = await tx.update(heartbeatRuns).set({ resultJson: { ...run.resultJson, executionRecovery: { kind: "bootstrap", providerWorkStarted: false, preparationSettledAt: now.toISOString() }, }, updatedAt: now }).where(eq(heartbeatRuns.id, run.id)).returning(); - if (holds.length) await notifyUnsafeWorkspaceExportRecovery(tx, { companyId: task.companyId, issueId: task.id }); + if (holds.length) { + await notifyUnsafeWorkspaceExportRecovery(tx, { companyId: task.companyId, issueId: task.id }); + await notifyDeliveryWork(tx, DELIVERY_QUEUES.workspaceRecovery); + } const resolved = holds.length ? await tx.update(issueRecoveryActions).set({ status: "resolved", outcome: "false_positive", resolutionNote: "The interrupted preparation has no provider execution and its cleanup is complete.", resolvedAt: now, updatedAt: now, // An automatic disposition may already have resolved the incident but // left replay blocked. Preserve its history while correcting that verdict. evidence: sql`jsonb_set(${issueRecoveryActions.evidence}, '{automaticRecovery,replay}', '"allowed"'::jsonb, false) || - ${JSON.stringify({ bootstrapPreparationSettledAt: now.toISOString() })}::jsonb`, + ${JSON.stringify({ bootstrapPreparationSettledAt: now.toISOString(), bootstrapPreparationContinuation: "pending" })}::jsonb`, }).where(inArray(issueRecoveryActions.id, holds.map(hold => hold.id))).returning({ id: issueRecoveryActions.id }) : []; if (resolved.length) await logActivity(tx as unknown as Db, { companyId: run.companyId, actorType: "system", actorId: "system", action: "issue.recovery_action_resolved", entityType: "issue", entityId: task.id, agentId: run.agentId, runId: run.id, @@ -386,6 +390,29 @@ export async function settleInterruptedNativeBootstrap( }); } +/** The source stays discoverable until normal admission records a successor. + * Known admission gates wait for eligibility events; unresolved writes retain + * the pending delivery deadline. Concurrent successful delivery never regresses. */ +export async function recordInterruptedPreparationContinuation(db: Db, run: Pick, + state: "deferred" | "delivered" | "delegated" | "exhausted" | "blocked", successorRunId?: string) { + return db.transaction(async tx => { + const actions = await tx.select().from(issueRecoveryActions).where(and( + eq(issueRecoveryActions.companyId, run.companyId), eq(issueRecoveryActions.fingerprint, `legacy-execution:${run.id}`), + sql`${issueRecoveryActions.evidence}->>'runId' = ${run.id}`, + sql`${issueRecoveryActions.evidence}->>'bootstrapPreparationSettledAt' is not null`, + )).for("update"); + const pending = actions.filter(action => + !["delivered", "delegated", "exhausted", "blocked"].includes(String(action.evidence.bootstrapPreparationContinuation)) + && action.evidence.bootstrapPreparationContinuation !== state); + if (!pending.length) return; + await notifyDeliveryWork(tx, DELIVERY_QUEUES.workspaceRecovery); + for (const action of pending) await tx.update(issueRecoveryActions).set({ updatedAt: new Date(), evidence: { + ...action.evidence, bootstrapPreparationContinuation: state, + ...(successorRunId ? { bootstrapPreparationContinuationRunId: successorRunId } : {}), + } }).where(eq(issueRecoveryActions.id, action.id)); + }); +} + /** * The executor that owned a cancelled legacy run proves that its gated * dispatch never entered the adapter. Such a Stop (for example a subtree pause diff --git a/server/src/services/remote-workspace-transport.ts b/server/src/services/remote-workspace-transport.ts index 3e821827f68..59df98adf52 100644 --- a/server/src/services/remote-workspace-transport.ts +++ b/server/src/services/remote-workspace-transport.ts @@ -398,9 +398,29 @@ export async function cleanupRemoteWorkspaceArchive(input: { await command(input.target, "cleanup", ["-e", script, input.remoteCwd, input.operationId], undefined, 10_000); } +/** + * Wall-clock milliseconds for each save phase, filled in as each phase ends so a + * failed save still reports the phases it finished. It holds only durations and + * archive counts, never paths or provider text. + */ +export type RemoteWorkspaceSavePhaseTiming = { + /** Archive helper install plus in-sandbox capture. */ + captureMs?: number; + /** Provider download of the captured archive. */ + transferMs?: number; + /** Server-side re-hash of the downloaded archive. */ + verifyMs?: number; + byteSize?: number; + entries?: number; +}; + +const elapsedSince = (started: number) => Math.max(0, Math.round(performance.now() - started)); + export async function captureRemoteWorkspaceArchive(input: { - target: RemoteWorkspaceTarget; remoteCwd: string; operationId: string; + target: RemoteWorkspaceTarget; remoteCwd: string; operationId: string; timing?: RemoteWorkspaceSavePhaseTiming; }) { + const timing = input.timing ?? {}; + const captureStarted = performance.now(); const staged = await installArchiveHelper(input.target, input.remoteCwd, input.operationId); // Publish the archive and receipt as one immutable directory. Concurrent // invocations own separate attempts; no invocation unlinks another's archive. @@ -444,14 +464,20 @@ export async function captureRemoteWorkspaceArchive(input: { }; if (!["legacy", "captured"].includes(layout) || !/^[a-f0-9]{64}$/.test(receipt.manifestDigest) || !/^[a-f0-9]{64}$/.test(receipt.sha256) || !Number.isSafeInteger(receipt.byteSize) || receipt.byteSize <= 0) throw new Error("remote_workspace_capture_receipt_invalid"); + timing.captureMs = elapsedSince(captureStarted); timing.byteSize = receipt.byteSize; + if (Number.isSafeInteger(receipt.entries) && receipt.entries >= 0) timing.entries = receipt.entries; const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-workspace-upload-")); const archivePath = path.join(root, "workspace.pcws"); try { + const transferStarted = performance.now(); await transferArchive(input.target, "syncOut", [{ operationId: input.operationId, files: [{ kind: "file", sourcePath: layout === "captured" ? path.posix.join(staged.directory, "captured", "workspace.pcws") : staged.archive, targetPath: archivePath }], }], receipt.byteSize); + timing.transferMs = elapsedSince(transferStarted); + const verifyStarted = performance.now(); const hash = createHash("sha256"); let bytes = 0; for await (const chunk of createReadStream(archivePath)) { hash.update(chunk); bytes += chunk.length; } + timing.verifyMs = elapsedSince(verifyStarted); if (hash.digest("hex") !== receipt.sha256 || bytes !== receipt.byteSize) throw new Error("remote_workspace_capture_transfer_mismatch"); return { ...receipt, archivePath, cleanup: () => rm(root, { recursive: true, force: true }) }; } catch (error) { diff --git a/server/src/services/remote-workspaces.integration.test.ts b/server/src/services/remote-workspaces.integration.test.ts index 7b8e13bf206..8c46f3edb43 100644 --- a/server/src/services/remote-workspaces.integration.test.ts +++ b/server/src/services/remote-workspaces.integration.test.ts @@ -1115,9 +1115,26 @@ suite("remote workspace lifecycle with real Postgres and independent filesystem expect(readRemoteWorkspaceAuthority((await workspace(reserved.id)).metadata)).toMatchObject({ dirty: true, writerRunId: ids.run, snapshotId: readRemoteWorkspaceAuthority(prepared.workspace.metadata)!.snapshotId }); expect((await fs.readdir(path.join(prepared.remoteRoot, ".paperclip-runtime", "workspace-snapshots"))).length).toBe(1); - const saved = await saveRemoteWorkspace({ db, storage, workspace: await workspace(reserved.id), runId: ids.run, + // Each settled save records where its time went: the failed upload keeps the + // phases it finished, and the retry of the same boundary records every phase. + const finalTiming = async () => (await db.select().from(workspaceOperations).where(and( + eq(workspaceOperations.executionWorkspaceId, reserved.id), eq(workspaceOperations.phase, "remote_workspace_snapshot"), + sql`${workspaceOperations.metadata}->>'boundary' = 'final'`))).map(save => save.metadata?.saveTiming); + const duration = expect.any(Number), archive = { byteSize: expect.any(Number), entries: expect.any(Number) }; + expect(await finalTiming()).toEqual([{ schema: "paperclip.remote-workspace-save-timing.v1", outcome: "failed", totalMs: duration, + captureMs: duration, transferMs: duration, verifyMs: duration, ...archive, at: expect.any(String) }]); + const beforeSuccess = await workspace(reserved.id); + const saved = await saveRemoteWorkspace({ db, storage, workspace: beforeSuccess, runId: ids.run, lease: prepared.lease, target: first, assertOwned: async () => undefined }); expect(saved.generation).toBe(2); + const [succeededTiming] = await finalTiming(); + // A lost-reply replay of the committed save keeps the measured record. + await expect(saveRemoteWorkspace({ db, storage, workspace: beforeSuccess, runId: ids.run, + lease: prepared.lease, target: first, assertOwned: async () => undefined })).resolves.toMatchObject({ snapshotId: saved.snapshotId }); + expect(await finalTiming()).toEqual([succeededTiming]); + expect(await finalTiming()).toEqual([{ schema: "paperclip.remote-workspace-save-timing.v1", outcome: "succeeded", totalMs: duration, + captureMs: duration, transferMs: duration, verifyMs: duration, uploadMs: duration, publishMs: duration, ...archive, + at: expect.any(String) }]); expect(await fs.readdir(path.join(prepared.remoteRoot, ".paperclip-runtime", "workspace-snapshots"))).toEqual([]); }, 60_000); @@ -1224,9 +1241,12 @@ suite("remote workspace lifecycle with real Postgres and independent filesystem .rejects.toThrow("remote_workspace_snapshot_operation_binding_changed"); expect(upload).not.toHaveBeenCalled(); const [held] = await db.select().from(workspaceOperations).where(eq(workspaceOperations.id, poisoned!.id)); - const { saveWork, ...originalMetadata } = held!.metadata!; + const { saveWork, saveTiming, ...originalMetadata } = held!.metadata!; expect(originalMetadata).toEqual(poisoned!.metadata); expect(saveWork).toMatchObject({ activeIds: [], unknown: true, quiescedAt: expect.any(String) }); + // The refused invocation records only its own failed duration; it never captured or uploaded. + expect(saveTiming).toEqual({ schema: "paperclip.remote-workspace-save-timing.v1", outcome: "failed", + totalMs: expect.any(Number), at: expect.any(String) }); expect(Number.isFinite(Date.parse((saveWork as { quiescedAt: string }).quiescedAt))).toBe(true); expect(await db.select().from(executionWorkspaceSnapshots).where(eq(executionWorkspaceSnapshots.sourceOperationId, poisoned!.id))) .toHaveLength(0); diff --git a/server/src/services/remote-workspaces.ts b/server/src/services/remote-workspaces.ts index 8af46c45c4e..63aa0bfe1e1 100644 --- a/server/src/services/remote-workspaces.ts +++ b/server/src/services/remote-workspaces.ts @@ -28,6 +28,7 @@ import { captureRemoteWorkspaceArchive, cleanupRemoteWorkspaceArchive, prepareRemoteWorkspaceDirectory, prepareRemoteRepository, remoteWorkspaceRepositoryPath, RemoteWorkspaceOperationTimeoutError, RemoteWorkspacePreparationTimeoutError, remoteWorkspaceFailureCause, requireRemoteWorkspaceTarget, restoreRemoteWorkspaceDirectory, RemoteWorkspaceRestoreError, type RemoteWorkspaceRestoreStep, + type RemoteWorkspaceSavePhaseTiming, } from "./remote-workspace-transport.js"; import { beginWorkspaceSnapshot, getWorkspaceSnapshot, loadWorkspaceSnapshotArchive, uploadWorkspaceSnapshot, @@ -46,6 +47,16 @@ type Workspace = Pick[0]>[0]; const object = (value: unknown): Record => value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; const SAVE_WORK_SCHEMA = "paperclip.remote-workspace-save-work.v1"; +const SAVE_TIMING_SCHEMA = "paperclip.remote-workspace-save-timing.v1"; + +/** Per-phase durations of the latest settled save invocation, kept in the operation's local metadata. */ +type RemoteWorkspaceSaveTiming = RemoteWorkspaceSavePhaseTiming & { + /** Encrypt, hash, and store the archive (zero work when a prior upload is reused). */ + uploadMs?: number; + /** Read the archive manifest and commit the saved authority. */ + publishMs?: number; + reusedUpload?: boolean; +}; const RETIRED_SAVE_SCHEMA = "paperclip.remote-workspace-retired-save.v1"; type SaveWork = { schema: typeof SAVE_WORK_SCHEMA; activeIds: string[]; unknown: boolean; quiescedAt: string | null }; function readSaveWork(metadata: Record | null | undefined): SaveWork | null { @@ -610,9 +621,15 @@ export function remoteWorkspaceSaveFailure(error: unknown): { code: string; caus return cause ? { code, cause } : { code }; } +function saveTimingRecord(timing: RemoteWorkspaceSaveTiming & { startedAt: number }, outcome: "succeeded" | "failed", at: Date) { + const { startedAt, ...phases } = timing; + return { schema: SAVE_TIMING_SCHEMA, outcome, totalMs: Math.max(0, Math.round(performance.now() - startedAt)), + ...phases, at: at.toISOString() }; +} + async function settleSaveInvocation(input: { db: Db; workspace: Workspace; runId: string; epoch: number; location: NonNullable; operationId: string; invocationId: string; - failure?: { code: string; cause?: string } }) { + failure?: { code: string; cause?: string }; timing?: RemoteWorkspaceSaveTiming & { startedAt: number } }) { return input.db.transaction(async tx => { const workspace = await currentWorkspace(tx, input.workspace.companyId, input.workspace.id); const [operation] = await tx.select().from(workspaceOperations).where(and(eq(workspaceOperations.id, input.operationId), @@ -629,6 +646,7 @@ async function settleSaveInvocation(input: { db: Db; workspace: Workspace; runId const failed = Boolean(failure) && operation.status === "running" && !activeIds.length && !work.unknown; await tx.update(workspaceOperations).set({ metadata: { ...operation.metadata, saveWork: { ...work, activeIds, quiescedAt: activeIds.length ? null : now.toISOString() }, + ...(input.timing ? { saveTiming: saveTimingRecord(input.timing, input.failure ? "failed" : "succeeded", now) } : {}), ...(failed && failure ? { lastFailure: { ...failure, at: now.toISOString() } } : {}) }, ...(failed && failure ? { status: "failed", exitCode: 1, finishedAt: now, stderrExcerpt: `${[failure.code, failure.cause].filter(Boolean).join("\n")}\n` } : {}), @@ -644,6 +662,7 @@ export async function saveRemoteWorkspace(input: { assertOwned: () => Promise; storage?: StorageService; boundary?: string; repositories?: NonNullable; }) { + const saveStarted = performance.now(); const target = requireRemoteWorkspaceTarget(input.target), boundary = input.boundary ?? "final"; const expected = readRemoteWorkspaceAuthority(input.workspace.metadata); if (!expected?.location || expected.location.providerLeaseId !== input.lease.providerLeaseId || expected.location.leaseId !== input.lease.id) throw new Error("remote_workspace_save_binding_mismatch"); @@ -696,21 +715,31 @@ export async function saveRemoteWorkspace(input: { let captured: Awaited> | null = null; let publicationScratch: string | undefined; let failure: { code: string; cause?: string } | undefined; + const timing: RemoteWorkspaceSaveTiming & { startedAt: number } = { startedAt: saveStarted }; + // A replay of an already-committed save measures no phase; it keeps the + // original save's timing instead of overwriting it with an empty record. + let replayedCommittedSave = false; try { const snapshot = await beginWorkspaceSnapshot(input.db, { companyId: input.workspace.companyId, workspaceId: input.workspace.id, operationId: operation.id, runId: input.runId, parentSnapshotId: operation.metadata?.parentSnapshotId as string | null, generation: operation.metadata?.generation as number, epoch: expected.epoch }); if (operation.status === "succeeded" && ["committed", "superseded"].includes(snapshot.status)) { + replayedCommittedSave = true; return { snapshotId: snapshot.id, generation: snapshot.generation, sha256: snapshot.sha256!, savedAt: snapshot.updatedAt.toISOString() }; } const existingUpload = snapshot.status === "uploaded" ? await getWorkspaceSnapshot(input.db, { companyId: input.workspace.companyId, workspaceId: input.workspace.id, snapshotId: snapshot.id }) : null; - captured = existingUpload ? null : await captureRemoteWorkspaceArchive({ target, remoteCwd: expected.location.remoteCwd, operationId: operation.id }); + if (existingUpload) timing.reusedUpload = true; + captured = existingUpload ? null : await captureRemoteWorkspaceArchive({ target, remoteCwd: expected.location.remoteCwd, + operationId: operation.id, timing }); await input.assertOwned(); + const uploadStarted = performance.now(); const saved = await uploadWorkspaceSnapshot({ db: input.db, storage: input.storage ?? getStorageService(), snapshotId: snapshot.id, companyId: input.workspace.companyId, workspaceId: input.workspace.id, archivePath: captured?.archivePath ?? "", manifestDigest: existingUpload?.manifestDigest ?? captured!.manifestDigest }); + timing.uploadMs = Math.max(0, Math.round(performance.now() - uploadStarted)); await input.assertOwned(); + const publishStarted = performance.now(); let publicationArchive = captured?.archivePath; if (!publicationArchive) { publicationScratch = await mkdtemp(path.join(os.tmpdir(), "pc-workspace-publication-")); @@ -756,6 +785,7 @@ export async function saveRemoteWorkspace(input: { deleteAfter: new Date(at.getTime() + 7 * 86400_000), updatedAt: at }).where(and(eq(executionWorkspaceSnapshots.id, snapshot.parentSnapshotId), eq(executionWorkspaceSnapshots.companyId, workspace.companyId), eq(executionWorkspaceSnapshots.executionWorkspaceId, workspace.id))); }); + timing.publishMs = Math.max(0, Math.round(performance.now() - publishStarted)); return { snapshotId: saved.id, generation: saved.generation, sha256: saved.sha256, savedAt: at.toISOString() }; } catch (error) { failure = remoteWorkspaceSaveFailure(error); @@ -768,7 +798,8 @@ export async function saveRemoteWorkspace(input: { catch { logger.warn({ runId: input.runId, operationId: operation.id }, "Remote workspace local archive cleanup failed"); } try { if (await settleSaveInvocation({ db: input.db, workspace: input.workspace, runId: input.runId, epoch: expected.epoch, - location: expected.location, operationId: operation.id, invocationId, failure })) { + location: expected.location, operationId: operation.id, invocationId, failure, + timing: replayedCommittedSave ? undefined : timing })) { await cleanupSavedExports({ db: input.db, companyId: input.workspace.companyId, workspaceId: input.workspace.id, location: expected.location, target, assertOwned: input.assertOwned }); } diff --git a/server/src/services/system-recovery.ts b/server/src/services/system-recovery.ts index 5b93c297570..b32580e3392 100644 --- a/server/src/services/system-recovery.ts +++ b/server/src/services/system-recovery.ts @@ -1,6 +1,6 @@ import { notifyIssueRecovery } from "./issue-recovery-notifications.js"; import { and, eq, sql } from "drizzle-orm"; -import { heartbeatRuns, issueComments, issueRecoveryActions, type Db } from "@paperclipai/db"; +import { agents, issues, heartbeatRuns, issueComments, issueRecoveryActions, type Db } from "@paperclipai/db"; import { writeExecutionControl } from "./execution-control-notifications.js"; import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notifications.js"; import { createSystemRecovery, readSystemRecovery, type SystemRecoveryState } from "./system-recovery-state.js"; @@ -127,8 +127,18 @@ export async function nextWorkspaceRecoveryAt(db: Db): Promise { then (${issueRecoveryActions.evidence}->>'runId')::uuid end`), sql`${heartbeatRuns.status} in ('failed', 'interrupted', 'timed_out', 'cancelled', 'succeeded')`, )).where(and( - workspaceRecoverySourceEligible(), - sql`${issueRecoveryActions.evidence}#>>'{systemRecovery,state}' in ('scheduled', 'running', 'awaiting_evidence')`, + workspaceRecoverySourceEligible(true), + sql`(${issueRecoveryActions.evidence}#>>'{systemRecovery,state}' in ('scheduled', 'running', 'awaiting_evidence') + or (${issueRecoveryActions.evidence}#>>'{systemRecovery,state}' = 'complete' + and ${issueRecoveryActions.evidence}->>'bootstrapPreparationSettledAt' is not null + and coalesce(${issueRecoveryActions.evidence}->>'bootstrapPreparationContinuation', 'pending') = 'pending' + and exists (select 1 from ${issues} inner join ${agents} + on ${agents.id} = ${heartbeatRuns.agentId} and ${agents.companyId} = ${heartbeatRuns.companyId} + where ${issues.id} = ${issueRecoveryActions.sourceIssueId} and ${issues.companyId} = ${heartbeatRuns.companyId} + and ${issues.assigneeAgentId} = ${heartbeatRuns.agentId} + and ${issues.status} in ('todo', 'in_progress', 'in_review') + and ${issues.executionRunId} is null and ${issues.checkoutRunId} is null + and ${agents.status} in ('idle', 'running', 'error'))))`, )).orderBy(sql`1`).limit(1); return rows.length ? Number(rows[0].at) : null; } diff --git a/server/src/services/workspace-recovery.test.ts b/server/src/services/workspace-recovery.test.ts index a8f766039d8..20f7b40be8e 100644 --- a/server/src/services/workspace-recovery.test.ts +++ b/server/src/services/workspace-recovery.test.ts @@ -1,3 +1,5 @@ +import { nextWorkspaceRecoveryAt } from "./system-recovery.js"; +import { notifyDeferredExecutionEligibility } from "./deferred-execution-notifications.js"; import { createAdmissionTransactionScope, createWakeAdmissionReader, createWakeAdmissionWriter } from "../modules/wake-queue/adapters/postgres.js"; import { createAdmitWakeBehindIssueExecution } from "../modules/wake-queue/application/use-cases.js"; import { filterZombieCoalesceTarget } from "./heartbeat.js"; @@ -9,7 +11,7 @@ import { isPreDispatchExecutionWaitVerified } from "./pre-dispatch-execution-wai import { heartbeatRunEvents } from "@paperclipai/db"; import { createHeartbeatQueue, shouldDeferFollowupWakeForSameIssue, shouldQueueFollowupForRunningIssueWake } from "./heartbeat/queue.js"; import { createHeartbeatQueueTestDependencies } from "../__tests__/helpers/heartbeat-queue.js"; -import { recordRequiredWorkspaceRestoreFailure, terminalizeLegacyExecution } from "./legacy-execution-recovery.js"; +import { recordRequiredWorkspaceRestoreFailure, terminalizeLegacyExecution, settleInterruptedNativeBootstrap } from "./legacy-execution-recovery.js"; import { admitExplicitNativeContinuation } from "./explicit-native-continuation.js"; import { withQueuedCommentIdsInWakePayload } from "./issue-queued-comment-queue.js"; import * as recoverySources from "./workspace-recovery-source.js"; @@ -1451,17 +1453,20 @@ describe("automatic retained workspace recovery", () => { expect(await readFile(path.join(f.remote, "saved.txt"), "utf8")).toBe("finished work\n"); }, 30_000); - async function interruptedRemotePreparation(interruption: "shutdown" | "lease" = "shutdown", sourceLayout: RecoverySourceLayout = "retained") { + async function interruptedRemotePreparation(interruption: "shutdown" | "lease" | "process_lost" = "shutdown", sourceLayout: RecoverySourceLayout = "retained") { const f = await acceptedRemoteExport(sourceLayout); await db.delete(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)); await db.delete(nativeRunResults).where(eq(nativeRunResults.runId, f.runId)); const recovery = { schema: "paperclip.workspace-restore-recovery.v1", leaseIds: [f.lease.id] }; await db.update(heartbeatRuns).set({ runtimeMode: "legacy", nativeIssueId: null, completionContractId: null, - status: "interrupted", executionStage: "preparing", - errorCode: interruption === "lease" ? "lease_released_before_terminal" : "server_shutdown_interrupted", + status: interruption === "process_lost" ? "failed" : "interrupted", executionStage: "preparing", + errorCode: interruption === "process_lost" ? "process_lost" : interruption === "lease" ? "lease_released_before_terminal" : "server_shutdown_interrupted", controllerBootId: randomUUID(), - error: interruption === "lease" ? "Environment lease released before preparation settled" : "Interrupted by graceful server shutdown (SIGTERM)", + error: interruption === "process_lost" ? "Process lost -- server may have restarted; retrying once" + : interruption === "lease" ? "Environment lease released before preparation settled" : "Interrupted by graceful server shutdown (SIGTERM)", resultJson: { workspaceRestoreFailure: "restore_failed", workspaceRestoreRecovery: recovery, + ...(interruption === "process_lost" ? { executionRecovery: { kind: "bootstrap", providerWorkStarted: false }, + processLossDiagnostic: { localCheck: "no_identifiers", pidRecorded: false, groupRecorded: false, retryEligible: true, runPredatesObserver: true } } : {}), ...(interruption === "lease" ? { cancellation: { source: "unknown", expected: false, initiator: { type: "system" }, reason: "Environment lease released before preparation settled", recordedAt: new Date().toISOString() } } : {}) }, @@ -1478,6 +1483,18 @@ describe("automatic retained workspace recovery", () => { return f; } + async function withheldPreparationRetry(f: { companyId: string; issueId: string; runId: string; actionId: string }) { + const source = await run(f.runId); + const [withheld] = await db.insert(heartbeatRuns).values({ companyId: f.companyId, agentId: source.agentId, + responsibleUserId: source.responsibleUserId, scopeKind: "issue", issueId: f.issueId, runtimeMode: "legacy", status: "cancelled", finishedAt: new Date(), + retryOfRunId: f.runId, scheduledRetryAttempt: 1, scheduledRetryReason: "transient_failure", + errorCode: "execution_reconciliation_required", contextSnapshot: { issueId: f.issueId }, + resultJson: { stopReason: "execution_reconciliation_required", timeoutSource: "stale_queued_run_gate", + executionWait: { issueId: f.issueId, recoveryActionId: f.actionId } }, + }).returning(); + return withheld; + } + async function failedRemotePreparation(sourceLayout: RecoverySourceLayout = "retained", startedMarker = false, warning = false) { const f = await interruptedRemotePreparation("shutdown", sourceLayout); const source = await run(f.runId); @@ -2016,9 +2033,9 @@ describe("automatic retained workspace recovery", () => { expect(await readFile(path.join(cwd, "saved.txt"), "utf8")).toBe("existing task work\n"); }); - it.each(["complete", "catalog", "catalog_warning", "catalog_unknown_warning", "catalog_provider_warning", "invalid_published_root"] as const)( + it.each(["complete", "automatic_lost", "catalog", "catalog_warning", "catalog_unknown_warning", "catalog_provider_warning", "invalid_published_root"] as const)( "dispatches the recovered failed-preparation Retry once through the native provider boundary (%s)", async mode => { - const unavailableCatalog = mode !== "complete" && mode !== "invalid_published_root", withWarning = mode.includes("warning"); + const unavailableCatalog = mode !== "complete" && mode !== "automatic_lost" && mode !== "invalid_published_root", withWarning = mode.includes("warning"); const localStorage = getStorageService(); const databaseUrl = process.env.DATABASE_URL, masterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY; const experimental = await instanceSettingsService(db).getExperimental(); @@ -2036,7 +2053,12 @@ describe("automatic retained workspace recovery", () => { storage = vi.spyOn(storageBackend, "getStorageService").mockReturnValue({ ...localStorage, provider: "s3" }); process.env.DATABASE_URL = connectionString; process.env.PAPERCLIP_SECRETS_MASTER_KEY = "63".repeat(32); await instanceSettingsService(db).updateExperimental({ enableNativeRunner: true, enableIsolatedWorkspaces: true }); - const f = await failedRemotePreparation(mode === "invalid_published_root" ? "raw" : "published"); + const f = mode === "automatic_lost" ? await interruptedRemotePreparation("process_lost", "published") + : await failedRemotePreparation(mode === "invalid_published_root" ? "raw" : "published"); + if (mode === "automatic_lost") { + await db.update(heartbeatRuns).set({ responsibleUserId: "local-board" }).where(eq(heartbeatRuns.id, f.runId)); + await withheldPreparationRetry(f); + } if (mode === "complete") await db.update(heartbeatRuns).set({ sessionIdBefore: randomUUID() }).where(eq(heartbeatRuns.id, f.runId)); await recoverRetainedWorkspace(f.input); const source = await run(f.runId), nextDirectory = path.join(root, randomUUID(), "next"); @@ -2144,12 +2166,19 @@ describe("automatic retained workspace recovery", () => { const app = boardRecoveryApp(undefined, heartbeat); const gate = new Promise(resolve => { releaseStart = resolve; }); heldStart = agentStarts.withAgentStartLock(source.agentId, () => gate); - const receipt = (await request(app).post(`/api/issues/${f.issueId}/retry`) + const receipt = mode === "automatic_lost" ? null : (await request(app).post(`/api/issues/${f.issueId}/retry`) .send({ observedRunId: f.runId, idempotencyKey: randomUUID() }).expect(202)).body; - await expect.poll(() => starts.mock.calls.filter(([agentId]) => agentId === source.agentId).length, { timeout: 10_000 }).toBe(2); - explicitResume = heartbeat.resumeTaskRetry(f.companyId, receipt.requestId); + if (mode === "automatic_lost") { + await Promise.all([heartbeat.resumeRemoteStopComments(await run(f.runId)), + heartbeat.resumeRemoteStopComments(await run(f.runId))]); + explicitResume = heartbeat.promoteDueScheduledRetries(new Date(Date.now() + 300_000)).then(() => undefined); + } else { + await expect.poll(() => starts.mock.calls.filter(([agentId]) => agentId === source.agentId).length, { timeout: 10_000 }).toBe(2); + explicitResume = heartbeat.resumeTaskRetry(f.companyId, receipt.requestId); + } const successors = await db.select().from(heartbeatRuns).where(and(eq(heartbeatRuns.companyId, f.companyId), - sql`${heartbeatRuns.contextSnapshot}->>'taskRetryRequestId' = ${receipt.requestId}`)); + mode === "automatic_lost" ? eq(heartbeatRuns.scheduledRetryReason, "interrupted_preparation") + : sql`${heartbeatRuns.contextSnapshot}->>'taskRetryRequestId' = ${receipt.requestId}`)); expect(successors).toHaveLength(1); if (unavailableCatalog) { const repoUrl = "https://github.com/example/revoked.git"; @@ -2226,7 +2255,9 @@ describe("automatic retained workspace recovery", () => { expect((await request(app).get(`/api/issues/${f.issueId}/queued-comments`).expect(200)).body.entries).toEqual([]); expect((await request(app).get(`/api/issues/${f.issueId}`).expect(200)).body.interruption) .toMatchObject({ state: "resolved", canRetry: false }); - await heartbeat.resumeTaskRetry(f.companyId, receipt.requestId); expect(startTurn).toHaveBeenCalledOnce(); + if (mode === "automatic_lost") await heartbeat.resumeRemoteStopComments(await run(f.runId)); + else await heartbeat.resumeTaskRetry(f.companyId, receipt.requestId); + expect(startTurn).toHaveBeenCalledOnce(); expect((await run(f.runId)).error).toBe(source.error); } finally { try { @@ -2290,6 +2321,216 @@ describe("automatic retained workspace recovery", () => { } finally { await restarted.$client.end(); } }, 30_000); + it.each([false, true])("continues recovered lost preparation once after its first retry was withheld (startup=%s)", async alreadyRecovered => { + const f = await interruptedRemotePreparation("process_lost"); + const withheld = await withheldPreparationRetry(f); + expect(await isPreDispatchExecutionWaitVerified(db, withheld)).toBe(true); + if (alreadyRecovered) await recoverRetainedWorkspace(f.input); + const restarted = createDb(connectionString); + try { + const heartbeat = heartbeatService(restarted); + await Promise.all(await reconcileRetainedWorkspaces(restarted, f.environmentRuntime, + heartbeat.resumeRemoteStopComments, true) ?? []); + await Promise.all([heartbeat.resumeRemoteStopComments(await run(f.runId)), + heartbeat.resumeRemoteStopComments(await run(f.runId))]); + const successors = await db.select().from(heartbeatRuns).where(and(eq(heartbeatRuns.companyId, f.companyId), + eq(heartbeatRuns.status, "scheduled_retry"))); + expect(successors).toHaveLength(1); + expect(successors[0]).toMatchObject({ retryOfRunId: withheld.id, scheduledRetryAttempt: 2, + scheduledRetryReason: "interrupted_preparation", contextSnapshot: { executionRetryAccounting: { failureRetries: 2 } } }); + expect(await getExecutionBlocker(db, f.companyId, f.issueId)).toBeNull(); + expect((await run(f.runId)).status).toBe("failed"); + expect(await run(withheld.id)).toMatchObject({ status: "cancelled", startedAt: null }); + expect(await hasVerifiedWorkspaceRepair(db, await run(f.runId))).toBe(true); + } finally { await restarted.$client.end(); } + }, 30_000); + + it.each(["withheld_first", "successor_first"] as const)("reuses the newest retry child without replacing a withheld sibling (%s)", async order => { + const f = await interruptedRemotePreparation("process_lost"); + const source = await run(f.runId); + const now = Date.now(); + const addSuccessor = async () => (await db.insert(heartbeatRuns).values({ + companyId: f.companyId, agentId: source.agentId, issueId: f.issueId, retryOfRunId: source.id, + status: "failed", runtimeMode: "native", nativeIssueId: f.issueId, + startedAt: new Date(now - 1000), finishedAt: new Date(now), createdAt: new Date(now - 1000), + errorCode: "native_provider_terminal_failed", contextSnapshot: { issueId: f.issueId }, + scheduledRetryAttempt: 2, scheduledRetryReason: "transient_failure", + }).returning())[0]; + const addWithheld = async () => { + const child = await withheldPreparationRetry(f); + await db.update(heartbeatRuns).set({ createdAt: new Date(now - 2000) }).where(eq(heartbeatRuns.id, child.id)); + return child; + }; + let successor: typeof heartbeatRuns.$inferSelect; + let withheld: typeof heartbeatRuns.$inferSelect; + if (order === "withheld_first") { withheld = await addWithheld(); successor = await addSuccessor(); } + else { successor = await addSuccessor(); withheld = await addWithheld(); } + await recoverRetainedWorkspace(f.input); + const heartbeat = heartbeatService(db); + await Promise.all([heartbeat.resumeRemoteStopComments(await run(f.runId)), + heartbeat.resumeRemoteStopComments(await run(f.runId))]); + expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.retryOfRunId, withheld.id))).toHaveLength(0); + expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.retryOfRunId, source.id))).toHaveLength(2); + expect(await run(successor!.id)).toEqual(successor!); + expect((await action(f.actionId)).evidence).toMatchObject({ bootstrapPreparationContinuation: "delivered", + bootstrapPreparationContinuationRunId: successor!.id }); + expect(await nextWorkspaceRecoveryAt(db)).toBeNull(); + }, 30_000); + + it.each([false, true])("continues a recovered lost preparation without an earlier retry (startup=%s)", async alreadyRecovered => { + const f = await interruptedRemotePreparation("process_lost"); + if (alreadyRecovered) await recoverRetainedWorkspace(f.input); + const restarted = createDb(connectionString); + try { + const heartbeat = heartbeatService(restarted); + await Promise.all(await reconcileRetainedWorkspaces(restarted, f.environmentRuntime, + heartbeat.resumeRemoteStopComments, true) ?? []); + await Promise.all([heartbeat.resumeRemoteStopComments(await run(f.runId)), + heartbeat.resumeRemoteStopComments(await run(f.runId))]); + const successors = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.retryOfRunId, f.runId)); + expect(successors).toHaveLength(1); + expect(successors[0]).toMatchObject({ status: "scheduled_retry", scheduledRetryAttempt: 1 }); + expect(await getExecutionBlocker(db, f.companyId, f.issueId)).toBeNull(); + expect(await nextWorkspaceRecoveryAt(restarted)).toBeNull(); + } finally { await restarted.$client.end(); } + }, 30_000); + + it.each(["restart", "pause", "claimed", "duplicate"] as const)("retains preparation continuation after settlement before enqueue (%s)", async mode => { + const f = await interruptedRemotePreparation("process_lost"), withheld = await withheldPreparationRetry(f); + await recoverRetainedWorkspace(f.input); + const source = await run(f.runId); + expect(await settleInterruptedNativeBootstrap(db, { run: source, providerDispatchStarted: false })).not.toBeNull(); + expect((await action(f.actionId)).evidence.automaticRecovery).toMatchObject({ replay: "allowed" }); + expect(await nextWorkspaceRecoveryAt(db)).not.toBeNull(); + const duplicateId = mode === "duplicate" ? randomUUID() : null; + if (duplicateId) { + const prior = await action(f.actionId); + await db.insert(issueRecoveryActions).values({ ...prior, id: duplicateId }); + } + if (mode === "claimed") { + const first = await heartbeatService(db).scheduleBoundedRetry(f.runId); + expect(first.outcome).toBe("scheduled"); + expect((await action(f.actionId)).evidence.bootstrapPreparationContinuation).toBe("pending"); + } + if (mode === "pause") await db.update(agents).set({ status: "paused" }).where(eq(agents.id, source.agentId)); + const restarted = createDb(connectionString); + try { + const heartbeat = heartbeatService(restarted); + await Promise.all(await reconcileRetainedWorkspaces(restarted, f.environmentRuntime, + heartbeat.resumeRemoteStopComments, true) ?? []); + if (mode === "pause") { + expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.retryOfRunId, withheld.id))).toHaveLength(0); + expect(await nextWorkspaceRecoveryAt(restarted)).toBeNull(); + expect((await action(f.actionId)).evidence.bootstrapPreparationContinuation).toBe("deferred"); + const wake = vi.fn(), unsubscribe = subscribeDeliveryWork(restarted, DELIVERY_QUEUES.workspaceRecovery, wake); + try { + const rollback = new Error("rollback eligibility change"); + await expect(restarted.transaction(async tx => { + await notifyDeferredExecutionEligibility(tx, { companyId: f.companyId }); + await tx.update(agents).set({ status: "idle" }).where(eq(agents.id, source.agentId)); + throw rollback; + })).rejects.toBe(rollback); + expect((await db.select().from(agents).where(eq(agents.id, source.agentId)))[0].status).toBe("paused"); + await Promise.all(await reconcileRetainedWorkspaces(restarted, f.environmentRuntime, + heartbeat.resumeRemoteStopComments, true) ?? []); + expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.retryOfRunId, withheld.id))).toHaveLength(0); + expect(await nextWorkspaceRecoveryAt(restarted)).toBeNull(); + wake.mockClear(); + await restarted.transaction(async tx => { + await notifyDeferredExecutionEligibility(tx, { companyId: f.companyId }); + await tx.update(agents).set({ status: "idle" }).where(eq(agents.id, source.agentId)); + }); + expect(wake).toHaveBeenCalled(); + } finally { unsubscribe(); } + await Promise.all(await reconcileRetainedWorkspaces(restarted, f.environmentRuntime, + heartbeat.resumeRemoteStopComments, true) ?? []); + } + const successors = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.retryOfRunId, withheld.id)); + expect(successors).toHaveLength(1); + expect(successors[0]).toMatchObject({ status: "scheduled_retry", scheduledRetryAttempt: 2 }); + expect((await action(f.actionId)).evidence).toMatchObject({ bootstrapPreparationContinuation: "delivered", + bootstrapPreparationContinuationRunId: successors[0].id }); + expect(await nextWorkspaceRecoveryAt(restarted)).toBeNull(); + if (duplicateId) expect((await action(duplicateId)).evidence.bootstrapPreparationContinuation).toBe("delivered"); + expect(await retainedWorkspaceCandidates(restarted, undefined, false, true)).not.toEqual( + expect.arrayContaining([expect.objectContaining({ run: expect.objectContaining({ id: f.runId }) })])); + } finally { await restarted.$client.end(); } + }, 30_000); + + it("revisits a pending continuation after enqueue fails before a new notification", async () => { + const f = await interruptedRemotePreparation("process_lost"), withheld = await withheldPreparationRetry(f); + await recoverRetainedWorkspace(f.input); + expect(await settleInterruptedNativeBootstrap(db, { run: await run(f.runId), providerDispatchStarted: false })).not.toBeNull(); + const heartbeat = heartbeatService(db), passiveSweep = {}; + let attempts = 0; + const onRecovered = async (source: Awaited>) => { + if (source.id !== f.runId) return; + if (++attempts === 1) throw new Error("database read unavailable before enqueue notification"); + await heartbeat.resumeRemoteStopComments(source); + }; + await Promise.all(await reconcileRetainedWorkspaces(db, f.environmentRuntime, onRecovered, true, undefined, passiveSweep) ?? []); + expect(attempts).toBe(1); + expect(await nextWorkspaceRecoveryAt(db)).not.toBeNull(); + expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.retryOfRunId, withheld.id))).toHaveLength(0); + await Promise.all(await reconcileRetainedWorkspaces(db, f.environmentRuntime, onRecovered, true, undefined, passiveSweep) ?? []); + expect(attempts).toBe(2); + expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.retryOfRunId, withheld.id))) + .toEqual([expect.objectContaining({ status: "scheduled_retry", scheduledRetryAttempt: 2 })]); + expect(await nextWorkspaceRecoveryAt(db)).toBeNull(); + }, 30_000); + + it.each(["no_certificate", "provider_started", "runtime_selected", "live_controller", "operator_stop", "missing_stop_receipt", "wrong_snapshot", "retry_exhausted", "human_reblock", "paused", "reassigned", "approval"] as const)( + "keeps lost preparation behind its %s gate after file preservation", async gate => { + const f = await interruptedRemotePreparation("process_lost"); + await recoverRetainedWorkspace(f.input); + const source = await run(f.runId); + if (gate === "no_certificate") await db.update(heartbeatRuns).set({ resultJson: { ...source.resultJson, + executionRecovery: null } }).where(eq(heartbeatRuns.id, f.runId)); + if (gate === "provider_started") await db.insert(heartbeatRunEvents).values({ companyId: f.companyId, + runId: f.runId, agentId: source.agentId, seq: 1, eventType: "native.process_start_requested", stream: "system" }); + if (gate === "runtime_selected") await db.update(heartbeatRuns).set({ runtimeModeResolvedAt: new Date() }).where(eq(heartbeatRuns.id, f.runId)); + if (gate === "live_controller") await db.update(heartbeatRuns).set({ controllerLeaseExpiresAt: new Date(Date.now() + 60_000) }).where(eq(heartbeatRuns.id, f.runId)); + if (gate === "operator_stop") await db.update(heartbeatRuns).set({ resultJson: { ...source.resultJson, + cancellation: requestedRunCancellation({ cancelledByActorType: "user", cancelledByUserId: "operator" }, "Stop requested"), + } }).where(eq(heartbeatRuns.id, f.runId)); + if (gate === "missing_stop_receipt") { + const [lease] = await db.select().from(environmentLeases).where(eq(environmentLeases.id, f.lease.id)); + await db.update(environmentLeases).set({ metadata: { ...lease.metadata, remoteExecutionTermination: null } }).where(eq(environmentLeases.id, f.lease.id)); + } + if (gate === "wrong_snapshot") await db.update(heartbeatRuns).set({ runnerProfileJson: { ...source.runnerProfileJson, + remoteWorkspace: { ...(source.runnerProfileJson!.remoteWorkspace as Record), snapshotId: randomUUID() }, + } }).where(eq(heartbeatRuns.id, f.runId)); + if (gate === "retry_exhausted") await db.update(heartbeatRuns).set({ scheduledRetryAttempt: 2 }).where(eq(heartbeatRuns.id, f.runId)); + if (gate === "human_reblock") await db.update(issues).set({ statusVersion: 3 }).where(eq(issues.id, f.issueId)); + if (gate === "paused") await db.update(agents).set({ status: "paused" }).where(eq(agents.id, source.agentId)); + if (gate === "reassigned") await db.update(issues).set({ assigneeAgentId: null, assigneeUserId: "board" }).where(eq(issues.id, f.issueId)); + if (gate === "approval") { + const approvalId = randomUUID(); + await db.insert(approvals).values({ id: approvalId, companyId: f.companyId, type: "hire_agent", status: "pending", payload: {} }); + await db.insert(issueApprovals).values({ companyId: f.companyId, issueId: f.issueId, approvalId }); + } + const heartbeat = heartbeatService(db); + await heartbeat.resumeRemoteStopComments(await run(f.runId)); + expect(await db.select().from(heartbeatRuns).where(and(eq(heartbeatRuns.companyId, f.companyId), + eq(heartbeatRuns.retryOfRunId, f.runId)))).toHaveLength(0); + expect((await run(f.runId)).error).toBe(source.error); + }, 30_000); + + it.each(["provider_evidence", "exhausted"] as const)("does not replace a withheld retry with %s", async gate => { + const f = await interruptedRemotePreparation("process_lost"), withheld = await withheldPreparationRetry(f); + if (gate === "provider_evidence") await db.insert(heartbeatRunEvents).values({ companyId: f.companyId, + runId: withheld.id, agentId: withheld.agentId, seq: 1, eventType: "tool.execution.started", stream: "system" }); + if (gate === "exhausted") await db.update(heartbeatRuns).set({ scheduledRetryAttempt: 2 }).where(eq(heartbeatRuns.id, withheld.id)); + await recoverRetainedWorkspace(f.input); + const heartbeat = heartbeatService(db); + await Promise.all([heartbeat.resumeRemoteStopComments(await run(f.runId)), + heartbeat.resumeRemoteStopComments(await run(f.runId))]); + expect(await db.select().from(heartbeatRuns).where(and(eq(heartbeatRuns.companyId, f.companyId), + eq(heartbeatRuns.retryOfRunId, withheld.id)))).toHaveLength(0); + expect(await run(withheld.id)).toMatchObject({ status: "cancelled", startedAt: null }); + expect(await readFile(path.join(f.remote, "saved.txt"), "utf8")).toBe("finished work\n"); + }, 30_000); + it("retries unpublished preparation from its preserved ancestor without an old failure-block receipt", async () => { const f = await seed("legacy", false); await requestWorkspaceRecovery({ ...f, idempotencyKey: randomUUID(), actorId: "board" }); diff --git a/server/src/services/workspace-recovery.ts b/server/src/services/workspace-recovery.ts index 766229bc093..88f0efc82cc 100644 --- a/server/src/services/workspace-recovery.ts +++ b/server/src/services/workspace-recovery.ts @@ -496,10 +496,16 @@ export function retainedWorkspaceCandidates(db: Db, cursor?: string, unfinishedO and ${agentWakeupRequests.status} = 'deferred_issue_execution' and ${agentWakeupRequests.requestedByActorType} = 'user' and ${agentWakeupRequests.payload}->>'issueId' = ${issueRecoveryActions.sourceIssueId}::text)`; const interruptedPreparation = sql`(${heartbeatRuns.runtimeMode} = 'legacy' - and ${heartbeatRuns.status} = 'interrupted' + and (${heartbeatRuns.status} = 'interrupted' or (${heartbeatRuns.status} = 'failed' + and ${heartbeatRuns.errorCode} = 'process_lost' + and ${heartbeatRuns.resultJson}#>>'{executionRecovery,kind}' = 'bootstrap' + and ${heartbeatRuns.resultJson}#>'{executionRecovery,providerWorkStarted}' = 'false'::jsonb)) and ${heartbeatRuns.runtimeModeResolvedAt} is null and ${heartbeatRuns.executionStage} = 'preparing' and ${heartbeatRuns.runnerProfileJson}#>>'{adapterDispatch,adapterType}' = 'paperclip_runner' - and ${issueRecoveryActions.evidence}#>>'{automaticRecovery,replay}' = 'blocked' + and (${issueRecoveryActions.evidence}#>>'{automaticRecovery,replay}' = 'blocked' + or (${issueRecoveryActions.evidence}#>>'{automaticRecovery,replay}' = 'allowed' + and ${issueRecoveryActions.evidence}->>'bootstrapPreparationSettledAt' is not null + and coalesce(${issueRecoveryActions.evidence}->>'bootstrapPreparationContinuation', 'pending') in ('pending', 'deferred'))) and exists (select 1 from ${issues} where ${issues.companyId} = ${heartbeatRuns.companyId} and ${issues.id} = ${issueRecoveryActions.sourceIssueId} and ${issues.assigneeAgentId} = ${heartbeatRuns.agentId} and ${issues.status} in ('todo', 'in_progress', 'in_review', 'blocked') @@ -512,7 +518,10 @@ export function retainedWorkspaceCandidates(db: Db, cursor?: string, unfinishedO then (${issueRecoveryActions.evidence}->>'runId')::uuid end`))) .where(and(inArray(heartbeatRuns.status, terminal), workspaceRecoverySourceEligible(true), - unfinishedOnly ? sql`${issueRecoveryActions.evidence}#>>'{systemRecovery,state}' in ('scheduled', 'running', 'awaiting_evidence')` : undefined, + unfinishedOnly ? sql`(${issueRecoveryActions.evidence}#>>'{systemRecovery,state}' in ('scheduled', 'running', 'awaiting_evidence') + or (${issueRecoveryActions.evidence}#>>'{systemRecovery,state}' = 'complete' + and ${issueRecoveryActions.evidence}->>'bootstrapPreparationSettledAt' is not null + and coalesce(${issueRecoveryActions.evidence}->>'bootstrapPreparationContinuation', 'pending') = 'pending'))` : undefined, obligationsOnly ? sql`(${issueRecoveryActions.evidence}#>>'{systemRecovery,state}' in ('scheduled', 'running', 'awaiting_evidence') or (${issueRecoveryActions.evidence}#>>'{systemRecovery,state}' = 'complete' and (${cleanup} or ${interruptedPreparation})) or ${waitingInput})` : undefined, sql`(${issueRecoveryActions.evidence}#>>'{systemRecovery,state}' in ('scheduled', 'running', 'awaiting_evidence', 'complete', 'exhausted') diff --git a/tests/ai-connections-app/playwright.config.ts b/tests/ai-connections-app/playwright.config.ts index 4d2c4748141..3901f6a5cb6 100644 --- a/tests/ai-connections-app/playwright.config.ts +++ b/tests/ai-connections-app/playwright.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ use: { baseURL: process.env.AI_CONNECTIONS_TEST_URL ?? "http://127.0.0.1:3100", browserName: "chromium", - reducedMotion: "reduce", + contextOptions: { reducedMotion: "reduce" }, screenshot: "only-on-failure", }, }); diff --git a/tests/ai-connections-review/playwright.config.ts b/tests/ai-connections-review/playwright.config.ts index 75b89da5f85..48896eaddad 100644 --- a/tests/ai-connections-review/playwright.config.ts +++ b/tests/ai-connections-review/playwright.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ use: { baseURL: "http://localhost:6116", browserName: "chromium", - reducedMotion: "reduce", + contextOptions: { reducedMotion: "reduce" }, }, webServer: { command: "node ../../scripts/serve-storybook-static.mjs --port 6116", diff --git a/tests/e2e/acp-stop-continuation.spec.ts b/tests/e2e/acp-stop-continuation.spec.ts index de927420a86..9b9037d027c 100644 --- a/tests/e2e/acp-stop-continuation.spec.ts +++ b/tests/e2e/acp-stop-continuation.spec.ts @@ -47,7 +47,7 @@ for (const { unfinishedWrite, stopResponse, sendSavedQueue = false } of [ .toContain("List my recent Drive files."); // Both actions stop the response; composer Stop must not create a task hold. - let stopped; + let stopped!: { status: string; resultJson?: { executionCancellation?: { state?: string } } | null }; if (stopResponse) { await page.getByRole("button", { name: "Stop", exact: true }).click(); } else { @@ -58,7 +58,7 @@ for (const { unfinishedWrite, stopResponse, sendSavedQueue = false } of [ return stopped.resultJson?.executionCancellation?.state; }, { timeout: 30_000 }).toBe("acknowledged"); expect(stopped.status).toBe("cancelled"); - expect(stopped.resultJson.executionCancellation.state).toBe("acknowledged"); + expect(stopped.resultJson?.executionCancellation?.state).toBe("acknowledged"); const writesAtStop = unfinishedWrite ? await readFile(path.join(root, "writes"), "utf8") : null; await page.reload(); if (stopResponse) { diff --git a/tests/e2e/agent-configuration.spec.ts b/tests/e2e/agent-configuration.spec.ts index 227e67ba551..653e0f6c5eb 100644 --- a/tests/e2e/agent-configuration.spec.ts +++ b/tests/e2e/agent-configuration.spec.ts @@ -51,7 +51,7 @@ async function document(page: Page): Promise { await editor(page).press("ControlOrMeta+a"); const selectedEditor = await editor(page).evaluate(element => { const selection = window.getSelection(); - return document.activeElement === element && !!selection?.anchorNode && !!selection.focusNode + return window.document.activeElement === element && !!selection?.anchorNode && !!selection.focusNode && element.contains(selection.anchorNode) && element.contains(selection.focusNode); }); if (!selectedEditor) return false; @@ -135,7 +135,7 @@ test("compact YAML stays small as the company grows and preserves hidden configu await json(await request.put(`/api/agents/${agent.id}/instructions-bundle/file`, { data: { path: "AGENTS.md", content: instructions, baseHash: entry.contentHash, baseRevisionId: entry.revision?.id ?? null, } })); - const profiles = []; + const profiles: Array<{ id: string }> = []; for (let index = 0; index < 8; index++) profiles.push(await json(await request.post(`/api/companies/${company.id}/tools/profiles`, { data: { profileKey: `profile-${index}`, name: `Profile ${index}`, defaultAction: "deny", entries: Array.from({ length: 30 }, (_, tool) => ({ selectorType: "tool_name", effect: "include", toolName: `fixture_tool_${tool}` })), @@ -238,7 +238,7 @@ test("Advanced YAML persists settings and commands through their real resource A expect((await json(await request.get(`/api/agents/${agent.id}/instructions-bundle`))).files.map((file: { path: string }) => file.path)).not.toContain("workflow.md"); expect(cleared.spec.skills).toBeUndefined(); expect(cleared.spec.tools).toBeUndefined(); - expect(cleared.spec.channels![0].settings.allowUnlinkedPeople).toBe(true); + expect(cleared.spec.channels![0].settings!.allowUnlinkedPeople).toBe(true); expect(cleared.spec.channels![0].settings).not.toHaveProperty("communicationInstructions"); expect((await json(await request.get(`/api/agents/${agent.id}/keys`))).find((row: { id: string }) => row.id === key.id).revokedAt).not.toBeNull(); await page.screenshot({ path: info.outputPath("saved-agent-yaml.png") }); @@ -305,7 +305,7 @@ test("a lost creation response requires reload and cannot blindly replay command // Reproduce the observed CI failure once: a copy contains surrounding page // text as well as the YAML header. The read helper must reacquire a clean // editor copy instead of accepting the header and parsing the whole page. - await page.evaluate(() => document.addEventListener("copy", event => { + await page.evaluate(() => window.document.addEventListener("copy", event => { event.clipboardData?.setData("text/plain", "Skip to Main Content\napiVersion: paperclip/v1\nCopied surrounding page text"); event.preventDefault(); }, { capture: true, once: true })); diff --git a/tests/e2e/board-attachment-receipts.spec.ts b/tests/e2e/board-attachment-receipts.spec.ts index 4b89c54f1a3..044e1d03772 100644 --- a/tests/e2e/board-attachment-receipts.spec.ts +++ b/tests/e2e/board-attachment-receipts.spec.ts @@ -3,6 +3,7 @@ import { expect, test, type APIRequestContext, + type APIResponse, type Locator, type Page, } from "@playwright/test"; @@ -18,7 +19,7 @@ type Attachment = { type Comment = { id: string; body: string; clientRequestId?: string | null }; async function body( - response: Awaited>, + response: Pick, ): Promise { expect(response.ok(), `${response.status()}: ${await response.text()}`).toBe( true, @@ -612,7 +613,7 @@ test("announcement dismissal handler tolerates detachment after its element snap const dismiss = page.getByRole("complementary", { name: "Paperclip announcements" }).getByRole("button", { name: "Dismiss announcement" }); const snapshot = await dismiss.elementHandles(); expect(snapshot).toHaveLength(1); - await snapshot[0]!.evaluate(element => element.remove()); + await snapshot[0]!.evaluate(element => (element as ChildNode).remove()); dismiss.elementHandles = async () => snapshot; await dismissAnnouncement(page, dismiss); await expect(dismiss).toHaveCount(0); diff --git a/tests/e2e/chat-adapters-ui-providers.spec.ts b/tests/e2e/chat-adapters-ui-providers.spec.ts index 6ebe799976c..bd3f627f2bc 100644 --- a/tests/e2e/chat-adapters-ui-providers.spec.ts +++ b/tests/e2e/chat-adapters-ui-providers.spec.ts @@ -614,13 +614,6 @@ test.describe.serial("native chat adapter UI", () => { await expectMinimumProviderSetup(page, provider); await fillProviderSetup(page, provider); - if (provider.provider === "github") { - // A completed click does not mean the async configure request reached the mock. - await expect.poll(() => mock.setupAttempts).toBe(2); - expect(mock.githubPrivateKeyMatchedFile).toBe(true); - expect(mock.githubPrivateKeyMatchedPaste).toBe(true); - } - if (provider.provider === "telegram") { const submittedToken = "123456:e2e-redacted"; // The next step intentionally has its own identity-readiness alert. @@ -762,25 +755,19 @@ test.describe.serial("native chat adapter UI", () => { { id: `resource-${provider.provider}`, enabled: true }, ]); - if (provider.provider === "github") { + const directMessages = page.getByRole("switch", { + name: "Allow direct messages", + }); + await expect(directMessages).toBeVisible(); + if (provider.provider === "discord") { await expect( - page.getByRole("heading", { name: "Private conversations" }), - ).toHaveCount(0); - } else { - const directMessages = page.getByRole("switch", { - name: "Allow direct messages", - }); - await expect(directMessages).toBeVisible(); - if (provider.provider === "discord") { - await expect( - page.getByText( - "People must also enable Direct Messages in their shared Discord server’s Privacy Settings.", - ), - ).toBeVisible(); - } - await directMessages.click(); - await expect.poll(() => mock.allowDirectMessages).toBe(true); + page.getByText( + "People must also enable Direct Messages in their shared Discord server’s Privacy Settings.", + ), + ).toBeVisible(); } + await directMessages.click(); + await expect.poll(() => mock.allowDirectMessages).toBe(true); if (provider.provider === "microsoft-teams") { const groupChats = page.getByRole("switch", { name: "Allow group chats", @@ -978,93 +965,13 @@ test.describe.serial("native chat adapter UI", () => { expect(new URL(page.url()).searchParams.get("reconnect")).toBe("1"); await expect( page.getByRole("heading", { - name: - provider.provider === "github" - ? "Reconnect GitHub App" - : provider.provider === "slack" ? "Add Slack credentials" : provider.setupHeading, + name: provider.provider === "slack" ? "Add Slack credentials" : provider.setupHeading, }), ).toBeVisible(); await expect( page.getByRole("button", { name: "Choose an active agent" }), ).toHaveCount(0); - if (provider.provider === "github") { - const connectButton = page.getByRole("button", { - name: provider.setupButton, - }); - await expect( - page.getByText( - /Under the target user or organization, create a new GitHub App/, - ), - ).toHaveCount(0); - await expect( - page.getByText( - /Leave App ID and private key blank to reuse saved credentials/, - ), - ).toBeVisible(); - await page.getByLabel("GitHub App ID").fill("123456"); - await page - .getByLabel("Private key (PEM)") - .fill("reconnect-private-key"); - await expect(connectButton).toBeEnabled(); - await page.evaluate((buttonName) => { - const state = window as typeof window & { - __githubConnectEnabledAfterRotation?: boolean; - }; - state.__githubConnectEnabledAfterRotation = false; - new MutationObserver(() => { - const button = [...document.querySelectorAll("button")].find( - (candidate) => candidate.textContent?.trim() === buttonName, - ); - if (button instanceof HTMLButtonElement && !button.disabled) { - state.__githubConnectEnabledAfterRotation = true; - } - }).observe(document.body, { - attributes: true, - childList: true, - subtree: true, - }); - }, provider.setupButton); - await page - .getByRole("button", { name: "Regenerate webhook secret" }) - .click(); - await expect(connectButton).toBeDisabled(); - await page.waitForTimeout(100); - expect( - await page.evaluate( - () => - ( - window as typeof window & { - __githubConnectEnabledAfterRotation?: boolean; - } - ).__githubConnectEnabledAfterRotation, - ), - ).toBe(false); - - await page.goBack(); - await page.getByText("Connection health and controls", { exact: true }).click(); - await expect( - page.getByRole("heading", { name: "Connection activity" }), - ).toBeVisible(); - await page - .getByRole("button", { name: "Reconnect", exact: true }) - .click(); - await expect( - page.getByRole("heading", { name: "Reconnect GitHub App" }), - ).toBeVisible(); - await page.getByLabel("GitHub App ID").fill("123456"); - await page - .getByLabel("Private key (PEM)") - .fill("reconnect-private-key"); - await expect( - page.getByRole("button", { name: provider.setupButton }), - ).toBeDisabled(); - mock.setWebhookVerified(); - await expect( - page.getByRole("button", { name: provider.setupButton }), - ).toBeEnabled(); - } - mock.setStatus("active"); await page.goto( `/${seed.prefix}/apps/chat/endpoint-${provider.provider}/activity`, diff --git a/tests/e2e/chat-adapters-ui.shared.ts b/tests/e2e/chat-adapters-ui.shared.ts index afa8a9686b9..5022b3afd17 100644 --- a/tests/e2e/chat-adapters-ui.shared.ts +++ b/tests/e2e/chat-adapters-ui.shared.ts @@ -284,6 +284,9 @@ export function endpointFixture(provider: ProviderCase, seed: Seed) { slackSetupMethod: undefined as "managed" | "automatic" | "manual" | "existing" | undefined, slackRegistration: undefined as SlackRegistrationState | undefined, slackApp: undefined as SlackAppConfiguration | undefined, + slackAccount: undefined as Record | undefined, + slackAvatar: undefined as Record | undefined, + github: undefined as Record | undefined, slackOAuthCallbackUri: "https://paperclip.example.test/api/chat-slack/oauth/callback", step: "provider_setup", authorizationUrl: @@ -301,7 +304,7 @@ export function endpointFixture(provider: ProviderCase, seed: Seed) { messagingEndpoint: `https://paperclip.example.test/api/chat-webhooks/public-${provider.provider}/microsoft-teams`, command: provider.provider === "slack" ? "/maya-public" : undefined, testStartedAt: null as string | null, - webhookVerifiedAt: null, + webhookVerifiedAt: null as string | null, webhookSecretConfigured: false, }, healthMessage: null, diff --git a/tests/e2e/company-context-refresh.spec.ts b/tests/e2e/company-context-refresh.spec.ts index 0a90d8a6fec..04f0698fb07 100644 --- a/tests/e2e/company-context-refresh.spec.ts +++ b/tests/e2e/company-context-refresh.spec.ts @@ -18,8 +18,8 @@ test.beforeAll(async () => { server: { host: "127.0.0.1", port: 0 }, plugins: [{ name: "company-context-regression", - resolveId(id) { if (id === "/src/__company_context_probe.tsx") return probePath; }, - load(id) { + resolveId(id: string) { if (id === "/src/__company_context_probe.tsx") return probePath; }, + load(id: string) { if (id !== probePath) return; return ` import React from "react"; @@ -39,7 +39,7 @@ test.beforeAll(async () => { }; `; }, - configureServer(vite) { + configureServer(vite: ViteDevServer) { vite.middlewares.use((req, res, next) => { if (req.url !== "/") return next(); res.setHeader("Content-Type", "text/html"); diff --git a/tests/e2e/connection-intents.spec.ts b/tests/e2e/connection-intents.spec.ts index 34ceaf43909..a2b37a9c4d8 100644 --- a/tests/e2e/connection-intents.spec.ts +++ b/tests/e2e/connection-intents.spec.ts @@ -250,7 +250,7 @@ async function waitForAgentRun( companyId: string, agentId: string, ) { - let terminalRun: { id: string; status: string } | null = null; + let terminalRun = null as { id: string; status: string } | null; await expect .poll( async () => { diff --git a/tests/e2e/execution-recovery/recovery.spec.ts b/tests/e2e/execution-recovery/recovery.spec.ts index 0ef482333e7..18ca0b85faf 100644 --- a/tests/e2e/execution-recovery/recovery.spec.ts +++ b/tests/e2e/execution-recovery/recovery.spec.ts @@ -109,7 +109,7 @@ for (const journey of [ }); try { const port = await listenOnFetchAllowedPort(fixture); - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, PAPERCLIP_RECOVERY_CEO_LINEAGE: journey === "ceo_lineage" ? "1" : "0", IN_FEED_FIXTURE_KEY: "not-a-real-model-key", diff --git a/tests/e2e/in-feed-native/completion-stream.spec.ts b/tests/e2e/in-feed-native/completion-stream.spec.ts index 3095ca5b01a..e63c95bc6b4 100644 --- a/tests/e2e/in-feed-native/completion-stream.spec.ts +++ b/tests/e2e/in-feed-native/completion-stream.spec.ts @@ -34,7 +34,7 @@ test('a completion tool does not cut off a delayed final answer', async ({ page let child: ChildProcess | undefined; let logs = ''; try { - const env = { ...process.env, STREAM_FIXTURE_KEY: 'not-a-real-model-key', NODE_ENV: 'test', + const env: NodeJS.ProcessEnv = { ...process.env, STREAM_FIXTURE_KEY: 'not-a-real-model-key', NODE_ENV: 'test', PATH: `${root}/tests/e2e/fixtures/completion-stream-bin:${process.env.PATH}` }; delete env.DATABASE_URL; delete env.DATABASE_MIGRATION_URL; diff --git a/tests/e2e/in-feed-native/connections.spec.ts b/tests/e2e/in-feed-native/connections.spec.ts index c242a14ea6a..5b3f124cfc8 100644 --- a/tests/e2e/in-feed-native/connections.spec.ts +++ b/tests/e2e/in-feed-native/connections.spec.ts @@ -25,7 +25,7 @@ for (const journey of ['connect', 'decline', 'restart'] as const) test(`fresh na }); try { const port = await listenOnFetchAllowedPort(fixture); - const env = { ...process.env, IN_FEED_FIXTURE_KEY: 'not-a-real-model-key', NODE_ENV: 'test', PAPERCLIP_TEST_CONNECTION_DELIVERY_HOLD: journey === 'restart' ? '1' : '0', PATH: `${root}/tests/e2e/fixtures/in-feed-bin:${process.env.PATH}` }; + const env: NodeJS.ProcessEnv = { ...process.env, IN_FEED_FIXTURE_KEY: 'not-a-real-model-key', NODE_ENV: 'test', PAPERCLIP_TEST_CONNECTION_DELIVERY_HOLD: journey === 'restart' ? '1' : '0', PATH: `${root}/tests/e2e/fixtures/in-feed-bin:${process.env.PATH}` }; delete env.DATABASE_URL; delete env.DATABASE_MIGRATION_URL; processHandle = spawn(process.execPath, ['cli/node_modules/tsx/dist/cli.mjs', 'cli/src/index.ts', 'test-drive', '--harness', 'codex', '--api-key-env', 'IN_FEED_FIXTURE_KEY', '--company-name', 'In-feed native fixture', '--no-browser'], { cwd: root, env, detached: true, stdio: ['ignore', 'pipe', 'pipe'] }); processHandle.stdout!.on('data', (chunk) => { logs += chunk.toString(); }); diff --git a/tests/e2e/playwright-composer-stop.config.ts b/tests/e2e/playwright-composer-stop.config.ts index a58f8507034..f98508ae384 100644 --- a/tests/e2e/playwright-composer-stop.config.ts +++ b/tests/e2e/playwright-composer-stop.config.ts @@ -27,7 +27,7 @@ if (fixture) { { mode: 0o755 }, ); } -const server = base.webServer as Exclude; +const server = base.webServer as Exclude; export default defineConfig({ ...base, testMatch: "composer-stop.spec.ts", diff --git a/tests/e2e/playwright-transcript-delivery.config.ts b/tests/e2e/playwright-transcript-delivery.config.ts index f2e35aa2d67..c1d6eea22d3 100644 --- a/tests/e2e/playwright-transcript-delivery.config.ts +++ b/tests/e2e/playwright-transcript-delivery.config.ts @@ -5,7 +5,7 @@ import { defineConfig } from "@playwright/test"; import base from "./playwright.config"; const providerHome = fs.mkdtempSync(path.join(os.tmpdir(), "transcript-provider-")); -const server = base.webServer as Exclude; +const server = base.webServer as Exclude; export default defineConfig({ ...base, testMatch: "transcript-delivery.journey.ts", diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json new file mode 100644 index 00000000000..ad82b65f0a9 --- /dev/null +++ b/tests/e2e/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "allowJs": true, + "jsx": "react-jsx", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "paths": { + "@/*": ["../../ui/src/*"], + "@paperclipai/shared": ["../../packages/shared/src/index.ts"] + }, + "rootDir": "../..", + "types": ["node"], + "typeRoots": ["../../server/node_modules/@types"] + }, + "include": ["./**/*.ts", "./**/*.tsx", "../../ui/src/vite-env.d.ts", "../../server/src/types/express.d.ts"] +} diff --git a/tests/e2e/tsconfig.layers.json b/tests/e2e/tsconfig.layers.json new file mode 100644 index 00000000000..f1c68632282 --- /dev/null +++ b/tests/e2e/tsconfig.layers.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": false, + "declarationMap": false + }, + "references": [{ "path": "../../server/node_modules/.cache/paperclip-typecheck/tsconfig.declarations.json" }] +} diff --git a/tests/perf/agent-chat/README.md b/tests/perf/agent-chat/README.md index 76234b9dcd7..1c62692be68 100644 --- a/tests/perf/agent-chat/README.md +++ b/tests/perf/agent-chat/README.md @@ -38,6 +38,47 @@ visible recent conversation at its intended position with an editable composer, not the loading shell. Retain per-frame geometry and server timings alongside sample medians/ranges; do not treat fewer transcript requests as latency proof. +Terminal native transcripts read during a visit are retained in the application's +query cache by company, run, status and projection. A cached return can reveal +those rows immediately while the normal cursor-based catch-up read runs in the +background. Live runs and uncached projections still wait for hydration. Cache +reuse does not remove reconnect or retry reads. Return navigation also exercises +cached question editors: a scroll event queued before their layout grows must +retain bottom-follow, while an upward gesture and Back reading anchors still win. +Mounted rows survive query-cache eviction. Expanding a cached preview restores +the full projection before catch-up, including older rows and late events. + +### Local comparison, 2026-11-06 + +Compared baseline `523818ea9bf81f96abf4e6b73e81c2ce87518c07` with the +terminal-history cache and scroll-event fix using production assets, Chromium +151.0.7922.34, a 1440×900 viewport and the same retained 600-message/300-run +fixture (with the rig's saved live replies). Five alternating pairs per profile; +no CPU throttling, profiling, builds or this task's test suites during samples. +Other worktrees remained active on the shared host. Browser contexts had fresh +HTTP/application caches for direct opens; in-app entry started from a freshly +loaded task collection; return reused that visit's application cache. + +Times are median milliseconds, with min–max in parentheses. Readiness means a +revealed conversation at the bottom with an editable composer. + +| Network | Journey | Baseline | Candidate | Median paired saving | +| --- | --- | --- | --- | --- | +| Loopback | Direct open | 439 (420–489) | 425 (409–474) | 10 | +| Loopback | In-app entry | 277 (268–284) | 279 (271–285) | −3 | +| Loopback | Cached return | 115 (106–120) | 118 (113–120) | −4 | +| 150 ms latency | Direct open | 3586 (3573–3597) | 3575 (3418–3595) | 10 | +| 150 ms latency | In-app entry | 1956 (1930–1959) | 1958 (1955–1961) | −4 | +| 150 ms latency | Cached return | 547 (538–553) | 131 (120–137) | 416 | + +Only the slower-network cached return establishes a reliable speed improvement: +all five paired deltas are positive. Direct opens and uncached navigation remain +unchanged. The unchanged server's issue-read timing medians were 18.2 vs 16.9 ms +on the latency profile (20 reads per variant). Baseline and candidate entry assets +were `index-8fGouAl3.js` and `index-BVkn60XK.js`. This is local synthetic proof, +not a claim about deployed staging latency. The cached-question reproduction went +from a persistent 198-pixel bottom gap to zero. + The chat's pending task questions use the attention service's scoped question projection. It keeps current access, audience, dismissal, queue, snooze and archive rules without constructing the company feed. Ranks are relative to the returned diff --git a/tests/perf/agent-chat/agent-chat-startup.spec.ts b/tests/perf/agent-chat/agent-chat-startup.spec.ts index f78e1ff86a1..6f03b2c4c5c 100644 --- a/tests/perf/agent-chat/agent-chat-startup.spec.ts +++ b/tests/perf/agent-chat/agent-chat-startup.spec.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { expect, test, type Page } from "@playwright/test"; import { longChatFixture, type ChatFixture } from "./agent-chat-fixture"; +import { coordinatorTaskQuestion, coordinatorTaskQuestionAttention } from "../../../ui/src/fixtures/coordinatorTaskQuestion"; let fixture: ChatFixture; let chatPath: string; @@ -96,6 +97,38 @@ test("cold long chat reveals one recent window without full-history reads or red await page.screenshot({ path: info.outputPath("recent-chat.png") }); }); +test("clicking away and reopening cached questions keeps the latest reply in view", async ({ page }, info) => { + const question = { + ...coordinatorTaskQuestion, + payload: { ...coordinatorTaskQuestion.payload, questionSet: { + schema: "paperclip.question_set.v1", questions: [ + { id: "reply", prompt: "Give a reply", answerMode: "text", required: true }, + ], + } }, + }; + await page.route("**/pending-questions", route => route.fulfill({ json: [coordinatorTaskQuestionAttention] })); + await page.route("**/api/issues/task-launch/interactions", async route => { + await new Promise(resolve => setTimeout(resolve, 600)); + await route.fulfill({ json: [question] }); + }); + const gap = () => scroller(page).evaluate(root => root.scrollHeight - root.clientHeight - root.scrollTop); + await page.goto(chatPath); await ready(page); + await expect(page.getByTestId("question-text-answer-composer")).toBeVisible(); + await expect.poll(gap).toBeLessThanOrEqual(2); + await editor(page).fill("Keep my draft on return."); + const navigation = page.viewportSize()!.width < 768 + ? page.getByRole("navigation", { name: "Mobile navigation" }) + : page.getByRole("navigation", { name: "Workspace navigation" }); + await navigation.getByRole("link", { name: "Tasks", exact: true }).click(); + await navigation.getByRole("link", { name: page.viewportSize()!.width < 768 ? /^Chat/ : "History Fixture", exact: true }).click(); + await ready(page); + await expect.poll(gap).toBeLessThanOrEqual(2); + await expect(editor(page)).toContainText("Keep my draft on return."); + await page.waitForTimeout(400); + expect(await gap()).toBeLessThanOrEqual(2); + await page.screenshot({ path: info.outputPath("cached-return.png") }); +}); + test("upward scrolling loads one delayed page and preserves the row, viewport and draft", async ({ page }, info) => { let release!: () => void; const held = new Promise(resolve => { release = resolve; }); @@ -129,7 +162,7 @@ test("upward scrolling loads one delayed page and preserves the row, viewport an test("failed history stops automatic retries and leaves a working inline retry", async ({ page }) => { let attempts = 0; await page.route(`**/api/issues/${fixture.issue.id}/comments?**`, async route => { - if (++attempts === 1) await route.fulfill({ status: 500, json: { error: "Synthetic older-page failure" } }); + if (++attempts === 1) await route.fulfill({ status: 422, json: { error: "Synthetic older-page failure" } }); else await route.continue(); }); await page.goto(chatPath); await ready(page); @@ -157,7 +190,7 @@ test("Back restores paged history and the reading position", async ({ page }) => ? page.getByRole("navigation", { name: "Mobile navigation" }) : page.getByRole("navigation", { name: "Workspace navigation" }); await navigation.getByRole("link", { name: "Tasks", exact: true }).click(); - await expect(page).toHaveURL(/\/issues$/); + await expect(page).toHaveURL(/\/issues(?:\?|$)/); await page.goBack(); await ready(page); await expect(page.locator('[id^="comment-"]')).toHaveCount(70); expect(Math.abs(await offset(page, saved.id) - saved.top)).toBeLessThanOrEqual(2); diff --git a/tests/perf/issue-detail/issue-detail.perf.spec.ts b/tests/perf/issue-detail/issue-detail.perf.spec.ts index 2dc5cca727d..e469e442533 100644 --- a/tests/perf/issue-detail/issue-detail.perf.spec.ts +++ b/tests/perf/issue-detail/issue-detail.perf.spec.ts @@ -10,13 +10,15 @@ const PAGE_READY_TIMEOUT_MS = 90_000; const HEADER_MEASURE = "issue-detail:navigate→header-paint"; const CONTENT_MEASURE = "issue-detail:navigate→content-paint"; -type Profile = { - name: "unthrottled" | "fast-4g-4x-cpu"; - latencyMs?: number; - downloadBytesPerSecond?: number; - uploadBytesPerSecond?: number; - cpuSlowdownRate?: number; -}; +type Profile = + | { name: "unthrottled"; latencyMs?: undefined } + | { + name: "fast-4g-4x-cpu"; + latencyMs: number; + downloadBytesPerSecond: number; + uploadBytesPerSecond: number; + cpuSlowdownRate: number; + }; type NetworkRecord = { requestId: string; diff --git a/tests/perf/issue-detail/rich-loading-fixture.ts b/tests/perf/issue-detail/rich-loading-fixture.ts index 7b32a59f6cb..a9fc3a0e539 100644 --- a/tests/perf/issue-detail/rich-loading-fixture.ts +++ b/tests/perf/issue-detail/rich-loading-fixture.ts @@ -83,7 +83,7 @@ export async function richLoadingFixture(request: APIRequestContext): Promise ({ - id: randomUUID(), companyId: company.id, issueId: issue.id, authorType: "agent", authorAgentId: agent.id, + id: randomUUID(), companyId: company.id, issueId: issue.id, authorType: "agent" as const, authorAgentId: agent.id, body: `## ${index === 199 ? "Latest investigation findings" : `Investigation update ${index}`}\n\n` + "Verified the saved workspace, inspected the reported behavior, and checked that the evidence still matches the current revision. ".repeat(index === 199 ? 12 : 5) + (index === 199 ? "\n\n[Read the saved analysis](#document-analysis) and [verification results](#document-verification).\n\n| Check | Result |\n|---|---|\n| Saved evidence | Complete |\n| Review | Waiting for your decision |" : ""), diff --git a/tests/perf/issue-detail/window-globals.d.ts b/tests/perf/issue-detail/window-globals.d.ts new file mode 100644 index 00000000000..4abf722fa69 --- /dev/null +++ b/tests/perf/issue-detail/window-globals.d.ts @@ -0,0 +1,9 @@ +export {}; + +// Mirrors the declarations in ui/src/lib/issue-detail-performance.ts, which these specs set from init scripts. +declare global { + interface Window { + __PAPERCLIP_ISSUE_DETAIL_NAVIGATE_START__?: number; + __PAPERCLIP_ISSUE_DETAIL_PROFILE__?: boolean; + } +} diff --git a/tests/runner-e2e/README.md b/tests/runner-e2e/README.md index 7e29d3539cf..2cdca967e73 100644 --- a/tests/runner-e2e/README.md +++ b/tests/runner-e2e/README.md @@ -2139,7 +2139,10 @@ retirement. Launcher cancellation first signals its outer group, so Playwright and the wrapper own graceful server shutdown. It allows 45 seconds for that chain before bounded 5-second forced cleanup of revalidated descendants. Normal launcher exit uses the -same retained descendant inventory. Zombies count as stopped; an unavailable or +same retained descendant inventory. Zombies count as stopped, including when +macOS answers a group signal with EPERM because every remaining member is an +unreaped zombie; EPERM stays fatal while a fresh inspection shows a running +member. An unavailable or uncertain identity inspection triggers bounded direct-child-only retirement and still fails the tree audit. Failed cleanup preserves the temporary state. Graceful owners are selected from the same validated table used for delivery; ESRCH allows diff --git a/tests/runner-e2e/process-tree-owner.test.ts b/tests/runner-e2e/process-tree-owner.test.ts index f53b9d4340f..4f32f088ca7 100644 --- a/tests/runner-e2e/process-tree-owner.test.ts +++ b/tests/runner-e2e/process-tree-owner.test.ts @@ -174,3 +174,38 @@ it.skipIf(process.platform === 'win32')('bounds fallback even when the known dir expect(signals).toEqual(['SIGTERM', 'SIGKILL']); } finally { owner.stopObserving(); } }); + +function epermFixture(afterSignal: ProcessObservation[]) { + let table = [row(process.pid, 1, 10), row(100, process.pid, 100), row(200, 100, 200)]; + const root = { pid: 100, exitCode: null as number | null, signalCode: null as NodeJS.Signals | null }; + const attempts: Array<[number, NodeJS.Signals]> = []; + const owner = createProcessTreeOwner(root as ChildProcess, { + readTable: async () => table, + signalGroup: (pid, signal) => { + attempts.push([pid, signal]); + if (pid !== 200) return; + // Darwin killpg reports EPERM, not ESRCH, once every group member exited + // but is not reaped yet. The group exits between inspection and signal. + table = [row(process.pid, 1, 10), row(100, process.pid, 100), ...afterSignal]; + throw Object.assign(new Error("kill EPERM"), { code: "EPERM" }); + }, + }); + return { owner, attempts }; +} + +it.skipIf(process.platform === "win32")("treats EPERM from a group that is now only zombies as already exited", async () => { + const f = epermFixture([row(200, 100, 200, "start-200", "Z")]); + try { + await f.owner.observe(); + await expect(f.owner.signal("SIGKILL")).resolves.toEqual([100]); + expect(f.attempts).toEqual([[200, "SIGKILL"], [100, "SIGKILL"]]); + } finally { f.owner.stopObserving(); } +}); + +it.skipIf(process.platform === "win32")("keeps EPERM fatal while the group still has a running member", async () => { + const f = epermFixture([row(200, 100, 200, "start-200", "Z"), row(201, 200, 200)]); + try { + await f.owner.observe(); + await expect(f.owner.signal("SIGKILL")).rejects.toThrow("kill EPERM"); + } finally { f.owner.stopObserving(); } +}); diff --git a/tests/runner-e2e/process-tree-owner.ts b/tests/runner-e2e/process-tree-owner.ts index 3dcae17791f..ed12bf10d72 100644 --- a/tests/runner-e2e/process-tree-owner.ts +++ b/tests/runner-e2e/process-tree-owner.ts @@ -138,7 +138,14 @@ export function createProcessTreeOwner(root: ChildProcess, options: { const ids = new Set(preserved.map(group => group.processGroupId)); return new Set(liveGroups().filter(group => !ids.has(group.processGroupId)).map(group => group.processGroupId)); } - function signalSnapshot(signal: NodeJS.Signals, selected?: ReadonlySet) { + /** Darwin killpg reports EPERM, not ESRCH, when every remaining group member + * has exited but is not yet reaped. A zombie-only group needs no signal; any + * running member keeps EPERM a real failure. */ + async function exitedSinceSnapshot(pid: number) { + const fresh = await readTable(); + return !!fresh && !fresh.some(row => row.processGroupId === pid && running(row)); + } + async function signalSnapshot(signal: NodeJS.Signals, selected?: ReadonlySet) { const currentProcessGroupId = table.find(row => row.pid === process.pid)?.processGroupId ?? null; if (currentProcessGroupId === null) throw new Error("Cleanup caller group identity is unavailable"); const live = liveGroups(); @@ -147,14 +154,18 @@ export function createProcessTreeOwner(root: ChildProcess, options: { for (const pid of ordered) { if (selected && !selected.has(pid)) continue; try { signalGroup(pid, signal); } - catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; else continue; } + catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code === "ESRCH" || (code === "EPERM" && await exitedSinceSnapshot(pid))) continue; + throw error; + } signaled.push(pid); } return signaled; } async function signal(signal: NodeJS.Signals, selected?: ReadonlySet) { await observe(); - return signalSnapshot(signal, selected); + return await signalSnapshot(signal, selected); } function gracefulRoots() { const live = liveGroups(); @@ -183,7 +194,7 @@ export function createProcessTreeOwner(root: ChildProcess, options: { // Choose and deliver from one validated table. ESRCH grants no ownership // coverage: the next grace poll can select that branch's surviving owner. const selected = new Set([...gracefulRoots()].filter(pid => !covered.has(pid))); - const delivered = signalSnapshot(signal, selected); + const delivered = await signalSnapshot(signal, selected); for (const pid of delivered) { if (pid === root.pid) directGraceDelivered = true; covered.add(pid); diff --git a/tests/storybook-visual/agent-personas.config.ts b/tests/storybook-visual/agent-personas.config.ts index bfda1740b67..bbdd8fbf8c6 100644 --- a/tests/storybook-visual/agent-personas.config.ts +++ b/tests/storybook-visual/agent-personas.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ snapshotPathTemplate: "{testDir}/.snapshots/agent-personas/{arg}{ext}", use: { browserName: "chromium", baseURL: process.env.PAPERCLIP_PERSONA_STORYBOOK_URL ?? "http://localhost:6106", - viewport: { width: 1200, height: 900 }, deviceScaleFactor: 1, reducedMotion: "reduce", + viewport: { width: 1200, height: 900 }, deviceScaleFactor: 1, contextOptions: { reducedMotion: "reduce" }, launchOptions: { args: ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"] }, }, }); diff --git a/tests/storybook-visual/agent-personas.spec.ts b/tests/storybook-visual/agent-personas.spec.ts index 555a8e653ec..d735eac2331 100644 --- a/tests/storybook-visual/agent-personas.spec.ts +++ b/tests/storybook-visual/agent-personas.spec.ts @@ -24,7 +24,7 @@ test("500 avatars load images without WebGL, live modules or avatar frame loops" await page.addInitScript(() => { (window as any).__personaProbe = { webgl: 0, frames: 0 }; const getContext = HTMLCanvasElement.prototype.getContext; - HTMLCanvasElement.prototype.getContext = function (kind: string, ...args: any[]) { + HTMLCanvasElement.prototype.getContext = function (this: HTMLCanvasElement, kind: string, ...args: any[]) { if (kind.includes("webgl")) (window as any).__personaProbe.webgl++; return (getContext as any).call(this, kind, ...args); } as any; diff --git a/tests/storybook-visual/app-shell-design.spec.ts b/tests/storybook-visual/app-shell-design.spec.ts index 1f55ff87cfd..17992c32723 100644 --- a/tests/storybook-visual/app-shell-design.spec.ts +++ b/tests/storybook-visual/app-shell-design.spec.ts @@ -140,6 +140,7 @@ test("right divider matches the left highlight, resizes in both directions, pers }); test("both pane dividers animate a double-click reset while manual sizing stays immediate", async ({ page }) => { + await page.emulateMedia({ reducedMotion: "no-preference" }); await openShell(page, "three-panes"); const left = page.getByRole("separator", { name: "Resize sidebar", exact: true }); const leftShell = page.locator(".design-page-navigation"); diff --git a/tests/storybook-visual/feature-gap-stories.config.ts b/tests/storybook-visual/feature-gap-stories.config.ts index d774c97fda9..c5a41bb9fa7 100644 --- a/tests/storybook-visual/feature-gap-stories.config.ts +++ b/tests/storybook-visual/feature-gap-stories.config.ts @@ -12,6 +12,6 @@ export default defineConfig({ use: { baseURL: process.env.PAPERCLIP_GAP_STORYBOOK_URL ?? "http://localhost:6136", viewport: { width: 1280, height: 900 }, - reducedMotion: "reduce", + contextOptions: { reducedMotion: "reduce" }, }, }); diff --git a/tests/storybook-visual/fireflies-pr.config.ts b/tests/storybook-visual/fireflies-pr.config.ts index 13bcabdcd24..3ea471f78f0 100644 --- a/tests/storybook-visual/fireflies-pr.config.ts +++ b/tests/storybook-visual/fireflies-pr.config.ts @@ -8,6 +8,6 @@ export default defineConfig({ retries: 0, outputDir: "./test-results/fireflies-pr", reporter: [["list"]], - use: { baseURL: "http://127.0.0.1:6149", viewport: { width: 1440, height: 1000 }, reducedMotion: "reduce", trace: "retain-on-failure" }, + use: { baseURL: "http://127.0.0.1:6149", viewport: { width: 1440, height: 1000 }, contextOptions: { reducedMotion: "reduce" }, trace: "retain-on-failure" }, webServer: { command: "node ../../scripts/serve-storybook-static.mjs --port 6149", url: "http://127.0.0.1:6149/index.json", reuseExistingServer: false }, }); diff --git a/tests/storybook-visual/imessage-photon.config.ts b/tests/storybook-visual/imessage-photon.config.ts index aaac3e55cef..6dc8bc0d5f9 100644 --- a/tests/storybook-visual/imessage-photon.config.ts +++ b/tests/storybook-visual/imessage-photon.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ testDir: ".", testMatch: "imessage-photon.spec.ts", workers: 2, timeout: 30_000, retries: 0, outputDir: "./test-results/imessage-photon", reporter: [["list"]], - use: { browserName: "chromium", baseURL: "http://127.0.0.1:6128", reducedMotion: "reduce" }, + use: { browserName: "chromium", baseURL: "http://127.0.0.1:6128", contextOptions: { reducedMotion: "reduce" } }, webServer: { command: "node ../../scripts/serve-storybook-static.mjs --port 6128", url: "http://127.0.0.1:6128/index.json", reuseExistingServer: false, diff --git a/tests/storybook-visual/in-feed-connections.config.ts b/tests/storybook-visual/in-feed-connections.config.ts index 32aa673a073..2325532f13d 100644 --- a/tests/storybook-visual/in-feed-connections.config.ts +++ b/tests/storybook-visual/in-feed-connections.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: '.', testMatch: 'in-feed-connections.spec.ts', workers: 4, timeout: 30_000, retries: 0, outputDir: './test-results/in-feed', reporter: [['list']], - use: { browserName: 'chromium', baseURL: 'http://127.0.0.1:6126', reducedMotion: 'reduce' }, + use: { browserName: 'chromium', baseURL: 'http://127.0.0.1:6126', contextOptions: { reducedMotion: 'reduce' } }, webServer: { command: 'node ../../scripts/serve-storybook-static.mjs --port 6126', url: 'http://127.0.0.1:6126/index.json', reuseExistingServer: false } }); diff --git a/tests/storybook-visual/playwright.config.ts b/tests/storybook-visual/playwright.config.ts index 0e7608f1859..2e099a3d53e 100644 --- a/tests/storybook-visual/playwright.config.ts +++ b/tests/storybook-visual/playwright.config.ts @@ -49,7 +49,7 @@ export default defineConfig({ deviceScaleFactor: 1, // JS-driven tickers/timers key off prefers-reduced-motion for // deterministic captures (CSS animations are already disabled). - reducedMotion: "reduce", + contextOptions: { reducedMotion: "reduce" }, baseURL: process.env.STORYBOOK_TEST_BASE_URL ?? `http://localhost:${storybookPort}`, }, webServer: process.env.STORYBOOK_TEST_BASE_URL ? undefined : { diff --git a/tests/storybook-visual/remote-mcp-connections.config.ts b/tests/storybook-visual/remote-mcp-connections.config.ts index 041b689c720..da791479e95 100644 --- a/tests/storybook-visual/remote-mcp-connections.config.ts +++ b/tests/storybook-visual/remote-mcp-connections.config.ts @@ -3,6 +3,6 @@ import { defineConfig } from "@playwright/test"; export default defineConfig({ testDir: ".", testMatch: "remote-mcp-connections.spec.ts", workers: 1, fullyParallel: false, timeout: 180_000, retries: 0, outputDir: "./test-results/remote-mcp", reporter: [["list"]], - use: { browserName: "chromium", baseURL: "http://127.0.0.1:6138", reducedMotion: "reduce", actionTimeout: 10_000, trace: "retain-on-failure" }, + use: { browserName: "chromium", baseURL: "http://127.0.0.1:6138", contextOptions: { reducedMotion: "reduce" }, actionTimeout: 10_000, trace: "retain-on-failure" }, webServer: { command: "node ../../scripts/serve-storybook-static.mjs --port 6138", url: "http://127.0.0.1:6138/index.json", reuseExistingServer: false }, }); diff --git a/tests/storybook-visual/repository-skill-packages.config.ts b/tests/storybook-visual/repository-skill-packages.config.ts index b969734a280..764afd07f02 100644 --- a/tests/storybook-visual/repository-skill-packages.config.ts +++ b/tests/storybook-visual/repository-skill-packages.config.ts @@ -11,6 +11,6 @@ export default defineConfig({ use: { baseURL: process.env.PAPERCLIP_REPOSITORY_STORYBOOK_URL ?? 'http://localhost:6106', viewport: { width: 1280, height: 900 }, - reducedMotion: 'reduce', + contextOptions: { reducedMotion: 'reduce' }, }, }); diff --git a/tests/storybook-visual/task-list-reproduction-trial.config.ts b/tests/storybook-visual/task-list-reproduction-trial.config.ts index e130e5f2ae4..17c0e04c082 100644 --- a/tests/storybook-visual/task-list-reproduction-trial.config.ts +++ b/tests/storybook-visual/task-list-reproduction-trial.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1, colorScheme: "dark", - reducedMotion: "reduce", + contextOptions: { reducedMotion: "reduce" }, locale: "en-US", timezoneId: "America/Los_Angeles", launchOptions: { executablePath: process.env.TRIAL_CHROMIUM_EXECUTABLE }, diff --git a/tests/storybook-visual/task-sidebar-loading.spec.ts b/tests/storybook-visual/task-sidebar-loading.spec.ts index 77bced22d48..c31526ee666 100644 --- a/tests/storybook-visual/task-sidebar-loading.spec.ts +++ b/tests/storybook-visual/task-sidebar-loading.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; -test.use({ viewport: { width: 1440, height: 960 }, reducedMotion: "no-preference" }); +test.use({ viewport: { width: 1440, height: 960 }, contextOptions: { reducedMotion: "no-preference" } }); async function sampleLayout(page: import("@playwright/test").Page, duration: number) { return page.evaluate(async (duration) => { diff --git a/tests/storybook-visual/trial-2/task-list-reproduction-trial.spec.ts b/tests/storybook-visual/trial-2/task-list-reproduction-trial.spec.ts index 395d3ba7c5e..46844829e97 100644 --- a/tests/storybook-visual/trial-2/task-list-reproduction-trial.spec.ts +++ b/tests/storybook-visual/trial-2/task-list-reproduction-trial.spec.ts @@ -10,7 +10,7 @@ const clock = "2026-10-15T00:00:00.000Z"; test.use({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1, - colorScheme: "dark", reducedMotion: "reduce", locale: "en-US", + colorScheme: "dark", contextOptions: { reducedMotion: "reduce" }, locale: "en-US", timezoneId: "America/Los_Angeles", launchOptions: { executablePath: process.env.TRIAL_CHROMIUM_EXECUTABLE }, }); diff --git a/tests/storybook-visual/typography-foundations.spec.ts b/tests/storybook-visual/typography-foundations.spec.ts index 6ebbadad34a..3e39dff26bf 100644 --- a/tests/storybook-visual/typography-foundations.spec.ts +++ b/tests/storybook-visual/typography-foundations.spec.ts @@ -390,7 +390,7 @@ test("mixed-font tool text shares one uninterrupted shimmer loop", async ({ page const animations = element.getAnimations({ subtree: true }); const animation = animations.find(candidate => (candidate as CSSAnimation).animationName === "shimmer-text-slide")!; const timing = animation.effect!.getTiming(); - const keyframes = animation.effect!.getKeyframes(); + const keyframes = (animation.effect as KeyframeEffect).getKeyframes(); // Sample the old dead zone: the background must keep moving after 60%. animation.pause(); animation.currentTime = Number(timing.duration) * 0.65; diff --git a/tests/tsconfig.playwright.json b/tests/tsconfig.playwright.json new file mode 100644 index 00000000000..615325aa972 --- /dev/null +++ b/tests/tsconfig.playwright.json @@ -0,0 +1,28 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "paths": { + "@/*": ["../ui/src/*"], + "@paperclipai/shared": ["../packages/shared/src/index.ts"] + }, + "rootDir": "..", + "types": ["node"], + "typeRoots": ["../server/node_modules/@types"] + }, + "include": [ + "ai-connections-app/**/*.ts", + "ai-connections-review/**/*.ts", + "canary-onboarding/**/*.ts", + "hiring-ai-connections/**/*.ts", + "html-preview/**/*.ts", + "perf/**/*.ts", + "release-smoke/**/*.ts", + "storybook-visual/**/*.ts", + "../ui/src/vite-env.d.ts" + ] +} diff --git a/ui/src/components/AttentionInteractionResolver.test.ts b/ui/src/components/AttentionInteractionResolver.replace.test.ts similarity index 100% rename from ui/src/components/AttentionInteractionResolver.test.ts rename to ui/src/components/AttentionInteractionResolver.replace.test.ts diff --git a/ui/src/components/task-chat/TaskMessageScroller.test.tsx b/ui/src/components/task-chat/TaskMessageScroller.test.tsx index 09989ab66a8..83b64236d03 100644 --- a/ui/src/components/task-chat/TaskMessageScroller.test.tsx +++ b/ui/src/components/task-chat/TaskMessageScroller.test.tsx @@ -186,6 +186,23 @@ describe("TaskMessageScroller", () => { expect(scroller().scrollTop).toBe(1000); }); + it("keeps following when content grows before the previous scroll event is delivered", async () => { + render(0); + fakeGeometry(scroller(), { clamp: true }); + render(1); + expect(scroller().scrollTop).toBe(600); + Object.defineProperty(scroller(), "scrollHeight", { value: 1400, configurable: true }); + await dispatch(scroller(), new Event("scroll", { bubbles: true })); + expect(scroller().scrollTop).toBe(1000); + expect(pill()).toBeNull(); + scroller().dispatchEvent(new WheelEvent("wheel", { bubbles: true, deltaY: -100 })); + Object.defineProperty(scroller(), "scrollHeight", { value: 1600, configurable: true }); + await scrollTo(scroller(), 900); + render(2); + expect(scroller().scrollTop).toBe(900); + await waitForPill(true); + }); + it("retains the reading anchor when a resize scroll event precedes the next width reflow", async () => { const resize: (() => void)[] = []; vi.stubGlobal("ResizeObserver", class { diff --git a/ui/src/components/task-chat/TaskMessageScroller.tsx b/ui/src/components/task-chat/TaskMessageScroller.tsx index e4d29ab117d..5a685117db6 100644 --- a/ui/src/components/task-chat/TaskMessageScroller.tsx +++ b/ui/src/components/task-chat/TaskMessageScroller.tsx @@ -68,6 +68,7 @@ export function TaskMessageScroller({ children, contentKey, className, scrolling const scrollbarIdleDelayRef = useRef(null); const [pillPhase, setPillPhase] = useState("hidden"); const previousScrollTop = useRef(0); + const previousScrollHeight = useRef(0); const touchStartY = useRef(null); const requestingHistory = useRef(false); const upwardIntent = useRef(false); @@ -186,6 +187,7 @@ export function TaskMessageScroller({ children, contentKey, className, scrolling // the reader scrolls or navigation selects a different position. rememberAnchor(false); previousScrollTop.current = el.scrollTop; + previousScrollHeight.current = el.scrollHeight; }, [rememberAnchor, scrollToBottom, hidePill]); const handleScroll = useCallback(() => { @@ -194,6 +196,12 @@ export function TaskMessageScroller({ children, contentKey, className, scrolling const moved = top !== previousScrollTop.current; const scrollingUp = top < previousScrollTop.current; previousScrollTop.current = top; + if (previousScrollHeight.current > 0 && pinnedRef.current && !scrollingUp && !upwardIntent.current && + ref.current.scrollHeight > previousScrollHeight.current) { + reconcileContent(); + hidePill(); + return; + } if ((scrollingUp || upwardIntent.current) && top <= 192 && !easingRef.current) requestEarlierHistory(); showScrollbarWhileScrolling(); // A growing composer shrinks this viewport. Some browsers dispatch the @@ -218,6 +226,7 @@ export function TaskMessageScroller({ children, contentKey, className, scrolling return; } pinnedRef.current = pinned; + if (pinned) upwardIntent.current = false; rememberAnchor(moved); if (pinned) hidePill(); else showPill(); @@ -229,10 +238,12 @@ export function TaskMessageScroller({ children, contentKey, className, scrolling showPill, showScrollbarWhileScrolling, requestEarlierHistory, + reconcileContent, ]); const handleJumpToLatest = useCallback(() => { pendingHashTarget.current = null; + upwardIntent.current = false; const el = ref.current; if (!el) return; if (isPinned()) { diff --git a/ui/src/components/transcript/useNativeRunTranscripts.test.tsx b/ui/src/components/transcript/useNativeRunTranscripts.test.tsx index 1e84d0f69db..633a3c16ac3 100644 --- a/ui/src/components/transcript/useNativeRunTranscripts.test.tsx +++ b/ui/src/components/transcript/useNativeRunTranscripts.test.tsx @@ -5,6 +5,7 @@ import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useNativeRunTranscripts } from "./useNativeRunTranscripts"; import { TRANSCRIPT_REQUEST_TIMEOUT_MS } from "./read-transcript-request"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const eventsMock = vi.hoisted(() => vi.fn()); const conversationEventsMock = vi.hoisted(() => vi.fn()); @@ -39,6 +40,7 @@ describe("useNativeRunTranscripts", () => { beforeEach(() => { vi.useFakeTimers(); eventsMock.mockReset(); + conversationEventsMock.mockReset(); container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); @@ -73,6 +75,111 @@ describe("useNativeRunTranscripts", () => { expect(state!.transcriptByRun.get("old")?.filter((entry) => entry.kind === "assistant").map((entry) => entry.text)).toEqual(["Earlier answer", "Latest answer"]); }); + it("reopens saved terminal history immediately and catches up without duplicating saved events", async () => { + const client = new QueryClient(); + let latest!: ReturnType; + const row = (seq: number) => ({ id: `event-${seq}`, runId: "saved", companyId: "company", createdAt: new Date().toISOString(), seq, eventType: "item.completed", payload: { prpEvent: { + schema: "paperclip.prp.event.v1", schemaVersion: 1, eventType: "item.completed", runId: "saved", + emittedAt: new Date().toISOString(), itemId: `answer-${seq}`, payload: { kind: "agentMessage", text: `Answer ${seq}` }, + } } }); + function Saved({ companyId = "company", compact = true, status = "succeeded", extra = false }) { + latest = useNativeRunTranscripts([ + { id: "saved", status, runtimeMode: "native", conversationOnly: compact }, + ...(extra ? [{ id: "earlier", status, runtimeMode: "native" as const, conversationOnly: compact }] : []), + ], { companyId }); + return null; + } + async function mount(props = {}) { + await act(async () => root.render()); + } + async function leave() { + await act(async () => root.render(null)); + } + conversationEventsMock.mockResolvedValueOnce([row(1)]); + await mount(); + expect(latest.hydratedRunIds.has("saved")).toBe(true); + await leave(); + let finish!: (rows: ReturnType[]) => void; + conversationEventsMock.mockImplementationOnce(() => new Promise(resolve => { finish = resolve; })); + await mount(); + expect(latest.isInitialHydrating).toBe(false); + expect(latest.transcriptByRun.has("saved")).toBe(true); + expect(conversationEventsMock.mock.calls.at(-1)?.[1]).toBe(1); + await act(async () => finish([row(2)])); + expect(latest.transcriptByRun.get("saved")?.filter(entry => entry.kind === "assistant").map(entry => entry.text)).toEqual(["Answer 1", "Answer 2"]); + await leave(); + conversationEventsMock.mockImplementation(() => new Promise(() => {})); + await mount(); + expect(conversationEventsMock.mock.calls.at(-1)?.[1]).toBe(2); + expect(latest.isInitialHydrating).toBe(false); + expect(latest.transcriptByRun.get("saved")?.filter(entry => entry.kind === "assistant").map(entry => entry.text)).toEqual(["Answer 1", "Answer 2"]); + await leave(); + conversationEventsMock.mockResolvedValue([]); + await mount(); + client.removeQueries({ queryKey: ["native-run-transcript"] }); + await mount({ extra: true }); + expect(latest.transcriptByRun.get("saved")?.filter(entry => entry.kind === "assistant").map(entry => entry.text)).toEqual(["Answer 1", "Answer 2"]); + conversationEventsMock.mockImplementation(() => new Promise(() => {})); + for (const props of [{ companyId: "other" }, { compact: false }, { status: "running" }]) { + await leave(); + eventsMock.mockImplementation(() => new Promise(() => {})); + await mount(props); + expect(latest.isInitialHydrating).toBe(true); + expect(latest.transcriptByRun.has("saved")).toBe(false); + } + await leave(); + client.clear(); + await mount(); + expect(latest.isInitialHydrating).toBe(true); + expect(latest.transcriptByRun.has("saved")).toBe(false); + await leave(); + client.clear(); + eventsMock.mockResolvedValueOnce([row(1), row(2)]); + await mount({ compact: false }); + conversationEventsMock.mockResolvedValueOnce([row(2)]); + await mount(); + await leave(); + conversationEventsMock.mockResolvedValueOnce([row(3)]); + await mount(); + expect(latest.transcriptByRun.get("saved")?.filter(entry => entry.kind === "assistant").map(entry => entry.text)).toEqual(["Answer 2", "Answer 3"]); + eventsMock.mockResolvedValueOnce([row(3)]); + await mount({ compact: false }); + expect(latest.transcriptByRun.get("saved")?.filter(entry => entry.kind === "assistant").map(entry => entry.text)).toEqual(["Answer 1", "Answer 2", "Answer 3"]); + await leave(); + client.clear(); + }); + + it("reuses a 300-run saved window without rescanning its cache on unchanged renders", async () => { + const client = new QueryClient(); + const runs = Array.from({ length: 300 }, (_, index) => ({ + id: `saved-${index}`, status: "succeeded", runtimeMode: "native" as const, conversationOnly: true, + })); + for (const run of runs) { + client.setQueryData(["native-run-transcript", "company", run.id, true, run.status], { events: [], cursor: 0 }); + } + const originalRead = client.getQueryData.bind(client); + let reads = 0; + vi.spyOn(client, "getQueryData").mockImplementation(((key) => { + if (key[0] === "native-run-transcript" && ++reads > runs.length) throw new Error("Saved history was rescanned"); + return originalRead(key); + }) as typeof client.getQueryData); + conversationEventsMock.mockImplementation(() => new Promise(() => {})); + let state!: ReturnType; + function Window({ tick }: { tick: number }) { + state = useNativeRunTranscripts(runs, { companyId: "company" }); + return {tick}; + } + for (let tick = 0; tick < 3; tick++) { + await act(async () => root.render()); + expect(state.isInitialHydrating).toBe(false); + expect(state.transcriptByRun.size).toBe(300); + } + expect(reads).toBe(300); + await act(async () => root.render(null)); + client.clear(); + vi.restoreAllMocks(); + }); + it("exposes event transport failures and retries terminal runs until recovery", async () => { eventsMock .mockRejectedValueOnce(new Error("event endpoint unavailable")) diff --git a/ui/src/components/transcript/useNativeRunTranscripts.ts b/ui/src/components/transcript/useNativeRunTranscripts.ts index e6ddf797ec1..fc1c0ceaa87 100644 --- a/ui/src/components/transcript/useNativeRunTranscripts.ts +++ b/ui/src/components/transcript/useNativeRunTranscripts.ts @@ -2,7 +2,8 @@ import { TranscriptSequence } from "../../lib/transcript-sequence"; import { useTranscriptLiveStream } from "../../context/LiveUpdatesProvider"; import { usePageVisibility } from "../../lib/page-visibility"; import { liveRunEvent, mergeRunEvents } from "../../lib/live-run-event"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { QueryClientContext } from "@tanstack/react-query"; import type { HeartbeatRunEvent } from "@paperclipai/shared"; import type { TranscriptEntry } from "@/adapters"; import { heartbeatsApi } from "@/api/heartbeats"; @@ -12,6 +13,14 @@ import { readTranscriptRequest } from "./read-transcript-request"; const EVENT_PAGE_SIZE = 1_000; const EVENT_POLL_INTERVAL_MS = 2_000; +interface SavedNativeHistory { + events: HeartbeatRunEvent[]; + cursor: number; +} + +const historyKey = (companyId: string, run: NativeRunTranscriptSource) => + ["native-run-transcript", companyId, run.id, Boolean(run.conversationOnly), run.status] as const; + export interface NativeRunTranscriptSource { id: string; status: string; @@ -29,6 +38,7 @@ function isLive(status: string): boolean { } export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource[], options: { companyId?: string | null; enabled?: boolean } = {}) { + const queryClient = useContext(QueryClientContext); const { visible } = usePageVisibility(); const nativeRunsKey = runs .filter((run) => run.runtimeMode === "native") @@ -41,7 +51,19 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource // eslint-disable-next-line react-hooks/exhaustive-deps [nativeRunsKey], ); + const savedHistory = useMemo(() => { + const saved = new Map(); + if (queryClient && options.companyId && options.enabled !== false) { + for (const run of nativeRuns) { + if (isLive(run.status)) continue; + const history = queryClient.getQueryData(historyKey(options.companyId, run)); + if (history) saved.set(run.id, history); + } + } + return saved; + }, [queryClient, options.companyId, options.enabled, nativeRuns]); const [eventsByRun, setEventsByRun] = useState>(new Map()); + const persistedEventsByRunRef = useRef(new Map()); const [errorsByRun, setErrorsByRun] = useState>(new Map()); const [hydratedRunIds, setHydratedRunIds] = useState>(new Set()); const [hydratedStatusByRun, setHydratedStatusByRun] = useState(new Map()); @@ -68,6 +90,7 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource previousCompanyRef.current = options.companyId; setEventsByRun(new Map()); setErrorsByRun(new Map()); setHydratedRunIds(new Set()); setHydratedStatusByRun(new Map()); projectionCacheRef.current.clear(); cursorByRunRef.current.clear(); conversationOnlyByRunRef.current.clear(); + persistedEventsByRunRef.current.clear(); sequencesRef.current.clear(); gapPendingRef.current.clear(); interruptedReadsRef.current.clear(); }, [options.companyId]); const stream = useTranscriptLiveStream(nativeRuns.filter(run => isLive(run.status)).map(run => run.id), options, event => { @@ -98,7 +121,12 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource const changedModes = new Set(); for (const run of nativeRuns) { const mode = Boolean(run.conversationOnly); - if (conversationOnlyByRunRef.current.get(run.id) !== mode) { changedModes.add(run.id); cursorByRunRef.current.delete(run.id); sequencesRef.current.delete(run.id); } + if (conversationOnlyByRunRef.current.get(run.id) !== mode) { + changedModes.add(run.id); cursorByRunRef.current.delete(run.id); sequencesRef.current.delete(run.id); + } + if (changedModes.has(run.id) || !persistedEventsByRunRef.current.has(run.id)) { + persistedEventsByRunRef.current.set(run.id, savedHistory.get(run.id)?.events ?? []); + } conversationOnlyByRunRef.current.set(run.id, mode); } let cancelled = false; @@ -111,7 +139,16 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource const next = new Map([...previous].filter(([id]) => retainedIds.has(id))); return next.size === previous.size ? previous : next; }; - setEventsByRun(retainMap); + setEventsByRun(previous => { + let next = retainMap(previous); + for (const [id, saved] of savedHistory) { + if (!next.has(id) || changedModes.has(id)) { + if (next === previous) next = new Map(next); + next.set(id, mergeRunEvents(saved.events, next.get(id) ?? [])); + } + } + return next; + }); setErrorsByRun(retainMap); setHydratedStatusByRun(retainMap); setHydratedRunIds((previous) => { @@ -123,6 +160,9 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource cursorByRunRef.current.delete(id); sequencesRef.current.delete(id); gapPendingRef.current.delete(id); } } + for (const id of persistedEventsByRunRef.current.keys()) { + if (!retainedIds.has(id)) persistedEventsByRunRef.current.delete(id); + } const refreshRun = async (run: NativeRunTranscriptSource) => { if (cancelled || historyReadsRef.current.has(run.id)) return; @@ -132,7 +172,8 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource historyReadsRef.current.set(run.id, controller.signal); let failed = false; try { - let cursor = cursorByRunRef.current.get(run.id) ?? 0; + const saved = savedHistory.get(run.id); + let cursor = cursorByRunRef.current.get(run.id) ?? saved?.cursor ?? 0; const observedBeforeRead = sequence(run.id).observed; const incoming: HeartbeatRunEvent[] = []; for (;;) { @@ -159,9 +200,19 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource if (!failed) gapPendingRef.current.delete(run.id); if (incoming.length > 0) setEventsByRun((previous) => { const next = new Map(previous); - next.set(run.id, mergeRunEvents(previous.get(run.id) ?? [], incoming)); + next.set(run.id, mergeRunEvents(previous.get(run.id) ?? saved?.events ?? [], incoming)); return next; }); + const previousEvents = persistedEventsByRunRef.current.get(run.id) ?? []; + const persistedEvents = incoming.length ? mergeRunEvents(previousEvents, incoming) : previousEvents; + persistedEventsByRunRef.current.set(run.id, persistedEvents); + if (!failed && !isLive(run.status) && queryClient && options.companyId) { + const key = historyKey(options.companyId, run); + queryClient.setQueryData(key, { + events: persistedEvents, + cursor: coveredCursor, + }); + } setErrorsByRun((previous) => { if (!previous.has(run.id)) return previous; const next = new Map(previous); @@ -217,12 +268,12 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource } for (const timer of timers.values()) window.clearTimeout(timer); }; - }, [nativeRuns, retryGeneration, visible, options.enabled, options.companyId, stream.healthy, stream.epoch]); + }, [nativeRuns, retryGeneration, visible, options.enabled, options.companyId, stream.healthy, stream.epoch, queryClient, savedHistory]); const transcriptByRun = useMemo(() => { const transcripts = new Map(); for (const run of nativeRuns) { - const events = eventsByRun.get(run.id); + const events = eventsByRun.get(run.id) ?? savedHistory.get(run.id)?.events; if (!events) continue; let cached = projectionCacheRef.current.get(run.id); if (!cached || cached.events !== events) { @@ -235,14 +286,16 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource if (!transcripts.has(id)) projectionCacheRef.current.delete(id); } return transcripts; - }, [eventsByRun, nativeRuns]); + }, [eventsByRun, nativeRuns, savedHistory]); const terminalHydratedRunIds = useMemo(() => new Set(nativeRuns .filter((run) => !isLive(run.status) && hydratedStatusByRun.get(run.id) === run.status) .map((run) => run.id)), [nativeRuns, hydratedStatusByRun]); + const availableRunIds = useMemo(() => new Set([...savedHistory.keys(), ...hydratedRunIds]), [savedHistory, hydratedRunIds]); + const availableTerminalRunIds = useMemo(() => new Set([...savedHistory.keys(), ...terminalHydratedRunIds]), [savedHistory, terminalHydratedRunIds]); return { - transcriptByRun, errorsByRun, hydratedRunIds, terminalHydratedRunIds, retry, - isInitialHydrating: nativeRuns.some((run) => !hydratedRunIds.has(run.id)), + transcriptByRun, errorsByRun, hydratedRunIds: availableRunIds, terminalHydratedRunIds: availableTerminalRunIds, retry, + isInitialHydrating: nativeRuns.some((run) => !availableRunIds.has(run.id)), }; }