From f65c7c3fd8568310c7039433c95f8f1df8bd2bbf Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 19 Apr 2026 15:33:24 -0400 Subject: [PATCH] feat(api): add GET /runs/{id}/files spec + RunFilesMeta schema Reintroduces the endpoint deleted in the April 5 server-only cleanup, this time targeted at the web UI (not the CLI). Route registered with not_implemented; real handler lands in Unit 5. - FileDiff gains optional change_kind, truncated, truncation_reason, binary, sensitive fields (all additive, back-compat) - New RunFilesMeta replaces PaginationMeta on PaginatedRunFileList (truncated, total_changed, to_sha, to_sha_committed_at, degraded, degraded_reason, patch, files_omitted_by_budget) - from_sha / to_sha query params reserved for future use (non-default values 400 in v1) Generated TS client picks up the new model; typecheck + openapi conformance tests pass. No existing consumers of PaginatedRunFileList['meta'] found in the monorepo. Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/api-reference/fabro-api.yaml | 146 +++++++++++++++++- lib/crates/fabro-server/src/server.rs | 2 + .../src/.openapi-generator/FILES | 1 + .../src/api/run-outputs-api.ts | 108 +++++++++++++ .../fabro-api-client/src/models/file-diff.ts | 40 ++++- .../fabro-api-client/src/models/index.ts | 1 + .../src/models/paginated-run-file-list.ts | 6 +- .../src/models/run-files-meta.ts | 63 ++++++++ 8 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 lib/packages/fabro-api-client/src/models/run-files-meta.ts diff --git a/docs/api-reference/fabro-api.yaml b/docs/api-reference/fabro-api.yaml index c00368aa2..8d6bcec5e 100644 --- a/docs/api-reference/fabro-api.yaml +++ b/docs/api-reference/fabro-api.yaml @@ -1013,6 +1013,61 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" + /api/v1/runs/{id}/files: + get: + operationId: listRunFiles + tags: [Run Outputs] + summary: List Run Files Changed + description: | + Returns the set of file changes produced by a run as a list of before/after diffs. + + While the run's sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`). + + Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`. + parameters: + - $ref: "#/components/parameters/RunId" + - $ref: "#/components/parameters/PageLimit" + - $ref: "#/components/parameters/PageOffset" + - name: from_sha + in: query + required: false + description: Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + schema: + type: string + pattern: "^[0-9a-f]{7,40}$" + - name: to_sha + in: query + required: false + description: Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + schema: + type: string + pattern: "^[0-9a-f]{7,40}$" + responses: + "200": + description: File diffs for the run + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedRunFileList" + "400": + description: Malformed query parameter (invalid SHA format, or non-default value for `from_sha`/`to_sha`). + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "404": + description: Run not found (or caller lacks access; returned as 404 to prevent enumeration). + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "503": + description: Transient sandbox subprocess failure (timeout, process kill). Safe to retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /api/v1/runs/{id}/stages/{stageId}/artifacts: get: operationId: listStageArtifacts @@ -4204,7 +4259,14 @@ components: example: 'import { parseArgs } from "node:util";' FileDiff: - description: A before/after pair showing changes to a single file. + description: | + A before/after pair showing changes to a single file. + + Contents conventions for non-modify cases: + - Added: `old_file.contents` is empty string; `new_file` holds the added contents. + - Deleted: `new_file.contents` is empty string; `old_file` holds the removed contents. + - Renamed (no content change): both sides hold identical contents; `old_file.name != new_file.name`. + - Symlink / submodule / binary / sensitive / truncated: contents are empty strings; consumers must render a placeholder based on the flag set. type: object required: - old_file @@ -4214,6 +4276,35 @@ components: $ref: "#/components/schemas/DiffFile" new_file: $ref: "#/components/schemas/DiffFile" + change_kind: + type: string + description: Optional classification of the change. Clients that don't recognize a value should fall back to inspecting the old/new contents. + enum: + - added + - modified + - deleted + - renamed + - symlink + - submodule + example: modified + truncated: + type: boolean + description: When `true`, `new_file.contents` and `old_file.contents` are empty strings because the file exceeded a cap (see `truncation_reason`). + example: false + truncation_reason: + type: string + description: Reason this file's contents were omitted. Absent when `truncated` is `false` or omitted. + enum: + - file_too_large + - budget_exhausted + binary: + type: boolean + description: When `true`, the file is non-textual; `contents` on both sides are empty strings. + example: false + sensitive: + type: boolean + description: When `true`, the file path matched the server's sensitive-path denylist; `contents` on both sides are empty strings regardless of truncation or binary flags. + example: false DiffStats: description: Aggregate line-change statistics for a diff. @@ -4231,8 +4322,57 @@ components: description: Total lines deleted. example: 234 + RunFilesMeta: + description: | + Metadata for a `PaginatedRunFileList` response. + + Replaces `PaginationMeta` on the files endpoint — the naturally-bounded list does not use cursor pagination but exposes caps and a degraded-response path instead. + type: object + required: + - truncated + - total_changed + properties: + truncated: + type: boolean + description: True when any cap (file count, per-file size, or aggregate size) was hit for this response. + example: false + files_omitted_by_budget: + type: integer + description: Number of files dropped because the aggregate 5 MiB budget was exhausted. Zero or absent when no files were dropped for budget reasons. + example: 0 + total_changed: + type: integer + description: Total files changed in the run (before caps were applied). May exceed `data.length` when truncation occurred. + example: 3 + to_sha: + type: string + description: Head SHA the diff (or patch) was resolved against. + pattern: "^[0-9a-f]{7,40}$" + example: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0" + to_sha_committed_at: + type: string + format: date-time + description: Commit time of `to_sha`, used by the UI for the "Checkpoint Xm ago" freshness label on Running runs. + degraded: + type: boolean + description: When `true`, this response carries only a unified patch string in `patch` (no per-file contents in `data`). The UI should render via a unified-patch component. + example: false + degraded_reason: + type: string + description: Why the response degraded to patch-only form. Absent when `degraded` is `false` or omitted. + enum: + - sandbox_unreachable + - sandbox_gone + - provider_unsupported + patch: + type: string + description: Unified-patch text captured at run end. Present only when `degraded` is `true`. Capped at 5 MiB; denylisted file sections are stripped and replaced with a placeholder line. + PaginatedRunFileList: - description: Paginated list of file diffs produced by a run. + description: | + List of file diffs produced by a run, with metadata describing truncation and degraded-response state. + + Naturally bounded: at most 200 files per response. Consumers should inspect `meta.truncated` rather than assuming `data.length` equals the run's total change count. type: object required: - data @@ -4243,7 +4383,7 @@ components: items: $ref: "#/components/schemas/FileDiff" meta: - $ref: "#/components/schemas/PaginationMeta" + $ref: "#/components/schemas/RunFilesMeta" # ── Billing Schemas ────────────────────────────────────────────────── diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 81fe5bebe..bb62a3645 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -1030,6 +1030,7 @@ fn demo_routes() -> Router> { .route("/runs/{id}/graph", get(demo::get_run_graph)) .route("/runs/{id}/stages", get(demo::get_run_stages)) .route("/runs/{id}/artifacts", get(demo::list_run_artifacts_stub)) + .route("/runs/{id}/files", get(not_implemented)) .route( "/runs/{id}/stages/{stageId}/turns", get(demo::get_stage_turns), @@ -1110,6 +1111,7 @@ fn real_routes() -> Router> { .route("/runs/{id}/graph", get(get_graph)) .route("/runs/{id}/stages", get(list_run_stages)) .route("/runs/{id}/artifacts", get(list_run_artifacts)) + .route("/runs/{id}/files", get(not_implemented)) .route("/runs/{id}/stages/{stageId}/turns", get(not_implemented)) .route( "/runs/{id}/stages/{stageId}/artifacts", diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 8cfd5d2aa..42b42198a 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -145,6 +145,7 @@ models/run-checkpoint.ts models/run-control-action.ts models/run-error.ts models/run-event.ts +models/run-files-meta.ts models/run-list-item.ts models/run-manifest.ts models/run-projection-checkpoints-inner-inner.ts diff --git a/lib/packages/fabro-api-client/src/api/run-outputs-api.ts b/lib/packages/fabro-api-client/src/api/run-outputs-api.ts index 03e1fa7c0..60cd60a0f 100644 --- a/lib/packages/fabro-api-client/src/api/run-outputs-api.ts +++ b/lib/packages/fabro-api-client/src/api/run-outputs-api.ts @@ -24,12 +24,74 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError // @ts-ignore import type { ErrorResponse } from '../models'; // @ts-ignore +import type { PaginatedRunFileList } from '../models'; +// @ts-ignore import type { RunBilling } from '../models'; /** * RunOutputsApi - axios parameter creator */ export const RunOutputsApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * Returns the set of file changes produced by a run as a list of before/after diffs. While the run\'s sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`). Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`. + * @summary List Run Files Changed + * @param {string} id Unique run identifier (ULID). + * @param {number} [pageLimit] Maximum number of items to return per page. + * @param {number} [pageOffset] Number of items to skip before returning results. + * @param {string} [fromSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + * @param {string} [toSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listRunFiles: async (id: string, pageLimit?: number, pageOffset?: number, fromSha?: string, toSha?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('listRunFiles', 'id', id) + const localVarPath = `/api/v1/runs/{id}/files` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication SessionCookie required + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (pageLimit !== undefined) { + localVarQueryParameter['page[limit]'] = pageLimit; + } + + if (pageOffset !== undefined) { + localVarQueryParameter['page[offset]'] = pageOffset; + } + + if (fromSha !== undefined) { + localVarQueryParameter['from_sha'] = fromSha; + } + + if (toSha !== undefined) { + localVarQueryParameter['to_sha'] = toSha; + } + + localVarHeaderParameter['Accept'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Returns token counts and billed totals broken down by stage and model for a specific run. * @summary Retrieve Run Billing @@ -79,6 +141,23 @@ export const RunOutputsApiAxiosParamCreator = function (configuration?: Configur export const RunOutputsApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = RunOutputsApiAxiosParamCreator(configuration) return { + /** + * Returns the set of file changes produced by a run as a list of before/after diffs. While the run\'s sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`). Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`. + * @summary List Run Files Changed + * @param {string} id Unique run identifier (ULID). + * @param {number} [pageLimit] Maximum number of items to return per page. + * @param {number} [pageOffset] Number of items to skip before returning results. + * @param {string} [fromSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + * @param {string} [toSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async listRunFiles(id: string, pageLimit?: number, pageOffset?: number, fromSha?: string, toSha?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listRunFiles(id, pageLimit, pageOffset, fromSha, toSha, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['RunOutputsApi.listRunFiles']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Returns token counts and billed totals broken down by stage and model for a specific run. * @summary Retrieve Run Billing @@ -101,6 +180,20 @@ export const RunOutputsApiFp = function(configuration?: Configuration) { export const RunOutputsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = RunOutputsApiFp(configuration) return { + /** + * Returns the set of file changes produced by a run as a list of before/after diffs. While the run\'s sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`). Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`. + * @summary List Run Files Changed + * @param {string} id Unique run identifier (ULID). + * @param {number} [pageLimit] Maximum number of items to return per page. + * @param {number} [pageOffset] Number of items to skip before returning results. + * @param {string} [fromSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + * @param {string} [toSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listRunFiles(id: string, pageLimit?: number, pageOffset?: number, fromSha?: string, toSha?: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.listRunFiles(id, pageLimit, pageOffset, fromSha, toSha, options).then((request) => request(axios, basePath)); + }, /** * Returns token counts and billed totals broken down by stage and model for a specific run. * @summary Retrieve Run Billing @@ -118,6 +211,21 @@ export const RunOutputsApiFactory = function (configuration?: Configuration, bas * RunOutputsApi - object-oriented interface */ export class RunOutputsApi extends BaseAPI { + /** + * Returns the set of file changes produced by a run as a list of before/after diffs. While the run\'s sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`). Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`. + * @summary List Run Files Changed + * @param {string} id Unique run identifier (ULID). + * @param {number} [pageLimit] Maximum number of items to return per page. + * @param {number} [pageOffset] Number of items to skip before returning results. + * @param {string} [fromSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + * @param {string} [toSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public listRunFiles(id: string, pageLimit?: number, pageOffset?: number, fromSha?: string, toSha?: string, options?: RawAxiosRequestConfig) { + return RunOutputsApiFp(this.configuration).listRunFiles(id, pageLimit, pageOffset, fromSha, toSha, options).then((request) => request(this.axios, this.basePath)); + } + /** * Returns token counts and billed totals broken down by stage and model for a specific run. * @summary Retrieve Run Billing diff --git a/lib/packages/fabro-api-client/src/models/file-diff.ts b/lib/packages/fabro-api-client/src/models/file-diff.ts index bae602244..b3d90709b 100644 --- a/lib/packages/fabro-api-client/src/models/file-diff.ts +++ b/lib/packages/fabro-api-client/src/models/file-diff.ts @@ -18,10 +18,48 @@ import type { DiffFile } from './diff-file'; /** - * A before/after pair showing changes to a single file. + * A before/after pair showing changes to a single file. Contents conventions for non-modify cases: - Added: `old_file.contents` is empty string; `new_file` holds the added contents. - Deleted: `new_file.contents` is empty string; `old_file` holds the removed contents. - Renamed (no content change): both sides hold identical contents; `old_file.name != new_file.name`. - Symlink / submodule / binary / sensitive / truncated: contents are empty strings; consumers must render a placeholder based on the flag set. */ export interface FileDiff { 'old_file': DiffFile; 'new_file': DiffFile; + /** + * Optional classification of the change. Clients that don\'t recognize a value should fall back to inspecting the old/new contents. + */ + 'change_kind'?: FileDiffChangeKindEnum; + /** + * When `true`, `new_file.contents` and `old_file.contents` are empty strings because the file exceeded a cap (see `truncation_reason`). + */ + 'truncated'?: boolean; + /** + * Reason this file\'s contents were omitted. Absent when `truncated` is `false` or omitted. + */ + 'truncation_reason'?: FileDiffTruncationReasonEnum; + /** + * When `true`, the file is non-textual; `contents` on both sides are empty strings. + */ + 'binary'?: boolean; + /** + * When `true`, the file path matched the server\'s sensitive-path denylist; `contents` on both sides are empty strings regardless of truncation or binary flags. + */ + 'sensitive'?: boolean; } +export const FileDiffChangeKindEnum = { + ADDED: 'added', + MODIFIED: 'modified', + DELETED: 'deleted', + RENAMED: 'renamed', + SYMLINK: 'symlink', + SUBMODULE: 'submodule' +} as const; + +export type FileDiffChangeKindEnum = typeof FileDiffChangeKindEnum[keyof typeof FileDiffChangeKindEnum]; +export const FileDiffTruncationReasonEnum = { + FILE_TOO_LARGE: 'file_too_large', + BUDGET_EXHAUSTED: 'budget_exhausted' +} as const; + +export type FileDiffTruncationReasonEnum = typeof FileDiffTruncationReasonEnum[keyof typeof FileDiffTruncationReasonEnum]; + + diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 77c22ec8b..2f416e7a4 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -125,6 +125,7 @@ export * from './run-checkpoint'; export * from './run-control-action'; export * from './run-error'; export * from './run-event'; +export * from './run-files-meta'; export * from './run-list-item'; export * from './run-manifest'; export * from './run-projection'; diff --git a/lib/packages/fabro-api-client/src/models/paginated-run-file-list.ts b/lib/packages/fabro-api-client/src/models/paginated-run-file-list.ts index 09e6cda56..9f9eaf440 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-run-file-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-run-file-list.ts @@ -18,13 +18,13 @@ import type { FileDiff } from './file-diff'; // May contain unused imports in some cases // @ts-ignore -import type { PaginationMeta } from './pagination-meta'; +import type { RunFilesMeta } from './run-files-meta'; /** - * Paginated list of file diffs produced by a run. + * List of file diffs produced by a run, with metadata describing truncation and degraded-response state. Naturally bounded: at most 200 files per response. Consumers should inspect `meta.truncated` rather than assuming `data.length` equals the run\'s total change count. */ export interface PaginatedRunFileList { 'data': Array; - 'meta': PaginationMeta; + 'meta': RunFilesMeta; } diff --git a/lib/packages/fabro-api-client/src/models/run-files-meta.ts b/lib/packages/fabro-api-client/src/models/run-files-meta.ts new file mode 100644 index 000000000..8c84ff985 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/run-files-meta.ts @@ -0,0 +1,63 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Metadata for a `PaginatedRunFileList` response. Replaces `PaginationMeta` on the files endpoint — the naturally-bounded list does not use cursor pagination but exposes caps and a degraded-response path instead. + */ +export interface RunFilesMeta { + /** + * True when any cap (file count, per-file size, or aggregate size) was hit for this response. + */ + 'truncated': boolean; + /** + * Number of files dropped because the aggregate 5 MiB budget was exhausted. Zero or absent when no files were dropped for budget reasons. + */ + 'files_omitted_by_budget'?: number; + /** + * Total files changed in the run (before caps were applied). May exceed `data.length` when truncation occurred. + */ + 'total_changed': number; + /** + * Head SHA the diff (or patch) was resolved against. + */ + 'to_sha'?: string; + /** + * Commit time of `to_sha`, used by the UI for the \"Checkpoint Xm ago\" freshness label on Running runs. + */ + 'to_sha_committed_at'?: string; + /** + * When `true`, this response carries only a unified patch string in `patch` (no per-file contents in `data`). The UI should render via a unified-patch component. + */ + 'degraded'?: boolean; + /** + * Why the response degraded to patch-only form. Absent when `degraded` is `false` or omitted. + */ + 'degraded_reason'?: RunFilesMetaDegradedReasonEnum; + /** + * Unified-patch text captured at run end. Present only when `degraded` is `true`. Capped at 5 MiB; denylisted file sections are stripped and replaced with a placeholder line. + */ + 'patch'?: string; +} + +export const RunFilesMetaDegradedReasonEnum = { + SANDBOX_UNREACHABLE: 'sandbox_unreachable', + SANDBOX_GONE: 'sandbox_gone', + PROVIDER_UNSUPPORTED: 'provider_unsupported' +} as const; + +export type RunFilesMetaDegradedReasonEnum = typeof RunFilesMetaDegradedReasonEnum[keyof typeof RunFilesMetaDegradedReasonEnum]; + +