diff --git a/biome.jsonc b/biome.jsonc index 53431934..b4a8d25d 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -72,6 +72,7 @@ "**/src/**/*.{ts,tsx}", "!**/src/app-server/connection/**", "!**/src/app-server/protocol/server-requests.ts", + "!**/src/app-server/protocol/thread.ts", "!**/src/app-server/protocol/turn.ts" ] }, diff --git a/src/app-server/protocol/thread.ts b/src/app-server/protocol/thread.ts index c991930c..aa491b5a 100644 --- a/src/app-server/protocol/thread.ts +++ b/src/app-server/protocol/thread.ts @@ -1,14 +1,15 @@ import type { Thread, ThreadProvenance } from "../../domain/threads/model"; +import type { Thread as GeneratedThread } from "../../generated/app-server/v2/Thread"; -export interface ThreadRecord { - id: string; - preview: string; - name: string | null; - createdAt: number; - updatedAt: number; - recencyAt?: number | null; - [key: string]: unknown; -} +type RequiredThreadRecordFields = "id" | "preview" | "name" | "createdAt" | "updatedAt"; + +export type ThreadRecord = Pick & + Partial> & { + /** Kept unknown at the protocol edge so a newer SessionSource variant degrades to `other` instead of breaking thread lists. */ + source?: unknown; + status?: unknown; + turns?: readonly GeneratedThread["turns"][number][]; + }; export function threadFromThreadRecord(thread: ThreadRecord, options: { archived?: boolean } = {}): Thread { const hasRecencyAt = Object.hasOwn(thread, "recencyAt"); diff --git a/src/app-server/services/rollout-token-usage.ts b/src/app-server/services/rollout-token-usage.ts index 9733caf1..410fa443 100644 --- a/src/app-server/services/rollout-token-usage.ts +++ b/src/app-server/services/rollout-token-usage.ts @@ -3,7 +3,7 @@ import type { ThreadTokenUsage, TokenUsageBreakdown } from "../../domain/runtime const ROLLOUT_TOKEN_USAGE_READ_TIMEOUT_MS = 2_000; const ROLLOUT_TOKEN_USAGE_MAX_BASE64_BYTES = 12 * 1024 * 1024; -export type RolloutReadFileBase64 = (path: string, options: { timeoutMs: number }) => Promise; +export type RolloutReadFileBase64 = (path: string, options: { timeoutMs: number }) => Promise; export async function recoverRolloutTokenUsage( path: string | null, @@ -11,12 +11,13 @@ export async function recoverRolloutTokenUsage( ): Promise { if (!path || !isAbsolutePath(path)) return null; - let dataBase64: string; + let dataBase64: string | null; try { dataBase64 = await readFileBase64(path, { timeoutMs: ROLLOUT_TOKEN_USAGE_READ_TIMEOUT_MS }); } catch { return null; } + if (dataBase64 === null) return null; if (dataBase64.length > ROLLOUT_TOKEN_USAGE_MAX_BASE64_BYTES) return null; const text = decodeBase64Text(dataBase64); diff --git a/src/app-server/services/threads.ts b/src/app-server/services/threads.ts index c4434a8e..6cd8a359 100644 --- a/src/app-server/services/threads.ts +++ b/src/app-server/services/threads.ts @@ -25,10 +25,6 @@ import type { AppServerRequestClient } from "./request-client"; const THREAD_LIST_PAGE_LIMIT = 100; export type ThreadTurnSortDirection = "asc" | "desc"; -export type TurnTranscriptSummaryClient = AppServerRequestClient; -export type ThreadForkClient = AppServerRequestClient; -export type ThreadRollbackClient = AppServerRequestClient; -export type ThreadCompactionClient = AppServerRequestClient; interface TurnTranscriptSummaryPage { summaries: TurnTranscriptSummary[]; @@ -165,7 +161,7 @@ export async function readThreadForArchiveExport(client: AppServerRequestClient, } export async function readCompletedTurnTranscriptSummariesPage( - client: TurnTranscriptSummaryClient, + client: AppServerRequestClient, threadId: string, cursor: string | null, limit: number, @@ -179,7 +175,7 @@ export async function readCompletedTurnTranscriptSummariesPage( } export async function readReferencedThreadTurnTranscriptSummaries( - client: TurnTranscriptSummaryClient, + client: AppServerRequestClient, threadId: string, limit = REFERENCED_THREAD_TURN_LIMIT, ): Promise { @@ -208,13 +204,13 @@ function threadRollbackSnapshotFromAppServerResponse(response: ThreadRollbackRes }; } -export async function rollbackThread(client: ThreadRollbackClient, threadId: string, numTurns?: number): Promise { +export async function rollbackThread(client: AppServerRequestClient, threadId: string, numTurns?: number): Promise { const response = await client.request("thread/rollback", { threadId, numTurns: numTurns ?? 1 }); return threadRollbackSnapshotFromAppServerResponse(response); } export async function forkThread( - client: ThreadForkClient, + client: AppServerRequestClient, threadId: string, cwd: string, lastTurnId: string | null = null, @@ -234,7 +230,7 @@ export interface EphemeralThreadForkSnapshot { } export async function forkEphemeralThread( - client: ThreadForkClient, + client: AppServerRequestClient, sourceThreadId: string, cwd: string, ): Promise { @@ -252,7 +248,7 @@ export async function forkEphemeralThread( }; } -export async function compactThread(client: ThreadCompactionClient, threadId: string): Promise { +export async function compactThread(client: AppServerRequestClient, threadId: string): Promise { await client.request("thread/compact/start", { threadId }); } diff --git a/src/domain/runtime/permissions.ts b/src/domain/runtime/permissions.ts index 00c7982f..804ac187 100644 --- a/src/domain/runtime/permissions.ts +++ b/src/domain/runtime/permissions.ts @@ -38,8 +38,11 @@ export interface RuntimePermissionState { } export interface RuntimePermissionKnownState { + /** True when the active-thread layer is authoritative, including an authoritative null meaning the detail was not reported. */ readonly approvalPolicyKnown: boolean; + /** True prevents fallback to a different config profile's sandbox; null still displays as not reported. */ readonly sandboxPolicyKnown: boolean; + /** True when the active profile identity, including an authoritative null, supersedes config fallback. */ readonly permissionProfileKnown: boolean; } diff --git a/src/features/chat/app-server/session-gateway.ts b/src/features/chat/app-server/session-gateway.ts index 75b09920..dce46157 100644 --- a/src/features/chat/app-server/session-gateway.ts +++ b/src/features/chat/app-server/session-gateway.ts @@ -23,7 +23,7 @@ interface ChatThreadReferenceResolverOptions { export interface ChatAppServerGateway extends ChatSessionTransports, ChatMetadataTransports { clientAccess: AppServerClientAccess; connectionAvailable(): boolean; - readFileBase64(path: string, options?: { timeoutMs?: number }): Promise; + readFileBase64(path: string, options?: { timeoutMs?: number }): Promise; threadReferences(options: ChatThreadReferenceResolverOptions): ThreadReferenceResolver; } @@ -70,9 +70,9 @@ async function readCurrentClientFileBase64( host: ChatAppServerGatewayHost, path: string, options: { timeoutMs?: number } = {}, -): Promise { +): Promise { const client = host.currentClient(); - if (!client) return ""; + if (!client) return null; const response = await client.request("fs/readFile", { path }, options); - return host.currentClient() === client ? response.dataBase64 : ""; + return host.currentClient() === client ? response.dataBase64 : null; } diff --git a/src/features/chat/app-server/thread-reference-resolver.ts b/src/features/chat/app-server/thread-reference-resolver.ts index 96ecb9c1..7f58ebd4 100644 --- a/src/features/chat/app-server/thread-reference-resolver.ts +++ b/src/features/chat/app-server/thread-reference-resolver.ts @@ -1,4 +1,5 @@ -import { readReferencedThreadTurnTranscriptSummaries, type TurnTranscriptSummaryClient } from "../../../app-server/services/threads"; +import type { AppServerRequestClient } from "../../../app-server/services/request-client"; +import { readReferencedThreadTurnTranscriptSummaries } from "../../../app-server/services/threads"; import { type CodexInput, codexTextInputWithAttachments } from "../../../domain/chat/input"; import { shortThreadId } from "../../../domain/threads/id"; import type { Thread } from "../../../domain/threads/model"; @@ -7,7 +8,7 @@ import type { ComposerInputSnapshot } from "../application/composer/input-snapsh import type { ThreadReferenceInput } from "../application/turns/slash-command-execution"; interface ThreadReferenceResolverHost { - currentClient(): TurnTranscriptSummaryClient | null; + currentClient(): AppServerRequestClient | null; prepareInput(text: string, snapshot: ComposerInputSnapshot): { text: string; input: CodexInput }; addSystemMessage(text: string): void; setStatus(status: string): void; diff --git a/src/features/chat/app-server/transports/session-transports.ts b/src/features/chat/app-server/transports/session-transports.ts index 50ffdcbb..d2a6b5e6 100644 --- a/src/features/chat/app-server/transports/session-transports.ts +++ b/src/features/chat/app-server/transports/session-transports.ts @@ -45,9 +45,6 @@ interface ChatAppServerTransportHost extends ConnectedChatAppServerClientHost { vaultPath: string; } -type ChatThreadHistoryClient = AppServerRequestClient; -type ChatThreadResumeClient = AppServerRequestClient; - interface AppServerThreadTurnsPage { readonly data: ThreadTurnsPage["turns"]; readonly nextCursor: string | null; @@ -244,7 +241,7 @@ async function withCurrentChatAppServerClient( } async function readChatThreadHistoryPage( - client: ChatThreadHistoryClient, + client: AppServerRequestClient, threadId: string, cursor: string | null, limit = 20, @@ -260,7 +257,7 @@ function chatThreadHistoryPageFromTurnsPage(page: ThreadTurnsPage): ThreadHistor }; } -async function resumeChatThread(client: ChatThreadResumeClient, threadId: string, cwd: string): Promise { +async function resumeChatThread(client: AppServerRequestClient, threadId: string, cwd: string): Promise { const response = await resumeThread(client, threadId, cwd); return { activation: threadActivationSnapshotFromAppServerResponse(response), diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index 9121caf4..376c4579 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -96,6 +96,7 @@ describe("Biome Grit plugin wiring", () => { "**/src/**/*.{ts,tsx}", "!**/src/app-server/connection/**", "!**/src/app-server/protocol/server-requests.ts", + "!**/src/app-server/protocol/thread.ts", "!**/src/app-server/protocol/turn.ts", ]); expect(projectPluginIncludes("no-restricted-css-policy.grit")).toEqual(["**/src/styles/**/*.css"]);