| author | |
| committer | |
| log | ddee418a8d6d3e242ca26a8053a886ecc3b56b53 |
| tree | 0cffe83bad79ecfdb8673cb1a233310c7e6e89db |
| parent | 2cb1a26f061fa9029ccbe2a614f02bb14b22dd45 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 164 insertions(+), 24 deletions(-)
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+2-13| ... | @@ -39,6 +39,7 @@ import { | ... | @@ -39,6 +39,7 @@ import { |
| 39 | type ProviderCommandReactorShape, | 39 | type ProviderCommandReactorShape, |
| 40 | } from "../Services/ProviderCommandReactor.ts"; | 40 | } from "../Services/ProviderCommandReactor.ts"; |
| 41 | import { forkParked, ServerActivation } from "../../serverActivation.ts"; | 41 | import { forkParked, ServerActivation } from "../../serverActivation.ts"; |
| 42 | import { canReplaceThreadTitle, DEFAULT_THREAD_TITLE } from "../threadTitles.ts"; | ||
| 42 | import { | 43 | import { |
| 43 | resolveSourceControlWriterModelSelection, | 44 | resolveSourceControlWriterModelSelection, |
| 44 | ServerSettingsService, | 45 | ServerSettingsService, |
| ... | @@ -91,7 +92,6 @@ const turnStartKeyForEvent = (event: ProviderIntentEvent): string => | ... | @@ -91,7 +92,6 @@ const turnStartKeyForEvent = (event: ProviderIntentEvent): string => |
| 91 | const HANDLED_TURN_START_KEY_MAX = 10_000; | 92 | const HANDLED_TURN_START_KEY_MAX = 10_000; |
| 92 | const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); | 93 | const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); |
| 93 | const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; | 94 | const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| 94 | const DEFAULT_THREAD_TITLE = "New thread"; | ||
| 95 | const MAX_REGENERATION_ATTACHMENTS = 4; | 95 | const MAX_REGENERATION_ATTACHMENTS = 4; |
| 96 | const MAX_THREAD_TITLE_CONTEXT_CHARS = 8_000; | 96 | const MAX_THREAD_TITLE_CONTEXT_CHARS = 8_000; |
| 97 | const MAX_FIRST_USER_TITLE_CONTEXT_CHARS = 2_000; | 97 | const MAX_FIRST_USER_TITLE_CONTEXT_CHARS = 2_000; |
| ... | @@ -227,18 +227,6 @@ export function providerErrorLabelFromInstanceHint(input: { | ... | @@ -227,18 +227,6 @@ export function providerErrorLabelFromInstanceHint(input: { |
| 227 | ); | 227 | ); |
| 228 | } | 228 | } |
| 229 | 229 | ||
| 230 | function canReplaceThreadTitle(currentTitle: string, titleSeed?: string): boolean { | ||
| 231 | const trimmedCurrentTitle = currentTitle.trim(); | ||
| 232 | if (trimmedCurrentTitle === DEFAULT_THREAD_TITLE) { | ||
| 233 | return true; | ||
| 234 | } | ||
| 235 | |||
| 236 | const trimmedTitleSeed = titleSeed?.trim(); | ||
| 237 | return trimmedTitleSeed !== undefined && trimmedTitleSeed.length > 0 | ||
| 238 | ? trimmedCurrentTitle === trimmedTitleSeed | ||
| 239 | : false; | ||
| 240 | } | ||
| 241 | |||
| 242 | function findProviderAdapterRequestError( | 230 | function findProviderAdapterRequestError( |
| 243 | cause: Cause.Cause<ProviderServiceError>, | 231 | cause: Cause.Cause<ProviderServiceError>, |
| 244 | ): ProviderAdapterRequestError | undefined { | 232 | ): ProviderAdapterRequestError | undefined { |
| ... | @@ -626,6 +614,7 @@ const make = Effect.gen(function* () { | ... | @@ -626,6 +614,7 @@ const make = Effect.gen(function* () { |
| 626 | ...(preferredProvider ? { provider: preferredProvider } : {}), | 614 | ...(preferredProvider ? { provider: preferredProvider } : {}), |
| 627 | providerInstanceId: desiredInstanceId, | 615 | providerInstanceId: desiredInstanceId, |
| 628 | ...(effectiveCwd ? { cwd: effectiveCwd } : {}), | 616 | ...(effectiveCwd ? { cwd: effectiveCwd } : {}), |
| 617 | ...(thread.title ? { title: thread.title } : {}), | ||
| 629 | modelSelection: desiredModelSelection, | 618 | modelSelection: desiredModelSelection, |
| 630 | ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), | 619 | ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), |
| 631 | runtimeMode: desiredRuntimeMode, | 620 | runtimeMode: desiredRuntimeMode, |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+53-4| ... | @@ -48,6 +48,7 @@ import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQu | ... | @@ -48,6 +48,7 @@ import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQu |
| 48 | import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts"; | 48 | import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts"; |
| 49 | import * as ThreadPlanProgress from "../ThreadPlanProgress.ts"; | 49 | import * as ThreadPlanProgress from "../ThreadPlanProgress.ts"; |
| 50 | import { ProviderRuntimeIngestionLive } from "./ProviderRuntimeIngestion.ts"; | 50 | import { ProviderRuntimeIngestionLive } from "./ProviderRuntimeIngestion.ts"; |
| 51 | import { DEFAULT_THREAD_TITLE } from "../threadTitles.ts"; | ||
| 51 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; | 52 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; |
| 52 | import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts"; | 53 | import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts"; |
| 53 | import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; | 54 | import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; |
| ... | @@ -221,7 +222,10 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -221,7 +222,10 @@ describe("ProviderRuntimeIngestion", () => { |
| 221 | } | 222 | } |
| 222 | }); | 223 | }); |
| 223 | 224 | ||
| 224 | async function createHarness(options?: { serverSettings?: Partial<ServerSettings> }) { | 225 | async function createHarness(options?: { |
| 226 | serverSettings?: Partial<ServerSettings>; | ||
| 227 | threadTitle?: string; | ||
| 228 | }) { | ||
| 225 | const workspaceRoot = makeTempDir("t3-provider-project-"); | 229 | const workspaceRoot = makeTempDir("t3-provider-project-"); |
| 226 | NodeFS.mkdirSync(NodePath.join(workspaceRoot, ".git")); | 230 | NodeFS.mkdirSync(NodePath.join(workspaceRoot, ".git")); |
| 227 | const provider = createProviderServiceHarness(); | 231 | const provider = createProviderServiceHarness(); |
| ... | @@ -277,7 +281,7 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -277,7 +281,7 @@ describe("ProviderRuntimeIngestion", () => { |
| 277 | commandId: CommandId.make("cmd-thread-create"), | 281 | commandId: CommandId.make("cmd-thread-create"), |
| 278 | threadId: ThreadId.make("thread-1"), | 282 | threadId: ThreadId.make("thread-1"), |
| 279 | projectId: asProjectId("project-1"), | 283 | projectId: asProjectId("project-1"), |
| 280 | title: "Thread", | 284 | title: options?.threadTitle ?? "Thread", |
| 281 | modelSelection: { | 285 | modelSelection: { |
| 282 | instanceId: ProviderInstanceId.make("codex"), | 286 | instanceId: ProviderInstanceId.make("codex"), |
| 283 | model: "gpt-5-codex", | 287 | model: "gpt-5-codex", |
| ... | @@ -2915,7 +2919,7 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -2915,7 +2919,7 @@ describe("ProviderRuntimeIngestion", () => { |
| 2915 | const thread = await waitForThread( | 2919 | const thread = await waitForThread( |
| 2916 | harness.readModel, | 2920 | harness.readModel, |
| 2917 | (entry) => | 2921 | (entry) => |
| 2918 | entry.title === "Renamed by provider" && | 2922 | entry.title === "Thread" && |
| 2919 | entry.activities.some( | 2923 | entry.activities.some( |
| 2920 | (activity: ProviderRuntimeTestActivity) => activity.kind === "turn.plan.updated", | 2924 | (activity: ProviderRuntimeTestActivity) => activity.kind === "turn.plan.updated", |
| 2921 | ) && | 2925 | ) && |
| ... | @@ -2930,7 +2934,7 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -2930,7 +2934,7 @@ describe("ProviderRuntimeIngestion", () => { |
| 2930 | ), | 2934 | ), |
| 2931 | ); | 2935 | ); |
| 2932 | 2936 | ||
| 2933 | expect(thread.title).toBe("Renamed by provider"); | 2937 | expect(thread.title).toBe("Thread"); |
| 2934 | 2938 | ||
| 2935 | const planActivity = thread.activities.find( | 2939 | const planActivity = thread.activities.find( |
| 2936 | (activity: ProviderRuntimeTestActivity) => activity.id === "evt-turn-plan-updated", | 2940 | (activity: ProviderRuntimeTestActivity) => activity.id === "evt-turn-plan-updated", |
| ... | @@ -2971,6 +2975,51 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -2971,6 +2975,51 @@ describe("ProviderRuntimeIngestion", () => { |
| 2971 | expect(checkpoint?.checkpointRef).toBe("provider-diff:evt-turn-diff-updated"); | 2975 | expect(checkpoint?.checkpointRef).toBe("provider-diff:evt-turn-diff-updated"); |
| 2972 | }); | 2976 | }); |
| 2973 | 2977 | ||
| 2978 | it("mirrors a provider title only while the thread still has the default title", async () => { | ||
| 2979 | const harness = await createHarness({ threadTitle: DEFAULT_THREAD_TITLE }); | ||
| 2980 | const now = "2026-01-01T00:00:00.000Z"; | ||
| 2981 | |||
| 2982 | harness.emit({ | ||
| 2983 | type: "thread.metadata.updated", | ||
| 2984 | eventId: asEventId("evt-thread-metadata-default"), | ||
| 2985 | provider: ProviderDriverKind.make("codex"), | ||
| 2986 | createdAt: now, | ||
| 2987 | threadId: asThreadId("thread-1"), | ||
| 2988 | payload: { | ||
| 2989 | name: "Renamed by provider", | ||
| 2990 | metadata: { source: "provider" }, | ||
| 2991 | }, | ||
| 2992 | }); | ||
| 2993 | |||
| 2994 | const thread = await waitForThread( | ||
| 2995 | harness.readModel, | ||
| 2996 | (entry) => entry.title === "Renamed by provider", | ||
| 2997 | ); | ||
| 2998 | expect(thread.title).toBe("Renamed by provider"); | ||
| 2999 | }); | ||
| 3000 | |||
| 3001 | it("rejects a provider title once the thread has a real title", async () => { | ||
| 3002 | const harness = await createHarness({ threadTitle: "User-set title" }); | ||
| 3003 | const now = "2026-01-01T00:00:00.000Z"; | ||
| 3004 | |||
| 3005 | harness.emit({ | ||
| 3006 | type: "thread.metadata.updated", | ||
| 3007 | eventId: asEventId("evt-thread-metadata-real"), | ||
| 3008 | provider: ProviderDriverKind.make("codex"), | ||
| 3009 | createdAt: now, | ||
| 3010 | threadId: asThreadId("thread-1"), | ||
| 3011 | payload: { | ||
| 3012 | name: "Renamed by provider", | ||
| 3013 | metadata: { source: "provider" }, | ||
| 3014 | }, | ||
| 3015 | }); | ||
| 3016 | |||
| 3017 | await harness.drain(); | ||
| 3018 | const readModel = await harness.readModel(); | ||
| 3019 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); | ||
| 3020 | expect(thread?.title).toBe("User-set title"); | ||
| 3021 | }); | ||
| 3022 | |||
| 2974 | it("projects context window updates into normalized thread activities", async () => { | 3023 | it("projects context window updates into normalized thread activities", async () => { |
| 2975 | const harness = await createHarness(); | 3024 | const harness = await createHarness(); |
| 2976 | const now = "2026-01-01T00:00:00.000Z"; | 3025 | const now = "2026-01-01T00:00:00.000Z"; |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+9-6| ... | @@ -43,6 +43,7 @@ import { | ... | @@ -43,6 +43,7 @@ import { |
| 43 | } from "../Services/ProviderRuntimeIngestion.ts"; | 43 | } from "../Services/ProviderRuntimeIngestion.ts"; |
| 44 | import { forkParked } from "../../serverActivation.ts"; | 44 | import { forkParked } from "../../serverActivation.ts"; |
| 45 | import { ServerSettingsService } from "../../serverSettings.ts"; | 45 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 46 | import { canReplaceThreadTitle } from "../threadTitles.ts"; | ||
| 46 | 47 | ||
| 47 | const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`; | 48 | const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`; |
| 48 | const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`; | 49 | const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`; |
| ... | @@ -1892,12 +1893,14 @@ const make = Effect.gen(function* () { | ... | @@ -1892,12 +1893,14 @@ const make = Effect.gen(function* () { |
| 1892 | } | 1893 | } |
| 1893 | 1894 | ||
| 1894 | if (event.type === "thread.metadata.updated" && event.payload.name) { | 1895 | if (event.type === "thread.metadata.updated" && event.payload.name) { |
| 1895 | yield* orchestrationEngine.dispatch({ | 1896 | if (canReplaceThreadTitle(thread.title)) { |
| 1896 | type: "thread.meta.update", | 1897 | yield* orchestrationEngine.dispatch({ |
| 1897 | commandId: yield* providerCommandId(event, "thread-meta-update"), | 1898 | type: "thread.meta.update", |
| 1898 | threadId: thread.id, | 1899 | commandId: yield* providerCommandId(event, "thread-meta-update"), |
| 1899 | title: event.payload.name, | 1900 | threadId: thread.id, |
| 1900 | }); | 1901 | title: event.payload.name, |
| 1902 | }); | ||
| 1903 | } | ||
| 1901 | } | 1904 | } |
| 1902 | 1905 | ||
| 1903 | if (event.type === "turn.diff.updated") { | 1906 | if (event.type === "turn.diff.updated") { |
apps/server/src/orchestration/threadTitles.ts created+13| ... | @@ -0,0 +1,13 @@ | ||
| 1 | export const DEFAULT_THREAD_TITLE = "New thread"; | ||
| 2 | |||
| 3 | export function canReplaceThreadTitle(currentTitle: string, titleSeed?: string): boolean { | ||
| 4 | const trimmedCurrentTitle = currentTitle.trim(); | ||
| 5 | if (trimmedCurrentTitle === DEFAULT_THREAD_TITLE) { | ||
| 6 | return true; | ||
| 7 | } | ||
| 8 | |||
| 9 | const trimmedTitleSeed = titleSeed?.trim(); | ||
| 10 | return trimmedTitleSeed !== undefined && trimmedTitleSeed.length > 0 | ||
| 11 | ? trimmedCurrentTitle === trimmedTitleSeed | ||
| 12 | : false; | ||
| 13 | } | ||
apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+67| ... | @@ -1191,6 +1191,73 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { | ... | @@ -1191,6 +1191,73 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { |
| 1191 | }), | 1191 | }), |
| 1192 | ); | 1192 | ); |
| 1193 | 1193 | ||
| 1194 | it.effect("passes the thread title to session.create when provided", () => | ||
| 1195 | Effect.gen(function* () { | ||
| 1196 | const adapter = yield* OpenCodeAdapter; | ||
| 1197 | const threadId = asThreadId("thread-opencode-title-provided"); | ||
| 1198 | |||
| 1199 | yield* adapter.startSession({ | ||
| 1200 | provider: ProviderDriverKind.make("opencode"), | ||
| 1201 | threadId, | ||
| 1202 | runtimeMode: "full-access", | ||
| 1203 | title: "Investigate reconnect failures", | ||
| 1204 | }); | ||
| 1205 | |||
| 1206 | NodeAssert.equal(runtimeMock.state.sessionCreateInputs.length, 1); | ||
| 1207 | NodeAssert.equal( | ||
| 1208 | runtimeMock.state.sessionCreateInputs[0]?.title, | ||
| 1209 | "Investigate reconnect failures", | ||
| 1210 | ); | ||
| 1211 | }), | ||
| 1212 | ); | ||
| 1213 | |||
| 1214 | it.effect("does not mirror OpenCode's default placeholder session titles", () => | ||
| 1215 | Effect.gen(function* () { | ||
| 1216 | const adapter = yield* OpenCodeAdapter; | ||
| 1217 | const threadId = asThreadId("thread-opencode-placeholder-title"); | ||
| 1218 | runtimeMock.state.subscribedEvents = [ | ||
| 1219 | { | ||
| 1220 | type: "session.updated", | ||
| 1221 | properties: { | ||
| 1222 | info: { | ||
| 1223 | id: "http://127.0.0.1:9999/session", | ||
| 1224 | title: "New session - 2026-08-09T10:20:30.456Z", | ||
| 1225 | }, | ||
| 1226 | }, | ||
| 1227 | }, | ||
| 1228 | { | ||
| 1229 | type: "session.updated", | ||
| 1230 | properties: { | ||
| 1231 | info: { | ||
| 1232 | id: "http://127.0.0.1:9999/session", | ||
| 1233 | title: "Investigate reconnect failures", | ||
| 1234 | }, | ||
| 1235 | }, | ||
| 1236 | }, | ||
| 1237 | ]; | ||
| 1238 | |||
| 1239 | const eventsFiber = yield* adapter.streamEvents.pipe( | ||
| 1240 | Stream.filter((event) => event.threadId === threadId), | ||
| 1241 | Stream.take(3), | ||
| 1242 | Stream.runCollect, | ||
| 1243 | Effect.forkChild, | ||
| 1244 | ); | ||
| 1245 | |||
| 1246 | yield* adapter.startSession({ | ||
| 1247 | provider: ProviderDriverKind.make("opencode"), | ||
| 1248 | threadId, | ||
| 1249 | runtimeMode: "full-access", | ||
| 1250 | }); | ||
| 1251 | |||
| 1252 | const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second"))); | ||
| 1253 | const metadataUpdated = events.filter((event) => event.type === "thread.metadata.updated"); | ||
| 1254 | NodeAssert.equal(metadataUpdated.length, 1); | ||
| 1255 | if (metadataUpdated[0]?.type === "thread.metadata.updated") { | ||
| 1256 | NodeAssert.equal(metadataUpdated[0].payload.name, "Investigate reconnect failures"); | ||
| 1257 | } | ||
| 1258 | }), | ||
| 1259 | ); | ||
| 1260 | |||
| 1194 | it.effect("writes provider-native observability records using the session thread id", () => | 1261 | it.effect("writes provider-native observability records using the session thread id", () => |
| 1195 | Effect.gen(function* () { | 1262 | Effect.gen(function* () { |
| 1196 | const nativeEvents: Array<{ | 1263 | const nativeEvents: Array<{ |
apps/server/src/provider/Layers/OpenCodeAdapter.ts+19-1| ... | @@ -201,7 +201,24 @@ function openCodeEventSessionTitle(event: OpenCodeSubscribedEvent): string | und | ... | @@ -201,7 +201,24 @@ function openCodeEventSessionTitle(event: OpenCodeSubscribedEvent): string | und |
| 201 | return undefined; | 201 | return undefined; |
| 202 | } | 202 | } |
| 203 | 203 | ||
| 204 | return trimText(event.properties.info.title); | 204 | const title = trimText(event.properties.info.title); |
| 205 | // OpenCode mints a placeholder title at session.create when no title was | ||
| 206 | // provided, and re-emits it on every `session.updated`. Mirroring it would | ||
| 207 | // overwrite the thread's real title (openCodeEventSessionTitle feeds the | ||
| 208 | // `thread.metadata.updated` mirror). Ignore OpenCode's auto-generated | ||
| 209 | // placeholders so the thread isn't locked onto them. | ||
| 210 | if (!title || isOpenCodeDefaultTitle(title)) { | ||
| 211 | return undefined; | ||
| 212 | } | ||
| 213 | |||
| 214 | return title; | ||
| 215 | } | ||
| 216 | |||
| 217 | const OPENCODE_DEFAULT_TITLE_PATTERN = | ||
| 218 | /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; | ||
| 219 | |||
| 220 | function isOpenCodeDefaultTitle(title: string): boolean { | ||
| 221 | return OPENCODE_DEFAULT_TITLE_PATTERN.test(title); | ||
| 205 | } | 222 | } |
| 206 | 223 | ||
| 207 | interface OpenCodeSessionContext { | 224 | interface OpenCodeSessionContext { |
| ... | @@ -1302,6 +1319,7 @@ export function makeOpenCodeAdapter( | ... | @@ -1302,6 +1319,7 @@ export function makeOpenCodeAdapter( |
| 1302 | } | 1319 | } |
| 1303 | const createdSession = yield* runOpenCodeSdk("session.create", () => | 1320 | const createdSession = yield* runOpenCodeSdk("session.create", () => |
| 1304 | client.session.create({ | 1321 | client.session.create({ |
| 1322 | ...(input.title ? { title: input.title } : {}), | ||
| 1305 | permission: buildOpenCodePermissionRules(input.runtimeMode), | 1323 | permission: buildOpenCodePermissionRules(input.runtimeMode), |
| 1306 | }), | 1324 | }), |
| 1307 | ); | 1325 | ); |
packages/contracts/src/provider.ts+1| ... | @@ -56,6 +56,7 @@ export const ProviderSessionStartInput = Schema.Struct({ | ... | @@ -56,6 +56,7 @@ export const ProviderSessionStartInput = Schema.Struct({ |
| 56 | // See ProviderSession for the migration story. | 56 | // See ProviderSession for the migration story. |
| 57 | providerInstanceId: Schema.optional(ProviderInstanceId), | 57 | providerInstanceId: Schema.optional(ProviderInstanceId), |
| 58 | cwd: Schema.optional(TrimmedNonEmptyString), | 58 | cwd: Schema.optional(TrimmedNonEmptyString), |
| 59 | title: Schema.optional(TrimmedNonEmptyString), | ||
| 59 | modelSelection: Schema.optional(ModelSelection), | 60 | modelSelection: Schema.optional(ModelSelection), |
| 60 | resumeCursor: Schema.optional(Schema.Unknown), | 61 | resumeCursor: Schema.optional(Schema.Unknown), |
| 61 | approvalPolicy: Schema.optional(ProviderApprovalPolicy), | 62 | approvalPolicy: Schema.optional(ProviderApprovalPolicy), |