| author | |
| committer | |
| log | 26f631b638205506d292c829feaf5d539e05804d |
| tree | 000a395457cb18a291b90b407b4f5b95ef5c318b |
| parent | 981cf405612932afa8a0b98b8c98474ef7e1015d |
- Replace provider session-id centric plumbing with thread-id based runtime handling across server orchestration and provider layers
- Update persistence repositories/migrations and contracts for threadless runtime semantics
- Refresh integration/unit tests and web session/store types to match the new runtime model54 files changed, 1865 insertions(+), 2665 deletions(-)
apps/server/integration/OrchestrationEngineHarness.integration.ts+233-235| ... | @@ -196,257 +196,255 @@ export const makeOrchestrationIntegrationHarness = ( | ... | @@ -196,257 +196,255 @@ export const makeOrchestrationIntegrationHarness = ( |
| 196 | options?: MakeOrchestrationIntegrationHarnessOptions, | 196 | options?: MakeOrchestrationIntegrationHarnessOptions, |
| 197 | ) => | 197 | ) => |
| 198 | Effect.gen(function* () { | 198 | Effect.gen(function* () { |
| 199 | const sleep = (ms: number) => Effect.sleep(ms); | 199 | const sleep = (ms: number) => Effect.sleep(ms); |
| 200 | const provider = options?.provider ?? "codex"; | 200 | const provider = options?.provider ?? "codex"; |
| 201 | const useRealCodex = options?.realCodex === true; | 201 | const useRealCodex = options?.realCodex === true; |
| 202 | const adapterHarness = useRealCodex | 202 | const adapterHarness = useRealCodex |
| 203 | ? null | 203 | ? null |
| 204 | : yield* makeTestProviderAdapterHarness({ | 204 | : yield* makeTestProviderAdapterHarness({ |
| 205 | provider, | 205 | provider, |
| 206 | }); | 206 | }); |
| 207 | const fakeRegistry = adapterHarness | 207 | const fakeRegistry = adapterHarness |
| 208 | ? Layer.succeed(ProviderAdapterRegistry, { | 208 | ? Layer.succeed(ProviderAdapterRegistry, { |
| 209 | getByProvider: (resolvedProvider) => | 209 | getByProvider: (resolvedProvider) => |
| 210 | resolvedProvider === adapterHarness.provider | 210 | resolvedProvider === adapterHarness.provider |
| 211 | ? Effect.succeed(adapterHarness.adapter) | 211 | ? Effect.succeed(adapterHarness.adapter) |
| 212 | : Effect.fail(new ProviderUnsupportedError({ provider: resolvedProvider })), | 212 | : Effect.fail(new ProviderUnsupportedError({ provider: resolvedProvider })), |
| 213 | listProviders: () => Effect.succeed([adapterHarness.provider]), | 213 | listProviders: () => Effect.succeed([adapterHarness.provider]), |
| 214 | } as typeof ProviderAdapterRegistry.Service) | 214 | } as typeof ProviderAdapterRegistry.Service) |
| 215 | : null; | 215 | : null; |
| 216 | const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-orchestration-integration-")); | 216 | const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-orchestration-integration-")); |
| 217 | const workspaceDir = path.join(rootDir, "workspace"); | 217 | const workspaceDir = path.join(rootDir, "workspace"); |
| 218 | const stateDir = path.join(rootDir, "state"); | 218 | const stateDir = path.join(rootDir, "state"); |
| 219 | const dbPath = path.join(stateDir, "state.sqlite"); | 219 | const dbPath = path.join(stateDir, "state.sqlite"); |
| 220 | fs.mkdirSync(workspaceDir, { recursive: true }); | 220 | fs.mkdirSync(workspaceDir, { recursive: true }); |
| 221 | fs.mkdirSync(stateDir, { recursive: true }); | 221 | fs.mkdirSync(stateDir, { recursive: true }); |
| 222 | initializeGitWorkspace(workspaceDir); | 222 | initializeGitWorkspace(workspaceDir); |
| 223 | 223 | ||
| 224 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); | 224 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); |
| 225 | const orchestrationLayer = OrchestrationEngineLive.pipe( | 225 | const orchestrationLayer = OrchestrationEngineLive.pipe( |
| 226 | Layer.provide(OrchestrationProjectionPipelineLive), | 226 | Layer.provide(OrchestrationProjectionPipelineLive), |
| 227 | Layer.provide(OrchestrationEventStoreLive), | 227 | Layer.provide(OrchestrationEventStoreLive), |
| 228 | Layer.provide(OrchestrationCommandReceiptRepositoryLive), | 228 | Layer.provide(OrchestrationCommandReceiptRepositoryLive), |
| 229 | ); | 229 | ); |
| 230 | const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( | 230 | const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( |
| 231 | Layer.provide(ProviderSessionRuntimeRepositoryLive), | 231 | Layer.provide(ProviderSessionRuntimeRepositoryLive), |
| 232 | ); | 232 | ); |
| 233 | const realCodexRegistry = Layer.effect( | 233 | const realCodexRegistry = Layer.effect( |
| 234 | ProviderAdapterRegistry, | 234 | ProviderAdapterRegistry, |
| 235 | Effect.gen(function* () { | 235 | Effect.gen(function* () { |
| 236 | const codexAdapter = yield* CodexAdapter; | 236 | const codexAdapter = yield* CodexAdapter; |
| 237 | return { | 237 | return { |
| 238 | getByProvider: (resolvedProvider) => | 238 | getByProvider: (resolvedProvider) => |
| 239 | resolvedProvider === "codex" | 239 | resolvedProvider === "codex" |
| 240 | ? Effect.succeed(codexAdapter) | 240 | ? Effect.succeed(codexAdapter) |
| 241 | : Effect.fail(new ProviderUnsupportedError({ provider: resolvedProvider })), | 241 | : Effect.fail(new ProviderUnsupportedError({ provider: resolvedProvider })), |
| 242 | listProviders: () => Effect.succeed(["codex"] as const), | 242 | listProviders: () => Effect.succeed(["codex"] as const), |
| 243 | } as typeof ProviderAdapterRegistry.Service; | 243 | } as typeof ProviderAdapterRegistry.Service; |
| 244 | }), | 244 | }), |
| 245 | ).pipe( | 245 | ).pipe( |
| 246 | Layer.provide(makeCodexAdapterLive()), | 246 | Layer.provide(makeCodexAdapterLive()), |
| 247 | Layer.provideMerge(ServerConfig.layerTest(workspaceDir, stateDir)), | 247 | Layer.provideMerge(ServerConfig.layerTest(workspaceDir, stateDir)), |
| 248 | Layer.provideMerge(NodeServices.layer), | 248 | Layer.provideMerge(NodeServices.layer), |
| 249 | Layer.provideMerge(providerSessionDirectoryLayer), | 249 | Layer.provideMerge(providerSessionDirectoryLayer), |
| 250 | ); | 250 | ); |
| 251 | const providerLayer = useRealCodex | 251 | const providerLayer = useRealCodex |
| 252 | ? makeProviderServiceLive().pipe( | 252 | ? makeProviderServiceLive().pipe( |
| 253 | Layer.provide(providerSessionDirectoryLayer), | 253 | Layer.provide(providerSessionDirectoryLayer), |
| 254 | Layer.provide(realCodexRegistry), | 254 | Layer.provide(realCodexRegistry), |
| 255 | ) | 255 | ) |
| 256 | : makeProviderServiceLive().pipe( | 256 | : makeProviderServiceLive().pipe( |
| 257 | Layer.provide(providerSessionDirectoryLayer), | 257 | Layer.provide(providerSessionDirectoryLayer), |
| 258 | Layer.provide(fakeRegistry!), | 258 | Layer.provide(fakeRegistry!), |
| 259 | ); | 259 | ); |
| 260 | 260 | ||
| 261 | const runtimeServicesLayer = Layer.mergeAll( | 261 | const runtimeServicesLayer = Layer.mergeAll( |
| 262 | orchestrationLayer, | 262 | orchestrationLayer, |
| 263 | OrchestrationProjectionSnapshotQueryLive, | 263 | OrchestrationProjectionSnapshotQueryLive, |
| 264 | ProjectionCheckpointRepositoryLive, | 264 | ProjectionCheckpointRepositoryLive, |
| 265 | ProjectionPendingApprovalRepositoryLive, | 265 | ProjectionPendingApprovalRepositoryLive, |
| 266 | CheckpointStoreLive, | 266 | CheckpointStoreLive, |
| 267 | providerLayer, | 267 | providerLayer, |
| 268 | ); | 268 | ); |
| 269 | const runtimeIngestionLayer = ProviderRuntimeIngestionLive.pipe( | 269 | const runtimeIngestionLayer = ProviderRuntimeIngestionLive.pipe( |
| 270 | Layer.provideMerge(runtimeServicesLayer), | 270 | Layer.provideMerge(runtimeServicesLayer), |
| 271 | ); | 271 | ); |
| 272 | const gitCoreLayer = Layer.succeed(GitCore, { | 272 | const gitCoreLayer = Layer.succeed(GitCore, { |
| 273 | renameBranch: (input: Parameters<GitCoreShape["renameBranch"]>[0]) => | 273 | renameBranch: (input: Parameters<GitCoreShape["renameBranch"]>[0]) => |
| 274 | Effect.succeed({ branch: input.newBranch }), | 274 | Effect.succeed({ branch: input.newBranch }), |
| 275 | } as unknown as GitCoreShape); | 275 | } as unknown as GitCoreShape); |
| 276 | const textGenerationLayer = Layer.succeed(TextGeneration, { | 276 | const textGenerationLayer = Layer.succeed(TextGeneration, { |
| 277 | generateBranchName: () => Effect.succeed({ branch: null }), | 277 | generateBranchName: () => Effect.succeed({ branch: null }), |
| 278 | } as unknown as TextGenerationShape); | 278 | } as unknown as TextGenerationShape); |
| 279 | const providerCommandReactorLayer = ProviderCommandReactorLive.pipe( | 279 | const providerCommandReactorLayer = ProviderCommandReactorLive.pipe( |
| 280 | Layer.provideMerge(runtimeServicesLayer), | 280 | Layer.provideMerge(runtimeServicesLayer), |
| 281 | Layer.provideMerge(gitCoreLayer), | 281 | Layer.provideMerge(gitCoreLayer), |
| 282 | Layer.provideMerge(textGenerationLayer), | 282 | Layer.provideMerge(textGenerationLayer), |
| 283 | ); | 283 | ); |
| 284 | const checkpointReactorLayer = CheckpointReactorLive.pipe( | 284 | const checkpointReactorLayer = CheckpointReactorLive.pipe( |
| 285 | Layer.provideMerge(runtimeServicesLayer), | 285 | Layer.provideMerge(runtimeServicesLayer), |
| 286 | ); | 286 | ); |
| 287 | const orchestrationReactorLayer = OrchestrationReactorLive.pipe( | 287 | const orchestrationReactorLayer = OrchestrationReactorLive.pipe( |
| 288 | Layer.provideMerge(runtimeIngestionLayer), | 288 | Layer.provideMerge(runtimeIngestionLayer), |
| 289 | Layer.provideMerge(providerCommandReactorLayer), | 289 | Layer.provideMerge(providerCommandReactorLayer), |
| 290 | Layer.provideMerge(checkpointReactorLayer), | 290 | Layer.provideMerge(checkpointReactorLayer), |
| 291 | ); | 291 | ); |
| 292 | const layer = orchestrationReactorLayer.pipe( | 292 | const layer = orchestrationReactorLayer.pipe( |
| 293 | Layer.provide(persistenceLayer), | 293 | Layer.provide(persistenceLayer), |
| 294 | Layer.provideMerge(ServerConfig.layerTest(workspaceDir, stateDir)), | 294 | Layer.provideMerge(ServerConfig.layerTest(workspaceDir, stateDir)), |
| 295 | Layer.provideMerge(NodeServices.layer), | 295 | Layer.provideMerge(NodeServices.layer), |
| 296 | ); | 296 | ); |
| 297 | 297 | ||
| 298 | const runtime = ManagedRuntime.make(layer); | 298 | const runtime = ManagedRuntime.make(layer); |
| 299 | const engine = yield* tryRuntimePromise("load OrchestrationEngine service", () => | 299 | const engine = yield* tryRuntimePromise("load OrchestrationEngine service", () => |
| 300 | runtime.runPromise(Effect.service(OrchestrationEngineService)), | 300 | runtime.runPromise(Effect.service(OrchestrationEngineService)), |
| 301 | ).pipe(Effect.orDie); | 301 | ).pipe(Effect.orDie); |
| 302 | const reactor = yield* tryRuntimePromise("load OrchestrationReactor service", () => | 302 | const reactor = yield* tryRuntimePromise("load OrchestrationReactor service", () => |
| 303 | runtime.runPromise(Effect.service(OrchestrationReactor)), | 303 | runtime.runPromise(Effect.service(OrchestrationReactor)), |
| 304 | ).pipe(Effect.orDie); | 304 | ).pipe(Effect.orDie); |
| 305 | const snapshotQuery = yield* tryRuntimePromise("load ProjectionSnapshotQuery service", () => | 305 | const snapshotQuery = yield* tryRuntimePromise("load ProjectionSnapshotQuery service", () => |
| 306 | runtime.runPromise(Effect.service(ProjectionSnapshotQuery)), | 306 | runtime.runPromise(Effect.service(ProjectionSnapshotQuery)), |
| 307 | ).pipe(Effect.orDie); | 307 | ).pipe(Effect.orDie); |
| 308 | const providerService = yield* tryRuntimePromise("load ProviderService service", () => | 308 | const providerService = yield* tryRuntimePromise("load ProviderService service", () => |
| 309 | runtime.runPromise(Effect.service(ProviderService)), | 309 | runtime.runPromise(Effect.service(ProviderService)), |
| 310 | ).pipe(Effect.orDie); | 310 | ).pipe(Effect.orDie); |
| 311 | const checkpointStore = yield* tryRuntimePromise("load CheckpointStore service", () => | 311 | const checkpointStore = yield* tryRuntimePromise("load CheckpointStore service", () => |
| 312 | runtime.runPromise(Effect.service(CheckpointStore)), | 312 | runtime.runPromise(Effect.service(CheckpointStore)), |
| 313 | ).pipe(Effect.orDie); | 313 | ).pipe(Effect.orDie); |
| 314 | const checkpointRepository = yield* tryRuntimePromise( | 314 | const checkpointRepository = yield* tryRuntimePromise( |
| 315 | "load ProjectionCheckpointRepository service", | 315 | "load ProjectionCheckpointRepository service", |
| 316 | () => runtime.runPromise(Effect.service(ProjectionCheckpointRepository)), | 316 | () => runtime.runPromise(Effect.service(ProjectionCheckpointRepository)), |
| 317 | ).pipe(Effect.orDie); | 317 | ).pipe(Effect.orDie); |
| 318 | const pendingApprovalRepository = yield* tryRuntimePromise( | 318 | const pendingApprovalRepository = yield* tryRuntimePromise( |
| 319 | "load ProjectionPendingApprovalRepository service", | 319 | "load ProjectionPendingApprovalRepository service", |
| 320 | () => runtime.runPromise(Effect.service(ProjectionPendingApprovalRepository)), | 320 | () => runtime.runPromise(Effect.service(ProjectionPendingApprovalRepository)), |
| 321 | ).pipe(Effect.orDie); | 321 | ).pipe(Effect.orDie); |
| 322 | 322 | ||
| 323 | const scope = yield* Scope.make("sequential"); | 323 | const scope = yield* Scope.make("sequential"); |
| 324 | yield* tryRuntimePromise("start OrchestrationReactor", () => | 324 | yield* tryRuntimePromise("start OrchestrationReactor", () => |
| 325 | runtime.runPromise(reactor.start.pipe(Scope.provide(scope))), | 325 | runtime.runPromise(reactor.start.pipe(Scope.provide(scope))), |
| 326 | ).pipe(Effect.orDie); | 326 | ).pipe(Effect.orDie); |
| 327 | yield* sleep(10); | 327 | yield* sleep(10); |
| 328 | 328 | ||
| 329 | const waitForThread: OrchestrationIntegrationHarness["waitForThread"] = ( | 329 | const waitForThread: OrchestrationIntegrationHarness["waitForThread"] = ( |
| 330 | threadId, | 330 | threadId, |
| 331 | predicate, | 331 | predicate, |
| 332 | timeoutMs, | ||
| 333 | ) => | ||
| 334 | waitFor( | ||
| 335 | snapshotQuery | ||
| 336 | .getSnapshot() | ||
| 337 | .pipe( | ||
| 338 | Effect.map( | ||
| 339 | (snapshot) => snapshot.threads.find((thread) => thread.id === threadId) ?? null, | ||
| 340 | ), | ||
| 341 | ), | ||
| 342 | (thread): thread is OrchestrationThread => thread !== null && predicate(thread), | ||
| 343 | `projected thread '${threadId}'`, | ||
| 344 | timeoutMs, | 332 | timeoutMs, |
| 345 | ) as Effect.Effect<OrchestrationThread, never>; | 333 | ) => |
| 334 | waitFor( | ||
| 335 | snapshotQuery | ||
| 336 | .getSnapshot() | ||
| 337 | .pipe( | ||
| 338 | Effect.map( | ||
| 339 | (snapshot) => snapshot.threads.find((thread) => thread.id === threadId) ?? null, | ||
| 340 | ), | ||
| 341 | ), | ||
| 342 | (thread): thread is OrchestrationThread => thread !== null && predicate(thread), | ||
| 343 | `projected thread '${threadId}'`, | ||
| 344 | timeoutMs, | ||
| 345 | ) as Effect.Effect<OrchestrationThread, never>; | ||
| 346 | 346 | ||
| 347 | const waitForDomainEvent: OrchestrationIntegrationHarness["waitForDomainEvent"] = ( | 347 | const waitForDomainEvent: OrchestrationIntegrationHarness["waitForDomainEvent"] = ( |
| 348 | predicate, | 348 | predicate, |
| 349 | timeoutMs, | ||
| 350 | ) => | ||
| 351 | waitFor( | ||
| 352 | Stream.runCollect(engine.readEvents(0)).pipe( | ||
| 353 | Effect.map((chunk): ReadonlyArray<OrchestrationEvent> => Array.from(chunk)), | ||
| 354 | ), | ||
| 355 | (events) => events.some(predicate), | ||
| 356 | "domain event", | ||
| 357 | timeoutMs, | 349 | timeoutMs, |
| 358 | ); | 350 | ) => |
| 351 | waitFor( | ||
| 352 | Stream.runCollect(engine.readEvents(0)).pipe( | ||
| 353 | Effect.map((chunk): ReadonlyArray<OrchestrationEvent> => Array.from(chunk)), | ||
| 354 | ), | ||
| 355 | (events) => events.some(predicate), | ||
| 356 | "domain event", | ||
| 357 | timeoutMs, | ||
| 358 | ); | ||
| 359 | 359 | ||
| 360 | const waitForPendingApproval: OrchestrationIntegrationHarness["waitForPendingApproval"] = ( | 360 | const waitForPendingApproval: OrchestrationIntegrationHarness["waitForPendingApproval"] = ( |
| 361 | requestId, | 361 | requestId, |
| 362 | predicate, | 362 | predicate, |
| 363 | timeoutMs, | 363 | timeoutMs, |
| 364 | ) => | 364 | ) => |
| 365 | waitFor( | 365 | waitFor( |
| 366 | pendingApprovalRepository | 366 | pendingApprovalRepository |
| 367 | .getByRequestId({ requestId: ApprovalRequestId.makeUnsafe(requestId) }) | 367 | .getByRequestId({ requestId: ApprovalRequestId.makeUnsafe(requestId) }) |
| 368 | .pipe( | 368 | .pipe( |
| 369 | Effect.map((row) => | 369 | Effect.map((row) => |
| 370 | Option.match(row, { | 370 | Option.match(row, { |
| 371 | onNone: () => null, | 371 | onNone: () => null, |
| 372 | onSome: (value) => ({ | 372 | onSome: (value) => ({ |
| 373 | status: value.status, | 373 | status: value.status, |
| 374 | decision: value.decision, | 374 | decision: value.decision, |
| 375 | resolvedAt: value.resolvedAt, | 375 | resolvedAt: value.resolvedAt, |
| 376 | }), | ||
| 376 | }), | 377 | }), |
| 377 | }), | 378 | ), |
| 378 | ), | 379 | ), |
| 379 | ), | 380 | ( |
| 380 | ( | 381 | row, |
| 381 | row, | 382 | ): row is { |
| 382 | ): row is { | 383 | readonly status: "pending" | "resolved"; |
| 383 | readonly status: "pending" | "resolved"; | 384 | readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; |
| 384 | readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; | 385 | readonly resolvedAt: string | null; |
| 385 | readonly resolvedAt: string | null; | 386 | } => row !== null && predicate(row), |
| 386 | } => row !== null && predicate(row), | 387 | `pending approval '${requestId}'`, |
| 387 | `pending approval '${requestId}'`, | 388 | timeoutMs, |
| 388 | timeoutMs, | 389 | ) as Effect.Effect< |
| 389 | ) as Effect.Effect< | 390 | { |
| 390 | { | 391 | readonly status: "pending" | "resolved"; |
| 391 | readonly status: "pending" | "resolved"; | 392 | readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; |
| 392 | readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; | 393 | readonly resolvedAt: string | null; |
| 393 | readonly resolvedAt: string | null; | 394 | }, |
| 394 | }, | 395 | never |
| 395 | never | 396 | >; |
| 396 | >; | ||
| 397 | 397 | ||
| 398 | let disposed = false; | 398 | let disposed = false; |
| 399 | const dispose = Effect.gen(function* () { | 399 | const dispose = Effect.gen(function* () { |
| 400 | if (disposed) { | 400 | if (disposed) { |
| 401 | return; | 401 | return; |
| 402 | } | 402 | } |
| 403 | disposed = true; | 403 | disposed = true; |
| 404 | 404 | ||
| 405 | const shutdown = Effect.gen(function* () { | 405 | const shutdown = Effect.gen(function* () { |
| 406 | const stopAllExit = yield* Effect.exit( | 406 | const stopAllExit = yield* Effect.exit( |
| 407 | Effect.promise(() => runtime.runPromise(providerService.stopAll())), | 407 | Effect.promise(() => runtime.runPromise(providerService.stopAll())), |
| 408 | ); | 408 | ); |
| 409 | const closeScopeExit = yield* Effect.exit( | 409 | const closeScopeExit = yield* Effect.exit(Scope.close(scope, Exit.void)); |
| 410 | Effect.promise(() => Effect.runPromise(Scope.close(scope, Exit.void))), | 410 | const disposeRuntimeExit = yield* Effect.exit(Effect.promise(() => runtime.dispose())); |
| 411 | ); | ||
| 412 | const disposeRuntimeExit = yield* Effect.exit(Effect.promise(() => runtime.dispose())); | ||
| 413 | 411 | ||
| 414 | const failureCause = Exit.isFailure(stopAllExit) | 412 | const failureCause = Exit.isFailure(stopAllExit) |
| 415 | ? stopAllExit.cause | 413 | ? stopAllExit.cause |
| 416 | : Exit.isFailure(closeScopeExit) | 414 | : Exit.isFailure(closeScopeExit) |
| 417 | ? closeScopeExit.cause | 415 | ? closeScopeExit.cause |
| 418 | : Exit.isFailure(disposeRuntimeExit) | 416 | : Exit.isFailure(disposeRuntimeExit) |
| 419 | ? disposeRuntimeExit.cause | 417 | ? disposeRuntimeExit.cause |
| 420 | : null; | 418 | : null; |
| 421 | 419 | ||
| 422 | if (failureCause) { | 420 | if (failureCause) { |
| 423 | return yield* Effect.failCause(failureCause); | 421 | return yield* Effect.failCause(failureCause); |
| 424 | } | 422 | } |
| 425 | }); | 423 | }); |
| 426 | 424 | ||
| 427 | yield* shutdown.pipe( | 425 | yield* shutdown.pipe( |
| 428 | Effect.ensuring( | 426 | Effect.ensuring( |
| 429 | Effect.sync(() => { | 427 | Effect.sync(() => { |
| 430 | fs.rmSync(rootDir, { recursive: true, force: true }); | 428 | fs.rmSync(rootDir, { recursive: true, force: true }); |
| 431 | }), | 429 | }), |
| 432 | ), | 430 | ), |
| 433 | ); | 431 | ); |
| 434 | }); | 432 | }); |
| 435 | 433 | ||
| 436 | return { | 434 | return { |
| 437 | rootDir, | 435 | rootDir, |
| 438 | workspaceDir, | 436 | workspaceDir, |
| 439 | dbPath, | 437 | dbPath, |
| 440 | adapterHarness: adapterHarness as TestProviderAdapterHarness, | 438 | adapterHarness: adapterHarness as TestProviderAdapterHarness, |
| 441 | engine, | 439 | engine, |
| 442 | snapshotQuery, | 440 | snapshotQuery, |
| 443 | providerService, | 441 | providerService, |
| 444 | checkpointStore, | 442 | checkpointStore, |
| 445 | checkpointRepository, | 443 | checkpointRepository, |
| 446 | pendingApprovalRepository, | 444 | pendingApprovalRepository, |
| 447 | waitForThread, | 445 | waitForThread, |
| 448 | waitForDomainEvent, | 446 | waitForDomainEvent, |
| 449 | waitForPendingApproval, | 447 | waitForPendingApproval, |
| 450 | dispose, | 448 | dispose, |
| 451 | } satisfies OrchestrationIntegrationHarness; | 449 | } satisfies OrchestrationIntegrationHarness; |
| 452 | }); | 450 | }); |
apps/server/integration/TestProviderAdapter.integration.ts+60-69| ... | @@ -7,9 +7,6 @@ import { | ... | @@ -7,9 +7,6 @@ import { |
| 7 | ProviderRuntimeEvent, | 7 | ProviderRuntimeEvent, |
| 8 | RuntimeSessionId, | 8 | RuntimeSessionId, |
| 9 | ProviderSession, | 9 | ProviderSession, |
| 10 | ProviderSessionId, | ||
| 11 | ProviderThreadId, | ||
| 12 | ProviderTurnId, | ||
| 13 | ProviderTurnStartResult, | 10 | ProviderTurnStartResult, |
| 14 | ThreadId, | 11 | ThreadId, |
| 15 | TurnId, | 12 | TurnId, |
| ... | @@ -39,9 +36,8 @@ export type FixtureProviderRuntimeEvent = { | ... | @@ -39,9 +36,8 @@ export type FixtureProviderRuntimeEvent = { |
| 39 | readonly type: string; | 36 | readonly type: string; |
| 40 | readonly eventId: EventId; | 37 | readonly eventId: EventId; |
| 41 | readonly provider: "codex" | "claudeCode" | "cursor"; | 38 | readonly provider: "codex" | "claudeCode" | "cursor"; |
| 42 | readonly sessionId: string; | ||
| 43 | readonly createdAt: string; | 39 | readonly createdAt: string; |
| 44 | readonly threadId?: string | undefined; | 40 | readonly threadId: string; |
| 45 | readonly turnId?: string | undefined; | 41 | readonly turnId?: string | undefined; |
| 46 | readonly itemId?: string | undefined; | 42 | readonly itemId?: string | undefined; |
| 47 | readonly requestId?: string | undefined; | 43 | readonly requestId?: string | undefined; |
| ... | @@ -184,18 +180,18 @@ export interface TestProviderAdapterHarness { | ... | @@ -184,18 +180,18 @@ export interface TestProviderAdapterHarness { |
| 184 | readonly adapter: ProviderAdapterShape<ProviderAdapterError>; | 180 | readonly adapter: ProviderAdapterShape<ProviderAdapterError>; |
| 185 | readonly provider: "codex" | "claudeCode"; | 181 | readonly provider: "codex" | "claudeCode"; |
| 186 | readonly queueTurnResponse: ( | 182 | readonly queueTurnResponse: ( |
| 187 | sessionId: string, | 183 | threadId: ThreadId, |
| 188 | response: TestTurnResponse, | 184 | response: TestTurnResponse, |
| 189 | ) => Effect.Effect<void, ProviderAdapterSessionNotFoundError>; | 185 | ) => Effect.Effect<void, ProviderAdapterSessionNotFoundError>; |
| 190 | readonly queueTurnResponseForNextSession: ( | 186 | readonly queueTurnResponseForNextSession: ( |
| 191 | response: TestTurnResponse, | 187 | response: TestTurnResponse, |
| 192 | ) => Effect.Effect<void, never>; | 188 | ) => Effect.Effect<void, never>; |
| 193 | readonly getStartCount: () => number; | 189 | readonly getStartCount: () => number; |
| 194 | readonly getRollbackCalls: (sessionId: string) => ReadonlyArray<number>; | 190 | readonly getRollbackCalls: (threadId: ThreadId) => ReadonlyArray<number>; |
| 195 | readonly getInterruptCalls: (sessionId: string) => ReadonlyArray<ProviderTurnId | undefined>; | 191 | readonly getInterruptCalls: (threadId: ThreadId) => ReadonlyArray<TurnId | undefined>; |
| 196 | readonly listActiveSessionIds: () => ReadonlyArray<ProviderSessionId>; | 192 | readonly listActiveSessionIds: () => ReadonlyArray<ThreadId>; |
| 197 | readonly getApprovalResponses: (sessionId: string) => ReadonlyArray<{ | 193 | readonly getApprovalResponses: (threadId: ThreadId) => ReadonlyArray<{ |
| 198 | readonly sessionId: ProviderSessionId; | 194 | readonly threadId: ThreadId; |
| 199 | readonly requestId: ApprovalRequestId; | 195 | readonly requestId: ApprovalRequestId; |
| 200 | readonly decision: ProviderApprovalDecision; | 196 | readonly decision: ProviderApprovalDecision; |
| 201 | }>; | 197 | }>; |
| ... | @@ -211,19 +207,19 @@ function nowIso(): string { | ... | @@ -211,19 +207,19 @@ function nowIso(): string { |
| 211 | 207 | ||
| 212 | function sessionNotFound( | 208 | function sessionNotFound( |
| 213 | provider: "codex" | "claudeCode", | 209 | provider: "codex" | "claudeCode", |
| 214 | sessionId: string, | 210 | threadId: ThreadId, |
| 215 | ): ProviderAdapterSessionNotFoundError { | 211 | ): ProviderAdapterSessionNotFoundError { |
| 216 | return new ProviderAdapterSessionNotFoundError({ | 212 | return new ProviderAdapterSessionNotFoundError({ |
| 217 | provider, | 213 | provider, |
| 218 | sessionId, | 214 | threadId: String(threadId), |
| 219 | }); | 215 | }); |
| 220 | } | 216 | } |
| 221 | 217 | ||
| 222 | function missingSessionEffect( | 218 | function missingSessionEffect( |
| 223 | provider: "codex" | "claudeCode", | 219 | provider: "codex" | "claudeCode", |
| 224 | sessionId: string, | 220 | threadId: ThreadId, |
| 225 | ): Effect.Effect<never, ProviderAdapterError> { | 221 | ): Effect.Effect<never, ProviderAdapterError> { |
| 226 | return Effect.fail(sessionNotFound(provider, sessionId)); | 222 | return Effect.fail(sessionNotFound(provider, threadId)); |
| 227 | } | 223 | } |
| 228 | 224 | ||
| 229 | export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapterHarnessOptions) => | 225 | export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapterHarnessOptions) => |
| ... | @@ -231,13 +227,13 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -231,13 +227,13 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 231 | const provider = options?.provider ?? "codex"; | 227 | const provider = options?.provider ?? "codex"; |
| 232 | const runtimeEvents = yield* Queue.unbounded<ProviderRuntimeEvent>(); | 228 | const runtimeEvents = yield* Queue.unbounded<ProviderRuntimeEvent>(); |
| 233 | let sessionCount = 0; | 229 | let sessionCount = 0; |
| 234 | const sessions = new Map<string, SessionState>(); | 230 | const sessions = new Map<ThreadId, SessionState>(); |
| 235 | const queuedResponsesForNextSession: TestTurnResponse[] = []; | 231 | const queuedResponsesForNextSession: TestTurnResponse[] = []; |
| 236 | const interruptCallsBySession = new Map<string, Array<ProviderTurnId | undefined>>(); | 232 | const interruptCallsBySession = new Map<ThreadId, Array<TurnId | undefined>>(); |
| 237 | const approvalResponsesBySession = new Map< | 233 | const approvalResponsesBySession = new Map< |
| 238 | string, | 234 | ThreadId, |
| 239 | Array<{ | 235 | Array<{ |
| 240 | readonly sessionId: ProviderSessionId; | 236 | readonly threadId: ThreadId; |
| 241 | readonly requestId: ApprovalRequestId; | 237 | readonly requestId: ApprovalRequestId; |
| 242 | readonly decision: ProviderApprovalDecision; | 238 | readonly decision: ProviderApprovalDecision; |
| 243 | }> | 239 | }> |
| ... | @@ -256,23 +252,21 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -256,23 +252,21 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 256 | } | 252 | } |
| 257 | 253 | ||
| 258 | sessionCount += 1; | 254 | sessionCount += 1; |
| 259 | const sessionId = ProviderSessionId.makeUnsafe(`test-session-${sessionCount}`); | 255 | const threadId = input.threadId; |
| 260 | const threadId = ProviderThreadId.makeUnsafe(`test-thread-${sessionCount}`); | ||
| 261 | const createdAt = nowIso(); | 256 | const createdAt = nowIso(); |
| 262 | 257 | ||
| 263 | const session: ProviderSession = { | 258 | const session: ProviderSession = { |
| 264 | sessionId, | ||
| 265 | provider, | 259 | provider, |
| 266 | status: "ready", | 260 | status: "ready", |
| 267 | runtimeMode: input.runtimeMode, | 261 | runtimeMode: input.runtimeMode, |
| 268 | threadId, | 262 | threadId, |
| 269 | cwd: input.cwd, | 263 | cwd: input.cwd, |
| 270 | resumeCursor: input.resumeCursor ?? { sessionId }, | 264 | resumeCursor: input.resumeCursor ?? { threadId: String(threadId), seed: sessionCount }, |
| 271 | createdAt, | 265 | createdAt, |
| 272 | updatedAt: createdAt, | 266 | updatedAt: createdAt, |
| 273 | }; | 267 | }; |
| 274 | 268 | ||
| 275 | sessions.set(sessionId, { | 269 | sessions.set(threadId, { |
| 276 | session, | 270 | session, |
| 277 | snapshot: { | 271 | snapshot: { |
| 278 | threadId, | 272 | threadId, |
| ... | @@ -288,21 +282,21 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -288,21 +282,21 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 288 | 282 | ||
| 289 | const sendTurn: ProviderAdapterShape<ProviderAdapterError>["sendTurn"] = (input) => | 283 | const sendTurn: ProviderAdapterShape<ProviderAdapterError>["sendTurn"] = (input) => |
| 290 | Effect.gen(function* () { | 284 | Effect.gen(function* () { |
| 291 | const state = sessions.get(input.sessionId); | 285 | const state = sessions.get(input.threadId); |
| 292 | if (!state) { | 286 | if (!state) { |
| 293 | return yield* missingSessionEffect(provider, input.sessionId); | 287 | return yield* missingSessionEffect(provider, input.threadId); |
| 294 | } | 288 | } |
| 295 | 289 | ||
| 296 | state.turnCount += 1; | 290 | state.turnCount += 1; |
| 297 | const turnCount = state.turnCount; | 291 | const turnCount = state.turnCount; |
| 298 | const turnId = ProviderTurnId.makeUnsafe(`turn-${turnCount}`); | 292 | const turnId = TurnId.makeUnsafe(`turn-${turnCount}`); |
| 299 | 293 | ||
| 300 | const response = state.queuedResponses.shift(); | 294 | const response = state.queuedResponses.shift(); |
| 301 | if (!response) { | 295 | if (!response) { |
| 302 | return yield* new ProviderAdapterValidationError({ | 296 | return yield* new ProviderAdapterValidationError({ |
| 303 | provider, | 297 | provider, |
| 304 | operation: "sendTurn", | 298 | operation: "sendTurn", |
| 305 | issue: `No queued turn response for session ${input.sessionId}.`, | 299 | issue: `No queued turn response for thread ${input.threadId}.`, |
| 306 | }); | 300 | }); |
| 307 | } | 301 | } |
| 308 | 302 | ||
| ... | @@ -313,14 +307,12 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -313,14 +307,12 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 313 | ...(fixtureEvent as Record<string, unknown>), | 307 | ...(fixtureEvent as Record<string, unknown>), |
| 314 | eventId: randomUUID(), | 308 | eventId: randomUUID(), |
| 315 | provider, | 309 | provider, |
| 316 | sessionId: RuntimeSessionId.makeUnsafe(input.sessionId), | 310 | sessionId: RuntimeSessionId.makeUnsafe(String(input.threadId)), |
| 317 | createdAt: nowIso(), | 311 | createdAt: nowIso(), |
| 318 | }; | 312 | }; |
| 319 | if (Object.hasOwn(rawEvent, "threadId")) { | 313 | rawEvent.threadId = state.snapshot.threadId; |
| 320 | rawEvent.threadId = ThreadId.makeUnsafe(state.snapshot.threadId); | ||
| 321 | } | ||
| 322 | if (Object.hasOwn(rawEvent, "turnId")) { | 314 | if (Object.hasOwn(rawEvent, "turnId")) { |
| 323 | rawEvent.turnId = TurnId.makeUnsafe(turnId); | 315 | rawEvent.turnId = turnId; |
| 324 | } | 316 | } |
| 325 | 317 | ||
| 326 | const runtimeEvent = normalizeFixtureEvent(rawEvent); | 318 | const runtimeEvent = normalizeFixtureEvent(rawEvent); |
| ... | @@ -373,10 +365,9 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -373,10 +365,9 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 373 | type: "turn.completed", | 365 | type: "turn.completed", |
| 374 | eventId: EventId.makeUnsafe(randomUUID()), | 366 | eventId: EventId.makeUnsafe(randomUUID()), |
| 375 | provider, | 367 | provider, |
| 376 | sessionId: RuntimeSessionId.makeUnsafe(input.sessionId), | ||
| 377 | createdAt: nowIso(), | 368 | createdAt: nowIso(), |
| 378 | threadId: ThreadId.makeUnsafe(state.snapshot.threadId), | 369 | threadId: state.snapshot.threadId, |
| 379 | turnId: TurnId.makeUnsafe(turnId), | 370 | turnId, |
| 380 | payload: { | 371 | payload: { |
| 381 | state: "completed", | 372 | state: "completed", |
| 382 | }, | 373 | }, |
| ... | @@ -394,60 +385,60 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -394,60 +385,60 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 394 | }); | 385 | }); |
| 395 | 386 | ||
| 396 | const interruptTurn: ProviderAdapterShape<ProviderAdapterError>["interruptTurn"] = ( | 387 | const interruptTurn: ProviderAdapterShape<ProviderAdapterError>["interruptTurn"] = ( |
| 397 | sessionId, | 388 | threadId, |
| 398 | turnId, | 389 | turnId, |
| 399 | ) => | 390 | ) => |
| 400 | sessions.has(sessionId) | 391 | sessions.has(threadId) |
| 401 | ? Effect.sync(() => { | 392 | ? Effect.sync(() => { |
| 402 | const existing = interruptCallsBySession.get(sessionId) ?? []; | 393 | const existing = interruptCallsBySession.get(threadId) ?? []; |
| 403 | existing.push(turnId); | 394 | existing.push(turnId); |
| 404 | interruptCallsBySession.set(sessionId, existing); | 395 | interruptCallsBySession.set(threadId, existing); |
| 405 | }) | 396 | }) |
| 406 | : missingSessionEffect(provider, sessionId); | 397 | : missingSessionEffect(provider, threadId); |
| 407 | 398 | ||
| 408 | const respondToRequest: ProviderAdapterShape<ProviderAdapterError>["respondToRequest"] = ( | 399 | const respondToRequest: ProviderAdapterShape<ProviderAdapterError>["respondToRequest"] = ( |
| 409 | sessionId, | 400 | threadId, |
| 410 | requestId, | 401 | requestId, |
| 411 | decision, | 402 | decision, |
| 412 | ) => | 403 | ) => |
| 413 | sessions.has(sessionId) | 404 | sessions.has(threadId) |
| 414 | ? Effect.sync(() => { | 405 | ? Effect.sync(() => { |
| 415 | const existing = approvalResponsesBySession.get(sessionId) ?? []; | 406 | const existing = approvalResponsesBySession.get(threadId) ?? []; |
| 416 | existing.push({ | 407 | existing.push({ |
| 417 | sessionId, | 408 | threadId, |
| 418 | requestId, | 409 | requestId, |
| 419 | decision, | 410 | decision, |
| 420 | }); | 411 | }); |
| 421 | approvalResponsesBySession.set(sessionId, existing); | 412 | approvalResponsesBySession.set(threadId, existing); |
| 422 | }) | 413 | }) |
| 423 | : missingSessionEffect(provider, sessionId); | 414 | : missingSessionEffect(provider, threadId); |
| 424 | 415 | ||
| 425 | const stopSession: ProviderAdapterShape<ProviderAdapterError>["stopSession"] = (sessionId) => | 416 | const stopSession: ProviderAdapterShape<ProviderAdapterError>["stopSession"] = (threadId) => |
| 426 | Effect.sync(() => { | 417 | Effect.sync(() => { |
| 427 | sessions.delete(sessionId); | 418 | sessions.delete(threadId); |
| 428 | }); | 419 | }); |
| 429 | 420 | ||
| 430 | const listSessions: ProviderAdapterShape<ProviderAdapterError>["listSessions"] = () => | 421 | const listSessions: ProviderAdapterShape<ProviderAdapterError>["listSessions"] = () => |
| 431 | Effect.sync(() => Array.from(sessions.values(), (state) => state.session)); | 422 | Effect.sync(() => Array.from(sessions.values(), (state) => state.session)); |
| 432 | 423 | ||
| 433 | const hasSession: ProviderAdapterShape<ProviderAdapterError>["hasSession"] = (sessionId) => | 424 | const hasSession: ProviderAdapterShape<ProviderAdapterError>["hasSession"] = (threadId) => |
| 434 | Effect.succeed(sessions.has(sessionId)); | 425 | Effect.succeed(sessions.has(threadId)); |
| 435 | 426 | ||
| 436 | const readThread: ProviderAdapterShape<ProviderAdapterError>["readThread"] = (sessionId) => { | 427 | const readThread: ProviderAdapterShape<ProviderAdapterError>["readThread"] = (threadId) => { |
| 437 | const state = sessions.get(sessionId); | 428 | const state = sessions.get(threadId); |
| 438 | if (!state) { | 429 | if (!state) { |
| 439 | return missingSessionEffect(provider, sessionId); | 430 | return missingSessionEffect(provider, threadId); |
| 440 | } | 431 | } |
| 441 | return Effect.succeed(state.snapshot); | 432 | return Effect.succeed(state.snapshot); |
| 442 | }; | 433 | }; |
| 443 | 434 | ||
| 444 | const rollbackThread: ProviderAdapterShape<ProviderAdapterError>["rollbackThread"] = ( | 435 | const rollbackThread: ProviderAdapterShape<ProviderAdapterError>["rollbackThread"] = ( |
| 445 | sessionId, | 436 | threadId, |
| 446 | numTurns, | 437 | numTurns, |
| 447 | ) => { | 438 | ) => { |
| 448 | const state = sessions.get(sessionId); | 439 | const state = sessions.get(threadId); |
| 449 | if (!state) { | 440 | if (!state) { |
| 450 | return missingSessionEffect(provider, sessionId); | 441 | return missingSessionEffect(provider, threadId); |
| 451 | } | 442 | } |
| 452 | if (!Number.isInteger(numTurns) || numTurns < 0 || numTurns > state.snapshot.turns.length) { | 443 | if (!Number.isInteger(numTurns) || numTurns < 0 || numTurns > state.snapshot.turns.length) { |
| 453 | return Effect.fail( | 444 | return Effect.fail( |
| ... | @@ -494,16 +485,16 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -494,16 +485,16 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 494 | }; | 485 | }; |
| 495 | 486 | ||
| 496 | const queueTurnResponse = ( | 487 | const queueTurnResponse = ( |
| 497 | sessionId: string, | 488 | threadId: ThreadId, |
| 498 | response: TestTurnResponse, | 489 | response: TestTurnResponse, |
| 499 | ): Effect.Effect<void, ProviderAdapterSessionNotFoundError> => | 490 | ): Effect.Effect<void, ProviderAdapterSessionNotFoundError> => |
| 500 | Effect.sync(() => sessions.get(sessionId)).pipe( | 491 | Effect.sync(() => sessions.get(threadId)).pipe( |
| 501 | Effect.flatMap((state) => | 492 | Effect.flatMap((state) => |
| 502 | state | 493 | state |
| 503 | ? Effect.sync(() => { | 494 | ? Effect.sync(() => { |
| 504 | state.queuedResponses.push(response); | 495 | state.queuedResponses.push(response); |
| 505 | }) | 496 | }) |
| 506 | : Effect.fail(sessionNotFound(provider, sessionId)), | 497 | : Effect.fail(sessionNotFound(provider, threadId)), |
| 507 | ), | 498 | ), |
| 508 | ); | 499 | ); |
| 509 | 500 | ||
| ... | @@ -514,8 +505,8 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -514,8 +505,8 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 514 | queuedResponsesForNextSession.push(response); | 505 | queuedResponsesForNextSession.push(response); |
| 515 | }); | 506 | }); |
| 516 | 507 | ||
| 517 | const getRollbackCalls = (sessionId: string): ReadonlyArray<number> => { | 508 | const getRollbackCalls = (threadId: ThreadId): ReadonlyArray<number> => { |
| 518 | const state = sessions.get(sessionId); | 509 | const state = sessions.get(threadId); |
| 519 | if (!state) { | 510 | if (!state) { |
| 520 | return []; | 511 | return []; |
| 521 | } | 512 | } |
| ... | @@ -524,25 +515,25 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -524,25 +515,25 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 524 | 515 | ||
| 525 | const getStartCount = (): number => sessionCount; | 516 | const getStartCount = (): number => sessionCount; |
| 526 | 517 | ||
| 527 | const getInterruptCalls = (sessionId: string): ReadonlyArray<ProviderTurnId | undefined> => { | 518 | const getInterruptCalls = (threadId: ThreadId): ReadonlyArray<TurnId | undefined> => { |
| 528 | const calls = interruptCallsBySession.get(sessionId); | 519 | const calls = interruptCallsBySession.get(threadId); |
| 529 | if (!calls) { | 520 | if (!calls) { |
| 530 | return []; | 521 | return []; |
| 531 | } | 522 | } |
| 532 | return [...calls]; | 523 | return [...calls]; |
| 533 | }; | 524 | }; |
| 534 | 525 | ||
| 535 | const listActiveSessionIds = (): ReadonlyArray<ProviderSessionId> => | 526 | const listActiveSessionIds = (): ReadonlyArray<ThreadId> => |
| 536 | Array.from(sessions.values(), (state) => state.session.sessionId); | 527 | Array.from(sessions.values(), (state) => state.session.threadId); |
| 537 | 528 | ||
| 538 | const getApprovalResponses = ( | 529 | const getApprovalResponses = ( |
| 539 | sessionId: string, | 530 | threadId: ThreadId, |
| 540 | ): ReadonlyArray<{ | 531 | ): ReadonlyArray<{ |
| 541 | readonly sessionId: ProviderSessionId; | 532 | readonly threadId: ThreadId; |
| 542 | readonly requestId: ApprovalRequestId; | 533 | readonly requestId: ApprovalRequestId; |
| 543 | readonly decision: ProviderApprovalDecision; | 534 | readonly decision: ProviderApprovalDecision; |
| 544 | }> => { | 535 | }> => { |
| 545 | const responses = approvalResponsesBySession.get(sessionId); | 536 | const responses = approvalResponsesBySession.get(threadId); |
| 546 | if (!responses) { | 537 | if (!responses) { |
| 547 | return []; | 538 | return []; |
| 548 | } | 539 | } |
apps/server/integration/orchestrationEngine.integration.test.ts+72-117| ... | @@ -7,9 +7,6 @@ import { | ... | @@ -7,9 +7,6 @@ import { |
| 7 | EventId, | 7 | EventId, |
| 8 | MessageId, | 8 | MessageId, |
| 9 | ProjectId, | 9 | ProjectId, |
| 10 | ProviderSessionId, | ||
| 11 | ProviderThreadId, | ||
| 12 | ProviderTurnId, | ||
| 13 | ThreadId, | 10 | ThreadId, |
| 14 | } from "@t3tools/contracts"; | 11 | } from "@t3tools/contracts"; |
| 15 | import { assert, it } from "@effect/vitest"; | 12 | import { assert, it } from "@effect/vitest"; |
| ... | @@ -27,18 +24,12 @@ import { checkpointRefForThreadTurn } from "../src/checkpointing/Utils.ts"; | ... | @@ -27,18 +24,12 @@ import { checkpointRefForThreadTurn } from "../src/checkpointing/Utils.ts"; |
| 27 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); | 24 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); |
| 28 | const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); | 25 | const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); |
| 29 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); | 26 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 30 | const asProviderSessionId = (value: string): ProviderSessionId => | ||
| 31 | ProviderSessionId.makeUnsafe(value); | ||
| 32 | const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId.makeUnsafe(value); | ||
| 33 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); | ||
| 34 | const asApprovalRequestId = (value: string): ApprovalRequestId => | 27 | const asApprovalRequestId = (value: string): ApprovalRequestId => |
| 35 | ApprovalRequestId.makeUnsafe(value); | 28 | ApprovalRequestId.makeUnsafe(value); |
| 36 | 29 | ||
| 37 | const PROJECT_ID = asProjectId("project-1"); | 30 | const PROJECT_ID = asProjectId("project-1"); |
| 38 | const THREAD_ID = ThreadId.makeUnsafe("thread-1"); | 31 | const THREAD_ID = ThreadId.makeUnsafe("thread-1"); |
| 39 | const FIXTURE_SESSION_ID = asProviderSessionId("fixture-session"); | 32 | const FIXTURE_TURN_ID = "fixture-turn"; |
| 40 | const FIXTURE_THREAD_ID = asProviderThreadId("fixture-thread"); | ||
| 41 | const FIXTURE_TURN_ID = asProviderTurnId("fixture-turn"); | ||
| 42 | const APPROVAL_REQUEST_ID = asApprovalRequestId("req-approval-1"); | 33 | const APPROVAL_REQUEST_ID = asApprovalRequestId("req-approval-1"); |
| 43 | type IntegrationProvider = "codex" | "claudeCode"; | 34 | type IntegrationProvider = "codex" | "claudeCode"; |
| 44 | 35 | ||
| ... | @@ -81,7 +72,6 @@ function runtimeBase(eventId: string, createdAt: string, provider: IntegrationPr | ... | @@ -81,7 +72,6 @@ function runtimeBase(eventId: string, createdAt: string, provider: IntegrationPr |
| 81 | return { | 72 | return { |
| 82 | eventId: asEventId(eventId), | 73 | eventId: asEventId(eventId), |
| 83 | provider, | 74 | provider, |
| 84 | sessionId: FIXTURE_SESSION_ID, | ||
| 85 | createdAt, | 75 | createdAt, |
| 86 | }; | 76 | }; |
| 87 | } | 77 | } |
| ... | @@ -167,20 +157,20 @@ it.live("runs a single turn end-to-end and persists checkpoint state in sqlite + | ... | @@ -167,20 +157,20 @@ it.live("runs a single turn end-to-end and persists checkpoint state in sqlite + |
| 167 | { | 157 | { |
| 168 | type: "turn.started", | 158 | type: "turn.started", |
| 169 | ...runtimeBase("evt-single-1", "2026-02-24T10:00:00.000Z"), | 159 | ...runtimeBase("evt-single-1", "2026-02-24T10:00:00.000Z"), |
| 170 | threadId: FIXTURE_THREAD_ID, | 160 | threadId: THREAD_ID, |
| 171 | turnId: FIXTURE_TURN_ID, | 161 | turnId: FIXTURE_TURN_ID, |
| 172 | }, | 162 | }, |
| 173 | { | 163 | { |
| 174 | type: "message.delta", | 164 | type: "message.delta", |
| 175 | ...runtimeBase("evt-single-2", "2026-02-24T10:00:00.100Z"), | 165 | ...runtimeBase("evt-single-2", "2026-02-24T10:00:00.100Z"), |
| 176 | threadId: FIXTURE_THREAD_ID, | 166 | threadId: THREAD_ID, |
| 177 | turnId: FIXTURE_TURN_ID, | 167 | turnId: FIXTURE_TURN_ID, |
| 178 | delta: "Single turn response.\n", | 168 | delta: "Single turn response.\n", |
| 179 | }, | 169 | }, |
| 180 | { | 170 | { |
| 181 | type: "turn.completed", | 171 | type: "turn.completed", |
| 182 | ...runtimeBase("evt-single-3", "2026-02-24T10:00:00.200Z"), | 172 | ...runtimeBase("evt-single-3", "2026-02-24T10:00:00.200Z"), |
| 183 | threadId: FIXTURE_THREAD_ID, | 173 | threadId: THREAD_ID, |
| 184 | turnId: FIXTURE_TURN_ID, | 174 | turnId: FIXTURE_TURN_ID, |
| 185 | status: "completed", | 175 | status: "completed", |
| 186 | }, | 176 | }, |
| ... | @@ -276,15 +266,12 @@ it.live.skipIf(!process.env.CODEX_BINARY_PATH)( | ... | @@ -276,15 +266,12 @@ it.live.skipIf(!process.env.CODEX_BINARY_PATH)( |
| 276 | (entry) => | 266 | (entry) => |
| 277 | entry.session?.status === "ready" && | 267 | entry.session?.status === "ready" && |
| 278 | entry.session.providerName === "codex" && | 268 | entry.session.providerName === "codex" && |
| 279 | entry.session.providerThreadId !== null && | ||
| 280 | entry.messages.some( | 269 | entry.messages.some( |
| 281 | (message) => message.role === "assistant" && message.streaming === false, | 270 | (message) => message.role === "assistant" && message.streaming === false, |
| 282 | ), | 271 | ), |
| 283 | 180_000, | 272 | 180_000, |
| 284 | ); | 273 | ); |
| 285 | 274 | assert.equal(firstThread.session?.threadId, "thread-1"); | |
| 286 | const originalProviderThreadId = firstThread.session?.providerThreadId; | ||
| 287 | assert.isNotNull(originalProviderThreadId); | ||
| 288 | 275 | ||
| 289 | yield* harness.engine.dispatch({ | 276 | yield* harness.engine.dispatch({ |
| 290 | type: "thread.turn.start", | 277 | type: "thread.turn.start", |
| ... | @@ -305,15 +292,13 @@ it.live.skipIf(!process.env.CODEX_BINARY_PATH)( | ... | @@ -305,15 +292,13 @@ it.live.skipIf(!process.env.CODEX_BINARY_PATH)( |
| 305 | (entry) => | 292 | (entry) => |
| 306 | entry.session?.status === "ready" && | 293 | entry.session?.status === "ready" && |
| 307 | entry.session.providerName === "codex" && | 294 | entry.session.providerName === "codex" && |
| 308 | entry.session.providerThreadId !== null && | ||
| 309 | entry.session.runtimeMode === "approval-required" && | 295 | entry.session.runtimeMode === "approval-required" && |
| 310 | entry.messages.some( | 296 | entry.messages.some( |
| 311 | (message) => message.role === "assistant" && message.text.includes("BETA"), | 297 | (message) => message.role === "assistant" && message.text.includes("BETA"), |
| 312 | ), | 298 | ), |
| 313 | 180_000, | 299 | 180_000, |
| 314 | ); | 300 | ); |
| 315 | 301 | assert.equal(secondThread.session?.threadId, "thread-1"); | |
| 316 | assert.equal(secondThread.session?.providerThreadId, originalProviderThreadId); | ||
| 317 | }), | 302 | }), |
| 318 | ), | 303 | ), |
| 319 | ); | 304 | ); |
| ... | @@ -328,13 +313,13 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => | ... | @@ -328,13 +313,13 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => |
| 328 | { | 313 | { |
| 329 | type: "turn.started", | 314 | type: "turn.started", |
| 330 | ...runtimeBase("evt-multi-1", "2026-02-24T10:01:00.000Z"), | 315 | ...runtimeBase("evt-multi-1", "2026-02-24T10:01:00.000Z"), |
| 331 | threadId: FIXTURE_THREAD_ID, | 316 | threadId: THREAD_ID, |
| 332 | turnId: FIXTURE_TURN_ID, | 317 | turnId: FIXTURE_TURN_ID, |
| 333 | }, | 318 | }, |
| 334 | { | 319 | { |
| 335 | type: "tool.started", | 320 | type: "tool.started", |
| 336 | ...runtimeBase("evt-multi-2", "2026-02-24T10:01:00.100Z"), | 321 | ...runtimeBase("evt-multi-2", "2026-02-24T10:01:00.100Z"), |
| 337 | threadId: FIXTURE_THREAD_ID, | 322 | threadId: THREAD_ID, |
| 338 | turnId: FIXTURE_TURN_ID, | 323 | turnId: FIXTURE_TURN_ID, |
| 339 | toolKind: "command", | 324 | toolKind: "command", |
| 340 | title: "Edit file", | 325 | title: "Edit file", |
| ... | @@ -343,7 +328,7 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => | ... | @@ -343,7 +328,7 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => |
| 343 | { | 328 | { |
| 344 | type: "tool.completed", | 329 | type: "tool.completed", |
| 345 | ...runtimeBase("evt-multi-3", "2026-02-24T10:01:00.200Z"), | 330 | ...runtimeBase("evt-multi-3", "2026-02-24T10:01:00.200Z"), |
| 346 | threadId: FIXTURE_THREAD_ID, | 331 | threadId: THREAD_ID, |
| 347 | turnId: FIXTURE_TURN_ID, | 332 | turnId: FIXTURE_TURN_ID, |
| 348 | toolKind: "command", | 333 | toolKind: "command", |
| 349 | title: "Edit file", | 334 | title: "Edit file", |
| ... | @@ -352,14 +337,14 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => | ... | @@ -352,14 +337,14 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => |
| 352 | { | 337 | { |
| 353 | type: "message.delta", | 338 | type: "message.delta", |
| 354 | ...runtimeBase("evt-multi-4", "2026-02-24T10:01:00.300Z"), | 339 | ...runtimeBase("evt-multi-4", "2026-02-24T10:01:00.300Z"), |
| 355 | threadId: FIXTURE_THREAD_ID, | 340 | threadId: THREAD_ID, |
| 356 | turnId: FIXTURE_TURN_ID, | 341 | turnId: FIXTURE_TURN_ID, |
| 357 | delta: "Updated README to v2.\n", | 342 | delta: "Updated README to v2.\n", |
| 358 | }, | 343 | }, |
| 359 | { | 344 | { |
| 360 | type: "turn.completed", | 345 | type: "turn.completed", |
| 361 | ...runtimeBase("evt-multi-5", "2026-02-24T10:01:00.400Z"), | 346 | ...runtimeBase("evt-multi-5", "2026-02-24T10:01:00.400Z"), |
| 362 | threadId: FIXTURE_THREAD_ID, | 347 | threadId: THREAD_ID, |
| 363 | turnId: FIXTURE_TURN_ID, | 348 | turnId: FIXTURE_TURN_ID, |
| 364 | status: "completed", | 349 | status: "completed", |
| 365 | }, | 350 | }, |
| ... | @@ -377,35 +362,30 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => | ... | @@ -377,35 +362,30 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => |
| 377 | text: "Make first edit", | 362 | text: "Make first edit", |
| 378 | }); | 363 | }); |
| 379 | 364 | ||
| 380 | const firstTurnThread = yield* harness.waitForThread( | 365 | yield* harness.waitForThread( |
| 381 | THREAD_ID, | 366 | THREAD_ID, |
| 382 | (entry) => entry.checkpoints.length === 1 && entry.session?.providerSessionId !== null, | 367 | (entry) => entry.checkpoints.length === 1 && entry.session?.threadId === "thread-1", |
| 383 | ); | 368 | ); |
| 384 | const sessionId = firstTurnThread.session?.providerSessionId; | ||
| 385 | assert.equal(sessionId !== null, true); | ||
| 386 | if (!sessionId) { | ||
| 387 | return; | ||
| 388 | } | ||
| 389 | 369 | ||
| 390 | yield* harness.adapterHarness.queueTurnResponse(sessionId, { | 370 | yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, { |
| 391 | events: [ | 371 | events: [ |
| 392 | { | 372 | { |
| 393 | type: "turn.started", | 373 | type: "turn.started", |
| 394 | ...runtimeBase("evt-multi-6", "2026-02-24T10:02:00.000Z"), | 374 | ...runtimeBase("evt-multi-6", "2026-02-24T10:02:00.000Z"), |
| 395 | threadId: FIXTURE_THREAD_ID, | 375 | threadId: THREAD_ID, |
| 396 | turnId: FIXTURE_TURN_ID, | 376 | turnId: FIXTURE_TURN_ID, |
| 397 | }, | 377 | }, |
| 398 | { | 378 | { |
| 399 | type: "message.delta", | 379 | type: "message.delta", |
| 400 | ...runtimeBase("evt-multi-7", "2026-02-24T10:02:00.100Z"), | 380 | ...runtimeBase("evt-multi-7", "2026-02-24T10:02:00.100Z"), |
| 401 | threadId: FIXTURE_THREAD_ID, | 381 | threadId: THREAD_ID, |
| 402 | turnId: FIXTURE_TURN_ID, | 382 | turnId: FIXTURE_TURN_ID, |
| 403 | delta: "Updated README to v3.\n", | 383 | delta: "Updated README to v3.\n", |
| 404 | }, | 384 | }, |
| 405 | { | 385 | { |
| 406 | type: "turn.completed", | 386 | type: "turn.completed", |
| 407 | ...runtimeBase("evt-multi-8", "2026-02-24T10:02:00.200Z"), | 387 | ...runtimeBase("evt-multi-8", "2026-02-24T10:02:00.200Z"), |
| 408 | threadId: FIXTURE_THREAD_ID, | 388 | threadId: THREAD_ID, |
| 409 | turnId: FIXTURE_TURN_ID, | 389 | turnId: FIXTURE_TURN_ID, |
| 410 | status: "completed", | 390 | status: "completed", |
| 411 | }, | 391 | }, |
| ... | @@ -492,13 +472,13 @@ it.live("tracks approval requests and resolves pending approvals on user respons | ... | @@ -492,13 +472,13 @@ it.live("tracks approval requests and resolves pending approvals on user respons |
| 492 | { | 472 | { |
| 493 | type: "turn.started", | 473 | type: "turn.started", |
| 494 | ...runtimeBase("evt-approval-1", "2026-02-24T10:03:00.000Z"), | 474 | ...runtimeBase("evt-approval-1", "2026-02-24T10:03:00.000Z"), |
| 495 | threadId: FIXTURE_THREAD_ID, | 475 | threadId: THREAD_ID, |
| 496 | turnId: FIXTURE_TURN_ID, | 476 | turnId: FIXTURE_TURN_ID, |
| 497 | }, | 477 | }, |
| 498 | { | 478 | { |
| 499 | type: "approval.requested", | 479 | type: "approval.requested", |
| 500 | ...runtimeBase("evt-approval-2", "2026-02-24T10:03:00.100Z"), | 480 | ...runtimeBase("evt-approval-2", "2026-02-24T10:03:00.100Z"), |
| 501 | threadId: FIXTURE_THREAD_ID, | 481 | threadId: THREAD_ID, |
| 502 | turnId: FIXTURE_TURN_ID, | 482 | turnId: FIXTURE_TURN_ID, |
| 503 | requestId: APPROVAL_REQUEST_ID, | 483 | requestId: APPROVAL_REQUEST_ID, |
| 504 | requestKind: "command", | 484 | requestKind: "command", |
| ... | @@ -507,7 +487,7 @@ it.live("tracks approval requests and resolves pending approvals on user respons | ... | @@ -507,7 +487,7 @@ it.live("tracks approval requests and resolves pending approvals on user respons |
| 507 | { | 487 | { |
| 508 | type: "turn.completed", | 488 | type: "turn.completed", |
| 509 | ...runtimeBase("evt-approval-3", "2026-02-24T10:03:00.200Z"), | 489 | ...runtimeBase("evt-approval-3", "2026-02-24T10:03:00.200Z"), |
| 510 | threadId: FIXTURE_THREAD_ID, | 490 | threadId: THREAD_ID, |
| 511 | turnId: FIXTURE_TURN_ID, | 491 | turnId: FIXTURE_TURN_ID, |
| 512 | status: "completed", | 492 | status: "completed", |
| 513 | }, | 493 | }, |
| ... | @@ -551,9 +531,8 @@ it.live("tracks approval requests and resolves pending approvals on user respons | ... | @@ -551,9 +531,8 @@ it.live("tracks approval requests and resolves pending approvals on user respons |
| 551 | assert.equal(resolvedRow.status, "resolved"); | 531 | assert.equal(resolvedRow.status, "resolved"); |
| 552 | assert.equal(resolvedRow.decision, "accept"); | 532 | assert.equal(resolvedRow.decision, "accept"); |
| 553 | 533 | ||
| 554 | const providerSessionId = thread.session?.providerSessionId ?? "test-session-1"; | ||
| 555 | const approvalResponses = yield* waitForSync( | 534 | const approvalResponses = yield* waitForSync( |
| 556 | () => harness.adapterHarness.getApprovalResponses(providerSessionId), | 535 | () => harness.adapterHarness.getApprovalResponses(THREAD_ID), |
| 557 | (responses) => responses.length === 1, | 536 | (responses) => responses.length === 1, |
| 558 | "provider approval response", | 537 | "provider approval response", |
| 559 | ); | 538 | ); |
| ... | @@ -574,13 +553,13 @@ it.live("records failed turn runtime state and checkpoint status as error", () = | ... | @@ -574,13 +553,13 @@ it.live("records failed turn runtime state and checkpoint status as error", () = |
| 574 | { | 553 | { |
| 575 | type: "turn.started", | 554 | type: "turn.started", |
| 576 | ...runtimeBase("evt-failure-1", "2026-02-24T10:04:00.000Z"), | 555 | ...runtimeBase("evt-failure-1", "2026-02-24T10:04:00.000Z"), |
| 577 | threadId: FIXTURE_THREAD_ID, | 556 | threadId: THREAD_ID, |
| 578 | turnId: FIXTURE_TURN_ID, | 557 | turnId: FIXTURE_TURN_ID, |
| 579 | }, | 558 | }, |
| 580 | { | 559 | { |
| 581 | type: "content.delta", | 560 | type: "content.delta", |
| 582 | ...runtimeBase("evt-failure-2", "2026-02-24T10:04:00.100Z"), | 561 | ...runtimeBase("evt-failure-2", "2026-02-24T10:04:00.100Z"), |
| 583 | threadId: FIXTURE_THREAD_ID, | 562 | threadId: THREAD_ID, |
| 584 | turnId: FIXTURE_TURN_ID, | 563 | turnId: FIXTURE_TURN_ID, |
| 585 | payload: { | 564 | payload: { |
| 586 | streamKind: "assistant_text", | 565 | streamKind: "assistant_text", |
| ... | @@ -590,7 +569,7 @@ it.live("records failed turn runtime state and checkpoint status as error", () = | ... | @@ -590,7 +569,7 @@ it.live("records failed turn runtime state and checkpoint status as error", () = |
| 590 | { | 569 | { |
| 591 | type: "runtime.error", | 570 | type: "runtime.error", |
| 592 | ...runtimeBase("evt-failure-3", "2026-02-24T10:04:00.200Z"), | 571 | ...runtimeBase("evt-failure-3", "2026-02-24T10:04:00.200Z"), |
| 593 | threadId: FIXTURE_THREAD_ID, | 572 | threadId: THREAD_ID, |
| 594 | turnId: FIXTURE_TURN_ID, | 573 | turnId: FIXTURE_TURN_ID, |
| 595 | payload: { | 574 | payload: { |
| 596 | message: "Sandbox command failed.", | 575 | message: "Sandbox command failed.", |
| ... | @@ -599,7 +578,7 @@ it.live("records failed turn runtime state and checkpoint status as error", () = | ... | @@ -599,7 +578,7 @@ it.live("records failed turn runtime state and checkpoint status as error", () = |
| 599 | { | 578 | { |
| 600 | type: "turn.completed", | 579 | type: "turn.completed", |
| 601 | ...runtimeBase("evt-failure-4", "2026-02-24T10:04:00.300Z"), | 580 | ...runtimeBase("evt-failure-4", "2026-02-24T10:04:00.300Z"), |
| 602 | threadId: FIXTURE_THREAD_ID, | 581 | threadId: THREAD_ID, |
| 603 | turnId: FIXTURE_TURN_ID, | 582 | turnId: FIXTURE_TURN_ID, |
| 604 | payload: { | 583 | payload: { |
| 605 | state: "failed", | 584 | state: "failed", |
| ... | @@ -653,13 +632,13 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git | ... | @@ -653,13 +632,13 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git |
| 653 | { | 632 | { |
| 654 | type: "turn.started", | 633 | type: "turn.started", |
| 655 | ...runtimeBase("evt-revert-1", "2026-02-24T10:05:00.000Z"), | 634 | ...runtimeBase("evt-revert-1", "2026-02-24T10:05:00.000Z"), |
| 656 | threadId: FIXTURE_THREAD_ID, | 635 | threadId: THREAD_ID, |
| 657 | turnId: FIXTURE_TURN_ID, | 636 | turnId: FIXTURE_TURN_ID, |
| 658 | }, | 637 | }, |
| 659 | { | 638 | { |
| 660 | type: "tool.started", | 639 | type: "tool.started", |
| 661 | ...runtimeBase("evt-revert-1-tool-started", "2026-02-24T10:05:00.025Z"), | 640 | ...runtimeBase("evt-revert-1-tool-started", "2026-02-24T10:05:00.025Z"), |
| 662 | threadId: FIXTURE_THREAD_ID, | 641 | threadId: THREAD_ID, |
| 663 | turnId: FIXTURE_TURN_ID, | 642 | turnId: FIXTURE_TURN_ID, |
| 664 | toolKind: "command", | 643 | toolKind: "command", |
| 665 | title: "Edit file", | 644 | title: "Edit file", |
| ... | @@ -668,7 +647,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git | ... | @@ -668,7 +647,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git |
| 668 | { | 647 | { |
| 669 | type: "tool.completed", | 648 | type: "tool.completed", |
| 670 | ...runtimeBase("evt-revert-1-tool-completed", "2026-02-24T10:05:00.035Z"), | 649 | ...runtimeBase("evt-revert-1-tool-completed", "2026-02-24T10:05:00.035Z"), |
| 671 | threadId: FIXTURE_THREAD_ID, | 650 | threadId: THREAD_ID, |
| 672 | turnId: FIXTURE_TURN_ID, | 651 | turnId: FIXTURE_TURN_ID, |
| 673 | toolKind: "command", | 652 | toolKind: "command", |
| 674 | title: "Edit file", | 653 | title: "Edit file", |
| ... | @@ -677,14 +656,14 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git | ... | @@ -677,14 +656,14 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git |
| 677 | { | 656 | { |
| 678 | type: "message.delta", | 657 | type: "message.delta", |
| 679 | ...runtimeBase("evt-revert-1a", "2026-02-24T10:05:00.050Z"), | 658 | ...runtimeBase("evt-revert-1a", "2026-02-24T10:05:00.050Z"), |
| 680 | threadId: FIXTURE_THREAD_ID, | 659 | threadId: THREAD_ID, |
| 681 | turnId: FIXTURE_TURN_ID, | 660 | turnId: FIXTURE_TURN_ID, |
| 682 | delta: "Updated README to v2.\n", | 661 | delta: "Updated README to v2.\n", |
| 683 | }, | 662 | }, |
| 684 | { | 663 | { |
| 685 | type: "turn.completed", | 664 | type: "turn.completed", |
| 686 | ...runtimeBase("evt-revert-2", "2026-02-24T10:05:00.100Z"), | 665 | ...runtimeBase("evt-revert-2", "2026-02-24T10:05:00.100Z"), |
| 687 | threadId: FIXTURE_THREAD_ID, | 666 | threadId: THREAD_ID, |
| 688 | turnId: FIXTURE_TURN_ID, | 667 | turnId: FIXTURE_TURN_ID, |
| 689 | status: "completed", | 668 | status: "completed", |
| 690 | }, | 669 | }, |
| ... | @@ -701,28 +680,23 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git | ... | @@ -701,28 +680,23 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git |
| 701 | text: "First edit", | 680 | text: "First edit", |
| 702 | }); | 681 | }); |
| 703 | 682 | ||
| 704 | const firstTurnThread = yield* harness.waitForThread( | 683 | yield* harness.waitForThread( |
| 705 | THREAD_ID, | 684 | THREAD_ID, |
| 706 | (entry) => entry.session?.providerSessionId !== null && entry.checkpoints.length === 1, | 685 | (entry) => entry.session?.threadId === "thread-1" && entry.checkpoints.length === 1, |
| 707 | ); | 686 | ); |
| 708 | const sessionId = firstTurnThread.session?.providerSessionId; | ||
| 709 | assert.equal(sessionId !== null, true); | ||
| 710 | if (!sessionId) { | ||
| 711 | return; | ||
| 712 | } | ||
| 713 | 687 | ||
| 714 | yield* harness.adapterHarness.queueTurnResponse(sessionId, { | 688 | yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, { |
| 715 | events: [ | 689 | events: [ |
| 716 | { | 690 | { |
| 717 | type: "turn.started", | 691 | type: "turn.started", |
| 718 | ...runtimeBase("evt-revert-3", "2026-02-24T10:05:01.000Z"), | 692 | ...runtimeBase("evt-revert-3", "2026-02-24T10:05:01.000Z"), |
| 719 | threadId: FIXTURE_THREAD_ID, | 693 | threadId: THREAD_ID, |
| 720 | turnId: FIXTURE_TURN_ID, | 694 | turnId: FIXTURE_TURN_ID, |
| 721 | }, | 695 | }, |
| 722 | { | 696 | { |
| 723 | type: "tool.started", | 697 | type: "tool.started", |
| 724 | ...runtimeBase("evt-revert-3-tool-started", "2026-02-24T10:05:01.025Z"), | 698 | ...runtimeBase("evt-revert-3-tool-started", "2026-02-24T10:05:01.025Z"), |
| 725 | threadId: FIXTURE_THREAD_ID, | 699 | threadId: THREAD_ID, |
| 726 | turnId: FIXTURE_TURN_ID, | 700 | turnId: FIXTURE_TURN_ID, |
| 727 | toolKind: "command", | 701 | toolKind: "command", |
| 728 | title: "Edit file", | 702 | title: "Edit file", |
| ... | @@ -731,7 +705,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git | ... | @@ -731,7 +705,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git |
| 731 | { | 705 | { |
| 732 | type: "tool.completed", | 706 | type: "tool.completed", |
| 733 | ...runtimeBase("evt-revert-3-tool-completed", "2026-02-24T10:05:01.035Z"), | 707 | ...runtimeBase("evt-revert-3-tool-completed", "2026-02-24T10:05:01.035Z"), |
| 734 | threadId: FIXTURE_THREAD_ID, | 708 | threadId: THREAD_ID, |
| 735 | turnId: FIXTURE_TURN_ID, | 709 | turnId: FIXTURE_TURN_ID, |
| 736 | toolKind: "command", | 710 | toolKind: "command", |
| 737 | title: "Edit file", | 711 | title: "Edit file", |
| ... | @@ -740,14 +714,14 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git | ... | @@ -740,14 +714,14 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git |
| 740 | { | 714 | { |
| 741 | type: "message.delta", | 715 | type: "message.delta", |
| 742 | ...runtimeBase("evt-revert-3a", "2026-02-24T10:05:01.050Z"), | 716 | ...runtimeBase("evt-revert-3a", "2026-02-24T10:05:01.050Z"), |
| 743 | threadId: FIXTURE_THREAD_ID, | 717 | threadId: THREAD_ID, |
| 744 | turnId: FIXTURE_TURN_ID, | 718 | turnId: FIXTURE_TURN_ID, |
| 745 | delta: "Updated README to v3.\n", | 719 | delta: "Updated README to v3.\n", |
| 746 | }, | 720 | }, |
| 747 | { | 721 | { |
| 748 | type: "turn.completed", | 722 | type: "turn.completed", |
| 749 | ...runtimeBase("evt-revert-4", "2026-02-24T10:05:01.100Z"), | 723 | ...runtimeBase("evt-revert-4", "2026-02-24T10:05:01.100Z"), |
| 750 | threadId: FIXTURE_THREAD_ID, | 724 | threadId: THREAD_ID, |
| 751 | turnId: FIXTURE_TURN_ID, | 725 | turnId: FIXTURE_TURN_ID, |
| 752 | status: "completed", | 726 | status: "completed", |
| 753 | }, | 727 | }, |
| ... | @@ -816,7 +790,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git | ... | @@ -816,7 +790,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git |
| 816 | gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)), | 790 | gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)), |
| 817 | false, | 791 | false, |
| 818 | ); | 792 | ); |
| 819 | assert.deepEqual(harness.adapterHarness.getRollbackCalls(sessionId), [1]); | 793 | assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]); |
| 820 | 794 | ||
| 821 | const checkpointRows = yield* harness.checkpointRepository.listByThreadId({ | 795 | const checkpointRows = yield* harness.checkpointRepository.listByThreadId({ |
| 822 | threadId: THREAD_ID, | 796 | threadId: THREAD_ID, |
| ... | @@ -874,20 +848,20 @@ it.live("starts a claudeCode session on first turn when provider is requested", | ... | @@ -874,20 +848,20 @@ it.live("starts a claudeCode session on first turn when provider is requested", |
| 874 | { | 848 | { |
| 875 | type: "turn.started", | 849 | type: "turn.started", |
| 876 | ...runtimeBase("evt-claude-start-1", "2026-02-24T10:10:00.000Z", "claudeCode"), | 850 | ...runtimeBase("evt-claude-start-1", "2026-02-24T10:10:00.000Z", "claudeCode"), |
| 877 | threadId: FIXTURE_THREAD_ID, | 851 | threadId: THREAD_ID, |
| 878 | turnId: FIXTURE_TURN_ID, | 852 | turnId: FIXTURE_TURN_ID, |
| 879 | }, | 853 | }, |
| 880 | { | 854 | { |
| 881 | type: "message.delta", | 855 | type: "message.delta", |
| 882 | ...runtimeBase("evt-claude-start-2", "2026-02-24T10:10:00.050Z", "claudeCode"), | 856 | ...runtimeBase("evt-claude-start-2", "2026-02-24T10:10:00.050Z", "claudeCode"), |
| 883 | threadId: FIXTURE_THREAD_ID, | 857 | threadId: THREAD_ID, |
| 884 | turnId: FIXTURE_TURN_ID, | 858 | turnId: FIXTURE_TURN_ID, |
| 885 | delta: "Claude first turn.\n", | 859 | delta: "Claude first turn.\n", |
| 886 | }, | 860 | }, |
| 887 | { | 861 | { |
| 888 | type: "turn.completed", | 862 | type: "turn.completed", |
| 889 | ...runtimeBase("evt-claude-start-3", "2026-02-24T10:10:00.100Z", "claudeCode"), | 863 | ...runtimeBase("evt-claude-start-3", "2026-02-24T10:10:00.100Z", "claudeCode"), |
| 890 | threadId: FIXTURE_THREAD_ID, | 864 | threadId: THREAD_ID, |
| 891 | turnId: FIXTURE_TURN_ID, | 865 | turnId: FIXTURE_TURN_ID, |
| 892 | status: "completed", | 866 | status: "completed", |
| 893 | }, | 867 | }, |
| ... | @@ -928,20 +902,20 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res | ... | @@ -928,20 +902,20 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res |
| 928 | { | 902 | { |
| 929 | type: "turn.started", | 903 | type: "turn.started", |
| 930 | ...runtimeBase("evt-claude-recover-1", "2026-02-24T10:11:00.000Z", "claudeCode"), | 904 | ...runtimeBase("evt-claude-recover-1", "2026-02-24T10:11:00.000Z", "claudeCode"), |
| 931 | threadId: FIXTURE_THREAD_ID, | 905 | threadId: THREAD_ID, |
| 932 | turnId: FIXTURE_TURN_ID, | 906 | turnId: FIXTURE_TURN_ID, |
| 933 | }, | 907 | }, |
| 934 | { | 908 | { |
| 935 | type: "message.delta", | 909 | type: "message.delta", |
| 936 | ...runtimeBase("evt-claude-recover-2", "2026-02-24T10:11:00.050Z", "claudeCode"), | 910 | ...runtimeBase("evt-claude-recover-2", "2026-02-24T10:11:00.050Z", "claudeCode"), |
| 937 | threadId: FIXTURE_THREAD_ID, | 911 | threadId: THREAD_ID, |
| 938 | turnId: FIXTURE_TURN_ID, | 912 | turnId: FIXTURE_TURN_ID, |
| 939 | delta: "Turn before restart.\n", | 913 | delta: "Turn before restart.\n", |
| 940 | }, | 914 | }, |
| 941 | { | 915 | { |
| 942 | type: "turn.completed", | 916 | type: "turn.completed", |
| 943 | ...runtimeBase("evt-claude-recover-3", "2026-02-24T10:11:00.100Z", "claudeCode"), | 917 | ...runtimeBase("evt-claude-recover-3", "2026-02-24T10:11:00.100Z", "claudeCode"), |
| 944 | threadId: FIXTURE_THREAD_ID, | 918 | threadId: THREAD_ID, |
| 945 | turnId: FIXTURE_TURN_ID, | 919 | turnId: FIXTURE_TURN_ID, |
| 946 | status: "completed", | 920 | status: "completed", |
| 947 | }, | 921 | }, |
| ... | @@ -956,16 +930,10 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res | ... | @@ -956,16 +930,10 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res |
| 956 | provider: "claudeCode", | 930 | provider: "claudeCode", |
| 957 | }); | 931 | }); |
| 958 | 932 | ||
| 959 | const firstThread = yield* harness.waitForThread( | 933 | yield* harness.waitForThread( |
| 960 | THREAD_ID, | 934 | THREAD_ID, |
| 961 | (entry) => | 935 | (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", |
| 962 | entry.latestTurn?.turnId === "turn-1" && entry.session?.providerSessionId !== null, | ||
| 963 | ); | 936 | ); |
| 964 | const staleSessionId = firstThread.session?.providerSessionId; | ||
| 965 | assert.equal(staleSessionId !== null, true); | ||
| 966 | if (!staleSessionId) { | ||
| 967 | return; | ||
| 968 | } | ||
| 969 | 937 | ||
| 970 | yield* harness.providerService.stopAll(); | 938 | yield* harness.providerService.stopAll(); |
| 971 | yield* waitForSync( | 939 | yield* waitForSync( |
| ... | @@ -979,20 +947,20 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res | ... | @@ -979,20 +947,20 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res |
| 979 | { | 947 | { |
| 980 | type: "turn.started", | 948 | type: "turn.started", |
| 981 | ...runtimeBase("evt-claude-recover-4", "2026-02-24T10:11:01.000Z", "claudeCode"), | 949 | ...runtimeBase("evt-claude-recover-4", "2026-02-24T10:11:01.000Z", "claudeCode"), |
| 982 | threadId: FIXTURE_THREAD_ID, | 950 | threadId: THREAD_ID, |
| 983 | turnId: FIXTURE_TURN_ID, | 951 | turnId: FIXTURE_TURN_ID, |
| 984 | }, | 952 | }, |
| 985 | { | 953 | { |
| 986 | type: "message.delta", | 954 | type: "message.delta", |
| 987 | ...runtimeBase("evt-claude-recover-5", "2026-02-24T10:11:01.050Z", "claudeCode"), | 955 | ...runtimeBase("evt-claude-recover-5", "2026-02-24T10:11:01.050Z", "claudeCode"), |
| 988 | threadId: FIXTURE_THREAD_ID, | 956 | threadId: THREAD_ID, |
| 989 | turnId: FIXTURE_TURN_ID, | 957 | turnId: FIXTURE_TURN_ID, |
| 990 | delta: "Turn after restart.\n", | 958 | delta: "Turn after restart.\n", |
| 991 | }, | 959 | }, |
| 992 | { | 960 | { |
| 993 | type: "turn.completed", | 961 | type: "turn.completed", |
| 994 | ...runtimeBase("evt-claude-recover-6", "2026-02-24T10:11:01.100Z", "claudeCode"), | 962 | ...runtimeBase("evt-claude-recover-6", "2026-02-24T10:11:01.100Z", "claudeCode"), |
| 995 | threadId: FIXTURE_THREAD_ID, | 963 | threadId: THREAD_ID, |
| 996 | turnId: FIXTURE_TURN_ID, | 964 | turnId: FIXTURE_TURN_ID, |
| 997 | status: "completed", | 965 | status: "completed", |
| 998 | }, | 966 | }, |
| ... | @@ -1021,7 +989,7 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res | ... | @@ -1021,7 +989,7 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res |
| 1021 | !entry.activities.some((activity) => activity.kind === "provider.turn.start.failed"), | 989 | !entry.activities.some((activity) => activity.kind === "provider.turn.start.failed"), |
| 1022 | ); | 990 | ); |
| 1023 | assert.equal(recoveredThread.session?.providerName, "claudeCode"); | 991 | assert.equal(recoveredThread.session?.providerName, "claudeCode"); |
| 1024 | assert.equal(recoveredThread.session?.providerSessionId, staleSessionId); | 992 | assert.equal(recoveredThread.session?.threadId, "thread-1"); |
| 1025 | }), | 993 | }), |
| 1026 | "claudeCode", | 994 | "claudeCode", |
| 1027 | ), | 995 | ), |
| ... | @@ -1038,13 +1006,13 @@ it.live("forwards claudeCode approval responses to the provider session", () => | ... | @@ -1038,13 +1006,13 @@ it.live("forwards claudeCode approval responses to the provider session", () => |
| 1038 | { | 1006 | { |
| 1039 | type: "turn.started", | 1007 | type: "turn.started", |
| 1040 | ...runtimeBase("evt-claude-approval-1", "2026-02-24T10:12:00.000Z", "claudeCode"), | 1008 | ...runtimeBase("evt-claude-approval-1", "2026-02-24T10:12:00.000Z", "claudeCode"), |
| 1041 | threadId: FIXTURE_THREAD_ID, | 1009 | threadId: THREAD_ID, |
| 1042 | turnId: FIXTURE_TURN_ID, | 1010 | turnId: FIXTURE_TURN_ID, |
| 1043 | }, | 1011 | }, |
| 1044 | { | 1012 | { |
| 1045 | type: "approval.requested", | 1013 | type: "approval.requested", |
| 1046 | ...runtimeBase("evt-claude-approval-2", "2026-02-24T10:12:00.050Z", "claudeCode"), | 1014 | ...runtimeBase("evt-claude-approval-2", "2026-02-24T10:12:00.050Z", "claudeCode"), |
| 1047 | threadId: FIXTURE_THREAD_ID, | 1015 | threadId: THREAD_ID, |
| 1048 | turnId: FIXTURE_TURN_ID, | 1016 | turnId: FIXTURE_TURN_ID, |
| 1049 | requestId: APPROVAL_REQUEST_ID, | 1017 | requestId: APPROVAL_REQUEST_ID, |
| 1050 | requestKind: "command", | 1018 | requestKind: "command", |
| ... | @@ -1053,7 +1021,7 @@ it.live("forwards claudeCode approval responses to the provider session", () => | ... | @@ -1053,7 +1021,7 @@ it.live("forwards claudeCode approval responses to the provider session", () => |
| 1053 | { | 1021 | { |
| 1054 | type: "turn.completed", | 1022 | type: "turn.completed", |
| 1055 | ...runtimeBase("evt-claude-approval-3", "2026-02-24T10:12:00.100Z", "claudeCode"), | 1023 | ...runtimeBase("evt-claude-approval-3", "2026-02-24T10:12:00.100Z", "claudeCode"), |
| 1056 | threadId: FIXTURE_THREAD_ID, | 1024 | threadId: THREAD_ID, |
| 1057 | turnId: FIXTURE_TURN_ID, | 1025 | turnId: FIXTURE_TURN_ID, |
| 1058 | status: "completed", | 1026 | status: "completed", |
| 1059 | }, | 1027 | }, |
| ... | @@ -1071,11 +1039,7 @@ it.live("forwards claudeCode approval responses to the provider session", () => | ... | @@ -1071,11 +1039,7 @@ it.live("forwards claudeCode approval responses to the provider session", () => |
| 1071 | const thread = yield* harness.waitForThread(THREAD_ID, (entry) => | 1039 | const thread = yield* harness.waitForThread(THREAD_ID, (entry) => |
| 1072 | entry.activities.some((activity) => activity.kind === "approval.requested"), | 1040 | entry.activities.some((activity) => activity.kind === "approval.requested"), |
| 1073 | ); | 1041 | ); |
| 1074 | const providerSessionId = thread.session?.providerSessionId; | 1042 | assert.equal(thread.session?.threadId, "thread-1"); |
| 1075 | assert.equal(providerSessionId !== null, true); | ||
| 1076 | if (!providerSessionId) { | ||
| 1077 | return; | ||
| 1078 | } | ||
| 1079 | 1043 | ||
| 1080 | yield* harness.engine.dispatch({ | 1044 | yield* harness.engine.dispatch({ |
| 1081 | type: "thread.approval.respond", | 1045 | type: "thread.approval.respond", |
| ... | @@ -1092,7 +1056,7 @@ it.live("forwards claudeCode approval responses to the provider session", () => | ... | @@ -1092,7 +1056,7 @@ it.live("forwards claudeCode approval responses to the provider session", () => |
| 1092 | ); | 1056 | ); |
| 1093 | 1057 | ||
| 1094 | const approvalResponses = yield* waitForSync( | 1058 | const approvalResponses = yield* waitForSync( |
| 1095 | () => harness.adapterHarness.getApprovalResponses(providerSessionId), | 1059 | () => harness.adapterHarness.getApprovalResponses(THREAD_ID), |
| 1096 | (responses) => responses.length === 1, | 1060 | (responses) => responses.length === 1, |
| 1097 | "claude provider approval response", | 1061 | "claude provider approval response", |
| 1098 | ); | 1062 | ); |
| ... | @@ -1113,20 +1077,20 @@ it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => | ... | @@ -1113,20 +1077,20 @@ it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => |
| 1113 | { | 1077 | { |
| 1114 | type: "turn.started", | 1078 | type: "turn.started", |
| 1115 | ...runtimeBase("evt-claude-interrupt-1", "2026-02-24T10:13:00.000Z", "claudeCode"), | 1079 | ...runtimeBase("evt-claude-interrupt-1", "2026-02-24T10:13:00.000Z", "claudeCode"), |
| 1116 | threadId: FIXTURE_THREAD_ID, | 1080 | threadId: THREAD_ID, |
| 1117 | turnId: FIXTURE_TURN_ID, | 1081 | turnId: FIXTURE_TURN_ID, |
| 1118 | }, | 1082 | }, |
| 1119 | { | 1083 | { |
| 1120 | type: "message.delta", | 1084 | type: "message.delta", |
| 1121 | ...runtimeBase("evt-claude-interrupt-2", "2026-02-24T10:13:00.050Z", "claudeCode"), | 1085 | ...runtimeBase("evt-claude-interrupt-2", "2026-02-24T10:13:00.050Z", "claudeCode"), |
| 1122 | threadId: FIXTURE_THREAD_ID, | 1086 | threadId: THREAD_ID, |
| 1123 | turnId: FIXTURE_TURN_ID, | 1087 | turnId: FIXTURE_TURN_ID, |
| 1124 | delta: "Long running output.\n", | 1088 | delta: "Long running output.\n", |
| 1125 | }, | 1089 | }, |
| 1126 | { | 1090 | { |
| 1127 | type: "turn.completed", | 1091 | type: "turn.completed", |
| 1128 | ...runtimeBase("evt-claude-interrupt-3", "2026-02-24T10:13:00.100Z", "claudeCode"), | 1092 | ...runtimeBase("evt-claude-interrupt-3", "2026-02-24T10:13:00.100Z", "claudeCode"), |
| 1129 | threadId: FIXTURE_THREAD_ID, | 1093 | threadId: THREAD_ID, |
| 1130 | turnId: FIXTURE_TURN_ID, | 1094 | turnId: FIXTURE_TURN_ID, |
| 1131 | status: "completed", | 1095 | status: "completed", |
| 1132 | }, | 1096 | }, |
| ... | @@ -1143,13 +1107,9 @@ it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => | ... | @@ -1143,13 +1107,9 @@ it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => |
| 1143 | 1107 | ||
| 1144 | const thread = yield* harness.waitForThread( | 1108 | const thread = yield* harness.waitForThread( |
| 1145 | THREAD_ID, | 1109 | THREAD_ID, |
| 1146 | (entry) => entry.session?.providerSessionId !== null, | 1110 | (entry) => entry.session?.threadId === "thread-1", |
| 1147 | ); | 1111 | ); |
| 1148 | const providerSessionId = thread.session?.providerSessionId; | 1112 | assert.equal(thread.session?.threadId, "thread-1"); |
| 1149 | assert.equal(providerSessionId !== null, true); | ||
| 1150 | if (!providerSessionId) { | ||
| 1151 | return; | ||
| 1152 | } | ||
| 1153 | 1113 | ||
| 1154 | yield* harness.engine.dispatch({ | 1114 | yield* harness.engine.dispatch({ |
| 1155 | type: "thread.turn.interrupt", | 1115 | type: "thread.turn.interrupt", |
| ... | @@ -1160,7 +1120,7 @@ it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => | ... | @@ -1160,7 +1120,7 @@ it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => |
| 1160 | yield* harness.waitForDomainEvent((event) => event.type === "thread.turn-interrupt-requested"); | 1120 | yield* harness.waitForDomainEvent((event) => event.type === "thread.turn-interrupt-requested"); |
| 1161 | 1121 | ||
| 1162 | const interruptCalls = yield* waitForSync( | 1122 | const interruptCalls = yield* waitForSync( |
| 1163 | () => harness.adapterHarness.getInterruptCalls(providerSessionId), | 1123 | () => harness.adapterHarness.getInterruptCalls(THREAD_ID), |
| 1164 | (calls) => calls.length === 1, | 1124 | (calls) => calls.length === 1, |
| 1165 | "claude provider interrupt call", | 1125 | "claude provider interrupt call", |
| 1166 | ); | 1126 | ); |
| ... | @@ -1181,20 +1141,20 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( | ... | @@ -1181,20 +1141,20 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( |
| 1181 | { | 1141 | { |
| 1182 | type: "turn.started", | 1142 | type: "turn.started", |
| 1183 | ...runtimeBase("evt-claude-revert-1", "2026-02-24T10:14:00.000Z", "claudeCode"), | 1143 | ...runtimeBase("evt-claude-revert-1", "2026-02-24T10:14:00.000Z", "claudeCode"), |
| 1184 | threadId: FIXTURE_THREAD_ID, | 1144 | threadId: THREAD_ID, |
| 1185 | turnId: FIXTURE_TURN_ID, | 1145 | turnId: FIXTURE_TURN_ID, |
| 1186 | }, | 1146 | }, |
| 1187 | { | 1147 | { |
| 1188 | type: "message.delta", | 1148 | type: "message.delta", |
| 1189 | ...runtimeBase("evt-claude-revert-2", "2026-02-24T10:14:00.050Z", "claudeCode"), | 1149 | ...runtimeBase("evt-claude-revert-2", "2026-02-24T10:14:00.050Z", "claudeCode"), |
| 1190 | threadId: FIXTURE_THREAD_ID, | 1150 | threadId: THREAD_ID, |
| 1191 | turnId: FIXTURE_TURN_ID, | 1151 | turnId: FIXTURE_TURN_ID, |
| 1192 | delta: "README -> v2\n", | 1152 | delta: "README -> v2\n", |
| 1193 | }, | 1153 | }, |
| 1194 | { | 1154 | { |
| 1195 | type: "turn.completed", | 1155 | type: "turn.completed", |
| 1196 | ...runtimeBase("evt-claude-revert-3", "2026-02-24T10:14:00.100Z", "claudeCode"), | 1156 | ...runtimeBase("evt-claude-revert-3", "2026-02-24T10:14:00.100Z", "claudeCode"), |
| 1197 | threadId: FIXTURE_THREAD_ID, | 1157 | threadId: THREAD_ID, |
| 1198 | turnId: FIXTURE_TURN_ID, | 1158 | turnId: FIXTURE_TURN_ID, |
| 1199 | status: "completed", | 1159 | status: "completed", |
| 1200 | }, | 1160 | }, |
| ... | @@ -1213,35 +1173,30 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( | ... | @@ -1213,35 +1173,30 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( |
| 1213 | provider: "claudeCode", | 1173 | provider: "claudeCode", |
| 1214 | }); | 1174 | }); |
| 1215 | 1175 | ||
| 1216 | const firstThread = yield* harness.waitForThread( | 1176 | yield* harness.waitForThread( |
| 1217 | THREAD_ID, | 1177 | THREAD_ID, |
| 1218 | (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.providerSessionId !== null, | 1178 | (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", |
| 1219 | ); | 1179 | ); |
| 1220 | const sessionId = firstThread.session?.providerSessionId; | ||
| 1221 | assert.equal(sessionId !== null, true); | ||
| 1222 | if (!sessionId) { | ||
| 1223 | return; | ||
| 1224 | } | ||
| 1225 | 1180 | ||
| 1226 | yield* harness.adapterHarness.queueTurnResponse(sessionId, { | 1181 | yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, { |
| 1227 | events: [ | 1182 | events: [ |
| 1228 | { | 1183 | { |
| 1229 | type: "turn.started", | 1184 | type: "turn.started", |
| 1230 | ...runtimeBase("evt-claude-revert-4", "2026-02-24T10:14:01.000Z", "claudeCode"), | 1185 | ...runtimeBase("evt-claude-revert-4", "2026-02-24T10:14:01.000Z", "claudeCode"), |
| 1231 | threadId: FIXTURE_THREAD_ID, | 1186 | threadId: THREAD_ID, |
| 1232 | turnId: FIXTURE_TURN_ID, | 1187 | turnId: FIXTURE_TURN_ID, |
| 1233 | }, | 1188 | }, |
| 1234 | { | 1189 | { |
| 1235 | type: "message.delta", | 1190 | type: "message.delta", |
| 1236 | ...runtimeBase("evt-claude-revert-5", "2026-02-24T10:14:01.050Z", "claudeCode"), | 1191 | ...runtimeBase("evt-claude-revert-5", "2026-02-24T10:14:01.050Z", "claudeCode"), |
| 1237 | threadId: FIXTURE_THREAD_ID, | 1192 | threadId: THREAD_ID, |
| 1238 | turnId: FIXTURE_TURN_ID, | 1193 | turnId: FIXTURE_TURN_ID, |
| 1239 | delta: "README -> v3\n", | 1194 | delta: "README -> v3\n", |
| 1240 | }, | 1195 | }, |
| 1241 | { | 1196 | { |
| 1242 | type: "turn.completed", | 1197 | type: "turn.completed", |
| 1243 | ...runtimeBase("evt-claude-revert-6", "2026-02-24T10:14:01.100Z", "claudeCode"), | 1198 | ...runtimeBase("evt-claude-revert-6", "2026-02-24T10:14:01.100Z", "claudeCode"), |
| 1244 | threadId: FIXTURE_THREAD_ID, | 1199 | threadId: THREAD_ID, |
| 1245 | turnId: FIXTURE_TURN_ID, | 1200 | turnId: FIXTURE_TURN_ID, |
| 1246 | status: "completed", | 1201 | status: "completed", |
| 1247 | }, | 1202 | }, |
| ... | @@ -1289,7 +1244,7 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( | ... | @@ -1289,7 +1244,7 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( |
| 1289 | gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)), | 1244 | gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)), |
| 1290 | false, | 1245 | false, |
| 1291 | ); | 1246 | ); |
| 1292 | assert.deepEqual(harness.adapterHarness.getRollbackCalls(sessionId), [1]); | 1247 | assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]); |
| 1293 | }), | 1248 | }), |
| 1294 | "claudeCode", | 1249 | "claudeCode", |
| 1295 | ), | 1250 | ), |
apps/server/integration/providerService.integration.test.ts+16-12| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import type { ProviderRuntimeEvent, ProviderSessionId } from "@t3tools/contracts"; | 1 | import type { ProviderRuntimeEvent } from "@t3tools/contracts"; |
| 2 | import { ThreadId } from "@t3tools/contracts"; | 2 | import { ThreadId } from "@t3tools/contracts"; |
| 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | import { it, assert } from "@effect/vitest"; | 4 | import { it, assert } from "@effect/vitest"; |
| ... | @@ -93,18 +93,18 @@ const collectEventsDuring = <A, E, R>( | ... | @@ -93,18 +93,18 @@ const collectEventsDuring = <A, E, R>( |
| 93 | const runTurn = (input: { | 93 | const runTurn = (input: { |
| 94 | readonly provider: ProviderServiceShape; | 94 | readonly provider: ProviderServiceShape; |
| 95 | readonly harness: TestProviderAdapterHarness; | 95 | readonly harness: TestProviderAdapterHarness; |
| 96 | readonly sessionId: ProviderSessionId; | 96 | readonly threadId: ThreadId; |
| 97 | readonly userText: string; | 97 | readonly userText: string; |
| 98 | readonly response: TestTurnResponse; | 98 | readonly response: TestTurnResponse; |
| 99 | }) => | 99 | }) => |
| 100 | Effect.gen(function* () { | 100 | Effect.gen(function* () { |
| 101 | yield* input.harness.queueTurnResponse(input.sessionId, input.response); | 101 | yield* input.harness.queueTurnResponse(input.threadId, input.response); |
| 102 | 102 | ||
| 103 | return yield* collectEventsDuring( | 103 | return yield* collectEventsDuring( |
| 104 | input.provider.streamEvents, | 104 | input.provider.streamEvents, |
| 105 | input.response.events.length, | 105 | input.response.events.length, |
| 106 | input.provider.sendTurn({ | 106 | input.provider.sendTurn({ |
| 107 | sessionId: input.sessionId, | 107 | threadId: input.threadId, |
| 108 | input: input.userText, | 108 | input: input.userText, |
| 109 | attachments: [], | 109 | attachments: [], |
| 110 | }), | 110 | }), |
| ... | @@ -120,6 +120,7 @@ it.effect("replays typed runtime fixture events", () => | ... | @@ -120,6 +120,7 @@ it.effect("replays typed runtime fixture events", () => |
| 120 | const session = yield* provider.startSession( | 120 | const session = yield* provider.startSession( |
| 121 | ThreadId.makeUnsafe("thread-integration-typed"), | 121 | ThreadId.makeUnsafe("thread-integration-typed"), |
| 122 | { | 122 | { |
| 123 | threadId: ThreadId.makeUnsafe("thread-integration-typed"), | ||
| 123 | provider: "codex", | 124 | provider: "codex", |
| 124 | cwd: fixture.cwd, | 125 | cwd: fixture.cwd, |
| 125 | runtimeMode: "full-access", | 126 | runtimeMode: "full-access", |
| ... | @@ -130,7 +131,7 @@ it.effect("replays typed runtime fixture events", () => | ... | @@ -130,7 +131,7 @@ it.effect("replays typed runtime fixture events", () => |
| 130 | const observedEvents = yield* runTurn({ | 131 | const observedEvents = yield* runTurn({ |
| 131 | provider, | 132 | provider, |
| 132 | harness: fixture.harness, | 133 | harness: fixture.harness, |
| 133 | sessionId: session.sessionId, | 134 | threadId: session.threadId, |
| 134 | userText: "hello", | 135 | userText: "hello", |
| 135 | response: { events: codexTurnTextFixture }, | 136 | response: { events: codexTurnTextFixture }, |
| 136 | }); | 137 | }); |
| ... | @@ -154,6 +155,7 @@ it.effect("replays file-changing fixture turn events", () => | ... | @@ -154,6 +155,7 @@ it.effect("replays file-changing fixture turn events", () => |
| 154 | const session = yield* provider.startSession( | 155 | const session = yield* provider.startSession( |
| 155 | ThreadId.makeUnsafe("thread-integration-tools"), | 156 | ThreadId.makeUnsafe("thread-integration-tools"), |
| 156 | { | 157 | { |
| 158 | threadId: ThreadId.makeUnsafe("thread-integration-tools"), | ||
| 157 | provider: "codex", | 159 | provider: "codex", |
| 158 | cwd: fixture.cwd, | 160 | cwd: fixture.cwd, |
| 159 | runtimeMode: "full-access", | 161 | runtimeMode: "full-access", |
| ... | @@ -164,7 +166,7 @@ it.effect("replays file-changing fixture turn events", () => | ... | @@ -164,7 +166,7 @@ it.effect("replays file-changing fixture turn events", () => |
| 164 | const observedEvents = yield* runTurn({ | 166 | const observedEvents = yield* runTurn({ |
| 165 | provider, | 167 | provider, |
| 166 | harness: fixture.harness, | 168 | harness: fixture.harness, |
| 167 | sessionId: session.sessionId, | 169 | threadId: session.threadId, |
| 168 | userText: "make a small change", | 170 | userText: "make a small change", |
| 169 | response: { | 171 | response: { |
| 170 | events: codexTurnToolFixture, | 172 | events: codexTurnToolFixture, |
| ... | @@ -192,6 +194,7 @@ it.effect("runs multi-turn tool/approval flow", () => | ... | @@ -192,6 +194,7 @@ it.effect("runs multi-turn tool/approval flow", () => |
| 192 | const session = yield* provider.startSession( | 194 | const session = yield* provider.startSession( |
| 193 | ThreadId.makeUnsafe("thread-integration-multi"), | 195 | ThreadId.makeUnsafe("thread-integration-multi"), |
| 194 | { | 196 | { |
| 197 | threadId: ThreadId.makeUnsafe("thread-integration-multi"), | ||
| 195 | provider: "codex", | 198 | provider: "codex", |
| 196 | cwd: fixture.cwd, | 199 | cwd: fixture.cwd, |
| 197 | runtimeMode: "full-access", | 200 | runtimeMode: "full-access", |
| ... | @@ -202,7 +205,7 @@ it.effect("runs multi-turn tool/approval flow", () => | ... | @@ -202,7 +205,7 @@ it.effect("runs multi-turn tool/approval flow", () => |
| 202 | const firstTurnEvents = yield* runTurn({ | 205 | const firstTurnEvents = yield* runTurn({ |
| 203 | provider, | 206 | provider, |
| 204 | harness: fixture.harness, | 207 | harness: fixture.harness, |
| 205 | sessionId: session.sessionId, | 208 | threadId: session.threadId, |
| 206 | userText: "turn 1", | 209 | userText: "turn 1", |
| 207 | response: { | 210 | response: { |
| 208 | events: codexTurnToolFixture, | 211 | events: codexTurnToolFixture, |
| ... | @@ -218,7 +221,7 @@ it.effect("runs multi-turn tool/approval flow", () => | ... | @@ -218,7 +221,7 @@ it.effect("runs multi-turn tool/approval flow", () => |
| 218 | const secondTurnEvents = yield* runTurn({ | 221 | const secondTurnEvents = yield* runTurn({ |
| 219 | provider, | 222 | provider, |
| 220 | harness: fixture.harness, | 223 | harness: fixture.harness, |
| 221 | sessionId: session.sessionId, | 224 | threadId: session.threadId, |
| 222 | userText: "turn 2 approval", | 225 | userText: "turn 2 approval", |
| 223 | response: { | 226 | response: { |
| 224 | events: codexTurnApprovalFixture, | 227 | events: codexTurnApprovalFixture, |
| ... | @@ -245,6 +248,7 @@ it.effect("rolls back provider conversation state only", () => | ... | @@ -245,6 +248,7 @@ it.effect("rolls back provider conversation state only", () => |
| 245 | const session = yield* provider.startSession( | 248 | const session = yield* provider.startSession( |
| 246 | ThreadId.makeUnsafe("thread-integration-rollback"), | 249 | ThreadId.makeUnsafe("thread-integration-rollback"), |
| 247 | { | 250 | { |
| 251 | threadId: ThreadId.makeUnsafe("thread-integration-rollback"), | ||
| 248 | provider: "codex", | 252 | provider: "codex", |
| 249 | cwd: fixture.cwd, | 253 | cwd: fixture.cwd, |
| 250 | runtimeMode: "full-access", | 254 | runtimeMode: "full-access", |
| ... | @@ -255,7 +259,7 @@ it.effect("rolls back provider conversation state only", () => | ... | @@ -255,7 +259,7 @@ it.effect("rolls back provider conversation state only", () => |
| 255 | yield* runTurn({ | 259 | yield* runTurn({ |
| 256 | provider, | 260 | provider, |
| 257 | harness: fixture.harness, | 261 | harness: fixture.harness, |
| 258 | sessionId: session.sessionId, | 262 | threadId: session.threadId, |
| 259 | userText: "turn 1", | 263 | userText: "turn 1", |
| 260 | response: { | 264 | response: { |
| 261 | events: codexTurnToolFixture, | 265 | events: codexTurnToolFixture, |
| ... | @@ -267,7 +271,7 @@ it.effect("rolls back provider conversation state only", () => | ... | @@ -267,7 +271,7 @@ it.effect("rolls back provider conversation state only", () => |
| 267 | yield* runTurn({ | 271 | yield* runTurn({ |
| 268 | provider, | 272 | provider, |
| 269 | harness: fixture.harness, | 273 | harness: fixture.harness, |
| 270 | sessionId: session.sessionId, | 274 | threadId: session.threadId, |
| 271 | userText: "turn 2 approval", | 275 | userText: "turn 2 approval", |
| 272 | response: { | 276 | response: { |
| 273 | events: codexTurnApprovalFixture, | 277 | events: codexTurnApprovalFixture, |
| ... | @@ -277,11 +281,11 @@ it.effect("rolls back provider conversation state only", () => | ... | @@ -277,11 +281,11 @@ it.effect("rolls back provider conversation state only", () => |
| 277 | }); | 281 | }); |
| 278 | 282 | ||
| 279 | yield* provider.rollbackConversation({ | 283 | yield* provider.rollbackConversation({ |
| 280 | sessionId: session.sessionId, | 284 | threadId: session.threadId, |
| 281 | numTurns: 1, | 285 | numTurns: 1, |
| 282 | }); | 286 | }); |
| 283 | 287 | ||
| 284 | const rollbackCalls = fixture.harness.getRollbackCalls(session.sessionId); | 288 | const rollbackCalls = fixture.harness.getRollbackCalls(session.threadId); |
| 285 | assert.deepEqual(rollbackCalls, [1]); | 289 | assert.deepEqual(rollbackCalls, [1]); |
| 286 | 290 | ||
| 287 | const readme = yield* readFileString(join(fixture.cwd, "README.md")); | 291 | const readme = yield* readFileString(join(fixture.cwd, "README.md")); |
apps/server/src/codexAppServerManager.test.ts+20-17| ... | @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto"; | ... | @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto"; |
| 3 | import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; | 3 | import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; |
| 4 | import os from "node:os"; | 4 | import os from "node:os"; |
| 5 | import path from "node:path"; | 5 | import path from "node:path"; |
| 6 | import { ProviderSessionId } from "@t3tools/contracts"; | 6 | import { ThreadId } from "@t3tools/contracts"; |
| 7 | 7 | ||
| 8 | import { | 8 | import { |
| 9 | CodexAppServerManager, | 9 | CodexAppServerManager, |
| ... | @@ -12,7 +12,7 @@ import { | ... | @@ -12,7 +12,7 @@ import { |
| 12 | normalizeCodexModelSlug, | 12 | normalizeCodexModelSlug, |
| 13 | } from "./codexAppServerManager"; | 13 | } from "./codexAppServerManager"; |
| 14 | 14 | ||
| 15 | const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value); | 15 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); |
| 16 | 16 | ||
| 17 | function createSendTurnHarness() { | 17 | function createSendTurnHarness() { |
| 18 | const manager = new CodexAppServerManager(); | 18 | const manager = new CodexAppServerManager(); |
| ... | @@ -168,6 +168,7 @@ describe("startSession", () => { | ... | @@ -168,6 +168,7 @@ describe("startSession", () => { |
| 168 | try { | 168 | try { |
| 169 | await expect( | 169 | await expect( |
| 170 | manager.startSession({ | 170 | manager.startSession({ |
| 171 | threadId: asThreadId("thread-1"), | ||
| 171 | provider: "codex", | 172 | provider: "codex", |
| 172 | runtimeMode: "full-access", | 173 | runtimeMode: "full-access", |
| 173 | }), | 174 | }), |
| ... | @@ -191,7 +192,7 @@ describe("sendTurn", () => { | ... | @@ -191,7 +192,7 @@ describe("sendTurn", () => { |
| 191 | createSendTurnHarness(); | 192 | createSendTurnHarness(); |
| 192 | 193 | ||
| 193 | const result = await manager.sendTurn({ | 194 | const result = await manager.sendTurn({ |
| 194 | sessionId: asSessionId("sess_1"), | 195 | threadId: asThreadId("thread_1"), |
| 195 | input: "Inspect this image", | 196 | input: "Inspect this image", |
| 196 | attachments: [ | 197 | attachments: [ |
| 197 | { | 198 | { |
| ... | @@ -207,7 +208,7 @@ describe("sendTurn", () => { | ... | @@ -207,7 +208,7 @@ describe("sendTurn", () => { |
| 207 | threadId: "thread_1", | 208 | threadId: "thread_1", |
| 208 | turnId: "turn_1", | 209 | turnId: "turn_1", |
| 209 | }); | 210 | }); |
| 210 | expect(requireSession).toHaveBeenCalledWith("sess_1"); | 211 | expect(requireSession).toHaveBeenCalledWith("thread_1"); |
| 211 | expect(sendRequest).toHaveBeenCalledWith(context, "turn/start", { | 212 | expect(sendRequest).toHaveBeenCalledWith(context, "turn/start", { |
| 212 | threadId: "thread_1", | 213 | threadId: "thread_1", |
| 213 | input: [ | 214 | input: [ |
| ... | @@ -234,7 +235,7 @@ describe("sendTurn", () => { | ... | @@ -234,7 +235,7 @@ describe("sendTurn", () => { |
| 234 | const { manager, context, sendRequest } = createSendTurnHarness(); | 235 | const { manager, context, sendRequest } = createSendTurnHarness(); |
| 235 | 236 | ||
| 236 | await manager.sendTurn({ | 237 | await manager.sendTurn({ |
| 237 | sessionId: asSessionId("sess_1"), | 238 | threadId: asThreadId("thread_1"), |
| 238 | attachments: [ | 239 | attachments: [ |
| 239 | { | 240 | { |
| 240 | type: "image", | 241 | type: "image", |
| ... | @@ -259,7 +260,7 @@ describe("sendTurn", () => { | ... | @@ -259,7 +260,7 @@ describe("sendTurn", () => { |
| 259 | 260 | ||
| 260 | await expect( | 261 | await expect( |
| 261 | manager.sendTurn({ | 262 | manager.sendTurn({ |
| 262 | sessionId: asSessionId("sess_1"), | 263 | threadId: asThreadId("thread_1"), |
| 263 | }), | 264 | }), |
| 264 | ).rejects.toThrow("Turn input must include text or attachments."); | 265 | ).rejects.toThrow("Turn input must include text or attachments."); |
| 265 | }); | 266 | }); |
| ... | @@ -280,9 +281,9 @@ describe("thread checkpoint control", () => { | ... | @@ -280,9 +281,9 @@ describe("thread checkpoint control", () => { |
| 280 | }, | 281 | }, |
| 281 | }); | 282 | }); |
| 282 | 283 | ||
| 283 | const result = await manager.readThread(asSessionId("sess_1")); | 284 | const result = await manager.readThread(asThreadId("thread_1")); |
| 284 | 285 | ||
| 285 | expect(requireSession).toHaveBeenCalledWith("sess_1"); | 286 | expect(requireSession).toHaveBeenCalledWith("thread_1"); |
| 286 | expect(sendRequest).toHaveBeenCalledWith(context, "thread/read", { | 287 | expect(sendRequest).toHaveBeenCalledWith(context, "thread/read", { |
| 287 | threadId: "thread_1", | 288 | threadId: "thread_1", |
| 288 | includeTurns: true, | 289 | includeTurns: true, |
| ... | @@ -310,7 +311,7 @@ describe("thread checkpoint control", () => { | ... | @@ -310,7 +311,7 @@ describe("thread checkpoint control", () => { |
| 310 | ], | 311 | ], |
| 311 | }); | 312 | }); |
| 312 | 313 | ||
| 313 | const result = await manager.readThread(asSessionId("sess_1")); | 314 | const result = await manager.readThread(asThreadId("thread_1")); |
| 314 | 315 | ||
| 315 | expect(sendRequest).toHaveBeenCalledWith(context, "thread/read", { | 316 | expect(sendRequest).toHaveBeenCalledWith(context, "thread/read", { |
| 316 | threadId: "thread_1", | 317 | threadId: "thread_1", |
| ... | @@ -336,7 +337,7 @@ describe("thread checkpoint control", () => { | ... | @@ -336,7 +337,7 @@ describe("thread checkpoint control", () => { |
| 336 | }, | 337 | }, |
| 337 | }); | 338 | }); |
| 338 | 339 | ||
| 339 | const result = await manager.rollbackThread(asSessionId("sess_1"), 2); | 340 | const result = await manager.rollbackThread(asThreadId("thread_1"), 2); |
| 340 | 341 | ||
| 341 | expect(sendRequest).toHaveBeenCalledWith(context, "thread/rollback", { | 342 | expect(sendRequest).toHaveBeenCalledWith(context, "thread/rollback", { |
| 342 | threadId: "thread_1", | 343 | threadId: "thread_1", |
| ... | @@ -364,6 +365,7 @@ describe.skipIf(!process.env.CODEX_BINARY_PATH)("startSession live Codex resume" | ... | @@ -364,6 +365,7 @@ describe.skipIf(!process.env.CODEX_BINARY_PATH)("startSession live Codex resume" |
| 364 | 365 | ||
| 365 | try { | 366 | try { |
| 366 | const firstSession = await manager.startSession({ | 367 | const firstSession = await manager.startSession({ |
| 368 | threadId: asThreadId("thread-live"), | ||
| 367 | provider: "codex", | 369 | provider: "codex", |
| 368 | cwd: workspaceDir, | 370 | cwd: workspaceDir, |
| 369 | runtimeMode: "full-access", | 371 | runtimeMode: "full-access", |
| ... | @@ -378,24 +380,25 @@ describe.skipIf(!process.env.CODEX_BINARY_PATH)("startSession live Codex resume" | ... | @@ -378,24 +380,25 @@ describe.skipIf(!process.env.CODEX_BINARY_PATH)("startSession live Codex resume" |
| 378 | }); | 380 | }); |
| 379 | 381 | ||
| 380 | const firstTurn = await manager.sendTurn({ | 382 | const firstTurn = await manager.sendTurn({ |
| 381 | sessionId: firstSession.sessionId, | 383 | threadId: firstSession.threadId, |
| 382 | input: `Reply with exactly the word ALPHA ${randomUUID()}`, | 384 | input: `Reply with exactly the word ALPHA ${randomUUID()}`, |
| 383 | }); | 385 | }); |
| 384 | 386 | ||
| 385 | expect(firstTurn.threadId).toBe(firstSession.threadId); | 387 | expect(firstTurn.threadId).toBe(firstSession.threadId); |
| 386 | 388 | ||
| 387 | await vi.waitFor(async () => { | 389 | await vi.waitFor(async () => { |
| 388 | const snapshot = await manager.readThread(firstSession.sessionId); | 390 | const snapshot = await manager.readThread(firstSession.threadId); |
| 389 | expect(snapshot.turns.length).toBeGreaterThan(0); | 391 | expect(snapshot.turns.length).toBeGreaterThan(0); |
| 390 | }, { timeout: 120_000, interval: 1_000 }); | 392 | }, { timeout: 120_000, interval: 1_000 }); |
| 391 | 393 | ||
| 392 | const firstSnapshot = await manager.readThread(firstSession.sessionId); | 394 | const firstSnapshot = await manager.readThread(firstSession.threadId); |
| 393 | const originalThreadId = firstSnapshot.threadId; | 395 | const originalThreadId = firstSnapshot.threadId; |
| 394 | const originalTurnCount = firstSnapshot.turns.length; | 396 | const originalTurnCount = firstSnapshot.turns.length; |
| 395 | 397 | ||
| 396 | manager.stopSession(firstSession.sessionId); | 398 | manager.stopSession(firstSession.threadId); |
| 397 | 399 | ||
| 398 | const resumedSession = await manager.startSession({ | 400 | const resumedSession = await manager.startSession({ |
| 401 | threadId: firstSession.threadId, | ||
| 399 | provider: "codex", | 402 | provider: "codex", |
| 400 | cwd: workspaceDir, | 403 | cwd: workspaceDir, |
| 401 | runtimeMode: "approval-required", | 404 | runtimeMode: "approval-required", |
| ... | @@ -412,17 +415,17 @@ describe.skipIf(!process.env.CODEX_BINARY_PATH)("startSession live Codex resume" | ... | @@ -412,17 +415,17 @@ describe.skipIf(!process.env.CODEX_BINARY_PATH)("startSession live Codex resume" |
| 412 | 415 | ||
| 413 | expect(resumedSession.threadId).toBe(originalThreadId); | 416 | expect(resumedSession.threadId).toBe(originalThreadId); |
| 414 | 417 | ||
| 415 | const resumedSnapshotBeforeTurn = await manager.readThread(resumedSession.sessionId); | 418 | const resumedSnapshotBeforeTurn = await manager.readThread(resumedSession.threadId); |
| 416 | expect(resumedSnapshotBeforeTurn.threadId).toBe(originalThreadId); | 419 | expect(resumedSnapshotBeforeTurn.threadId).toBe(originalThreadId); |
| 417 | expect(resumedSnapshotBeforeTurn.turns.length).toBeGreaterThanOrEqual(originalTurnCount); | 420 | expect(resumedSnapshotBeforeTurn.turns.length).toBeGreaterThanOrEqual(originalTurnCount); |
| 418 | 421 | ||
| 419 | await manager.sendTurn({ | 422 | await manager.sendTurn({ |
| 420 | sessionId: resumedSession.sessionId, | 423 | threadId: resumedSession.threadId, |
| 421 | input: `Reply with exactly the word BETA ${randomUUID()}`, | 424 | input: `Reply with exactly the word BETA ${randomUUID()}`, |
| 422 | }); | 425 | }); |
| 423 | 426 | ||
| 424 | await vi.waitFor(async () => { | 427 | await vi.waitFor(async () => { |
| 425 | const snapshot = await manager.readThread(resumedSession.sessionId); | 428 | const snapshot = await manager.readThread(resumedSession.threadId); |
| 426 | expect(snapshot.turns.length).toBeGreaterThan(originalTurnCount); | 429 | expect(snapshot.turns.length).toBeGreaterThan(originalTurnCount); |
| 427 | }, { timeout: 120_000, interval: 1_000 }); | 430 | }, { timeout: 120_000, interval: 1_000 }); |
| 428 | } finally { | 431 | } finally { |
apps/server/src/codexAppServerManager.ts+118-108| ... | @@ -8,9 +8,8 @@ import { | ... | @@ -8,9 +8,8 @@ import { |
| 8 | EventId, | 8 | EventId, |
| 9 | ProviderItemId, | 9 | ProviderItemId, |
| 10 | ProviderRequestKind, | 10 | ProviderRequestKind, |
| 11 | ProviderSessionId, | 11 | ThreadId, |
| 12 | ProviderThreadId, | 12 | TurnId, |
| 13 | ProviderTurnId, | ||
| 14 | normalizeModelSlug, | 13 | normalizeModelSlug, |
| 15 | type ProviderApprovalDecision, | 14 | type ProviderApprovalDecision, |
| 16 | type ProviderEvent, | 15 | type ProviderEvent, |
| ... | @@ -18,7 +17,7 @@ import { | ... | @@ -18,7 +17,7 @@ import { |
| 18 | type ProviderSessionStartInput, | 17 | type ProviderSessionStartInput, |
| 19 | type ProviderTurnStartResult, | 18 | type ProviderTurnStartResult, |
| 20 | } from "@t3tools/contracts"; | 19 | } from "@t3tools/contracts"; |
| 21 | import { Effect } from "effect"; | 20 | import { Effect, ServiceMap } from "effect"; |
| 22 | 21 | ||
| 23 | type PendingRequestKey = string; | 22 | type PendingRequestKey = string; |
| 24 | 23 | ||
| ... | @@ -34,8 +33,8 @@ interface PendingApprovalRequest { | ... | @@ -34,8 +33,8 @@ interface PendingApprovalRequest { |
| 34 | jsonRpcId: string | number; | 33 | jsonRpcId: string | number; |
| 35 | method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval"; | 34 | method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval"; |
| 36 | requestKind: ProviderRequestKind; | 35 | requestKind: ProviderRequestKind; |
| 37 | threadId?: ProviderThreadId; | 36 | threadId: ThreadId; |
| 38 | turnId?: ProviderTurnId; | 37 | turnId?: TurnId; |
| 39 | itemId?: ProviderItemId; | 38 | itemId?: ProviderItemId; |
| 40 | } | 39 | } |
| 41 | 40 | ||
| ... | @@ -72,7 +71,7 @@ interface JsonRpcNotification { | ... | @@ -72,7 +71,7 @@ interface JsonRpcNotification { |
| 72 | } | 71 | } |
| 73 | 72 | ||
| 74 | export interface CodexAppServerSendTurnInput { | 73 | export interface CodexAppServerSendTurnInput { |
| 75 | readonly sessionId: ProviderSessionId; | 74 | readonly threadId: ThreadId; |
| 76 | readonly input?: string; | 75 | readonly input?: string; |
| 77 | readonly attachments?: ReadonlyArray<{ type: "image"; url: string }>; | 76 | readonly attachments?: ReadonlyArray<{ type: "image"; url: string }>; |
| 78 | readonly model?: string; | 77 | readonly model?: string; |
| ... | @@ -80,12 +79,12 @@ export interface CodexAppServerSendTurnInput { | ... | @@ -80,12 +79,12 @@ export interface CodexAppServerSendTurnInput { |
| 80 | } | 79 | } |
| 81 | 80 | ||
| 82 | export interface CodexThreadTurnSnapshot { | 81 | export interface CodexThreadTurnSnapshot { |
| 83 | id: ProviderTurnId; | 82 | id: TurnId; |
| 84 | items: unknown[]; | 83 | items: unknown[]; |
| 85 | } | 84 | } |
| 86 | 85 | ||
| 87 | export interface CodexThreadSnapshot { | 86 | export interface CodexThreadSnapshot { |
| 88 | threadId: ProviderThreadId; | 87 | threadId: string; |
| 89 | turns: CodexThreadTurnSnapshot[]; | 88 | turns: CodexThreadTurnSnapshot[]; |
| 90 | } | 89 | } |
| 91 | 90 | ||
| ... | @@ -191,10 +190,16 @@ export interface CodexAppServerManagerEvents { | ... | @@ -191,10 +190,16 @@ export interface CodexAppServerManagerEvents { |
| 191 | } | 190 | } |
| 192 | 191 | ||
| 193 | export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEvents> { | 192 | export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEvents> { |
| 194 | private readonly sessions = new Map<ProviderSessionId, CodexSessionContext>(); | 193 | private readonly sessions = new Map<ThreadId, CodexSessionContext>(); |
| 194 | |||
| 195 | private runPromise: (effect: Effect.Effect<unknown, never>) => Promise<unknown>; | ||
| 196 | constructor(services?: ServiceMap.ServiceMap<never>) { | ||
| 197 | super(); | ||
| 198 | this.runPromise = services ? Effect.runPromiseWith(services) : Effect.runPromise; | ||
| 199 | } | ||
| 195 | 200 | ||
| 196 | async startSession(input: ProviderSessionStartInput): Promise<ProviderSession> { | 201 | async startSession(input: ProviderSessionStartInput): Promise<ProviderSession> { |
| 197 | const sessionId = ProviderSessionId.makeUnsafe(randomUUID()); | 202 | const threadId = input.threadId; |
| 198 | const now = new Date().toISOString(); | 203 | const now = new Date().toISOString(); |
| 199 | let context: CodexSessionContext | undefined; | 204 | let context: CodexSessionContext | undefined; |
| 200 | 205 | ||
| ... | @@ -202,12 +207,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -202,12 +207,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 202 | const resolvedCwd = input.cwd ?? process.cwd(); | 207 | const resolvedCwd = input.cwd ?? process.cwd(); |
| 203 | 208 | ||
| 204 | const session: ProviderSession = { | 209 | const session: ProviderSession = { |
| 205 | sessionId, | ||
| 206 | provider: "codex", | 210 | provider: "codex", |
| 207 | status: "connecting", | 211 | status: "connecting", |
| 208 | runtimeMode: input.runtimeMode, | 212 | runtimeMode: input.runtimeMode, |
| 209 | model: normalizeCodexModelSlug(input.model), | 213 | model: normalizeCodexModelSlug(input.model), |
| 210 | cwd: resolvedCwd, | 214 | cwd: resolvedCwd, |
| 215 | threadId, | ||
| 211 | createdAt: now, | 216 | createdAt: now, |
| 212 | updatedAt: now, | 217 | updatedAt: now, |
| 213 | }; | 218 | }; |
| ... | @@ -236,7 +241,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -236,7 +241,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 236 | stopping: false, | 241 | stopping: false, |
| 237 | }; | 242 | }; |
| 238 | 243 | ||
| 239 | this.sessions.set(sessionId, context); | 244 | this.sessions.set(threadId, context); |
| 240 | this.attachProcessListeners(context); | 245 | this.attachProcessListeners(context); |
| 241 | 246 | ||
| 242 | this.emitLifecycleEvent(context, "session/connecting", "Starting codex app-server"); | 247 | this.emitLifecycleEvent(context, "session/connecting", "Starting codex app-server"); |
| ... | @@ -274,12 +279,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -274,12 +279,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 274 | : "Starting a new Codex thread.", | 279 | : "Starting a new Codex thread.", |
| 275 | ); | 280 | ); |
| 276 | await Effect.logInfo("codex app-server opening thread", { | 281 | await Effect.logInfo("codex app-server opening thread", { |
| 277 | sessionId, | 282 | threadId, |
| 278 | requestedRuntimeMode: input.runtimeMode, | 283 | requestedRuntimeMode: input.runtimeMode, |
| 279 | requestedModel: normalizedModel ?? null, | 284 | requestedModel: normalizedModel ?? null, |
| 280 | requestedCwd: resolvedCwd, | 285 | requestedCwd: resolvedCwd, |
| 281 | resumeThreadId: resumeThreadId ?? null, | 286 | resumeThreadId: resumeThreadId ?? null, |
| 282 | }).pipe(Effect.runPromise); | 287 | }).pipe(this.runPromise); |
| 283 | 288 | ||
| 284 | let threadOpenMethod: "thread/start" | "thread/resume" = "thread/start"; | 289 | let threadOpenMethod: "thread/start" | "thread/resume" = "thread/start"; |
| 285 | let threadOpenResponse: unknown; | 290 | let threadOpenResponse: unknown; |
| ... | @@ -298,12 +303,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -298,12 +303,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 298 | error instanceof Error ? error.message : "Codex thread resume failed.", | 303 | error instanceof Error ? error.message : "Codex thread resume failed.", |
| 299 | ); | 304 | ); |
| 300 | await Effect.logWarning("codex app-server thread resume failed", { | 305 | await Effect.logWarning("codex app-server thread resume failed", { |
| 301 | sessionId, | 306 | threadId, |
| 302 | requestedRuntimeMode: input.runtimeMode, | 307 | requestedRuntimeMode: input.runtimeMode, |
| 303 | resumeThreadId, | 308 | resumeThreadId, |
| 304 | recoverable: false, | 309 | recoverable: false, |
| 305 | cause: error instanceof Error ? error.message : String(error), | 310 | cause: error instanceof Error ? error.message : String(error), |
| 306 | }).pipe(Effect.runPromise); | 311 | }).pipe(this.runPromise); |
| 307 | throw error; | 312 | throw error; |
| 308 | } | 313 | } |
| 309 | 314 | ||
| ... | @@ -314,12 +319,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -314,12 +319,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 314 | `Could not resume thread ${resumeThreadId}; started a new thread instead.`, | 319 | `Could not resume thread ${resumeThreadId}; started a new thread instead.`, |
| 315 | ); | 320 | ); |
| 316 | await Effect.logWarning("codex app-server thread resume fell back to fresh start", { | 321 | await Effect.logWarning("codex app-server thread resume fell back to fresh start", { |
| 317 | sessionId, | 322 | threadId, |
| 318 | requestedRuntimeMode: input.runtimeMode, | 323 | requestedRuntimeMode: input.runtimeMode, |
| 319 | resumeThreadId, | 324 | resumeThreadId, |
| 320 | recoverable: true, | 325 | recoverable: true, |
| 321 | cause: error instanceof Error ? error.message : String(error), | 326 | cause: error instanceof Error ? error.message : String(error), |
| 322 | }).pipe(Effect.runPromise); | 327 | }).pipe(this.runPromise); |
| 323 | threadOpenResponse = await this.sendRequest(context, "thread/start", threadStartParams); | 328 | threadOpenResponse = await this.sendRequest(context, "thread/start", threadStartParams); |
| 324 | } | 329 | } |
| 325 | } else { | 330 | } else { |
| ... | @@ -334,22 +339,25 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -334,22 +339,25 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 334 | if (!threadIdRaw) { | 339 | if (!threadIdRaw) { |
| 335 | throw new Error(`${threadOpenMethod} response did not include a thread id.`); | 340 | throw new Error(`${threadOpenMethod} response did not include a thread id.`); |
| 336 | } | 341 | } |
| 337 | const threadId = ProviderThreadId.makeUnsafe(threadIdRaw); | 342 | const providerThreadId = threadIdRaw; |
| 338 | 343 | ||
| 339 | this.updateSession(context, { | 344 | this.updateSession(context, { |
| 340 | status: "ready", | 345 | status: "ready", |
| 341 | threadId, | 346 | resumeCursor: { threadId: providerThreadId }, |
| 342 | resumeCursor: { threadId }, | ||
| 343 | }); | 347 | }); |
| 344 | this.emitLifecycleEvent(context, "session/threadOpenResolved", `Codex ${threadOpenMethod} resolved.`); | 348 | this.emitLifecycleEvent( |
| 349 | context, | ||
| 350 | "session/threadOpenResolved", | ||
| 351 | `Codex ${threadOpenMethod} resolved.`, | ||
| 352 | ); | ||
| 345 | await Effect.logInfo("codex app-server thread open resolved", { | 353 | await Effect.logInfo("codex app-server thread open resolved", { |
| 346 | sessionId, | 354 | threadId, |
| 347 | threadOpenMethod, | 355 | threadOpenMethod, |
| 348 | requestedResumeThreadId: resumeThreadId ?? null, | 356 | requestedResumeThreadId: resumeThreadId ?? null, |
| 349 | resolvedThreadId: threadId, | 357 | resolvedThreadId: providerThreadId, |
| 350 | requestedRuntimeMode: input.runtimeMode, | 358 | requestedRuntimeMode: input.runtimeMode, |
| 351 | }).pipe(Effect.runPromise); | 359 | }).pipe(this.runPromise); |
| 352 | this.emitLifecycleEvent(context, "session/ready", `Connected to thread ${threadId}`); | 360 | this.emitLifecycleEvent(context, "session/ready", `Connected to thread ${providerThreadId}`); |
| 353 | return { ...context.session }; | 361 | return { ...context.session }; |
| 354 | } catch (error) { | 362 | } catch (error) { |
| 355 | const message = error instanceof Error ? error.message : "Failed to start Codex session."; | 363 | const message = error instanceof Error ? error.message : "Failed to start Codex session."; |
| ... | @@ -359,13 +367,13 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -359,13 +367,13 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 359 | lastError: message, | 367 | lastError: message, |
| 360 | }); | 368 | }); |
| 361 | this.emitErrorEvent(context, "session/startFailed", message); | 369 | this.emitErrorEvent(context, "session/startFailed", message); |
| 362 | this.stopSession(sessionId); | 370 | this.stopSession(threadId); |
| 363 | } else { | 371 | } else { |
| 364 | this.emitEvent({ | 372 | this.emitEvent({ |
| 365 | id: EventId.makeUnsafe(randomUUID()), | 373 | id: EventId.makeUnsafe(randomUUID()), |
| 366 | kind: "error", | 374 | kind: "error", |
| 367 | provider: "codex", | 375 | provider: "codex", |
| 368 | sessionId, | 376 | threadId, |
| 369 | createdAt: new Date().toISOString(), | 377 | createdAt: new Date().toISOString(), |
| 370 | method: "session/startFailed", | 378 | method: "session/startFailed", |
| 371 | message, | 379 | message, |
| ... | @@ -376,10 +384,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -376,10 +384,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 376 | } | 384 | } |
| 377 | 385 | ||
| 378 | async sendTurn(input: CodexAppServerSendTurnInput): Promise<ProviderTurnStartResult> { | 386 | async sendTurn(input: CodexAppServerSendTurnInput): Promise<ProviderTurnStartResult> { |
| 379 | const context = this.requireSession(input.sessionId); | 387 | const context = this.requireSession(input.threadId); |
| 380 | if (!context.session.threadId) { | ||
| 381 | throw new Error("Session is missing a thread id."); | ||
| 382 | } | ||
| 383 | 388 | ||
| 384 | const turnInput: Array< | 389 | const turnInput: Array< |
| 385 | { type: "text"; text: string; text_elements: [] } | { type: "image"; url: string } | 390 | { type: "text"; text: string; text_elements: [] } | { type: "image"; url: string } |
| ... | @@ -403,15 +408,23 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -403,15 +408,23 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 403 | throw new Error("Turn input must include text or attachments."); | 408 | throw new Error("Turn input must include text or attachments."); |
| 404 | } | 409 | } |
| 405 | 410 | ||
| 411 | const providerThreadId = readResumeThreadId({ | ||
| 412 | threadId: context.session.threadId, | ||
| 413 | runtimeMode: context.session.runtimeMode, | ||
| 414 | resumeCursor: context.session.resumeCursor, | ||
| 415 | }); | ||
| 416 | if (!providerThreadId) { | ||
| 417 | throw new Error("Session is missing provider resume thread id."); | ||
| 418 | } | ||
| 406 | const turnStartParams: { | 419 | const turnStartParams: { |
| 407 | threadId: ProviderThreadId; | 420 | threadId: string; |
| 408 | input: Array< | 421 | input: Array< |
| 409 | { type: "text"; text: string; text_elements: [] } | { type: "image"; url: string } | 422 | { type: "text"; text: string; text_elements: [] } | { type: "image"; url: string } |
| 410 | >; | 423 | >; |
| 411 | model?: string; | 424 | model?: string; |
| 412 | effort?: string; | 425 | effort?: string; |
| 413 | } = { | 426 | } = { |
| 414 | threadId: context.session.threadId, | 427 | threadId: providerThreadId, |
| 415 | input: turnInput, | 428 | input: turnInput, |
| 416 | }; | 429 | }; |
| 417 | const normalizedModel = normalizeCodexModelSlug(input.model); | 430 | const normalizedModel = normalizeCodexModelSlug(input.model); |
| ... | @@ -429,7 +442,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -429,7 +442,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 429 | if (!turnIdRaw) { | 442 | if (!turnIdRaw) { |
| 430 | throw new Error("turn/start response did not include a turn id."); | 443 | throw new Error("turn/start response did not include a turn id."); |
| 431 | } | 444 | } |
| 432 | const turnId = ProviderTurnId.makeUnsafe(turnIdRaw); | 445 | const turnId = TurnId.makeUnsafe(turnIdRaw); |
| 433 | 446 | ||
| 434 | this.updateSession(context, { | 447 | this.updateSession(context, { |
| 435 | status: "running", | 448 | status: "running", |
| ... | @@ -448,49 +461,59 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -448,49 +461,59 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 448 | }; | 461 | }; |
| 449 | } | 462 | } |
| 450 | 463 | ||
| 451 | async interruptTurn(sessionId: ProviderSessionId, turnId?: ProviderTurnId): Promise<void> { | 464 | async interruptTurn(threadId: ThreadId, turnId?: TurnId): Promise<void> { |
| 452 | const context = this.requireSession(sessionId); | 465 | const context = this.requireSession(threadId); |
| 453 | const effectiveTurnId = turnId ?? context.session.activeTurnId; | 466 | const effectiveTurnId = turnId ?? context.session.activeTurnId; |
| 454 | 467 | ||
| 455 | if (!effectiveTurnId || !context.session.threadId) { | 468 | const providerThreadId = readResumeThreadId({ |
| 469 | threadId: context.session.threadId, | ||
| 470 | runtimeMode: context.session.runtimeMode, | ||
| 471 | resumeCursor: context.session.resumeCursor, | ||
| 472 | }); | ||
| 473 | if (!effectiveTurnId || !providerThreadId) { | ||
| 456 | return; | 474 | return; |
| 457 | } | 475 | } |
| 458 | 476 | ||
| 459 | await this.sendRequest(context, "turn/interrupt", { | 477 | await this.sendRequest(context, "turn/interrupt", { |
| 460 | threadId: context.session.threadId, | 478 | threadId: providerThreadId, |
| 461 | turnId: effectiveTurnId, | 479 | turnId: effectiveTurnId, |
| 462 | }); | 480 | }); |
| 463 | } | 481 | } |
| 464 | 482 | ||
| 465 | async readThread(sessionId: ProviderSessionId): Promise<CodexThreadSnapshot> { | 483 | async readThread(threadId: ThreadId): Promise<CodexThreadSnapshot> { |
| 466 | const context = this.requireSession(sessionId); | 484 | const context = this.requireSession(threadId); |
| 467 | const threadId = context.session.threadId; | 485 | const providerThreadId = readResumeThreadId({ |
| 468 | if (!threadId) { | 486 | threadId: context.session.threadId, |
| 469 | throw new Error("Session is missing a thread id."); | 487 | runtimeMode: context.session.runtimeMode, |
| 488 | resumeCursor: context.session.resumeCursor, | ||
| 489 | }); | ||
| 490 | if (!providerThreadId) { | ||
| 491 | throw new Error("Session is missing a provider resume thread id."); | ||
| 470 | } | 492 | } |
| 471 | 493 | ||
| 472 | const response = await this.sendRequest(context, "thread/read", { | 494 | const response = await this.sendRequest(context, "thread/read", { |
| 473 | threadId, | 495 | threadId: providerThreadId, |
| 474 | includeTurns: true, | 496 | includeTurns: true, |
| 475 | }); | 497 | }); |
| 476 | return this.parseThreadSnapshot("thread/read", response); | 498 | return this.parseThreadSnapshot("thread/read", response); |
| 477 | } | 499 | } |
| 478 | 500 | ||
| 479 | async rollbackThread( | 501 | async rollbackThread(threadId: ThreadId, numTurns: number): Promise<CodexThreadSnapshot> { |
| 480 | sessionId: ProviderSessionId, | 502 | const context = this.requireSession(threadId); |
| 481 | numTurns: number, | 503 | const providerThreadId = readResumeThreadId({ |
| 482 | ): Promise<CodexThreadSnapshot> { | 504 | threadId: context.session.threadId, |
| 483 | const context = this.requireSession(sessionId); | 505 | runtimeMode: context.session.runtimeMode, |
| 484 | const threadId = context.session.threadId; | 506 | resumeCursor: context.session.resumeCursor, |
| 485 | if (!threadId) { | 507 | }); |
| 486 | throw new Error("Session is missing a thread id."); | 508 | if (!providerThreadId) { |
| 509 | throw new Error("Session is missing a provider resume thread id."); | ||
| 487 | } | 510 | } |
| 488 | if (!Number.isInteger(numTurns) || numTurns < 1) { | 511 | if (!Number.isInteger(numTurns) || numTurns < 1) { |
| 489 | throw new Error("numTurns must be an integer >= 1."); | 512 | throw new Error("numTurns must be an integer >= 1."); |
| 490 | } | 513 | } |
| 491 | 514 | ||
| 492 | const response = await this.sendRequest(context, "thread/rollback", { | 515 | const response = await this.sendRequest(context, "thread/rollback", { |
| 493 | threadId, | 516 | threadId: providerThreadId, |
| 494 | numTurns, | 517 | numTurns, |
| 495 | }); | 518 | }); |
| 496 | this.updateSession(context, { | 519 | this.updateSession(context, { |
| ... | @@ -501,11 +524,11 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -501,11 +524,11 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 501 | } | 524 | } |
| 502 | 525 | ||
| 503 | async respondToRequest( | 526 | async respondToRequest( |
| 504 | sessionId: ProviderSessionId, | 527 | threadId: ThreadId, |
| 505 | requestId: ApprovalRequestId, | 528 | requestId: ApprovalRequestId, |
| 506 | decision: ProviderApprovalDecision, | 529 | decision: ProviderApprovalDecision, |
| 507 | ): Promise<void> { | 530 | ): Promise<void> { |
| 508 | const context = this.requireSession(sessionId); | 531 | const context = this.requireSession(threadId); |
| 509 | const pendingRequest = context.pendingApprovals.get(requestId); | 532 | const pendingRequest = context.pendingApprovals.get(requestId); |
| 510 | if (!pendingRequest) { | 533 | if (!pendingRequest) { |
| 511 | throw new Error(`Unknown pending approval request: ${requestId}`); | 534 | throw new Error(`Unknown pending approval request: ${requestId}`); |
| ... | @@ -523,10 +546,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -523,10 +546,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 523 | id: EventId.makeUnsafe(randomUUID()), | 546 | id: EventId.makeUnsafe(randomUUID()), |
| 524 | kind: "notification", | 547 | kind: "notification", |
| 525 | provider: "codex", | 548 | provider: "codex", |
| 526 | sessionId: context.session.sessionId, | 549 | threadId: context.session.threadId, |
| 527 | createdAt: new Date().toISOString(), | 550 | createdAt: new Date().toISOString(), |
| 528 | method: "item/requestApproval/decision", | 551 | method: "item/requestApproval/decision", |
| 529 | threadId: pendingRequest.threadId, | ||
| 530 | turnId: pendingRequest.turnId, | 552 | turnId: pendingRequest.turnId, |
| 531 | itemId: pendingRequest.itemId, | 553 | itemId: pendingRequest.itemId, |
| 532 | requestId: pendingRequest.requestId, | 554 | requestId: pendingRequest.requestId, |
| ... | @@ -539,8 +561,8 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -539,8 +561,8 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 539 | }); | 561 | }); |
| 540 | } | 562 | } |
| 541 | 563 | ||
| 542 | stopSession(sessionId: ProviderSessionId): void { | 564 | stopSession(threadId: ThreadId): void { |
| 543 | const context = this.sessions.get(sessionId); | 565 | const context = this.sessions.get(threadId); |
| 544 | if (!context) { | 566 | if (!context) { |
| 545 | return; | 567 | return; |
| 546 | } | 568 | } |
| ... | @@ -565,7 +587,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -565,7 +587,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 565 | activeTurnId: undefined, | 587 | activeTurnId: undefined, |
| 566 | }); | 588 | }); |
| 567 | this.emitLifecycleEvent(context, "session/closed", "Session stopped"); | 589 | this.emitLifecycleEvent(context, "session/closed", "Session stopped"); |
| 568 | this.sessions.delete(sessionId); | 590 | this.sessions.delete(threadId); |
| 569 | } | 591 | } |
| 570 | 592 | ||
| 571 | listSessions(): ProviderSession[] { | 593 | listSessions(): ProviderSession[] { |
| ... | @@ -574,24 +596,24 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -574,24 +596,24 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 574 | })); | 596 | })); |
| 575 | } | 597 | } |
| 576 | 598 | ||
| 577 | hasSession(sessionId: ProviderSessionId): boolean { | 599 | hasSession(threadId: ThreadId): boolean { |
| 578 | return this.sessions.has(sessionId); | 600 | return this.sessions.has(threadId); |
| 579 | } | 601 | } |
| 580 | 602 | ||
| 581 | stopAll(): void { | 603 | stopAll(): void { |
| 582 | for (const sessionId of this.sessions.keys()) { | 604 | for (const threadId of this.sessions.keys()) { |
| 583 | this.stopSession(sessionId); | 605 | this.stopSession(threadId); |
| 584 | } | 606 | } |
| 585 | } | 607 | } |
| 586 | 608 | ||
| 587 | private requireSession(sessionId: ProviderSessionId): CodexSessionContext { | 609 | private requireSession(threadId: ThreadId): CodexSessionContext { |
| 588 | const context = this.sessions.get(sessionId); | 610 | const context = this.sessions.get(threadId); |
| 589 | if (!context) { | 611 | if (!context) { |
| 590 | throw new Error(`Unknown session: ${sessionId}`); | 612 | throw new Error(`Unknown session for thread: ${threadId}`); |
| 591 | } | 613 | } |
| 592 | 614 | ||
| 593 | if (context.session.status === "closed") { | 615 | if (context.session.status === "closed") { |
| 594 | throw new Error(`Session is closed: ${sessionId}`); | 616 | throw new Error(`Session is closed for thread: ${threadId}`); |
| 595 | } | 617 | } |
| 596 | 618 | ||
| 597 | return context; | 619 | return context; |
| ... | @@ -636,7 +658,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -636,7 +658,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 636 | lastError: code === 0 ? context.session.lastError : message, | 658 | lastError: code === 0 ? context.session.lastError : message, |
| 637 | }); | 659 | }); |
| 638 | this.emitLifecycleEvent(context, "session/exited", message); | 660 | this.emitLifecycleEvent(context, "session/exited", message); |
| 639 | this.sessions.delete(context.session.sessionId); | 661 | this.sessions.delete(context.session.threadId); |
| 640 | }); | 662 | }); |
| 641 | } | 663 | } |
| 642 | 664 | ||
| ... | @@ -698,10 +720,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -698,10 +720,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 698 | id: EventId.makeUnsafe(randomUUID()), | 720 | id: EventId.makeUnsafe(randomUUID()), |
| 699 | kind: "notification", | 721 | kind: "notification", |
| 700 | provider: "codex", | 722 | provider: "codex", |
| 701 | sessionId: context.session.sessionId, | 723 | threadId: context.session.threadId, |
| 702 | createdAt: new Date().toISOString(), | 724 | createdAt: new Date().toISOString(), |
| 703 | method: notification.method, | 725 | method: notification.method, |
| 704 | threadId: route.threadId, | ||
| 705 | turnId: route.turnId, | 726 | turnId: route.turnId, |
| 706 | itemId: route.itemId, | 727 | itemId: route.itemId, |
| 707 | textDelta, | 728 | textDelta, |
| ... | @@ -709,19 +730,17 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -709,19 +730,17 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 709 | }); | 730 | }); |
| 710 | 731 | ||
| 711 | if (notification.method === "thread/started") { | 732 | if (notification.method === "thread/started") { |
| 712 | const threadId = toProviderThreadId( | 733 | const providerThreadId = normalizeProviderThreadId( |
| 713 | this.readString(this.readObject(notification.params)?.thread, "id"), | 734 | this.readString(this.readObject(notification.params)?.thread, "id"), |
| 714 | ); | 735 | ); |
| 715 | if (threadId) { | 736 | if (providerThreadId) { |
| 716 | this.updateSession(context, { threadId }); | 737 | this.updateSession(context, { resumeCursor: { threadId: providerThreadId } }); |
| 717 | } | 738 | } |
| 718 | return; | 739 | return; |
| 719 | } | 740 | } |
| 720 | 741 | ||
| 721 | if (notification.method === "turn/started") { | 742 | if (notification.method === "turn/started") { |
| 722 | const turnId = toProviderTurnId( | 743 | const turnId = toTurnId(this.readString(this.readObject(notification.params)?.turn, "id")); |
| 723 | this.readString(this.readObject(notification.params)?.turn, "id"), | ||
| 724 | ); | ||
| 725 | this.updateSession(context, { | 744 | this.updateSession(context, { |
| 726 | status: "running", | 745 | status: "running", |
| 727 | activeTurnId: turnId, | 746 | activeTurnId: turnId, |
| ... | @@ -766,7 +785,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -766,7 +785,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 766 | ? "item/commandExecution/requestApproval" | 785 | ? "item/commandExecution/requestApproval" |
| 767 | : "item/fileChange/requestApproval", | 786 | : "item/fileChange/requestApproval", |
| 768 | requestKind, | 787 | requestKind, |
| 769 | ...(route.threadId ? { threadId: route.threadId } : {}), | 788 | threadId: context.session.threadId, |
| 770 | ...(route.turnId ? { turnId: route.turnId } : {}), | 789 | ...(route.turnId ? { turnId: route.turnId } : {}), |
| 771 | ...(route.itemId ? { itemId: route.itemId } : {}), | 790 | ...(route.itemId ? { itemId: route.itemId } : {}), |
| 772 | }; | 791 | }; |
| ... | @@ -777,10 +796,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -777,10 +796,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 777 | id: EventId.makeUnsafe(randomUUID()), | 796 | id: EventId.makeUnsafe(randomUUID()), |
| 778 | kind: "request", | 797 | kind: "request", |
| 779 | provider: "codex", | 798 | provider: "codex", |
| 780 | sessionId: context.session.sessionId, | 799 | threadId: context.session.threadId, |
| 781 | createdAt: new Date().toISOString(), | 800 | createdAt: new Date().toISOString(), |
| 782 | method: request.method, | 801 | method: request.method, |
| 783 | threadId: route.threadId, | ||
| 784 | turnId: route.turnId, | 802 | turnId: route.turnId, |
| 785 | itemId: route.itemId, | 803 | itemId: route.itemId, |
| 786 | requestId, | 804 | requestId, |
| ... | @@ -872,7 +890,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -872,7 +890,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 872 | id: EventId.makeUnsafe(randomUUID()), | 890 | id: EventId.makeUnsafe(randomUUID()), |
| 873 | kind: "session", | 891 | kind: "session", |
| 874 | provider: "codex", | 892 | provider: "codex", |
| 875 | sessionId: context.session.sessionId, | 893 | threadId: context.session.threadId, |
| 876 | createdAt: new Date().toISOString(), | 894 | createdAt: new Date().toISOString(), |
| 877 | method, | 895 | method, |
| 878 | message, | 896 | message, |
| ... | @@ -884,7 +902,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -884,7 +902,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 884 | id: EventId.makeUnsafe(randomUUID()), | 902 | id: EventId.makeUnsafe(randomUUID()), |
| 885 | kind: "error", | 903 | kind: "error", |
| 886 | provider: "codex", | 904 | provider: "codex", |
| 887 | sessionId: context.session.sessionId, | 905 | threadId: context.session.threadId, |
| 888 | createdAt: new Date().toISOString(), | 906 | createdAt: new Date().toISOString(), |
| 889 | method, | 907 | method, |
| 890 | message, | 908 | message, |
| ... | @@ -908,6 +926,10 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -908,6 +926,10 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 908 | return "command"; | 926 | return "command"; |
| 909 | } | 927 | } |
| 910 | 928 | ||
| 929 | if (method === "item/fileRead/requestApproval") { | ||
| 930 | return "file-read"; | ||
| 931 | } | ||
| 932 | |||
| 911 | if (method === "item/fileChange/requestApproval") { | 933 | if (method === "item/fileChange/requestApproval") { |
| 912 | return "file-change"; | 934 | return "file-change"; |
| 913 | } | 935 | } |
| ... | @@ -923,14 +945,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -923,14 +945,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 923 | if (!threadIdRaw) { | 945 | if (!threadIdRaw) { |
| 924 | throw new Error(`${method} response did not include a thread id.`); | 946 | throw new Error(`${method} response did not include a thread id.`); |
| 925 | } | 947 | } |
| 926 | const threadId = ProviderThreadId.makeUnsafe(threadIdRaw); | ||
| 927 | |||
| 928 | const turnsRaw = | 948 | const turnsRaw = |
| 929 | this.readArray(thread, "turns") ?? this.readArray(responseRecord, "turns") ?? []; | 949 | this.readArray(thread, "turns") ?? this.readArray(responseRecord, "turns") ?? []; |
| 930 | const turns = turnsRaw.map((turnValue, index) => { | 950 | const turns = turnsRaw.map((turnValue, index) => { |
| 931 | const turn = this.readObject(turnValue); | 951 | const turn = this.readObject(turnValue); |
| 932 | const turnIdRaw = this.readString(turn, "id") ?? `${threadIdRaw}:turn:${index + 1}`; | 952 | const turnIdRaw = this.readString(turn, "id") ?? `${threadIdRaw}:turn:${index + 1}`; |
| 933 | const turnId = ProviderTurnId.makeUnsafe(turnIdRaw); | 953 | const turnId = TurnId.makeUnsafe(turnIdRaw); |
| 934 | const items = this.readArray(turn, "items") ?? []; | 954 | const items = this.readArray(turn, "items") ?? []; |
| 935 | return { | 955 | return { |
| 936 | id: turnId, | 956 | id: turnId, |
| ... | @@ -939,7 +959,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -939,7 +959,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 939 | }); | 959 | }); |
| 940 | 960 | ||
| 941 | return { | 961 | return { |
| 942 | threadId, | 962 | threadId: threadIdRaw, |
| 943 | turns, | 963 | turns, |
| 944 | }; | 964 | }; |
| 945 | } | 965 | } |
| ... | @@ -977,31 +997,21 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -977,31 +997,21 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 977 | } | 997 | } |
| 978 | 998 | ||
| 979 | private readRouteFields(params: unknown): { | 999 | private readRouteFields(params: unknown): { |
| 980 | threadId?: ProviderThreadId; | 1000 | turnId?: TurnId; |
| 981 | turnId?: ProviderTurnId; | ||
| 982 | itemId?: ProviderItemId; | 1001 | itemId?: ProviderItemId; |
| 983 | } { | 1002 | } { |
| 984 | const route: { | 1003 | const route: { |
| 985 | threadId?: ProviderThreadId; | 1004 | turnId?: TurnId; |
| 986 | turnId?: ProviderTurnId; | ||
| 987 | itemId?: ProviderItemId; | 1005 | itemId?: ProviderItemId; |
| 988 | } = {}; | 1006 | } = {}; |
| 989 | 1007 | ||
| 990 | const threadId = toProviderThreadId( | 1008 | const turnId = toTurnId( |
| 991 | this.readString(params, "threadId") ?? | ||
| 992 | this.readString(this.readObject(params, "thread"), "id"), | ||
| 993 | ); | ||
| 994 | const turnId = toProviderTurnId( | ||
| 995 | this.readString(params, "turnId") ?? this.readString(this.readObject(params, "turn"), "id"), | 1009 | this.readString(params, "turnId") ?? this.readString(this.readObject(params, "turn"), "id"), |
| 996 | ); | 1010 | ); |
| 997 | const itemId = toProviderItemId( | 1011 | const itemId = toProviderItemId( |
| 998 | this.readString(params, "itemId") ?? this.readString(this.readObject(params, "item"), "id"), | 1012 | this.readString(params, "itemId") ?? this.readString(this.readObject(params, "item"), "id"), |
| 999 | ); | 1013 | ); |
| 1000 | 1014 | ||
| 1001 | if (threadId) { | ||
| 1002 | route.threadId = threadId; | ||
| 1003 | } | ||
| 1004 | |||
| 1005 | if (turnId) { | 1015 | if (turnId) { |
| 1006 | route.turnId = turnId; | 1016 | route.turnId = turnId; |
| 1007 | } | 1017 | } |
| ... | @@ -1065,8 +1075,8 @@ function brandIfNonEmpty<T extends string>( | ... | @@ -1065,8 +1075,8 @@ function brandIfNonEmpty<T extends string>( |
| 1065 | return normalized?.length ? maker(normalized) : undefined; | 1075 | return normalized?.length ? maker(normalized) : undefined; |
| 1066 | } | 1076 | } |
| 1067 | 1077 | ||
| 1068 | function toProviderThreadId(value: string | undefined): ProviderThreadId | undefined { | 1078 | function normalizeProviderThreadId(value: string | undefined): string | undefined { |
| 1069 | return brandIfNonEmpty(value, ProviderThreadId.makeUnsafe); | 1079 | return brandIfNonEmpty(value, (normalized) => normalized); |
| 1070 | } | 1080 | } |
| 1071 | 1081 | ||
| 1072 | function readCodexProviderOptions(input: ProviderSessionStartInput): { | 1082 | function readCodexProviderOptions(input: ProviderSessionStartInput): { |
| ... | @@ -1083,20 +1093,20 @@ function readCodexProviderOptions(input: ProviderSessionStartInput): { | ... | @@ -1083,20 +1093,20 @@ function readCodexProviderOptions(input: ProviderSessionStartInput): { |
| 1083 | }; | 1093 | }; |
| 1084 | } | 1094 | } |
| 1085 | 1095 | ||
| 1086 | function readResumeCursorThreadId(resumeCursor: unknown): ProviderThreadId | undefined { | 1096 | function readResumeCursorThreadId(resumeCursor: unknown): string | undefined { |
| 1087 | if (!resumeCursor || typeof resumeCursor !== "object" || Array.isArray(resumeCursor)) { | 1097 | if (!resumeCursor || typeof resumeCursor !== "object" || Array.isArray(resumeCursor)) { |
| 1088 | return undefined; | 1098 | return undefined; |
| 1089 | } | 1099 | } |
| 1090 | const rawThreadId = (resumeCursor as Record<string, unknown>).threadId; | 1100 | const rawThreadId = (resumeCursor as Record<string, unknown>).threadId; |
| 1091 | return typeof rawThreadId === "string" ? toProviderThreadId(rawThreadId) : undefined; | 1101 | return typeof rawThreadId === "string" ? normalizeProviderThreadId(rawThreadId) : undefined; |
| 1092 | } | 1102 | } |
| 1093 | 1103 | ||
| 1094 | function readResumeThreadId(input: ProviderSessionStartInput): ProviderThreadId | undefined { | 1104 | function readResumeThreadId(input: ProviderSessionStartInput): string | undefined { |
| 1095 | return readResumeCursorThreadId(input.resumeCursor); | 1105 | return readResumeCursorThreadId(input.resumeCursor); |
| 1096 | } | 1106 | } |
| 1097 | 1107 | ||
| 1098 | function toProviderTurnId(value: string | undefined): ProviderTurnId | undefined { | 1108 | function toTurnId(value: string | undefined): TurnId | undefined { |
| 1099 | return brandIfNonEmpty(value, ProviderTurnId.makeUnsafe); | 1109 | return brandIfNonEmpty(value, TurnId.makeUnsafe); |
| 1100 | } | 1110 | } |
| 1101 | 1111 | ||
| 1102 | function toProviderItemId(value: string | undefined): ProviderItemId | undefined { | 1112 | function toProviderItemId(value: string | undefined): ProviderItemId | undefined { |
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+43-71| ... | @@ -9,9 +9,6 @@ import { | ... | @@ -9,9 +9,6 @@ import { |
| 9 | EventId, | 9 | EventId, |
| 10 | MessageId, | 10 | MessageId, |
| 11 | ProjectId, | 11 | ProjectId, |
| 12 | ProviderSessionId, | ||
| 13 | ProviderThreadId, | ||
| 14 | ProviderTurnId, | ||
| 15 | ThreadId, | 12 | ThreadId, |
| 16 | TurnId, | 13 | TurnId, |
| 17 | } from "@t3tools/contracts"; | 14 | } from "@t3tools/contracts"; |
| ... | @@ -40,18 +37,14 @@ import { checkpointRefForThreadTurn } from "../../checkpointing/Utils.ts"; | ... | @@ -40,18 +37,14 @@ import { checkpointRefForThreadTurn } from "../../checkpointing/Utils.ts"; |
| 40 | import { ServerConfig } from "../../config.ts"; | 37 | import { ServerConfig } from "../../config.ts"; |
| 41 | 38 | ||
| 42 | const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); | 39 | const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); |
| 43 | const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value); | ||
| 44 | const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId.makeUnsafe(value); | ||
| 45 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); | ||
| 46 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); | 40 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 47 | 41 | ||
| 48 | type LegacyProviderRuntimeEvent = { | 42 | type LegacyProviderRuntimeEvent = { |
| 49 | readonly type: string; | 43 | readonly type: string; |
| 50 | readonly eventId: EventId; | 44 | readonly eventId: EventId; |
| 51 | readonly provider: "codex" | "claudeCode" | "cursor"; | 45 | readonly provider: "codex" | "claudeCode" | "cursor"; |
| 52 | readonly sessionId: string; | ||
| 53 | readonly createdAt: string; | 46 | readonly createdAt: string; |
| 54 | readonly threadId?: string | undefined; | 47 | readonly threadId: ThreadId; |
| 55 | readonly turnId?: string | undefined; | 48 | readonly turnId?: string | undefined; |
| 56 | readonly itemId?: string | undefined; | 49 | readonly itemId?: string | undefined; |
| 57 | readonly requestId?: string | undefined; | 50 | readonly requestId?: string | undefined; |
| ... | @@ -68,7 +61,7 @@ function createProviderServiceHarness( | ... | @@ -68,7 +61,7 @@ function createProviderServiceHarness( |
| 68 | const now = new Date().toISOString(); | 61 | const now = new Date().toISOString(); |
| 69 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); | 62 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| 70 | const rollbackConversation = vi.fn( | 63 | const rollbackConversation = vi.fn( |
| 71 | (_input: { readonly sessionId: ProviderSessionId; readonly numTurns: number }) => Effect.void, | 64 | (_input: { readonly threadId: ThreadId; readonly numTurns: number }) => Effect.void, |
| 72 | ); | 65 | ); |
| 73 | 66 | ||
| 74 | const unsupported = <A>() => | 67 | const unsupported = <A>() => |
| ... | @@ -77,11 +70,10 @@ function createProviderServiceHarness( | ... | @@ -77,11 +70,10 @@ function createProviderServiceHarness( |
| 77 | hasSession | 70 | hasSession |
| 78 | ? Effect.succeed([ | 71 | ? Effect.succeed([ |
| 79 | { | 72 | { |
| 80 | sessionId: asSessionId("sess-1"), | ||
| 81 | provider: providerName, | 73 | provider: providerName, |
| 82 | status: "ready", | 74 | status: "ready", |
| 83 | runtimeMode: "full-access", | 75 | runtimeMode: "full-access", |
| 84 | threadId: asProviderThreadId("provider-thread-1"), | 76 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 85 | cwd: sessionCwd, | 77 | cwd: sessionCwd, |
| 86 | createdAt: now, | 78 | createdAt: now, |
| 87 | updatedAt: now, | 79 | updatedAt: now, |
| ... | @@ -344,8 +336,6 @@ describe("CheckpointReactor", () => { | ... | @@ -344,8 +336,6 @@ describe("CheckpointReactor", () => { |
| 344 | threadId: ThreadId.makeUnsafe("thread-1"), | 336 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 345 | status: "ready", | 337 | status: "ready", |
| 346 | providerName: "codex", | 338 | providerName: "codex", |
| 347 | providerSessionId: asSessionId("sess-1"), | ||
| 348 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 349 | runtimeMode: "approval-required", | 339 | runtimeMode: "approval-required", |
| 350 | activeTurnId: null, | 340 | activeTurnId: null, |
| 351 | lastError: null, | 341 | lastError: null, |
| ... | @@ -359,10 +349,10 @@ describe("CheckpointReactor", () => { | ... | @@ -359,10 +349,10 @@ describe("CheckpointReactor", () => { |
| 359 | type: "turn.started", | 349 | type: "turn.started", |
| 360 | eventId: EventId.makeUnsafe("evt-turn-started-1"), | 350 | eventId: EventId.makeUnsafe("evt-turn-started-1"), |
| 361 | provider: "codex", | 351 | provider: "codex", |
| 362 | sessionId: asSessionId("sess-1"), | 352 | |
| 363 | createdAt: new Date().toISOString(), | 353 | createdAt: new Date().toISOString(), |
| 364 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 354 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 365 | turnId: asProviderTurnId("turn-1"), | 355 | turnId: asTurnId("turn-1"), |
| 366 | }); | 356 | }); |
| 367 | await waitForGitRefExists( | 357 | await waitForGitRefExists( |
| 368 | harness.cwd, | 358 | harness.cwd, |
| ... | @@ -374,10 +364,10 @@ describe("CheckpointReactor", () => { | ... | @@ -374,10 +364,10 @@ describe("CheckpointReactor", () => { |
| 374 | type: "turn.completed", | 364 | type: "turn.completed", |
| 375 | eventId: EventId.makeUnsafe("evt-turn-completed-1"), | 365 | eventId: EventId.makeUnsafe("evt-turn-completed-1"), |
| 376 | provider: "codex", | 366 | provider: "codex", |
| 377 | sessionId: asSessionId("sess-1"), | 367 | |
| 378 | createdAt: new Date().toISOString(), | 368 | createdAt: new Date().toISOString(), |
| 379 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 369 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 380 | turnId: asProviderTurnId("turn-1"), | 370 | turnId: asTurnId("turn-1"), |
| 381 | payload: { state: "completed" }, | 371 | payload: { state: "completed" }, |
| 382 | }); | 372 | }); |
| 383 | 373 | ||
| ... | @@ -422,8 +412,6 @@ describe("CheckpointReactor", () => { | ... | @@ -422,8 +412,6 @@ describe("CheckpointReactor", () => { |
| 422 | threadId: ThreadId.makeUnsafe("thread-1"), | 412 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 423 | status: "running", | 413 | status: "running", |
| 424 | providerName: "codex", | 414 | providerName: "codex", |
| 425 | providerSessionId: asSessionId("sess-1"), | ||
| 426 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 427 | runtimeMode: "approval-required", | 415 | runtimeMode: "approval-required", |
| 428 | activeTurnId: asTurnId("turn-main"), | 416 | activeTurnId: asTurnId("turn-main"), |
| 429 | lastError: null, | 417 | lastError: null, |
| ... | @@ -437,10 +425,10 @@ describe("CheckpointReactor", () => { | ... | @@ -437,10 +425,10 @@ describe("CheckpointReactor", () => { |
| 437 | type: "turn.started", | 425 | type: "turn.started", |
| 438 | eventId: EventId.makeUnsafe("evt-turn-started-main"), | 426 | eventId: EventId.makeUnsafe("evt-turn-started-main"), |
| 439 | provider: "codex", | 427 | provider: "codex", |
| 440 | sessionId: asSessionId("sess-1"), | 428 | |
| 441 | createdAt: new Date().toISOString(), | 429 | createdAt: new Date().toISOString(), |
| 442 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 430 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 443 | turnId: asProviderTurnId("turn-main"), | 431 | turnId: asTurnId("turn-main"), |
| 444 | }); | 432 | }); |
| 445 | await waitForGitRefExists( | 433 | await waitForGitRefExists( |
| 446 | harness.cwd, | 434 | harness.cwd, |
| ... | @@ -453,10 +441,10 @@ describe("CheckpointReactor", () => { | ... | @@ -453,10 +441,10 @@ describe("CheckpointReactor", () => { |
| 453 | type: "turn.completed", | 441 | type: "turn.completed", |
| 454 | eventId: EventId.makeUnsafe("evt-turn-completed-aux"), | 442 | eventId: EventId.makeUnsafe("evt-turn-completed-aux"), |
| 455 | provider: "codex", | 443 | provider: "codex", |
| 456 | sessionId: asSessionId("sess-1"), | 444 | |
| 457 | createdAt: new Date().toISOString(), | 445 | createdAt: new Date().toISOString(), |
| 458 | threadId: ProviderThreadId.makeUnsafe("provider-thread-aux"), | 446 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 459 | turnId: asProviderTurnId("turn-aux"), | 447 | turnId: asTurnId("turn-aux"), |
| 460 | payload: { state: "completed" }, | 448 | payload: { state: "completed" }, |
| 461 | }); | 449 | }); |
| 462 | 450 | ||
| ... | @@ -471,10 +459,10 @@ describe("CheckpointReactor", () => { | ... | @@ -471,10 +459,10 @@ describe("CheckpointReactor", () => { |
| 471 | type: "turn.completed", | 459 | type: "turn.completed", |
| 472 | eventId: EventId.makeUnsafe("evt-turn-completed-main"), | 460 | eventId: EventId.makeUnsafe("evt-turn-completed-main"), |
| 473 | provider: "codex", | 461 | provider: "codex", |
| 474 | sessionId: asSessionId("sess-1"), | 462 | |
| 475 | createdAt: new Date().toISOString(), | 463 | createdAt: new Date().toISOString(), |
| 476 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 464 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 477 | turnId: asProviderTurnId("turn-main"), | 465 | turnId: asTurnId("turn-main"), |
| 478 | payload: { state: "completed" }, | 466 | payload: { state: "completed" }, |
| 479 | }); | 467 | }); |
| 480 | 468 | ||
| ... | @@ -501,8 +489,6 @@ describe("CheckpointReactor", () => { | ... | @@ -501,8 +489,6 @@ describe("CheckpointReactor", () => { |
| 501 | threadId: ThreadId.makeUnsafe("thread-1"), | 489 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 502 | status: "ready", | 490 | status: "ready", |
| 503 | providerName: "claudeCode", | 491 | providerName: "claudeCode", |
| 504 | providerSessionId: asSessionId("sess-1"), | ||
| 505 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 506 | runtimeMode: "approval-required", | 492 | runtimeMode: "approval-required", |
| 507 | activeTurnId: null, | 493 | activeTurnId: null, |
| 508 | lastError: null, | 494 | lastError: null, |
| ... | @@ -516,10 +502,10 @@ describe("CheckpointReactor", () => { | ... | @@ -516,10 +502,10 @@ describe("CheckpointReactor", () => { |
| 516 | type: "turn.started", | 502 | type: "turn.started", |
| 517 | eventId: EventId.makeUnsafe("evt-turn-started-claude-1"), | 503 | eventId: EventId.makeUnsafe("evt-turn-started-claude-1"), |
| 518 | provider: "claudeCode", | 504 | provider: "claudeCode", |
| 519 | sessionId: asSessionId("sess-1"), | 505 | |
| 520 | createdAt: new Date().toISOString(), | 506 | createdAt: new Date().toISOString(), |
| 521 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 507 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 522 | turnId: asProviderTurnId("turn-claude-1"), | 508 | turnId: asTurnId("turn-claude-1"), |
| 523 | }); | 509 | }); |
| 524 | await waitForGitRefExists( | 510 | await waitForGitRefExists( |
| 525 | harness.cwd, | 511 | harness.cwd, |
| ... | @@ -531,10 +517,10 @@ describe("CheckpointReactor", () => { | ... | @@ -531,10 +517,10 @@ describe("CheckpointReactor", () => { |
| 531 | type: "turn.completed", | 517 | type: "turn.completed", |
| 532 | eventId: EventId.makeUnsafe("evt-turn-completed-claude-1"), | 518 | eventId: EventId.makeUnsafe("evt-turn-completed-claude-1"), |
| 533 | provider: "claudeCode", | 519 | provider: "claudeCode", |
| 534 | sessionId: asSessionId("sess-1"), | 520 | |
| 535 | createdAt: new Date().toISOString(), | 521 | createdAt: new Date().toISOString(), |
| 536 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 522 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 537 | turnId: asProviderTurnId("turn-claude-1"), | 523 | turnId: asTurnId("turn-claude-1"), |
| 538 | payload: { state: "completed" }, | 524 | payload: { state: "completed" }, |
| 539 | }); | 525 | }); |
| 540 | 526 | ||
| ... | @@ -563,8 +549,6 @@ describe("CheckpointReactor", () => { | ... | @@ -563,8 +549,6 @@ describe("CheckpointReactor", () => { |
| 563 | threadId: ThreadId.makeUnsafe("thread-1"), | 549 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 564 | status: "ready", | 550 | status: "ready", |
| 565 | providerName: "codex", | 551 | providerName: "codex", |
| 566 | providerSessionId: asSessionId("sess-1"), | ||
| 567 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 568 | runtimeMode: "approval-required", | 552 | runtimeMode: "approval-required", |
| 569 | activeTurnId: null, | 553 | activeTurnId: null, |
| 570 | lastError: null, | 554 | lastError: null, |
| ... | @@ -578,10 +562,10 @@ describe("CheckpointReactor", () => { | ... | @@ -578,10 +562,10 @@ describe("CheckpointReactor", () => { |
| 578 | type: "turn.completed", | 562 | type: "turn.completed", |
| 579 | eventId: EventId.makeUnsafe("evt-turn-completed-missing-baseline"), | 563 | eventId: EventId.makeUnsafe("evt-turn-completed-missing-baseline"), |
| 580 | provider: "codex", | 564 | provider: "codex", |
| 581 | sessionId: asSessionId("sess-1"), | 565 | |
| 582 | createdAt: new Date().toISOString(), | 566 | createdAt: new Date().toISOString(), |
| 583 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 567 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 584 | turnId: asProviderTurnId("turn-missing-baseline"), | 568 | turnId: asTurnId("turn-missing-baseline"), |
| 585 | payload: { state: "completed" }, | 569 | payload: { state: "completed" }, |
| 586 | }); | 570 | }); |
| 587 | 571 | ||
| ... | @@ -652,8 +636,6 @@ describe("CheckpointReactor", () => { | ... | @@ -652,8 +636,6 @@ describe("CheckpointReactor", () => { |
| 652 | threadId: ThreadId.makeUnsafe("thread-1"), | 636 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 653 | status: "running", | 637 | status: "running", |
| 654 | providerName: "codex", | 638 | providerName: "codex", |
| 655 | providerSessionId: asSessionId("sess-missing"), | ||
| 656 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-missing"), | ||
| 657 | runtimeMode: "approval-required", | 639 | runtimeMode: "approval-required", |
| 658 | activeTurnId: asTurnId("turn-missing-cwd"), | 640 | activeTurnId: asTurnId("turn-missing-cwd"), |
| 659 | lastError: null, | 641 | lastError: null, |
| ... | @@ -668,10 +650,10 @@ describe("CheckpointReactor", () => { | ... | @@ -668,10 +650,10 @@ describe("CheckpointReactor", () => { |
| 668 | type: "turn.completed", | 650 | type: "turn.completed", |
| 669 | eventId: EventId.makeUnsafe("evt-turn-completed-missing-provider-cwd"), | 651 | eventId: EventId.makeUnsafe("evt-turn-completed-missing-provider-cwd"), |
| 670 | provider: "codex", | 652 | provider: "codex", |
| 671 | sessionId: asSessionId("sess-missing"), | 653 | |
| 672 | createdAt: new Date().toISOString(), | 654 | createdAt: new Date().toISOString(), |
| 673 | threadId: ProviderThreadId.makeUnsafe("provider-thread-missing"), | 655 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 674 | turnId: asProviderTurnId("turn-missing-cwd"), | 656 | turnId: asTurnId("turn-missing-cwd"), |
| 675 | payload: { state: "completed" }, | 657 | payload: { state: "completed" }, |
| 676 | }); | 658 | }); |
| 677 | 659 | ||
| ... | @@ -701,8 +683,6 @@ describe("CheckpointReactor", () => { | ... | @@ -701,8 +683,6 @@ describe("CheckpointReactor", () => { |
| 701 | threadId: ThreadId.makeUnsafe("thread-1"), | 683 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 702 | status: "ready", | 684 | status: "ready", |
| 703 | providerName: "codex", | 685 | providerName: "codex", |
| 704 | providerSessionId: asSessionId("sess-1"), | ||
| 705 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 706 | runtimeMode: "approval-required", | 686 | runtimeMode: "approval-required", |
| 707 | activeTurnId: null, | 687 | activeTurnId: null, |
| 708 | lastError: null, | 688 | lastError: null, |
| ... | @@ -716,10 +696,10 @@ describe("CheckpointReactor", () => { | ... | @@ -716,10 +696,10 @@ describe("CheckpointReactor", () => { |
| 716 | type: "checkpoint.captured", | 696 | type: "checkpoint.captured", |
| 717 | eventId: EventId.makeUnsafe("evt-checkpoint-captured-3"), | 697 | eventId: EventId.makeUnsafe("evt-checkpoint-captured-3"), |
| 718 | provider: "codex", | 698 | provider: "codex", |
| 719 | sessionId: asSessionId("sess-1"), | 699 | |
| 720 | createdAt: new Date().toISOString(), | 700 | createdAt: new Date().toISOString(), |
| 721 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 701 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 722 | turnId: asProviderTurnId("turn-3"), | 702 | turnId: asTurnId("turn-3"), |
| 723 | turnCount: 3, | 703 | turnCount: 3, |
| 724 | status: "completed", | 704 | status: "completed", |
| 725 | }); | 705 | }); |
| ... | @@ -753,8 +733,6 @@ describe("CheckpointReactor", () => { | ... | @@ -753,8 +733,6 @@ describe("CheckpointReactor", () => { |
| 753 | threadId: ThreadId.makeUnsafe("thread-1"), | 733 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 754 | status: "ready", | 734 | status: "ready", |
| 755 | providerName: "codex", | 735 | providerName: "codex", |
| 756 | providerSessionId: asSessionId("sess-1"), | ||
| 757 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 758 | runtimeMode: "approval-required", | 736 | runtimeMode: "approval-required", |
| 759 | activeTurnId: null, | 737 | activeTurnId: null, |
| 760 | lastError: null, | 738 | lastError: null, |
| ... | @@ -768,10 +746,10 @@ describe("CheckpointReactor", () => { | ... | @@ -768,10 +746,10 @@ describe("CheckpointReactor", () => { |
| 768 | type: "turn.completed", | 746 | type: "turn.completed", |
| 769 | eventId: EventId.makeUnsafe("evt-runtime-capture-failure"), | 747 | eventId: EventId.makeUnsafe("evt-runtime-capture-failure"), |
| 770 | provider: "codex", | 748 | provider: "codex", |
| 771 | sessionId: asSessionId("sess-1"), | 749 | |
| 772 | createdAt: new Date().toISOString(), | 750 | createdAt: new Date().toISOString(), |
| 773 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 751 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 774 | turnId: asProviderTurnId("turn-runtime-failure"), | 752 | turnId: asTurnId("turn-runtime-failure"), |
| 775 | payload: { state: "completed" }, | 753 | payload: { state: "completed" }, |
| 776 | }); | 754 | }); |
| 777 | 755 | ||
| ... | @@ -779,10 +757,10 @@ describe("CheckpointReactor", () => { | ... | @@ -779,10 +757,10 @@ describe("CheckpointReactor", () => { |
| 779 | type: "turn.started", | 757 | type: "turn.started", |
| 780 | eventId: EventId.makeUnsafe("evt-turn-started-after-runtime-failure"), | 758 | eventId: EventId.makeUnsafe("evt-turn-started-after-runtime-failure"), |
| 781 | provider: "codex", | 759 | provider: "codex", |
| 782 | sessionId: asSessionId("sess-1"), | 760 | |
| 783 | createdAt: new Date().toISOString(), | 761 | createdAt: new Date().toISOString(), |
| 784 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 762 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 785 | turnId: asProviderTurnId("turn-after-runtime-failure"), | 763 | turnId: asTurnId("turn-after-runtime-failure"), |
| 786 | }); | 764 | }); |
| 787 | 765 | ||
| 788 | await waitForGitRefExists( | 766 | await waitForGitRefExists( |
| ... | @@ -807,8 +785,6 @@ describe("CheckpointReactor", () => { | ... | @@ -807,8 +785,6 @@ describe("CheckpointReactor", () => { |
| 807 | threadId: ThreadId.makeUnsafe("thread-1"), | 785 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 808 | status: "ready", | 786 | status: "ready", |
| 809 | providerName: "codex", | 787 | providerName: "codex", |
| 810 | providerSessionId: asSessionId("sess-1"), | ||
| 811 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 812 | runtimeMode: "approval-required", | 788 | runtimeMode: "approval-required", |
| 813 | activeTurnId: null, | 789 | activeTurnId: null, |
| 814 | lastError: null, | 790 | lastError: null, |
| ... | @@ -865,7 +841,7 @@ describe("CheckpointReactor", () => { | ... | @@ -865,7 +841,7 @@ describe("CheckpointReactor", () => { |
| 865 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); | 841 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); |
| 866 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); | 842 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); |
| 867 | expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ | 843 | expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ |
| 868 | sessionId: asSessionId("sess-1"), | 844 | |
| 869 | numTurns: 1, | 845 | numTurns: 1, |
| 870 | }); | 846 | }); |
| 871 | expect(fs.readFileSync(path.join(harness.cwd, "README.md"), "utf8")).toBe("v2\n"); | 847 | expect(fs.readFileSync(path.join(harness.cwd, "README.md"), "utf8")).toBe("v2\n"); |
| ... | @@ -887,8 +863,6 @@ describe("CheckpointReactor", () => { | ... | @@ -887,8 +863,6 @@ describe("CheckpointReactor", () => { |
| 887 | threadId: ThreadId.makeUnsafe("thread-1"), | 863 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 888 | status: "ready", | 864 | status: "ready", |
| 889 | providerName: "claudeCode", | 865 | providerName: "claudeCode", |
| 890 | providerSessionId: asSessionId("sess-1"), | ||
| 891 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 892 | runtimeMode: "approval-required", | 866 | runtimeMode: "approval-required", |
| 893 | activeTurnId: null, | 867 | activeTurnId: null, |
| 894 | lastError: null, | 868 | lastError: null, |
| ... | @@ -940,7 +914,7 @@ describe("CheckpointReactor", () => { | ... | @@ -940,7 +914,7 @@ describe("CheckpointReactor", () => { |
| 940 | await waitForEvent(harness.engine, (event) => event.type === "thread.reverted"); | 914 | await waitForEvent(harness.engine, (event) => event.type === "thread.reverted"); |
| 941 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); | 915 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); |
| 942 | expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ | 916 | expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ |
| 943 | sessionId: asSessionId("sess-1"), | 917 | |
| 944 | numTurns: 1, | 918 | numTurns: 1, |
| 945 | }); | 919 | }); |
| 946 | }); | 920 | }); |
| ... | @@ -958,8 +932,6 @@ describe("CheckpointReactor", () => { | ... | @@ -958,8 +932,6 @@ describe("CheckpointReactor", () => { |
| 958 | threadId: ThreadId.makeUnsafe("thread-1"), | 932 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 959 | status: "ready", | 933 | status: "ready", |
| 960 | providerName: "codex", | 934 | providerName: "codex", |
| 961 | providerSessionId: asSessionId("sess-1"), | ||
| 962 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 963 | runtimeMode: "approval-required", | 935 | runtimeMode: "approval-required", |
| 964 | activeTurnId: null, | 936 | activeTurnId: null, |
| 965 | lastError: null, | 937 | lastError: null, |
| ... | @@ -1032,11 +1004,11 @@ describe("CheckpointReactor", () => { | ... | @@ -1032,11 +1004,11 @@ describe("CheckpointReactor", () => { |
| 1032 | 1004 | ||
| 1033 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(2); | 1005 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(2); |
| 1034 | expect(harness.provider.rollbackConversation.mock.calls[0]?.[0]).toEqual({ | 1006 | expect(harness.provider.rollbackConversation.mock.calls[0]?.[0]).toEqual({ |
| 1035 | sessionId: asSessionId("sess-1"), | 1007 | |
| 1036 | numTurns: 1, | 1008 | numTurns: 1, |
| 1037 | }); | 1009 | }); |
| 1038 | expect(harness.provider.rollbackConversation.mock.calls[1]?.[0]).toEqual({ | 1010 | expect(harness.provider.rollbackConversation.mock.calls[1]?.[0]).toEqual({ |
| 1039 | sessionId: asSessionId("sess-1"), | 1011 | |
| 1040 | numTurns: 1, | 1012 | numTurns: 1, |
| 1041 | }); | 1013 | }); |
| 1042 | }); | 1014 | }); |
apps/server/src/orchestration/Layers/CheckpointReactor.ts+27-71| ... | @@ -2,13 +2,10 @@ import { | ... | @@ -2,13 +2,10 @@ import { |
| 2 | CommandId, | 2 | CommandId, |
| 3 | EventId, | 3 | EventId, |
| 4 | MessageId, | 4 | MessageId, |
| 5 | ProviderSessionId, | ||
| 6 | ProviderThreadId, | ||
| 7 | ThreadId, | 5 | ThreadId, |
| 8 | TurnId, | 6 | TurnId, |
| 9 | type OrchestrationEvent, | 7 | type OrchestrationEvent, |
| 10 | type ProviderRuntimeEvent, | 8 | type ProviderRuntimeEvent, |
| 11 | RuntimeSessionId, | ||
| 12 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 13 | import { Cause, Effect, Layer, Option, Queue, Stream } from "effect"; | 10 | import { Cause, Effect, Layer, Option, Queue, Stream } from "effect"; |
| 14 | 11 | ||
| ... | @@ -38,14 +35,6 @@ function toTurnId(value: string | undefined): TurnId | null { | ... | @@ -38,14 +35,6 @@ function toTurnId(value: string | undefined): TurnId | null { |
| 38 | return value === undefined ? null : TurnId.makeUnsafe(String(value)); | 35 | return value === undefined ? null : TurnId.makeUnsafe(String(value)); |
| 39 | } | 36 | } |
| 40 | 37 | ||
| 41 | function toProviderThreadId(value: string | undefined): ProviderThreadId | null { | ||
| 42 | return value === undefined ? null : ProviderThreadId.makeUnsafe(String(value)); | ||
| 43 | } | ||
| 44 | |||
| 45 | function toProviderSessionId(value: RuntimeSessionId): ProviderSessionId { | ||
| 46 | return ProviderSessionId.makeUnsafe(String(value)); | ||
| 47 | } | ||
| 48 | |||
| 49 | function sameId(left: string | null | undefined, right: string | null | undefined): boolean { | 38 | function sameId(left: string | null | undefined, right: string | null | undefined): boolean { |
| 50 | if (left === null || left === undefined || right === null || right === undefined) { | 39 | if (left === null || left === undefined || right === null || right === undefined) { |
| 51 | return false; | 40 | return false; |
| ... | @@ -100,107 +89,75 @@ const make = Effect.gen(function* () { | ... | @@ -100,107 +89,75 @@ const make = Effect.gen(function* () { |
| 100 | }); | 89 | }); |
| 101 | 90 | ||
| 102 | const appendCaptureFailureActivity = (input: { | 91 | const appendCaptureFailureActivity = (input: { |
| 103 | readonly sessionId: ProviderSessionId; | 92 | readonly threadId: ThreadId; |
| 104 | readonly turnId: TurnId | null; | 93 | readonly turnId: TurnId | null; |
| 105 | readonly detail: string; | 94 | readonly detail: string; |
| 106 | readonly createdAt: string; | 95 | readonly createdAt: string; |
| 107 | }) => | 96 | }) => |
| 108 | Effect.gen(function* () { | 97 | orchestrationEngine.dispatch({ |
| 109 | const readModel = yield* orchestrationEngine.getReadModel(); | 98 | type: "thread.activity.append", |
| 110 | const thread = readModel.threads.find( | 99 | commandId: serverCommandId("checkpoint-capture-failure"), |
| 111 | (entry) => entry.session?.providerSessionId === input.sessionId, | 100 | threadId: input.threadId, |
| 112 | ); | 101 | activity: { |
| 113 | if (!thread) { | 102 | id: EventId.makeUnsafe(crypto.randomUUID()), |
| 114 | return; | 103 | tone: "error", |
| 115 | } | 104 | kind: "checkpoint.capture.failed", |
| 116 | yield* orchestrationEngine.dispatch({ | 105 | summary: "Checkpoint capture failed", |
| 117 | type: "thread.activity.append", | 106 | payload: { |
| 118 | commandId: serverCommandId("checkpoint-capture-failure"), | 107 | detail: input.detail, |
| 119 | threadId: thread.id, | ||
| 120 | activity: { | ||
| 121 | id: EventId.makeUnsafe(crypto.randomUUID()), | ||
| 122 | tone: "error", | ||
| 123 | kind: "checkpoint.capture.failed", | ||
| 124 | summary: "Checkpoint capture failed", | ||
| 125 | payload: { | ||
| 126 | detail: input.detail, | ||
| 127 | }, | ||
| 128 | turnId: input.turnId, | ||
| 129 | createdAt: input.createdAt, | ||
| 130 | }, | 108 | }, |
| 109 | turnId: input.turnId, | ||
| 131 | createdAt: input.createdAt, | 110 | createdAt: input.createdAt, |
| 132 | }); | 111 | }, |
| 112 | createdAt: input.createdAt, | ||
| 133 | }); | 113 | }); |
| 134 | 114 | ||
| 135 | const resolveSessionRuntimeForThread = Effect.fnUntraced(function* ( | 115 | const resolveSessionRuntimeForThread = Effect.fnUntraced(function* ( |
| 136 | threadId: ThreadId, | 116 | threadId: ThreadId, |
| 137 | ): Effect.fn.Return< | 117 | ): Effect.fn.Return< |
| 138 | Option.Option<{ readonly sessionId: ProviderSessionId; readonly cwd: string }> | 118 | Option.Option<{ readonly threadId: ThreadId; readonly cwd: string }> |
| 139 | > { | 119 | > { |
| 140 | const readModel = yield* orchestrationEngine.getReadModel(); | 120 | const readModel = yield* orchestrationEngine.getReadModel(); |
| 141 | const thread = readModel.threads.find((entry) => entry.id === threadId); | 121 | const thread = readModel.threads.find((entry) => entry.id === threadId); |
| 142 | const projectedSessionId = thread?.session?.providerSessionId ?? null; | ||
| 143 | const projectedProviderThreadId = thread?.session?.providerThreadId ?? undefined; | ||
| 144 | 122 | ||
| 145 | const sessions = yield* providerService.listSessions(); | 123 | const sessions = yield* providerService.listSessions(); |
| 146 | 124 | ||
| 147 | const findSessionWithCwd = ( | 125 | const findSessionWithCwd = ( |
| 148 | session: (typeof sessions)[number] | undefined, | 126 | session: (typeof sessions)[number] | undefined, |
| 149 | ): Option.Option<{ readonly sessionId: ProviderSessionId; readonly cwd: string }> => { | 127 | ): Option.Option<{ readonly threadId: ThreadId; readonly cwd: string }> => { |
| 150 | if (!session?.cwd) { | 128 | if (!session?.cwd) { |
| 151 | return Option.none(); | 129 | return Option.none(); |
| 152 | } | 130 | } |
| 153 | return Option.some({ sessionId: session.sessionId, cwd: session.cwd }); | 131 | return Option.some({ threadId: session.threadId, cwd: session.cwd }); |
| 154 | }; | 132 | }; |
| 155 | 133 | ||
| 156 | if (projectedSessionId !== null) { | 134 | if (thread) { |
| 157 | const projectedSession = sessions.find((session) => session.sessionId === projectedSessionId); | 135 | const projectedSession = sessions.find( |
| 136 | (session) => session.threadId === thread.id, | ||
| 137 | ); | ||
| 158 | const fromProjected = findSessionWithCwd(projectedSession); | 138 | const fromProjected = findSessionWithCwd(projectedSession); |
| 159 | if (Option.isSome(fromProjected)) { | 139 | if (Option.isSome(fromProjected)) { |
| 160 | return fromProjected; | 140 | return fromProjected; |
| 161 | } | 141 | } |
| 162 | } | 142 | } |
| 163 | 143 | ||
| 164 | if (projectedProviderThreadId) { | ||
| 165 | const matchedSession = sessions.find( | ||
| 166 | (session) => session.threadId === projectedProviderThreadId, | ||
| 167 | ); | ||
| 168 | const fromProviderThread = findSessionWithCwd(matchedSession); | ||
| 169 | if (Option.isSome(fromProviderThread)) { | ||
| 170 | return fromProviderThread; | ||
| 171 | } | ||
| 172 | } | ||
| 173 | |||
| 174 | return Option.none(); | 144 | return Option.none(); |
| 175 | }); | 145 | }); |
| 176 | 146 | ||
| 177 | const captureCheckpointFromTurnCompletion = Effect.fnUntraced(function* ( | 147 | const captureCheckpointFromTurnCompletion = Effect.fnUntraced(function* ( |
| 178 | event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>, | 148 | event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>, |
| 179 | ) { | 149 | ) { |
| 180 | const providerSessionId = toProviderSessionId(event.sessionId); | ||
| 181 | const turnId = toTurnId(event.turnId); | 150 | const turnId = toTurnId(event.turnId); |
| 182 | if (!turnId) { | 151 | if (!turnId) { |
| 183 | return; | 152 | return; |
| 184 | } | 153 | } |
| 185 | 154 | ||
| 186 | const readModel = yield* orchestrationEngine.getReadModel(); | 155 | const readModel = yield* orchestrationEngine.getReadModel(); |
| 187 | const thread = readModel.threads.find( | 156 | const thread = readModel.threads.find((entry) => entry.id === event.threadId); |
| 188 | (entry) => entry.session?.providerSessionId === providerSessionId, | ||
| 189 | ); | ||
| 190 | if (!thread) { | 157 | if (!thread) { |
| 191 | return; | 158 | return; |
| 192 | } | 159 | } |
| 193 | 160 | ||
| 194 | const projectedProviderThreadId = thread.session?.providerThreadId ?? null; | ||
| 195 | const eventProviderThreadId = toProviderThreadId(event.threadId); | ||
| 196 | if ( | ||
| 197 | projectedProviderThreadId !== null && | ||
| 198 | eventProviderThreadId !== null && | ||
| 199 | !sameId(projectedProviderThreadId, eventProviderThreadId) | ||
| 200 | ) { | ||
| 201 | return; | ||
| 202 | } | ||
| 203 | |||
| 204 | // When a primary turn is active, only that turn may produce completion checkpoints. | 161 | // When a primary turn is active, only that turn may produce completion checkpoints. |
| 205 | if (thread.session?.activeTurnId && !sameId(thread.session.activeTurnId, turnId)) { | 162 | if (thread.session?.activeTurnId && !sameId(thread.session.activeTurnId, turnId)) { |
| 206 | return; | 163 | return; |
| ... | @@ -272,7 +229,7 @@ const make = Effect.gen(function* () { | ... | @@ -272,7 +229,7 @@ const make = Effect.gen(function* () { |
| 272 | ), | 229 | ), |
| 273 | Effect.tapError((error) => | 230 | Effect.tapError((error) => |
| 274 | appendCaptureFailureActivity({ | 231 | appendCaptureFailureActivity({ |
| 275 | sessionId: providerSessionId, | 232 | threadId: thread.id, |
| 276 | turnId, | 233 | turnId, |
| 277 | detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`, | 234 | detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`, |
| 278 | createdAt: event.createdAt, | 235 | createdAt: event.createdAt, |
| ... | @@ -332,7 +289,6 @@ const make = Effect.gen(function* () { | ... | @@ -332,7 +289,6 @@ const make = Effect.gen(function* () { |
| 332 | const ensurePreTurnBaselineFromTurnStart = Effect.fnUntraced(function* ( | 289 | const ensurePreTurnBaselineFromTurnStart = Effect.fnUntraced(function* ( |
| 333 | event: Extract<ProviderRuntimeEvent, { type: "turn.started" }>, | 290 | event: Extract<ProviderRuntimeEvent, { type: "turn.started" }>, |
| 334 | ) { | 291 | ) { |
| 335 | const providerSessionId = toProviderSessionId(event.sessionId); | ||
| 336 | const turnId = toTurnId(event.turnId); | 292 | const turnId = toTurnId(event.turnId); |
| 337 | if (!turnId) { | 293 | if (!turnId) { |
| 338 | return; | 294 | return; |
| ... | @@ -340,7 +296,7 @@ const make = Effect.gen(function* () { | ... | @@ -340,7 +296,7 @@ const make = Effect.gen(function* () { |
| 340 | 296 | ||
| 341 | const readModel = yield* orchestrationEngine.getReadModel(); | 297 | const readModel = yield* orchestrationEngine.getReadModel(); |
| 342 | const thread = readModel.threads.find( | 298 | const thread = readModel.threads.find( |
| 343 | (entry) => entry.session?.providerSessionId === providerSessionId, | 299 | (entry) => entry.id === event.threadId, |
| 344 | ); | 300 | ); |
| 345 | if (!thread) { | 301 | if (!thread) { |
| 346 | return; | 302 | return; |
| ... | @@ -520,7 +476,7 @@ const make = Effect.gen(function* () { | ... | @@ -520,7 +476,7 @@ const make = Effect.gen(function* () { |
| 520 | const rolledBackTurns = Math.max(0, currentTurnCount - event.payload.turnCount); | 476 | const rolledBackTurns = Math.max(0, currentTurnCount - event.payload.turnCount); |
| 521 | if (rolledBackTurns > 0) { | 477 | if (rolledBackTurns > 0) { |
| 522 | yield* providerService.rollbackConversation({ | 478 | yield* providerService.rollbackConversation({ |
| 523 | sessionId: sessionRuntime.value.sessionId, | 479 | threadId: sessionRuntime.value.threadId, |
| 524 | numTurns: rolledBackTurns, | 480 | numTurns: rolledBackTurns, |
| 525 | }); | 481 | }); |
| 526 | } | 482 | } |
| ... | @@ -588,7 +544,7 @@ const make = Effect.gen(function* () { | ... | @@ -588,7 +544,7 @@ const make = Effect.gen(function* () { |
| 588 | yield* captureCheckpointFromTurnCompletion(event).pipe( | 544 | yield* captureCheckpointFromTurnCompletion(event).pipe( |
| 589 | Effect.catch((error) => | 545 | Effect.catch((error) => |
| 590 | appendCaptureFailureActivity({ | 546 | appendCaptureFailureActivity({ |
| 591 | sessionId: toProviderSessionId(event.sessionId), | 547 | threadId: event.threadId, |
| 592 | turnId, | 548 | turnId, |
| 593 | detail: error.message, | 549 | detail: error.message, |
| 594 | createdAt: new Date().toISOString(), | 550 | createdAt: new Date().toISOString(), |
apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+1-8| ... | @@ -4,8 +4,6 @@ import { | ... | @@ -4,8 +4,6 @@ import { |
| 4 | CorrelationId, | 4 | CorrelationId, |
| 5 | EventId, | 5 | EventId, |
| 6 | MessageId, | 6 | MessageId, |
| 7 | ProviderSessionId, | ||
| 8 | ProviderThreadId, | ||
| 9 | ProjectId, | 7 | ProjectId, |
| 10 | ThreadId, | 8 | ThreadId, |
| 11 | TurnId, | 9 | TurnId, |
| ... | @@ -1771,18 +1769,13 @@ it.effect("restores pending turn-start metadata across projection pipeline resta | ... | @@ -1771,18 +1769,13 @@ it.effect("restores pending turn-start metadata across projection pipeline resta |
| 1771 | commandId: CommandId.makeUnsafe("cmd-restart-2"), | 1769 | commandId: CommandId.makeUnsafe("cmd-restart-2"), |
| 1772 | causationEventId: null, | 1770 | causationEventId: null, |
| 1773 | correlationId: CorrelationId.makeUnsafe("cmd-restart-2"), | 1771 | correlationId: CorrelationId.makeUnsafe("cmd-restart-2"), |
| 1774 | metadata: { | 1772 | metadata: {}, |
| 1775 | providerSessionId: ProviderSessionId.makeUnsafe("provider-session-restart"), | ||
| 1776 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-restart"), | ||
| 1777 | }, | ||
| 1778 | payload: { | 1773 | payload: { |
| 1779 | threadId, | 1774 | threadId, |
| 1780 | session: { | 1775 | session: { |
| 1781 | threadId, | 1776 | threadId, |
| 1782 | status: "running", | 1777 | status: "running", |
| 1783 | providerName: "codex", | 1778 | providerName: "codex", |
| 1784 | providerSessionId: ProviderSessionId.makeUnsafe("provider-session-restart"), | ||
| 1785 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-restart"), | ||
| 1786 | runtimeMode: "approval-required", | 1779 | runtimeMode: "approval-required", |
| 1787 | activeTurnId: turnId, | 1780 | activeTurnId: turnId, |
| 1788 | lastError: null, | 1781 | lastError: null, |
apps/server/src/orchestration/Layers/ProjectionPipeline.ts-2| ... | @@ -665,8 +665,6 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { | ... | @@ -665,8 +665,6 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { |
| 665 | threadId: event.payload.threadId, | 665 | threadId: event.payload.threadId, |
| 666 | status: event.payload.session.status, | 666 | status: event.payload.session.status, |
| 667 | providerName: event.payload.session.providerName, | 667 | providerName: event.payload.session.providerName, |
| 668 | providerSessionId: event.payload.session.providerSessionId, | ||
| 669 | providerThreadId: event.payload.session.providerThreadId, | ||
| 670 | runtimeMode: event.payload.session.runtimeMode, | 668 | runtimeMode: event.payload.session.runtimeMode, |
| 671 | activeTurnId: event.payload.session.activeTurnId, | 669 | activeTurnId: event.payload.session.activeTurnId, |
| 672 | lastError: event.payload.session.lastError, | 670 | lastError: event.payload.session.lastError, |
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts-7| ... | @@ -3,8 +3,6 @@ import { | ... | @@ -3,8 +3,6 @@ import { |
| 3 | EventId, | 3 | EventId, |
| 4 | MessageId, | 4 | MessageId, |
| 5 | ProjectId, | 5 | ProjectId, |
| 6 | ProviderSessionId, | ||
| 7 | ProviderThreadId, | ||
| 8 | ThreadId, | 6 | ThreadId, |
| 9 | TurnId, | 7 | TurnId, |
| 10 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| ... | @@ -22,9 +20,6 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); | ... | @@ -22,9 +20,6 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 22 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); | 20 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); |
| 23 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); | 21 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 24 | const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.makeUnsafe(value); | 22 | const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.makeUnsafe(value); |
| 25 | const asProviderSessionId = (value: string): ProviderSessionId => | ||
| 26 | ProviderSessionId.makeUnsafe(value); | ||
| 27 | const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId.makeUnsafe(value); | ||
| 28 | 23 | ||
| 29 | const projectionSnapshotLayer = it.layer( | 24 | const projectionSnapshotLayer = it.layer( |
| 30 | OrchestrationProjectionSnapshotQueryLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)), | 25 | OrchestrationProjectionSnapshotQueryLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)), |
| ... | @@ -290,8 +285,6 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { | ... | @@ -290,8 +285,6 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { |
| 290 | threadId: ThreadId.makeUnsafe("thread-1"), | 285 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 291 | status: "running", | 286 | status: "running", |
| 292 | providerName: "codex", | 287 | providerName: "codex", |
| 293 | providerSessionId: asProviderSessionId("provider-session-1"), | ||
| 294 | providerThreadId: asProviderThreadId("provider-thread-1"), | ||
| 295 | runtimeMode: "approval-required", | 288 | runtimeMode: "approval-required", |
| 296 | activeTurnId: asTurnId("turn-1"), | 289 | activeTurnId: asTurnId("turn-1"), |
| 297 | lastError: null, | 290 | lastError: null, |
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts-2| ... | @@ -461,8 +461,6 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -461,8 +461,6 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 461 | threadId: row.threadId, | 461 | threadId: row.threadId, |
| 462 | status: row.status, | 462 | status: row.status, |
| 463 | providerName: row.providerName, | 463 | providerName: row.providerName, |
| 464 | providerSessionId: row.providerSessionId, | ||
| 465 | providerThreadId: row.providerThreadId, | ||
| 466 | runtimeMode: row.runtimeMode, | 464 | runtimeMode: row.runtimeMode, |
| 467 | activeTurnId: row.activeTurnId, | 465 | activeTurnId: row.activeTurnId, |
| 468 | lastError: row.lastError, | 466 | lastError: row.lastError, |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+31-39| ... | @@ -9,9 +9,6 @@ import { | ... | @@ -9,9 +9,6 @@ import { |
| 9 | EventId, | 9 | EventId, |
| 10 | MessageId, | 10 | MessageId, |
| 11 | ProjectId, | 11 | ProjectId, |
| 12 | ProviderSessionId, | ||
| 13 | ProviderThreadId, | ||
| 14 | ProviderTurnId, | ||
| 15 | ThreadId, | 12 | ThreadId, |
| 16 | TurnId, | 13 | TurnId, |
| 17 | } from "@t3tools/contracts"; | 14 | } from "@t3tools/contracts"; |
| ... | @@ -38,8 +35,6 @@ import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts"; | ... | @@ -38,8 +35,6 @@ import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts"; |
| 38 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 35 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 39 | 36 | ||
| 40 | const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); | 37 | const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); |
| 41 | const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value); | ||
| 42 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); | ||
| 43 | const asApprovalRequestId = (value: string): ApprovalRequestId => | 38 | const asApprovalRequestId = (value: string): ApprovalRequestId => |
| 44 | ApprovalRequestId.makeUnsafe(value); | 39 | ApprovalRequestId.makeUnsafe(value); |
| 45 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); | 40 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); |
| ... | @@ -110,8 +105,14 @@ describe("ProviderCommandReactor", () => { | ... | @@ -110,8 +105,14 @@ describe("ProviderCommandReactor", () => { |
| 110 | typeof input === "object" && input !== null && "model" in input && typeof input.model === "string" | 105 | typeof input === "object" && input !== null && "model" in input && typeof input.model === "string" |
| 111 | ? input.model | 106 | ? input.model |
| 112 | : undefined; | 107 | : undefined; |
| 108 | const threadId = | ||
| 109 | typeof input === "object" && | ||
| 110 | input !== null && | ||
| 111 | "threadId" in input && | ||
| 112 | typeof input.threadId === "string" | ||
| 113 | ? ThreadId.makeUnsafe(input.threadId) | ||
| 114 | : ThreadId.makeUnsafe(`thread-${sessionIndex}`); | ||
| 113 | const session: ProviderSession = { | 115 | const session: ProviderSession = { |
| 114 | sessionId: asSessionId(`sess-${sessionIndex}`), | ||
| 115 | provider, | 116 | provider, |
| 116 | status: "ready" as const, | 117 | status: "ready" as const, |
| 117 | runtimeMode: | 118 | runtimeMode: |
| ... | @@ -122,7 +123,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -122,7 +123,7 @@ describe("ProviderCommandReactor", () => { |
| 122 | ? input.runtimeMode | 123 | ? input.runtimeMode |
| 123 | : "full-access", | 124 | : "full-access", |
| 124 | ...(model !== undefined ? { model } : {}), | 125 | ...(model !== undefined ? { model } : {}), |
| 125 | threadId: ProviderThreadId.makeUnsafe(`provider-thread-${sessionIndex}`), | 126 | threadId, |
| 126 | resumeCursor: resumeCursor ?? { opaque: `cursor-${sessionIndex}` }, | 127 | resumeCursor: resumeCursor ?? { opaque: `cursor-${sessionIndex}` }, |
| 127 | createdAt: now, | 128 | createdAt: now, |
| 128 | updatedAt: now, | 129 | updatedAt: now, |
| ... | @@ -132,22 +133,22 @@ describe("ProviderCommandReactor", () => { | ... | @@ -132,22 +133,22 @@ describe("ProviderCommandReactor", () => { |
| 132 | }); | 133 | }); |
| 133 | const sendTurn = vi.fn((_: unknown) => | 134 | const sendTurn = vi.fn((_: unknown) => |
| 134 | Effect.succeed({ | 135 | Effect.succeed({ |
| 135 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 136 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 136 | turnId: asProviderTurnId("provider-turn-1"), | 137 | turnId: asTurnId("turn-1"), |
| 137 | }), | 138 | }), |
| 138 | ); | 139 | ); |
| 139 | const interruptTurn = vi.fn((_: unknown) => Effect.void); | 140 | const interruptTurn = vi.fn((_: unknown) => Effect.void); |
| 140 | const respondToRequest = vi.fn<ProviderServiceShape["respondToRequest"]>(() => Effect.void); | 141 | const respondToRequest = vi.fn<ProviderServiceShape["respondToRequest"]>(() => Effect.void); |
| 141 | const stopSession = vi.fn((input: unknown) => | 142 | const stopSession = vi.fn((input: unknown) => |
| 142 | Effect.sync(() => { | 143 | Effect.sync(() => { |
| 143 | const sessionId = | 144 | const threadId = |
| 144 | typeof input === "object" && input !== null && "sessionId" in input | 145 | typeof input === "object" && input !== null && "threadId" in input |
| 145 | ? (input as { sessionId?: ProviderSessionId }).sessionId | 146 | ? (input as { threadId?: ThreadId }).threadId |
| 146 | : undefined; | 147 | : undefined; |
| 147 | if (!sessionId) { | 148 | if (!threadId) { |
| 148 | return; | 149 | return; |
| 149 | } | 150 | } |
| 150 | const index = runtimeSessions.findIndex((session) => session.sessionId === sessionId); | 151 | const index = runtimeSessions.findIndex((session) => session.threadId === threadId); |
| 151 | if (index >= 0) { | 152 | if (index >= 0) { |
| 152 | runtimeSessions.splice(index, 1); | 153 | runtimeSessions.splice(index, 1); |
| 153 | } | 154 | } |
| ... | @@ -284,7 +285,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -284,7 +285,7 @@ describe("ProviderCommandReactor", () => { |
| 284 | 285 | ||
| 285 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); | 286 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); |
| 286 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | 287 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); |
| 287 | expect(thread?.session?.providerSessionId).toBe("sess-1"); | 288 | expect(thread?.session?.threadId).toBe("thread-1"); |
| 288 | expect(thread?.session?.runtimeMode).toBe("approval-required"); | 289 | expect(thread?.session?.runtimeMode).toBe("approval-required"); |
| 289 | }); | 290 | }); |
| 290 | 291 | ||
| ... | @@ -321,7 +322,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -321,7 +322,7 @@ describe("ProviderCommandReactor", () => { |
| 321 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); | 322 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); |
| 322 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | 323 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); |
| 323 | expect(thread?.session?.providerName).toBe("claudeCode"); | 324 | expect(thread?.session?.providerName).toBe("claudeCode"); |
| 324 | expect(thread?.session?.providerSessionId).toBe("sess-1"); | 325 | expect(thread?.session?.threadId).toBe("thread-1"); |
| 325 | }); | 326 | }); |
| 326 | 327 | ||
| 327 | it("starts first turn with cursor provider when provider is specified", async () => { | 328 | it("starts first turn with cursor provider when provider is specified", async () => { |
| ... | @@ -357,7 +358,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -357,7 +358,7 @@ describe("ProviderCommandReactor", () => { |
| 357 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); | 358 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); |
| 358 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | 359 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); |
| 359 | expect(thread?.session?.providerName).toBe("cursor"); | 360 | expect(thread?.session?.providerName).toBe("cursor"); |
| 360 | expect(thread?.session?.providerSessionId).toBe("sess-1"); | 361 | expect(thread?.session?.threadId).toBe("thread-1"); |
| 361 | }); | 362 | }); |
| 362 | 363 | ||
| 363 | it("reuses the same provider session when runtime mode is unchanged", async () => { | 364 | it("reuses the same provider session when runtime mode is unchanged", async () => { |
| ... | @@ -500,7 +501,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -500,7 +501,7 @@ describe("ProviderCommandReactor", () => { |
| 500 | expect(harness.stopSession.mock.calls.length).toBe(0); | 501 | expect(harness.stopSession.mock.calls.length).toBe(0); |
| 501 | expect(harness.startSession.mock.calls.length).toBe(1); | 502 | expect(harness.startSession.mock.calls.length).toBe(1); |
| 502 | expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({ | 503 | expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({ |
| 503 | sessionId: asSessionId("sess-1"), | 504 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 504 | model: "gpt-5.3-codex", | 505 | model: "gpt-5.3-codex", |
| 505 | }); | 506 | }); |
| 506 | }); | 507 | }); |
| ... | @@ -554,8 +555,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -554,8 +555,6 @@ describe("ProviderCommandReactor", () => { |
| 554 | return thread?.runtimeMode === "approval-required"; | 555 | return thread?.runtimeMode === "approval-required"; |
| 555 | }); | 556 | }); |
| 556 | await waitFor(() => harness.startSession.mock.calls.length === 2); | 557 | await waitFor(() => harness.startSession.mock.calls.length === 2); |
| 557 | await waitFor(() => harness.stopSession.mock.calls.length === 1); | ||
| 558 | |||
| 559 | await Effect.runPromise( | 558 | await Effect.runPromise( |
| 560 | harness.engine.dispatch({ | 559 | harness.engine.dispatch({ |
| 561 | type: "thread.turn.start", | 560 | type: "thread.turn.start", |
| ... | @@ -574,18 +573,19 @@ describe("ProviderCommandReactor", () => { | ... | @@ -574,18 +573,19 @@ describe("ProviderCommandReactor", () => { |
| 574 | 573 | ||
| 575 | await waitFor(() => harness.sendTurn.mock.calls.length === 2); | 574 | await waitFor(() => harness.sendTurn.mock.calls.length === 2); |
| 576 | 575 | ||
| 577 | expect(harness.stopSession.mock.calls[0]?.[0]).toEqual({ sessionId: asSessionId("sess-1") }); | 576 | expect(harness.stopSession.mock.calls.length).toBe(0); |
| 578 | expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ | 577 | expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ |
| 578 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 579 | resumeCursor: { opaque: "cursor-1" }, | 579 | resumeCursor: { opaque: "cursor-1" }, |
| 580 | runtimeMode: "approval-required", | 580 | runtimeMode: "approval-required", |
| 581 | }); | 581 | }); |
| 582 | expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({ | 582 | expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({ |
| 583 | sessionId: asSessionId("sess-2"), | 583 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 584 | }); | 584 | }); |
| 585 | 585 | ||
| 586 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); | 586 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); |
| 587 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | 587 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); |
| 588 | expect(thread?.session?.providerSessionId).toBe("sess-2"); | 588 | expect(thread?.session?.threadId).toBe("thread-1"); |
| 589 | expect(thread?.session?.runtimeMode).toBe("approval-required"); | 589 | expect(thread?.session?.runtimeMode).toBe("approval-required"); |
| 590 | }); | 590 | }); |
| 591 | 591 | ||
| ... | @@ -629,12 +629,12 @@ describe("ProviderCommandReactor", () => { | ... | @@ -629,12 +629,12 @@ describe("ProviderCommandReactor", () => { |
| 629 | }), | 629 | }), |
| 630 | ); | 630 | ); |
| 631 | 631 | ||
| 632 | await waitFor(() => harness.stopSession.mock.calls.length === 1); | ||
| 633 | await waitFor(() => harness.startSession.mock.calls.length === 2); | 632 | await waitFor(() => harness.startSession.mock.calls.length === 2); |
| 634 | await waitFor(() => harness.sendTurn.mock.calls.length === 2); | 633 | await waitFor(() => harness.sendTurn.mock.calls.length === 2); |
| 635 | 634 | ||
| 636 | expect(harness.stopSession.mock.calls[0]?.[0]).toEqual({ sessionId: asSessionId("sess-1") }); | 635 | expect(harness.stopSession.mock.calls.length).toBe(0); |
| 637 | expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ | 636 | expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ |
| 637 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 638 | provider: "claudeCode", | 638 | provider: "claudeCode", |
| 639 | runtimeMode: "approval-required", | 639 | runtimeMode: "approval-required", |
| 640 | }); | 640 | }); |
| ... | @@ -642,7 +642,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -642,7 +642,7 @@ describe("ProviderCommandReactor", () => { |
| 642 | 642 | ||
| 643 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); | 643 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); |
| 644 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | 644 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); |
| 645 | expect(thread?.session?.providerSessionId).toBe("sess-2"); | 645 | expect(thread?.session?.threadId).toBe("thread-1"); |
| 646 | expect(thread?.session?.providerName).toBe("claudeCode"); | 646 | expect(thread?.session?.providerName).toBe("claudeCode"); |
| 647 | expect(thread?.session?.runtimeMode).toBe("approval-required"); | 647 | expect(thread?.session?.runtimeMode).toBe("approval-required"); |
| 648 | }); | 648 | }); |
| ... | @@ -707,7 +707,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -707,7 +707,7 @@ describe("ProviderCommandReactor", () => { |
| 707 | 707 | ||
| 708 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); | 708 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); |
| 709 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | 709 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); |
| 710 | expect(thread?.session?.providerSessionId).toBe("sess-1"); | 710 | expect(thread?.session?.threadId).toBe("thread-1"); |
| 711 | expect(thread?.session?.runtimeMode).toBe("full-access"); | 711 | expect(thread?.session?.runtimeMode).toBe("full-access"); |
| 712 | }); | 712 | }); |
| 713 | 713 | ||
| ... | @@ -724,8 +724,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -724,8 +724,6 @@ describe("ProviderCommandReactor", () => { |
| 724 | threadId: ThreadId.makeUnsafe("thread-1"), | 724 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 725 | status: "running", | 725 | status: "running", |
| 726 | providerName: "codex", | 726 | providerName: "codex", |
| 727 | providerSessionId: asSessionId("sess-1"), | ||
| 728 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 729 | runtimeMode: "approval-required", | 727 | runtimeMode: "approval-required", |
| 730 | activeTurnId: asTurnId("turn-1"), | 728 | activeTurnId: asTurnId("turn-1"), |
| 731 | lastError: null, | 729 | lastError: null, |
| ... | @@ -747,7 +745,8 @@ describe("ProviderCommandReactor", () => { | ... | @@ -747,7 +745,8 @@ describe("ProviderCommandReactor", () => { |
| 747 | 745 | ||
| 748 | await waitFor(() => harness.interruptTurn.mock.calls.length === 1); | 746 | await waitFor(() => harness.interruptTurn.mock.calls.length === 1); |
| 749 | expect(harness.interruptTurn.mock.calls[0]?.[0]).toEqual({ | 747 | expect(harness.interruptTurn.mock.calls[0]?.[0]).toEqual({ |
| 750 | sessionId: "sess-1", | 748 | threadId: "thread-1", |
| 749 | turnId: "turn-1", | ||
| 751 | }); | 750 | }); |
| 752 | }); | 751 | }); |
| 753 | 752 | ||
| ... | @@ -764,8 +763,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -764,8 +763,6 @@ describe("ProviderCommandReactor", () => { |
| 764 | threadId: ThreadId.makeUnsafe("thread-1"), | 763 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 765 | status: "running", | 764 | status: "running", |
| 766 | providerName: "codex", | 765 | providerName: "codex", |
| 767 | providerSessionId: asSessionId("sess-1"), | ||
| 768 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 769 | runtimeMode: "approval-required", | 766 | runtimeMode: "approval-required", |
| 770 | activeTurnId: null, | 767 | activeTurnId: null, |
| 771 | lastError: null, | 768 | lastError: null, |
| ... | @@ -788,7 +785,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -788,7 +785,7 @@ describe("ProviderCommandReactor", () => { |
| 788 | 785 | ||
| 789 | await waitFor(() => harness.respondToRequest.mock.calls.length === 1); | 786 | await waitFor(() => harness.respondToRequest.mock.calls.length === 1); |
| 790 | expect(harness.respondToRequest.mock.calls[0]?.[0]).toEqual({ | 787 | expect(harness.respondToRequest.mock.calls[0]?.[0]).toEqual({ |
| 791 | sessionId: "sess-1", | 788 | threadId: "thread-1", |
| 792 | requestId: "approval-request-1", | 789 | requestId: "approval-request-1", |
| 793 | decision: "accept", | 790 | decision: "accept", |
| 794 | }); | 791 | }); |
| ... | @@ -816,8 +813,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -816,8 +813,6 @@ describe("ProviderCommandReactor", () => { |
| 816 | threadId: ThreadId.makeUnsafe("thread-1"), | 813 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 817 | status: "running", | 814 | status: "running", |
| 818 | providerName: "cursor", | 815 | providerName: "cursor", |
| 819 | providerSessionId: asSessionId("sess-1"), | ||
| 820 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 821 | runtimeMode: "approval-required", | 816 | runtimeMode: "approval-required", |
| 822 | activeTurnId: null, | 817 | activeTurnId: null, |
| 823 | lastError: null, | 818 | lastError: null, |
| ... | @@ -901,8 +896,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -901,8 +896,6 @@ describe("ProviderCommandReactor", () => { |
| 901 | threadId: ThreadId.makeUnsafe("thread-1"), | 896 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 902 | status: "ready", | 897 | status: "ready", |
| 903 | providerName: "codex", | 898 | providerName: "codex", |
| 904 | providerSessionId: asSessionId("sess-1"), | ||
| 905 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 906 | runtimeMode: "approval-required", | 899 | runtimeMode: "approval-required", |
| 907 | activeTurnId: null, | 900 | activeTurnId: null, |
| 908 | lastError: null, | 901 | lastError: null, |
| ... | @@ -926,8 +919,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -926,8 +919,7 @@ describe("ProviderCommandReactor", () => { |
| 926 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | 919 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); |
| 927 | expect(thread?.session).not.toBeNull(); | 920 | expect(thread?.session).not.toBeNull(); |
| 928 | expect(thread?.session?.status).toBe("stopped"); | 921 | expect(thread?.session?.status).toBe("stopped"); |
| 929 | expect(thread?.session?.providerSessionId).toBeNull(); | 922 | expect(thread?.session?.threadId).toBe("thread-1"); |
| 930 | expect(thread?.session?.providerThreadId).toBeNull(); | ||
| 931 | expect(thread?.session?.activeTurnId).toBeNull(); | 923 | expect(thread?.session?.activeTurnId).toBeNull(); |
| 932 | }); | 924 | }); |
| 933 | }); | 925 | }); |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+26-45| ... | @@ -4,9 +4,8 @@ import { | ... | @@ -4,9 +4,8 @@ import { |
| 4 | EventId, | 4 | EventId, |
| 5 | type OrchestrationEvent, | 5 | type OrchestrationEvent, |
| 6 | type ProviderKind, | 6 | type ProviderKind, |
| 7 | type ProviderSessionId, | ||
| 8 | type OrchestrationSession, | 7 | type OrchestrationSession, |
| 9 | type ThreadId, | 8 | ThreadId, |
| 10 | type ProviderSession, | 9 | type ProviderSession, |
| 11 | type RuntimeMode, | 10 | type RuntimeMode, |
| 12 | type TurnId, | 11 | type TurnId, |
| ... | @@ -220,9 +219,9 @@ const make = Effect.gen(function* () { | ... | @@ -220,9 +219,9 @@ const make = Effect.gen(function* () { |
| 220 | projects: readModel.projects, | 219 | projects: readModel.projects, |
| 221 | }); | 220 | }); |
| 222 | 221 | ||
| 223 | const resolveActiveSession = (sessionId: ProviderSessionId) => | 222 | const resolveActiveSession = (threadId: ThreadId) => |
| 224 | providerService.listSessions().pipe( | 223 | providerService.listSessions().pipe( |
| 225 | Effect.map((sessions) => sessions.find((session) => session.sessionId === sessionId)), | 224 | Effect.map((sessions) => sessions.find((session) => session.threadId === threadId)), |
| 226 | ); | 225 | ); |
| 227 | 226 | ||
| 228 | const startProviderSession = (input?: { | 227 | const startProviderSession = (input?: { |
| ... | @@ -230,6 +229,7 @@ const make = Effect.gen(function* () { | ... | @@ -230,6 +229,7 @@ const make = Effect.gen(function* () { |
| 230 | readonly provider?: ProviderKind; | 229 | readonly provider?: ProviderKind; |
| 231 | }) => | 230 | }) => |
| 232 | providerService.startSession(threadId, { | 231 | providerService.startSession(threadId, { |
| 232 | threadId, | ||
| 233 | ...(input?.provider ?? preferredProvider | 233 | ...(input?.provider ?? preferredProvider |
| 234 | ? { provider: input?.provider ?? preferredProvider } | 234 | ? { provider: input?.provider ?? preferredProvider } |
| 235 | : {}), | 235 | : {}), |
| ... | @@ -246,8 +246,6 @@ const make = Effect.gen(function* () { | ... | @@ -246,8 +246,6 @@ const make = Effect.gen(function* () { |
| 246 | threadId, | 246 | threadId, |
| 247 | status: mapProviderSessionStatusToOrchestrationStatus(session.status), | 247 | status: mapProviderSessionStatusToOrchestrationStatus(session.status), |
| 248 | providerName: session.provider, | 248 | providerName: session.provider, |
| 249 | providerSessionId: session.sessionId, | ||
| 250 | providerThreadId: session.threadId ?? null, | ||
| 251 | runtimeMode: desiredRuntimeMode, | 249 | runtimeMode: desiredRuntimeMode, |
| 252 | // Provider turn ids are not orchestration turn ids. | 250 | // Provider turn ids are not orchestration turn ids. |
| 253 | activeTurnId: null, | 251 | activeTurnId: null, |
| ... | @@ -257,11 +255,12 @@ const make = Effect.gen(function* () { | ... | @@ -257,11 +255,12 @@ const make = Effect.gen(function* () { |
| 257 | createdAt, | 255 | createdAt, |
| 258 | }); | 256 | }); |
| 259 | 257 | ||
| 260 | const existingSessionId = thread.session?.providerSessionId; | 258 | const existingSessionThreadId = |
| 261 | if (existingSessionId) { | 259 | thread.session && thread.session.status !== "stopped" ? thread.id : null; |
| 260 | if (existingSessionThreadId) { | ||
| 262 | const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode; | 261 | const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode; |
| 263 | const providerChanged = options?.provider !== undefined && options.provider !== currentProvider; | 262 | const providerChanged = options?.provider !== undefined && options.provider !== currentProvider; |
| 264 | const activeSession = yield* resolveActiveSession(existingSessionId); | 263 | const activeSession = yield* resolveActiveSession(existingSessionThreadId); |
| 265 | const sessionModelSwitch = | 264 | const sessionModelSwitch = |
| 266 | currentProvider === undefined | 265 | currentProvider === undefined |
| 267 | ? "in-session" | 266 | ? "in-session" |
| ... | @@ -271,12 +270,8 @@ const make = Effect.gen(function* () { | ... | @@ -271,12 +270,8 @@ const make = Effect.gen(function* () { |
| 271 | const shouldRestartForModelChange = | 270 | const shouldRestartForModelChange = |
| 272 | modelChanged && sessionModelSwitch === "restart-session"; | 271 | modelChanged && sessionModelSwitch === "restart-session"; |
| 273 | 272 | ||
| 274 | if ( | 273 | if (!runtimeModeChanged && !providerChanged && !shouldRestartForModelChange) { |
| 275 | !runtimeModeChanged && | 274 | return existingSessionThreadId; |
| 276 | !providerChanged && | ||
| 277 | !shouldRestartForModelChange | ||
| 278 | ) { | ||
| 279 | return existingSessionId; | ||
| 280 | } | 275 | } |
| 281 | 276 | ||
| 282 | const resumeCursor = | 277 | const resumeCursor = |
| ... | @@ -285,7 +280,7 @@ const make = Effect.gen(function* () { | ... | @@ -285,7 +280,7 @@ const make = Effect.gen(function* () { |
| 285 | : (activeSession?.resumeCursor ?? undefined); | 280 | : (activeSession?.resumeCursor ?? undefined); |
| 286 | yield* Effect.logInfo("provider command reactor restarting provider session", { | 281 | yield* Effect.logInfo("provider command reactor restarting provider session", { |
| 287 | threadId, | 282 | threadId, |
| 288 | existingSessionId, | 283 | existingSessionThreadId, |
| 289 | currentProvider, | 284 | currentProvider, |
| 290 | desiredProvider: options?.provider ?? currentProvider, | 285 | desiredProvider: options?.provider ?? currentProvider, |
| 291 | currentRuntimeMode: thread.session?.runtimeMode, | 286 | currentRuntimeMode: thread.session?.runtimeMode, |
| ... | @@ -302,30 +297,20 @@ const make = Effect.gen(function* () { | ... | @@ -302,30 +297,20 @@ const make = Effect.gen(function* () { |
| 302 | }); | 297 | }); |
| 303 | yield* Effect.logInfo("provider command reactor restarted provider session", { | 298 | yield* Effect.logInfo("provider command reactor restarted provider session", { |
| 304 | threadId, | 299 | threadId, |
| 305 | previousSessionId: existingSessionId, | 300 | previousSessionId: existingSessionThreadId, |
| 306 | restartedSessionId: restartedSession.sessionId, | 301 | restartedSessionThreadId: restartedSession.threadId, |
| 307 | provider: restartedSession.provider, | 302 | provider: restartedSession.provider, |
| 308 | providerThreadId: restartedSession.threadId, | ||
| 309 | runtimeMode: restartedSession.runtimeMode, | 303 | runtimeMode: restartedSession.runtimeMode, |
| 310 | }); | 304 | }); |
| 311 | yield* bindSessionToThread(restartedSession); | 305 | yield* bindSessionToThread(restartedSession); |
| 312 | yield* providerService.stopSession({ sessionId: existingSessionId }).pipe( | 306 | return restartedSession.threadId; |
| 313 | Effect.catchCause((cause) => | ||
| 314 | Effect.logWarning("provider command reactor failed to stop superseded provider session", { | ||
| 315 | threadId, | ||
| 316 | sessionId: existingSessionId, | ||
| 317 | cause: Cause.pretty(cause), | ||
| 318 | }), | ||
| 319 | ), | ||
| 320 | ); | ||
| 321 | return restartedSession.sessionId; | ||
| 322 | } | 307 | } |
| 323 | 308 | ||
| 324 | const startedSession = yield* startProviderSession( | 309 | const startedSession = yield* startProviderSession( |
| 325 | options?.provider !== undefined ? { provider: options.provider } : undefined, | 310 | options?.provider !== undefined ? { provider: options.provider } : undefined, |
| 326 | ); | 311 | ); |
| 327 | yield* bindSessionToThread(startedSession); | 312 | yield* bindSessionToThread(startedSession); |
| 328 | return startedSession.sessionId; | 313 | return startedSession.threadId; |
| 329 | }); | 314 | }); |
| 330 | 315 | ||
| 331 | const sendTurnForThread = Effect.fnUntraced(function* (input: { | 316 | const sendTurnForThread = Effect.fnUntraced(function* (input: { |
| ... | @@ -341,14 +326,14 @@ const make = Effect.gen(function* () { | ... | @@ -341,14 +326,14 @@ const make = Effect.gen(function* () { |
| 341 | if (!thread) { | 326 | if (!thread) { |
| 342 | return; | 327 | return; |
| 343 | } | 328 | } |
| 344 | const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, { | 329 | yield* ensureSessionForThread(input.threadId, input.createdAt, { |
| 345 | ...(input.provider !== undefined ? { provider: input.provider } : {}), | 330 | ...(input.provider !== undefined ? { provider: input.provider } : {}), |
| 346 | ...(input.model !== undefined ? { model: input.model } : {}), | 331 | ...(input.model !== undefined ? { model: input.model } : {}), |
| 347 | }); | 332 | }); |
| 348 | const normalizedInput = toNonEmptyProviderInput(input.messageText); | 333 | const normalizedInput = toNonEmptyProviderInput(input.messageText); |
| 349 | const normalizedAttachments = input.attachments ?? []; | 334 | const normalizedAttachments = input.attachments ?? []; |
| 350 | const activeSession = yield* providerService.listSessions().pipe( | 335 | const activeSession = yield* providerService.listSessions().pipe( |
| 351 | Effect.map((sessions) => sessions.find((session) => session.sessionId === sessionId)), | 336 | Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)), |
| 352 | ); | 337 | ); |
| 353 | const sessionModelSwitch = | 338 | const sessionModelSwitch = |
| 354 | activeSession === undefined | 339 | activeSession === undefined |
| ... | @@ -358,7 +343,7 @@ const make = Effect.gen(function* () { | ... | @@ -358,7 +343,7 @@ const make = Effect.gen(function* () { |
| 358 | sessionModelSwitch === "unsupported" ? activeSession?.model : input.model; | 343 | sessionModelSwitch === "unsupported" ? activeSession?.model : input.model; |
| 359 | 344 | ||
| 360 | yield* providerService.sendTurn({ | 345 | yield* providerService.sendTurn({ |
| 361 | sessionId, | 346 | threadId: input.threadId, |
| 362 | ...(normalizedInput ? { input: normalizedInput } : {}), | 347 | ...(normalizedInput ? { input: normalizedInput } : {}), |
| 363 | ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), | 348 | ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), |
| 364 | ...(modelForTurn !== undefined ? { model: modelForTurn } : {}), | 349 | ...(modelForTurn !== undefined ? { model: modelForTurn } : {}), |
| ... | @@ -487,8 +472,8 @@ const make = Effect.gen(function* () { | ... | @@ -487,8 +472,8 @@ const make = Effect.gen(function* () { |
| 487 | if (!thread) { | 472 | if (!thread) { |
| 488 | return; | 473 | return; |
| 489 | } | 474 | } |
| 490 | const sessionId = thread.session?.providerSessionId; | 475 | const hasSession = thread.session && thread.session.status !== "stopped"; |
| 491 | if (!sessionId) { | 476 | if (!hasSession) { |
| 492 | return yield* appendProviderFailureActivity({ | 477 | return yield* appendProviderFailureActivity({ |
| 493 | threadId: event.payload.threadId, | 478 | threadId: event.payload.threadId, |
| 494 | kind: "provider.turn.interrupt.failed", | 479 | kind: "provider.turn.interrupt.failed", |
| ... | @@ -500,7 +485,7 @@ const make = Effect.gen(function* () { | ... | @@ -500,7 +485,7 @@ const make = Effect.gen(function* () { |
| 500 | } | 485 | } |
| 501 | 486 | ||
| 502 | // Orchestration turn ids are not provider turn ids, so interrupt by session. | 487 | // Orchestration turn ids are not provider turn ids, so interrupt by session. |
| 503 | yield* providerService.interruptTurn({ sessionId }); | 488 | yield* providerService.interruptTurn({ threadId: event.payload.threadId }); |
| 504 | }); | 489 | }); |
| 505 | 490 | ||
| 506 | const processApprovalResponseRequested = Effect.fnUntraced(function* ( | 491 | const processApprovalResponseRequested = Effect.fnUntraced(function* ( |
| ... | @@ -510,8 +495,8 @@ const make = Effect.gen(function* () { | ... | @@ -510,8 +495,8 @@ const make = Effect.gen(function* () { |
| 510 | if (!thread) { | 495 | if (!thread) { |
| 511 | return; | 496 | return; |
| 512 | } | 497 | } |
| 513 | const sessionId = thread.session?.providerSessionId; | 498 | const hasSession = thread.session && thread.session.status !== "stopped"; |
| 514 | if (!sessionId) { | 499 | if (!hasSession) { |
| 515 | return yield* appendProviderFailureActivity({ | 500 | return yield* appendProviderFailureActivity({ |
| 516 | threadId: event.payload.threadId, | 501 | threadId: event.payload.threadId, |
| 517 | kind: "provider.approval.respond.failed", | 502 | kind: "provider.approval.respond.failed", |
| ... | @@ -525,7 +510,7 @@ const make = Effect.gen(function* () { | ... | @@ -525,7 +510,7 @@ const make = Effect.gen(function* () { |
| 525 | 510 | ||
| 526 | yield* providerService | 511 | yield* providerService |
| 527 | .respondToRequest({ | 512 | .respondToRequest({ |
| 528 | sessionId, | 513 | threadId: event.payload.threadId, |
| 529 | requestId: event.payload.requestId, | 514 | requestId: event.payload.requestId, |
| 530 | decision: event.payload.decision, | 515 | decision: event.payload.decision, |
| 531 | }) | 516 | }) |
| ... | @@ -559,10 +544,8 @@ const make = Effect.gen(function* () { | ... | @@ -559,10 +544,8 @@ const make = Effect.gen(function* () { |
| 559 | } | 544 | } |
| 560 | 545 | ||
| 561 | const now = event.payload.createdAt; | 546 | const now = event.payload.createdAt; |
| 562 | const sessionId = thread.session?.providerSessionId; | 547 | if (thread.session && thread.session.status !== "stopped") { |
| 563 | 548 | yield* providerService.stopSession({ threadId: thread.id }); | |
| 564 | if (sessionId) { | ||
| 565 | yield* providerService.stopSession({ sessionId }); | ||
| 566 | } | 549 | } |
| 567 | 550 | ||
| 568 | yield* setThreadSession({ | 551 | yield* setThreadSession({ |
| ... | @@ -571,8 +554,6 @@ const make = Effect.gen(function* () { | ... | @@ -571,8 +554,6 @@ const make = Effect.gen(function* () { |
| 571 | threadId: thread.id, | 554 | threadId: thread.id, |
| 572 | status: "stopped", | 555 | status: "stopped", |
| 573 | providerName: thread.session?.providerName ?? null, | 556 | providerName: thread.session?.providerName ?? null, |
| 574 | providerSessionId: null, | ||
| 575 | providerThreadId: null, | ||
| 576 | runtimeMode: thread.session?.runtimeMode ?? DEFAULT_RUNTIME_MODE, | 557 | runtimeMode: thread.session?.runtimeMode ?? DEFAULT_RUNTIME_MODE, |
| 577 | activeTurnId: null, | 558 | activeTurnId: null, |
| 578 | lastError: thread.session?.lastError ?? null, | 559 | lastError: thread.session?.lastError ?? null, |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+89-113| ... | @@ -6,10 +6,8 @@ import { | ... | @@ -6,10 +6,8 @@ import { |
| 6 | MessageId, | 6 | MessageId, |
| 7 | ProjectId, | 7 | ProjectId, |
| 8 | ProviderItemId, | 8 | ProviderItemId, |
| 9 | ProviderSessionId, | ||
| 10 | ProviderThreadId, | ||
| 11 | ProviderTurnId, | ||
| 12 | ThreadId, | 9 | ThreadId, |
| 10 | TurnId, | ||
| 13 | } from "@t3tools/contracts"; | 11 | } from "@t3tools/contracts"; |
| 14 | import { Effect, Exit, Layer, ManagedRuntime, PubSub, Scope, Stream } from "effect"; | 12 | import { Effect, Exit, Layer, ManagedRuntime, PubSub, Scope, Stream } from "effect"; |
| 15 | import { afterEach, describe, expect, it } from "vitest"; | 13 | import { afterEach, describe, expect, it } from "vitest"; |
| ... | @@ -33,20 +31,18 @@ import { ServerConfig } from "../../config.ts"; | ... | @@ -33,20 +31,18 @@ import { ServerConfig } from "../../config.ts"; |
| 33 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 31 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 34 | 32 | ||
| 35 | const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); | 33 | const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); |
| 36 | const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value); | ||
| 37 | const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId.makeUnsafe(value); | ||
| 38 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); | ||
| 39 | const asItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(value); | 34 | const asItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(value); |
| 40 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); | 35 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 41 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); | 36 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); |
| 37 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); | ||
| 38 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); | ||
| 42 | 39 | ||
| 43 | type LegacyProviderRuntimeEvent = { | 40 | type LegacyProviderRuntimeEvent = { |
| 44 | readonly type: string; | 41 | readonly type: string; |
| 45 | readonly eventId: EventId; | 42 | readonly eventId: EventId; |
| 46 | readonly provider: "codex" | "claudeCode" | "cursor"; | 43 | readonly provider: "codex" | "claudeCode" | "cursor"; |
| 47 | readonly sessionId: string; | ||
| 48 | readonly createdAt: string; | 44 | readonly createdAt: string; |
| 49 | readonly threadId?: string | undefined; | 45 | readonly threadId: ThreadId; |
| 50 | readonly turnId?: string | undefined; | 46 | readonly turnId?: string | undefined; |
| 51 | readonly itemId?: string | undefined; | 47 | readonly itemId?: string | undefined; |
| 52 | readonly requestId?: string | undefined; | 48 | readonly requestId?: string | undefined; |
| ... | @@ -182,8 +178,6 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -182,8 +178,6 @@ describe("ProviderRuntimeIngestion", () => { |
| 182 | threadId: ThreadId.makeUnsafe("thread-1"), | 178 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 183 | status: "ready", | 179 | status: "ready", |
| 184 | providerName: "codex", | 180 | providerName: "codex", |
| 185 | providerSessionId: asSessionId("sess-1"), | ||
| 186 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | ||
| 187 | runtimeMode: "approval-required", | 181 | runtimeMode: "approval-required", |
| 188 | activeTurnId: null, | 182 | activeTurnId: null, |
| 189 | updatedAt: createdAt, | 183 | updatedAt: createdAt, |
| ... | @@ -207,9 +201,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -207,9 +201,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 207 | type: "turn.started", | 201 | type: "turn.started", |
| 208 | eventId: asEventId("evt-turn-started"), | 202 | eventId: asEventId("evt-turn-started"), |
| 209 | provider: "codex", | 203 | provider: "codex", |
| 210 | sessionId: asSessionId("sess-1"), | 204 | threadId: asThreadId("thread-1"), |
| 211 | createdAt: now, | 205 | createdAt: now, |
| 212 | turnId: asProviderTurnId("turn-1"), | 206 | turnId: asTurnId("turn-1"), |
| 213 | }); | 207 | }); |
| 214 | 208 | ||
| 215 | await waitForThread( | 209 | await waitForThread( |
| ... | @@ -221,9 +215,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -221,9 +215,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 221 | type: "turn.completed", | 215 | type: "turn.completed", |
| 222 | eventId: asEventId("evt-turn-completed"), | 216 | eventId: asEventId("evt-turn-completed"), |
| 223 | provider: "codex", | 217 | provider: "codex", |
| 224 | sessionId: asSessionId("sess-1"), | 218 | threadId: asThreadId("thread-1"), |
| 225 | createdAt: new Date().toISOString(), | 219 | createdAt: new Date().toISOString(), |
| 226 | turnId: asProviderTurnId("turn-1"), | 220 | turnId: asTurnId("turn-1"), |
| 227 | payload: { | 221 | payload: { |
| 228 | state: "failed", | 222 | state: "failed", |
| 229 | errorMessage: "turn failed", | 223 | errorMessage: "turn failed", |
| ... | @@ -249,9 +243,8 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -249,9 +243,8 @@ describe("ProviderRuntimeIngestion", () => { |
| 249 | type: "session.state.changed", | 243 | type: "session.state.changed", |
| 250 | eventId: asEventId("evt-session-state-waiting"), | 244 | eventId: asEventId("evt-session-state-waiting"), |
| 251 | provider: "codex", | 245 | provider: "codex", |
| 252 | sessionId: asSessionId("sess-1"), | 246 | threadId: asThreadId("thread-1"), |
| 253 | createdAt: waitingAt, | 247 | createdAt: waitingAt, |
| 254 | threadId: asProviderThreadId("provider-thread-1"), | ||
| 255 | payload: { | 248 | payload: { |
| 256 | state: "waiting", | 249 | state: "waiting", |
| 257 | reason: "awaiting approval", | 250 | reason: "awaiting approval", |
| ... | @@ -269,9 +262,8 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -269,9 +262,8 @@ describe("ProviderRuntimeIngestion", () => { |
| 269 | type: "session.state.changed", | 262 | type: "session.state.changed", |
| 270 | eventId: asEventId("evt-session-state-error"), | 263 | eventId: asEventId("evt-session-state-error"), |
| 271 | provider: "codex", | 264 | provider: "codex", |
| 272 | sessionId: asSessionId("sess-1"), | 265 | threadId: asThreadId("thread-1"), |
| 273 | createdAt: new Date().toISOString(), | 266 | createdAt: new Date().toISOString(), |
| 274 | threadId: asProviderThreadId("provider-thread-1"), | ||
| 275 | payload: { | 267 | payload: { |
| 276 | state: "error", | 268 | state: "error", |
| 277 | reason: "provider crashed", | 269 | reason: "provider crashed", |
| ... | @@ -292,9 +284,8 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -292,9 +284,8 @@ describe("ProviderRuntimeIngestion", () => { |
| 292 | type: "session.state.changed", | 284 | type: "session.state.changed", |
| 293 | eventId: asEventId("evt-session-state-stopped"), | 285 | eventId: asEventId("evt-session-state-stopped"), |
| 294 | provider: "codex", | 286 | provider: "codex", |
| 295 | sessionId: asSessionId("sess-1"), | 287 | threadId: asThreadId("thread-1"), |
| 296 | createdAt: new Date().toISOString(), | 288 | createdAt: new Date().toISOString(), |
| 297 | threadId: asProviderThreadId("provider-thread-1"), | ||
| 298 | payload: { | 289 | payload: { |
| 299 | state: "stopped", | 290 | state: "stopped", |
| 300 | }, | 291 | }, |
| ... | @@ -314,9 +305,8 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -314,9 +305,8 @@ describe("ProviderRuntimeIngestion", () => { |
| 314 | type: "session.state.changed", | 305 | type: "session.state.changed", |
| 315 | eventId: asEventId("evt-session-state-ready"), | 306 | eventId: asEventId("evt-session-state-ready"), |
| 316 | provider: "codex", | 307 | provider: "codex", |
| 317 | sessionId: asSessionId("sess-1"), | 308 | threadId: asThreadId("thread-1"), |
| 318 | createdAt: new Date().toISOString(), | 309 | createdAt: new Date().toISOString(), |
| 319 | threadId: asProviderThreadId("provider-thread-1"), | ||
| 320 | payload: { | 310 | payload: { |
| 321 | state: "ready", | 311 | state: "ready", |
| 322 | }, | 312 | }, |
| ... | @@ -341,10 +331,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -341,10 +331,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 341 | type: "turn.started", | 331 | type: "turn.started", |
| 342 | eventId: asEventId("evt-turn-started-midturn-lifecycle"), | 332 | eventId: asEventId("evt-turn-started-midturn-lifecycle"), |
| 343 | provider: "codex", | 333 | provider: "codex", |
| 344 | sessionId: asSessionId("sess-1"), | ||
| 345 | createdAt: now, | 334 | createdAt: now, |
| 346 | threadId: asProviderThreadId("provider-thread-1"), | 335 | threadId: asThreadId("thread-1"), |
| 347 | turnId: asProviderTurnId("turn-midturn-lifecycle"), | 336 | turnId: asTurnId("turn-midturn-lifecycle"), |
| 348 | }); | 337 | }); |
| 349 | 338 | ||
| 350 | await waitForThread( | 339 | await waitForThread( |
| ... | @@ -358,17 +347,15 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -358,17 +347,15 @@ describe("ProviderRuntimeIngestion", () => { |
| 358 | type: "thread.started", | 347 | type: "thread.started", |
| 359 | eventId: asEventId("evt-thread-started-midturn-lifecycle"), | 348 | eventId: asEventId("evt-thread-started-midturn-lifecycle"), |
| 360 | provider: "codex", | 349 | provider: "codex", |
| 361 | sessionId: asSessionId("sess-1"), | ||
| 362 | createdAt: new Date().toISOString(), | 350 | createdAt: new Date().toISOString(), |
| 363 | threadId: asProviderThreadId("provider-thread-1"), | 351 | threadId: asThreadId("thread-1"), |
| 364 | }); | 352 | }); |
| 365 | harness.emit({ | 353 | harness.emit({ |
| 366 | type: "session.started", | 354 | type: "session.started", |
| 367 | eventId: asEventId("evt-session-started-midturn-lifecycle"), | 355 | eventId: asEventId("evt-session-started-midturn-lifecycle"), |
| 368 | provider: "codex", | 356 | provider: "codex", |
| 369 | sessionId: asSessionId("sess-1"), | ||
| 370 | createdAt: new Date().toISOString(), | 357 | createdAt: new Date().toISOString(), |
| 371 | threadId: asProviderThreadId("provider-thread-1"), | 358 | threadId: asThreadId("thread-1"), |
| 372 | }); | 359 | }); |
| 373 | 360 | ||
| 374 | await Effect.runPromise(Effect.sleep("40 millis")); | 361 | await Effect.runPromise(Effect.sleep("40 millis")); |
| ... | @@ -381,10 +368,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -381,10 +368,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 381 | type: "turn.completed", | 368 | type: "turn.completed", |
| 382 | eventId: asEventId("evt-turn-completed-midturn-lifecycle"), | 369 | eventId: asEventId("evt-turn-completed-midturn-lifecycle"), |
| 383 | provider: "codex", | 370 | provider: "codex", |
| 384 | sessionId: asSessionId("sess-1"), | ||
| 385 | createdAt: new Date().toISOString(), | 371 | createdAt: new Date().toISOString(), |
| 386 | threadId: asProviderThreadId("provider-thread-1"), | 372 | threadId: asThreadId("thread-1"), |
| 387 | turnId: asProviderTurnId("turn-midturn-lifecycle"), | 373 | turnId: asTurnId("turn-midturn-lifecycle"), |
| 388 | status: "completed", | 374 | status: "completed", |
| 389 | }); | 375 | }); |
| 390 | 376 | ||
| ... | @@ -402,10 +388,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -402,10 +388,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 402 | type: "turn.started", | 388 | type: "turn.started", |
| 403 | eventId: asEventId("evt-turn-started-primary"), | 389 | eventId: asEventId("evt-turn-started-primary"), |
| 404 | provider: "codex", | 390 | provider: "codex", |
| 405 | sessionId: asSessionId("sess-1"), | ||
| 406 | createdAt: now, | 391 | createdAt: now, |
| 407 | threadId: asProviderThreadId("provider-thread-1"), | 392 | threadId: asThreadId("thread-1"), |
| 408 | turnId: asProviderTurnId("turn-primary"), | 393 | turnId: asTurnId("turn-primary"), |
| 409 | }); | 394 | }); |
| 410 | 395 | ||
| 411 | await waitForThread( | 396 | await waitForThread( |
| ... | @@ -418,10 +403,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -418,10 +403,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 418 | type: "turn.completed", | 403 | type: "turn.completed", |
| 419 | eventId: asEventId("evt-turn-completed-aux"), | 404 | eventId: asEventId("evt-turn-completed-aux"), |
| 420 | provider: "codex", | 405 | provider: "codex", |
| 421 | sessionId: asSessionId("sess-1"), | ||
| 422 | createdAt: new Date().toISOString(), | 406 | createdAt: new Date().toISOString(), |
| 423 | threadId: asProviderThreadId("provider-thread-aux"), | 407 | threadId: asThreadId("thread-1"), |
| 424 | turnId: asProviderTurnId("turn-aux"), | 408 | turnId: asTurnId("turn-aux"), |
| 425 | status: "completed", | 409 | status: "completed", |
| 426 | }); | 410 | }); |
| 427 | 411 | ||
| ... | @@ -437,10 +421,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -437,10 +421,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 437 | type: "turn.completed", | 421 | type: "turn.completed", |
| 438 | eventId: asEventId("evt-turn-completed-primary"), | 422 | eventId: asEventId("evt-turn-completed-primary"), |
| 439 | provider: "codex", | 423 | provider: "codex", |
| 440 | sessionId: asSessionId("sess-1"), | ||
| 441 | createdAt: new Date().toISOString(), | 424 | createdAt: new Date().toISOString(), |
| 442 | threadId: asProviderThreadId("provider-thread-1"), | 425 | threadId: asThreadId("thread-1"), |
| 443 | turnId: asProviderTurnId("turn-primary"), | 426 | turnId: asTurnId("turn-primary"), |
| 444 | status: "completed", | 427 | status: "completed", |
| 445 | }); | 428 | }); |
| 446 | 429 | ||
| ... | @@ -463,8 +446,6 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -463,8 +446,6 @@ describe("ProviderRuntimeIngestion", () => { |
| 463 | threadId: ThreadId.makeUnsafe("thread-1"), | 446 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 464 | status: "ready", | 447 | status: "ready", |
| 465 | providerName: "claudeCode", | 448 | providerName: "claudeCode", |
| 466 | providerSessionId: asSessionId("sess-1"), | ||
| 467 | providerThreadId: asProviderThreadId("claude-thread-placeholder"), | ||
| 468 | runtimeMode: "approval-required", | 449 | runtimeMode: "approval-required", |
| 469 | activeTurnId: null, | 450 | activeTurnId: null, |
| 470 | updatedAt: seededAt, | 451 | updatedAt: seededAt, |
| ... | @@ -478,10 +459,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -478,10 +459,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 478 | type: "turn.started", | 459 | type: "turn.started", |
| 479 | eventId: asEventId("evt-turn-started-claude-placeholder"), | 460 | eventId: asEventId("evt-turn-started-claude-placeholder"), |
| 480 | provider: "claudeCode", | 461 | provider: "claudeCode", |
| 481 | sessionId: asSessionId("sess-1"), | ||
| 482 | createdAt: new Date().toISOString(), | 462 | createdAt: new Date().toISOString(), |
| 483 | threadId: asProviderThreadId("provider-thread-real"), | 463 | threadId: asThreadId("thread-1"), |
| 484 | turnId: asProviderTurnId("turn-claude-placeholder"), | 464 | turnId: asTurnId("turn-claude-placeholder"), |
| 485 | }); | 465 | }); |
| 486 | 466 | ||
| 487 | await waitForThread( | 467 | await waitForThread( |
| ... | @@ -495,10 +475,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -495,10 +475,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 495 | type: "turn.completed", | 475 | type: "turn.completed", |
| 496 | eventId: asEventId("evt-turn-completed-claude-placeholder"), | 476 | eventId: asEventId("evt-turn-completed-claude-placeholder"), |
| 497 | provider: "claudeCode", | 477 | provider: "claudeCode", |
| 498 | sessionId: asSessionId("sess-1"), | ||
| 499 | createdAt: new Date().toISOString(), | 478 | createdAt: new Date().toISOString(), |
| 500 | threadId: asProviderThreadId("provider-thread-real"), | 479 | threadId: asThreadId("thread-1"), |
| 501 | turnId: asProviderTurnId("turn-claude-placeholder"), | 480 | turnId: asTurnId("turn-claude-placeholder"), |
| 502 | status: "completed", | 481 | status: "completed", |
| 503 | }); | 482 | }); |
| 504 | 483 | ||
| ... | @@ -516,9 +495,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -516,9 +495,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 516 | type: "turn.started", | 495 | type: "turn.started", |
| 517 | eventId: asEventId("evt-turn-started-guarded"), | 496 | eventId: asEventId("evt-turn-started-guarded"), |
| 518 | provider: "codex", | 497 | provider: "codex", |
| 519 | sessionId: asSessionId("sess-1"), | ||
| 520 | createdAt: now, | 498 | createdAt: now, |
| 521 | turnId: asProviderTurnId("turn-guarded-main"), | 499 | threadId: asThreadId("thread-1"), |
| 500 | turnId: asTurnId("turn-guarded-main"), | ||
| 522 | }); | 501 | }); |
| 523 | 502 | ||
| 524 | await waitForThread( | 503 | await waitForThread( |
| ... | @@ -532,9 +511,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -532,9 +511,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 532 | type: "turn.completed", | 511 | type: "turn.completed", |
| 533 | eventId: asEventId("evt-turn-completed-guarded-other"), | 512 | eventId: asEventId("evt-turn-completed-guarded-other"), |
| 534 | provider: "codex", | 513 | provider: "codex", |
| 535 | sessionId: asSessionId("sess-1"), | ||
| 536 | createdAt: new Date().toISOString(), | 514 | createdAt: new Date().toISOString(), |
| 537 | turnId: asProviderTurnId("turn-guarded-other"), | 515 | threadId: asThreadId("thread-1"), |
| 516 | turnId: asTurnId("turn-guarded-other"), | ||
| 538 | status: "completed", | 517 | status: "completed", |
| 539 | }); | 518 | }); |
| 540 | 519 | ||
| ... | @@ -550,9 +529,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -550,9 +529,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 550 | type: "turn.completed", | 529 | type: "turn.completed", |
| 551 | eventId: asEventId("evt-turn-completed-guarded-main"), | 530 | eventId: asEventId("evt-turn-completed-guarded-main"), |
| 552 | provider: "codex", | 531 | provider: "codex", |
| 553 | sessionId: asSessionId("sess-1"), | ||
| 554 | createdAt: new Date().toISOString(), | 532 | createdAt: new Date().toISOString(), |
| 555 | turnId: asProviderTurnId("turn-guarded-main"), | 533 | threadId: asThreadId("thread-1"), |
| 534 | turnId: asTurnId("turn-guarded-main"), | ||
| 556 | status: "completed", | 535 | status: "completed", |
| 557 | }); | 536 | }); |
| 558 | 537 | ||
| ... | @@ -570,9 +549,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -570,9 +549,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 570 | type: "content.delta", | 549 | type: "content.delta", |
| 571 | eventId: asEventId("evt-message-delta-1"), | 550 | eventId: asEventId("evt-message-delta-1"), |
| 572 | provider: "codex", | 551 | provider: "codex", |
| 573 | sessionId: asSessionId("sess-1"), | ||
| 574 | createdAt: now, | 552 | createdAt: now, |
| 575 | turnId: asProviderTurnId("turn-2"), | 553 | threadId: asThreadId("thread-1"), |
| 554 | turnId: asTurnId("turn-2"), | ||
| 576 | itemId: asItemId("item-1"), | 555 | itemId: asItemId("item-1"), |
| 577 | payload: { | 556 | payload: { |
| 578 | streamKind: "assistant_text", | 557 | streamKind: "assistant_text", |
| ... | @@ -583,9 +562,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -583,9 +562,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 583 | type: "content.delta", | 562 | type: "content.delta", |
| 584 | eventId: asEventId("evt-message-delta-2"), | 563 | eventId: asEventId("evt-message-delta-2"), |
| 585 | provider: "codex", | 564 | provider: "codex", |
| 586 | sessionId: asSessionId("sess-1"), | ||
| 587 | createdAt: now, | 565 | createdAt: now, |
| 588 | turnId: asProviderTurnId("turn-2"), | 566 | threadId: asThreadId("thread-1"), |
| 567 | turnId: asTurnId("turn-2"), | ||
| 589 | itemId: asItemId("item-1"), | 568 | itemId: asItemId("item-1"), |
| 590 | payload: { | 569 | payload: { |
| 591 | streamKind: "assistant_text", | 570 | streamKind: "assistant_text", |
| ... | @@ -596,9 +575,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -596,9 +575,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 596 | type: "item.completed", | 575 | type: "item.completed", |
| 597 | eventId: asEventId("evt-message-completed"), | 576 | eventId: asEventId("evt-message-completed"), |
| 598 | provider: "codex", | 577 | provider: "codex", |
| 599 | sessionId: asSessionId("sess-1"), | ||
| 600 | createdAt: now, | 578 | createdAt: now, |
| 601 | turnId: asProviderTurnId("turn-2"), | 579 | threadId: asThreadId("thread-1"), |
| 580 | turnId: asTurnId("turn-2"), | ||
| 602 | itemId: asItemId("item-1"), | 581 | itemId: asItemId("item-1"), |
| 603 | payload: { | 582 | payload: { |
| 604 | itemType: "assistant_message", | 583 | itemType: "assistant_message", |
| ... | @@ -627,9 +606,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -627,9 +606,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 627 | type: "item.completed", | 606 | type: "item.completed", |
| 628 | eventId: asEventId("evt-assistant-item-completed-no-delta"), | 607 | eventId: asEventId("evt-assistant-item-completed-no-delta"), |
| 629 | provider: "codex", | 608 | provider: "codex", |
| 630 | sessionId: asSessionId("sess-1"), | ||
| 631 | createdAt: now, | 609 | createdAt: now, |
| 632 | turnId: asProviderTurnId("turn-no-delta"), | 610 | threadId: asThreadId("thread-1"), |
| 611 | turnId: asTurnId("turn-no-delta"), | ||
| 633 | itemId: asItemId("item-no-delta"), | 612 | itemId: asItemId("item-no-delta"), |
| 634 | payload: { | 613 | payload: { |
| 635 | itemType: "assistant_message", | 614 | itemType: "assistant_message", |
| ... | @@ -659,9 +638,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -659,9 +638,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 659 | type: "turn.started", | 638 | type: "turn.started", |
| 660 | eventId: asEventId("evt-turn-started-buffered"), | 639 | eventId: asEventId("evt-turn-started-buffered"), |
| 661 | provider: "codex", | 640 | provider: "codex", |
| 662 | sessionId: asSessionId("sess-1"), | ||
| 663 | createdAt: now, | 641 | createdAt: now, |
| 664 | turnId: asProviderTurnId("turn-buffered"), | 642 | threadId: asThreadId("thread-1"), |
| 643 | turnId: asTurnId("turn-buffered"), | ||
| 665 | }); | 644 | }); |
| 666 | await waitForThread( | 645 | await waitForThread( |
| 667 | harness.engine, | 646 | harness.engine, |
| ... | @@ -673,9 +652,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -673,9 +652,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 673 | type: "content.delta", | 652 | type: "content.delta", |
| 674 | eventId: asEventId("evt-message-delta-buffered"), | 653 | eventId: asEventId("evt-message-delta-buffered"), |
| 675 | provider: "codex", | 654 | provider: "codex", |
| 676 | sessionId: asSessionId("sess-1"), | ||
| 677 | createdAt: now, | 655 | createdAt: now, |
| 678 | turnId: asProviderTurnId("turn-buffered"), | 656 | threadId: asThreadId("thread-1"), |
| 657 | turnId: asTurnId("turn-buffered"), | ||
| 679 | itemId: asItemId("item-buffered"), | 658 | itemId: asItemId("item-buffered"), |
| 680 | payload: { | 659 | payload: { |
| 681 | streamKind: "assistant_text", | 660 | streamKind: "assistant_text", |
| ... | @@ -698,9 +677,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -698,9 +677,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 698 | type: "item.completed", | 677 | type: "item.completed", |
| 699 | eventId: asEventId("evt-message-completed-buffered"), | 678 | eventId: asEventId("evt-message-completed-buffered"), |
| 700 | provider: "codex", | 679 | provider: "codex", |
| 701 | sessionId: asSessionId("sess-1"), | ||
| 702 | createdAt: now, | 680 | createdAt: now, |
| 703 | turnId: asProviderTurnId("turn-buffered"), | 681 | threadId: asThreadId("thread-1"), |
| 682 | turnId: asTurnId("turn-buffered"), | ||
| 704 | itemId: asItemId("item-buffered"), | 683 | itemId: asItemId("item-buffered"), |
| 705 | payload: { | 684 | payload: { |
| 706 | itemType: "assistant_message", | 685 | itemType: "assistant_message", |
| ... | @@ -747,9 +726,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -747,9 +726,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 747 | type: "turn.started", | 726 | type: "turn.started", |
| 748 | eventId: asEventId("evt-turn-started-streaming-mode"), | 727 | eventId: asEventId("evt-turn-started-streaming-mode"), |
| 749 | provider: "codex", | 728 | provider: "codex", |
| 750 | sessionId: asSessionId("sess-1"), | ||
| 751 | createdAt: now, | 729 | createdAt: now, |
| 752 | turnId: asProviderTurnId("turn-streaming-mode"), | 730 | threadId: asThreadId("thread-1"), |
| 731 | turnId: asTurnId("turn-streaming-mode"), | ||
| 753 | }); | 732 | }); |
| 754 | await waitForThread( | 733 | await waitForThread( |
| 755 | harness.engine, | 734 | harness.engine, |
| ... | @@ -762,9 +741,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -762,9 +741,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 762 | type: "content.delta", | 741 | type: "content.delta", |
| 763 | eventId: asEventId("evt-message-delta-streaming-mode"), | 742 | eventId: asEventId("evt-message-delta-streaming-mode"), |
| 764 | provider: "codex", | 743 | provider: "codex", |
| 765 | sessionId: asSessionId("sess-1"), | ||
| 766 | createdAt: now, | 744 | createdAt: now, |
| 767 | turnId: asProviderTurnId("turn-streaming-mode"), | 745 | threadId: asThreadId("thread-1"), |
| 746 | turnId: asTurnId("turn-streaming-mode"), | ||
| 768 | itemId: asItemId("item-streaming-mode"), | 747 | itemId: asItemId("item-streaming-mode"), |
| 769 | payload: { | 748 | payload: { |
| 770 | streamKind: "assistant_text", | 749 | streamKind: "assistant_text", |
| ... | @@ -789,9 +768,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -789,9 +768,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 789 | type: "item.completed", | 768 | type: "item.completed", |
| 790 | eventId: asEventId("evt-message-completed-streaming-mode"), | 769 | eventId: asEventId("evt-message-completed-streaming-mode"), |
| 791 | provider: "codex", | 770 | provider: "codex", |
| 792 | sessionId: asSessionId("sess-1"), | ||
| 793 | createdAt: now, | 771 | createdAt: now, |
| 794 | turnId: asProviderTurnId("turn-streaming-mode"), | 772 | threadId: asThreadId("thread-1"), |
| 773 | turnId: asTurnId("turn-streaming-mode"), | ||
| 795 | itemId: asItemId("item-streaming-mode"), | 774 | itemId: asItemId("item-streaming-mode"), |
| 796 | payload: { | 775 | payload: { |
| 797 | itemType: "assistant_message", | 776 | itemType: "assistant_message", |
| ... | @@ -821,9 +800,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -821,9 +800,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 821 | type: "turn.started", | 800 | type: "turn.started", |
| 822 | eventId: asEventId("evt-turn-started-buffer-spill"), | 801 | eventId: asEventId("evt-turn-started-buffer-spill"), |
| 823 | provider: "codex", | 802 | provider: "codex", |
| 824 | sessionId: asSessionId("sess-1"), | ||
| 825 | createdAt: now, | 803 | createdAt: now, |
| 826 | turnId: asProviderTurnId("turn-buffer-spill"), | 804 | threadId: asThreadId("thread-1"), |
| 805 | turnId: asTurnId("turn-buffer-spill"), | ||
| 827 | }); | 806 | }); |
| 828 | await waitForThread( | 807 | await waitForThread( |
| 829 | harness.engine, | 808 | harness.engine, |
| ... | @@ -836,9 +815,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -836,9 +815,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 836 | type: "content.delta", | 815 | type: "content.delta", |
| 837 | eventId: asEventId("evt-message-delta-buffer-spill"), | 816 | eventId: asEventId("evt-message-delta-buffer-spill"), |
| 838 | provider: "codex", | 817 | provider: "codex", |
| 839 | sessionId: asSessionId("sess-1"), | ||
| 840 | createdAt: now, | 818 | createdAt: now, |
| 841 | turnId: asProviderTurnId("turn-buffer-spill"), | 819 | threadId: asThreadId("thread-1"), |
| 820 | turnId: asTurnId("turn-buffer-spill"), | ||
| 842 | itemId: asItemId("item-buffer-spill"), | 821 | itemId: asItemId("item-buffer-spill"), |
| 843 | payload: { | 822 | payload: { |
| 844 | streamKind: "assistant_text", | 823 | streamKind: "assistant_text", |
| ... | @@ -849,9 +828,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -849,9 +828,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 849 | type: "item.completed", | 828 | type: "item.completed", |
| 850 | eventId: asEventId("evt-message-completed-buffer-spill"), | 829 | eventId: asEventId("evt-message-completed-buffer-spill"), |
| 851 | provider: "codex", | 830 | provider: "codex", |
| 852 | sessionId: asSessionId("sess-1"), | ||
| 853 | createdAt: now, | 831 | createdAt: now, |
| 854 | turnId: asProviderTurnId("turn-buffer-spill"), | 832 | threadId: asThreadId("thread-1"), |
| 833 | turnId: asTurnId("turn-buffer-spill"), | ||
| 855 | itemId: asItemId("item-buffer-spill"), | 834 | itemId: asItemId("item-buffer-spill"), |
| 856 | payload: { | 835 | payload: { |
| 857 | itemType: "assistant_message", | 836 | itemType: "assistant_message", |
| ... | @@ -881,9 +860,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -881,9 +860,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 881 | type: "turn.started", | 860 | type: "turn.started", |
| 882 | eventId: asEventId("evt-turn-started-for-complete-dedup"), | 861 | eventId: asEventId("evt-turn-started-for-complete-dedup"), |
| 883 | provider: "codex", | 862 | provider: "codex", |
| 884 | sessionId: asSessionId("sess-1"), | ||
| 885 | createdAt: now, | 863 | createdAt: now, |
| 886 | turnId: asProviderTurnId("turn-complete-dedup"), | 864 | threadId: asThreadId("thread-1"), |
| 865 | turnId: asTurnId("turn-complete-dedup"), | ||
| 887 | }); | 866 | }); |
| 888 | 867 | ||
| 889 | await waitForThread( | 868 | await waitForThread( |
| ... | @@ -897,9 +876,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -897,9 +876,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 897 | type: "content.delta", | 876 | type: "content.delta", |
| 898 | eventId: asEventId("evt-message-delta-for-complete-dedup"), | 877 | eventId: asEventId("evt-message-delta-for-complete-dedup"), |
| 899 | provider: "codex", | 878 | provider: "codex", |
| 900 | sessionId: asSessionId("sess-1"), | ||
| 901 | createdAt: now, | 879 | createdAt: now, |
| 902 | turnId: asProviderTurnId("turn-complete-dedup"), | 880 | threadId: asThreadId("thread-1"), |
| 881 | turnId: asTurnId("turn-complete-dedup"), | ||
| 903 | itemId: asItemId("item-complete-dedup"), | 882 | itemId: asItemId("item-complete-dedup"), |
| 904 | payload: { | 883 | payload: { |
| 905 | streamKind: "assistant_text", | 884 | streamKind: "assistant_text", |
| ... | @@ -910,9 +889,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -910,9 +889,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 910 | type: "item.completed", | 889 | type: "item.completed", |
| 911 | eventId: asEventId("evt-message-completed-for-complete-dedup"), | 890 | eventId: asEventId("evt-message-completed-for-complete-dedup"), |
| 912 | provider: "codex", | 891 | provider: "codex", |
| 913 | sessionId: asSessionId("sess-1"), | ||
| 914 | createdAt: now, | 892 | createdAt: now, |
| 915 | turnId: asProviderTurnId("turn-complete-dedup"), | 893 | threadId: asThreadId("thread-1"), |
| 894 | turnId: asTurnId("turn-complete-dedup"), | ||
| 916 | itemId: asItemId("item-complete-dedup"), | 895 | itemId: asItemId("item-complete-dedup"), |
| 917 | payload: { | 896 | payload: { |
| 918 | itemType: "assistant_message", | 897 | itemType: "assistant_message", |
| ... | @@ -923,9 +902,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -923,9 +902,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 923 | type: "turn.completed", | 902 | type: "turn.completed", |
| 924 | eventId: asEventId("evt-turn-completed-for-complete-dedup"), | 903 | eventId: asEventId("evt-turn-completed-for-complete-dedup"), |
| 925 | provider: "codex", | 904 | provider: "codex", |
| 926 | sessionId: asSessionId("sess-1"), | ||
| 927 | createdAt: now, | 905 | createdAt: now, |
| 928 | turnId: asProviderTurnId("turn-complete-dedup"), | 906 | threadId: asThreadId("thread-1"), |
| 907 | turnId: asTurnId("turn-complete-dedup"), | ||
| 929 | payload: { | 908 | payload: { |
| 930 | state: "completed", | 909 | state: "completed", |
| 931 | }, | 910 | }, |
| ... | @@ -967,8 +946,8 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -967,8 +946,8 @@ describe("ProviderRuntimeIngestion", () => { |
| 967 | type: "request.opened", | 946 | type: "request.opened", |
| 968 | eventId: asEventId("evt-request-opened"), | 947 | eventId: asEventId("evt-request-opened"), |
| 969 | provider: "codex", | 948 | provider: "codex", |
| 970 | sessionId: asSessionId("sess-1"), | ||
| 971 | createdAt: now, | 949 | createdAt: now, |
| 950 | threadId: asThreadId("thread-1"), | ||
| 972 | requestId: ApprovalRequestId.makeUnsafe("req-open"), | 951 | requestId: ApprovalRequestId.makeUnsafe("req-open"), |
| 973 | payload: { | 952 | payload: { |
| 974 | requestType: "command_execution_approval", | 953 | requestType: "command_execution_approval", |
| ... | @@ -980,8 +959,8 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -980,8 +959,8 @@ describe("ProviderRuntimeIngestion", () => { |
| 980 | type: "request.resolved", | 959 | type: "request.resolved", |
| 981 | eventId: asEventId("evt-request-resolved"), | 960 | eventId: asEventId("evt-request-resolved"), |
| 982 | provider: "codex", | 961 | provider: "codex", |
| 983 | sessionId: asSessionId("sess-1"), | ||
| 984 | createdAt: now, | 962 | createdAt: now, |
| 963 | threadId: asThreadId("thread-1"), | ||
| 985 | requestId: ApprovalRequestId.makeUnsafe("req-open"), | 964 | requestId: ApprovalRequestId.makeUnsafe("req-open"), |
| 986 | payload: { | 965 | payload: { |
| 987 | requestType: "command_execution_approval", | 966 | requestType: "command_execution_approval", |
| ... | @@ -1033,9 +1012,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -1033,9 +1012,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 1033 | type: "runtime.error", | 1012 | type: "runtime.error", |
| 1034 | eventId: asEventId("evt-runtime-error"), | 1013 | eventId: asEventId("evt-runtime-error"), |
| 1035 | provider: "codex", | 1014 | provider: "codex", |
| 1036 | sessionId: asSessionId("sess-1"), | ||
| 1037 | createdAt: now, | 1015 | createdAt: now, |
| 1038 | turnId: asProviderTurnId("turn-3"), | 1016 | threadId: asThreadId("thread-1"), |
| 1017 | turnId: asTurnId("turn-3"), | ||
| 1039 | payload: { | 1018 | payload: { |
| 1040 | message: "runtime exploded", | 1019 | message: "runtime exploded", |
| 1041 | }, | 1020 | }, |
| ... | @@ -1060,26 +1039,24 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -1060,26 +1039,24 @@ describe("ProviderRuntimeIngestion", () => { |
| 1060 | type: "session.started", | 1039 | type: "session.started", |
| 1061 | eventId: asEventId("evt-session-started"), | 1040 | eventId: asEventId("evt-session-started"), |
| 1062 | provider: "codex", | 1041 | provider: "codex", |
| 1063 | sessionId: asSessionId("sess-1"), | ||
| 1064 | createdAt: now, | 1042 | createdAt: now, |
| 1065 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 1043 | threadId: asThreadId("thread-1"), |
| 1066 | message: "session started", | 1044 | message: "session started", |
| 1067 | }); | 1045 | }); |
| 1068 | harness.emit({ | 1046 | harness.emit({ |
| 1069 | type: "thread.started", | 1047 | type: "thread.started", |
| 1070 | eventId: asEventId("evt-thread-started"), | 1048 | eventId: asEventId("evt-thread-started"), |
| 1071 | provider: "codex", | 1049 | provider: "codex", |
| 1072 | sessionId: asSessionId("sess-1"), | ||
| 1073 | createdAt: now, | 1050 | createdAt: now, |
| 1074 | threadId: ProviderThreadId.makeUnsafe("provider-thread-2"), | 1051 | threadId: asThreadId("thread-1"), |
| 1075 | }); | 1052 | }); |
| 1076 | harness.emit({ | 1053 | harness.emit({ |
| 1077 | type: "item.started", | 1054 | type: "item.started", |
| 1078 | eventId: asEventId("evt-tool-started"), | 1055 | eventId: asEventId("evt-tool-started"), |
| 1079 | provider: "codex", | 1056 | provider: "codex", |
| 1080 | sessionId: asSessionId("sess-1"), | ||
| 1081 | createdAt: now, | 1057 | createdAt: now, |
| 1082 | turnId: asProviderTurnId("turn-9"), | 1058 | threadId: asThreadId("thread-1"), |
| 1059 | turnId: asTurnId("turn-9"), | ||
| 1083 | payload: { | 1060 | payload: { |
| 1084 | itemType: "command_execution", | 1061 | itemType: "command_execution", |
| 1085 | status: "in_progress", | 1062 | status: "in_progress", |
| ... | @@ -1114,9 +1091,8 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -1114,9 +1091,8 @@ describe("ProviderRuntimeIngestion", () => { |
| 1114 | type: "thread.metadata.updated", | 1091 | type: "thread.metadata.updated", |
| 1115 | eventId: asEventId("evt-thread-metadata-updated"), | 1092 | eventId: asEventId("evt-thread-metadata-updated"), |
| 1116 | provider: "codex", | 1093 | provider: "codex", |
| 1117 | sessionId: asSessionId("sess-1"), | ||
| 1118 | createdAt: now, | 1094 | createdAt: now, |
| 1119 | threadId: asProviderThreadId("provider-thread-1"), | 1095 | threadId: asThreadId("thread-1"), |
| 1120 | payload: { | 1096 | payload: { |
| 1121 | name: "Renamed by provider", | 1097 | name: "Renamed by provider", |
| 1122 | metadata: { source: "provider" }, | 1098 | metadata: { source: "provider" }, |
| ... | @@ -1127,9 +1103,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -1127,9 +1103,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 1127 | type: "turn.plan.updated", | 1103 | type: "turn.plan.updated", |
| 1128 | eventId: asEventId("evt-turn-plan-updated"), | 1104 | eventId: asEventId("evt-turn-plan-updated"), |
| 1129 | provider: "codex", | 1105 | provider: "codex", |
| 1130 | sessionId: asSessionId("sess-1"), | ||
| 1131 | createdAt: now, | 1106 | createdAt: now, |
| 1132 | turnId: asProviderTurnId("turn-p1"), | 1107 | threadId: asThreadId("thread-1"), |
| 1108 | turnId: asTurnId("turn-p1"), | ||
| 1133 | payload: { | 1109 | payload: { |
| 1134 | explanation: "Working through the plan", | 1110 | explanation: "Working through the plan", |
| 1135 | plan: [ | 1111 | plan: [ |
| ... | @@ -1143,9 +1119,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -1143,9 +1119,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 1143 | type: "item.updated", | 1119 | type: "item.updated", |
| 1144 | eventId: asEventId("evt-item-updated"), | 1120 | eventId: asEventId("evt-item-updated"), |
| 1145 | provider: "codex", | 1121 | provider: "codex", |
| 1146 | sessionId: asSessionId("sess-1"), | ||
| 1147 | createdAt: now, | 1122 | createdAt: now, |
| 1148 | turnId: asProviderTurnId("turn-p1"), | 1123 | threadId: asThreadId("thread-1"), |
| 1124 | turnId: asTurnId("turn-p1"), | ||
| 1149 | itemId: asItemId("item-p1-tool"), | 1125 | itemId: asItemId("item-p1-tool"), |
| 1150 | payload: { | 1126 | payload: { |
| 1151 | itemType: "command_execution", | 1127 | itemType: "command_execution", |
| ... | @@ -1160,9 +1136,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -1160,9 +1136,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 1160 | type: "runtime.warning", | 1136 | type: "runtime.warning", |
| 1161 | eventId: asEventId("evt-runtime-warning"), | 1137 | eventId: asEventId("evt-runtime-warning"), |
| 1162 | provider: "codex", | 1138 | provider: "codex", |
| 1163 | sessionId: asSessionId("sess-1"), | ||
| 1164 | createdAt: now, | 1139 | createdAt: now, |
| 1165 | turnId: asProviderTurnId("turn-p1"), | 1140 | threadId: asThreadId("thread-1"), |
| 1141 | turnId: asTurnId("turn-p1"), | ||
| 1166 | payload: { | 1142 | payload: { |
| 1167 | message: "Provider got slow", | 1143 | message: "Provider got slow", |
| 1168 | detail: { latencyMs: 1500 }, | 1144 | detail: { latencyMs: 1500 }, |
| ... | @@ -1173,9 +1149,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -1173,9 +1149,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 1173 | type: "turn.diff.updated", | 1149 | type: "turn.diff.updated", |
| 1174 | eventId: asEventId("evt-turn-diff-updated"), | 1150 | eventId: asEventId("evt-turn-diff-updated"), |
| 1175 | provider: "codex", | 1151 | provider: "codex", |
| 1176 | sessionId: asSessionId("sess-1"), | ||
| 1177 | createdAt: now, | 1152 | createdAt: now, |
| 1178 | turnId: asProviderTurnId("turn-p1"), | 1153 | threadId: asThreadId("thread-1"), |
| 1154 | turnId: asTurnId("turn-p1"), | ||
| 1179 | itemId: asItemId("item-p1-assistant"), | 1155 | itemId: asItemId("item-p1-assistant"), |
| 1180 | payload: { | 1156 | payload: { |
| 1181 | unifiedDiff: "diff --git a/file.txt b/file.txt\n+hello\n", | 1157 | unifiedDiff: "diff --git a/file.txt b/file.txt\n+hello\n", |
| ... | @@ -1249,9 +1225,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -1249,9 +1225,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 1249 | type: "content.delta", | 1225 | type: "content.delta", |
| 1250 | eventId: asEventId("evt-invalid-delta"), | 1226 | eventId: asEventId("evt-invalid-delta"), |
| 1251 | provider: "codex", | 1227 | provider: "codex", |
| 1252 | sessionId: asSessionId("sess-1"), | ||
| 1253 | createdAt: now, | 1228 | createdAt: now, |
| 1254 | turnId: asProviderTurnId("turn-invalid"), | 1229 | threadId: asThreadId("thread-1"), |
| 1230 | turnId: asTurnId("turn-invalid"), | ||
| 1255 | itemId: asItemId("item-invalid"), | 1231 | itemId: asItemId("item-invalid"), |
| 1256 | payload: { | 1232 | payload: { |
| 1257 | streamKind: "assistant_text", | 1233 | streamKind: "assistant_text", |
| ... | @@ -1263,9 +1239,9 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -1263,9 +1239,9 @@ describe("ProviderRuntimeIngestion", () => { |
| 1263 | type: "runtime.error", | 1239 | type: "runtime.error", |
| 1264 | eventId: asEventId("evt-runtime-error-after-failure"), | 1240 | eventId: asEventId("evt-runtime-error-after-failure"), |
| 1265 | provider: "codex", | 1241 | provider: "codex", |
| 1266 | sessionId: asSessionId("sess-1"), | ||
| 1267 | createdAt: new Date().toISOString(), | 1242 | createdAt: new Date().toISOString(), |
| 1268 | turnId: asProviderTurnId("turn-after-failure"), | 1243 | threadId: asThreadId("thread-1"), |
| 1244 | turnId: asTurnId("turn-after-failure"), | ||
| 1269 | payload: { | 1245 | payload: { |
| 1270 | message: "runtime still processed", | 1246 | message: "runtime still processed", |
| 1271 | }, | 1247 | }, |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+33-95| ... | @@ -5,13 +5,10 @@ import { | ... | @@ -5,13 +5,10 @@ import { |
| 5 | MessageId, | 5 | MessageId, |
| 6 | type OrchestrationEvent, | 6 | type OrchestrationEvent, |
| 7 | CheckpointRef, | 7 | CheckpointRef, |
| 8 | ProviderThreadId, | 8 | ThreadId, |
| 9 | type ThreadId, | ||
| 10 | TurnId, | 9 | TurnId, |
| 11 | type OrchestrationThreadActivity, | 10 | type OrchestrationThreadActivity, |
| 12 | type ProviderRuntimeEvent, | 11 | type ProviderRuntimeEvent, |
| 13 | ProviderSessionId, | ||
| 14 | RuntimeSessionId, | ||
| 15 | } from "@t3tools/contracts"; | 12 | } from "@t3tools/contracts"; |
| 16 | import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Ref, Stream } from "effect"; | 13 | import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Ref, Stream } from "effect"; |
| 17 | 14 | ||
| ... | @@ -22,7 +19,7 @@ import { | ... | @@ -22,7 +19,7 @@ import { |
| 22 | type ProviderRuntimeIngestionShape, | 19 | type ProviderRuntimeIngestionShape, |
| 23 | } from "../Services/ProviderRuntimeIngestion.ts"; | 20 | } from "../Services/ProviderRuntimeIngestion.ts"; |
| 24 | 21 | ||
| 25 | const providerTurnKey = (sessionId: ProviderSessionId, turnId: TurnId) => `${sessionId}:${turnId}`; | 22 | const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`; |
| 26 | const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId => | 23 | const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId => |
| 27 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); | 24 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); |
| 28 | 25 | ||
| ... | @@ -53,14 +50,6 @@ function toTurnId(value: TurnId | string | undefined): TurnId | undefined { | ... | @@ -53,14 +50,6 @@ function toTurnId(value: TurnId | string | undefined): TurnId | undefined { |
| 53 | return value === undefined ? undefined : TurnId.makeUnsafe(String(value)); | 50 | return value === undefined ? undefined : TurnId.makeUnsafe(String(value)); |
| 54 | } | 51 | } |
| 55 | 52 | ||
| 56 | function toProviderThreadId(value: string | undefined): ProviderThreadId | null { | ||
| 57 | return value === undefined ? null : ProviderThreadId.makeUnsafe(String(value)); | ||
| 58 | } | ||
| 59 | |||
| 60 | function toProviderSessionId(value: RuntimeSessionId): ProviderSessionId { | ||
| 61 | return ProviderSessionId.makeUnsafe(String(value)); | ||
| 62 | } | ||
| 63 | |||
| 64 | function toApprovalRequestId(value: string | undefined): ApprovalRequestId | undefined { | 53 | function toApprovalRequestId(value: string | undefined): ApprovalRequestId | undefined { |
| 65 | return value === undefined ? undefined : ApprovalRequestId.makeUnsafe(value); | 54 | return value === undefined ? undefined : ApprovalRequestId.makeUnsafe(value); |
| 66 | } | 55 | } |
| ... | @@ -72,13 +61,6 @@ function sameId(left: string | null | undefined, right: string | null | undefine | ... | @@ -72,13 +61,6 @@ function sameId(left: string | null | undefined, right: string | null | undefine |
| 72 | return left === right; | 61 | return left === right; |
| 73 | } | 62 | } |
| 74 | 63 | ||
| 75 | function isSyntheticClaudeThreadId( | ||
| 76 | provider: ProviderRuntimeEvent["provider"], | ||
| 77 | threadId: ProviderThreadId | null, | ||
| 78 | ): boolean { | ||
| 79 | return provider === "claudeCode" && threadId !== null && threadId.startsWith("claude-thread-"); | ||
| 80 | } | ||
| 81 | |||
| 82 | function truncateDetail(value: string, limit = 180): string { | 64 | function truncateDetail(value: string, limit = 180): string { |
| 83 | return value.length > limit ? `${value.slice(0, limit - 3)}...` : value; | 65 | return value.length > limit ? `${value.slice(0, limit - 3)}...` : value; |
| 84 | } | 66 | } |
| ... | @@ -148,11 +130,13 @@ function orchestrationSessionStatusFromRuntimeState( | ... | @@ -148,11 +130,13 @@ function orchestrationSessionStatusFromRuntimeState( |
| 148 | 130 | ||
| 149 | function requestKindFromCanonicalRequestType( | 131 | function requestKindFromCanonicalRequestType( |
| 150 | requestType: string | undefined, | 132 | requestType: string | undefined, |
| 151 | ): "command" | "file-change" | undefined { | 133 | ): "command" | "file-read" | "file-change" | undefined { |
| 152 | switch (requestType) { | 134 | switch (requestType) { |
| 153 | case "command_execution_approval": | 135 | case "command_execution_approval": |
| 154 | case "exec_command_approval": | 136 | case "exec_command_approval": |
| 155 | return "command"; | 137 | return "command"; |
| 138 | case "file_read_approval": | ||
| 139 | return "file-read"; | ||
| 156 | case "file_change_approval": | 140 | case "file_change_approval": |
| 157 | case "apply_patch_approval": | 141 | case "apply_patch_approval": |
| 158 | return "file-change"; | 142 | return "file-change"; |
| ... | @@ -194,6 +178,8 @@ function runtimeEventToActivities( | ... | @@ -194,6 +178,8 @@ function runtimeEventToActivities( |
| 194 | summary: | 178 | summary: |
| 195 | requestKind === "command" | 179 | requestKind === "command" |
| 196 | ? "Command approval requested" | 180 | ? "Command approval requested" |
| 181 | : requestKind === "file-read" | ||
| 182 | ? "File-read approval requested" | ||
| 197 | : requestKind === "file-change" | 183 | : requestKind === "file-change" |
| 198 | ? "File-change approval requested" | 184 | ? "File-change approval requested" |
| 199 | : "Approval requested", | 185 | : "Approval requested", |
| ... | @@ -380,15 +366,15 @@ const make = Effect.gen(function* () { | ... | @@ -380,15 +366,15 @@ const make = Effect.gen(function* () { |
| 380 | }); | 366 | }); |
| 381 | 367 | ||
| 382 | const rememberAssistantMessageId = ( | 368 | const rememberAssistantMessageId = ( |
| 383 | sessionId: ProviderSessionId, | 369 | threadId: ThreadId, |
| 384 | turnId: TurnId, | 370 | turnId: TurnId, |
| 385 | messageId: MessageId, | 371 | messageId: MessageId, |
| 386 | ) => | 372 | ) => |
| 387 | Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)).pipe( | 373 | Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe( |
| 388 | Effect.flatMap((existingIds) => | 374 | Effect.flatMap((existingIds) => |
| 389 | Cache.set( | 375 | Cache.set( |
| 390 | turnMessageIdsByTurnKey, | 376 | turnMessageIdsByTurnKey, |
| 391 | providerTurnKey(sessionId, turnId), | 377 | providerTurnKey(threadId, turnId), |
| 392 | Option.match(existingIds, { | 378 | Option.match(existingIds, { |
| 393 | onNone: () => new Set([messageId]), | 379 | onNone: () => new Set([messageId]), |
| 394 | onSome: (ids) => { | 380 | onSome: (ids) => { |
| ... | @@ -402,11 +388,11 @@ const make = Effect.gen(function* () { | ... | @@ -402,11 +388,11 @@ const make = Effect.gen(function* () { |
| 402 | ); | 388 | ); |
| 403 | 389 | ||
| 404 | const forgetAssistantMessageId = ( | 390 | const forgetAssistantMessageId = ( |
| 405 | sessionId: ProviderSessionId, | 391 | threadId: ThreadId, |
| 406 | turnId: TurnId, | 392 | turnId: TurnId, |
| 407 | messageId: MessageId, | 393 | messageId: MessageId, |
| 408 | ) => | 394 | ) => |
| 409 | Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)).pipe( | 395 | Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe( |
| 410 | Effect.flatMap((existingIds) => | 396 | Effect.flatMap((existingIds) => |
| 411 | Option.match(existingIds, { | 397 | Option.match(existingIds, { |
| 412 | onNone: () => Effect.void, | 398 | onNone: () => Effect.void, |
| ... | @@ -414,23 +400,23 @@ const make = Effect.gen(function* () { | ... | @@ -414,23 +400,23 @@ const make = Effect.gen(function* () { |
| 414 | const nextIds = new Set(ids); | 400 | const nextIds = new Set(ids); |
| 415 | nextIds.delete(messageId); | 401 | nextIds.delete(messageId); |
| 416 | if (nextIds.size === 0) { | 402 | if (nextIds.size === 0) { |
| 417 | return Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)); | 403 | return Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)); |
| 418 | } | 404 | } |
| 419 | return Cache.set(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId), nextIds); | 405 | return Cache.set(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), nextIds); |
| 420 | }, | 406 | }, |
| 421 | }), | 407 | }), |
| 422 | ), | 408 | ), |
| 423 | ); | 409 | ); |
| 424 | 410 | ||
| 425 | const getAssistantMessageIdsForTurn = (sessionId: ProviderSessionId, turnId: TurnId) => | 411 | const getAssistantMessageIdsForTurn = (threadId: ThreadId, turnId: TurnId) => |
| 426 | Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)).pipe( | 412 | Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe( |
| 427 | Effect.map((existingIds) => | 413 | Effect.map((existingIds) => |
| 428 | Option.getOrElse(existingIds, (): Set<MessageId> => new Set<MessageId>()), | 414 | Option.getOrElse(existingIds, (): Set<MessageId> => new Set<MessageId>()), |
| 429 | ), | 415 | ), |
| 430 | ); | 416 | ); |
| 431 | 417 | ||
| 432 | const clearAssistantMessageIdsForTurn = (sessionId: ProviderSessionId, turnId: TurnId) => | 418 | const clearAssistantMessageIdsForTurn = (threadId: ThreadId, turnId: TurnId) => |
| 433 | Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)); | 419 | Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)); |
| 434 | 420 | ||
| 435 | const appendBufferedAssistantText = (messageId: MessageId, delta: string) => | 421 | const appendBufferedAssistantText = (messageId: MessageId, delta: string) => |
| 436 | Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( | 422 | Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( |
| ... | @@ -505,9 +491,9 @@ const make = Effect.gen(function* () { | ... | @@ -505,9 +491,9 @@ const make = Effect.gen(function* () { |
| 505 | yield* clearAssistantMessageState(input.messageId); | 491 | yield* clearAssistantMessageState(input.messageId); |
| 506 | }); | 492 | }); |
| 507 | 493 | ||
| 508 | const clearTurnStateForSession = (sessionId: ProviderSessionId) => | 494 | const clearTurnStateForSession = (threadId: ThreadId) => |
| 509 | Effect.gen(function* () { | 495 | Effect.gen(function* () { |
| 510 | const prefix = `${sessionId}:`; | 496 | const prefix = `${threadId}:`; |
| 511 | const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey)); | 497 | const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey)); |
| 512 | yield* Effect.forEach( | 498 | yield* Effect.forEach( |
| 513 | turnKeys, | 499 | turnKeys, |
| ... | @@ -532,29 +518,14 @@ const make = Effect.gen(function* () { | ... | @@ -532,29 +518,14 @@ const make = Effect.gen(function* () { |
| 532 | 518 | ||
| 533 | const processRuntimeEvent = (event: ProviderRuntimeEvent) => | 519 | const processRuntimeEvent = (event: ProviderRuntimeEvent) => |
| 534 | Effect.gen(function* () { | 520 | Effect.gen(function* () { |
| 535 | const providerSessionId = toProviderSessionId(event.sessionId); | ||
| 536 | const readModel = yield* orchestrationEngine.getReadModel(); | 521 | const readModel = yield* orchestrationEngine.getReadModel(); |
| 537 | const thread = readModel.threads.find( | 522 | const thread = readModel.threads.find((entry) => entry.id === event.threadId); |
| 538 | (entry) => entry.session?.providerSessionId === providerSessionId, | ||
| 539 | ); | ||
| 540 | if (!thread) return; | 523 | if (!thread) return; |
| 541 | 524 | ||
| 542 | const now = event.createdAt; | 525 | const now = event.createdAt; |
| 543 | const sessionProviderThreadId = thread.session?.providerThreadId ?? null; | ||
| 544 | const scopedSessionProviderThreadId = isSyntheticClaudeThreadId( | ||
| 545 | event.provider, | ||
| 546 | sessionProviderThreadId, | ||
| 547 | ) | ||
| 548 | ? null | ||
| 549 | : sessionProviderThreadId; | ||
| 550 | const eventProviderThreadId = toProviderThreadId(event.threadId); | ||
| 551 | const eventTurnId = toTurnId(event.turnId); | 526 | const eventTurnId = toTurnId(event.turnId); |
| 552 | const activeTurnId = thread.session?.activeTurnId ?? null; | 527 | const activeTurnId = thread.session?.activeTurnId ?? null; |
| 553 | 528 | ||
| 554 | const matchesThreadScope = | ||
| 555 | eventProviderThreadId === null || | ||
| 556 | scopedSessionProviderThreadId === null || | ||
| 557 | sameId(eventProviderThreadId, scopedSessionProviderThreadId); | ||
| 558 | const conflictsWithActiveTurn = | 529 | const conflictsWithActiveTurn = |
| 559 | activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId); | 530 | activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId); |
| 560 | const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined; | 531 | const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined; |
| ... | @@ -568,27 +539,10 @@ const make = Effect.gen(function* () { | ... | @@ -568,27 +539,10 @@ const make = Effect.gen(function* () { |
| 568 | return true; | 539 | return true; |
| 569 | case "session.started": | 540 | case "session.started": |
| 570 | case "thread.started": | 541 | case "thread.started": |
| 571 | if (!matchesThreadScope) { | ||
| 572 | return false; | ||
| 573 | } | ||
| 574 | // Never let auxiliary/provider-side spawned threads replace the primary thread binding. | ||
| 575 | if ( | ||
| 576 | eventProviderThreadId !== null && | ||
| 577 | scopedSessionProviderThreadId !== null && | ||
| 578 | !sameId(eventProviderThreadId, scopedSessionProviderThreadId) | ||
| 579 | ) { | ||
| 580 | return false; | ||
| 581 | } | ||
| 582 | return true; | 542 | return true; |
| 583 | case "turn.started": | 543 | case "turn.started": |
| 584 | if (!matchesThreadScope) { | ||
| 585 | return false; | ||
| 586 | } | ||
| 587 | return !conflictsWithActiveTurn; | 544 | return !conflictsWithActiveTurn; |
| 588 | case "turn.completed": | 545 | case "turn.completed": |
| 589 | if (!matchesThreadScope) { | ||
| 590 | return false; | ||
| 591 | } | ||
| 592 | if (conflictsWithActiveTurn || missingTurnForActiveTurn) { | 546 | if (conflictsWithActiveTurn || missingTurnForActiveTurn) { |
| 593 | return false; | 547 | return false; |
| 594 | } | 548 | } |
| ... | @@ -617,12 +571,6 @@ const make = Effect.gen(function* () { | ... | @@ -617,12 +571,6 @@ const make = Effect.gen(function* () { |
| 617 | : event.type === "turn.completed" || event.type === "session.exited" | 571 | : event.type === "turn.completed" || event.type === "session.exited" |
| 618 | ? null | 572 | ? null |
| 619 | : activeTurnId; | 573 | : activeTurnId; |
| 620 | const providerThreadIdFromEvent = | ||
| 621 | event.type === "thread.started" | ||
| 622 | ? toProviderThreadId(event.threadId) | ||
| 623 | : toProviderThreadId(event.threadId); | ||
| 624 | const providerThreadId = | ||
| 625 | providerThreadIdFromEvent ?? scopedSessionProviderThreadId ?? null; | ||
| 626 | const status = (() => { | 574 | const status = (() => { |
| 627 | switch (event.type) { | 575 | switch (event.type) { |
| 628 | case "session.state.changed": | 576 | case "session.state.changed": |
| ... | @@ -658,8 +606,6 @@ const make = Effect.gen(function* () { | ... | @@ -658,8 +606,6 @@ const make = Effect.gen(function* () { |
| 658 | threadId: thread.id, | 606 | threadId: thread.id, |
| 659 | status, | 607 | status, |
| 660 | providerName: event.provider, | 608 | providerName: event.provider, |
| 661 | providerSessionId, | ||
| 662 | providerThreadId, | ||
| 663 | runtimeMode: thread.session?.runtimeMode ?? "full-access", | 609 | runtimeMode: thread.session?.runtimeMode ?? "full-access", |
| 664 | activeTurnId: nextActiveTurnId, | 610 | activeTurnId: nextActiveTurnId, |
| 665 | lastError, | 611 | lastError, |
| ... | @@ -677,11 +623,11 @@ const make = Effect.gen(function* () { | ... | @@ -677,11 +623,11 @@ const make = Effect.gen(function* () { |
| 677 | 623 | ||
| 678 | if (assistantDelta && assistantDelta.length > 0) { | 624 | if (assistantDelta && assistantDelta.length > 0) { |
| 679 | const assistantMessageId = MessageId.makeUnsafe( | 625 | const assistantMessageId = MessageId.makeUnsafe( |
| 680 | `assistant:${event.itemId ?? event.turnId ?? event.sessionId}`, | 626 | `assistant:${event.itemId ?? event.turnId ?? event.eventId}`, |
| 681 | ); | 627 | ); |
| 682 | const turnId = toTurnId(event.turnId); | 628 | const turnId = toTurnId(event.turnId); |
| 683 | if (turnId) { | 629 | if (turnId) { |
| 684 | yield* rememberAssistantMessageId(providerSessionId, turnId, assistantMessageId); | 630 | yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId); |
| 685 | } | 631 | } |
| 686 | 632 | ||
| 687 | const assistantDeliveryMode = yield* Ref.get(assistantDeliveryModeRef); | 633 | const assistantDeliveryMode = yield* Ref.get(assistantDeliveryModeRef); |
| ... | @@ -714,7 +660,7 @@ const make = Effect.gen(function* () { | ... | @@ -714,7 +660,7 @@ const make = Effect.gen(function* () { |
| 714 | const assistantCompletion = | 660 | const assistantCompletion = |
| 715 | event.type === "item.completed" && event.payload.itemType === "assistant_message" | 661 | event.type === "item.completed" && event.payload.itemType === "assistant_message" |
| 716 | ? { | 662 | ? { |
| 717 | messageId: MessageId.makeUnsafe(`assistant:${event.itemId ?? event.turnId ?? event.sessionId}`), | 663 | messageId: MessageId.makeUnsafe(`assistant:${event.itemId ?? event.turnId ?? event.eventId}`), |
| 718 | fallbackText: event.payload.detail, | 664 | fallbackText: event.payload.detail, |
| 719 | } | 665 | } |
| 720 | : undefined; | 666 | : undefined; |
| ... | @@ -723,7 +669,7 @@ const make = Effect.gen(function* () { | ... | @@ -723,7 +669,7 @@ const make = Effect.gen(function* () { |
| 723 | const assistantMessageId = assistantCompletion.messageId; | 669 | const assistantMessageId = assistantCompletion.messageId; |
| 724 | const turnId = toTurnId(event.turnId); | 670 | const turnId = toTurnId(event.turnId); |
| 725 | if (turnId) { | 671 | if (turnId) { |
| 726 | yield* rememberAssistantMessageId(providerSessionId, turnId, assistantMessageId); | 672 | yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId); |
| 727 | } | 673 | } |
| 728 | 674 | ||
| 729 | yield* finalizeAssistantMessage({ | 675 | yield* finalizeAssistantMessage({ |
| ... | @@ -740,14 +686,14 @@ const make = Effect.gen(function* () { | ... | @@ -740,14 +686,14 @@ const make = Effect.gen(function* () { |
| 740 | }); | 686 | }); |
| 741 | 687 | ||
| 742 | if (turnId) { | 688 | if (turnId) { |
| 743 | yield* forgetAssistantMessageId(providerSessionId, turnId, assistantMessageId); | 689 | yield* forgetAssistantMessageId(thread.id, turnId, assistantMessageId); |
| 744 | } | 690 | } |
| 745 | } | 691 | } |
| 746 | 692 | ||
| 747 | if (event.type === "turn.completed") { | 693 | if (event.type === "turn.completed") { |
| 748 | const turnId = toTurnId(event.turnId); | 694 | const turnId = toTurnId(event.turnId); |
| 749 | if (turnId) { | 695 | if (turnId) { |
| 750 | const assistantMessageIds = yield* getAssistantMessageIdsForTurn(providerSessionId, turnId); | 696 | const assistantMessageIds = yield* getAssistantMessageIdsForTurn(thread.id, turnId); |
| 751 | yield* Effect.forEach( | 697 | yield* Effect.forEach( |
| 752 | assistantMessageIds, | 698 | assistantMessageIds, |
| 753 | (assistantMessageId) => | 699 | (assistantMessageId) => |
| ... | @@ -762,12 +708,12 @@ const make = Effect.gen(function* () { | ... | @@ -762,12 +708,12 @@ const make = Effect.gen(function* () { |
| 762 | }), | 708 | }), |
| 763 | { concurrency: 1 }, | 709 | { concurrency: 1 }, |
| 764 | ).pipe(Effect.asVoid); | 710 | ).pipe(Effect.asVoid); |
| 765 | yield* clearAssistantMessageIdsForTurn(providerSessionId, turnId); | 711 | yield* clearAssistantMessageIdsForTurn(thread.id, turnId); |
| 766 | } | 712 | } |
| 767 | } | 713 | } |
| 768 | 714 | ||
| 769 | if (event.type === "session.exited") { | 715 | if (event.type === "session.exited") { |
| 770 | yield* clearTurnStateForSession(providerSessionId); | 716 | yield* clearTurnStateForSession(thread.id); |
| 771 | } | 717 | } |
| 772 | 718 | ||
| 773 | if (event.type === "runtime.error") { | 719 | if (event.type === "runtime.error") { |
| ... | @@ -775,15 +721,9 @@ const make = Effect.gen(function* () { | ... | @@ -775,15 +721,9 @@ const make = Effect.gen(function* () { |
| 775 | 721 | ||
| 776 | const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD | 722 | const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD |
| 777 | ? true | 723 | ? true |
| 778 | : matchesThreadScope && | 724 | : activeTurnId === null || |
| 779 | (activeTurnId === null || | 725 | eventTurnId === undefined || |
| 780 | eventTurnId === undefined || | 726 | sameId(activeTurnId, eventTurnId); |
| 781 | sameId(activeTurnId, eventTurnId)); | ||
| 782 | |||
| 783 | const providerThreadId = | ||
| 784 | event.threadId !== undefined | ||
| 785 | ? ProviderThreadId.makeUnsafe(event.threadId) | ||
| 786 | : (thread.session?.providerThreadId ?? null); | ||
| 787 | 727 | ||
| 788 | if (shouldApplyRuntimeError) { | 728 | if (shouldApplyRuntimeError) { |
| 789 | yield* orchestrationEngine.dispatch({ | 729 | yield* orchestrationEngine.dispatch({ |
| ... | @@ -794,8 +734,6 @@ const make = Effect.gen(function* () { | ... | @@ -794,8 +734,6 @@ const make = Effect.gen(function* () { |
| 794 | threadId: thread.id, | 734 | threadId: thread.id, |
| 795 | status: "error", | 735 | status: "error", |
| 796 | providerName: event.provider, | 736 | providerName: event.provider, |
| 797 | providerSessionId, | ||
| 798 | providerThreadId, | ||
| 799 | runtimeMode: thread.session?.runtimeMode ?? "full-access", | 737 | runtimeMode: thread.session?.runtimeMode ?? "full-access", |
| 800 | activeTurnId: eventTurnId ?? null, | 738 | activeTurnId: eventTurnId ?? null, |
| 801 | lastError: runtimeErrorMessage, | 739 | lastError: runtimeErrorMessage, |
| ... | @@ -819,7 +757,7 @@ const make = Effect.gen(function* () { | ... | @@ -819,7 +757,7 @@ const make = Effect.gen(function* () { |
| 819 | const turnId = toTurnId(event.turnId); | 757 | const turnId = toTurnId(event.turnId); |
| 820 | if (turnId) { | 758 | if (turnId) { |
| 821 | const assistantMessageId = MessageId.makeUnsafe( | 759 | const assistantMessageId = MessageId.makeUnsafe( |
| 822 | `assistant:${event.itemId ?? event.turnId ?? event.sessionId}`, | 760 | `assistant:${event.itemId ?? event.turnId ?? event.eventId}`, |
| 823 | ); | 761 | ); |
| 824 | yield* orchestrationEngine.dispatch({ | 762 | yield* orchestrationEngine.dispatch({ |
| 825 | type: "thread.turn.diff.complete", | 763 | type: "thread.turn.diff.complete", |
apps/server/src/orchestration/decider.ts+1-8| ... | @@ -391,14 +391,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -391,14 +391,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 391 | aggregateId: command.threadId, | 391 | aggregateId: command.threadId, |
| 392 | occurredAt: command.createdAt, | 392 | occurredAt: command.createdAt, |
| 393 | commandId: command.commandId, | 393 | commandId: command.commandId, |
| 394 | metadata: { | 394 | metadata: {}, |
| 395 | ...(command.session.providerSessionId !== null | ||
| 396 | ? { providerSessionId: command.session.providerSessionId } | ||
| 397 | : {}), | ||
| 398 | ...(command.session.providerThreadId !== null | ||
| 399 | ? { providerThreadId: command.session.providerThreadId } | ||
| 400 | : {}), | ||
| 401 | }, | ||
| 402 | }), | 395 | }), |
| 403 | type: "thread.session-set", | 396 | type: "thread.session-set", |
| 404 | payload: { | 397 | payload: { |
apps/server/src/persistence/Layers/OrchestrationEventStore.ts-2| ... | @@ -74,8 +74,6 @@ function inferActorKind( | ... | @@ -74,8 +74,6 @@ function inferActorKind( |
| 74 | return "server"; | 74 | return "server"; |
| 75 | } | 75 | } |
| 76 | if ( | 76 | if ( |
| 77 | event.metadata.providerSessionId !== undefined || | ||
| 78 | event.metadata.providerThreadId !== undefined || | ||
| 79 | event.metadata.providerTurnId !== undefined || | 77 | event.metadata.providerTurnId !== undefined || |
| 80 | event.metadata.providerItemId !== undefined || | 78 | event.metadata.providerItemId !== undefined || |
| 81 | event.metadata.adapterKey !== undefined | 79 | event.metadata.adapterKey !== undefined |
apps/server/src/persistence/Layers/ProjectionThreadSessions.ts-8| ... | @@ -23,8 +23,6 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { | ... | @@ -23,8 +23,6 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { |
| 23 | thread_id, | 23 | thread_id, |
| 24 | status, | 24 | status, |
| 25 | provider_name, | 25 | provider_name, |
| 26 | provider_session_id, | ||
| 27 | provider_thread_id, | ||
| 28 | runtime_mode, | 26 | runtime_mode, |
| 29 | active_turn_id, | 27 | active_turn_id, |
| 30 | last_error, | 28 | last_error, |
| ... | @@ -34,8 +32,6 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { | ... | @@ -34,8 +32,6 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { |
| 34 | ${row.threadId}, | 32 | ${row.threadId}, |
| 35 | ${row.status}, | 33 | ${row.status}, |
| 36 | ${row.providerName}, | 34 | ${row.providerName}, |
| 37 | ${row.providerSessionId}, | ||
| 38 | ${row.providerThreadId}, | ||
| 39 | ${row.runtimeMode}, | 35 | ${row.runtimeMode}, |
| 40 | ${row.activeTurnId}, | 36 | ${row.activeTurnId}, |
| 41 | ${row.lastError}, | 37 | ${row.lastError}, |
| ... | @@ -45,8 +41,6 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { | ... | @@ -45,8 +41,6 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { |
| 45 | DO UPDATE SET | 41 | DO UPDATE SET |
| 46 | status = excluded.status, | 42 | status = excluded.status, |
| 47 | provider_name = excluded.provider_name, | 43 | provider_name = excluded.provider_name, |
| 48 | provider_session_id = excluded.provider_session_id, | ||
| 49 | provider_thread_id = excluded.provider_thread_id, | ||
| 50 | runtime_mode = excluded.runtime_mode, | 44 | runtime_mode = excluded.runtime_mode, |
| 51 | active_turn_id = excluded.active_turn_id, | 45 | active_turn_id = excluded.active_turn_id, |
| 52 | last_error = excluded.last_error, | 46 | last_error = excluded.last_error, |
| ... | @@ -63,8 +57,6 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { | ... | @@ -63,8 +57,6 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { |
| 63 | thread_id AS "threadId", | 57 | thread_id AS "threadId", |
| 64 | status, | 58 | status, |
| 65 | provider_name AS "providerName", | 59 | provider_name AS "providerName", |
| 66 | provider_session_id AS "providerSessionId", | ||
| 67 | provider_thread_id AS "providerThreadId", | ||
| 68 | runtime_mode AS "runtimeMode", | 60 | runtime_mode AS "runtimeMode", |
| 69 | active_turn_id AS "activeTurnId", | 61 | active_turn_id AS "activeTurnId", |
| 70 | last_error AS "lastError", | 62 | last_error AS "lastError", |
apps/server/src/persistence/Layers/ProviderSessionRuntime.ts+20-30| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { ProviderSessionId } from "@t3tools/contracts"; | 1 | import { ThreadId } from "@t3tools/contracts"; |
| 2 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 2 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 3 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | 3 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; |
| 4 | import { Effect, Layer, Option, Schema, Struct } from "effect"; | 4 | import { Effect, Layer, Option, Schema, Struct } from "effect"; |
| ... | @@ -24,7 +24,7 @@ const ProviderSessionRuntimeDbRowSchema = ProviderSessionRuntime.mapFields( | ... | @@ -24,7 +24,7 @@ const ProviderSessionRuntimeDbRowSchema = ProviderSessionRuntime.mapFields( |
| 24 | const decodeRuntime = Schema.decodeUnknownEffect(ProviderSessionRuntime); | 24 | const decodeRuntime = Schema.decodeUnknownEffect(ProviderSessionRuntime); |
| 25 | 25 | ||
| 26 | const GetRuntimeRequestSchema = Schema.Struct({ | 26 | const GetRuntimeRequestSchema = Schema.Struct({ |
| 27 | providerSessionId: ProviderSessionId, | 27 | threadId: ThreadId, |
| 28 | }); | 28 | }); |
| 29 | 29 | ||
| 30 | const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema; | 30 | const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema; |
| ... | @@ -44,11 +44,9 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { | ... | @@ -44,11 +44,9 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { |
| 44 | execute: (runtime) => | 44 | execute: (runtime) => |
| 45 | sql` | 45 | sql` |
| 46 | INSERT INTO provider_session_runtime ( | 46 | INSERT INTO provider_session_runtime ( |
| 47 | provider_session_id, | ||
| 48 | thread_id, | 47 | thread_id, |
| 49 | provider_name, | 48 | provider_name, |
| 50 | adapter_key, | 49 | adapter_key, |
| 51 | provider_thread_id, | ||
| 52 | runtime_mode, | 50 | runtime_mode, |
| 53 | status, | 51 | status, |
| 54 | last_seen_at, | 52 | last_seen_at, |
| ... | @@ -56,23 +54,19 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { | ... | @@ -56,23 +54,19 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { |
| 56 | runtime_payload_json | 54 | runtime_payload_json |
| 57 | ) | 55 | ) |
| 58 | VALUES ( | 56 | VALUES ( |
| 59 | ${runtime.providerSessionId}, | ||
| 60 | ${runtime.threadId}, | 57 | ${runtime.threadId}, |
| 61 | ${runtime.providerName}, | 58 | ${runtime.providerName}, |
| 62 | ${runtime.adapterKey}, | 59 | ${runtime.adapterKey}, |
| 63 | ${runtime.providerThreadId}, | ||
| 64 | ${runtime.runtimeMode}, | 60 | ${runtime.runtimeMode}, |
| 65 | ${runtime.status}, | 61 | ${runtime.status}, |
| 66 | ${runtime.lastSeenAt}, | 62 | ${runtime.lastSeenAt}, |
| 67 | ${runtime.resumeCursor}, | 63 | ${runtime.resumeCursor}, |
| 68 | ${runtime.runtimePayload} | 64 | ${runtime.runtimePayload} |
| 69 | ) | 65 | ) |
| 70 | ON CONFLICT (provider_session_id) | 66 | ON CONFLICT (thread_id) |
| 71 | DO UPDATE SET | 67 | DO UPDATE SET |
| 72 | thread_id = excluded.thread_id, | ||
| 73 | provider_name = excluded.provider_name, | 68 | provider_name = excluded.provider_name, |
| 74 | adapter_key = excluded.adapter_key, | 69 | adapter_key = excluded.adapter_key, |
| 75 | provider_thread_id = excluded.provider_thread_id, | ||
| 76 | runtime_mode = excluded.runtime_mode, | 70 | runtime_mode = excluded.runtime_mode, |
| 77 | status = excluded.status, | 71 | status = excluded.status, |
| 78 | last_seen_at = excluded.last_seen_at, | 72 | last_seen_at = excluded.last_seen_at, |
| ... | @@ -81,24 +75,22 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { | ... | @@ -81,24 +75,22 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { |
| 81 | `, | 75 | `, |
| 82 | }); | 76 | }); |
| 83 | 77 | ||
| 84 | const getRuntimeRowBySessionId = SqlSchema.findOneOption({ | 78 | const getRuntimeRowByThreadId = SqlSchema.findOneOption({ |
| 85 | Request: GetRuntimeRequestSchema, | 79 | Request: GetRuntimeRequestSchema, |
| 86 | Result: ProviderSessionRuntimeDbRowSchema, | 80 | Result: ProviderSessionRuntimeDbRowSchema, |
| 87 | execute: ({ providerSessionId }) => | 81 | execute: ({ threadId }) => |
| 88 | sql` | 82 | sql` |
| 89 | SELECT | 83 | SELECT |
| 90 | provider_session_id AS "providerSessionId", | ||
| 91 | thread_id AS "threadId", | 84 | thread_id AS "threadId", |
| 92 | provider_name AS "providerName", | 85 | provider_name AS "providerName", |
| 93 | adapter_key AS "adapterKey", | 86 | adapter_key AS "adapterKey", |
| 94 | provider_thread_id AS "providerThreadId", | ||
| 95 | runtime_mode AS "runtimeMode", | 87 | runtime_mode AS "runtimeMode", |
| 96 | status, | 88 | status, |
| 97 | last_seen_at AS "lastSeenAt", | 89 | last_seen_at AS "lastSeenAt", |
| 98 | resume_cursor_json AS "resumeCursor", | 90 | resume_cursor_json AS "resumeCursor", |
| 99 | runtime_payload_json AS "runtimePayload" | 91 | runtime_payload_json AS "runtimePayload" |
| 100 | FROM provider_session_runtime | 92 | FROM provider_session_runtime |
| 101 | WHERE provider_session_id = ${providerSessionId} | 93 | WHERE thread_id = ${threadId} |
| 102 | `, | 94 | `, |
| 103 | }); | 95 | }); |
| 104 | 96 | ||
| ... | @@ -108,27 +100,25 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { | ... | @@ -108,27 +100,25 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { |
| 108 | execute: () => | 100 | execute: () => |
| 109 | sql` | 101 | sql` |
| 110 | SELECT | 102 | SELECT |
| 111 | provider_session_id AS "providerSessionId", | ||
| 112 | thread_id AS "threadId", | 103 | thread_id AS "threadId", |
| 113 | provider_name AS "providerName", | 104 | provider_name AS "providerName", |
| 114 | adapter_key AS "adapterKey", | 105 | adapter_key AS "adapterKey", |
| 115 | provider_thread_id AS "providerThreadId", | ||
| 116 | runtime_mode AS "runtimeMode", | 106 | runtime_mode AS "runtimeMode", |
| 117 | status, | 107 | status, |
| 118 | last_seen_at AS "lastSeenAt", | 108 | last_seen_at AS "lastSeenAt", |
| 119 | resume_cursor_json AS "resumeCursor", | 109 | resume_cursor_json AS "resumeCursor", |
| 120 | runtime_payload_json AS "runtimePayload" | 110 | runtime_payload_json AS "runtimePayload" |
| 121 | FROM provider_session_runtime | 111 | FROM provider_session_runtime |
| 122 | ORDER BY last_seen_at ASC, provider_session_id ASC | 112 | ORDER BY last_seen_at ASC, thread_id ASC |
| 123 | `, | 113 | `, |
| 124 | }); | 114 | }); |
| 125 | 115 | ||
| 126 | const deleteRuntimeBySessionId = SqlSchema.void({ | 116 | const deleteRuntimeByThreadId = SqlSchema.void({ |
| 127 | Request: DeleteRuntimeRequestSchema, | 117 | Request: DeleteRuntimeRequestSchema, |
| 128 | execute: ({ providerSessionId }) => | 118 | execute: ({ threadId }) => |
| 129 | sql` | 119 | sql` |
| 130 | DELETE FROM provider_session_runtime | 120 | DELETE FROM provider_session_runtime |
| 131 | WHERE provider_session_id = ${providerSessionId} | 121 | WHERE thread_id = ${threadId} |
| 132 | `, | 122 | `, |
| 133 | }); | 123 | }); |
| 134 | 124 | ||
| ... | @@ -142,12 +132,12 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { | ... | @@ -142,12 +132,12 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { |
| 142 | ), | 132 | ), |
| 143 | ); | 133 | ); |
| 144 | 134 | ||
| 145 | const getBySessionId: ProviderSessionRuntimeRepositoryShape["getBySessionId"] = (input) => | 135 | const getByThreadId: ProviderSessionRuntimeRepositoryShape["getByThreadId"] = (input) => |
| 146 | getRuntimeRowBySessionId(input).pipe( | 136 | getRuntimeRowByThreadId(input).pipe( |
| 147 | Effect.mapError( | 137 | Effect.mapError( |
| 148 | toPersistenceSqlOrDecodeError( | 138 | toPersistenceSqlOrDecodeError( |
| 149 | "ProviderSessionRuntimeRepository.getBySessionId:query", | 139 | "ProviderSessionRuntimeRepository.getByThreadId:query", |
| 150 | "ProviderSessionRuntimeRepository.getBySessionId:decodeRow", | 140 | "ProviderSessionRuntimeRepository.getByThreadId:decodeRow", |
| 151 | ), | 141 | ), |
| 152 | ), | 142 | ), |
| 153 | Effect.flatMap((runtimeRowOption) => | 143 | Effect.flatMap((runtimeRowOption) => |
| ... | @@ -157,7 +147,7 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { | ... | @@ -157,7 +147,7 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { |
| 157 | decodeRuntime(row).pipe( | 147 | decodeRuntime(row).pipe( |
| 158 | Effect.mapError( | 148 | Effect.mapError( |
| 159 | toPersistenceDecodeError( | 149 | toPersistenceDecodeError( |
| 160 | "ProviderSessionRuntimeRepository.getBySessionId:rowToRuntime", | 150 | "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime", |
| 161 | ), | 151 | ), |
| 162 | ), | 152 | ), |
| 163 | Effect.map((runtime) => Option.some(runtime)), | 153 | Effect.map((runtime) => Option.some(runtime)), |
| ... | @@ -188,18 +178,18 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { | ... | @@ -188,18 +178,18 @@ const makeProviderSessionRuntimeRepository = Effect.gen(function* () { |
| 188 | ), | 178 | ), |
| 189 | ); | 179 | ); |
| 190 | 180 | ||
| 191 | const deleteBySessionId: ProviderSessionRuntimeRepositoryShape["deleteBySessionId"] = (input) => | 181 | const deleteByThreadId: ProviderSessionRuntimeRepositoryShape["deleteByThreadId"] = (input) => |
| 192 | deleteRuntimeBySessionId(input).pipe( | 182 | deleteRuntimeByThreadId(input).pipe( |
| 193 | Effect.mapError( | 183 | Effect.mapError( |
| 194 | toPersistenceSqlError("ProviderSessionRuntimeRepository.deleteBySessionId:query"), | 184 | toPersistenceSqlError("ProviderSessionRuntimeRepository.deleteByThreadId:query"), |
| 195 | ), | 185 | ), |
| 196 | ); | 186 | ); |
| 197 | 187 | ||
| 198 | return { | 188 | return { |
| 199 | upsert, | 189 | upsert, |
| 200 | getBySessionId, | 190 | getByThreadId, |
| 201 | list, | 191 | list, |
| 202 | deleteBySessionId, | 192 | deleteByThreadId, |
| 203 | } satisfies ProviderSessionRuntimeRepositoryShape; | 193 | } satisfies ProviderSessionRuntimeRepositoryShape; |
| 204 | }); | 194 | }); |
| 205 | 195 |
apps/server/src/persistence/Layers/ProviderSessions.ts deleted-225| ... | @@ -1,225 +0,0 @@ | ||
| 1 | import { ProviderSessionId, ThreadId } from "@t3tools/contracts"; | ||
| 2 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | ||
| 3 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | ||
| 4 | import { Effect, Layer, Option, Schema } from "effect"; | ||
| 5 | |||
| 6 | import { | ||
| 7 | ProviderSessionRepositoryPersistenceError, | ||
| 8 | ProviderSessionRepositoryValidationError, | ||
| 9 | } from "../Errors.ts"; | ||
| 10 | import { | ||
| 11 | ProviderSessionRepository, | ||
| 12 | type ProviderSessionEntry, | ||
| 13 | type ProviderSessionRepositoryShape, | ||
| 14 | } from "../Services/ProviderSessions.ts"; | ||
| 15 | |||
| 16 | const ProviderKind = Schema.Literals(["codex", "claudeCode", "cursor"]); | ||
| 17 | |||
| 18 | const ProviderSessionRowSchema = Schema.Struct({ | ||
| 19 | sessionId: ProviderSessionId, | ||
| 20 | provider: ProviderKind, | ||
| 21 | threadId: Schema.NullOr(ThreadId), | ||
| 22 | createdAt: Schema.String, | ||
| 23 | updatedAt: Schema.String, | ||
| 24 | }); | ||
| 25 | |||
| 26 | const SessionIdRequestSchema = Schema.Struct({ | ||
| 27 | sessionId: ProviderSessionId, | ||
| 28 | }); | ||
| 29 | |||
| 30 | const UpsertSessionRequestSchema = Schema.Struct({ | ||
| 31 | sessionId: ProviderSessionId, | ||
| 32 | provider: ProviderKind, | ||
| 33 | threadId: Schema.NullOr(ThreadId), | ||
| 34 | }); | ||
| 35 | |||
| 36 | function errorMessage(cause: unknown, fallback: string): string { | ||
| 37 | if (cause instanceof Error && cause.message.length > 0) { | ||
| 38 | return cause.message; | ||
| 39 | } | ||
| 40 | return fallback; | ||
| 41 | } | ||
| 42 | |||
| 43 | function toValidationError( | ||
| 44 | operation: string, | ||
| 45 | cause: unknown, | ||
| 46 | ): ProviderSessionRepositoryValidationError { | ||
| 47 | return new ProviderSessionRepositoryValidationError({ | ||
| 48 | operation, | ||
| 49 | issue: errorMessage(cause, "Invalid provider session repository input."), | ||
| 50 | cause, | ||
| 51 | }); | ||
| 52 | } | ||
| 53 | |||
| 54 | function decodeInput<S extends Schema.Top>(schema: S, input: unknown, operation: string) { | ||
| 55 | return Schema.decodeUnknownEffect(schema)(input).pipe( | ||
| 56 | Effect.mapError((cause) => toValidationError(operation, cause)), | ||
| 57 | ); | ||
| 58 | } | ||
| 59 | |||
| 60 | function toPersistenceError( | ||
| 61 | operation: string, | ||
| 62 | cause: unknown, | ||
| 63 | ): ProviderSessionRepositoryPersistenceError { | ||
| 64 | return new ProviderSessionRepositoryPersistenceError({ | ||
| 65 | operation, | ||
| 66 | detail: `Failed to execute ${operation}.`, | ||
| 67 | cause, | ||
| 68 | }); | ||
| 69 | } | ||
| 70 | |||
| 71 | function toEntry(row: Schema.Schema.Type<typeof ProviderSessionRowSchema>): ProviderSessionEntry { | ||
| 72 | return { | ||
| 73 | sessionId: row.sessionId, | ||
| 74 | provider: row.provider, | ||
| 75 | ...(row.threadId !== null ? { threadId: row.threadId } : {}), | ||
| 76 | createdAt: row.createdAt, | ||
| 77 | updatedAt: row.updatedAt, | ||
| 78 | }; | ||
| 79 | } | ||
| 80 | |||
| 81 | const makeProviderSessionRepository = Effect.gen(function* () { | ||
| 82 | const sql = yield* SqlClient.SqlClient; | ||
| 83 | |||
| 84 | const upsertSessionRow = SqlSchema.findOne({ | ||
| 85 | Request: UpsertSessionRequestSchema, | ||
| 86 | Result: ProviderSessionRowSchema, | ||
| 87 | execute: (request) => | ||
| 88 | sql` | ||
| 89 | INSERT INTO provider_sessions ( | ||
| 90 | session_id, | ||
| 91 | provider, | ||
| 92 | thread_id, | ||
| 93 | created_at, | ||
| 94 | updated_at | ||
| 95 | ) | ||
| 96 | VALUES ( | ||
| 97 | ${request.sessionId}, | ||
| 98 | ${request.provider}, | ||
| 99 | ${request.threadId}, | ||
| 100 | CURRENT_TIMESTAMP, | ||
| 101 | CURRENT_TIMESTAMP | ||
| 102 | ) | ||
| 103 | ON CONFLICT (session_id) | ||
| 104 | DO UPDATE SET | ||
| 105 | provider = excluded.provider, | ||
| 106 | thread_id = excluded.thread_id, | ||
| 107 | updated_at = CURRENT_TIMESTAMP | ||
| 108 | RETURNING | ||
| 109 | session_id AS "sessionId", | ||
| 110 | provider, | ||
| 111 | thread_id AS "threadId", | ||
| 112 | created_at AS "createdAt", | ||
| 113 | updated_at AS "updatedAt" | ||
| 114 | `, | ||
| 115 | }); | ||
| 116 | |||
| 117 | const findSessionRow = SqlSchema.findOneOption({ | ||
| 118 | Request: SessionIdRequestSchema, | ||
| 119 | Result: ProviderSessionRowSchema, | ||
| 120 | execute: ({ sessionId }) => | ||
| 121 | sql` | ||
| 122 | SELECT | ||
| 123 | session_id AS "sessionId", | ||
| 124 | provider, | ||
| 125 | thread_id AS "threadId", | ||
| 126 | created_at AS "createdAt", | ||
| 127 | updated_at AS "updatedAt" | ||
| 128 | FROM provider_sessions | ||
| 129 | WHERE session_id = ${sessionId} | ||
| 130 | `, | ||
| 131 | }); | ||
| 132 | |||
| 133 | const listSessionRows = SqlSchema.findAll({ | ||
| 134 | Request: Schema.Void, | ||
| 135 | Result: ProviderSessionRowSchema, | ||
| 136 | execute: () => | ||
| 137 | sql` | ||
| 138 | SELECT | ||
| 139 | session_id AS "sessionId", | ||
| 140 | provider, | ||
| 141 | thread_id AS "threadId", | ||
| 142 | created_at AS "createdAt", | ||
| 143 | updated_at AS "updatedAt" | ||
| 144 | FROM provider_sessions | ||
| 145 | ORDER BY created_at ASC, session_id ASC | ||
| 146 | `, | ||
| 147 | }); | ||
| 148 | |||
| 149 | const deleteSessionRow = SqlSchema.void({ | ||
| 150 | Request: SessionIdRequestSchema, | ||
| 151 | execute: ({ sessionId }) => sql`DELETE FROM provider_sessions WHERE session_id = ${sessionId}`, | ||
| 152 | }); | ||
| 153 | |||
| 154 | const upsertSession: ProviderSessionRepositoryShape["upsertSession"] = (input) => | ||
| 155 | Effect.gen(function* () { | ||
| 156 | const parsed = yield* decodeInput( | ||
| 157 | UpsertSessionRequestSchema, | ||
| 158 | { | ||
| 159 | sessionId: input.sessionId, | ||
| 160 | provider: input.provider, | ||
| 161 | threadId: input.threadId ?? null, | ||
| 162 | }, | ||
| 163 | "ProviderSessionRepository.upsertSession", | ||
| 164 | ); | ||
| 165 | |||
| 166 | yield* upsertSessionRow(parsed).pipe( | ||
| 167 | Effect.mapError((cause) => | ||
| 168 | toPersistenceError("ProviderSessionRepository.upsertSession:query", cause), | ||
| 169 | ), | ||
| 170 | Effect.asVoid, | ||
| 171 | ); | ||
| 172 | }); | ||
| 173 | |||
| 174 | const getSession: ProviderSessionRepositoryShape["getSession"] = (input) => | ||
| 175 | Effect.gen(function* () { | ||
| 176 | const parsed = yield* decodeInput( | ||
| 177 | SessionIdRequestSchema, | ||
| 178 | { sessionId: input.sessionId }, | ||
| 179 | "ProviderSessionRepository.getSession", | ||
| 180 | ); | ||
| 181 | |||
| 182 | const row = yield* findSessionRow(parsed).pipe( | ||
| 183 | Effect.mapError((cause) => | ||
| 184 | toPersistenceError("ProviderSessionRepository.getSession:query", cause), | ||
| 185 | ), | ||
| 186 | ); | ||
| 187 | |||
| 188 | return Option.map(row, toEntry); | ||
| 189 | }); | ||
| 190 | |||
| 191 | const listSessions: ProviderSessionRepositoryShape["listSessions"] = () => | ||
| 192 | listSessionRows(undefined).pipe( | ||
| 193 | Effect.mapError((cause) => | ||
| 194 | toPersistenceError("ProviderSessionRepository.listSessions:query", cause), | ||
| 195 | ), | ||
| 196 | Effect.map((rows) => rows.map(toEntry)), | ||
| 197 | ); | ||
| 198 | |||
| 199 | const deleteSession: ProviderSessionRepositoryShape["deleteSession"] = (input) => | ||
| 200 | Effect.gen(function* () { | ||
| 201 | const parsed = yield* decodeInput( | ||
| 202 | SessionIdRequestSchema, | ||
| 203 | { sessionId: input.sessionId }, | ||
| 204 | "ProviderSessionRepository.deleteSession", | ||
| 205 | ); | ||
| 206 | |||
| 207 | yield* deleteSessionRow(parsed).pipe( | ||
| 208 | Effect.mapError((cause) => | ||
| 209 | toPersistenceError("ProviderSessionRepository.deleteSession:query", cause), | ||
| 210 | ), | ||
| 211 | ); | ||
| 212 | }); | ||
| 213 | |||
| 214 | return { | ||
| 215 | upsertSession, | ||
| 216 | getSession, | ||
| 217 | listSessions, | ||
| 218 | deleteSession, | ||
| 219 | } satisfies ProviderSessionRepositoryShape; | ||
| 220 | }); | ||
| 221 | |||
| 222 | export const ProviderSessionRepositoryLive = Layer.effect( | ||
| 223 | ProviderSessionRepository, | ||
| 224 | makeProviderSessionRepository, | ||
| 225 | ); | ||
apps/server/src/persistence/Migrations/004_ProviderSessionRuntime.ts+2-8| ... | @@ -6,11 +6,10 @@ export default Effect.gen(function* () { | ... | @@ -6,11 +6,10 @@ export default Effect.gen(function* () { |
| 6 | 6 | ||
| 7 | yield* sql` | 7 | yield* sql` |
| 8 | CREATE TABLE IF NOT EXISTS provider_session_runtime ( | 8 | CREATE TABLE IF NOT EXISTS provider_session_runtime ( |
| 9 | provider_session_id TEXT PRIMARY KEY, | 9 | thread_id TEXT PRIMARY KEY, |
| 10 | thread_id TEXT NOT NULL, | ||
| 11 | provider_name TEXT NOT NULL, | 10 | provider_name TEXT NOT NULL, |
| 12 | adapter_key TEXT NOT NULL, | 11 | adapter_key TEXT NOT NULL, |
| 13 | provider_thread_id TEXT, | 12 | runtime_mode TEXT NOT NULL DEFAULT 'full-access', |
| 14 | status TEXT NOT NULL, | 13 | status TEXT NOT NULL, |
| 15 | last_seen_at TEXT NOT NULL, | 14 | last_seen_at TEXT NOT NULL, |
| 16 | resume_cursor_json TEXT, | 15 | resume_cursor_json TEXT, |
| ... | @@ -18,11 +17,6 @@ export default Effect.gen(function* () { | ... | @@ -18,11 +17,6 @@ export default Effect.gen(function* () { |
| 18 | ) | 17 | ) |
| 19 | `; | 18 | `; |
| 20 | 19 | ||
| 21 | yield* sql` | ||
| 22 | CREATE INDEX IF NOT EXISTS idx_provider_session_runtime_thread | ||
| 23 | ON provider_session_runtime(thread_id) | ||
| 24 | `; | ||
| 25 | |||
| 26 | yield* sql` | 20 | yield* sql` |
| 27 | CREATE INDEX IF NOT EXISTS idx_provider_session_runtime_status | 21 | CREATE INDEX IF NOT EXISTS idx_provider_session_runtime_status |
| 28 | ON provider_session_runtime(status) | 22 | ON provider_session_runtime(status) |
apps/server/src/persistence/Migrations/009_ProviderSessionRuntimeMode.ts+1-12| ... | @@ -2,16 +2,5 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; | ... | @@ -2,16 +2,5 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | ||
| 4 | export default Effect.gen(function* () { | 4 | export default Effect.gen(function* () { |
| 5 | const sql = yield* SqlClient.SqlClient; | 5 | yield* SqlClient.SqlClient; |
| 6 | |||
| 7 | yield* sql` | ||
| 8 | ALTER TABLE provider_session_runtime | ||
| 9 | ADD COLUMN runtime_mode TEXT NOT NULL DEFAULT 'full-access' | ||
| 10 | `; | ||
| 11 | |||
| 12 | yield* sql` | ||
| 13 | UPDATE provider_session_runtime | ||
| 14 | SET runtime_mode = 'full-access' | ||
| 15 | WHERE runtime_mode IS NULL | ||
| 16 | `; | ||
| 17 | }); | 6 | }); |
apps/server/src/persistence/Services/ProjectionThreadSessions.ts-4| ... | @@ -10,8 +10,6 @@ import { | ... | @@ -10,8 +10,6 @@ import { |
| 10 | RuntimeMode, | 10 | RuntimeMode, |
| 11 | IsoDateTime, | 11 | IsoDateTime, |
| 12 | OrchestrationSessionStatus, | 12 | OrchestrationSessionStatus, |
| 13 | ProviderSessionId, | ||
| 14 | ProviderThreadId, | ||
| 15 | ThreadId, | 13 | ThreadId, |
| 16 | TurnId, | 14 | TurnId, |
| 17 | } from "@t3tools/contracts"; | 15 | } from "@t3tools/contracts"; |
| ... | @@ -24,8 +22,6 @@ export const ProjectionThreadSession = Schema.Struct({ | ... | @@ -24,8 +22,6 @@ export const ProjectionThreadSession = Schema.Struct({ |
| 24 | threadId: ThreadId, | 22 | threadId: ThreadId, |
| 25 | status: OrchestrationSessionStatus, | 23 | status: OrchestrationSessionStatus, |
| 26 | providerName: Schema.NullOr(Schema.String), | 24 | providerName: Schema.NullOr(Schema.String), |
| 27 | providerSessionId: Schema.NullOr(ProviderSessionId), | ||
| 28 | providerThreadId: Schema.NullOr(ProviderThreadId), | ||
| 29 | runtimeMode: RuntimeMode, | 25 | runtimeMode: RuntimeMode, |
| 30 | activeTurnId: Schema.NullOr(TurnId), | 26 | activeTurnId: Schema.NullOr(TurnId), |
| 31 | lastError: Schema.NullOr(Schema.String), | 27 | lastError: Schema.NullOr(Schema.String), |
apps/server/src/persistence/Services/ProviderSessionRuntime.ts+7-15| ... | @@ -7,9 +7,7 @@ | ... | @@ -7,9 +7,7 @@ |
| 7 | */ | 7 | */ |
| 8 | import { | 8 | import { |
| 9 | IsoDateTime, | 9 | IsoDateTime, |
| 10 | ProviderSessionId, | ||
| 11 | ProviderSessionRuntimeStatus, | 10 | ProviderSessionRuntimeStatus, |
| 12 | ProviderThreadId, | ||
| 13 | RuntimeMode, | 11 | RuntimeMode, |
| 14 | ThreadId, | 12 | ThreadId, |
| 15 | } from "@t3tools/contracts"; | 13 | } from "@t3tools/contracts"; |
| ... | @@ -19,11 +17,9 @@ import type { Effect } from "effect"; | ... | @@ -19,11 +17,9 @@ import type { Effect } from "effect"; |
| 19 | import type { ProviderSessionRuntimeRepositoryError } from "../Errors.ts"; | 17 | import type { ProviderSessionRuntimeRepositoryError } from "../Errors.ts"; |
| 20 | 18 | ||
| 21 | export const ProviderSessionRuntime = Schema.Struct({ | 19 | export const ProviderSessionRuntime = Schema.Struct({ |
| 22 | providerSessionId: ProviderSessionId, | ||
| 23 | threadId: ThreadId, | 20 | threadId: ThreadId, |
| 24 | providerName: Schema.String, | 21 | providerName: Schema.String, |
| 25 | adapterKey: Schema.String, | 22 | adapterKey: Schema.String, |
| 26 | providerThreadId: Schema.NullOr(ProviderThreadId), | ||
| 27 | runtimeMode: RuntimeMode, | 23 | runtimeMode: RuntimeMode, |
| 28 | status: ProviderSessionRuntimeStatus, | 24 | status: ProviderSessionRuntimeStatus, |
| 29 | lastSeenAt: IsoDateTime, | 25 | lastSeenAt: IsoDateTime, |
| ... | @@ -32,14 +28,10 @@ export const ProviderSessionRuntime = Schema.Struct({ | ... | @@ -32,14 +28,10 @@ export const ProviderSessionRuntime = Schema.Struct({ |
| 32 | }); | 28 | }); |
| 33 | export type ProviderSessionRuntime = typeof ProviderSessionRuntime.Type; | 29 | export type ProviderSessionRuntime = typeof ProviderSessionRuntime.Type; |
| 34 | 30 | ||
| 35 | export const GetProviderSessionRuntimeInput = Schema.Struct({ | 31 | export const GetProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId }); |
| 36 | providerSessionId: ProviderSessionId, | ||
| 37 | }); | ||
| 38 | export type GetProviderSessionRuntimeInput = typeof GetProviderSessionRuntimeInput.Type; | 32 | export type GetProviderSessionRuntimeInput = typeof GetProviderSessionRuntimeInput.Type; |
| 39 | 33 | ||
| 40 | export const DeleteProviderSessionRuntimeInput = Schema.Struct({ | 34 | export const DeleteProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId }); |
| 41 | providerSessionId: ProviderSessionId, | ||
| 42 | }); | ||
| 43 | export type DeleteProviderSessionRuntimeInput = typeof DeleteProviderSessionRuntimeInput.Type; | 35 | export type DeleteProviderSessionRuntimeInput = typeof DeleteProviderSessionRuntimeInput.Type; |
| 44 | 36 | ||
| 45 | /** | 37 | /** |
| ... | @@ -49,16 +41,16 @@ export interface ProviderSessionRuntimeRepositoryShape { | ... | @@ -49,16 +41,16 @@ export interface ProviderSessionRuntimeRepositoryShape { |
| 49 | /** | 41 | /** |
| 50 | * Insert or replace a provider runtime row. | 42 | * Insert or replace a provider runtime row. |
| 51 | * | 43 | * |
| 52 | * Upserts by `providerSessionId`, including JSON payload/cursor fields. | 44 | * Upserts by canonical `threadId`, including JSON payload/cursor fields. |
| 53 | */ | 45 | */ |
| 54 | readonly upsert: ( | 46 | readonly upsert: ( |
| 55 | runtime: ProviderSessionRuntime, | 47 | runtime: ProviderSessionRuntime, |
| 56 | ) => Effect.Effect<void, ProviderSessionRuntimeRepositoryError>; | 48 | ) => Effect.Effect<void, ProviderSessionRuntimeRepositoryError>; |
| 57 | 49 | ||
| 58 | /** | 50 | /** |
| 59 | * Read provider runtime state by provider session id. | 51 | * Read provider runtime state by canonical thread id. |
| 60 | */ | 52 | */ |
| 61 | readonly getBySessionId: ( | 53 | readonly getByThreadId: ( |
| 62 | input: GetProviderSessionRuntimeInput, | 54 | input: GetProviderSessionRuntimeInput, |
| 63 | ) => Effect.Effect<Option.Option<ProviderSessionRuntime>, ProviderSessionRuntimeRepositoryError>; | 55 | ) => Effect.Effect<Option.Option<ProviderSessionRuntime>, ProviderSessionRuntimeRepositoryError>; |
| 64 | 56 | ||
| ... | @@ -73,9 +65,9 @@ export interface ProviderSessionRuntimeRepositoryShape { | ... | @@ -73,9 +65,9 @@ export interface ProviderSessionRuntimeRepositoryShape { |
| 73 | >; | 65 | >; |
| 74 | 66 | ||
| 75 | /** | 67 | /** |
| 76 | * Delete provider runtime state by provider session id. | 68 | * Delete provider runtime state by canonical thread id. |
| 77 | */ | 69 | */ |
| 78 | readonly deleteBySessionId: ( | 70 | readonly deleteByThreadId: ( |
| 79 | input: DeleteProviderSessionRuntimeInput, | 71 | input: DeleteProviderSessionRuntimeInput, |
| 80 | ) => Effect.Effect<void, ProviderSessionRuntimeRepositoryError>; | 72 | ) => Effect.Effect<void, ProviderSessionRuntimeRepositoryError>; |
| 81 | } | 73 | } |
apps/server/src/persistence/Services/ProviderSessions.ts deleted-81| ... | @@ -1,81 +0,0 @@ | ||
| 1 | /** | ||
| 2 | * ProviderSessionRepository - Repository interface for provider session lookup. | ||
| 3 | * | ||
| 4 | * Owns persistence operations that map internal sessions to provider kinds and | ||
| 5 | * optional thread ownership. | ||
| 6 | * | ||
| 7 | * @module ProviderSessionRepository | ||
| 8 | */ | ||
| 9 | import type { ProviderKind, ProviderSessionId, ThreadId } from "@t3tools/contracts"; | ||
| 10 | import { Option, ServiceMap } from "effect"; | ||
| 11 | import type { Effect } from "effect"; | ||
| 12 | |||
| 13 | import type { ProviderSessionRepositoryError } from "../Errors.ts"; | ||
| 14 | |||
| 15 | export interface ProviderSessionEntry { | ||
| 16 | readonly sessionId: ProviderSessionId; | ||
| 17 | readonly provider: ProviderKind; | ||
| 18 | readonly threadId?: ThreadId; | ||
| 19 | readonly createdAt: string; | ||
| 20 | readonly updatedAt: string; | ||
| 21 | } | ||
| 22 | |||
| 23 | export interface UpsertProviderSessionInput { | ||
| 24 | readonly sessionId: ProviderSessionId; | ||
| 25 | readonly provider: ProviderKind; | ||
| 26 | readonly threadId?: ThreadId; | ||
| 27 | } | ||
| 28 | |||
| 29 | export interface DeleteProviderSessionInput { | ||
| 30 | readonly sessionId: ProviderSessionId; | ||
| 31 | } | ||
| 32 | |||
| 33 | export interface GetProviderSessionInput { | ||
| 34 | readonly sessionId: ProviderSessionId; | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * ProviderSessionRepositoryShape - Service API for provider-session records. | ||
| 39 | */ | ||
| 40 | export interface ProviderSessionRepositoryShape { | ||
| 41 | /** | ||
| 42 | * Insert or replace a provider-session row. | ||
| 43 | * | ||
| 44 | * Upserts by `sessionId`. | ||
| 45 | */ | ||
| 46 | readonly upsertSession: ( | ||
| 47 | input: UpsertProviderSessionInput, | ||
| 48 | ) => Effect.Effect<void, ProviderSessionRepositoryError>; | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Read a provider-session row by session id. | ||
| 52 | */ | ||
| 53 | readonly getSession: ( | ||
| 54 | input: GetProviderSessionInput, | ||
| 55 | ) => Effect.Effect<Option.Option<ProviderSessionEntry>, ProviderSessionRepositoryError>; | ||
| 56 | |||
| 57 | /** | ||
| 58 | * List all provider-session rows. | ||
| 59 | * | ||
| 60 | * Returned in deterministic creation order. | ||
| 61 | */ | ||
| 62 | readonly listSessions: () => Effect.Effect< | ||
| 63 | ReadonlyArray<ProviderSessionEntry>, | ||
| 64 | ProviderSessionRepositoryError | ||
| 65 | >; | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Delete a provider-session row by session id. | ||
| 69 | */ | ||
| 70 | readonly deleteSession: ( | ||
| 71 | input: DeleteProviderSessionInput, | ||
| 72 | ) => Effect.Effect<void, ProviderSessionRepositoryError>; | ||
| 73 | } | ||
| 74 | |||
| 75 | /** | ||
| 76 | * ProviderSessionRepository - Service tag for provider-session persistence. | ||
| 77 | */ | ||
| 78 | export class ProviderSessionRepository extends ServiceMap.Service< | ||
| 79 | ProviderSessionRepository, | ||
| 80 | ProviderSessionRepositoryShape | ||
| 81 | >()("t3/persistence/Services/ProviderSessions/ProviderSessionRepository") {} | ||
apps/server/src/provider/Errors.ts+8-8| ... | @@ -26,12 +26,12 @@ export class ProviderAdapterSessionNotFoundError extends Schema.TaggedErrorClass | ... | @@ -26,12 +26,12 @@ export class ProviderAdapterSessionNotFoundError extends Schema.TaggedErrorClass |
| 26 | "ProviderAdapterSessionNotFoundError", | 26 | "ProviderAdapterSessionNotFoundError", |
| 27 | { | 27 | { |
| 28 | provider: Schema.String, | 28 | provider: Schema.String, |
| 29 | sessionId: Schema.String, | 29 | threadId: Schema.String, |
| 30 | cause: Schema.optional(Schema.Defect), | 30 | cause: Schema.optional(Schema.Defect), |
| 31 | }, | 31 | }, |
| 32 | ) { | 32 | ) { |
| 33 | override get message(): string { | 33 | override get message(): string { |
| 34 | return `Unknown ${this.provider} adapter session: ${this.sessionId}`; | 34 | return `Unknown ${this.provider} adapter thread: ${this.threadId}`; |
| 35 | } | 35 | } |
| 36 | } | 36 | } |
| 37 | 37 | ||
| ... | @@ -42,12 +42,12 @@ export class ProviderAdapterSessionClosedError extends Schema.TaggedErrorClass<P | ... | @@ -42,12 +42,12 @@ export class ProviderAdapterSessionClosedError extends Schema.TaggedErrorClass<P |
| 42 | "ProviderAdapterSessionClosedError", | 42 | "ProviderAdapterSessionClosedError", |
| 43 | { | 43 | { |
| 44 | provider: Schema.String, | 44 | provider: Schema.String, |
| 45 | sessionId: Schema.String, | 45 | threadId: Schema.String, |
| 46 | cause: Schema.optional(Schema.Defect), | 46 | cause: Schema.optional(Schema.Defect), |
| 47 | }, | 47 | }, |
| 48 | ) { | 48 | ) { |
| 49 | override get message(): string { | 49 | override get message(): string { |
| 50 | return `${this.provider} adapter session is closed: ${this.sessionId}`; | 50 | return `${this.provider} adapter thread is closed: ${this.threadId}`; |
| 51 | } | 51 | } |
| 52 | } | 52 | } |
| 53 | 53 | ||
| ... | @@ -75,13 +75,13 @@ export class ProviderAdapterProcessError extends Schema.TaggedErrorClass<Provide | ... | @@ -75,13 +75,13 @@ export class ProviderAdapterProcessError extends Schema.TaggedErrorClass<Provide |
| 75 | "ProviderAdapterProcessError", | 75 | "ProviderAdapterProcessError", |
| 76 | { | 76 | { |
| 77 | provider: Schema.String, | 77 | provider: Schema.String, |
| 78 | sessionId: Schema.String, | 78 | threadId: Schema.String, |
| 79 | detail: Schema.String, | 79 | detail: Schema.String, |
| 80 | cause: Schema.optional(Schema.Defect), | 80 | cause: Schema.optional(Schema.Defect), |
| 81 | }, | 81 | }, |
| 82 | ) { | 82 | ) { |
| 83 | override get message(): string { | 83 | override get message(): string { |
| 84 | return `Provider adapter process error (${this.provider}) for session ${this.sessionId}: ${this.detail}`; | 84 | return `Provider adapter process error (${this.provider}) for thread ${this.threadId}: ${this.detail}`; |
| 85 | } | 85 | } |
| 86 | } | 86 | } |
| 87 | 87 | ||
| ... | @@ -122,12 +122,12 @@ export class ProviderUnsupportedError extends Schema.TaggedErrorClass<ProviderUn | ... | @@ -122,12 +122,12 @@ export class ProviderUnsupportedError extends Schema.TaggedErrorClass<ProviderUn |
| 122 | export class ProviderSessionNotFoundError extends Schema.TaggedErrorClass<ProviderSessionNotFoundError>()( | 122 | export class ProviderSessionNotFoundError extends Schema.TaggedErrorClass<ProviderSessionNotFoundError>()( |
| 123 | "ProviderSessionNotFoundError", | 123 | "ProviderSessionNotFoundError", |
| 124 | { | 124 | { |
| 125 | sessionId: Schema.String, | 125 | threadId: Schema.String, |
| 126 | cause: Schema.optional(Schema.Defect), | 126 | cause: Schema.optional(Schema.Defect), |
| 127 | }, | 127 | }, |
| 128 | ) { | 128 | ) { |
| 129 | override get message(): string { | 129 | override get message(): string { |
| 130 | return `Unknown provider session: ${this.sessionId}`; | 130 | return `Unknown provider thread: ${this.threadId}`; |
| 131 | } | 131 | } |
| 132 | } | 132 | } |
| 133 | 133 |
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+34-18| ... | @@ -5,7 +5,7 @@ import type { | ... | @@ -5,7 +5,7 @@ import type { |
| 5 | SDKMessage, | 5 | SDKMessage, |
| 6 | SDKUserMessage, | 6 | SDKUserMessage, |
| 7 | } from "@anthropic-ai/claude-agent-sdk"; | 7 | } from "@anthropic-ai/claude-agent-sdk"; |
| 8 | import { ApprovalRequestId, type ProviderTurnId } from "@t3tools/contracts"; | 8 | import { ApprovalRequestId, ThreadId } from "@t3tools/contracts"; |
| 9 | import { assert, describe, it } from "@effect/vitest"; | 9 | import { assert, describe, it } from "@effect/vitest"; |
| 10 | import { Effect, Fiber, Random, Stream } from "effect"; | 10 | import { Effect, Fiber, Random, Stream } from "effect"; |
| 11 | 11 | ||
| ... | @@ -161,13 +161,17 @@ function makeDeterministicRandomService(seed = 0x1234_5678): { | ... | @@ -161,13 +161,17 @@ function makeDeterministicRandomService(seed = 0x1234_5678): { |
| 161 | }; | 161 | }; |
| 162 | } | 162 | } |
| 163 | 163 | ||
| 164 | |||
| 165 | const THREAD_ID = ThreadId.makeUnsafe("thread-claude-1"); | ||
| 166 | const RESUME_THREAD_ID = ThreadId.makeUnsafe("thread-claude-resume"); | ||
| 167 | |||
| 164 | describe("ClaudeCodeAdapterLive", () => { | 168 | describe("ClaudeCodeAdapterLive", () => { |
| 165 | it.effect("returns validation error for non-claudeCode provider on startSession", () => { | 169 | it.effect("returns validation error for non-claudeCode provider on startSession", () => { |
| 166 | const harness = makeHarness(); | 170 | const harness = makeHarness(); |
| 167 | return Effect.gen(function* () { | 171 | return Effect.gen(function* () { |
| 168 | const adapter = yield* ClaudeCodeAdapter; | 172 | const adapter = yield* ClaudeCodeAdapter; |
| 169 | const result = yield* adapter | 173 | const result = yield* adapter |
| 170 | .startSession({ provider: "codex", runtimeMode: "full-access" }) | 174 | .startSession({ threadId: THREAD_ID, provider: "codex", runtimeMode: "full-access" }) |
| 171 | .pipe(Effect.result); | 175 | .pipe(Effect.result); |
| 172 | 176 | ||
| 173 | assert.equal(result._tag, "Failure"); | 177 | assert.equal(result._tag, "Failure"); |
| ... | @@ -193,6 +197,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -193,6 +197,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 193 | return Effect.gen(function* () { | 197 | return Effect.gen(function* () { |
| 194 | const adapter = yield* ClaudeCodeAdapter; | 198 | const adapter = yield* ClaudeCodeAdapter; |
| 195 | yield* adapter.startSession({ | 199 | yield* adapter.startSession({ |
| 200 | threadId: THREAD_ID, | ||
| 196 | provider: "claudeCode", | 201 | provider: "claudeCode", |
| 197 | runtimeMode: "full-access", | 202 | runtimeMode: "full-access", |
| 198 | }); | 203 | }); |
| ... | @@ -211,6 +216,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -211,6 +216,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 211 | return Effect.gen(function* () { | 216 | return Effect.gen(function* () { |
| 212 | const adapter = yield* ClaudeCodeAdapter; | 217 | const adapter = yield* ClaudeCodeAdapter; |
| 213 | yield* adapter.startSession({ | 218 | yield* adapter.startSession({ |
| 219 | threadId: THREAD_ID, | ||
| 214 | provider: "claudeCode", | 220 | provider: "claudeCode", |
| 215 | runtimeMode: "full-access", | 221 | runtimeMode: "full-access", |
| 216 | providerOptions: { | 222 | providerOptions: { |
| ... | @@ -240,13 +246,14 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -240,13 +246,14 @@ describe("ClaudeCodeAdapterLive", () => { |
| 240 | ); | 246 | ); |
| 241 | 247 | ||
| 242 | const session = yield* adapter.startSession({ | 248 | const session = yield* adapter.startSession({ |
| 249 | threadId: THREAD_ID, | ||
| 243 | provider: "claudeCode", | 250 | provider: "claudeCode", |
| 244 | model: "claude-sonnet-4-5", | 251 | model: "claude-sonnet-4-5", |
| 245 | runtimeMode: "full-access", | 252 | runtimeMode: "full-access", |
| 246 | }); | 253 | }); |
| 247 | 254 | ||
| 248 | const turn = yield* adapter.sendTurn({ | 255 | const turn = yield* adapter.sendTurn({ |
| 249 | sessionId: session.sessionId, | 256 | threadId: session.threadId, |
| 250 | input: "hello", | 257 | input: "hello", |
| 251 | attachments: [], | 258 | attachments: [], |
| 252 | }); | 259 | }); |
| ... | @@ -376,12 +383,13 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -376,12 +383,13 @@ describe("ClaudeCodeAdapterLive", () => { |
| 376 | ); | 383 | ); |
| 377 | 384 | ||
| 378 | const session = yield* adapter.startSession({ | 385 | const session = yield* adapter.startSession({ |
| 386 | threadId: THREAD_ID, | ||
| 379 | provider: "claudeCode", | 387 | provider: "claudeCode", |
| 380 | runtimeMode: "full-access", | 388 | runtimeMode: "full-access", |
| 381 | }); | 389 | }); |
| 382 | 390 | ||
| 383 | const turn = yield* adapter.sendTurn({ | 391 | const turn = yield* adapter.sendTurn({ |
| 384 | sessionId: session.sessionId, | 392 | threadId: session.threadId, |
| 385 | input: "hello", | 393 | input: "hello", |
| 386 | attachments: [], | 394 | attachments: [], |
| 387 | }); | 395 | }); |
| ... | @@ -464,12 +472,13 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -464,12 +472,13 @@ describe("ClaudeCodeAdapterLive", () => { |
| 464 | ); | 472 | ); |
| 465 | 473 | ||
| 466 | const session = yield* adapter.startSession({ | 474 | const session = yield* adapter.startSession({ |
| 475 | threadId: THREAD_ID, | ||
| 467 | provider: "claudeCode", | 476 | provider: "claudeCode", |
| 468 | runtimeMode: "full-access", | 477 | runtimeMode: "full-access", |
| 469 | }); | 478 | }); |
| 470 | 479 | ||
| 471 | const turn = yield* adapter.sendTurn({ | 480 | const turn = yield* adapter.sendTurn({ |
| 472 | sessionId: session.sessionId, | 481 | threadId: session.threadId, |
| 473 | input: "hello", | 482 | input: "hello", |
| 474 | attachments: [], | 483 | attachments: [], |
| 475 | }); | 484 | }); |
| ... | @@ -533,13 +542,14 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -533,13 +542,14 @@ describe("ClaudeCodeAdapterLive", () => { |
| 533 | ); | 542 | ); |
| 534 | 543 | ||
| 535 | const session = yield* adapter.startSession({ | 544 | const session = yield* adapter.startSession({ |
| 545 | threadId: THREAD_ID, | ||
| 536 | provider: "claudeCode", | 546 | provider: "claudeCode", |
| 537 | runtimeMode: "full-access", | 547 | runtimeMode: "full-access", |
| 538 | }); | 548 | }); |
| 539 | assert.equal(session.threadId, undefined); | 549 | assert.equal(session.threadId, undefined); |
| 540 | 550 | ||
| 541 | const turn = yield* adapter.sendTurn({ | 551 | const turn = yield* adapter.sendTurn({ |
| 542 | sessionId: session.sessionId, | 552 | threadId: session.threadId, |
| 543 | input: "hello", | 553 | input: "hello", |
| 544 | attachments: [], | 554 | attachments: [], |
| 545 | }); | 555 | }); |
| ... | @@ -602,6 +612,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -602,6 +612,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 602 | const adapter = yield* ClaudeCodeAdapter; | 612 | const adapter = yield* ClaudeCodeAdapter; |
| 603 | 613 | ||
| 604 | const session = yield* adapter.startSession({ | 614 | const session = yield* adapter.startSession({ |
| 615 | threadId: THREAD_ID, | ||
| 605 | provider: "claudeCode", | 616 | provider: "claudeCode", |
| 606 | runtimeMode: "approval-required", | 617 | runtimeMode: "approval-required", |
| 607 | }); | 618 | }); |
| ... | @@ -647,7 +658,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -647,7 +658,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 647 | } | 658 | } |
| 648 | 659 | ||
| 649 | yield* adapter.respondToRequest( | 660 | yield* adapter.respondToRequest( |
| 650 | session.sessionId, | 661 | session.threadId, |
| 651 | ApprovalRequestId.makeUnsafe(runtimeRequestId), | 662 | ApprovalRequestId.makeUnsafe(runtimeRequestId), |
| 652 | "accept", | 663 | "accept", |
| 653 | ); | 664 | ); |
| ... | @@ -678,6 +689,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -678,6 +689,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 678 | const adapter = yield* ClaudeCodeAdapter; | 689 | const adapter = yield* ClaudeCodeAdapter; |
| 679 | 690 | ||
| 680 | const session = yield* adapter.startSession({ | 691 | const session = yield* adapter.startSession({ |
| 692 | threadId: RESUME_THREAD_ID, | ||
| 681 | provider: "claudeCode", | 693 | provider: "claudeCode", |
| 682 | resumeCursor: { | 694 | resumeCursor: { |
| 683 | threadId: "resume-thread-1", | 695 | threadId: "resume-thread-1", |
| ... | @@ -711,6 +723,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -711,6 +723,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 711 | const adapter = yield* ClaudeCodeAdapter; | 723 | const adapter = yield* ClaudeCodeAdapter; |
| 712 | 724 | ||
| 713 | const session = yield* adapter.startSession({ | 725 | const session = yield* adapter.startSession({ |
| 726 | threadId: THREAD_ID, | ||
| 714 | provider: "claudeCode", | 727 | provider: "claudeCode", |
| 715 | runtimeMode: "full-access", | 728 | runtimeMode: "full-access", |
| 716 | }); | 729 | }); |
| ... | @@ -734,12 +747,13 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -734,12 +747,13 @@ describe("ClaudeCodeAdapterLive", () => { |
| 734 | const adapter = yield* ClaudeCodeAdapter; | 747 | const adapter = yield* ClaudeCodeAdapter; |
| 735 | 748 | ||
| 736 | const session = yield* adapter.startSession({ | 749 | const session = yield* adapter.startSession({ |
| 750 | threadId: THREAD_ID, | ||
| 737 | provider: "claudeCode", | 751 | provider: "claudeCode", |
| 738 | runtimeMode: "full-access", | 752 | runtimeMode: "full-access", |
| 739 | }); | 753 | }); |
| 740 | 754 | ||
| 741 | const firstTurn = yield* adapter.sendTurn({ | 755 | const firstTurn = yield* adapter.sendTurn({ |
| 742 | sessionId: session.sessionId, | 756 | threadId: session.threadId, |
| 743 | input: "first", | 757 | input: "first", |
| 744 | attachments: [], | 758 | attachments: [], |
| 745 | }); | 759 | }); |
| ... | @@ -765,7 +779,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -765,7 +779,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 765 | } | 779 | } |
| 766 | 780 | ||
| 767 | const secondTurn = yield* adapter.sendTurn({ | 781 | const secondTurn = yield* adapter.sendTurn({ |
| 768 | sessionId: session.sessionId, | 782 | threadId: session.threadId, |
| 769 | input: "second", | 783 | input: "second", |
| 770 | attachments: [], | 784 | attachments: [], |
| 771 | }); | 785 | }); |
| ... | @@ -790,16 +804,16 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -790,16 +804,16 @@ describe("ClaudeCodeAdapterLive", () => { |
| 790 | assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); | 804 | assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); |
| 791 | } | 805 | } |
| 792 | 806 | ||
| 793 | const threadBeforeRollback = yield* adapter.readThread(session.sessionId); | 807 | const threadBeforeRollback = yield* adapter.readThread(session.threadId); |
| 794 | assert.equal(threadBeforeRollback.turns.length, 2); | 808 | assert.equal(threadBeforeRollback.turns.length, 2); |
| 795 | 809 | ||
| 796 | const rolledBack = yield* adapter.rollbackThread(session.sessionId, 1); | 810 | const rolledBack = yield* adapter.rollbackThread(session.threadId, 1); |
| 797 | assert.equal(rolledBack.turns.length, 1); | 811 | assert.equal(rolledBack.turns.length, 1); |
| 798 | assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId as ProviderTurnId); | 812 | assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId); |
| 799 | 813 | ||
| 800 | const threadAfterRollback = yield* adapter.readThread(session.sessionId); | 814 | const threadAfterRollback = yield* adapter.readThread(session.threadId); |
| 801 | assert.equal(threadAfterRollback.turns.length, 1); | 815 | assert.equal(threadAfterRollback.turns.length, 1); |
| 802 | assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId as ProviderTurnId); | 816 | assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId); |
| 803 | }).pipe( | 817 | }).pipe( |
| 804 | Effect.provideService(Random.Random, makeDeterministicRandomService()), | 818 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 805 | Effect.provide(harness.layer), | 819 | Effect.provide(harness.layer), |
| ... | @@ -812,11 +826,12 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -812,11 +826,12 @@ describe("ClaudeCodeAdapterLive", () => { |
| 812 | const adapter = yield* ClaudeCodeAdapter; | 826 | const adapter = yield* ClaudeCodeAdapter; |
| 813 | 827 | ||
| 814 | const session = yield* adapter.startSession({ | 828 | const session = yield* adapter.startSession({ |
| 829 | threadId: THREAD_ID, | ||
| 815 | provider: "claudeCode", | 830 | provider: "claudeCode", |
| 816 | runtimeMode: "full-access", | 831 | runtimeMode: "full-access", |
| 817 | }); | 832 | }); |
| 818 | yield* adapter.sendTurn({ | 833 | yield* adapter.sendTurn({ |
| 819 | sessionId: session.sessionId, | 834 | threadId: session.threadId, |
| 820 | input: "hello", | 835 | input: "hello", |
| 821 | model: "claude-opus-4-6", | 836 | model: "claude-opus-4-6", |
| 822 | attachments: [], | 837 | attachments: [], |
| ... | @@ -834,7 +849,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -834,7 +849,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 834 | event?: { | 849 | event?: { |
| 835 | provider?: string; | 850 | provider?: string; |
| 836 | method?: string; | 851 | method?: string; |
| 837 | sessionId?: string; | 852 | threadId?: string; |
| 838 | turnId?: string; | 853 | turnId?: string; |
| 839 | }; | 854 | }; |
| 840 | }> = []; | 855 | }> = []; |
| ... | @@ -852,11 +867,12 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -852,11 +867,12 @@ describe("ClaudeCodeAdapterLive", () => { |
| 852 | const adapter = yield* ClaudeCodeAdapter; | 867 | const adapter = yield* ClaudeCodeAdapter; |
| 853 | 868 | ||
| 854 | const session = yield* adapter.startSession({ | 869 | const session = yield* adapter.startSession({ |
| 870 | threadId: THREAD_ID, | ||
| 855 | provider: "claudeCode", | 871 | provider: "claudeCode", |
| 856 | runtimeMode: "full-access", | 872 | runtimeMode: "full-access", |
| 857 | }); | 873 | }); |
| 858 | const turn = yield* adapter.sendTurn({ | 874 | const turn = yield* adapter.sendTurn({ |
| 859 | sessionId: session.sessionId, | 875 | threadId: session.threadId, |
| 860 | input: "hello", | 876 | input: "hello", |
| 861 | attachments: [], | 877 | attachments: [], |
| 862 | }); | 878 | }); |
| ... | @@ -895,7 +911,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -895,7 +911,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 895 | 911 | ||
| 896 | assert.equal(nativeEvents.length > 0, true); | 912 | assert.equal(nativeEvents.length > 0, true); |
| 897 | assert.equal(nativeEvents.some((record) => record.event?.provider === "claudeCode"), true); | 913 | assert.equal(nativeEvents.some((record) => record.event?.provider === "claudeCode"), true); |
| 898 | assert.equal(nativeEvents.some((record) => record.event?.sessionId === session.sessionId), true); | 914 | assert.equal(nativeEvents.some((record) => String(record.event?.threadId) === String(session.threadId)), true); |
| 899 | assert.equal( | 915 | assert.equal( |
| 900 | nativeEvents.some((record) => String(record.event?.turnId) === String(turn.turnId)), | 916 | nativeEvents.some((record) => String(record.event?.turnId) === String(turn.turnId)), |
| 901 | true, | 917 | true, |
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+135-202| ... | @@ -27,13 +27,9 @@ import { | ... | @@ -27,13 +27,9 @@ import { |
| 27 | type ProviderRuntimeEvent, | 27 | type ProviderRuntimeEvent, |
| 28 | type ProviderRuntimeTurnStatus, | 28 | type ProviderRuntimeTurnStatus, |
| 29 | type ProviderSendTurnInput, | 29 | type ProviderSendTurnInput, |
| 30 | ProviderSessionId, | ||
| 31 | type ProviderSession, | 30 | type ProviderSession, |
| 32 | ProviderThreadId, | ||
| 33 | ProviderTurnId, | ||
| 34 | RuntimeItemId, | 31 | RuntimeItemId, |
| 35 | RuntimeRequestId, | 32 | RuntimeRequestId, |
| 36 | RuntimeSessionId, | ||
| 37 | RuntimeTaskId, | 33 | RuntimeTaskId, |
| 38 | ThreadId, | 34 | ThreadId, |
| 39 | TurnId, | 35 | TurnId, |
| ... | @@ -63,14 +59,14 @@ type PromptQueueItem = | ... | @@ -63,14 +59,14 @@ type PromptQueueItem = |
| 63 | }; | 59 | }; |
| 64 | 60 | ||
| 65 | interface ClaudeResumeState { | 61 | interface ClaudeResumeState { |
| 66 | readonly threadId?: ProviderThreadId; | 62 | readonly threadId?: ThreadId; |
| 67 | readonly resume?: string; | 63 | readonly resume?: string; |
| 68 | readonly resumeSessionAt?: string; | 64 | readonly resumeSessionAt?: string; |
| 69 | readonly turnCount?: number; | 65 | readonly turnCount?: number; |
| 70 | } | 66 | } |
| 71 | 67 | ||
| 72 | interface ClaudeTurnState { | 68 | interface ClaudeTurnState { |
| 73 | readonly turnId: ProviderTurnId; | 69 | readonly turnId: TurnId; |
| 74 | readonly assistantItemId: string; | 70 | readonly assistantItemId: string; |
| 75 | readonly startedAt: string; | 71 | readonly startedAt: string; |
| 76 | readonly items: Array<unknown>; | 72 | readonly items: Array<unknown>; |
| ... | @@ -102,13 +98,13 @@ interface ClaudeSessionContext { | ... | @@ -102,13 +98,13 @@ interface ClaudeSessionContext { |
| 102 | resumeSessionId: string | undefined; | 98 | resumeSessionId: string | undefined; |
| 103 | readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>; | 99 | readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>; |
| 104 | readonly turns: Array<{ | 100 | readonly turns: Array<{ |
| 105 | id: ReturnType<typeof ProviderTurnId.makeUnsafe>; | 101 | id: TurnId; |
| 106 | items: Array<unknown>; | 102 | items: Array<unknown>; |
| 107 | }>; | 103 | }>; |
| 108 | readonly inFlightTools: Map<number, ToolInFlight>; | 104 | readonly inFlightTools: Map<number, ToolInFlight>; |
| 109 | turnState: ClaudeTurnState | undefined; | 105 | turnState: ClaudeTurnState | undefined; |
| 110 | lastAssistantUuid: string | undefined; | 106 | lastAssistantUuid: string | undefined; |
| 111 | lastThreadStartedId: ProviderThreadId | undefined; | 107 | lastThreadStartedId: string | undefined; |
| 112 | stopped: boolean; | 108 | stopped: boolean; |
| 113 | } | 109 | } |
| 114 | 110 | ||
| ... | @@ -144,22 +140,14 @@ function toMessage(cause: unknown, fallback: string): string { | ... | @@ -144,22 +140,14 @@ function toMessage(cause: unknown, fallback: string): string { |
| 144 | return fallback; | 140 | return fallback; |
| 145 | } | 141 | } |
| 146 | 142 | ||
| 147 | function asRuntimeSessionId(value: ProviderSessionId): RuntimeSessionId { | ||
| 148 | return RuntimeSessionId.makeUnsafe(value); | ||
| 149 | } | ||
| 150 | |||
| 151 | function asRuntimeThreadId(value: ProviderThreadId): ThreadId { | ||
| 152 | return ThreadId.makeUnsafe(value); | ||
| 153 | } | ||
| 154 | |||
| 155 | function asRuntimeTurnId(value: ProviderTurnId): TurnId { | ||
| 156 | return TurnId.makeUnsafe(value); | ||
| 157 | } | ||
| 158 | |||
| 159 | function asRuntimeItemId(value: string): RuntimeItemId { | 143 | function asRuntimeItemId(value: string): RuntimeItemId { |
| 160 | return RuntimeItemId.makeUnsafe(value); | 144 | return RuntimeItemId.makeUnsafe(value); |
| 161 | } | 145 | } |
| 162 | 146 | ||
| 147 | function asCanonicalTurnId(value: TurnId): TurnId { | ||
| 148 | return value; | ||
| 149 | } | ||
| 150 | |||
| 163 | function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { | 151 | function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { |
| 164 | return RuntimeRequestId.makeUnsafe(value); | 152 | return RuntimeRequestId.makeUnsafe(value); |
| 165 | } | 153 | } |
| ... | @@ -192,7 +180,7 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef | ... | @@ -192,7 +180,7 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef |
| 192 | const threadIdCandidate = typeof cursor.threadId === "string" ? cursor.threadId : undefined; | 180 | const threadIdCandidate = typeof cursor.threadId === "string" ? cursor.threadId : undefined; |
| 193 | const threadId = | 181 | const threadId = |
| 194 | threadIdCandidate && !isSyntheticClaudeThreadId(threadIdCandidate) | 182 | threadIdCandidate && !isSyntheticClaudeThreadId(threadIdCandidate) |
| 195 | ? threadIdCandidate | 183 | ? ThreadId.makeUnsafe(threadIdCandidate) |
| 196 | : undefined; | 184 | : undefined; |
| 197 | const resumeCandidate = | 185 | const resumeCandidate = |
| 198 | typeof cursor.resume === "string" | 186 | typeof cursor.resume === "string" |
| ... | @@ -206,7 +194,7 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef | ... | @@ -206,7 +194,7 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef |
| 206 | const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined; | 194 | const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined; |
| 207 | 195 | ||
| 208 | return { | 196 | return { |
| 209 | ...(threadId ? { threadId: ProviderThreadId.makeUnsafe(threadId) } : {}), | 197 | ...(threadId ? { threadId } : {}), |
| 210 | ...(resume ? { resume } : {}), | 198 | ...(resume ? { resume } : {}), |
| 211 | ...(resumeSessionAt ? { resumeSessionAt } : {}), | 199 | ...(resumeSessionAt ? { resumeSessionAt } : {}), |
| 212 | ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0 | 200 | ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0 |
| ... | @@ -243,6 +231,10 @@ function classifyToolItemType(toolName: string): CanonicalItemType { | ... | @@ -243,6 +231,10 @@ function classifyToolItemType(toolName: string): CanonicalItemType { |
| 243 | } | 231 | } |
| 244 | 232 | ||
| 245 | function classifyRequestType(toolName: string): CanonicalRequestType { | 233 | function classifyRequestType(toolName: string): CanonicalRequestType { |
| 234 | const normalized = toolName.toLowerCase(); | ||
| 235 | if (normalized === "read" || normalized.includes("read file") || normalized.includes("view")) { | ||
| 236 | return "file_read_approval"; | ||
| 237 | } | ||
| 246 | return classifyToolItemType(toolName) === "command_execution" | 238 | return classifyToolItemType(toolName) === "command_execution" |
| 247 | ? "command_execution_approval" | 239 | ? "command_execution_approval" |
| 248 | : "file_change_approval"; | 240 | : "file_change_approval"; |
| ... | @@ -324,6 +316,12 @@ function streamKindFromDeltaType(deltaType: string): "assistant_text" | "reasoni | ... | @@ -324,6 +316,12 @@ function streamKindFromDeltaType(deltaType: string): "assistant_text" | "reasoni |
| 324 | return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text"; | 316 | return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text"; |
| 325 | } | 317 | } |
| 326 | 318 | ||
| 319 | function providerThreadRef( | ||
| 320 | context: ClaudeSessionContext, | ||
| 321 | ): { readonly providerThreadId: string } | {} { | ||
| 322 | return context.resumeSessionId ? { providerThreadId: context.resumeSessionId } : {}; | ||
| 323 | } | ||
| 324 | |||
| 327 | function extractAssistantText(message: SDKMessage): string { | 325 | function extractAssistantText(message: SDKMessage): string { |
| 328 | if (message.type !== "assistant") { | 326 | if (message.type !== "assistant") { |
| 329 | return ""; | 327 | return ""; |
| ... | @@ -353,21 +351,21 @@ function extractAssistantText(message: SDKMessage): string { | ... | @@ -353,21 +351,21 @@ function extractAssistantText(message: SDKMessage): string { |
| 353 | } | 351 | } |
| 354 | 352 | ||
| 355 | function toSessionError( | 353 | function toSessionError( |
| 356 | sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>, | 354 | threadId: ThreadId, |
| 357 | cause: unknown, | 355 | cause: unknown, |
| 358 | ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { | 356 | ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { |
| 359 | const normalized = toMessage(cause, "").toLowerCase(); | 357 | const normalized = toMessage(cause, "").toLowerCase(); |
| 360 | if (normalized.includes("unknown session") || normalized.includes("not found")) { | 358 | if (normalized.includes("unknown session") || normalized.includes("not found")) { |
| 361 | return new ProviderAdapterSessionNotFoundError({ | 359 | return new ProviderAdapterSessionNotFoundError({ |
| 362 | provider: PROVIDER, | 360 | provider: PROVIDER, |
| 363 | sessionId, | 361 | threadId, |
| 364 | cause, | 362 | cause, |
| 365 | }); | 363 | }); |
| 366 | } | 364 | } |
| 367 | if (normalized.includes("closed")) { | 365 | if (normalized.includes("closed")) { |
| 368 | return new ProviderAdapterSessionClosedError({ | 366 | return new ProviderAdapterSessionClosedError({ |
| 369 | provider: PROVIDER, | 367 | provider: PROVIDER, |
| 370 | sessionId, | 368 | threadId, |
| 371 | cause, | 369 | cause, |
| 372 | }); | 370 | }); |
| 373 | } | 371 | } |
| ... | @@ -375,11 +373,11 @@ function toSessionError( | ... | @@ -375,11 +373,11 @@ function toSessionError( |
| 375 | } | 373 | } |
| 376 | 374 | ||
| 377 | function toRequestError( | 375 | function toRequestError( |
| 378 | sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>, | 376 | threadId: ThreadId, |
| 379 | method: string, | 377 | method: string, |
| 380 | cause: unknown, | 378 | cause: unknown, |
| 381 | ): ProviderAdapterError { | 379 | ): ProviderAdapterError { |
| 382 | const sessionError = toSessionError(sessionId, cause); | 380 | const sessionError = toSessionError(threadId, cause); |
| 383 | if (sessionError) { | 381 | if (sessionError) { |
| 384 | return sessionError; | 382 | return sessionError; |
| 385 | } | 383 | } |
| ... | @@ -469,7 +467,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -469,7 +467,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 469 | readonly options: ClaudeQueryOptions; | 467 | readonly options: ClaudeQueryOptions; |
| 470 | }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime); | 468 | }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime); |
| 471 | 469 | ||
| 472 | const sessions = new Map<ProviderSessionId, ClaudeSessionContext>(); | 470 | const sessions = new Map<ThreadId, ClaudeSessionContext>(); |
| 473 | const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); | 471 | const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); |
| 474 | 472 | ||
| 475 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); | 473 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); |
| ... | @@ -502,11 +500,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -502,11 +500,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 502 | : crypto.randomUUID(), | 500 | : crypto.randomUUID(), |
| 503 | kind: "notification", | 501 | kind: "notification", |
| 504 | provider: PROVIDER, | 502 | provider: PROVIDER, |
| 505 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 506 | createdAt: observedAt, | 503 | createdAt: observedAt, |
| 507 | method: sdkNativeMethod(message), | 504 | method: sdkNativeMethod(message), |
| 508 | ...(typeof message.session_id === "string" ? { threadId: message.session_id } : {}), | 505 | ...(typeof message.session_id === "string" |
| 509 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | 506 | ? { providerThreadId: message.session_id } |
| 507 | : {}), | ||
| 508 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), | ||
| 510 | ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}), | 509 | ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}), |
| 511 | payload: message, | 510 | payload: message, |
| 512 | }, | 511 | }, |
| ... | @@ -518,9 +517,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -518,9 +517,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 518 | const snapshotThread = ( | 517 | const snapshotThread = ( |
| 519 | context: ClaudeSessionContext, | 518 | context: ClaudeSessionContext, |
| 520 | ): Effect.Effect<{ | 519 | ): Effect.Effect<{ |
| 521 | threadId: ReturnType<typeof ProviderThreadId.makeUnsafe>; | 520 | threadId: ThreadId; |
| 522 | turns: ReadonlyArray<{ | 521 | turns: ReadonlyArray<{ |
| 523 | id: ReturnType<typeof ProviderTurnId.makeUnsafe>; | 522 | id: TurnId; |
| 524 | items: ReadonlyArray<unknown>; | 523 | items: ReadonlyArray<unknown>; |
| 525 | }>; | 524 | }>; |
| 526 | }, ProviderAdapterValidationError> => | 525 | }, ProviderAdapterValidationError> => |
| ... | @@ -569,19 +568,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -569,19 +568,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 569 | if (typeof message.session_id !== "string" || message.session_id.length === 0) { | 568 | if (typeof message.session_id !== "string" || message.session_id.length === 0) { |
| 570 | return; | 569 | return; |
| 571 | } | 570 | } |
| 572 | const nextThreadId = ProviderThreadId.makeUnsafe(message.session_id); | 571 | const nextThreadId = message.session_id; |
| 573 | context.resumeSessionId = message.session_id; | 572 | context.resumeSessionId = message.session_id; |
| 574 | const changed = context.session.threadId !== nextThreadId; | 573 | yield* updateResumeCursor(context); |
| 575 | |||
| 576 | if (changed) { | ||
| 577 | const updatedAt = yield* nowIso; | ||
| 578 | context.session = { | ||
| 579 | ...context.session, | ||
| 580 | threadId: nextThreadId, | ||
| 581 | updatedAt, | ||
| 582 | }; | ||
| 583 | yield* updateResumeCursor(context); | ||
| 584 | } | ||
| 585 | 574 | ||
| 586 | if (context.lastThreadStartedId !== nextThreadId) { | 575 | if (context.lastThreadStartedId !== nextThreadId) { |
| 587 | context.lastThreadStartedId = nextThreadId; | 576 | context.lastThreadStartedId = nextThreadId; |
| ... | @@ -590,16 +579,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -590,16 +579,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 590 | type: "thread.started", | 579 | type: "thread.started", |
| 591 | eventId: stamp.eventId, | 580 | eventId: stamp.eventId, |
| 592 | provider: PROVIDER, | 581 | provider: PROVIDER, |
| 593 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 594 | createdAt: stamp.createdAt, | 582 | createdAt: stamp.createdAt, |
| 595 | threadId: asRuntimeThreadId(nextThreadId), | 583 | threadId: context.session.threadId, |
| 596 | payload: { | 584 | payload: { |
| 597 | providerThreadId: nextThreadId, | 585 | providerThreadId: nextThreadId, |
| 598 | }, | 586 | }, |
| 599 | providerRefs: { | 587 | providerRefs: {}, |
| 600 | providerSessionId: context.session.sessionId, | ||
| 601 | providerThreadId: nextThreadId, | ||
| 602 | }, | ||
| 603 | raw: { | 588 | raw: { |
| 604 | source: "claude.sdk.message", | 589 | source: "claude.sdk.message", |
| 605 | method: "claude/thread/started", | 590 | method: "claude/thread/started", |
| ... | @@ -626,19 +611,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -626,19 +611,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 626 | type: "runtime.error", | 611 | type: "runtime.error", |
| 627 | eventId: stamp.eventId, | 612 | eventId: stamp.eventId, |
| 628 | provider: PROVIDER, | 613 | provider: PROVIDER, |
| 629 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 630 | createdAt: stamp.createdAt, | 614 | createdAt: stamp.createdAt, |
| 631 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 615 | threadId: context.session.threadId, |
| 632 | ...(turnState ? { turnId: asRuntimeTurnId(turnState.turnId) } : {}), | 616 | ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), |
| 633 | payload: { | 617 | payload: { |
| 634 | message, | 618 | message, |
| 635 | class: "provider_error", | 619 | class: "provider_error", |
| 636 | ...(cause !== undefined ? { detail: cause } : {}), | 620 | ...(cause !== undefined ? { detail: cause } : {}), |
| 637 | }, | 621 | }, |
| 638 | providerRefs: { | 622 | providerRefs: { |
| 639 | providerSessionId: context.session.sessionId, | 623 | ...providerThreadRef(context), |
| 640 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 624 | ...(turnState ? { providerTurnId: String(turnState.turnId) } : {}), |
| 641 | ...(turnState ? { providerTurnId: turnState.turnId } : {}), | ||
| 642 | }, | 625 | }, |
| 643 | }); | 626 | }); |
| 644 | }); | 627 | }); |
| ... | @@ -655,18 +638,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -655,18 +638,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 655 | type: "runtime.warning", | 638 | type: "runtime.warning", |
| 656 | eventId: stamp.eventId, | 639 | eventId: stamp.eventId, |
| 657 | provider: PROVIDER, | 640 | provider: PROVIDER, |
| 658 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 659 | createdAt: stamp.createdAt, | 641 | createdAt: stamp.createdAt, |
| 660 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 642 | threadId: context.session.threadId, |
| 661 | ...(turnState ? { turnId: asRuntimeTurnId(turnState.turnId) } : {}), | 643 | ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), |
| 662 | payload: { | 644 | payload: { |
| 663 | message, | 645 | message, |
| 664 | ...(detail !== undefined ? { detail } : {}), | 646 | ...(detail !== undefined ? { detail } : {}), |
| 665 | }, | 647 | }, |
| 666 | providerRefs: { | 648 | providerRefs: { |
| 667 | providerSessionId: context.session.sessionId, | 649 | ...providerThreadRef(context), |
| 668 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 650 | ...(turnState ? { providerTurnId: String(turnState.turnId) } : {}), |
| 669 | ...(turnState ? { providerTurnId: turnState.turnId } : {}), | ||
| 670 | }, | 651 | }, |
| 671 | }); | 652 | }); |
| 672 | }); | 653 | }); |
| ... | @@ -685,9 +666,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -685,9 +666,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 685 | type: "turn.completed", | 666 | type: "turn.completed", |
| 686 | eventId: stamp.eventId, | 667 | eventId: stamp.eventId, |
| 687 | provider: PROVIDER, | 668 | provider: PROVIDER, |
| 688 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 689 | createdAt: stamp.createdAt, | 669 | createdAt: stamp.createdAt, |
| 690 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 670 | threadId: context.session.threadId, |
| 691 | payload: { | 671 | payload: { |
| 692 | state: status, | 672 | state: status, |
| 693 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), | 673 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), |
| ... | @@ -698,10 +678,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -698,10 +678,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 698 | : {}), | 678 | : {}), |
| 699 | ...(errorMessage ? { errorMessage } : {}), | 679 | ...(errorMessage ? { errorMessage } : {}), |
| 700 | }, | 680 | }, |
| 701 | providerRefs: { | 681 | providerRefs: {}, |
| 702 | providerSessionId: context.session.sessionId, | ||
| 703 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 704 | }, | ||
| 705 | }); | 682 | }); |
| 706 | return; | 683 | return; |
| 707 | } | 684 | } |
| ... | @@ -713,19 +690,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -713,19 +690,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 713 | type: "content.delta", | 690 | type: "content.delta", |
| 714 | eventId: deltaStamp.eventId, | 691 | eventId: deltaStamp.eventId, |
| 715 | provider: PROVIDER, | 692 | provider: PROVIDER, |
| 716 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 717 | createdAt: deltaStamp.createdAt, | 693 | createdAt: deltaStamp.createdAt, |
| 718 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 694 | threadId: context.session.threadId, |
| 719 | turnId: asRuntimeTurnId(turnState.turnId), | 695 | turnId: turnState.turnId, |
| 720 | itemId: asRuntimeItemId(turnState.assistantItemId), | 696 | itemId: asRuntimeItemId(turnState.assistantItemId), |
| 721 | payload: { | 697 | payload: { |
| 722 | streamKind: "assistant_text", | 698 | streamKind: "assistant_text", |
| 723 | delta: turnState.fallbackAssistantText, | 699 | delta: turnState.fallbackAssistantText, |
| 724 | }, | 700 | }, |
| 725 | providerRefs: { | 701 | providerRefs: { |
| 726 | providerSessionId: context.session.sessionId, | 702 | ...providerThreadRef(context), |
| 727 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 703 | providerTurnId: String(turnState.turnId), |
| 728 | providerTurnId: turnState.turnId, | ||
| 729 | providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), | 704 | providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), |
| 730 | }, | 705 | }, |
| 731 | }); | 706 | }); |
| ... | @@ -736,19 +711,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -736,19 +711,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 736 | type: "item.completed", | 711 | type: "item.completed", |
| 737 | eventId: stamp.eventId, | 712 | eventId: stamp.eventId, |
| 738 | provider: PROVIDER, | 713 | provider: PROVIDER, |
| 739 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 740 | createdAt: stamp.createdAt, | 714 | createdAt: stamp.createdAt, |
| 741 | itemId: asRuntimeItemId(turnState.assistantItemId), | 715 | itemId: asRuntimeItemId(turnState.assistantItemId), |
| 742 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 716 | threadId: context.session.threadId, |
| 743 | turnId: asRuntimeTurnId(turnState.turnId), | 717 | turnId: turnState.turnId, |
| 744 | payload: { | 718 | payload: { |
| 745 | itemType: "assistant_message", | 719 | itemType: "assistant_message", |
| 746 | status: "completed", | 720 | status: "completed", |
| 747 | title: "Assistant message", | 721 | title: "Assistant message", |
| 748 | }, | 722 | }, |
| 749 | providerRefs: { | 723 | providerRefs: { |
| 750 | providerSessionId: context.session.sessionId, | 724 | ...providerThreadRef(context), |
| 751 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 752 | providerTurnId: turnState.turnId, | 725 | providerTurnId: turnState.turnId, |
| 753 | providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), | 726 | providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), |
| 754 | }, | 727 | }, |
| ... | @@ -765,10 +738,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -765,10 +738,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 765 | type: "turn.completed", | 738 | type: "turn.completed", |
| 766 | eventId: stamp.eventId, | 739 | eventId: stamp.eventId, |
| 767 | provider: PROVIDER, | 740 | provider: PROVIDER, |
| 768 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 769 | createdAt: stamp.createdAt, | 741 | createdAt: stamp.createdAt, |
| 770 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 742 | threadId: context.session.threadId, |
| 771 | turnId: asRuntimeTurnId(turnState.turnId), | 743 | turnId: turnState.turnId, |
| 772 | payload: { | 744 | payload: { |
| 773 | state: status, | 745 | state: status, |
| 774 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), | 746 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), |
| ... | @@ -780,8 +752,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -780,8 +752,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 780 | ...(errorMessage ? { errorMessage } : {}), | 752 | ...(errorMessage ? { errorMessage } : {}), |
| 781 | }, | 753 | }, |
| 782 | providerRefs: { | 754 | providerRefs: { |
| 783 | providerSessionId: context.session.sessionId, | 755 | ...providerThreadRef(context), |
| 784 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 785 | providerTurnId: turnState.turnId, | 756 | providerTurnId: turnState.turnId, |
| 786 | }, | 757 | }, |
| 787 | }); | 758 | }); |
| ... | @@ -826,18 +797,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -826,18 +797,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 826 | type: "content.delta", | 797 | type: "content.delta", |
| 827 | eventId: stamp.eventId, | 798 | eventId: stamp.eventId, |
| 828 | provider: PROVIDER, | 799 | provider: PROVIDER, |
| 829 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 830 | createdAt: stamp.createdAt, | 800 | createdAt: stamp.createdAt, |
| 831 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 801 | threadId: context.session.threadId, |
| 832 | turnId: asRuntimeTurnId(context.turnState.turnId), | 802 | turnId: context.turnState.turnId, |
| 833 | itemId: asRuntimeItemId(context.turnState.assistantItemId), | 803 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 834 | payload: { | 804 | payload: { |
| 835 | streamKind: streamKindFromDeltaType(event.delta.type), | 805 | streamKind: streamKindFromDeltaType(event.delta.type), |
| 836 | delta: event.delta.text, | 806 | delta: event.delta.text, |
| 837 | }, | 807 | }, |
| 838 | providerRefs: { | 808 | providerRefs: { |
| 839 | providerSessionId: context.session.sessionId, | 809 | ...providerThreadRef(context), |
| 840 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 841 | providerTurnId: context.turnState.turnId, | 810 | providerTurnId: context.turnState.turnId, |
| 842 | providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), | 811 | providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), |
| 843 | }, | 812 | }, |
| ... | @@ -884,10 +853,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -884,10 +853,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 884 | type: "item.started", | 853 | type: "item.started", |
| 885 | eventId: stamp.eventId, | 854 | eventId: stamp.eventId, |
| 886 | provider: PROVIDER, | 855 | provider: PROVIDER, |
| 887 | sessionId: asRuntimeSessionId(context.session.sessionId), | 856 | createdAt: stamp.createdAt, |
| 888 | createdAt: stamp.createdAt, | 857 | threadId: context.session.threadId, |
| 889 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 858 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 890 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | ||
| 891 | itemId: asRuntimeItemId(tool.itemId), | 859 | itemId: asRuntimeItemId(tool.itemId), |
| 892 | payload: { | 860 | payload: { |
| 893 | itemType: tool.itemType, | 861 | itemType: tool.itemType, |
| ... | @@ -900,9 +868,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -900,9 +868,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 900 | }, | 868 | }, |
| 901 | }, | 869 | }, |
| 902 | providerRefs: { | 870 | providerRefs: { |
| 903 | providerSessionId: context.session.sessionId, | 871 | ...providerThreadRef(context), |
| 904 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 872 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 905 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | ||
| 906 | providerItemId: ProviderItemId.makeUnsafe(tool.itemId), | 873 | providerItemId: ProviderItemId.makeUnsafe(tool.itemId), |
| 907 | }, | 874 | }, |
| 908 | raw: { | 875 | raw: { |
| ... | @@ -927,10 +894,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -927,10 +894,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 927 | type: "item.completed", | 894 | type: "item.completed", |
| 928 | eventId: stamp.eventId, | 895 | eventId: stamp.eventId, |
| 929 | provider: PROVIDER, | 896 | provider: PROVIDER, |
| 930 | sessionId: asRuntimeSessionId(context.session.sessionId), | 897 | createdAt: stamp.createdAt, |
| 931 | createdAt: stamp.createdAt, | 898 | threadId: context.session.threadId, |
| 932 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 899 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 933 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | ||
| 934 | itemId: asRuntimeItemId(tool.itemId), | 900 | itemId: asRuntimeItemId(tool.itemId), |
| 935 | payload: { | 901 | payload: { |
| 936 | itemType: tool.itemType, | 902 | itemType: tool.itemType, |
| ... | @@ -939,9 +905,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -939,9 +905,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 939 | ...(tool.detail ? { detail: tool.detail } : {}), | 905 | ...(tool.detail ? { detail: tool.detail } : {}), |
| 940 | }, | 906 | }, |
| 941 | providerRefs: { | 907 | providerRefs: { |
| 942 | providerSessionId: context.session.sessionId, | 908 | ...providerThreadRef(context), |
| 943 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 909 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 944 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | ||
| 945 | providerItemId: ProviderItemId.makeUnsafe(tool.itemId), | 910 | providerItemId: ProviderItemId.makeUnsafe(tool.itemId), |
| 946 | }, | 911 | }, |
| 947 | raw: { | 912 | raw: { |
| ... | @@ -980,10 +945,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -980,10 +945,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 980 | type: "item.updated", | 945 | type: "item.updated", |
| 981 | eventId: stamp.eventId, | 946 | eventId: stamp.eventId, |
| 982 | provider: PROVIDER, | 947 | provider: PROVIDER, |
| 983 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 984 | createdAt: stamp.createdAt, | 948 | createdAt: stamp.createdAt, |
| 985 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 949 | threadId: context.session.threadId, |
| 986 | turnId: asRuntimeTurnId(context.turnState.turnId), | 950 | turnId: context.turnState.turnId, |
| 987 | itemId: asRuntimeItemId(context.turnState.assistantItemId), | 951 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 988 | payload: { | 952 | payload: { |
| 989 | itemType: "assistant_message", | 953 | itemType: "assistant_message", |
| ... | @@ -992,8 +956,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -992,8 +956,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 992 | data: message.message, | 956 | data: message.message, |
| 993 | }, | 957 | }, |
| 994 | providerRefs: { | 958 | providerRefs: { |
| 995 | providerSessionId: context.session.sessionId, | 959 | ...providerThreadRef(context), |
| 996 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 997 | providerTurnId: context.turnState.turnId, | 960 | providerTurnId: context.turnState.turnId, |
| 998 | providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), | 961 | providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), |
| 999 | }, | 962 | }, |
| ... | @@ -1041,13 +1004,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1041,13 +1004,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1041 | const base = { | 1004 | const base = { |
| 1042 | eventId: stamp.eventId, | 1005 | eventId: stamp.eventId, |
| 1043 | provider: PROVIDER, | 1006 | provider: PROVIDER, |
| 1044 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 1045 | createdAt: stamp.createdAt, | 1007 | createdAt: stamp.createdAt, |
| 1046 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1008 | threadId: context.session.threadId, |
| 1047 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | 1009 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1048 | providerRefs: { | 1010 | providerRefs: { |
| 1049 | providerSessionId: context.session.sessionId, | 1011 | ...providerThreadRef(context), |
| 1050 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 1051 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | 1012 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), |
| 1052 | }, | 1013 | }, |
| 1053 | raw: { | 1014 | raw: { |
| ... | @@ -1202,13 +1163,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1202,13 +1163,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1202 | const base = { | 1163 | const base = { |
| 1203 | eventId: stamp.eventId, | 1164 | eventId: stamp.eventId, |
| 1204 | provider: PROVIDER, | 1165 | provider: PROVIDER, |
| 1205 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 1206 | createdAt: stamp.createdAt, | 1166 | createdAt: stamp.createdAt, |
| 1207 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1167 | threadId: context.session.threadId, |
| 1208 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | 1168 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1209 | providerRefs: { | 1169 | providerRefs: { |
| 1210 | providerSessionId: context.session.sessionId, | 1170 | ...providerThreadRef(context), |
| 1211 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 1212 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | 1171 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), |
| 1213 | }, | 1172 | }, |
| 1214 | raw: { | 1173 | raw: { |
| ... | @@ -1284,6 +1243,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1284,6 +1243,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1284 | case "stream_event": | 1243 | case "stream_event": |
| 1285 | yield* handleStreamEvent(context, message); | 1244 | yield* handleStreamEvent(context, message); |
| 1286 | return; | 1245 | return; |
| 1246 | case "user": | ||
| 1247 | return; | ||
| 1287 | case "assistant": | 1248 | case "assistant": |
| 1288 | yield* handleAssistantMessage(context, message); | 1249 | yield* handleAssistantMessage(context, message); |
| 1289 | return; | 1250 | return; |
| ... | @@ -1341,19 +1302,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1341,19 +1302,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1341 | type: "request.resolved", | 1302 | type: "request.resolved", |
| 1342 | eventId: stamp.eventId, | 1303 | eventId: stamp.eventId, |
| 1343 | provider: PROVIDER, | 1304 | provider: PROVIDER, |
| 1344 | sessionId: asRuntimeSessionId(context.session.sessionId), | 1305 | createdAt: stamp.createdAt, |
| 1345 | createdAt: stamp.createdAt, | 1306 | threadId: context.session.threadId, |
| 1346 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1307 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1347 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | ||
| 1348 | requestId: asRuntimeRequestId(requestId), | 1308 | requestId: asRuntimeRequestId(requestId), |
| 1349 | payload: { | 1309 | payload: { |
| 1350 | requestType: pending.requestType, | 1310 | requestType: pending.requestType, |
| 1351 | decision: "cancel", | 1311 | decision: "cancel", |
| 1352 | }, | 1312 | }, |
| 1353 | providerRefs: { | 1313 | providerRefs: { |
| 1354 | providerSessionId: context.session.sessionId, | 1314 | ...providerThreadRef(context), |
| 1355 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 1315 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 1356 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | ||
| 1357 | providerRequestId: requestId, | 1316 | providerRequestId: requestId, |
| 1358 | }, | 1317 | }, |
| 1359 | }); | 1318 | }); |
| ... | @@ -1382,32 +1341,28 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1382,32 +1341,28 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1382 | type: "session.exited", | 1341 | type: "session.exited", |
| 1383 | eventId: stamp.eventId, | 1342 | eventId: stamp.eventId, |
| 1384 | provider: PROVIDER, | 1343 | provider: PROVIDER, |
| 1385 | sessionId: asRuntimeSessionId(context.session.sessionId), | 1344 | createdAt: stamp.createdAt, |
| 1386 | createdAt: stamp.createdAt, | 1345 | threadId: context.session.threadId, |
| 1387 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | ||
| 1388 | payload: { | 1346 | payload: { |
| 1389 | reason: "Session stopped", | 1347 | reason: "Session stopped", |
| 1390 | exitKind: "graceful", | 1348 | exitKind: "graceful", |
| 1391 | }, | 1349 | }, |
| 1392 | providerRefs: { | 1350 | providerRefs: {}, |
| 1393 | providerSessionId: context.session.sessionId, | ||
| 1394 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 1395 | }, | ||
| 1396 | }); | 1351 | }); |
| 1397 | } | 1352 | } |
| 1398 | 1353 | ||
| 1399 | sessions.delete(context.session.sessionId); | 1354 | sessions.delete(context.session.threadId); |
| 1400 | }); | 1355 | }); |
| 1401 | 1356 | ||
| 1402 | const requireSession = ( | 1357 | const requireSession = ( |
| 1403 | sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>, | 1358 | threadId: ThreadId, |
| 1404 | ): Effect.Effect<ClaudeSessionContext, ProviderAdapterError> => { | 1359 | ): Effect.Effect<ClaudeSessionContext, ProviderAdapterError> => { |
| 1405 | const context = sessions.get(sessionId); | 1360 | const context = sessions.get(threadId); |
| 1406 | if (!context) { | 1361 | if (!context) { |
| 1407 | return Effect.fail( | 1362 | return Effect.fail( |
| 1408 | new ProviderAdapterSessionNotFoundError({ | 1363 | new ProviderAdapterSessionNotFoundError({ |
| 1409 | provider: PROVIDER, | 1364 | provider: PROVIDER, |
| 1410 | sessionId, | 1365 | threadId, |
| 1411 | }), | 1366 | }), |
| 1412 | ); | 1367 | ); |
| 1413 | } | 1368 | } |
| ... | @@ -1415,7 +1370,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1415,7 +1370,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1415 | return Effect.fail( | 1370 | return Effect.fail( |
| 1416 | new ProviderAdapterSessionClosedError({ | 1371 | new ProviderAdapterSessionClosedError({ |
| 1417 | provider: PROVIDER, | 1372 | provider: PROVIDER, |
| 1418 | sessionId, | 1373 | threadId, |
| 1419 | }), | 1374 | }), |
| 1420 | ); | 1375 | ); |
| 1421 | } | 1376 | } |
| ... | @@ -1433,11 +1388,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1433,11 +1388,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1433 | } | 1388 | } |
| 1434 | 1389 | ||
| 1435 | const startedAt = yield* nowIso; | 1390 | const startedAt = yield* nowIso; |
| 1436 | const sessionId = ProviderSessionId.makeUnsafe( | ||
| 1437 | `claude-session-${yield* Random.nextUUIDv4}`, | ||
| 1438 | ); | ||
| 1439 | const resumeState = readClaudeResumeState(input.resumeCursor); | 1391 | const resumeState = readClaudeResumeState(input.resumeCursor); |
| 1440 | const threadId = resumeState?.threadId; | 1392 | const threadId = input.threadId; |
| 1441 | 1393 | ||
| 1442 | const promptQueue = yield* Queue.unbounded<PromptQueueItem>(); | 1394 | const promptQueue = yield* Queue.unbounded<PromptQueueItem>(); |
| 1443 | const prompt = Stream.fromQueue(promptQueue).pipe( | 1395 | const prompt = Stream.fromQueue(promptQueue).pipe( |
| ... | @@ -1488,10 +1440,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1488,10 +1440,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1488 | type: "request.opened", | 1440 | type: "request.opened", |
| 1489 | eventId: requestedStamp.eventId, | 1441 | eventId: requestedStamp.eventId, |
| 1490 | provider: PROVIDER, | 1442 | provider: PROVIDER, |
| 1491 | sessionId: asRuntimeSessionId(context.session.sessionId), | 1443 | createdAt: requestedStamp.createdAt, |
| 1492 | createdAt: requestedStamp.createdAt, | 1444 | threadId: context.session.threadId, |
| 1493 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1445 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1494 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | ||
| 1495 | requestId: asRuntimeRequestId(requestId), | 1446 | requestId: asRuntimeRequestId(requestId), |
| 1496 | payload: { | 1447 | payload: { |
| 1497 | requestType, | 1448 | requestType, |
| ... | @@ -1503,11 +1454,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1503,11 +1454,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1503 | }, | 1454 | }, |
| 1504 | }, | 1455 | }, |
| 1505 | providerRefs: { | 1456 | providerRefs: { |
| 1506 | providerSessionId: context.session.sessionId, | 1457 | ...(context.session.threadId |
| 1507 | ...(context.session.threadId | ||
| 1508 | ? { providerThreadId: context.session.threadId } | 1458 | ? { providerThreadId: context.session.threadId } |
| 1509 | : {}), | 1459 | : {}), |
| 1510 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | 1460 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 1511 | providerRequestId: requestId, | 1461 | providerRequestId: requestId, |
| 1512 | }, | 1462 | }, |
| 1513 | raw: { | 1463 | raw: { |
| ... | @@ -1542,21 +1492,19 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1542,21 +1492,19 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1542 | type: "request.resolved", | 1492 | type: "request.resolved", |
| 1543 | eventId: resolvedStamp.eventId, | 1493 | eventId: resolvedStamp.eventId, |
| 1544 | provider: PROVIDER, | 1494 | provider: PROVIDER, |
| 1545 | sessionId: asRuntimeSessionId(context.session.sessionId), | 1495 | createdAt: resolvedStamp.createdAt, |
| 1546 | createdAt: resolvedStamp.createdAt, | 1496 | threadId: context.session.threadId, |
| 1547 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1497 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1548 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | ||
| 1549 | requestId: asRuntimeRequestId(requestId), | 1498 | requestId: asRuntimeRequestId(requestId), |
| 1550 | payload: { | 1499 | payload: { |
| 1551 | requestType, | 1500 | requestType, |
| 1552 | decision, | 1501 | decision, |
| 1553 | }, | 1502 | }, |
| 1554 | providerRefs: { | 1503 | providerRefs: { |
| 1555 | providerSessionId: context.session.sessionId, | 1504 | ...(context.session.threadId |
| 1556 | ...(context.session.threadId | ||
| 1557 | ? { providerThreadId: context.session.threadId } | 1505 | ? { providerThreadId: context.session.threadId } |
| 1558 | : {}), | 1506 | : {}), |
| 1559 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | 1507 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 1560 | providerRequestId: requestId, | 1508 | providerRequestId: requestId, |
| 1561 | }, | 1509 | }, |
| 1562 | raw: { | 1510 | raw: { |
| ... | @@ -1623,14 +1571,14 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1623,14 +1571,14 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1623 | catch: (cause) => | 1571 | catch: (cause) => |
| 1624 | new ProviderAdapterProcessError({ | 1572 | new ProviderAdapterProcessError({ |
| 1625 | provider: PROVIDER, | 1573 | provider: PROVIDER, |
| 1626 | sessionId, | 1574 | threadId, |
| 1627 | detail: toMessage(cause, "Failed to start Claude runtime session."), | 1575 | detail: toMessage(cause, "Failed to start Claude runtime session."), |
| 1628 | cause, | 1576 | cause, |
| 1629 | }), | 1577 | }), |
| 1630 | }); | 1578 | }); |
| 1631 | 1579 | ||
| 1632 | const session: ProviderSession = { | 1580 | const session: ProviderSession = { |
| 1633 | sessionId, | 1581 | threadId, |
| 1634 | provider: PROVIDER, | 1582 | provider: PROVIDER, |
| 1635 | status: "ready", | 1583 | status: "ready", |
| 1636 | runtimeMode: input.runtimeMode, | 1584 | runtimeMode: input.runtimeMode, |
| ... | @@ -1664,21 +1612,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1664,21 +1612,17 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1664 | stopped: false, | 1612 | stopped: false, |
| 1665 | }; | 1613 | }; |
| 1666 | yield* Ref.set(contextRef, context); | 1614 | yield* Ref.set(contextRef, context); |
| 1667 | sessions.set(sessionId, context); | 1615 | sessions.set(threadId, context); |
| 1668 | 1616 | ||
| 1669 | const sessionStartedStamp = yield* makeEventStamp(); | 1617 | const sessionStartedStamp = yield* makeEventStamp(); |
| 1670 | yield* offerRuntimeEvent({ | 1618 | yield* offerRuntimeEvent({ |
| 1671 | type: "session.started", | 1619 | type: "session.started", |
| 1672 | eventId: sessionStartedStamp.eventId, | 1620 | eventId: sessionStartedStamp.eventId, |
| 1673 | provider: PROVIDER, | 1621 | provider: PROVIDER, |
| 1674 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1675 | createdAt: sessionStartedStamp.createdAt, | 1622 | createdAt: sessionStartedStamp.createdAt, |
| 1676 | ...(threadId ? { threadId: asRuntimeThreadId(threadId) } : {}), | 1623 | threadId, |
| 1677 | payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {}, | 1624 | payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {}, |
| 1678 | providerRefs: { | 1625 | providerRefs: {}, |
| 1679 | providerSessionId: sessionId, | ||
| 1680 | ...(threadId ? { providerThreadId: threadId } : {}), | ||
| 1681 | }, | ||
| 1682 | }); | 1626 | }); |
| 1683 | 1627 | ||
| 1684 | const configuredStamp = yield* makeEventStamp(); | 1628 | const configuredStamp = yield* makeEventStamp(); |
| ... | @@ -1686,9 +1630,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1686,9 +1630,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1686 | type: "session.configured", | 1630 | type: "session.configured", |
| 1687 | eventId: configuredStamp.eventId, | 1631 | eventId: configuredStamp.eventId, |
| 1688 | provider: PROVIDER, | 1632 | provider: PROVIDER, |
| 1689 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1690 | createdAt: configuredStamp.createdAt, | 1633 | createdAt: configuredStamp.createdAt, |
| 1691 | ...(threadId ? { threadId: asRuntimeThreadId(threadId) } : {}), | 1634 | threadId, |
| 1692 | payload: { | 1635 | payload: { |
| 1693 | config: { | 1636 | config: { |
| 1694 | ...(input.model ? { model: input.model } : {}), | 1637 | ...(input.model ? { model: input.model } : {}), |
| ... | @@ -1699,10 +1642,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1699,10 +1642,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1699 | : {}), | 1642 | : {}), |
| 1700 | }, | 1643 | }, |
| 1701 | }, | 1644 | }, |
| 1702 | providerRefs: { | 1645 | providerRefs: {}, |
| 1703 | providerSessionId: sessionId, | ||
| 1704 | ...(threadId ? { providerThreadId: threadId } : {}), | ||
| 1705 | }, | ||
| 1706 | }); | 1646 | }); |
| 1707 | 1647 | ||
| 1708 | const readyStamp = yield* makeEventStamp(); | 1648 | const readyStamp = yield* makeEventStamp(); |
| ... | @@ -1710,16 +1650,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1710,16 +1650,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1710 | type: "session.state.changed", | 1650 | type: "session.state.changed", |
| 1711 | eventId: readyStamp.eventId, | 1651 | eventId: readyStamp.eventId, |
| 1712 | provider: PROVIDER, | 1652 | provider: PROVIDER, |
| 1713 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1714 | createdAt: readyStamp.createdAt, | 1653 | createdAt: readyStamp.createdAt, |
| 1715 | ...(threadId ? { threadId: asRuntimeThreadId(threadId) } : {}), | 1654 | threadId, |
| 1716 | payload: { | 1655 | payload: { |
| 1717 | state: "ready", | 1656 | state: "ready", |
| 1718 | }, | 1657 | }, |
| 1719 | providerRefs: { | 1658 | providerRefs: {}, |
| 1720 | providerSessionId: sessionId, | ||
| 1721 | ...(threadId ? { providerThreadId: threadId } : {}), | ||
| 1722 | }, | ||
| 1723 | }); | 1659 | }); |
| 1724 | 1660 | ||
| 1725 | Effect.runFork(runSdkStream(context)); | 1661 | Effect.runFork(runSdkStream(context)); |
| ... | @@ -1731,24 +1667,24 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1731,24 +1667,24 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1731 | 1667 | ||
| 1732 | const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) => | 1668 | const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) => |
| 1733 | Effect.gen(function* () { | 1669 | Effect.gen(function* () { |
| 1734 | const context = yield* requireSession(input.sessionId); | 1670 | const context = yield* requireSession(input.threadId); |
| 1735 | 1671 | ||
| 1736 | if (context.turnState) { | 1672 | if (context.turnState) { |
| 1737 | return yield* new ProviderAdapterValidationError({ | 1673 | return yield* new ProviderAdapterValidationError({ |
| 1738 | provider: PROVIDER, | 1674 | provider: PROVIDER, |
| 1739 | operation: "sendTurn", | 1675 | operation: "sendTurn", |
| 1740 | issue: `Session '${input.sessionId}' already has an active turn '${context.turnState.turnId}'.`, | 1676 | issue: `Thread '${input.threadId}' already has an active turn '${context.turnState.turnId}'.`, |
| 1741 | }); | 1677 | }); |
| 1742 | } | 1678 | } |
| 1743 | 1679 | ||
| 1744 | if (input.model) { | 1680 | if (input.model) { |
| 1745 | yield* Effect.tryPromise({ | 1681 | yield* Effect.tryPromise({ |
| 1746 | try: () => context.query.setModel(input.model), | 1682 | try: () => context.query.setModel(input.model), |
| 1747 | catch: (cause) => toRequestError(input.sessionId, "turn/setModel", cause), | 1683 | catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause), |
| 1748 | }); | 1684 | }); |
| 1749 | } | 1685 | } |
| 1750 | 1686 | ||
| 1751 | const turnId = ProviderTurnId.makeUnsafe(yield* Random.nextUUIDv4); | 1687 | const turnId = TurnId.makeUnsafe(yield* Random.nextUUIDv4); |
| 1752 | const turnState: ClaudeTurnState = { | 1688 | const turnState: ClaudeTurnState = { |
| 1753 | turnId, | 1689 | turnId, |
| 1754 | assistantItemId: yield* Random.nextUUIDv4, | 1690 | assistantItemId: yield* Random.nextUUIDv4, |
| ... | @@ -1773,18 +1709,15 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1773,18 +1709,15 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1773 | type: "turn.started", | 1709 | type: "turn.started", |
| 1774 | eventId: turnStartedStamp.eventId, | 1710 | eventId: turnStartedStamp.eventId, |
| 1775 | provider: PROVIDER, | 1711 | provider: PROVIDER, |
| 1776 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 1777 | createdAt: turnStartedStamp.createdAt, | 1712 | createdAt: turnStartedStamp.createdAt, |
| 1778 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1713 | threadId: context.session.threadId, |
| 1779 | turnId: asRuntimeTurnId(turnId), | 1714 | turnId, |
| 1780 | payload: { | 1715 | payload: { |
| 1781 | ...(input.model ? { model: input.model } : {}), | 1716 | ...(input.model ? { model: input.model } : {}), |
| 1782 | ...(input.effort ? { effort: input.effort } : {}), | 1717 | ...(input.effort ? { effort: input.effort } : {}), |
| 1783 | }, | 1718 | }, |
| 1784 | providerRefs: { | 1719 | providerRefs: { |
| 1785 | providerSessionId: context.session.sessionId, | 1720 | providerTurnId: String(turnId), |
| 1786 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 1787 | providerTurnId: turnId, | ||
| 1788 | }, | 1721 | }, |
| 1789 | }); | 1722 | }); |
| 1790 | 1723 | ||
| ... | @@ -1793,10 +1726,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1793,10 +1726,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1793 | yield* Queue.offer(context.promptQueue, { | 1726 | yield* Queue.offer(context.promptQueue, { |
| 1794 | type: "message", | 1727 | type: "message", |
| 1795 | message, | 1728 | message, |
| 1796 | }).pipe(Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause))); | 1729 | }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause))); |
| 1797 | 1730 | ||
| 1798 | return { | 1731 | return { |
| 1799 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1732 | threadId: context.session.threadId, |
| 1800 | turnId, | 1733 | turnId, |
| 1801 | ...(context.session.resumeCursor !== undefined | 1734 | ...(context.session.resumeCursor !== undefined |
| 1802 | ? { resumeCursor: context.session.resumeCursor } | 1735 | ? { resumeCursor: context.session.resumeCursor } |
| ... | @@ -1804,24 +1737,24 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1804,24 +1737,24 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1804 | }; | 1737 | }; |
| 1805 | }); | 1738 | }); |
| 1806 | 1739 | ||
| 1807 | const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (sessionId, _turnId) => | 1740 | const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (threadId, _turnId) => |
| 1808 | Effect.gen(function* () { | 1741 | Effect.gen(function* () { |
| 1809 | const context = yield* requireSession(sessionId); | 1742 | const context = yield* requireSession(threadId); |
| 1810 | yield* Effect.tryPromise({ | 1743 | yield* Effect.tryPromise({ |
| 1811 | try: () => context.query.interrupt(), | 1744 | try: () => context.query.interrupt(), |
| 1812 | catch: (cause) => toRequestError(sessionId, "turn/interrupt", cause), | 1745 | catch: (cause) => toRequestError(threadId, "turn/interrupt", cause), |
| 1813 | }); | 1746 | }); |
| 1814 | }); | 1747 | }); |
| 1815 | 1748 | ||
| 1816 | const readThread: ClaudeCodeAdapterShape["readThread"] = (sessionId) => | 1749 | const readThread: ClaudeCodeAdapterShape["readThread"] = (threadId) => |
| 1817 | Effect.gen(function* () { | 1750 | Effect.gen(function* () { |
| 1818 | const context = yield* requireSession(sessionId); | 1751 | const context = yield* requireSession(threadId); |
| 1819 | return yield* snapshotThread(context); | 1752 | return yield* snapshotThread(context); |
| 1820 | }); | 1753 | }); |
| 1821 | 1754 | ||
| 1822 | const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (sessionId, numTurns) => | 1755 | const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (threadId, numTurns) => |
| 1823 | Effect.gen(function* () { | 1756 | Effect.gen(function* () { |
| 1824 | const context = yield* requireSession(sessionId); | 1757 | const context = yield* requireSession(threadId); |
| 1825 | const nextLength = Math.max(0, context.turns.length - numTurns); | 1758 | const nextLength = Math.max(0, context.turns.length - numTurns); |
| 1826 | context.turns.splice(nextLength); | 1759 | context.turns.splice(nextLength); |
| 1827 | yield* updateResumeCursor(context); | 1760 | yield* updateResumeCursor(context); |
| ... | @@ -1829,12 +1762,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1829,12 +1762,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1829 | }); | 1762 | }); |
| 1830 | 1763 | ||
| 1831 | const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = ( | 1764 | const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = ( |
| 1832 | sessionId, | 1765 | threadId, |
| 1833 | requestId, | 1766 | requestId, |
| 1834 | decision, | 1767 | decision, |
| 1835 | ) => | 1768 | ) => |
| 1836 | Effect.gen(function* () { | 1769 | Effect.gen(function* () { |
| 1837 | const context = yield* requireSession(sessionId); | 1770 | const context = yield* requireSession(threadId); |
| 1838 | const pending = context.pendingApprovals.get(requestId); | 1771 | const pending = context.pendingApprovals.get(requestId); |
| 1839 | if (!pending) { | 1772 | if (!pending) { |
| 1840 | return yield* new ProviderAdapterRequestError({ | 1773 | return yield* new ProviderAdapterRequestError({ |
| ... | @@ -1848,9 +1781,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1848,9 +1781,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1848 | yield* Deferred.succeed(pending.decision, decision); | 1781 | yield* Deferred.succeed(pending.decision, decision); |
| 1849 | }); | 1782 | }); |
| 1850 | 1783 | ||
| 1851 | const stopSession: ClaudeCodeAdapterShape["stopSession"] = (sessionId) => | 1784 | const stopSession: ClaudeCodeAdapterShape["stopSession"] = (threadId) => |
| 1852 | Effect.gen(function* () { | 1785 | Effect.gen(function* () { |
| 1853 | const context = yield* requireSession(sessionId); | 1786 | const context = yield* requireSession(threadId); |
| 1854 | yield* stopSessionInternal(context, { | 1787 | yield* stopSessionInternal(context, { |
| 1855 | emitExitEvent: true, | 1788 | emitExitEvent: true, |
| 1856 | }); | 1789 | }); |
| ... | @@ -1859,9 +1792,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1859,9 +1792,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1859 | const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => | 1792 | const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => |
| 1860 | Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); | 1793 | Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); |
| 1861 | 1794 | ||
| 1862 | const hasSession: ClaudeCodeAdapterShape["hasSession"] = (sessionId) => | 1795 | const hasSession: ClaudeCodeAdapterShape["hasSession"] = (threadId) => |
| 1863 | Effect.sync(() => { | 1796 | Effect.sync(() => { |
| 1864 | const context = sessions.get(sessionId); | 1797 | const context = sessions.get(threadId); |
| 1865 | return context !== undefined && !context.stopped; | 1798 | return context !== undefined && !context.stopped; |
| 1866 | }); | 1799 | }); |
| 1867 | 1800 |
apps/server/src/provider/Layers/CodexAdapter.test.ts+31-34| ... | @@ -4,12 +4,11 @@ import { | ... | @@ -4,12 +4,11 @@ import { |
| 4 | ProviderItemId, | 4 | ProviderItemId, |
| 5 | type ProviderApprovalDecision, | 5 | type ProviderApprovalDecision, |
| 6 | type ProviderEvent, | 6 | type ProviderEvent, |
| 7 | ProviderSessionId, | ||
| 8 | type ProviderSession, | 7 | type ProviderSession, |
| 9 | type ProviderSessionStartInput, | 8 | type ProviderSessionStartInput, |
| 10 | ProviderThreadId, | ||
| 11 | ProviderTurnId, | ||
| 12 | type ProviderTurnStartResult, | 9 | type ProviderTurnStartResult, |
| 10 | ThreadId, | ||
| 11 | TurnId, | ||
| 13 | } from "@t3tools/contracts"; | 12 | } from "@t3tools/contracts"; |
| 14 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 13 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 15 | import { afterAll, assert, it, vi } from "@effect/vitest"; | 14 | import { afterAll, assert, it, vi } from "@effect/vitest"; |
| ... | @@ -27,8 +26,8 @@ import { CodexAdapter } from "../Services/CodexAdapter.ts"; | ... | @@ -27,8 +26,8 @@ import { CodexAdapter } from "../Services/CodexAdapter.ts"; |
| 27 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; | 26 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; |
| 28 | import { makeCodexAdapterLive } from "./CodexAdapter.ts"; | 27 | import { makeCodexAdapterLive } from "./CodexAdapter.ts"; |
| 29 | 28 | ||
| 30 | const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value); | 29 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); |
| 31 | const asTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); | 30 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 32 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); | 31 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 33 | const asItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(value); | 32 | const asItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(value); |
| 34 | 33 | ||
| ... | @@ -37,11 +36,10 @@ class FakeCodexManager extends CodexAppServerManager { | ... | @@ -37,11 +36,10 @@ class FakeCodexManager extends CodexAppServerManager { |
| 37 | async (input: ProviderSessionStartInput): Promise<ProviderSession> => { | 36 | async (input: ProviderSessionStartInput): Promise<ProviderSession> => { |
| 38 | const now = new Date().toISOString(); | 37 | const now = new Date().toISOString(); |
| 39 | return { | 38 | return { |
| 40 | sessionId: asSessionId("sess-1"), | ||
| 41 | provider: "codex", | 39 | provider: "codex", |
| 42 | status: "ready", | 40 | status: "ready", |
| 43 | runtimeMode: input.runtimeMode, | 41 | runtimeMode: input.runtimeMode, |
| 44 | threadId: ProviderThreadId.makeUnsafe("thread-1"), | 42 | threadId: input.threadId, |
| 45 | cwd: input.cwd, | 43 | cwd: input.cwd, |
| 46 | createdAt: now, | 44 | createdAt: now, |
| 47 | updatedAt: now, | 45 | updatedAt: now, |
| ... | @@ -51,28 +49,28 @@ class FakeCodexManager extends CodexAppServerManager { | ... | @@ -51,28 +49,28 @@ class FakeCodexManager extends CodexAppServerManager { |
| 51 | 49 | ||
| 52 | public sendTurnImpl = vi.fn( | 50 | public sendTurnImpl = vi.fn( |
| 53 | async (_input: CodexAppServerSendTurnInput): Promise<ProviderTurnStartResult> => ({ | 51 | async (_input: CodexAppServerSendTurnInput): Promise<ProviderTurnStartResult> => ({ |
| 54 | threadId: ProviderThreadId.makeUnsafe("thread-1"), | 52 | threadId: asThreadId("thread-1"), |
| 55 | turnId: asTurnId("turn-1"), | 53 | turnId: asTurnId("turn-1"), |
| 56 | }), | 54 | }), |
| 57 | ); | 55 | ); |
| 58 | 56 | ||
| 59 | public interruptTurnImpl = vi.fn( | 57 | public interruptTurnImpl = vi.fn( |
| 60 | async (_sessionId: ProviderSessionId, _turnId?: ProviderTurnId): Promise<void> => undefined, | 58 | async (_threadId: ThreadId, _turnId?: TurnId): Promise<void> => undefined, |
| 61 | ); | 59 | ); |
| 62 | 60 | ||
| 63 | public readThreadImpl = vi.fn(async (_sessionId: ProviderSessionId) => ({ | 61 | public readThreadImpl = vi.fn(async (_threadId: ThreadId) => ({ |
| 64 | threadId: ProviderThreadId.makeUnsafe("thread-1"), | 62 | threadId: asThreadId("thread-1"), |
| 65 | turns: [], | 63 | turns: [], |
| 66 | })); | 64 | })); |
| 67 | 65 | ||
| 68 | public rollbackThreadImpl = vi.fn(async (_sessionId: ProviderSessionId, _numTurns: number) => ({ | 66 | public rollbackThreadImpl = vi.fn(async (_threadId: ThreadId, _numTurns: number) => ({ |
| 69 | threadId: ProviderThreadId.makeUnsafe("thread-1"), | 67 | threadId: asThreadId("thread-1"), |
| 70 | turns: [], | 68 | turns: [], |
| 71 | })); | 69 | })); |
| 72 | 70 | ||
| 73 | public respondToRequestImpl = vi.fn( | 71 | public respondToRequestImpl = vi.fn( |
| 74 | async ( | 72 | async ( |
| 75 | _sessionId: ProviderSessionId, | 73 | _threadId: ThreadId, |
| 76 | _requestId: ApprovalRequestId, | 74 | _requestId: ApprovalRequestId, |
| 77 | _decision: ProviderApprovalDecision, | 75 | _decision: ProviderApprovalDecision, |
| 78 | ): Promise<void> => undefined, | 76 | ): Promise<void> => undefined, |
| ... | @@ -88,33 +86,33 @@ class FakeCodexManager extends CodexAppServerManager { | ... | @@ -88,33 +86,33 @@ class FakeCodexManager extends CodexAppServerManager { |
| 88 | return this.sendTurnImpl(input); | 86 | return this.sendTurnImpl(input); |
| 89 | } | 87 | } |
| 90 | 88 | ||
| 91 | override interruptTurn(sessionId: ProviderSessionId, turnId?: ProviderTurnId): Promise<void> { | 89 | override interruptTurn(threadId: ThreadId, turnId?: TurnId): Promise<void> { |
| 92 | return this.interruptTurnImpl(sessionId, turnId); | 90 | return this.interruptTurnImpl(threadId, turnId); |
| 93 | } | 91 | } |
| 94 | 92 | ||
| 95 | override readThread(sessionId: ProviderSessionId) { | 93 | override readThread(threadId: ThreadId) { |
| 96 | return this.readThreadImpl(sessionId); | 94 | return this.readThreadImpl(threadId); |
| 97 | } | 95 | } |
| 98 | 96 | ||
| 99 | override rollbackThread(sessionId: ProviderSessionId, numTurns: number) { | 97 | override rollbackThread(threadId: ThreadId, numTurns: number) { |
| 100 | return this.rollbackThreadImpl(sessionId, numTurns); | 98 | return this.rollbackThreadImpl(threadId, numTurns); |
| 101 | } | 99 | } |
| 102 | 100 | ||
| 103 | override respondToRequest( | 101 | override respondToRequest( |
| 104 | sessionId: ProviderSessionId, | 102 | threadId: ThreadId, |
| 105 | requestId: ApprovalRequestId, | 103 | requestId: ApprovalRequestId, |
| 106 | decision: ProviderApprovalDecision, | 104 | decision: ProviderApprovalDecision, |
| 107 | ): Promise<void> { | 105 | ): Promise<void> { |
| 108 | return this.respondToRequestImpl(sessionId, requestId, decision); | 106 | return this.respondToRequestImpl(threadId, requestId, decision); |
| 109 | } | 107 | } |
| 110 | 108 | ||
| 111 | override stopSession(_sessionId: ProviderSessionId): void {} | 109 | override stopSession(_threadId: ThreadId): void {} |
| 112 | 110 | ||
| 113 | override listSessions(): ProviderSession[] { | 111 | override listSessions(): ProviderSession[] { |
| 114 | return []; | 112 | return []; |
| 115 | } | 113 | } |
| 116 | 114 | ||
| 117 | override hasSession(_sessionId: ProviderSessionId): boolean { | 115 | override hasSession(_threadId: ThreadId): boolean { |
| 118 | return false; | 116 | return false; |
| 119 | } | 117 | } |
| 120 | 118 | ||
| ... | @@ -128,9 +126,8 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory | ... | @@ -128,9 +126,8 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory |
| 128 | getProvider: () => | 126 | getProvider: () => |
| 129 | Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")), | 127 | Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")), |
| 130 | getBinding: () => Effect.succeed(Option.none()), | 128 | getBinding: () => Effect.succeed(Option.none()), |
| 131 | getThreadId: () => Effect.succeed(Option.none()), | ||
| 132 | remove: () => Effect.void, | 129 | remove: () => Effect.void, |
| 133 | listSessionIds: () => Effect.succeed([]), | 130 | listThreadIds: () => Effect.succeed([]), |
| 134 | }); | 131 | }); |
| 135 | 132 | ||
| 136 | const validationManager = new FakeCodexManager(); | 133 | const validationManager = new FakeCodexManager(); |
| ... | @@ -149,6 +146,7 @@ validationLayer("CodexAdapterLive validation", (it) => { | ... | @@ -149,6 +146,7 @@ validationLayer("CodexAdapterLive validation", (it) => { |
| 149 | const result = yield* adapter | 146 | const result = yield* adapter |
| 150 | .startSession({ | 147 | .startSession({ |
| 151 | provider: "claudeCode", | 148 | provider: "claudeCode", |
| 149 | threadId: asThreadId("thread-1"), | ||
| 152 | runtimeMode: "full-access", | 150 | runtimeMode: "full-access", |
| 153 | }) | 151 | }) |
| 154 | .pipe(Effect.result); | 152 | .pipe(Effect.result); |
| ... | @@ -184,7 +182,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { | ... | @@ -184,7 +182,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { |
| 184 | const adapter = yield* CodexAdapter; | 182 | const adapter = yield* CodexAdapter; |
| 185 | const result = yield* adapter | 183 | const result = yield* adapter |
| 186 | .sendTurn({ | 184 | .sendTurn({ |
| 187 | sessionId: asSessionId("sess-missing"), | 185 | threadId: asThreadId("sess-missing"), |
| 188 | input: "hello", | 186 | input: "hello", |
| 189 | attachments: [], | 187 | attachments: [], |
| 190 | }) | 188 | }) |
| ... | @@ -200,7 +198,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { | ... | @@ -200,7 +198,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { |
| 200 | return; | 198 | return; |
| 201 | } | 199 | } |
| 202 | assert.equal(result.failure.provider, "codex"); | 200 | assert.equal(result.failure.provider, "codex"); |
| 203 | assert.equal(result.failure.sessionId, "sess-missing"); | 201 | assert.equal(result.failure.threadId, "sess-missing"); |
| 204 | assert.instanceOf(result.failure.cause, Error); | 202 | assert.instanceOf(result.failure.cause, Error); |
| 205 | }), | 203 | }), |
| 206 | ); | 204 | ); |
| ... | @@ -225,10 +223,9 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { | ... | @@ -225,10 +223,9 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { |
| 225 | id: asEventId("evt-msg-complete"), | 223 | id: asEventId("evt-msg-complete"), |
| 226 | kind: "notification", | 224 | kind: "notification", |
| 227 | provider: "codex", | 225 | provider: "codex", |
| 228 | sessionId: asSessionId("sess-1"), | ||
| 229 | createdAt: new Date().toISOString(), | 226 | createdAt: new Date().toISOString(), |
| 230 | method: "item/completed", | 227 | method: "item/completed", |
| 231 | threadId: ProviderThreadId.makeUnsafe("thread-1"), | 228 | threadId: asThreadId("thread-1"), |
| 232 | turnId: asTurnId("turn-1"), | 229 | turnId: asTurnId("turn-1"), |
| 233 | itemId: asItemId("msg_1"), | 230 | itemId: asItemId("msg_1"), |
| 234 | payload: { | 231 | payload: { |
| ... | @@ -265,7 +262,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { | ... | @@ -265,7 +262,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { |
| 265 | id: asEventId("evt-session-closed"), | 262 | id: asEventId("evt-session-closed"), |
| 266 | kind: "session", | 263 | kind: "session", |
| 267 | provider: "codex", | 264 | provider: "codex", |
| 268 | sessionId: asSessionId("sess-1"), | 265 | threadId: asThreadId("thread-1"), |
| 269 | createdAt: new Date().toISOString(), | 266 | createdAt: new Date().toISOString(), |
| 270 | method: "session/closed", | 267 | method: "session/closed", |
| 271 | message: "Session stopped", | 268 | message: "Session stopped", |
| ... | @@ -282,7 +279,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { | ... | @@ -282,7 +279,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { |
| 282 | if (firstEvent.value.type !== "session.exited") { | 279 | if (firstEvent.value.type !== "session.exited") { |
| 283 | return; | 280 | return; |
| 284 | } | 281 | } |
| 285 | assert.equal(firstEvent.value.sessionId, "sess-1"); | 282 | assert.equal(firstEvent.value.threadId, "thread-1"); |
| 286 | assert.equal(firstEvent.value.payload.reason, "Session stopped"); | 283 | assert.equal(firstEvent.value.payload.reason, "Session stopped"); |
| 287 | }), | 284 | }), |
| 288 | ); | 285 | ); |
| ... | @@ -296,7 +293,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { | ... | @@ -296,7 +293,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { |
| 296 | id: asEventId("evt-request-resolved"), | 293 | id: asEventId("evt-request-resolved"), |
| 297 | kind: "notification", | 294 | kind: "notification", |
| 298 | provider: "codex", | 295 | provider: "codex", |
| 299 | sessionId: asSessionId("sess-1"), | 296 | threadId: asThreadId("thread-1"), |
| 300 | createdAt: new Date().toISOString(), | 297 | createdAt: new Date().toISOString(), |
| 301 | method: "serverRequest/resolved", | 298 | method: "serverRequest/resolved", |
| 302 | requestId: ApprovalRequestId.makeUnsafe("req-1"), | 299 | requestId: ApprovalRequestId.makeUnsafe("req-1"), |
| ... | @@ -334,7 +331,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { | ... | @@ -334,7 +331,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { |
| 334 | id: asEventId("evt-windows-sandbox-failed"), | 331 | id: asEventId("evt-windows-sandbox-failed"), |
| 335 | kind: "notification", | 332 | kind: "notification", |
| 336 | provider: "codex", | 333 | provider: "codex", |
| 337 | sessionId: asSessionId("sess-1"), | 334 | threadId: asThreadId("thread-1"), |
| 338 | createdAt: new Date().toISOString(), | 335 | createdAt: new Date().toISOString(), |
| 339 | method: "windowsSandbox/setupCompleted", | 336 | method: "windowsSandbox/setupCompleted", |
| 340 | message: "Sandbox setup failed", | 337 | message: "Sandbox setup failed", |
apps/server/src/provider/Layers/CodexAdapter.ts+150-153| ... | @@ -13,16 +13,11 @@ import { | ... | @@ -13,16 +13,11 @@ import { |
| 13 | type ProviderRuntimeEvent, | 13 | type ProviderRuntimeEvent, |
| 14 | RuntimeItemId, | 14 | RuntimeItemId, |
| 15 | RuntimeRequestId, | 15 | RuntimeRequestId, |
| 16 | RuntimeSessionId, | ||
| 17 | ProviderApprovalDecision, | 16 | ProviderApprovalDecision, |
| 18 | ProviderItemId, | 17 | ProviderItemId, |
| 19 | ProviderSessionId, | ||
| 20 | ProviderThreadId, | ||
| 21 | ProviderTurnId, | ||
| 22 | ThreadId, | 18 | ThreadId, |
| 23 | TurnId, | ||
| 24 | } from "@t3tools/contracts"; | 19 | } from "@t3tools/contracts"; |
| 25 | import { Effect, FileSystem, Layer, Option, Queue, Schema, Stream } from "effect"; | 20 | import { Effect, FileSystem, Layer, Queue, Schema, ServiceMap, Stream } from "effect"; |
| 26 | 21 | ||
| 27 | import { | 22 | import { |
| 28 | ProviderAdapterProcessError, | 23 | ProviderAdapterProcessError, |
| ... | @@ -33,7 +28,6 @@ import { | ... | @@ -33,7 +28,6 @@ import { |
| 33 | type ProviderAdapterError, | 28 | type ProviderAdapterError, |
| 34 | } from "../Errors.ts"; | 29 | } from "../Errors.ts"; |
| 35 | import { CodexAdapter, type CodexAdapterShape } from "../Services/CodexAdapter.ts"; | 30 | import { CodexAdapter, type CodexAdapterShape } from "../Services/CodexAdapter.ts"; |
| 36 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; | ||
| 37 | import { CodexAppServerManager } from "../../codexAppServerManager.ts"; | 31 | import { CodexAppServerManager } from "../../codexAppServerManager.ts"; |
| 38 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; | 32 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| 39 | import { ServerConfig } from "../../config.ts"; | 33 | import { ServerConfig } from "../../config.ts"; |
| ... | @@ -43,7 +37,7 @@ const PROVIDER = "codex" as const; | ... | @@ -43,7 +37,7 @@ const PROVIDER = "codex" as const; |
| 43 | 37 | ||
| 44 | export interface CodexAdapterLiveOptions { | 38 | export interface CodexAdapterLiveOptions { |
| 45 | readonly manager?: CodexAppServerManager; | 39 | readonly manager?: CodexAppServerManager; |
| 46 | readonly makeManager?: () => CodexAppServerManager; | 40 | readonly makeManager?: (services?: ServiceMap.ServiceMap<never>) => CodexAppServerManager; |
| 47 | readonly nativeEventLogPath?: string; | 41 | readonly nativeEventLogPath?: string; |
| 48 | readonly nativeEventLogger?: EventNdjsonLogger; | 42 | readonly nativeEventLogger?: EventNdjsonLogger; |
| 49 | } | 43 | } |
| ... | @@ -56,33 +50,29 @@ function toMessage(cause: unknown, fallback: string): string { | ... | @@ -56,33 +50,29 @@ function toMessage(cause: unknown, fallback: string): string { |
| 56 | } | 50 | } |
| 57 | 51 | ||
| 58 | function toSessionError( | 52 | function toSessionError( |
| 59 | sessionId: ProviderSessionId, | 53 | threadId: ThreadId, |
| 60 | cause: unknown, | 54 | cause: unknown, |
| 61 | ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { | 55 | ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { |
| 62 | const normalized = toMessage(cause, "").toLowerCase(); | 56 | const normalized = toMessage(cause, "").toLowerCase(); |
| 63 | if (normalized.includes("unknown session") || normalized.includes("unknown provider session")) { | 57 | if (normalized.includes("unknown session") || normalized.includes("unknown provider session")) { |
| 64 | return new ProviderAdapterSessionNotFoundError({ | 58 | return new ProviderAdapterSessionNotFoundError({ |
| 65 | provider: PROVIDER, | 59 | provider: PROVIDER, |
| 66 | sessionId, | 60 | threadId, |
| 67 | cause, | 61 | cause, |
| 68 | }); | 62 | }); |
| 69 | } | 63 | } |
| 70 | if (normalized.includes("session is closed")) { | 64 | if (normalized.includes("session is closed")) { |
| 71 | return new ProviderAdapterSessionClosedError({ | 65 | return new ProviderAdapterSessionClosedError({ |
| 72 | provider: PROVIDER, | 66 | provider: PROVIDER, |
| 73 | sessionId, | 67 | threadId, |
| 74 | cause, | 68 | cause, |
| 75 | }); | 69 | }); |
| 76 | } | 70 | } |
| 77 | return undefined; | 71 | return undefined; |
| 78 | } | 72 | } |
| 79 | 73 | ||
| 80 | function toRequestError( | 74 | function toRequestError(threadId: ThreadId, method: string, cause: unknown): ProviderAdapterError { |
| 81 | sessionId: ProviderSessionId, | 75 | const sessionError = toSessionError(threadId, cause); |
| 82 | method: string, | ||
| 83 | cause: unknown, | ||
| 84 | ): ProviderAdapterError { | ||
| 85 | const sessionError = toSessionError(sessionId, cause); | ||
| 86 | if (sessionError) { | 76 | if (sessionError) { |
| 87 | return sessionError; | 77 | return sessionError; |
| 88 | } | 78 | } |
| ... | @@ -139,7 +129,8 @@ function toCanonicalItemType(raw: unknown): CanonicalItemType { | ... | @@ -139,7 +129,8 @@ function toCanonicalItemType(raw: unknown): CanonicalItemType { |
| 139 | if (type.includes("reasoning") || type.includes("thought")) return "reasoning"; | 129 | if (type.includes("reasoning") || type.includes("thought")) return "reasoning"; |
| 140 | if (type.includes("plan") || type.includes("todo")) return "plan"; | 130 | if (type.includes("plan") || type.includes("todo")) return "plan"; |
| 141 | if (type.includes("command")) return "command_execution"; | 131 | if (type.includes("command")) return "command_execution"; |
| 142 | if (type.includes("file change") || type.includes("patch") || type.includes("edit")) return "file_change"; | 132 | if (type.includes("file change") || type.includes("patch") || type.includes("edit")) |
| 133 | return "file_change"; | ||
| 143 | if (type.includes("mcp")) return "mcp_tool_call"; | 134 | if (type.includes("mcp")) return "mcp_tool_call"; |
| 144 | if (type.includes("dynamic tool")) return "dynamic_tool_call"; | 135 | if (type.includes("dynamic tool")) return "dynamic_tool_call"; |
| 145 | if (type.includes("collab")) return "collab_agent_tool_call"; | 136 | if (type.includes("collab")) return "collab_agent_tool_call"; |
| ... | @@ -211,6 +202,8 @@ function toRequestTypeFromMethod(method: string): CanonicalRequestType { | ... | @@ -211,6 +202,8 @@ function toRequestTypeFromMethod(method: string): CanonicalRequestType { |
| 211 | switch (method) { | 202 | switch (method) { |
| 212 | case "item/commandExecution/requestApproval": | 203 | case "item/commandExecution/requestApproval": |
| 213 | return "command_execution_approval"; | 204 | return "command_execution_approval"; |
| 205 | case "item/fileRead/requestApproval": | ||
| 206 | return "file_read_approval"; | ||
| 214 | case "item/fileChange/requestApproval": | 207 | case "item/fileChange/requestApproval": |
| 215 | return "file_change_approval"; | 208 | return "file_change_approval"; |
| 216 | case "applyPatchApproval": | 209 | case "applyPatchApproval": |
| ... | @@ -232,6 +225,8 @@ function toRequestTypeFromKind(kind: unknown): CanonicalRequestType { | ... | @@ -232,6 +225,8 @@ function toRequestTypeFromKind(kind: unknown): CanonicalRequestType { |
| 232 | switch (kind) { | 225 | switch (kind) { |
| 233 | case "command": | 226 | case "command": |
| 234 | return "command_execution_approval"; | 227 | return "command_execution_approval"; |
| 228 | case "file-read": | ||
| 229 | return "file_read_approval"; | ||
| 235 | case "file-change": | 230 | case "file-change": |
| 236 | return "file_change_approval"; | 231 | return "file_change_approval"; |
| 237 | default: | 232 | default: |
| ... | @@ -239,7 +234,9 @@ function toRequestTypeFromKind(kind: unknown): CanonicalRequestType { | ... | @@ -239,7 +234,9 @@ function toRequestTypeFromKind(kind: unknown): CanonicalRequestType { |
| 239 | } | 234 | } |
| 240 | } | 235 | } |
| 241 | 236 | ||
| 242 | function toRequestTypeFromResolvedPayload(payload: Record<string, unknown> | undefined): CanonicalRequestType { | 237 | function toRequestTypeFromResolvedPayload( |
| 238 | payload: Record<string, unknown> | undefined, | ||
| 239 | ): CanonicalRequestType { | ||
| 243 | const request = asObject(payload?.request); | 240 | const request = asObject(payload?.request); |
| 244 | const method = asString(request?.method) ?? asString(payload?.method); | 241 | const method = asString(request?.method) ?? asString(payload?.method); |
| 245 | if (method) { | 242 | if (method) { |
| ... | @@ -252,13 +249,9 @@ function toRequestTypeFromResolvedPayload(payload: Record<string, unknown> | und | ... | @@ -252,13 +249,9 @@ function toRequestTypeFromResolvedPayload(payload: Record<string, unknown> | und |
| 252 | return "unknown"; | 249 | return "unknown"; |
| 253 | } | 250 | } |
| 254 | 251 | ||
| 255 | function toThreadState(value: unknown): | 252 | function toThreadState( |
| 256 | | "active" | 253 | value: unknown, |
| 257 | | "idle" | 254 | ): "active" | "idle" | "archived" | "closed" | "compacted" | "error" { |
| 258 | | "archived" | ||
| 259 | | "closed" | ||
| 260 | | "compacted" | ||
| 261 | | "error" { | ||
| 262 | switch (value) { | 255 | switch (value) { |
| 263 | case "idle": | 256 | case "idle": |
| 264 | return "idle"; | 257 | return "idle"; |
| ... | @@ -278,7 +271,13 @@ function toThreadState(value: unknown): | ... | @@ -278,7 +271,13 @@ function toThreadState(value: unknown): |
| 278 | 271 | ||
| 279 | function contentStreamKindFromMethod( | 272 | function contentStreamKindFromMethod( |
| 280 | method: string, | 273 | method: string, |
| 281 | ): "assistant_text" | "reasoning_text" | "reasoning_summary_text" | "plan_text" | "command_output" | "file_change_output" { | 274 | ): |
| 275 | | "assistant_text" | ||
| 276 | | "reasoning_text" | ||
| 277 | | "reasoning_summary_text" | ||
| 278 | | "plan_text" | ||
| 279 | | "command_output" | ||
| 280 | | "file_change_output" { | ||
| 282 | switch (method) { | 281 | switch (method) { |
| 283 | case "item/agentMessage/delta": | 282 | case "item/agentMessage/delta": |
| 284 | return "assistant_text"; | 283 | return "assistant_text"; |
| ... | @@ -297,18 +296,6 @@ function contentStreamKindFromMethod( | ... | @@ -297,18 +296,6 @@ function contentStreamKindFromMethod( |
| 297 | } | 296 | } |
| 298 | } | 297 | } |
| 299 | 298 | ||
| 300 | function asRuntimeSessionId(sessionId: ProviderSessionId): RuntimeSessionId { | ||
| 301 | return RuntimeSessionId.makeUnsafe(sessionId); | ||
| 302 | } | ||
| 303 | |||
| 304 | function asRuntimeThreadId(threadId: ProviderThreadId): ThreadId { | ||
| 305 | return ThreadId.makeUnsafe(threadId); | ||
| 306 | } | ||
| 307 | |||
| 308 | function asRuntimeTurnId(turnId: ProviderTurnId): TurnId { | ||
| 309 | return TurnId.makeUnsafe(turnId); | ||
| 310 | } | ||
| 311 | |||
| 312 | function asRuntimeItemId(itemId: ProviderItemId): RuntimeItemId { | 299 | function asRuntimeItemId(itemId: ProviderItemId): RuntimeItemId { |
| 313 | return RuntimeItemId.makeUnsafe(itemId); | 300 | return RuntimeItemId.makeUnsafe(itemId); |
| 314 | } | 301 | } |
| ... | @@ -321,29 +308,28 @@ function eventRawSource(event: ProviderEvent): NonNullable<ProviderRuntimeEvent[ | ... | @@ -321,29 +308,28 @@ function eventRawSource(event: ProviderEvent): NonNullable<ProviderRuntimeEvent[ |
| 321 | return event.kind === "request" ? "codex.app-server.request" : "codex.app-server.notification"; | 308 | return event.kind === "request" ? "codex.app-server.request" : "codex.app-server.notification"; |
| 322 | } | 309 | } |
| 323 | 310 | ||
| 324 | function providerRefsFromEvent(event: ProviderEvent): ProviderRuntimeEvent["providerRefs"] | undefined { | 311 | function providerRefsFromEvent( |
| 325 | const refs: Record<string, string> = { | 312 | event: ProviderEvent, |
| 326 | providerSessionId: event.sessionId, | 313 | ): ProviderRuntimeEvent["providerRefs"] | undefined { |
| 327 | }; | 314 | const refs: Record<string, string> = {}; |
| 328 | if (event.threadId) refs.providerThreadId = event.threadId; | ||
| 329 | if (event.turnId) refs.providerTurnId = event.turnId; | 315 | if (event.turnId) refs.providerTurnId = event.turnId; |
| 330 | if (event.itemId) refs.providerItemId = event.itemId; | 316 | if (event.itemId) refs.providerItemId = event.itemId; |
| 331 | if (event.requestId) refs.providerRequestId = event.requestId; | 317 | if (event.requestId) refs.providerRequestId = event.requestId; |
| 332 | 318 | ||
| 333 | return Object.keys(refs).length > 0 | 319 | return Object.keys(refs).length > 0 ? (refs as ProviderRuntimeEvent["providerRefs"]) : undefined; |
| 334 | ? (refs as ProviderRuntimeEvent["providerRefs"]) | ||
| 335 | : undefined; | ||
| 336 | } | 320 | } |
| 337 | 321 | ||
| 338 | function runtimeEventBase(event: ProviderEvent): Omit<ProviderRuntimeEvent, "type" | "payload"> { | 322 | function runtimeEventBase( |
| 323 | event: ProviderEvent, | ||
| 324 | canonicalThreadId: ThreadId, | ||
| 325 | ): Omit<ProviderRuntimeEvent, "type" | "payload"> { | ||
| 339 | const refs = providerRefsFromEvent(event); | 326 | const refs = providerRefsFromEvent(event); |
| 340 | return { | 327 | return { |
| 341 | eventId: event.id, | 328 | eventId: event.id, |
| 342 | provider: event.provider, | 329 | provider: event.provider, |
| 343 | sessionId: asRuntimeSessionId(event.sessionId), | 330 | threadId: canonicalThreadId, |
| 344 | createdAt: event.createdAt, | 331 | createdAt: event.createdAt, |
| 345 | ...(event.threadId ? { threadId: asRuntimeThreadId(event.threadId) } : {}), | 332 | ...(event.turnId ? { turnId: event.turnId } : {}), |
| 346 | ...(event.turnId ? { turnId: asRuntimeTurnId(event.turnId) } : {}), | ||
| 347 | ...(event.itemId ? { itemId: asRuntimeItemId(event.itemId) } : {}), | 333 | ...(event.itemId ? { itemId: asRuntimeItemId(event.itemId) } : {}), |
| 348 | ...(event.requestId ? { requestId: asRuntimeRequestId(event.requestId) } : {}), | 334 | ...(event.requestId ? { requestId: asRuntimeRequestId(event.requestId) } : {}), |
| 349 | ...(refs ? { providerRefs: refs } : {}), | 335 | ...(refs ? { providerRefs: refs } : {}), |
| ... | @@ -357,6 +343,7 @@ function runtimeEventBase(event: ProviderEvent): Omit<ProviderRuntimeEvent, "typ | ... | @@ -357,6 +343,7 @@ function runtimeEventBase(event: ProviderEvent): Omit<ProviderRuntimeEvent, "typ |
| 357 | 343 | ||
| 358 | function mapItemLifecycle( | 344 | function mapItemLifecycle( |
| 359 | event: ProviderEvent, | 345 | event: ProviderEvent, |
| 346 | canonicalThreadId: ThreadId, | ||
| 360 | lifecycle: "item.started" | "item.updated" | "item.completed", | 347 | lifecycle: "item.started" | "item.updated" | "item.completed", |
| 361 | ): ProviderRuntimeEvent | undefined { | 348 | ): ProviderRuntimeEvent | undefined { |
| 362 | const payload = asObject(event.payload); | 349 | const payload = asObject(event.payload); |
| ... | @@ -380,7 +367,7 @@ function mapItemLifecycle( | ... | @@ -380,7 +367,7 @@ function mapItemLifecycle( |
| 380 | : undefined; | 367 | : undefined; |
| 381 | 368 | ||
| 382 | return { | 369 | return { |
| 383 | ...runtimeEventBase(event), | 370 | ...runtimeEventBase(event, canonicalThreadId), |
| 384 | type: lifecycle, | 371 | type: lifecycle, |
| 385 | payload: { | 372 | payload: { |
| 386 | itemType, | 373 | itemType, |
| ... | @@ -392,7 +379,10 @@ function mapItemLifecycle( | ... | @@ -392,7 +379,10 @@ function mapItemLifecycle( |
| 392 | }; | 379 | }; |
| 393 | } | 380 | } |
| 394 | 381 | ||
| 395 | function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntimeEvent> { | 382 | function mapToRuntimeEvents( |
| 383 | event: ProviderEvent, | ||
| 384 | canonicalThreadId: ThreadId, | ||
| 385 | ): ReadonlyArray<ProviderRuntimeEvent> { | ||
| 396 | const payload = asObject(event.payload); | 386 | const payload = asObject(event.payload); |
| 397 | const turn = asObject(payload?.turn); | 387 | const turn = asObject(payload?.turn); |
| 398 | 388 | ||
| ... | @@ -402,7 +392,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -402,7 +392,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 402 | } | 392 | } |
| 403 | return [ | 393 | return [ |
| 404 | { | 394 | { |
| 405 | ...runtimeEventBase(event), | 395 | ...runtimeEventBase(event, canonicalThreadId), |
| 406 | type: "runtime.error", | 396 | type: "runtime.error", |
| 407 | payload: { | 397 | payload: { |
| 408 | message: event.message, | 398 | message: event.message, |
| ... | @@ -418,7 +408,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -418,7 +408,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 418 | asString(payload?.command) ?? asString(payload?.reason) ?? asString(payload?.prompt); | 408 | asString(payload?.command) ?? asString(payload?.reason) ?? asString(payload?.prompt); |
| 419 | return [ | 409 | return [ |
| 420 | { | 410 | { |
| 421 | ...runtimeEventBase(event), | 411 | ...runtimeEventBase(event, canonicalThreadId), |
| 422 | type: "request.opened", | 412 | type: "request.opened", |
| 423 | payload: { | 413 | payload: { |
| 424 | requestType: toRequestTypeFromMethod(event.method), | 414 | requestType: toRequestTypeFromMethod(event.method), |
| ... | @@ -437,7 +427,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -437,7 +427,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 437 | : toRequestTypeFromMethod(event.method); | 427 | : toRequestTypeFromMethod(event.method); |
| 438 | return [ | 428 | return [ |
| 439 | { | 429 | { |
| 440 | ...runtimeEventBase(event), | 430 | ...runtimeEventBase(event, canonicalThreadId), |
| 441 | type: "request.resolved", | 431 | type: "request.resolved", |
| 442 | payload: { | 432 | payload: { |
| 443 | requestType, | 433 | requestType, |
| ... | @@ -451,7 +441,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -451,7 +441,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 451 | if (event.method === "session/connecting") { | 441 | if (event.method === "session/connecting") { |
| 452 | return [ | 442 | return [ |
| 453 | { | 443 | { |
| 454 | ...runtimeEventBase(event), | 444 | ...runtimeEventBase(event, canonicalThreadId), |
| 455 | type: "session.state.changed", | 445 | type: "session.state.changed", |
| 456 | payload: { | 446 | payload: { |
| 457 | state: "starting", | 447 | state: "starting", |
| ... | @@ -464,7 +454,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -464,7 +454,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 464 | if (event.method === "session/ready") { | 454 | if (event.method === "session/ready") { |
| 465 | return [ | 455 | return [ |
| 466 | { | 456 | { |
| 467 | ...runtimeEventBase(event), | 457 | ...runtimeEventBase(event, canonicalThreadId), |
| 468 | type: "session.state.changed", | 458 | type: "session.state.changed", |
| 469 | payload: { | 459 | payload: { |
| 470 | state: "ready", | 460 | state: "ready", |
| ... | @@ -477,7 +467,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -477,7 +467,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 477 | if (event.method === "session/started") { | 467 | if (event.method === "session/started") { |
| 478 | return [ | 468 | return [ |
| 479 | { | 469 | { |
| 480 | ...runtimeEventBase(event), | 470 | ...runtimeEventBase(event, canonicalThreadId), |
| 481 | type: "session.started", | 471 | type: "session.started", |
| 482 | payload: { | 472 | payload: { |
| 483 | ...(event.message ? { message: event.message } : {}), | 473 | ...(event.message ? { message: event.message } : {}), |
| ... | @@ -490,7 +480,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -490,7 +480,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 490 | if (event.method === "session/exited" || event.method === "session/closed") { | 480 | if (event.method === "session/exited" || event.method === "session/closed") { |
| 491 | return [ | 481 | return [ |
| 492 | { | 482 | { |
| 493 | ...runtimeEventBase(event), | 483 | ...runtimeEventBase(event, canonicalThreadId), |
| 494 | type: "session.exited", | 484 | type: "session.exited", |
| 495 | payload: { | 485 | payload: { |
| 496 | ...(event.message ? { reason: event.message } : {}), | 486 | ...(event.message ? { reason: event.message } : {}), |
| ... | @@ -502,15 +492,13 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -502,15 +492,13 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 502 | 492 | ||
| 503 | if (event.method === "thread/started") { | 493 | if (event.method === "thread/started") { |
| 504 | const payloadThreadId = asString(asObject(payload?.thread)?.id); | 494 | const payloadThreadId = asString(asObject(payload?.thread)?.id); |
| 505 | const providerThreadId = | 495 | const providerThreadId = payloadThreadId ?? asString(payload?.threadId); |
| 506 | event.threadId ?? | ||
| 507 | (payloadThreadId ? ProviderThreadId.makeUnsafe(payloadThreadId) : undefined); | ||
| 508 | if (!providerThreadId) { | 496 | if (!providerThreadId) { |
| 509 | return []; | 497 | return []; |
| 510 | } | 498 | } |
| 511 | return [ | 499 | return [ |
| 512 | { | 500 | { |
| 513 | ...runtimeEventBase(event), | 501 | ...runtimeEventBase(event, canonicalThreadId), |
| 514 | type: "thread.started", | 502 | type: "thread.started", |
| 515 | payload: { | 503 | payload: { |
| 516 | providerThreadId, | 504 | providerThreadId, |
| ... | @@ -528,8 +516,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -528,8 +516,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 528 | ) { | 516 | ) { |
| 529 | return [ | 517 | return [ |
| 530 | { | 518 | { |
| 531 | ...runtimeEventBase(event), | ||
| 532 | type: "thread.state.changed", | 519 | type: "thread.state.changed", |
| 520 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 533 | payload: { | 521 | payload: { |
| 534 | state: | 522 | state: |
| 535 | event.method === "thread/archived" | 523 | event.method === "thread/archived" |
| ... | @@ -548,8 +536,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -548,8 +536,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 548 | if (event.method === "thread/name/updated") { | 536 | if (event.method === "thread/name/updated") { |
| 549 | return [ | 537 | return [ |
| 550 | { | 538 | { |
| 551 | ...runtimeEventBase(event), | ||
| 552 | type: "thread.metadata.updated", | 539 | type: "thread.metadata.updated", |
| 540 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 553 | payload: { | 541 | payload: { |
| 554 | ...(asString(payload?.threadName) ? { name: asString(payload?.threadName) } : {}), | 542 | ...(asString(payload?.threadName) ? { name: asString(payload?.threadName) } : {}), |
| 555 | ...(event.payload !== undefined ? { metadata: asObject(event.payload) } : {}), | 543 | ...(event.payload !== undefined ? { metadata: asObject(event.payload) } : {}), |
| ... | @@ -561,8 +549,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -561,8 +549,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 561 | if (event.method === "thread/tokenUsage/updated") { | 549 | if (event.method === "thread/tokenUsage/updated") { |
| 562 | return [ | 550 | return [ |
| 563 | { | 551 | { |
| 564 | ...runtimeEventBase(event), | ||
| 565 | type: "thread.token-usage.updated", | 552 | type: "thread.token-usage.updated", |
| 553 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 566 | payload: { | 554 | payload: { |
| 567 | usage: event.payload ?? {}, | 555 | usage: event.payload ?? {}, |
| 568 | }, | 556 | }, |
| ... | @@ -571,16 +559,14 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -571,16 +559,14 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 571 | } | 559 | } |
| 572 | 560 | ||
| 573 | if (event.method === "turn/started") { | 561 | if (event.method === "turn/started") { |
| 574 | const payloadTurnId = asString(turn?.id); | 562 | const turnId = event.turnId; |
| 575 | const turnId = | ||
| 576 | event.turnId ?? (payloadTurnId ? ProviderTurnId.makeUnsafe(payloadTurnId) : undefined); | ||
| 577 | if (!turnId) { | 563 | if (!turnId) { |
| 578 | return []; | 564 | return []; |
| 579 | } | 565 | } |
| 580 | return [ | 566 | return [ |
| 581 | { | 567 | { |
| 582 | ...runtimeEventBase(event), | 568 | ...runtimeEventBase(event, canonicalThreadId), |
| 583 | turnId: asRuntimeTurnId(turnId), | 569 | turnId, |
| 584 | type: "turn.started", | 570 | type: "turn.started", |
| 585 | payload: { | 571 | payload: { |
| 586 | ...(asString(turn?.model) ? { model: asString(turn?.model) } : {}), | 572 | ...(asString(turn?.model) ? { model: asString(turn?.model) } : {}), |
| ... | @@ -594,7 +580,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -594,7 +580,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 594 | const errorMessage = asString(asObject(turn?.error)?.message); | 580 | const errorMessage = asString(asObject(turn?.error)?.message); |
| 595 | return [ | 581 | return [ |
| 596 | { | 582 | { |
| 597 | ...runtimeEventBase(event), | 583 | ...runtimeEventBase(event, canonicalThreadId), |
| 598 | type: "turn.completed", | 584 | type: "turn.completed", |
| 599 | payload: { | 585 | payload: { |
| 600 | state: toTurnStatus(turn?.status), | 586 | state: toTurnStatus(turn?.status), |
| ... | @@ -613,7 +599,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -613,7 +599,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 613 | if (event.method === "turn/aborted") { | 599 | if (event.method === "turn/aborted") { |
| 614 | return [ | 600 | return [ |
| 615 | { | 601 | { |
| 616 | ...runtimeEventBase(event), | 602 | ...runtimeEventBase(event, canonicalThreadId), |
| 617 | type: "turn.aborted", | 603 | type: "turn.aborted", |
| 618 | payload: { | 604 | payload: { |
| 619 | reason: event.message ?? "Turn aborted", | 605 | reason: event.message ?? "Turn aborted", |
| ... | @@ -626,10 +612,12 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -626,10 +612,12 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 626 | const steps = Array.isArray(payload?.plan) ? payload.plan : []; | 612 | const steps = Array.isArray(payload?.plan) ? payload.plan : []; |
| 627 | return [ | 613 | return [ |
| 628 | { | 614 | { |
| 629 | ...runtimeEventBase(event), | 615 | ...runtimeEventBase(event, canonicalThreadId), |
| 630 | type: "turn.plan.updated", | 616 | type: "turn.plan.updated", |
| 631 | payload: { | 617 | payload: { |
| 632 | ...(asString(payload?.explanation) ? { explanation: asString(payload?.explanation) } : {}), | 618 | ...(asString(payload?.explanation) |
| 619 | ? { explanation: asString(payload?.explanation) } | ||
| 620 | : {}), | ||
| 633 | plan: steps | 621 | plan: steps |
| 634 | .map((entry) => asObject(entry)) | 622 | .map((entry) => asObject(entry)) |
| 635 | .filter((entry): entry is Record<string, unknown> => entry !== undefined) | 623 | .filter((entry): entry is Record<string, unknown> => entry !== undefined) |
| ... | @@ -648,7 +636,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -648,7 +636,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 648 | if (event.method === "turn/diff/updated") { | 636 | if (event.method === "turn/diff/updated") { |
| 649 | return [ | 637 | return [ |
| 650 | { | 638 | { |
| 651 | ...runtimeEventBase(event), | 639 | ...runtimeEventBase(event, canonicalThreadId), |
| 652 | type: "turn.diff.updated", | 640 | type: "turn.diff.updated", |
| 653 | payload: { | 641 | payload: { |
| 654 | unifiedDiff: | 642 | unifiedDiff: |
| ... | @@ -662,12 +650,12 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -662,12 +650,12 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 662 | } | 650 | } |
| 663 | 651 | ||
| 664 | if (event.method === "item/started") { | 652 | if (event.method === "item/started") { |
| 665 | const started = mapItemLifecycle(event, "item.started"); | 653 | const started = mapItemLifecycle(event, canonicalThreadId, "item.started"); |
| 666 | return started ? [started] : []; | 654 | return started ? [started] : []; |
| 667 | } | 655 | } |
| 668 | 656 | ||
| 669 | if (event.method === "item/completed") { | 657 | if (event.method === "item/completed") { |
| 670 | const completed = mapItemLifecycle(event, "item.completed"); | 658 | const completed = mapItemLifecycle(event, canonicalThreadId, "item.completed"); |
| 671 | return completed ? [completed] : []; | 659 | return completed ? [completed] : []; |
| 672 | } | 660 | } |
| 673 | 661 | ||
| ... | @@ -675,7 +663,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -675,7 +663,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 675 | event.method === "item/reasoning/summaryPartAdded" || | 663 | event.method === "item/reasoning/summaryPartAdded" || |
| 676 | event.method === "item/commandExecution/terminalInteraction" | 664 | event.method === "item/commandExecution/terminalInteraction" |
| 677 | ) { | 665 | ) { |
| 678 | const updated = mapItemLifecycle(event, "item.updated"); | 666 | const updated = mapItemLifecycle(event, canonicalThreadId, "item.updated"); |
| 679 | return updated ? [updated] : []; | 667 | return updated ? [updated] : []; |
| 680 | } | 668 | } |
| 681 | 669 | ||
| ... | @@ -697,13 +685,17 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -697,13 +685,17 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 697 | } | 685 | } |
| 698 | return [ | 686 | return [ |
| 699 | { | 687 | { |
| 700 | ...runtimeEventBase(event), | 688 | ...runtimeEventBase(event, canonicalThreadId), |
| 701 | type: "content.delta", | 689 | type: "content.delta", |
| 702 | payload: { | 690 | payload: { |
| 703 | streamKind: contentStreamKindFromMethod(event.method), | 691 | streamKind: contentStreamKindFromMethod(event.method), |
| 704 | delta, | 692 | delta, |
| 705 | ...(typeof payload?.contentIndex === "number" ? { contentIndex: payload.contentIndex } : {}), | 693 | ...(typeof payload?.contentIndex === "number" |
| 706 | ...(typeof payload?.summaryIndex === "number" ? { summaryIndex: payload.summaryIndex } : {}), | 694 | ? { contentIndex: payload.contentIndex } |
| 695 | : {}), | ||
| 696 | ...(typeof payload?.summaryIndex === "number" | ||
| 697 | ? { summaryIndex: payload.summaryIndex } | ||
| 698 | : {}), | ||
| 707 | }, | 699 | }, |
| 708 | }, | 700 | }, |
| 709 | ]; | 701 | ]; |
| ... | @@ -712,7 +704,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -712,7 +704,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 712 | if (event.method === "item/mcpToolCall/progress") { | 704 | if (event.method === "item/mcpToolCall/progress") { |
| 713 | return [ | 705 | return [ |
| 714 | { | 706 | { |
| 715 | ...runtimeEventBase(event), | 707 | ...runtimeEventBase(event, canonicalThreadId), |
| 716 | type: "tool.progress", | 708 | type: "tool.progress", |
| 717 | payload: { | 709 | payload: { |
| 718 | ...(asString(payload?.toolUseId) ? { toolUseId: asString(payload?.toolUseId) } : {}), | 710 | ...(asString(payload?.toolUseId) ? { toolUseId: asString(payload?.toolUseId) } : {}), |
| ... | @@ -735,7 +727,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -735,7 +727,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 735 | : "unknown"; | 727 | : "unknown"; |
| 736 | return [ | 728 | return [ |
| 737 | { | 729 | { |
| 738 | ...runtimeEventBase(event), | 730 | ...runtimeEventBase(event, canonicalThreadId), |
| 739 | type: "request.resolved", | 731 | type: "request.resolved", |
| 740 | payload: { | 732 | payload: { |
| 741 | requestType, | 733 | requestType, |
| ... | @@ -748,8 +740,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -748,8 +740,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 748 | if (event.method === "model/rerouted") { | 740 | if (event.method === "model/rerouted") { |
| 749 | return [ | 741 | return [ |
| 750 | { | 742 | { |
| 751 | ...runtimeEventBase(event), | ||
| 752 | type: "model.rerouted", | 743 | type: "model.rerouted", |
| 744 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 753 | payload: { | 745 | payload: { |
| 754 | fromModel: asString(payload?.fromModel) ?? "unknown", | 746 | fromModel: asString(payload?.fromModel) ?? "unknown", |
| 755 | toModel: asString(payload?.toModel) ?? "unknown", | 747 | toModel: asString(payload?.toModel) ?? "unknown", |
| ... | @@ -762,8 +754,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -762,8 +754,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 762 | if (event.method === "deprecationNotice") { | 754 | if (event.method === "deprecationNotice") { |
| 763 | return [ | 755 | return [ |
| 764 | { | 756 | { |
| 765 | ...runtimeEventBase(event), | ||
| 766 | type: "deprecation.notice", | 757 | type: "deprecation.notice", |
| 758 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 767 | payload: { | 759 | payload: { |
| 768 | summary: asString(payload?.summary) ?? "Deprecation notice", | 760 | summary: asString(payload?.summary) ?? "Deprecation notice", |
| 769 | ...(asString(payload?.details) ? { details: asString(payload?.details) } : {}), | 761 | ...(asString(payload?.details) ? { details: asString(payload?.details) } : {}), |
| ... | @@ -775,8 +767,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -775,8 +767,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 775 | if (event.method === "configWarning") { | 767 | if (event.method === "configWarning") { |
| 776 | return [ | 768 | return [ |
| 777 | { | 769 | { |
| 778 | ...runtimeEventBase(event), | ||
| 779 | type: "config.warning", | 770 | type: "config.warning", |
| 771 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 780 | payload: { | 772 | payload: { |
| 781 | summary: asString(payload?.summary) ?? "Configuration warning", | 773 | summary: asString(payload?.summary) ?? "Configuration warning", |
| 782 | ...(asString(payload?.details) ? { details: asString(payload?.details) } : {}), | 774 | ...(asString(payload?.details) ? { details: asString(payload?.details) } : {}), |
| ... | @@ -790,8 +782,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -790,8 +782,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 790 | if (event.method === "account/updated") { | 782 | if (event.method === "account/updated") { |
| 791 | return [ | 783 | return [ |
| 792 | { | 784 | { |
| 793 | ...runtimeEventBase(event), | ||
| 794 | type: "account.updated", | 785 | type: "account.updated", |
| 786 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 795 | payload: { | 787 | payload: { |
| 796 | account: event.payload ?? {}, | 788 | account: event.payload ?? {}, |
| 797 | }, | 789 | }, |
| ... | @@ -802,8 +794,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -802,8 +794,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 802 | if (event.method === "account/rateLimits/updated") { | 794 | if (event.method === "account/rateLimits/updated") { |
| 803 | return [ | 795 | return [ |
| 804 | { | 796 | { |
| 805 | ...runtimeEventBase(event), | ||
| 806 | type: "account.rate-limits.updated", | 797 | type: "account.rate-limits.updated", |
| 798 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 807 | payload: { | 799 | payload: { |
| 808 | rateLimits: event.payload ?? {}, | 800 | rateLimits: event.payload ?? {}, |
| 809 | }, | 801 | }, |
| ... | @@ -814,8 +806,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -814,8 +806,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 814 | if (event.method === "mcpServer/oauthLogin/completed") { | 806 | if (event.method === "mcpServer/oauthLogin/completed") { |
| 815 | return [ | 807 | return [ |
| 816 | { | 808 | { |
| 817 | ...runtimeEventBase(event), | ||
| 818 | type: "mcp.oauth.completed", | 809 | type: "mcp.oauth.completed", |
| 810 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 819 | payload: { | 811 | payload: { |
| 820 | success: payload?.success === true, | 812 | success: payload?.success === true, |
| 821 | ...(asString(payload?.name) ? { name: asString(payload?.name) } : {}), | 813 | ...(asString(payload?.name) ? { name: asString(payload?.name) } : {}), |
| ... | @@ -829,8 +821,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -829,8 +821,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 829 | const realtimeSessionId = asString(payload?.realtimeSessionId); | 821 | const realtimeSessionId = asString(payload?.realtimeSessionId); |
| 830 | return [ | 822 | return [ |
| 831 | { | 823 | { |
| 832 | ...runtimeEventBase(event), | ||
| 833 | type: "thread.realtime.started", | 824 | type: "thread.realtime.started", |
| 825 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 834 | payload: { | 826 | payload: { |
| 835 | realtimeSessionId, | 827 | realtimeSessionId, |
| 836 | }, | 828 | }, |
| ... | @@ -841,8 +833,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -841,8 +833,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 841 | if (event.method === "thread/realtime/itemAdded") { | 833 | if (event.method === "thread/realtime/itemAdded") { |
| 842 | return [ | 834 | return [ |
| 843 | { | 835 | { |
| 844 | ...runtimeEventBase(event), | ||
| 845 | type: "thread.realtime.item-added", | 836 | type: "thread.realtime.item-added", |
| 837 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 846 | payload: { | 838 | payload: { |
| 847 | item: event.payload ?? {}, | 839 | item: event.payload ?? {}, |
| 848 | }, | 840 | }, |
| ... | @@ -853,8 +845,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -853,8 +845,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 853 | if (event.method === "thread/realtime/outputAudio/delta") { | 845 | if (event.method === "thread/realtime/outputAudio/delta") { |
| 854 | return [ | 846 | return [ |
| 855 | { | 847 | { |
| 856 | ...runtimeEventBase(event), | ||
| 857 | type: "thread.realtime.audio.delta", | 848 | type: "thread.realtime.audio.delta", |
| 849 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 858 | payload: { | 850 | payload: { |
| 859 | audio: event.payload ?? {}, | 851 | audio: event.payload ?? {}, |
| 860 | }, | 852 | }, |
| ... | @@ -866,8 +858,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -866,8 +858,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 866 | const message = asString(payload?.message) ?? event.message ?? "Realtime error"; | 858 | const message = asString(payload?.message) ?? event.message ?? "Realtime error"; |
| 867 | return [ | 859 | return [ |
| 868 | { | 860 | { |
| 869 | ...runtimeEventBase(event), | ||
| 870 | type: "thread.realtime.error", | 861 | type: "thread.realtime.error", |
| 862 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 871 | payload: { | 863 | payload: { |
| 872 | message, | 864 | message, |
| 873 | }, | 865 | }, |
| ... | @@ -878,8 +870,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -878,8 +870,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 878 | if (event.method === "thread/realtime/closed") { | 870 | if (event.method === "thread/realtime/closed") { |
| 879 | return [ | 871 | return [ |
| 880 | { | 872 | { |
| 881 | ...runtimeEventBase(event), | ||
| 882 | type: "thread.realtime.closed", | 873 | type: "thread.realtime.closed", |
| 874 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 883 | payload: { | 875 | payload: { |
| 884 | reason: event.message, | 876 | reason: event.message, |
| 885 | }, | 877 | }, |
| ... | @@ -892,8 +884,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -892,8 +884,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 892 | asString(asObject(payload?.error)?.message) ?? event.message ?? "Provider runtime error"; | 884 | asString(asObject(payload?.error)?.message) ?? event.message ?? "Provider runtime error"; |
| 893 | return [ | 885 | return [ |
| 894 | { | 886 | { |
| 895 | ...runtimeEventBase(event), | ||
| 896 | type: "runtime.error", | 887 | type: "runtime.error", |
| 888 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 897 | payload: { | 889 | payload: { |
| 898 | message, | 890 | message, |
| 899 | class: "provider_error", | 891 | class: "provider_error", |
| ... | @@ -906,8 +898,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -906,8 +898,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 906 | if (event.method === "windows/worldWritableWarning") { | 898 | if (event.method === "windows/worldWritableWarning") { |
| 907 | return [ | 899 | return [ |
| 908 | { | 900 | { |
| 909 | ...runtimeEventBase(event), | ||
| 910 | type: "runtime.warning", | 901 | type: "runtime.warning", |
| 902 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 911 | payload: { | 903 | payload: { |
| 912 | message: event.message ?? "Windows world-writable warning", | 904 | message: event.message ?? "Windows world-writable warning", |
| 913 | ...(event.payload !== undefined ? { detail: event.payload } : {}), | 905 | ...(event.payload !== undefined ? { detail: event.payload } : {}), |
| ... | @@ -924,8 +916,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -924,8 +916,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 924 | 916 | ||
| 925 | return [ | 917 | return [ |
| 926 | { | 918 | { |
| 927 | ...runtimeEventBase(event), | ||
| 928 | type: "session.state.changed", | 919 | type: "session.state.changed", |
| 920 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 929 | payload: { | 921 | payload: { |
| 930 | state: success === false ? "error" : "ready", | 922 | state: success === false ? "error" : "ready", |
| 931 | reason: success === false ? failureMessage : successMessage, | 923 | reason: success === false ? failureMessage : successMessage, |
| ... | @@ -935,8 +927,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -935,8 +927,8 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 935 | ...(success === false | 927 | ...(success === false |
| 936 | ? [ | 928 | ? [ |
| 937 | { | 929 | { |
| 938 | ...runtimeEventBase(event), | ||
| 939 | type: "runtime.warning" as const, | 930 | type: "runtime.warning" as const, |
| 931 | ...runtimeEventBase(event, canonicalThreadId), | ||
| 940 | payload: { | 932 | payload: { |
| 941 | message: failureMessage, | 933 | message: failureMessage, |
| 942 | ...(event.payload !== undefined ? { detail: event.payload } : {}), | 934 | ...(event.payload !== undefined ? { detail: event.payload } : {}), |
| ... | @@ -954,7 +946,6 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | ... | @@ -954,7 +946,6 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 954 | Effect.gen(function* () { | 946 | Effect.gen(function* () { |
| 955 | const fileSystem = yield* FileSystem.FileSystem; | 947 | const fileSystem = yield* FileSystem.FileSystem; |
| 956 | const serverConfig = yield* Effect.service(ServerConfig); | 948 | const serverConfig = yield* Effect.service(ServerConfig); |
| 957 | const directory = yield* ProviderSessionDirectory; | ||
| 958 | const nativeEventLogger = | 949 | const nativeEventLogger = |
| 959 | options?.nativeEventLogger ?? | 950 | options?.nativeEventLogger ?? |
| 960 | (options?.nativeEventLogPath !== undefined | 951 | (options?.nativeEventLogPath !== undefined |
| ... | @@ -964,14 +955,12 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | ... | @@ -964,14 +955,12 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 964 | : undefined); | 955 | : undefined); |
| 965 | 956 | ||
| 966 | const manager = yield* Effect.acquireRelease( | 957 | const manager = yield* Effect.acquireRelease( |
| 967 | Effect.sync(() => { | 958 | Effect.gen(function* () { |
| 968 | if (options?.manager) { | 959 | if (options?.manager) { |
| 969 | return options.manager; | 960 | return options.manager; |
| 970 | } | 961 | } |
| 971 | if (options?.makeManager) { | 962 | const services = yield* Effect.services<never>(); |
| 972 | return options.makeManager(); | 963 | return options?.makeManager?.(services) ?? new CodexAppServerManager(services); |
| 973 | } | ||
| 974 | return new CodexAppServerManager(); | ||
| 975 | }), | 964 | }), |
| 976 | (manager) => | 965 | (manager) => |
| 977 | Effect.sync(() => { | 966 | Effect.sync(() => { |
| ... | @@ -999,11 +988,13 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | ... | @@ -999,11 +988,13 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 999 | catch: (cause) => | 988 | catch: (cause) => |
| 1000 | new ProviderAdapterProcessError({ | 989 | new ProviderAdapterProcessError({ |
| 1001 | provider: PROVIDER, | 990 | provider: PROVIDER, |
| 1002 | sessionId: "pending", | 991 | threadId: input.threadId, |
| 1003 | detail: toMessage(cause, "Failed to start Codex adapter session."), | 992 | detail: toMessage(cause, "Failed to start Codex adapter session."), |
| 1004 | cause, | 993 | cause, |
| 1005 | }), | 994 | }), |
| 1006 | }); | 995 | }).pipe( |
| 996 | Effect.map((session) => session), | ||
| 997 | ); | ||
| 1007 | }; | 998 | }; |
| 1008 | 999 | ||
| 1009 | const sendTurn: CodexAdapterShape["sendTurn"] = (input) => | 1000 | const sendTurn: CodexAdapterShape["sendTurn"] = (input) => |
| ... | @@ -1018,7 +1009,7 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | ... | @@ -1018,7 +1009,7 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 1018 | }); | 1009 | }); |
| 1019 | if (!attachmentPath) { | 1010 | if (!attachmentPath) { |
| 1020 | return yield* toRequestError( | 1011 | return yield* toRequestError( |
| 1021 | input.sessionId, | 1012 | input.threadId, |
| 1022 | "turn/start", | 1013 | "turn/start", |
| 1023 | new Error(`Invalid attachment id '${attachment.id}'.`), | 1014 | new Error(`Invalid attachment id '${attachment.id}'.`), |
| 1024 | ); | 1015 | ); |
| ... | @@ -1026,7 +1017,7 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | ... | @@ -1026,7 +1017,7 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 1026 | const bytes = yield* fileSystem | 1017 | const bytes = yield* fileSystem |
| 1027 | .readFile(attachmentPath) | 1018 | .readFile(attachmentPath) |
| 1028 | .pipe( | 1019 | .pipe( |
| 1029 | Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause)), | 1020 | Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause)), |
| 1030 | ); | 1021 | ); |
| 1031 | return { | 1022 | return { |
| 1032 | type: "image" as const, | 1023 | type: "image" as const, |
| ... | @@ -1039,7 +1030,7 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | ... | @@ -1039,7 +1030,7 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 1039 | return yield* Effect.tryPromise({ | 1030 | return yield* Effect.tryPromise({ |
| 1040 | try: () => { | 1031 | try: () => { |
| 1041 | const managerInput = { | 1032 | const managerInput = { |
| 1042 | sessionId: input.sessionId, | 1033 | threadId: input.threadId, |
| 1043 | ...(input.input !== undefined ? { input: input.input } : {}), | 1034 | ...(input.input !== undefined ? { input: input.input } : {}), |
| 1044 | ...(input.model !== undefined ? { model: input.model } : {}), | 1035 | ...(input.model !== undefined ? { model: input.model } : {}), |
| 1045 | ...(input.effort !== undefined ? { effort: input.effort } : {}), | 1036 | ...(input.effort !== undefined ? { effort: input.effort } : {}), |
| ... | @@ -1047,23 +1038,33 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | ... | @@ -1047,23 +1038,33 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 1047 | }; | 1038 | }; |
| 1048 | return manager.sendTurn(managerInput); | 1039 | return manager.sendTurn(managerInput); |
| 1049 | }, | 1040 | }, |
| 1050 | catch: (cause) => toRequestError(input.sessionId, "turn/start", cause), | 1041 | catch: (cause) => toRequestError(input.threadId, "turn/start", cause), |
| 1051 | }); | 1042 | }).pipe( |
| 1043 | Effect.map((result) => ({ | ||
| 1044 | ...result, | ||
| 1045 | threadId: input.threadId, | ||
| 1046 | })), | ||
| 1047 | ); | ||
| 1052 | }); | 1048 | }); |
| 1053 | 1049 | ||
| 1054 | const interruptTurn: CodexAdapterShape["interruptTurn"] = (sessionId, turnId) => | 1050 | const interruptTurn: CodexAdapterShape["interruptTurn"] = (threadId, turnId) => |
| 1055 | Effect.tryPromise({ | 1051 | Effect.tryPromise({ |
| 1056 | try: () => manager.interruptTurn(sessionId, turnId), | 1052 | try: () => manager.interruptTurn(threadId, turnId), |
| 1057 | catch: (cause) => toRequestError(sessionId, "turn/interrupt", cause), | 1053 | catch: (cause) => toRequestError(threadId, "turn/interrupt", cause), |
| 1058 | }); | 1054 | }); |
| 1059 | 1055 | ||
| 1060 | const readThread: CodexAdapterShape["readThread"] = (sessionId) => | 1056 | const readThread: CodexAdapterShape["readThread"] = (threadId) => |
| 1061 | Effect.tryPromise({ | 1057 | Effect.tryPromise({ |
| 1062 | try: () => manager.readThread(sessionId), | 1058 | try: () => manager.readThread(threadId), |
| 1063 | catch: (cause) => toRequestError(sessionId, "thread/read", cause), | 1059 | catch: (cause) => toRequestError(threadId, "thread/read", cause), |
| 1064 | }); | 1060 | }).pipe( |
| 1065 | 1061 | Effect.map((snapshot) => ({ | |
| 1066 | const rollbackThread: CodexAdapterShape["rollbackThread"] = (sessionId, numTurns) => { | 1062 | threadId, |
| 1063 | turns: snapshot.turns, | ||
| 1064 | })), | ||
| 1065 | ); | ||
| 1066 | |||
| 1067 | const rollbackThread: CodexAdapterShape["rollbackThread"] = (threadId, numTurns) => { | ||
| 1067 | if (!Number.isInteger(numTurns) || numTurns < 1) { | 1068 | if (!Number.isInteger(numTurns) || numTurns < 1) { |
| 1068 | return Effect.fail( | 1069 | return Effect.fail( |
| 1069 | new ProviderAdapterValidationError({ | 1070 | new ProviderAdapterValidationError({ |
| ... | @@ -1075,31 +1076,36 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | ... | @@ -1075,31 +1076,36 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 1075 | } | 1076 | } |
| 1076 | 1077 | ||
| 1077 | return Effect.tryPromise({ | 1078 | return Effect.tryPromise({ |
| 1078 | try: () => manager.rollbackThread(sessionId, numTurns), | 1079 | try: () => manager.rollbackThread(threadId, numTurns), |
| 1079 | catch: (cause) => toRequestError(sessionId, "thread/rollback", cause), | 1080 | catch: (cause) => toRequestError(threadId, "thread/rollback", cause), |
| 1080 | }); | 1081 | }).pipe( |
| 1082 | Effect.map((snapshot) => ({ | ||
| 1083 | threadId, | ||
| 1084 | turns: snapshot.turns, | ||
| 1085 | })), | ||
| 1086 | ); | ||
| 1081 | }; | 1087 | }; |
| 1082 | 1088 | ||
| 1083 | const respondToRequest: CodexAdapterShape["respondToRequest"] = ( | 1089 | const respondToRequest: CodexAdapterShape["respondToRequest"] = ( |
| 1084 | sessionId, | 1090 | threadId, |
| 1085 | requestId, | 1091 | requestId, |
| 1086 | decision, | 1092 | decision, |
| 1087 | ) => | 1093 | ) => |
| 1088 | Effect.tryPromise({ | 1094 | Effect.tryPromise({ |
| 1089 | try: () => manager.respondToRequest(sessionId, requestId, decision), | 1095 | try: () => manager.respondToRequest(threadId, requestId, decision), |
| 1090 | catch: (cause) => toRequestError(sessionId, "item/requestApproval/decision", cause), | 1096 | catch: (cause) => toRequestError(threadId, "item/requestApproval/decision", cause), |
| 1091 | }); | 1097 | }); |
| 1092 | 1098 | ||
| 1093 | const stopSession: CodexAdapterShape["stopSession"] = (sessionId) => | 1099 | const stopSession: CodexAdapterShape["stopSession"] = (threadId) => |
| 1094 | Effect.sync(() => { | 1100 | Effect.sync(() => { |
| 1095 | manager.stopSession(sessionId); | 1101 | manager.stopSession(threadId); |
| 1096 | }); | 1102 | }); |
| 1097 | 1103 | ||
| 1098 | const listSessions: CodexAdapterShape["listSessions"] = () => | 1104 | const listSessions: CodexAdapterShape["listSessions"] = () => |
| 1099 | Effect.sync(() => manager.listSessions()); | 1105 | Effect.sync(() => manager.listSessions()); |
| 1100 | 1106 | ||
| 1101 | const hasSession: CodexAdapterShape["hasSession"] = (sessionId) => | 1107 | const hasSession: CodexAdapterShape["hasSession"] = (threadId) => |
| 1102 | Effect.sync(() => manager.hasSession(sessionId)); | 1108 | Effect.sync(() => manager.hasSession(threadId)); |
| 1103 | 1109 | ||
| 1104 | const stopAll: CodexAdapterShape["stopAll"] = () => | 1110 | const stopAll: CodexAdapterShape["stopAll"] = () => |
| 1105 | Effect.sync(() => { | 1111 | Effect.sync(() => { |
| ... | @@ -1115,33 +1121,24 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | ... | @@ -1115,33 +1121,24 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 1115 | if (!nativeEventLogger) { | 1121 | if (!nativeEventLogger) { |
| 1116 | return; | 1122 | return; |
| 1117 | } | 1123 | } |
| 1118 | const orchestrationThreadId = yield* Effect.catch( | 1124 | yield* nativeEventLogger.write(event, event.threadId); |
| 1119 | directory.getThreadId(event.sessionId).pipe( | ||
| 1120 | Effect.map((threadIdOption) => | ||
| 1121 | Option.isSome(threadIdOption) ? threadIdOption.value : null, | ||
| 1122 | ), | ||
| 1123 | ), | ||
| 1124 | () => Effect.succeed(null), | ||
| 1125 | ); | ||
| 1126 | yield* nativeEventLogger.write(event, orchestrationThreadId); | ||
| 1127 | }); | 1125 | }); |
| 1128 | 1126 | ||
| 1129 | const services = yield* Effect.services<never>(); | 1127 | const services = yield* Effect.services<never>(); |
| 1130 | const listener = (event: ProviderEvent) => | 1128 | const listener = (event: ProviderEvent) => |
| 1131 | Effect.gen(function* () { | 1129 | Effect.gen(function* () { |
| 1132 | yield* writeNativeEvent(event); | 1130 | yield* writeNativeEvent(event); |
| 1133 | const runtimeEvents = mapToRuntimeEvents(event); | 1131 | const runtimeEvents = mapToRuntimeEvents(event, event.threadId); |
| 1134 | if (runtimeEvents.length === 0) { | 1132 | if (runtimeEvents.length === 0) { |
| 1135 | yield* Effect.logDebug("ignoring unhandled Codex provider event", { | 1133 | yield* Effect.logDebug("ignoring unhandled Codex provider event", { |
| 1136 | method: event.method, | 1134 | method: event.method, |
| 1137 | sessionId: event.sessionId, | 1135 | threadId: event.threadId, |
| 1138 | threadId: event.threadId, | 1136 | turnId: event.turnId, |
| 1139 | turnId: event.turnId, | 1137 | itemId: event.itemId, |
| 1140 | itemId: event.itemId, | 1138 | }); |
| 1141 | }); | 1139 | return; |
| 1142 | return; | 1140 | } |
| 1143 | } | 1141 | yield* Queue.offerAll(runtimeEventQueue, runtimeEvents); |
| 1144 | yield* Queue.offerAll(runtimeEventQueue, runtimeEvents); | ||
| 1145 | }).pipe(Effect.runPromiseWith(services)); | 1142 | }).pipe(Effect.runPromiseWith(services)); |
| 1146 | manager.on("event", listener); | 1143 | manager.on("event", listener); |
| 1147 | return listener; | 1144 | return listener; |
apps/server/src/provider/Layers/CursorAdapter.test.ts+33-16| ... | @@ -2,7 +2,7 @@ import { EventEmitter } from "node:events"; | ... | @@ -2,7 +2,7 @@ import { EventEmitter } from "node:events"; |
| 2 | import { PassThrough } from "node:stream"; | 2 | import { PassThrough } from "node:stream"; |
| 3 | import readline from "node:readline"; | 3 | import readline from "node:readline"; |
| 4 | 4 | ||
| 5 | import { ApprovalRequestId } from "@t3tools/contracts"; | 5 | import { ApprovalRequestId, ThreadId } from "@t3tools/contracts"; |
| 6 | import { assert, describe, it } from "@effect/vitest"; | 6 | import { assert, describe, it } from "@effect/vitest"; |
| 7 | import { Effect, Fiber, Stream } from "effect"; | 7 | import { Effect, Fiber, Stream } from "effect"; |
| 8 | 8 | ||
| ... | @@ -10,6 +10,10 @@ import { ProviderAdapterValidationError } from "../Errors.ts"; | ... | @@ -10,6 +10,10 @@ import { ProviderAdapterValidationError } from "../Errors.ts"; |
| 10 | import { CursorAdapter } from "../Services/CursorAdapter.ts"; | 10 | import { CursorAdapter } from "../Services/CursorAdapter.ts"; |
| 11 | import { makeCursorAdapterLive } from "./CursorAdapter.ts"; | 11 | import { makeCursorAdapterLive } from "./CursorAdapter.ts"; |
| 12 | 12 | ||
| 13 | const THREAD_ID = ThreadId.makeUnsafe("thread-cursor-1"); | ||
| 14 | const RESUME_THREAD_ID = ThreadId.makeUnsafe("thread-cursor-resume"); | ||
| 15 | const LEGACY_RESUME_THREAD_ID = ThreadId.makeUnsafe("thread-cursor-legacy"); | ||
| 16 | |||
| 13 | class FakeCursorAcpProcess extends EventEmitter { | 17 | class FakeCursorAcpProcess extends EventEmitter { |
| 14 | readonly stdin = new PassThrough(); | 18 | readonly stdin = new PassThrough(); |
| 15 | readonly stdout = new PassThrough(); | 19 | readonly stdout = new PassThrough(); |
| ... | @@ -256,7 +260,11 @@ describe("CursorAdapterLive", () => { | ... | @@ -256,7 +260,11 @@ describe("CursorAdapterLive", () => { |
| 256 | return Effect.gen(function* () { | 260 | return Effect.gen(function* () { |
| 257 | const adapter = yield* CursorAdapter; | 261 | const adapter = yield* CursorAdapter; |
| 258 | const result = yield* adapter | 262 | const result = yield* adapter |
| 259 | .startSession({ provider: "codex", runtimeMode: "full-access" }) | 263 | .startSession({ |
| 264 | provider: "codex", | ||
| 265 | threadId: THREAD_ID, | ||
| 266 | runtimeMode: "full-access", | ||
| 267 | }) | ||
| 260 | .pipe(Effect.result); | 268 | .pipe(Effect.result); |
| 261 | 269 | ||
| 262 | assert.equal(result._tag, "Failure"); | 270 | assert.equal(result._tag, "Failure"); |
| ... | @@ -290,12 +298,13 @@ describe("CursorAdapterLive", () => { | ... | @@ -290,12 +298,13 @@ describe("CursorAdapterLive", () => { |
| 290 | 298 | ||
| 291 | const session = yield* adapter.startSession({ | 299 | const session = yield* adapter.startSession({ |
| 292 | provider: "cursor", | 300 | provider: "cursor", |
| 301 | threadId: THREAD_ID, | ||
| 293 | cwd: "/tmp/project", | 302 | cwd: "/tmp/project", |
| 294 | runtimeMode: "full-access", | 303 | runtimeMode: "full-access", |
| 295 | }); | 304 | }); |
| 296 | 305 | ||
| 297 | const turn = yield* adapter.sendTurn({ | 306 | const turn = yield* adapter.sendTurn({ |
| 298 | sessionId: session.sessionId, | 307 | threadId: session.threadId, |
| 299 | input: "hello", | 308 | input: "hello", |
| 300 | attachments: [], | 309 | attachments: [], |
| 301 | }); | 310 | }); |
| ... | @@ -355,6 +364,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -355,6 +364,7 @@ describe("CursorAdapterLive", () => { |
| 355 | const adapter = yield* CursorAdapter; | 364 | const adapter = yield* CursorAdapter; |
| 356 | yield* adapter.startSession({ | 365 | yield* adapter.startSession({ |
| 357 | provider: "cursor", | 366 | provider: "cursor", |
| 367 | threadId: THREAD_ID, | ||
| 358 | model: "composer-1.5", | 368 | model: "composer-1.5", |
| 359 | runtimeMode: "full-access", | 369 | runtimeMode: "full-access", |
| 360 | }); | 370 | }); |
| ... | @@ -364,13 +374,13 @@ describe("CursorAdapterLive", () => { | ... | @@ -364,13 +374,13 @@ describe("CursorAdapterLive", () => { |
| 364 | }); | 374 | }); |
| 365 | 375 | ||
| 366 | it.effect("writes provider-native observability records when enabled", () => { | 376 | it.effect("writes provider-native observability records when enabled", () => { |
| 367 | const nativeEvents: Array<{ | 377 | const nativeEvents: Array<{ |
| 368 | event?: { | 378 | event?: { |
| 369 | provider?: string; | 379 | provider?: string; |
| 370 | method?: string; | 380 | method?: string; |
| 371 | sessionId?: string; | 381 | threadId?: string; |
| 372 | }; | 382 | }; |
| 373 | }> = []; | 383 | }> = []; |
| 374 | const fake = new FakeCursorAcpProcess(); | 384 | const fake = new FakeCursorAcpProcess(); |
| 375 | const layer = makeCursorAdapterLive({ | 385 | const layer = makeCursorAdapterLive({ |
| 376 | createProcess: () => fake as never, | 386 | createProcess: () => fake as never, |
| ... | @@ -388,13 +398,14 @@ describe("CursorAdapterLive", () => { | ... | @@ -388,13 +398,14 @@ describe("CursorAdapterLive", () => { |
| 388 | const adapter = yield* CursorAdapter; | 398 | const adapter = yield* CursorAdapter; |
| 389 | const session = yield* adapter.startSession({ | 399 | const session = yield* adapter.startSession({ |
| 390 | provider: "cursor", | 400 | provider: "cursor", |
| 401 | threadId: THREAD_ID, | ||
| 391 | cwd: "/tmp/project", | 402 | cwd: "/tmp/project", |
| 392 | runtimeMode: "full-access", | 403 | runtimeMode: "full-access", |
| 393 | }); | 404 | }); |
| 394 | 405 | ||
| 395 | assert.equal(nativeEvents.length > 0, true); | 406 | assert.equal(nativeEvents.length > 0, true); |
| 396 | assert.equal(nativeEvents.some((record) => record.event?.provider === "cursor"), true); | 407 | assert.equal(nativeEvents.some((record) => record.event?.provider === "cursor"), true); |
| 397 | assert.equal(nativeEvents.some((record) => record.event?.sessionId === session.sessionId), true); | 408 | assert.equal(nativeEvents.some((record) => record.event?.threadId === session.threadId), true); |
| 398 | assert.equal(nativeEvents.some((record) => record.event?.method === "cursor/acp/response"), true); | 409 | assert.equal(nativeEvents.some((record) => record.event?.method === "cursor/acp/response"), true); |
| 399 | }).pipe(Effect.provide(layer)); | 410 | }).pipe(Effect.provide(layer)); |
| 400 | }); | 411 | }); |
| ... | @@ -409,6 +420,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -409,6 +420,7 @@ describe("CursorAdapterLive", () => { |
| 409 | const adapter = yield* CursorAdapter; | 420 | const adapter = yield* CursorAdapter; |
| 410 | const session = yield* adapter.startSession({ | 421 | const session = yield* adapter.startSession({ |
| 411 | provider: "cursor", | 422 | provider: "cursor", |
| 423 | threadId: RESUME_THREAD_ID, | ||
| 412 | cwd: "/tmp/project", | 424 | cwd: "/tmp/project", |
| 413 | resumeCursor: { | 425 | resumeCursor: { |
| 414 | acpSessionId: "acp-session-resume", | 426 | acpSessionId: "acp-session-resume", |
| ... | @@ -426,7 +438,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -426,7 +438,7 @@ describe("CursorAdapterLive", () => { |
| 426 | cwd: "/tmp/project", | 438 | cwd: "/tmp/project", |
| 427 | mcpServers: [], | 439 | mcpServers: [], |
| 428 | }); | 440 | }); |
| 429 | assert.equal(session.threadId, "acp-session-resume"); | 441 | assert.equal(session.threadId, RESUME_THREAD_ID); |
| 430 | assert.deepEqual(session.resumeCursor, { | 442 | assert.deepEqual(session.resumeCursor, { |
| 431 | acpSessionId: "acp-session-resume", | 443 | acpSessionId: "acp-session-resume", |
| 432 | }); | 444 | }); |
| ... | @@ -443,6 +455,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -443,6 +455,7 @@ describe("CursorAdapterLive", () => { |
| 443 | const adapter = yield* CursorAdapter; | 455 | const adapter = yield* CursorAdapter; |
| 444 | const session = yield* adapter.startSession({ | 456 | const session = yield* adapter.startSession({ |
| 445 | provider: "cursor", | 457 | provider: "cursor", |
| 458 | threadId: LEGACY_RESUME_THREAD_ID, | ||
| 446 | cwd: "/tmp/project", | 459 | cwd: "/tmp/project", |
| 447 | resumeCursor: { | 460 | resumeCursor: { |
| 448 | sessionId: "acp-session-legacy", | 461 | sessionId: "acp-session-legacy", |
| ... | @@ -456,7 +469,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -456,7 +469,7 @@ describe("CursorAdapterLive", () => { |
| 456 | cwd: "/tmp/project", | 469 | cwd: "/tmp/project", |
| 457 | mcpServers: [], | 470 | mcpServers: [], |
| 458 | }); | 471 | }); |
| 459 | assert.equal(session.threadId, "acp-session-legacy"); | 472 | assert.equal(session.threadId, LEGACY_RESUME_THREAD_ID); |
| 460 | assert.deepEqual(session.resumeCursor, { | 473 | assert.deepEqual(session.resumeCursor, { |
| 461 | acpSessionId: "acp-session-legacy", | 474 | acpSessionId: "acp-session-legacy", |
| 462 | }); | 475 | }); |
| ... | @@ -474,6 +487,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -474,6 +487,7 @@ describe("CursorAdapterLive", () => { |
| 474 | 487 | ||
| 475 | const session = yield* adapter.startSession({ | 488 | const session = yield* adapter.startSession({ |
| 476 | provider: "cursor", | 489 | provider: "cursor", |
| 490 | threadId: THREAD_ID, | ||
| 477 | runtimeMode: "approval-required", | 491 | runtimeMode: "approval-required", |
| 478 | }); | 492 | }); |
| 479 | 493 | ||
| ... | @@ -498,7 +512,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -498,7 +512,7 @@ describe("CursorAdapterLive", () => { |
| 498 | } | 512 | } |
| 499 | 513 | ||
| 500 | yield* adapter.respondToRequest( | 514 | yield* adapter.respondToRequest( |
| 501 | session.sessionId, | 515 | session.threadId, |
| 502 | ApprovalRequestId.makeUnsafe(runtimeRequestId), | 516 | ApprovalRequestId.makeUnsafe(runtimeRequestId), |
| 503 | "acceptForSession", | 517 | "acceptForSession", |
| 504 | ); | 518 | ); |
| ... | @@ -528,6 +542,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -528,6 +542,7 @@ describe("CursorAdapterLive", () => { |
| 528 | 542 | ||
| 529 | yield* adapter.startSession({ | 543 | yield* adapter.startSession({ |
| 530 | provider: "cursor", | 544 | provider: "cursor", |
| 545 | threadId: THREAD_ID, | ||
| 531 | runtimeMode: "full-access", | 546 | runtimeMode: "full-access", |
| 532 | }); | 547 | }); |
| 533 | 548 | ||
| ... | @@ -562,6 +577,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -562,6 +577,7 @@ describe("CursorAdapterLive", () => { |
| 562 | const adapter = yield* CursorAdapter; | 577 | const adapter = yield* CursorAdapter; |
| 563 | const session = yield* adapter.startSession({ | 578 | const session = yield* adapter.startSession({ |
| 564 | provider: "cursor", | 579 | provider: "cursor", |
| 580 | threadId: THREAD_ID, | ||
| 565 | runtimeMode: "full-access", | 581 | runtimeMode: "full-access", |
| 566 | }); | 582 | }); |
| 567 | 583 | ||
| ... | @@ -569,7 +585,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -569,7 +585,7 @@ describe("CursorAdapterLive", () => { |
| 569 | 585 | ||
| 570 | const result = yield* adapter | 586 | const result = yield* adapter |
| 571 | .sendTurn({ | 587 | .sendTurn({ |
| 572 | sessionId: session.sessionId, | 588 | threadId: session.threadId, |
| 573 | input: " ", | 589 | input: " ", |
| 574 | attachments: [], | 590 | attachments: [], |
| 575 | }) | 591 | }) |
| ... | @@ -608,11 +624,12 @@ describe("CursorAdapterLive", () => { | ... | @@ -608,11 +624,12 @@ describe("CursorAdapterLive", () => { |
| 608 | 624 | ||
| 609 | const session = yield* adapter.startSession({ | 625 | const session = yield* adapter.startSession({ |
| 610 | provider: "cursor", | 626 | provider: "cursor", |
| 627 | threadId: THREAD_ID, | ||
| 611 | runtimeMode: "full-access", | 628 | runtimeMode: "full-access", |
| 612 | }); | 629 | }); |
| 613 | 630 | ||
| 614 | yield* adapter.sendTurn({ | 631 | yield* adapter.sendTurn({ |
| 615 | sessionId: session.sessionId, | 632 | threadId: session.threadId, |
| 616 | input: "hello", | 633 | input: "hello", |
| 617 | attachments: [], | 634 | attachments: [], |
| 618 | }); | 635 | }); |
apps/server/src/provider/Layers/CursorAdapter.ts+96-174| ... | @@ -16,16 +16,12 @@ import { | ... | @@ -16,16 +16,12 @@ import { |
| 16 | type CanonicalItemType, | 16 | type CanonicalItemType, |
| 17 | type CanonicalRequestType, | 17 | type CanonicalRequestType, |
| 18 | ProviderItemId, | 18 | ProviderItemId, |
| 19 | ProviderSessionId, | ||
| 20 | type ProviderRuntimeEvent, | 19 | type ProviderRuntimeEvent, |
| 21 | type ProviderSession, | 20 | type ProviderSession, |
| 22 | ProviderThreadId, | ||
| 23 | ProviderTurnId, | ||
| 24 | type ProviderTurnStartResult, | 21 | type ProviderTurnStartResult, |
| 25 | type RuntimeMode, | 22 | type RuntimeMode, |
| 26 | RuntimeItemId, | 23 | RuntimeItemId, |
| 27 | RuntimeRequestId, | 24 | RuntimeRequestId, |
| 28 | RuntimeSessionId, | ||
| 29 | ThreadId, | 25 | ThreadId, |
| 30 | TurnId, | 26 | TurnId, |
| 31 | } from "@t3tools/contracts"; | 27 | } from "@t3tools/contracts"; |
| ... | @@ -72,7 +68,7 @@ interface PendingPermission { | ... | @@ -72,7 +68,7 @@ interface PendingPermission { |
| 72 | } | 68 | } |
| 73 | 69 | ||
| 74 | interface CursorTurnState { | 70 | interface CursorTurnState { |
| 75 | readonly turnId: ReturnType<typeof ProviderTurnId.makeUnsafe>; | 71 | readonly turnId: TurnId; |
| 76 | readonly assistantItemId: ReturnType<typeof ProviderItemId.makeUnsafe>; | 72 | readonly assistantItemId: ReturnType<typeof ProviderItemId.makeUnsafe>; |
| 77 | readonly startedToolCalls: Set<string>; | 73 | readonly startedToolCalls: Set<string>; |
| 78 | readonly toolCalls: Map<string, { itemType: CanonicalItemType; title: string }>; | 74 | readonly toolCalls: Map<string, { itemType: CanonicalItemType; title: string }>; |
| ... | @@ -87,7 +83,7 @@ interface CursorSessionContext { | ... | @@ -87,7 +83,7 @@ interface CursorSessionContext { |
| 87 | readonly pending: Map<string, PendingRequest>; | 83 | readonly pending: Map<string, PendingRequest>; |
| 88 | readonly pendingPermissions: Map<ApprovalRequestId, PendingPermission>; | 84 | readonly pendingPermissions: Map<ApprovalRequestId, PendingPermission>; |
| 89 | readonly turns: Array<{ | 85 | readonly turns: Array<{ |
| 90 | id: ReturnType<typeof ProviderTurnId.makeUnsafe>; | 86 | id: TurnId; |
| 91 | items: Array<unknown>; | 87 | items: Array<unknown>; |
| 92 | }>; | 88 | }>; |
| 93 | turnState: CursorTurnState | undefined; | 89 | turnState: CursorTurnState | undefined; |
| ... | @@ -114,18 +110,6 @@ function toMessage(cause: unknown, fallback: string): string { | ... | @@ -114,18 +110,6 @@ function toMessage(cause: unknown, fallback: string): string { |
| 114 | return fallback; | 110 | return fallback; |
| 115 | } | 111 | } |
| 116 | 112 | ||
| 117 | function asRuntimeSessionId(value: ProviderSessionId): RuntimeSessionId { | ||
| 118 | return RuntimeSessionId.makeUnsafe(value); | ||
| 119 | } | ||
| 120 | |||
| 121 | function asRuntimeThreadId(value: ProviderThreadId): ThreadId { | ||
| 122 | return ThreadId.makeUnsafe(value); | ||
| 123 | } | ||
| 124 | |||
| 125 | function asRuntimeTurnId(value: ProviderTurnId): TurnId { | ||
| 126 | return TurnId.makeUnsafe(value); | ||
| 127 | } | ||
| 128 | |||
| 129 | function asRuntimeItemId(value: string): RuntimeItemId { | 113 | function asRuntimeItemId(value: string): RuntimeItemId { |
| 130 | return RuntimeItemId.makeUnsafe(value); | 114 | return RuntimeItemId.makeUnsafe(value); |
| 131 | } | 115 | } |
| ... | @@ -139,21 +123,21 @@ function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { | ... | @@ -139,21 +123,21 @@ function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { |
| 139 | } | 123 | } |
| 140 | 124 | ||
| 141 | function toSessionError( | 125 | function toSessionError( |
| 142 | sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>, | 126 | threadId: ThreadId, |
| 143 | cause: unknown, | 127 | cause: unknown, |
| 144 | ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { | 128 | ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { |
| 145 | const normalized = toMessage(cause, "").toLowerCase(); | 129 | const normalized = toMessage(cause, "").toLowerCase(); |
| 146 | if (normalized.includes("unknown session") || normalized.includes("not found")) { | 130 | if (normalized.includes("unknown session") || normalized.includes("not found")) { |
| 147 | return new ProviderAdapterSessionNotFoundError({ | 131 | return new ProviderAdapterSessionNotFoundError({ |
| 148 | provider: PROVIDER, | 132 | provider: PROVIDER, |
| 149 | sessionId, | 133 | threadId, |
| 150 | cause, | 134 | cause, |
| 151 | }); | 135 | }); |
| 152 | } | 136 | } |
| 153 | if (normalized.includes("closed")) { | 137 | if (normalized.includes("closed")) { |
| 154 | return new ProviderAdapterSessionClosedError({ | 138 | return new ProviderAdapterSessionClosedError({ |
| 155 | provider: PROVIDER, | 139 | provider: PROVIDER, |
| 156 | sessionId, | 140 | threadId, |
| 157 | cause, | 141 | cause, |
| 158 | }); | 142 | }); |
| 159 | } | 143 | } |
| ... | @@ -161,11 +145,11 @@ function toSessionError( | ... | @@ -161,11 +145,11 @@ function toSessionError( |
| 161 | } | 145 | } |
| 162 | 146 | ||
| 163 | function toRequestError( | 147 | function toRequestError( |
| 164 | sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>, | 148 | threadId: ThreadId, |
| 165 | method: string, | 149 | method: string, |
| 166 | cause: unknown, | 150 | cause: unknown, |
| 167 | ): ProviderAdapterError { | 151 | ): ProviderAdapterError { |
| 168 | const sessionError = toSessionError(sessionId, cause); | 152 | const sessionError = toSessionError(threadId, cause); |
| 169 | if (sessionError) { | 153 | if (sessionError) { |
| 170 | return sessionError; | 154 | return sessionError; |
| 171 | } | 155 | } |
| ... | @@ -336,7 +320,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -336,7 +320,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 336 | const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id)); | 320 | const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id)); |
| 337 | const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); | 321 | const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); |
| 338 | 322 | ||
| 339 | const sessions = new Map<ProviderSessionId, CursorSessionContext>(); | 323 | const sessions = new Map<ThreadId, CursorSessionContext>(); |
| 340 | const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); | 324 | const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); |
| 341 | 325 | ||
| 342 | const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => | 326 | const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => |
| ... | @@ -431,19 +415,16 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -431,19 +415,16 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 431 | type: "runtime.warning", | 415 | type: "runtime.warning", |
| 432 | eventId: stamp.eventId, | 416 | eventId: stamp.eventId, |
| 433 | provider: PROVIDER, | 417 | provider: PROVIDER, |
| 434 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 435 | createdAt: stamp.createdAt, | 418 | createdAt: stamp.createdAt, |
| 436 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 419 | threadId: context.session.threadId, |
| 437 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | 420 | ...((context.turnState ? { turnId: context.turnState.turnId } : {})), |
| 438 | payload: { | 421 | payload: { |
| 439 | message, | 422 | message, |
| 440 | ...(detail !== undefined ? { detail } : {}), | 423 | ...(detail !== undefined ? { detail } : {}), |
| 441 | }, | 424 | }, |
| 442 | providerRefs: { | 425 | ...(context.turnState |
| 443 | providerSessionId: context.session.sessionId, | 426 | ? { providerRefs: { providerTurnId: String(context.turnState.turnId) } } |
| 444 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 427 | : {}), |
| 445 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | ||
| 446 | }, | ||
| 447 | }); | 428 | }); |
| 448 | }); | 429 | }); |
| 449 | 430 | ||
| ... | @@ -464,10 +445,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -464,10 +445,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 464 | type: "item.completed", | 445 | type: "item.completed", |
| 465 | eventId: itemStamp.eventId, | 446 | eventId: itemStamp.eventId, |
| 466 | provider: PROVIDER, | 447 | provider: PROVIDER, |
| 467 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 468 | createdAt: itemStamp.createdAt, | 448 | createdAt: itemStamp.createdAt, |
| 469 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 449 | threadId: context.session.threadId, |
| 470 | turnId: asRuntimeTurnId(turnState.turnId), | 450 | turnId: turnState.turnId, |
| 471 | itemId: asRuntimeItemId(turnState.assistantItemId), | 451 | itemId: asRuntimeItemId(turnState.assistantItemId), |
| 472 | payload: { | 452 | payload: { |
| 473 | itemType: "assistant_message", | 453 | itemType: "assistant_message", |
| ... | @@ -475,9 +455,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -475,9 +455,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 475 | title: "Assistant message", | 455 | title: "Assistant message", |
| 476 | }, | 456 | }, |
| 477 | providerRefs: { | 457 | providerRefs: { |
| 478 | providerSessionId: context.session.sessionId, | 458 | providerTurnId: String(turnState.turnId), |
| 479 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 480 | providerTurnId: turnState.turnId, | ||
| 481 | providerItemId: turnState.assistantItemId, | 459 | providerItemId: turnState.assistantItemId, |
| 482 | }, | 460 | }, |
| 483 | }); | 461 | }); |
| ... | @@ -492,19 +470,16 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -492,19 +470,16 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 492 | type: "turn.completed", | 470 | type: "turn.completed", |
| 493 | eventId: stamp.eventId, | 471 | eventId: stamp.eventId, |
| 494 | provider: PROVIDER, | 472 | provider: PROVIDER, |
| 495 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 496 | createdAt: stamp.createdAt, | 473 | createdAt: stamp.createdAt, |
| 497 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 474 | threadId: context.session.threadId, |
| 498 | turnId: asRuntimeTurnId(turnState.turnId), | 475 | turnId: turnState.turnId, |
| 499 | payload: { | 476 | payload: { |
| 500 | state, | 477 | state, |
| 501 | ...(stopReason ? { stopReason } : {}), | 478 | ...(stopReason ? { stopReason } : {}), |
| 502 | ...(errorMessage ? { errorMessage } : {}), | 479 | ...(errorMessage ? { errorMessage } : {}), |
| 503 | }, | 480 | }, |
| 504 | providerRefs: { | 481 | providerRefs: { |
| 505 | providerSessionId: context.session.sessionId, | 482 | providerTurnId: String(turnState.turnId), |
| 506 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 507 | providerTurnId: turnState.turnId, | ||
| 508 | }, | 483 | }, |
| 509 | }); | 484 | }); |
| 510 | 485 | ||
| ... | @@ -572,10 +547,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -572,10 +547,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 572 | type: "request.resolved", | 547 | type: "request.resolved", |
| 573 | eventId: stamp.eventId, | 548 | eventId: stamp.eventId, |
| 574 | provider: PROVIDER, | 549 | provider: PROVIDER, |
| 575 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 576 | createdAt: stamp.createdAt, | 550 | createdAt: stamp.createdAt, |
| 577 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 551 | threadId: context.session.threadId, |
| 578 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | 552 | ...((context.turnState ? { turnId: context.turnState.turnId } : {})), |
| 579 | requestId: asRuntimeRequestId(requestId), | 553 | requestId: asRuntimeRequestId(requestId), |
| 580 | payload: { | 554 | payload: { |
| 581 | requestType, | 555 | requestType, |
| ... | @@ -586,9 +560,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -586,9 +560,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 586 | }, | 560 | }, |
| 587 | }, | 561 | }, |
| 588 | providerRefs: { | 562 | providerRefs: { |
| 589 | providerSessionId: context.session.sessionId, | 563 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 590 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 591 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | ||
| 592 | providerRequestId: String(decoded.id), | 564 | providerRequestId: String(decoded.id), |
| 593 | }, | 565 | }, |
| 594 | raw: { | 566 | raw: { |
| ... | @@ -614,10 +586,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -614,10 +586,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 614 | type: "request.opened", | 586 | type: "request.opened", |
| 615 | eventId: stamp.eventId, | 587 | eventId: stamp.eventId, |
| 616 | provider: PROVIDER, | 588 | provider: PROVIDER, |
| 617 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 618 | createdAt: stamp.createdAt, | 589 | createdAt: stamp.createdAt, |
| 619 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 590 | threadId: context.session.threadId, |
| 620 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | 591 | ...((context.turnState ? { turnId: context.turnState.turnId } : {})), |
| 621 | requestId: asRuntimeRequestId(requestId), | 592 | requestId: asRuntimeRequestId(requestId), |
| 622 | payload: { | 593 | payload: { |
| 623 | requestType, | 594 | requestType, |
| ... | @@ -625,9 +596,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -625,9 +596,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 625 | args: decoded.params, | 596 | args: decoded.params, |
| 626 | }, | 597 | }, |
| 627 | providerRefs: { | 598 | providerRefs: { |
| 628 | providerSessionId: context.session.sessionId, | 599 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 629 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 630 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | ||
| 631 | providerRequestId: String(decoded.id), | 600 | providerRequestId: String(decoded.id), |
| 632 | }, | 601 | }, |
| 633 | raw: { | 602 | raw: { |
| ... | @@ -659,14 +628,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -659,14 +628,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 659 | 628 | ||
| 660 | const base = { | 629 | const base = { |
| 661 | provider: PROVIDER, | 630 | provider: PROVIDER, |
| 662 | sessionId: asRuntimeSessionId(context.session.sessionId), | 631 | threadId: context.session.threadId, |
| 663 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 632 | ...((context.turnState ? { turnId: context.turnState.turnId } : {})), |
| 664 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | 633 | ...(context.turnState |
| 665 | providerRefs: { | 634 | ? { providerRefs: { providerTurnId: String(context.turnState.turnId) } } |
| 666 | providerSessionId: context.session.sessionId, | 635 | : {}), |
| 667 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 668 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | ||
| 669 | }, | ||
| 670 | raw: { | 636 | raw: { |
| 671 | source: "cursor.acp.notification" as const, | 637 | source: "cursor.acp.notification" as const, |
| 672 | method: decoded.method, | 638 | method: decoded.method, |
| ... | @@ -701,7 +667,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -701,7 +667,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 701 | type: "content.delta", | 667 | type: "content.delta", |
| 702 | eventId: stamp.eventId, | 668 | eventId: stamp.eventId, |
| 703 | createdAt: stamp.createdAt, | 669 | createdAt: stamp.createdAt, |
| 704 | turnId: asRuntimeTurnId(context.turnState.turnId), | 670 | turnId: context.turnState.turnId, |
| 705 | itemId: asRuntimeItemId(context.turnState.assistantItemId), | 671 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 706 | payload: { | 672 | payload: { |
| 707 | streamKind: "reasoning_text", | 673 | streamKind: "reasoning_text", |
| ... | @@ -720,7 +686,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -720,7 +686,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 720 | type: "content.delta", | 686 | type: "content.delta", |
| 721 | eventId: stamp.eventId, | 687 | eventId: stamp.eventId, |
| 722 | createdAt: stamp.createdAt, | 688 | createdAt: stamp.createdAt, |
| 723 | turnId: asRuntimeTurnId(context.turnState.turnId), | 689 | turnId: context.turnState.turnId, |
| 724 | itemId: asRuntimeItemId(context.turnState.assistantItemId), | 690 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 725 | payload: { | 691 | payload: { |
| 726 | streamKind: "assistant_text", | 692 | streamKind: "assistant_text", |
| ... | @@ -746,7 +712,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -746,7 +712,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 746 | type: "item.started", | 712 | type: "item.started", |
| 747 | eventId: stamp.eventId, | 713 | eventId: stamp.eventId, |
| 748 | createdAt: stamp.createdAt, | 714 | createdAt: stamp.createdAt, |
| 749 | turnId: asRuntimeTurnId(context.turnState.turnId), | 715 | turnId: context.turnState.turnId, |
| 750 | itemId: asRuntimeItemId(update.toolCallId), | 716 | itemId: asRuntimeItemId(update.toolCallId), |
| 751 | payload: { | 717 | payload: { |
| 752 | itemType, | 718 | itemType, |
| ... | @@ -765,7 +731,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -765,7 +731,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 765 | type: "item.updated", | 731 | type: "item.updated", |
| 766 | eventId: stamp.eventId, | 732 | eventId: stamp.eventId, |
| 767 | createdAt: stamp.createdAt, | 733 | createdAt: stamp.createdAt, |
| 768 | turnId: asRuntimeTurnId(context.turnState.turnId), | 734 | turnId: context.turnState.turnId, |
| 769 | itemId: asRuntimeItemId(update.toolCallId), | 735 | itemId: asRuntimeItemId(update.toolCallId), |
| 770 | payload: { | 736 | payload: { |
| 771 | itemType, | 737 | itemType, |
| ... | @@ -791,7 +757,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -791,7 +757,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 791 | type: eventType, | 757 | type: eventType, |
| 792 | eventId: stamp.eventId, | 758 | eventId: stamp.eventId, |
| 793 | createdAt: stamp.createdAt, | 759 | createdAt: stamp.createdAt, |
| 794 | turnId: asRuntimeTurnId(context.turnState.turnId), | 760 | turnId: context.turnState.turnId, |
| 795 | itemId: asRuntimeItemId(update.toolCallId), | 761 | itemId: asRuntimeItemId(update.toolCallId), |
| 796 | payload: { | 762 | payload: { |
| 797 | itemType, | 763 | itemType, |
| ... | @@ -857,10 +823,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -857,10 +823,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 857 | id: EventId.makeUnsafe(randomUUID()), | 823 | id: EventId.makeUnsafe(randomUUID()), |
| 858 | kind: nativeKind, | 824 | kind: nativeKind, |
| 859 | provider: PROVIDER, | 825 | provider: PROVIDER, |
| 860 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 861 | createdAt: new Date().toISOString(), | 826 | createdAt: new Date().toISOString(), |
| 862 | method: nativeMethod, | 827 | method: nativeMethod, |
| 863 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 828 | threadId: context.session.threadId, |
| 864 | ...(context.turnState ? { turnId: String(context.turnState.turnId) } : {}), | 829 | ...(context.turnState ? { turnId: String(context.turnState.turnId) } : {}), |
| 865 | payload: message, | 830 | payload: message, |
| 866 | }, | 831 | }, |
| ... | @@ -956,36 +921,31 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -956,36 +921,31 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 956 | 921 | ||
| 957 | if (options?.emitExitEvent !== false) { | 922 | if (options?.emitExitEvent !== false) { |
| 958 | const stamp = yield* makeEventStamp(); | 923 | const stamp = yield* makeEventStamp(); |
| 959 | yield* offerRuntimeEvent({ | 924 | yield* offerRuntimeEvent({ |
| 960 | type: "session.exited", | 925 | type: "session.exited", |
| 961 | eventId: stamp.eventId, | 926 | eventId: stamp.eventId, |
| 962 | provider: PROVIDER, | 927 | provider: PROVIDER, |
| 963 | sessionId: asRuntimeSessionId(context.session.sessionId), | 928 | createdAt: stamp.createdAt, |
| 964 | createdAt: stamp.createdAt, | 929 | threadId: context.session.threadId, |
| 965 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 930 | payload: { |
| 966 | payload: { | 931 | reason: "Session stopped", |
| 967 | reason: "Session stopped", | 932 | exitKind: "graceful", |
| 968 | exitKind: "graceful", | 933 | }, |
| 969 | }, | 934 | }); |
| 970 | providerRefs: { | 935 | } |
| 971 | providerSessionId: context.session.sessionId, | ||
| 972 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 973 | }, | ||
| 974 | }); | ||
| 975 | } | ||
| 976 | 936 | ||
| 977 | sessions.delete(context.session.sessionId); | 937 | sessions.delete(context.session.threadId); |
| 978 | }); | 938 | }); |
| 979 | 939 | ||
| 980 | const requireSession = ( | 940 | const requireSession = ( |
| 981 | sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>, | 941 | threadId: ThreadId, |
| 982 | ): Effect.Effect<CursorSessionContext, ProviderAdapterError> => { | 942 | ): Effect.Effect<CursorSessionContext, ProviderAdapterError> => { |
| 983 | const context = sessions.get(sessionId); | 943 | const context = sessions.get(threadId); |
| 984 | if (!context) { | 944 | if (!context) { |
| 985 | return Effect.fail( | 945 | return Effect.fail( |
| 986 | new ProviderAdapterSessionNotFoundError({ | 946 | new ProviderAdapterSessionNotFoundError({ |
| 987 | provider: PROVIDER, | 947 | provider: PROVIDER, |
| 988 | sessionId, | 948 | threadId, |
| 989 | }), | 949 | }), |
| 990 | ); | 950 | ); |
| 991 | } | 951 | } |
| ... | @@ -993,7 +953,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -993,7 +953,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 993 | return Effect.fail( | 953 | return Effect.fail( |
| 994 | new ProviderAdapterSessionClosedError({ | 954 | new ProviderAdapterSessionClosedError({ |
| 995 | provider: PROVIDER, | 955 | provider: PROVIDER, |
| 996 | sessionId, | 956 | threadId, |
| 997 | }), | 957 | }), |
| 998 | ); | 958 | ); |
| 999 | } | 959 | } |
| ... | @@ -1011,7 +971,6 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1011,7 +971,6 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1011 | } | 971 | } |
| 1012 | 972 | ||
| 1013 | const startedAt = yield* nowIso; | 973 | const startedAt = yield* nowIso; |
| 1014 | const sessionId = ProviderSessionId.makeUnsafe(yield* Random.nextUUIDv4); | ||
| 1015 | const cwd = input.cwd ?? process.cwd(); | 974 | const cwd = input.cwd ?? process.cwd(); |
| 1016 | const cursorOptions = input.providerOptions?.cursor as { binaryPath?: string } | undefined; | 975 | const cursorOptions = input.providerOptions?.cursor as { binaryPath?: string } | undefined; |
| 1017 | const binaryPath = cursorOptions?.binaryPath ?? "agent"; | 976 | const binaryPath = cursorOptions?.binaryPath ?? "agent"; |
| ... | @@ -1028,7 +987,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1028,7 +987,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1028 | catch: (cause) => | 987 | catch: (cause) => |
| 1029 | new ProviderAdapterProcessError({ | 988 | new ProviderAdapterProcessError({ |
| 1030 | provider: PROVIDER, | 989 | provider: PROVIDER, |
| 1031 | sessionId, | 990 | threadId: input.threadId, |
| 1032 | detail: toMessage(cause, "Failed to spawn Cursor ACP process."), | 991 | detail: toMessage(cause, "Failed to spawn Cursor ACP process."), |
| 1033 | cause, | 992 | cause, |
| 1034 | }), | 993 | }), |
| ... | @@ -1037,7 +996,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1037,7 +996,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1037 | const output = readline.createInterface({ input: child.stdout }); | 996 | const output = readline.createInterface({ input: child.stdout }); |
| 1038 | 997 | ||
| 1039 | const session: ProviderSession = { | 998 | const session: ProviderSession = { |
| 1040 | sessionId, | 999 | threadId: input.threadId, |
| 1041 | provider: PROVIDER, | 1000 | provider: PROVIDER, |
| 1042 | status: "connecting", | 1001 | status: "connecting", |
| 1043 | runtimeMode: input.runtimeMode, | 1002 | runtimeMode: input.runtimeMode, |
| ... | @@ -1085,10 +1044,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1085,10 +1044,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1085 | type: "runtime.error", | 1044 | type: "runtime.error", |
| 1086 | eventId: stamp.eventId, | 1045 | eventId: stamp.eventId, |
| 1087 | provider: PROVIDER, | 1046 | provider: PROVIDER, |
| 1088 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 1089 | createdAt: stamp.createdAt, | 1047 | createdAt: stamp.createdAt, |
| 1090 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1048 | threadId: context.session.threadId, |
| 1091 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | 1049 | ...((context.turnState ? { turnId: context.turnState.turnId } : {})), |
| 1092 | payload: { | 1050 | payload: { |
| 1093 | message: error.message || "Cursor ACP process error.", | 1051 | message: error.message || "Cursor ACP process error.", |
| 1094 | class: "transport_error", | 1052 | class: "transport_error", |
| ... | @@ -1118,9 +1076,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1118,9 +1076,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1118 | type: "session.exited", | 1076 | type: "session.exited", |
| 1119 | eventId: stamp.eventId, | 1077 | eventId: stamp.eventId, |
| 1120 | provider: PROVIDER, | 1078 | provider: PROVIDER, |
| 1121 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 1122 | createdAt: stamp.createdAt, | 1079 | createdAt: stamp.createdAt, |
| 1123 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1080 | threadId: context.session.threadId, |
| 1124 | payload: { | 1081 | payload: { |
| 1125 | reason: `Cursor ACP exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`, | 1082 | reason: `Cursor ACP exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`, |
| 1126 | exitKind: code === 0 ? "graceful" : "error", | 1083 | exitKind: code === 0 ? "graceful" : "error", |
| ... | @@ -1128,19 +1085,19 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1128,19 +1085,19 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1128 | }, | 1085 | }, |
| 1129 | }); | 1086 | }); |
| 1130 | 1087 | ||
| 1131 | sessions.delete(context.session.sessionId); | 1088 | sessions.delete(context.session.threadId); |
| 1132 | }), | 1089 | }), |
| 1133 | ); | 1090 | ); |
| 1134 | }); | 1091 | }); |
| 1135 | 1092 | ||
| 1136 | sessions.set(sessionId, context); | 1093 | sessions.set(input.threadId, context); |
| 1137 | 1094 | ||
| 1138 | const initializeResult = yield* Effect.tryPromise({ | 1095 | const initializeResult = yield* Effect.tryPromise({ |
| 1139 | try: async () => | 1096 | try: async () => |
| 1140 | sendRequest(context, "initialize", { | 1097 | sendRequest(context, "initialize", { |
| 1141 | protocolVersion: CURSOR_ACP_PROTOCOL_VERSION, | 1098 | protocolVersion: CURSOR_ACP_PROTOCOL_VERSION, |
| 1142 | }), | 1099 | }), |
| 1143 | catch: (cause) => toRequestError(sessionId, "initialize", cause), | 1100 | catch: (cause) => toRequestError(input.threadId, "initialize", cause), |
| 1144 | }); | 1101 | }); |
| 1145 | const decodedInitialize = yield* Effect.try({ | 1102 | const decodedInitialize = yield* Effect.try({ |
| 1146 | try: () => Schema.decodeUnknownSync(CursorAcpInitializeResult)(initializeResult), | 1103 | try: () => Schema.decodeUnknownSync(CursorAcpInitializeResult)(initializeResult), |
| ... | @@ -1158,14 +1115,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1158,14 +1115,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1158 | type: "session.configured", | 1115 | type: "session.configured", |
| 1159 | eventId: initStamp.eventId, | 1116 | eventId: initStamp.eventId, |
| 1160 | provider: PROVIDER, | 1117 | provider: PROVIDER, |
| 1161 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1162 | createdAt: initStamp.createdAt, | 1118 | createdAt: initStamp.createdAt, |
| 1119 | threadId: input.threadId, | ||
| 1163 | payload: { | 1120 | payload: { |
| 1164 | config: decodedInitialize, | 1121 | config: decodedInitialize, |
| 1165 | }, | 1122 | }, |
| 1166 | providerRefs: { | ||
| 1167 | providerSessionId: sessionId, | ||
| 1168 | }, | ||
| 1169 | raw: { | 1123 | raw: { |
| 1170 | source: "cursor.acp.response", | 1124 | source: "cursor.acp.response", |
| 1171 | method: "initialize", | 1125 | method: "initialize", |
| ... | @@ -1179,14 +1133,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1179,14 +1133,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1179 | type: "auth.status", | 1133 | type: "auth.status", |
| 1180 | eventId: authStartStamp.eventId, | 1134 | eventId: authStartStamp.eventId, |
| 1181 | provider: PROVIDER, | 1135 | provider: PROVIDER, |
| 1182 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1183 | createdAt: authStartStamp.createdAt, | 1136 | createdAt: authStartStamp.createdAt, |
| 1137 | threadId: input.threadId, | ||
| 1184 | payload: { | 1138 | payload: { |
| 1185 | isAuthenticating: true, | 1139 | isAuthenticating: true, |
| 1186 | }, | 1140 | }, |
| 1187 | providerRefs: { | ||
| 1188 | providerSessionId: sessionId, | ||
| 1189 | }, | ||
| 1190 | raw: { | 1141 | raw: { |
| 1191 | source: "cursor.acp.request", | 1142 | source: "cursor.acp.request", |
| 1192 | method: "authenticate", | 1143 | method: "authenticate", |
| ... | @@ -1196,7 +1147,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1196,7 +1147,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1196 | 1147 | ||
| 1197 | const authenticateResult = yield* Effect.tryPromise({ | 1148 | const authenticateResult = yield* Effect.tryPromise({ |
| 1198 | try: async () => sendRequest(context, "authenticate", authenticateRequest), | 1149 | try: async () => sendRequest(context, "authenticate", authenticateRequest), |
| 1199 | catch: (cause) => toRequestError(sessionId, "authenticate", cause), | 1150 | catch: (cause) => toRequestError(input.threadId, "authenticate", cause), |
| 1200 | }).pipe( | 1151 | }).pipe( |
| 1201 | Effect.match({ | 1152 | Effect.match({ |
| 1202 | onFailure: (error) => ({ ok: false as const, error }), | 1153 | onFailure: (error) => ({ ok: false as const, error }), |
| ... | @@ -1209,15 +1160,12 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1209,15 +1160,12 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1209 | type: "auth.status", | 1160 | type: "auth.status", |
| 1210 | eventId: authEndStamp.eventId, | 1161 | eventId: authEndStamp.eventId, |
| 1211 | provider: PROVIDER, | 1162 | provider: PROVIDER, |
| 1212 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1213 | createdAt: authEndStamp.createdAt, | 1163 | createdAt: authEndStamp.createdAt, |
| 1164 | threadId: input.threadId, | ||
| 1214 | payload: { | 1165 | payload: { |
| 1215 | isAuthenticating: false, | 1166 | isAuthenticating: false, |
| 1216 | error: toMessage(authenticateResult.error, "Cursor authentication failed."), | 1167 | error: toMessage(authenticateResult.error, "Cursor authentication failed."), |
| 1217 | }, | 1168 | }, |
| 1218 | providerRefs: { | ||
| 1219 | providerSessionId: sessionId, | ||
| 1220 | }, | ||
| 1221 | raw: { | 1169 | raw: { |
| 1222 | source: "cursor.acp.response", | 1170 | source: "cursor.acp.response", |
| 1223 | method: "authenticate", | 1171 | method: "authenticate", |
| ... | @@ -1231,14 +1179,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1231,14 +1179,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1231 | type: "auth.status", | 1179 | type: "auth.status", |
| 1232 | eventId: authEndStamp.eventId, | 1180 | eventId: authEndStamp.eventId, |
| 1233 | provider: PROVIDER, | 1181 | provider: PROVIDER, |
| 1234 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1235 | createdAt: authEndStamp.createdAt, | 1182 | createdAt: authEndStamp.createdAt, |
| 1183 | threadId: input.threadId, | ||
| 1236 | payload: { | 1184 | payload: { |
| 1237 | isAuthenticating: false, | 1185 | isAuthenticating: false, |
| 1238 | }, | 1186 | }, |
| 1239 | providerRefs: { | ||
| 1240 | providerSessionId: sessionId, | ||
| 1241 | }, | ||
| 1242 | raw: { | 1187 | raw: { |
| 1243 | source: "cursor.acp.response", | 1188 | source: "cursor.acp.response", |
| 1244 | method: "authenticate", | 1189 | method: "authenticate", |
| ... | @@ -1273,15 +1218,13 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1273,15 +1218,13 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1273 | const decoded = Schema.decodeUnknownSync(CursorAcpSessionNewResult)(result); | 1218 | const decoded = Schema.decodeUnknownSync(CursorAcpSessionNewResult)(result); |
| 1274 | return decoded.sessionId; | 1219 | return decoded.sessionId; |
| 1275 | }, | 1220 | }, |
| 1276 | catch: (cause) => toRequestError(sessionId, "session/new|session/load", cause), | 1221 | catch: (cause) => toRequestError(input.threadId, "session/new|session/load", cause), |
| 1277 | }); | 1222 | }); |
| 1278 | 1223 | ||
| 1279 | const threadId = ProviderThreadId.makeUnsafe(acpSessionId); | ||
| 1280 | context.acpSessionId = acpSessionId; | 1224 | context.acpSessionId = acpSessionId; |
| 1281 | context.session = { | 1225 | context.session = { |
| 1282 | ...context.session, | 1226 | ...context.session, |
| 1283 | status: "ready", | 1227 | status: "ready", |
| 1284 | threadId, | ||
| 1285 | resumeCursor: { | 1228 | resumeCursor: { |
| 1286 | acpSessionId, | 1229 | acpSessionId, |
| 1287 | }, | 1230 | }, |
| ... | @@ -1293,14 +1236,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1293,14 +1236,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1293 | type: "session.started", | 1236 | type: "session.started", |
| 1294 | eventId: sessionStartedStamp.eventId, | 1237 | eventId: sessionStartedStamp.eventId, |
| 1295 | provider: PROVIDER, | 1238 | provider: PROVIDER, |
| 1296 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1297 | createdAt: sessionStartedStamp.createdAt, | 1239 | createdAt: sessionStartedStamp.createdAt, |
| 1298 | threadId: asRuntimeThreadId(threadId), | 1240 | threadId: input.threadId, |
| 1299 | payload: resumeState?.acpSessionId ? { resume: input.resumeCursor } : {}, | 1241 | payload: resumeState?.acpSessionId ? { resume: input.resumeCursor } : {}, |
| 1300 | providerRefs: { | ||
| 1301 | providerSessionId: sessionId, | ||
| 1302 | providerThreadId: threadId, | ||
| 1303 | }, | ||
| 1304 | }); | 1242 | }); |
| 1305 | 1243 | ||
| 1306 | const threadStartedStamp = yield* makeEventStamp(); | 1244 | const threadStartedStamp = yield* makeEventStamp(); |
| ... | @@ -1308,15 +1246,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1308,15 +1246,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1308 | type: "thread.started", | 1246 | type: "thread.started", |
| 1309 | eventId: threadStartedStamp.eventId, | 1247 | eventId: threadStartedStamp.eventId, |
| 1310 | provider: PROVIDER, | 1248 | provider: PROVIDER, |
| 1311 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1312 | createdAt: threadStartedStamp.createdAt, | 1249 | createdAt: threadStartedStamp.createdAt, |
| 1313 | threadId: asRuntimeThreadId(threadId), | 1250 | threadId: input.threadId, |
| 1314 | payload: { | 1251 | payload: { |
| 1315 | providerThreadId: threadId, | 1252 | providerThreadId: acpSessionId, |
| 1316 | }, | ||
| 1317 | providerRefs: { | ||
| 1318 | providerSessionId: sessionId, | ||
| 1319 | providerThreadId: threadId, | ||
| 1320 | }, | 1253 | }, |
| 1321 | }); | 1254 | }); |
| 1322 | 1255 | ||
| ... | @@ -1325,16 +1258,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1325,16 +1258,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1325 | type: "session.state.changed", | 1258 | type: "session.state.changed", |
| 1326 | eventId: readyStamp.eventId, | 1259 | eventId: readyStamp.eventId, |
| 1327 | provider: PROVIDER, | 1260 | provider: PROVIDER, |
| 1328 | sessionId: asRuntimeSessionId(sessionId), | ||
| 1329 | createdAt: readyStamp.createdAt, | 1261 | createdAt: readyStamp.createdAt, |
| 1330 | threadId: asRuntimeThreadId(threadId), | 1262 | threadId: input.threadId, |
| 1331 | payload: { | 1263 | payload: { |
| 1332 | state: "ready", | 1264 | state: "ready", |
| 1333 | }, | 1265 | }, |
| 1334 | providerRefs: { | ||
| 1335 | providerSessionId: sessionId, | ||
| 1336 | providerThreadId: threadId, | ||
| 1337 | }, | ||
| 1338 | }); | 1266 | }); |
| 1339 | 1267 | ||
| 1340 | return { | 1268 | return { |
| ... | @@ -1344,13 +1272,13 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1344,13 +1272,13 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1344 | 1272 | ||
| 1345 | const sendTurn: CursorAdapterShape["sendTurn"] = (input) => | 1273 | const sendTurn: CursorAdapterShape["sendTurn"] = (input) => |
| 1346 | Effect.gen(function* () { | 1274 | Effect.gen(function* () { |
| 1347 | const context = yield* requireSession(input.sessionId); | 1275 | const context = yield* requireSession(input.threadId); |
| 1348 | 1276 | ||
| 1349 | if (context.turnState) { | 1277 | if (context.turnState) { |
| 1350 | return yield* new ProviderAdapterValidationError({ | 1278 | return yield* new ProviderAdapterValidationError({ |
| 1351 | provider: PROVIDER, | 1279 | provider: PROVIDER, |
| 1352 | operation: "sendTurn", | 1280 | operation: "sendTurn", |
| 1353 | issue: `Session '${input.sessionId}' already has an active turn '${context.turnState.turnId}'.`, | 1281 | issue: `Thread '${input.threadId}' already has an active turn '${context.turnState.turnId}'.`, |
| 1354 | }); | 1282 | }); |
| 1355 | } | 1283 | } |
| 1356 | 1284 | ||
| ... | @@ -1363,7 +1291,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1363,7 +1291,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1363 | }); | 1291 | }); |
| 1364 | } | 1292 | } |
| 1365 | 1293 | ||
| 1366 | const turnId = ProviderTurnId.makeUnsafe(yield* Random.nextUUIDv4); | 1294 | const turnId = TurnId.makeUnsafe(yield* Random.nextUUIDv4); |
| 1367 | const turnState: CursorTurnState = { | 1295 | const turnState: CursorTurnState = { |
| 1368 | turnId, | 1296 | turnId, |
| 1369 | assistantItemId: asProviderItemId(yield* Random.nextUUIDv4), | 1297 | assistantItemId: asProviderItemId(yield* Random.nextUUIDv4), |
| ... | @@ -1385,18 +1313,15 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1385,18 +1313,15 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1385 | type: "turn.started", | 1313 | type: "turn.started", |
| 1386 | eventId: startedStamp.eventId, | 1314 | eventId: startedStamp.eventId, |
| 1387 | provider: PROVIDER, | 1315 | provider: PROVIDER, |
| 1388 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 1389 | createdAt: startedStamp.createdAt, | 1316 | createdAt: startedStamp.createdAt, |
| 1390 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1317 | threadId: context.session.threadId, |
| 1391 | turnId: asRuntimeTurnId(turnId), | 1318 | turnId, |
| 1392 | payload: { | 1319 | payload: { |
| 1393 | ...(input.model ? { model: input.model } : {}), | 1320 | ...(input.model ? { model: input.model } : {}), |
| 1394 | ...(input.effort ? { effort: input.effort } : {}), | 1321 | ...(input.effort ? { effort: input.effort } : {}), |
| 1395 | }, | 1322 | }, |
| 1396 | providerRefs: { | 1323 | providerRefs: { |
| 1397 | providerSessionId: context.session.sessionId, | 1324 | providerTurnId: String(turnId), |
| 1398 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 1399 | providerTurnId: turnId, | ||
| 1400 | }, | 1325 | }, |
| 1401 | }); | 1326 | }); |
| 1402 | 1327 | ||
| ... | @@ -1406,7 +1331,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1406,7 +1331,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1406 | sessionId: context.acpSessionId, | 1331 | sessionId: context.acpSessionId, |
| 1407 | prompt: [{ type: "text", text: promptText }], | 1332 | prompt: [{ type: "text", text: promptText }], |
| 1408 | }), | 1333 | }), |
| 1409 | catch: (cause) => toRequestError(input.sessionId, "session/prompt", cause), | 1334 | catch: (cause) => toRequestError(input.threadId, "session/prompt", cause), |
| 1410 | }); | 1335 | }); |
| 1411 | 1336 | ||
| 1412 | const promptResult = yield* Effect.try({ | 1337 | const promptResult = yield* Effect.try({ |
| ... | @@ -1438,7 +1363,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1438,7 +1363,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1438 | }; | 1363 | }; |
| 1439 | 1364 | ||
| 1440 | return { | 1365 | return { |
| 1441 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1366 | threadId: context.session.threadId, |
| 1442 | turnId, | 1367 | turnId, |
| 1443 | ...(context.session.resumeCursor !== undefined | 1368 | ...(context.session.resumeCursor !== undefined |
| 1444 | ? { resumeCursor: context.session.resumeCursor } | 1369 | ? { resumeCursor: context.session.resumeCursor } |
| ... | @@ -1446,9 +1371,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1446,9 +1371,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1446 | } satisfies ProviderTurnStartResult; | 1371 | } satisfies ProviderTurnStartResult; |
| 1447 | }); | 1372 | }); |
| 1448 | 1373 | ||
| 1449 | const interruptTurn: CursorAdapterShape["interruptTurn"] = (sessionId, _turnId) => | 1374 | const interruptTurn: CursorAdapterShape["interruptTurn"] = (threadId, _turnId) => |
| 1450 | Effect.gen(function* () { | 1375 | Effect.gen(function* () { |
| 1451 | const context = yield* requireSession(sessionId); | 1376 | const context = yield* requireSession(threadId); |
| 1452 | if (!context.turnState) { | 1377 | if (!context.turnState) { |
| 1453 | return; | 1378 | return; |
| 1454 | } | 1379 | } |
| ... | @@ -1456,7 +1381,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1456,7 +1381,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1456 | const cancelResult = yield* Effect.tryPromise({ | 1381 | const cancelResult = yield* Effect.tryPromise({ |
| 1457 | try: async () => | 1382 | try: async () => |
| 1458 | sendRequest(context, "session/cancel", { sessionId: context.acpSessionId }, 15_000), | 1383 | sendRequest(context, "session/cancel", { sessionId: context.acpSessionId }, 15_000), |
| 1459 | catch: (cause) => toRequestError(sessionId, "session/cancel", cause), | 1384 | catch: (cause) => toRequestError(threadId, "session/cancel", cause), |
| 1460 | }).pipe( | 1385 | }).pipe( |
| 1461 | Effect.match({ | 1386 | Effect.match({ |
| 1462 | onFailure: (error) => ({ ok: false as const, error }), | 1387 | onFailure: (error) => ({ ok: false as const, error }), |
| ... | @@ -1475,11 +1400,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1475,11 +1400,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1475 | yield* completeTurn(context, "interrupted", "Turn interrupted by user.", "cancelled"); | 1400 | yield* completeTurn(context, "interrupted", "Turn interrupted by user.", "cancelled"); |
| 1476 | }); | 1401 | }); |
| 1477 | 1402 | ||
| 1478 | const readThread: CursorAdapterShape["readThread"] = (sessionId) => | 1403 | const readThread: CursorAdapterShape["readThread"] = (threadId) => |
| 1479 | Effect.gen(function* () { | 1404 | Effect.gen(function* () { |
| 1480 | const context = yield* requireSession(sessionId); | 1405 | const context = yield* requireSession(threadId); |
| 1481 | return { | 1406 | return { |
| 1482 | threadId: ProviderThreadId.makeUnsafe(context.acpSessionId), | 1407 | threadId: context.session.threadId, |
| 1483 | turns: context.turns.map((turn) => ({ | 1408 | turns: context.turns.map((turn) => ({ |
| 1484 | id: turn.id, | 1409 | id: turn.id, |
| 1485 | items: [...turn.items], | 1410 | items: [...turn.items], |
| ... | @@ -1487,22 +1412,22 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1487,22 +1412,22 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1487 | }; | 1412 | }; |
| 1488 | }); | 1413 | }); |
| 1489 | 1414 | ||
| 1490 | const rollbackThread: CursorAdapterShape["rollbackThread"] = (sessionId, _numTurns) => | 1415 | const rollbackThread: CursorAdapterShape["rollbackThread"] = (threadId, _numTurns) => |
| 1491 | Effect.fail( | 1416 | Effect.fail( |
| 1492 | new ProviderAdapterRequestError({ | 1417 | new ProviderAdapterRequestError({ |
| 1493 | provider: PROVIDER, | 1418 | provider: PROVIDER, |
| 1494 | method: "thread/rollback", | 1419 | method: "thread/rollback", |
| 1495 | detail: `Cursor ACP does not support thread rollback for session '${sessionId}'.`, | 1420 | detail: `Cursor ACP does not support thread rollback for thread '${threadId}'.`, |
| 1496 | }), | 1421 | }), |
| 1497 | ); | 1422 | ); |
| 1498 | 1423 | ||
| 1499 | const respondToRequest: CursorAdapterShape["respondToRequest"] = ( | 1424 | const respondToRequest: CursorAdapterShape["respondToRequest"] = ( |
| 1500 | sessionId, | 1425 | threadId, |
| 1501 | requestId, | 1426 | requestId, |
| 1502 | decision, | 1427 | decision, |
| 1503 | ) => | 1428 | ) => |
| 1504 | Effect.gen(function* () { | 1429 | Effect.gen(function* () { |
| 1505 | const context = yield* requireSession(sessionId); | 1430 | const context = yield* requireSession(threadId); |
| 1506 | const pending = context.pendingPermissions.get(requestId); | 1431 | const pending = context.pendingPermissions.get(requestId); |
| 1507 | if (!pending) { | 1432 | if (!pending) { |
| 1508 | return yield* new ProviderAdapterRequestError({ | 1433 | return yield* new ProviderAdapterRequestError({ |
| ... | @@ -1540,10 +1465,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1540,10 +1465,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1540 | type: "request.resolved", | 1465 | type: "request.resolved", |
| 1541 | eventId: stamp.eventId, | 1466 | eventId: stamp.eventId, |
| 1542 | provider: PROVIDER, | 1467 | provider: PROVIDER, |
| 1543 | sessionId: asRuntimeSessionId(context.session.sessionId), | ||
| 1544 | createdAt: stamp.createdAt, | 1468 | createdAt: stamp.createdAt, |
| 1545 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), | 1469 | threadId: context.session.threadId, |
| 1546 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), | 1470 | ...((context.turnState ? { turnId: context.turnState.turnId } : {})), |
| 1547 | requestId: asRuntimeRequestId(requestId), | 1471 | requestId: asRuntimeRequestId(requestId), |
| 1548 | payload: { | 1472 | payload: { |
| 1549 | requestType: pending.requestType, | 1473 | requestType: pending.requestType, |
| ... | @@ -1553,9 +1477,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1553,9 +1477,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1553 | }, | 1477 | }, |
| 1554 | }, | 1478 | }, |
| 1555 | providerRefs: { | 1479 | providerRefs: { |
| 1556 | providerSessionId: context.session.sessionId, | 1480 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 1557 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | ||
| 1558 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | ||
| 1559 | providerRequestId: String(pending.jsonRpcId), | 1481 | providerRequestId: String(pending.jsonRpcId), |
| 1560 | }, | 1482 | }, |
| 1561 | raw: { | 1483 | raw: { |
| ... | @@ -1568,9 +1490,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1568,9 +1490,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1568 | }); | 1490 | }); |
| 1569 | }); | 1491 | }); |
| 1570 | 1492 | ||
| 1571 | const stopSession: CursorAdapterShape["stopSession"] = (sessionId) => | 1493 | const stopSession: CursorAdapterShape["stopSession"] = (threadId) => |
| 1572 | Effect.gen(function* () { | 1494 | Effect.gen(function* () { |
| 1573 | const context = yield* requireSession(sessionId); | 1495 | const context = yield* requireSession(threadId); |
| 1574 | yield* stopSessionInternal(context, { | 1496 | yield* stopSessionInternal(context, { |
| 1575 | emitExitEvent: true, | 1497 | emitExitEvent: true, |
| 1576 | }); | 1498 | }); |
| ... | @@ -1579,9 +1501,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1579,9 +1501,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1579 | const listSessions: CursorAdapterShape["listSessions"] = () => | 1501 | const listSessions: CursorAdapterShape["listSessions"] = () => |
| 1580 | Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); | 1502 | Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); |
| 1581 | 1503 | ||
| 1582 | const hasSession: CursorAdapterShape["hasSession"] = (sessionId) => | 1504 | const hasSession: CursorAdapterShape["hasSession"] = (threadId) => |
| 1583 | Effect.sync(() => { | 1505 | Effect.sync(() => { |
| 1584 | const context = sessions.get(sessionId); | 1506 | const context = sessions.get(threadId); |
| 1585 | return context !== undefined && !context.stopping; | 1507 | return context !== undefined && !context.stopping; |
| 1586 | }); | 1508 | }); |
| 1587 | 1509 |
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts+1-1| ... | @@ -28,7 +28,7 @@ const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOption | ... | @@ -28,7 +28,7 @@ const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOption |
| 28 | const adapters = | 28 | const adapters = |
| 29 | options?.adapters !== undefined | 29 | options?.adapters !== undefined |
| 30 | ? options.adapters | 30 | ? options.adapters |
| 31 | : [yield* CodexAdapter, yield* ClaudeCodeAdapter, yield* CursorAdapter]; | 31 | : [yield* CodexAdapter, yield* CursorAdapter, yield* ClaudeCodeAdapter]; |
| 32 | const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); | 32 | const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); |
| 33 | 33 | ||
| 34 | const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => { | 34 | const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => { |
apps/server/src/provider/Layers/ProviderService.test.ts+79-85| ... | @@ -14,10 +14,8 @@ import { | ... | @@ -14,10 +14,8 @@ import { |
| 14 | EventId, | 14 | EventId, |
| 15 | type ProviderKind, | 15 | type ProviderKind, |
| 16 | ProviderSessionStartInput, | 16 | ProviderSessionStartInput, |
| 17 | ProviderSessionId, | ||
| 18 | ProviderThreadId, | ||
| 19 | ProviderTurnId, | ||
| 20 | ThreadId, | 17 | ThreadId, |
| 18 | TurnId, | ||
| 21 | } from "@t3tools/contracts"; | 19 | } from "@t3tools/contracts"; |
| 22 | import { it, assert, vi } from "@effect/vitest"; | 20 | import { it, assert, vi } from "@effect/vitest"; |
| 23 | import { assertFailure } from "@effect/vitest/utils"; | 21 | import { assertFailure } from "@effect/vitest/utils"; |
| ... | @@ -27,8 +25,8 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; | ... | @@ -27,8 +25,8 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 27 | 25 | ||
| 28 | import { | 26 | import { |
| 29 | ProviderAdapterSessionNotFoundError, | 27 | ProviderAdapterSessionNotFoundError, |
| 30 | ProviderSessionNotFoundError, | ||
| 31 | ProviderUnsupportedError, | 28 | ProviderUnsupportedError, |
| 29 | ProviderValidationError, | ||
| 32 | type ProviderAdapterError, | 30 | type ProviderAdapterError, |
| 33 | } from "../Errors.ts"; | 31 | } from "../Errors.ts"; |
| 34 | import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts"; | 32 | import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts"; |
| ... | @@ -45,20 +43,17 @@ import { | ... | @@ -45,20 +43,17 @@ import { |
| 45 | SqlitePersistenceMemory, | 43 | SqlitePersistenceMemory, |
| 46 | } from "../../persistence/Layers/Sqlite.ts"; | 44 | } from "../../persistence/Layers/Sqlite.ts"; |
| 47 | 45 | ||
| 48 | const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value); | ||
| 49 | const asTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); | ||
| 50 | const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId.makeUnsafe(value); | ||
| 51 | const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.makeUnsafe(value); | 46 | const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.makeUnsafe(value); |
| 52 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); | 47 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 53 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); | 48 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); |
| 49 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); | ||
| 54 | 50 | ||
| 55 | type LegacyProviderRuntimeEvent = { | 51 | type LegacyProviderRuntimeEvent = { |
| 56 | readonly type: string; | 52 | readonly type: string; |
| 57 | readonly eventId: EventId; | 53 | readonly eventId: EventId; |
| 58 | readonly provider: "codex" | "claudeCode" | "cursor"; | 54 | readonly provider: "codex" | "claudeCode" | "cursor"; |
| 59 | readonly sessionId: string; | ||
| 60 | readonly createdAt: string; | 55 | readonly createdAt: string; |
| 61 | readonly threadId?: string | undefined; | 56 | readonly threadId: ThreadId; |
| 62 | readonly turnId?: string | undefined; | 57 | readonly turnId?: string | undefined; |
| 63 | readonly itemId?: string | undefined; | 58 | readonly itemId?: string | undefined; |
| 64 | readonly requestId?: string | undefined; | 59 | readonly requestId?: string | undefined; |
| ... | @@ -67,28 +62,23 @@ type LegacyProviderRuntimeEvent = { | ... | @@ -67,28 +62,23 @@ type LegacyProviderRuntimeEvent = { |
| 67 | }; | 62 | }; |
| 68 | 63 | ||
| 69 | function makeFakeCodexAdapter(provider: ProviderKind = "codex") { | 64 | function makeFakeCodexAdapter(provider: ProviderKind = "codex") { |
| 70 | const sessions = new Map<ProviderSessionId, ProviderSession>(); | 65 | const sessions = new Map<ThreadId, ProviderSession>(); |
| 71 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); | 66 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| 72 | let nextSession = 1; | ||
| 73 | 67 | ||
| 74 | const startSession = vi.fn((input: ProviderSessionStartInput) => | 68 | const startSession = vi.fn((input: ProviderSessionStartInput) => |
| 75 | Effect.sync(() => { | 69 | Effect.sync(() => { |
| 76 | const now = new Date().toISOString(); | 70 | const now = new Date().toISOString(); |
| 77 | const next = nextSession; | ||
| 78 | const threadId = ProviderThreadId.makeUnsafe(`thread-${next}`); | ||
| 79 | const session: ProviderSession = { | 71 | const session: ProviderSession = { |
| 80 | sessionId: ProviderSessionId.makeUnsafe(`sess-${next}`), | ||
| 81 | provider, | 72 | provider, |
| 82 | status: "ready", | 73 | status: "ready", |
| 83 | runtimeMode: input.runtimeMode, | 74 | runtimeMode: input.runtimeMode, |
| 84 | threadId, | 75 | threadId: input.threadId, |
| 85 | resumeCursor: input.resumeCursor ?? { opaque: `cursor-${next}` }, | 76 | resumeCursor: input.resumeCursor ?? { opaque: `cursor-${String(input.threadId)}` }, |
| 86 | cwd: input.cwd ?? process.cwd(), | 77 | cwd: input.cwd ?? process.cwd(), |
| 87 | createdAt: now, | 78 | createdAt: now, |
| 88 | updatedAt: now, | 79 | updatedAt: now, |
| 89 | }; | 80 | }; |
| 90 | nextSession += 1; | 81 | sessions.set(session.threadId, session); |
| 91 | sessions.set(session.sessionId, session); | ||
| 92 | return session; | 82 | return session; |
| 93 | }), | 83 | }), |
| 94 | ); | 84 | ); |
| ... | @@ -97,39 +87,39 @@ function makeFakeCodexAdapter(provider: ProviderKind = "codex") { | ... | @@ -97,39 +87,39 @@ function makeFakeCodexAdapter(provider: ProviderKind = "codex") { |
| 97 | ( | 87 | ( |
| 98 | input: ProviderSendTurnInput, | 88 | input: ProviderSendTurnInput, |
| 99 | ): Effect.Effect<ProviderTurnStartResult, ProviderAdapterError> => { | 89 | ): Effect.Effect<ProviderTurnStartResult, ProviderAdapterError> => { |
| 100 | if (!sessions.has(input.sessionId)) { | 90 | if (!sessions.has(input.threadId)) { |
| 101 | return Effect.fail( | 91 | return Effect.fail( |
| 102 | new ProviderAdapterSessionNotFoundError({ | 92 | new ProviderAdapterSessionNotFoundError({ |
| 103 | provider, | 93 | provider, |
| 104 | sessionId: input.sessionId, | 94 | threadId: input.threadId, |
| 105 | }), | 95 | }), |
| 106 | ); | 96 | ); |
| 107 | } | 97 | } |
| 108 | 98 | ||
| 109 | return Effect.succeed({ | 99 | return Effect.succeed({ |
| 110 | threadId: ProviderThreadId.makeUnsafe("thread-1"), | 100 | threadId: input.threadId, |
| 111 | turnId: ProviderTurnId.makeUnsafe("turn-1"), | 101 | turnId: TurnId.makeUnsafe(`turn-${String(input.threadId)}`), |
| 112 | }); | 102 | }); |
| 113 | }, | 103 | }, |
| 114 | ); | 104 | ); |
| 115 | 105 | ||
| 116 | const interruptTurn = vi.fn( | 106 | const interruptTurn = vi.fn( |
| 117 | (_sessionId: string, _turnId?: string): Effect.Effect<void, ProviderAdapterError> => | 107 | (_threadId: ThreadId, _turnId?: TurnId): Effect.Effect<void, ProviderAdapterError> => |
| 118 | Effect.void, | 108 | Effect.void, |
| 119 | ); | 109 | ); |
| 120 | 110 | ||
| 121 | const respondToRequest = vi.fn( | 111 | const respondToRequest = vi.fn( |
| 122 | ( | 112 | ( |
| 123 | _sessionId: string, | 113 | _threadId: ThreadId, |
| 124 | _requestId: string, | 114 | _requestId: string, |
| 125 | _decision: ProviderApprovalDecision, | 115 | _decision: ProviderApprovalDecision, |
| 126 | ): Effect.Effect<void, ProviderAdapterError> => Effect.void, | 116 | ): Effect.Effect<void, ProviderAdapterError> => Effect.void, |
| 127 | ); | 117 | ); |
| 128 | 118 | ||
| 129 | const stopSession = vi.fn( | 119 | const stopSession = vi.fn( |
| 130 | (sessionId: string): Effect.Effect<void, ProviderAdapterError> => | 120 | (threadId: ThreadId): Effect.Effect<void, ProviderAdapterError> => |
| 131 | Effect.sync(() => { | 121 | Effect.sync(() => { |
| 132 | sessions.delete(ProviderSessionId.makeUnsafe(sessionId)); | 122 | sessions.delete(threadId); |
| 133 | }), | 123 | }), |
| 134 | ); | 124 | ); |
| 135 | 125 | ||
| ... | @@ -139,32 +129,31 @@ function makeFakeCodexAdapter(provider: ProviderKind = "codex") { | ... | @@ -139,32 +129,31 @@ function makeFakeCodexAdapter(provider: ProviderKind = "codex") { |
| 139 | ); | 129 | ); |
| 140 | 130 | ||
| 141 | const hasSession = vi.fn( | 131 | const hasSession = vi.fn( |
| 142 | (sessionId: string): Effect.Effect<boolean> => | 132 | (threadId: ThreadId): Effect.Effect<boolean> => Effect.succeed(sessions.has(threadId)), |
| 143 | Effect.succeed(sessions.has(ProviderSessionId.makeUnsafe(sessionId))), | ||
| 144 | ); | 133 | ); |
| 145 | 134 | ||
| 146 | const readThread = vi.fn( | 135 | const readThread = vi.fn( |
| 147 | ( | 136 | ( |
| 148 | _sessionId: ProviderSessionId, | 137 | threadId: ThreadId, |
| 149 | ): Effect.Effect< | 138 | ): Effect.Effect< |
| 150 | { | 139 | { |
| 151 | threadId: ProviderThreadId; | 140 | threadId: ThreadId; |
| 152 | turns: ReadonlyArray<{ id: ProviderTurnId; items: readonly [] }>; | 141 | turns: ReadonlyArray<{ id: TurnId; items: readonly [] }>; |
| 153 | }, | 142 | }, |
| 154 | ProviderAdapterError | 143 | ProviderAdapterError |
| 155 | > => | 144 | > => |
| 156 | Effect.succeed({ | 145 | Effect.succeed({ |
| 157 | threadId: ProviderThreadId.makeUnsafe("thread-1"), | 146 | threadId, |
| 158 | turns: [{ id: asTurnId("turn-1"), items: [] }], | 147 | turns: [{ id: asTurnId("turn-1"), items: [] }], |
| 159 | }), | 148 | }), |
| 160 | ); | 149 | ); |
| 161 | 150 | ||
| 162 | const rollbackThread = vi.fn( | 151 | const rollbackThread = vi.fn( |
| 163 | ( | 152 | ( |
| 164 | _sessionId: ProviderSessionId, | 153 | threadId: ThreadId, |
| 165 | _numTurns: number, | 154 | _numTurns: number, |
| 166 | ): Effect.Effect<{ threadId: ProviderThreadId; turns: readonly [] }, ProviderAdapterError> => | 155 | ): Effect.Effect<{ threadId: ThreadId; turns: readonly [] }, ProviderAdapterError> => |
| 167 | Effect.succeed({ threadId: ProviderThreadId.makeUnsafe("thread-1"), turns: [] }), | 156 | Effect.succeed({ threadId, turns: [] }), |
| 168 | ); | 157 | ); |
| 169 | 158 | ||
| 170 | const stopAll = vi.fn( | 159 | const stopAll = vi.fn( |
| ... | @@ -277,7 +266,6 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( | ... | @@ -277,7 +266,6 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( |
| 277 | yield* Effect.gen(function* () { | 266 | yield* Effect.gen(function* () { |
| 278 | const directory = yield* ProviderSessionDirectory; | 267 | const directory = yield* ProviderSessionDirectory; |
| 279 | yield* directory.upsert({ | 268 | yield* directory.upsert({ |
| 280 | sessionId: asSessionId("sess-stale"), | ||
| 281 | provider: "codex", | 269 | provider: "codex", |
| 282 | threadId: ThreadId.makeUnsafe("thread-stale"), | 270 | threadId: ThreadId.makeUnsafe("thread-stale"), |
| 283 | }); | 271 | }); |
| ... | @@ -294,13 +282,13 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( | ... | @@ -294,13 +282,13 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( |
| 294 | 282 | ||
| 295 | const persistedProvider = yield* Effect.gen(function* () { | 283 | const persistedProvider = yield* Effect.gen(function* () { |
| 296 | const directory = yield* ProviderSessionDirectory; | 284 | const directory = yield* ProviderSessionDirectory; |
| 297 | return yield* directory.getProvider(asSessionId("sess-stale")); | 285 | return yield* directory.getProvider(asThreadId("thread-stale")); |
| 298 | }).pipe(Effect.provide(directoryLayer)); | 286 | }).pipe(Effect.provide(directoryLayer)); |
| 299 | assert.equal(persistedProvider, "codex"); | 287 | assert.equal(persistedProvider, "codex"); |
| 300 | 288 | ||
| 301 | const runtime = yield* Effect.gen(function* () { | 289 | const runtime = yield* Effect.gen(function* () { |
| 302 | const repository = yield* ProviderSessionRuntimeRepository; | 290 | const repository = yield* ProviderSessionRuntimeRepository; |
| 303 | return yield* repository.getBySessionId({ providerSessionId: asSessionId("sess-stale") }); | 291 | return yield* repository.getByThreadId({ threadId: asThreadId("thread-stale") }); |
| 304 | }).pipe(Effect.provide(runtimeRepositoryLayer)); | 292 | }).pipe(Effect.provide(runtimeRepositoryLayer)); |
| 305 | assert.equal(Option.isSome(runtime), true); | 293 | assert.equal(Option.isSome(runtime), true); |
| 306 | 294 | ||
| ... | @@ -348,10 +336,12 @@ it.effect( | ... | @@ -348,10 +336,12 @@ it.effect( |
| 348 | 336 | ||
| 349 | const startedSession = yield* Effect.gen(function* () { | 337 | const startedSession = yield* Effect.gen(function* () { |
| 350 | const provider = yield* ProviderService; | 338 | const provider = yield* ProviderService; |
| 351 | return yield* provider.startSession(asThreadId("thread-1"), { | 339 | const threadId = asThreadId("thread-1"); |
| 340 | return yield* provider.startSession(threadId, { | ||
| 352 | provider: "codex", | 341 | provider: "codex", |
| 353 | cwd: "/tmp/project", | 342 | cwd: "/tmp/project", |
| 354 | runtimeMode: "full-access", | 343 | runtimeMode: "full-access", |
| 344 | threadId, | ||
| 355 | }); | 345 | }); |
| 356 | }).pipe(Effect.provide(firstProviderLayer)); | 346 | }).pipe(Effect.provide(firstProviderLayer)); |
| 357 | 347 | ||
| ... | @@ -362,9 +352,7 @@ it.effect( | ... | @@ -362,9 +352,7 @@ it.effect( |
| 362 | 352 | ||
| 363 | const persistedAfterStopAll = yield* Effect.gen(function* () { | 353 | const persistedAfterStopAll = yield* Effect.gen(function* () { |
| 364 | const repository = yield* ProviderSessionRuntimeRepository; | 354 | const repository = yield* ProviderSessionRuntimeRepository; |
| 365 | return yield* repository.getBySessionId({ | 355 | return yield* repository.getByThreadId({ threadId: startedSession.threadId }); |
| 366 | providerSessionId: startedSession.sessionId, | ||
| 367 | }); | ||
| 368 | }).pipe(Effect.provide(runtimeRepositoryLayer)); | 356 | }).pipe(Effect.provide(runtimeRepositoryLayer)); |
| 369 | assert.equal(Option.isSome(persistedAfterStopAll), true); | 357 | assert.equal(Option.isSome(persistedAfterStopAll), true); |
| 370 | if (Option.isSome(persistedAfterStopAll)) { | 358 | if (Option.isSome(persistedAfterStopAll)) { |
| ... | @@ -394,7 +382,7 @@ it.effect( | ... | @@ -394,7 +382,7 @@ it.effect( |
| 394 | yield* Effect.gen(function* () { | 382 | yield* Effect.gen(function* () { |
| 395 | const provider = yield* ProviderService; | 383 | const provider = yield* ProviderService; |
| 396 | yield* provider.rollbackConversation({ | 384 | yield* provider.rollbackConversation({ |
| 397 | sessionId: startedSession.sessionId, | 385 | threadId: startedSession.threadId, |
| 398 | numTurns: 1, | 386 | numTurns: 1, |
| 399 | }); | 387 | }); |
| 400 | }).pipe(Effect.provide(secondProviderLayer)); | 388 | }).pipe(Effect.provide(secondProviderLayer)); |
| ... | @@ -430,6 +418,7 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -430,6 +418,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 430 | 418 | ||
| 431 | const session = yield* provider.startSession(asThreadId("thread-1"), { | 419 | const session = yield* provider.startSession(asThreadId("thread-1"), { |
| 432 | provider: "codex", | 420 | provider: "codex", |
| 421 | threadId: asThreadId("thread-1"), | ||
| 433 | cwd: "/tmp/project", | 422 | cwd: "/tmp/project", |
| 434 | runtimeMode: "full-access", | 423 | runtimeMode: "full-access", |
| 435 | }); | 424 | }); |
| ... | @@ -439,40 +428,43 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -439,40 +428,43 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 439 | assert.equal(sessions.length, 1); | 428 | assert.equal(sessions.length, 1); |
| 440 | 429 | ||
| 441 | yield* provider.sendTurn({ | 430 | yield* provider.sendTurn({ |
| 442 | sessionId: session.sessionId, | 431 | threadId: session.threadId, |
| 443 | input: "hello", | 432 | input: "hello", |
| 444 | attachments: [], | 433 | attachments: [], |
| 445 | }); | 434 | }); |
| 446 | assert.equal(routing.codex.sendTurn.mock.calls.length, 1); | 435 | assert.equal(routing.codex.sendTurn.mock.calls.length, 1); |
| 447 | 436 | ||
| 448 | yield* provider.interruptTurn({ sessionId: session.sessionId }); | 437 | yield* provider.interruptTurn({ threadId: session.threadId }); |
| 449 | assert.deepEqual(routing.codex.interruptTurn.mock.calls, [[session.sessionId, undefined]]); | 438 | assert.deepEqual(routing.codex.interruptTurn.mock.calls, [[session.threadId, undefined]]); |
| 450 | 439 | ||
| 451 | yield* provider.respondToRequest({ | 440 | yield* provider.respondToRequest({ |
| 452 | sessionId: session.sessionId, | 441 | threadId: session.threadId, |
| 453 | requestId: asRequestId("req-1"), | 442 | requestId: asRequestId("req-1"), |
| 454 | decision: "accept", | 443 | decision: "accept", |
| 455 | }); | 444 | }); |
| 456 | assert.deepEqual(routing.codex.respondToRequest.mock.calls, [ | 445 | assert.deepEqual(routing.codex.respondToRequest.mock.calls, [ |
| 457 | [session.sessionId, asRequestId("req-1"), "accept"], | 446 | [session.threadId, asRequestId("req-1"), "accept"], |
| 458 | ]); | 447 | ]); |
| 459 | 448 | ||
| 460 | yield* provider.rollbackConversation({ | 449 | yield* provider.rollbackConversation({ |
| 461 | sessionId: session.sessionId, | 450 | threadId: session.threadId, |
| 462 | numTurns: 0, | 451 | numTurns: 0, |
| 463 | }); | 452 | }); |
| 464 | 453 | ||
| 465 | yield* provider.stopSession({ sessionId: session.sessionId }); | 454 | yield* provider.stopSession({ threadId: session.threadId }); |
| 466 | const sendAfterStop = yield* Effect.result( | 455 | const sendAfterStop = yield* Effect.result( |
| 467 | provider.sendTurn({ | 456 | provider.sendTurn({ |
| 468 | sessionId: session.sessionId, | 457 | threadId: session.threadId, |
| 469 | input: "after-stop", | 458 | input: "after-stop", |
| 470 | attachments: [], | 459 | attachments: [], |
| 471 | }), | 460 | }), |
| 472 | ); | 461 | ); |
| 473 | assertFailure( | 462 | assertFailure( |
| 474 | sendAfterStop, | 463 | sendAfterStop, |
| 475 | new ProviderSessionNotFoundError({ sessionId: session.sessionId }), | 464 | new ProviderValidationError({ |
| 465 | operation: "ProviderService.sendTurn", | ||
| 466 | issue: `Cannot route thread '${session.threadId}' because no persisted provider binding exists.`, | ||
| 467 | }), | ||
| 476 | ); | 468 | ); |
| 477 | }), | 469 | }), |
| 478 | ); | 470 | ); |
| ... | @@ -483,6 +475,7 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -483,6 +475,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 483 | 475 | ||
| 484 | const session = yield* provider.startSession(asThreadId("thread-claude"), { | 476 | const session = yield* provider.startSession(asThreadId("thread-claude"), { |
| 485 | provider: "claudeCode", | 477 | provider: "claudeCode", |
| 478 | threadId: asThreadId("thread-claude"), | ||
| 486 | cwd: "/tmp/project-claude", | 479 | cwd: "/tmp/project-claude", |
| 487 | runtimeMode: "full-access", | 480 | runtimeMode: "full-access", |
| 488 | }); | 481 | }); |
| ... | @@ -505,15 +498,16 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -505,15 +498,16 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 505 | 498 | ||
| 506 | const initial = yield* provider.startSession(asThreadId("thread-1"), { | 499 | const initial = yield* provider.startSession(asThreadId("thread-1"), { |
| 507 | provider: "codex", | 500 | provider: "codex", |
| 501 | threadId: asThreadId("thread-1"), | ||
| 508 | cwd: "/tmp/project", | 502 | cwd: "/tmp/project", |
| 509 | runtimeMode: "full-access", | 503 | runtimeMode: "full-access", |
| 510 | }); | 504 | }); |
| 511 | yield* routing.codex.stopSession(initial.sessionId); | 505 | yield* routing.codex.stopSession(initial.threadId); |
| 512 | routing.codex.startSession.mockClear(); | 506 | routing.codex.startSession.mockClear(); |
| 513 | routing.codex.rollbackThread.mockClear(); | 507 | routing.codex.rollbackThread.mockClear(); |
| 514 | 508 | ||
| 515 | yield* provider.rollbackConversation({ | 509 | yield* provider.rollbackConversation({ |
| 516 | sessionId: initial.sessionId, | 510 | threadId: initial.threadId, |
| 517 | numTurns: 1, | 511 | numTurns: 1, |
| 518 | }); | 512 | }); |
| 519 | 513 | ||
| ... | @@ -544,6 +538,7 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -544,6 +538,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 544 | 538 | ||
| 545 | const initial = yield* provider.startSession(asThreadId("thread-1"), { | 539 | const initial = yield* provider.startSession(asThreadId("thread-1"), { |
| 546 | provider: "codex", | 540 | provider: "codex", |
| 541 | threadId: asThreadId("thread-1"), | ||
| 547 | cwd: "/tmp/project-send-turn", | 542 | cwd: "/tmp/project-send-turn", |
| 548 | runtimeMode: "full-access", | 543 | runtimeMode: "full-access", |
| 549 | }); | 544 | }); |
| ... | @@ -553,7 +548,7 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -553,7 +548,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 553 | routing.codex.sendTurn.mockClear(); | 548 | routing.codex.sendTurn.mockClear(); |
| 554 | 549 | ||
| 555 | yield* provider.sendTurn({ | 550 | yield* provider.sendTurn({ |
| 556 | sessionId: initial.sessionId, | 551 | threadId: initial.threadId, |
| 557 | input: "resume", | 552 | input: "resume", |
| 558 | attachments: [], | 553 | attachments: [], |
| 559 | }); | 554 | }); |
| ... | @@ -583,10 +578,12 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -583,10 +578,12 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 583 | 578 | ||
| 584 | yield* provider.startSession(asThreadId("thread-1"), { | 579 | yield* provider.startSession(asThreadId("thread-1"), { |
| 585 | provider: "codex", | 580 | provider: "codex", |
| 581 | threadId: asThreadId("thread-1"), | ||
| 586 | runtimeMode: "full-access", | 582 | runtimeMode: "full-access", |
| 587 | }); | 583 | }); |
| 588 | yield* provider.startSession(asThreadId("thread-2"), { | 584 | yield* provider.startSession(asThreadId("thread-2"), { |
| 589 | provider: "codex", | 585 | provider: "codex", |
| 586 | threadId: asThreadId("thread-2"), | ||
| 590 | runtimeMode: "full-access", | 587 | runtimeMode: "full-access", |
| 591 | }); | 588 | }); |
| 592 | 589 | ||
| ... | @@ -604,16 +601,17 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -604,16 +601,17 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 604 | 601 | ||
| 605 | const session = yield* provider.startSession(asThreadId("thread-1"), { | 602 | const session = yield* provider.startSession(asThreadId("thread-1"), { |
| 606 | provider: "codex", | 603 | provider: "codex", |
| 604 | threadId: asThreadId("thread-1"), | ||
| 607 | runtimeMode: "full-access", | 605 | runtimeMode: "full-access", |
| 608 | }); | 606 | }); |
| 609 | yield* provider.sendTurn({ | 607 | yield* provider.sendTurn({ |
| 610 | sessionId: session.sessionId, | 608 | threadId: session.threadId, |
| 611 | input: "hello", | 609 | input: "hello", |
| 612 | attachments: [], | 610 | attachments: [], |
| 613 | }); | 611 | }); |
| 614 | 612 | ||
| 615 | const runningRuntime = yield* runtimeRepository.getBySessionId({ | 613 | const runningRuntime = yield* runtimeRepository.getByThreadId({ |
| 616 | providerSessionId: session.sessionId, | 614 | threadId: session.threadId, |
| 617 | }); | 615 | }); |
| 618 | assert.equal(Option.isSome(runningRuntime), true); | 616 | assert.equal(Option.isSome(runningRuntime), true); |
| 619 | if (Option.isSome(runningRuntime)) { | 617 | if (Option.isSome(runningRuntime)) { |
| ... | @@ -638,8 +636,8 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -638,8 +636,8 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 638 | } | 636 | } |
| 639 | 637 | ||
| 640 | yield* provider.stopAll(); | 638 | yield* provider.stopAll(); |
| 641 | const stoppedRuntime = yield* runtimeRepository.getBySessionId({ | 639 | const stoppedRuntime = yield* runtimeRepository.getByThreadId({ |
| 642 | providerSessionId: session.sessionId, | 640 | threadId: session.threadId, |
| 643 | }); | 641 | }); |
| 644 | assert.equal(Option.isSome(stoppedRuntime), true); | 642 | assert.equal(Option.isSome(stoppedRuntime), true); |
| 645 | if (Option.isSome(stoppedRuntime)) { | 643 | if (Option.isSome(stoppedRuntime)) { |
| ... | @@ -656,6 +654,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -656,6 +654,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 656 | const provider = yield* ProviderService; | 654 | const provider = yield* ProviderService; |
| 657 | const session = yield* provider.startSession(asThreadId("thread-1"), { | 655 | const session = yield* provider.startSession(asThreadId("thread-1"), { |
| 658 | provider: "codex", | 656 | provider: "codex", |
| 657 | threadId: asThreadId("thread-1"), | ||
| 659 | runtimeMode: "full-access", | 658 | runtimeMode: "full-access", |
| 660 | }); | 659 | }); |
| 661 | 660 | ||
| ... | @@ -669,9 +668,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -669,9 +668,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 669 | type: "turn.completed", | 668 | type: "turn.completed", |
| 670 | eventId: asEventId("evt-1"), | 669 | eventId: asEventId("evt-1"), |
| 671 | provider: "codex", | 670 | provider: "codex", |
| 672 | sessionId: session.sessionId, | ||
| 673 | createdAt: new Date().toISOString(), | 671 | createdAt: new Date().toISOString(), |
| 674 | threadId: asProviderThreadId("thread-1"), | 672 | threadId: session.threadId, |
| 675 | turnId: asTurnId("turn-1"), | 673 | turnId: asTurnId("turn-1"), |
| 676 | status: "completed", | 674 | status: "completed", |
| 677 | }; | 675 | }; |
| ... | @@ -694,6 +692,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -694,6 +692,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 694 | const provider = yield* ProviderService; | 692 | const provider = yield* ProviderService; |
| 695 | const session = yield* provider.startSession(asThreadId("thread-seq"), { | 693 | const session = yield* provider.startSession(asThreadId("thread-seq"), { |
| 696 | provider: "codex", | 694 | provider: "codex", |
| 695 | threadId: asThreadId("thread-seq"), | ||
| 697 | runtimeMode: "full-access", | 696 | runtimeMode: "full-access", |
| 698 | }); | 697 | }); |
| 699 | 698 | ||
| ... | @@ -708,9 +707,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -708,9 +707,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 708 | type: "tool.started", | 707 | type: "tool.started", |
| 709 | eventId: asEventId("evt-seq-1"), | 708 | eventId: asEventId("evt-seq-1"), |
| 710 | provider: "codex", | 709 | provider: "codex", |
| 711 | sessionId: session.sessionId, | ||
| 712 | createdAt: new Date().toISOString(), | 710 | createdAt: new Date().toISOString(), |
| 713 | threadId: asProviderThreadId("thread-1"), | 711 | threadId: session.threadId, |
| 714 | turnId: asTurnId("turn-1"), | 712 | turnId: asTurnId("turn-1"), |
| 715 | toolKind: "command", | 713 | toolKind: "command", |
| 716 | title: "Command run", | 714 | title: "Command run", |
| ... | @@ -719,9 +717,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -719,9 +717,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 719 | type: "tool.completed", | 717 | type: "tool.completed", |
| 720 | eventId: asEventId("evt-seq-2"), | 718 | eventId: asEventId("evt-seq-2"), |
| 721 | provider: "codex", | 719 | provider: "codex", |
| 722 | sessionId: session.sessionId, | ||
| 723 | createdAt: new Date().toISOString(), | 720 | createdAt: new Date().toISOString(), |
| 724 | threadId: asProviderThreadId("thread-1"), | 721 | threadId: session.threadId, |
| 725 | turnId: asTurnId("turn-1"), | 722 | turnId: asTurnId("turn-1"), |
| 726 | toolKind: "command", | 723 | toolKind: "command", |
| 727 | title: "Command run", | 724 | title: "Command run", |
| ... | @@ -730,9 +727,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -730,9 +727,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 730 | type: "turn.completed", | 727 | type: "turn.completed", |
| 731 | eventId: asEventId("evt-seq-3"), | 728 | eventId: asEventId("evt-seq-3"), |
| 732 | provider: "codex", | 729 | provider: "codex", |
| 733 | sessionId: session.sessionId, | ||
| 734 | createdAt: new Date().toISOString(), | 730 | createdAt: new Date().toISOString(), |
| 735 | threadId: asProviderThreadId("thread-1"), | 731 | threadId: session.threadId, |
| 736 | turnId: asTurnId("turn-1"), | 732 | turnId: asTurnId("turn-1"), |
| 737 | status: "completed", | 733 | status: "completed", |
| 738 | }); | 734 | }); |
| ... | @@ -751,6 +747,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -751,6 +747,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 751 | const provider = yield* ProviderService; | 747 | const provider = yield* ProviderService; |
| 752 | const session = yield* provider.startSession(asThreadId("thread-1"), { | 748 | const session = yield* provider.startSession(asThreadId("thread-1"), { |
| 753 | provider: "codex", | 749 | provider: "codex", |
| 750 | threadId: asThreadId("thread-1"), | ||
| 754 | runtimeMode: "full-access", | 751 | runtimeMode: "full-access", |
| 755 | }); | 752 | }); |
| 756 | 753 | ||
| ... | @@ -775,9 +772,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -775,9 +772,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 775 | type: "tool.completed", | 772 | type: "tool.completed", |
| 776 | eventId: asEventId("evt-ordered-1"), | 773 | eventId: asEventId("evt-ordered-1"), |
| 777 | provider: "codex", | 774 | provider: "codex", |
| 778 | sessionId: session.sessionId, | ||
| 779 | createdAt: new Date().toISOString(), | 775 | createdAt: new Date().toISOString(), |
| 780 | threadId: asProviderThreadId("thread-1"), | 776 | threadId: session.threadId, |
| 781 | turnId: asTurnId("turn-1"), | 777 | turnId: asTurnId("turn-1"), |
| 782 | toolKind: "command", | 778 | toolKind: "command", |
| 783 | title: "Command run", | 779 | title: "Command run", |
| ... | @@ -787,9 +783,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -787,9 +783,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 787 | type: "message.delta", | 783 | type: "message.delta", |
| 788 | eventId: asEventId("evt-ordered-2"), | 784 | eventId: asEventId("evt-ordered-2"), |
| 789 | provider: "codex", | 785 | provider: "codex", |
| 790 | sessionId: session.sessionId, | ||
| 791 | createdAt: new Date().toISOString(), | 786 | createdAt: new Date().toISOString(), |
| 792 | threadId: asProviderThreadId("thread-1"), | 787 | threadId: session.threadId, |
| 793 | turnId: asTurnId("turn-1"), | 788 | turnId: asTurnId("turn-1"), |
| 794 | delta: "hello", | 789 | delta: "hello", |
| 795 | }, | 790 | }, |
| ... | @@ -797,9 +792,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -797,9 +792,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 797 | type: "turn.completed", | 792 | type: "turn.completed", |
| 798 | eventId: asEventId("evt-ordered-3"), | 793 | eventId: asEventId("evt-ordered-3"), |
| 799 | provider: "codex", | 794 | provider: "codex", |
| 800 | sessionId: session.sessionId, | ||
| 801 | createdAt: new Date().toISOString(), | 795 | createdAt: new Date().toISOString(), |
| 802 | threadId: asProviderThreadId("thread-1"), | 796 | threadId: session.threadId, |
| 803 | turnId: asTurnId("turn-1"), | 797 | turnId: asTurnId("turn-1"), |
| 804 | status: "completed", | 798 | status: "completed", |
| 805 | }, | 799 | }, |
| ... | @@ -854,32 +848,32 @@ validation.layer("ProviderServiceLive validation", (it) => { | ... | @@ -854,32 +848,32 @@ validation.layer("ProviderServiceLive validation", (it) => { |
| 854 | Effect.sync(() => { | 848 | Effect.sync(() => { |
| 855 | const now = new Date().toISOString(); | 849 | const now = new Date().toISOString(); |
| 856 | return { | 850 | return { |
| 857 | sessionId: asSessionId("sess-missing-thread"), | ||
| 858 | provider: "codex", | 851 | provider: "codex", |
| 859 | status: "ready", | 852 | status: "ready", |
| 853 | threadId: input.threadId, | ||
| 860 | runtimeMode: input.runtimeMode, | 854 | runtimeMode: input.runtimeMode, |
| 861 | cwd: input.cwd ?? process.cwd(), | 855 | cwd: input.cwd ?? process.cwd(), |
| 862 | createdAt: now, | 856 | createdAt: now, |
| 863 | updatedAt: now, | 857 | updatedAt: now, |
| 864 | } satisfies ProviderSession; | 858 | } satisfies ProviderSession; |
| 865 | }), | 859 | }), |
| 866 | ); | 860 | ); |
| 867 | 861 | ||
| 868 | const session = yield* provider.startSession(asThreadId("thread-missing"), { | 862 | const session = yield* provider.startSession(asThreadId("thread-missing"), { |
| 869 | provider: "codex", | 863 | provider: "codex", |
| 864 | threadId: asThreadId("thread-missing"), | ||
| 870 | cwd: "/tmp/project", | 865 | cwd: "/tmp/project", |
| 871 | runtimeMode: "full-access", | 866 | runtimeMode: "full-access", |
| 872 | }); | 867 | }); |
| 873 | 868 | ||
| 874 | assert.equal(session.sessionId, asSessionId("sess-missing-thread")); | 869 | assert.equal(session.threadId, asThreadId("thread-missing")); |
| 875 | assert.equal(session.threadId, undefined); | ||
| 876 | 870 | ||
| 877 | const runtime = yield* runtimeRepository.getBySessionId({ | 871 | const runtime = yield* runtimeRepository.getByThreadId({ |
| 878 | providerSessionId: session.sessionId, | 872 | threadId: session.threadId, |
| 879 | }); | 873 | }); |
| 880 | assert.equal(Option.isSome(runtime), true); | 874 | assert.equal(Option.isSome(runtime), true); |
| 881 | if (Option.isSome(runtime)) { | 875 | if (Option.isSome(runtime)) { |
| 882 | assert.equal(runtime.value.providerThreadId, null); | 876 | assert.equal(runtime.value.threadId, session.threadId); |
| 883 | } | 877 | } |
| 884 | }), | 878 | }), |
| 885 | ); | 879 | ); |
apps/server/src/provider/Layers/ProviderService.ts+62-255| ... | @@ -11,8 +11,6 @@ | ... | @@ -11,8 +11,6 @@ |
| 11 | */ | 11 | */ |
| 12 | import { | 12 | import { |
| 13 | NonNegativeInt, | 13 | NonNegativeInt, |
| 14 | ProviderSessionId, | ||
| 15 | RuntimeSessionId, | ||
| 16 | ThreadId, | 14 | ThreadId, |
| 17 | ProviderInterruptTurnInput, | 15 | ProviderInterruptTurnInput, |
| 18 | ProviderRespondToRequestInput, | 16 | ProviderRespondToRequestInput, |
| ... | @@ -22,14 +20,14 @@ import { | ... | @@ -22,14 +20,14 @@ import { |
| 22 | type ProviderRuntimeEvent, | 20 | type ProviderRuntimeEvent, |
| 23 | type ProviderSession, | 21 | type ProviderSession, |
| 24 | } from "@t3tools/contracts"; | 22 | } from "@t3tools/contracts"; |
| 25 | import { Effect, Layer, Option, PubSub, Queue, Ref, Schema, SchemaIssue, Stream } from "effect"; | 23 | import { Effect, Layer, Option, PubSub, Queue, Schema, SchemaIssue, Stream } from "effect"; |
| 26 | 24 | ||
| 27 | import { ProviderSessionNotFoundError, ProviderValidationError } from "../Errors.ts"; | 25 | import { ProviderValidationError } from "../Errors.ts"; |
| 28 | import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; | 26 | import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; |
| 29 | import { ProviderService, type ProviderServiceShape } from "../Services/ProviderService.ts"; | 27 | import { ProviderService, type ProviderServiceShape } from "../Services/ProviderService.ts"; |
| 30 | import { | 28 | import { |
| 31 | ProviderSessionDirectory, | 29 | ProviderSessionDirectory, |
| 32 | type ProviderSessionBinding, | 30 | type ProviderRuntimeBinding, |
| 33 | } from "../Services/ProviderSessionDirectory.ts"; | 31 | } from "../Services/ProviderSessionDirectory.ts"; |
| 34 | import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; | 32 | import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; |
| 35 | 33 | ||
| ... | @@ -39,7 +37,7 @@ export interface ProviderServiceLiveOptions { | ... | @@ -39,7 +37,7 @@ export interface ProviderServiceLiveOptions { |
| 39 | } | 37 | } |
| 40 | 38 | ||
| 41 | const ProviderRollbackConversationInput = Schema.Struct({ | 39 | const ProviderRollbackConversationInput = Schema.Struct({ |
| 42 | sessionId: ProviderSessionId, | 40 | threadId: ThreadId, |
| 43 | numTurns: NonNegativeInt, | 41 | numTurns: NonNegativeInt, |
| 44 | }); | 42 | }); |
| 45 | 43 | ||
| ... | @@ -96,7 +94,7 @@ function toRuntimePayloadFromSession(session: ProviderSession): Record<string, u | ... | @@ -96,7 +94,7 @@ function toRuntimePayloadFromSession(session: ProviderSession): Record<string, u |
| 96 | } | 94 | } |
| 97 | 95 | ||
| 98 | function readPersistedCwd( | 96 | function readPersistedCwd( |
| 99 | runtimePayload: ProviderSessionBinding["runtimePayload"], | 97 | runtimePayload: ProviderRuntimeBinding["runtimePayload"], |
| 100 | ): string | undefined { | 98 | ): string | undefined { |
| 101 | if (!runtimePayload || typeof runtimePayload !== "object" || Array.isArray(runtimePayload)) { | 99 | if (!runtimePayload || typeof runtimePayload !== "object" || Array.isArray(runtimePayload)) { |
| 102 | return undefined; | 100 | return undefined; |
| ... | @@ -121,47 +119,12 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -121,47 +119,12 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 121 | const directory = yield* ProviderSessionDirectory; | 119 | const directory = yield* ProviderSessionDirectory; |
| 122 | const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); | 120 | const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); |
| 123 | const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>(); | 121 | const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>(); |
| 124 | const routedSessionAliasesRef = yield* Ref.make<Map<ProviderSessionId, ProviderSessionId>>( | ||
| 125 | new Map(), | ||
| 126 | ); | ||
| 127 | const asRuntimeSessionId = (sessionId: ProviderSessionId): RuntimeSessionId => | ||
| 128 | RuntimeSessionId.makeUnsafe(sessionId); | ||
| 129 | const asProviderSessionId = (sessionId: RuntimeSessionId): ProviderSessionId => | ||
| 130 | ProviderSessionId.makeUnsafe(sessionId); | ||
| 131 | |||
| 132 | const canonicalizeRuntimeEventSession = ( | ||
| 133 | event: ProviderRuntimeEvent, | ||
| 134 | ): Effect.Effect<ProviderRuntimeEvent> => | ||
| 135 | Ref.get(routedSessionAliasesRef).pipe( | ||
| 136 | Effect.map((aliases) => { | ||
| 137 | for (const [staleSessionId, liveSessionId] of aliases) { | ||
| 138 | if (asRuntimeSessionId(liveSessionId) === event.sessionId) { | ||
| 139 | return { | ||
| 140 | ...event, | ||
| 141 | sessionId: asRuntimeSessionId(staleSessionId), | ||
| 142 | } satisfies ProviderRuntimeEvent; | ||
| 143 | } | ||
| 144 | } | ||
| 145 | return event; | ||
| 146 | }), | ||
| 147 | ); | ||
| 148 | 122 | ||
| 149 | const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => | 123 | const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => |
| 150 | canonicalizeRuntimeEventSession(event).pipe( | 124 | Effect.succeed(event).pipe( |
| 151 | Effect.tap((canonicalEvent) => | 125 | Effect.tap((canonicalEvent) => |
| 152 | canonicalEventLogger | 126 | canonicalEventLogger |
| 153 | ? Effect.flatMap( | 127 | ? canonicalEventLogger.write(canonicalEvent, null) |
| 154 | Effect.catch( | ||
| 155 | directory.getThreadId(asProviderSessionId(canonicalEvent.sessionId)).pipe( | ||
| 156 | Effect.map((threadIdOption) => | ||
| 157 | Option.isSome(threadIdOption) ? threadIdOption.value : null, | ||
| 158 | ), | ||
| 159 | ), | ||
| 160 | () => Effect.succeed<ThreadId | null>(null), | ||
| 161 | ), | ||
| 162 | (orchestrationThreadId) => | ||
| 163 | canonicalEventLogger.write(canonicalEvent, orchestrationThreadId), | ||
| 164 | ) | ||
| 165 | : Effect.void, | 128 | : Effect.void, |
| 166 | ), | 129 | ), |
| 167 | Effect.flatMap((canonicalEvent) => PubSub.publish(runtimeEventPubSub, canonicalEvent)), | 130 | Effect.flatMap((canonicalEvent) => PubSub.publish(runtimeEventPubSub, canonicalEvent)), |
| ... | @@ -172,56 +135,13 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -172,56 +135,13 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 172 | session: ProviderSession, | 135 | session: ProviderSession, |
| 173 | threadId: ThreadId, | 136 | threadId: ThreadId, |
| 174 | ) => | 137 | ) => |
| 175 | Effect.gen(function* () { | 138 | directory.upsert({ |
| 176 | const providerThreadId = session.threadId; | 139 | threadId, |
| 177 | 140 | provider: session.provider, | |
| 178 | yield* directory.upsert({ | 141 | runtimeMode: session.runtimeMode, |
| 179 | sessionId: session.sessionId, | 142 | status: toRuntimeStatus(session), |
| 180 | provider: session.provider, | 143 | ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}), |
| 181 | threadId, | 144 | runtimePayload: toRuntimePayloadFromSession(session), |
| 182 | ...(providerThreadId !== undefined ? { providerThreadId } : {}), | ||
| 183 | runtimeMode: session.runtimeMode, | ||
| 184 | status: toRuntimeStatus(session), | ||
| 185 | ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}), | ||
| 186 | runtimePayload: toRuntimePayloadFromSession(session), | ||
| 187 | }); | ||
| 188 | |||
| 189 | return providerThreadId; | ||
| 190 | }); | ||
| 191 | |||
| 192 | const clearAliasKey = (staleSessionId: ProviderSessionId) => | ||
| 193 | Ref.update(routedSessionAliasesRef, (current) => { | ||
| 194 | if (!current.has(staleSessionId)) { | ||
| 195 | return current; | ||
| 196 | } | ||
| 197 | const next = new Map(current); | ||
| 198 | next.delete(staleSessionId); | ||
| 199 | return next; | ||
| 200 | }); | ||
| 201 | |||
| 202 | const clearAliasesReferencing = (sessionId: ProviderSessionId) => | ||
| 203 | Ref.update(routedSessionAliasesRef, (current) => { | ||
| 204 | let changed = false; | ||
| 205 | const next = new Map<ProviderSessionId, ProviderSessionId>(); | ||
| 206 | for (const [key, value] of current) { | ||
| 207 | if (key === sessionId || value === sessionId) { | ||
| 208 | changed = true; | ||
| 209 | continue; | ||
| 210 | } | ||
| 211 | next.set(key, value); | ||
| 212 | } | ||
| 213 | return changed ? next : current; | ||
| 214 | }); | ||
| 215 | |||
| 216 | const setAlias = (staleSessionId: ProviderSessionId, liveSessionId: ProviderSessionId) => | ||
| 217 | Ref.update(routedSessionAliasesRef, (current) => { | ||
| 218 | const existing = current.get(staleSessionId); | ||
| 219 | if (existing === liveSessionId) { | ||
| 220 | return current; | ||
| 221 | } | ||
| 222 | const next = new Map(current); | ||
| 223 | next.set(staleSessionId, liveSessionId); | ||
| 224 | return next; | ||
| 225 | }); | 145 | }); |
| 226 | 146 | ||
| 227 | const providers = yield* registry.listProviders(); | 147 | const providers = yield* registry.listProviders(); |
| ... | @@ -244,166 +164,77 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -244,166 +164,77 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 244 | ).pipe(Effect.asVoid); | 164 | ).pipe(Effect.asVoid); |
| 245 | 165 | ||
| 246 | const recoverSessionForThread = (input: { | 166 | const recoverSessionForThread = (input: { |
| 247 | readonly staleSessionId: ProviderSessionId; | 167 | readonly binding: ProviderRuntimeBinding; |
| 248 | readonly binding: ProviderSessionBinding & { readonly threadId: ThreadId }; | ||
| 249 | readonly operation: string; | 168 | readonly operation: string; |
| 250 | }) => | 169 | }) => |
| 251 | Effect.gen(function* () { | 170 | Effect.gen(function* () { |
| 252 | const adapter = yield* registry.getByProvider(input.binding.provider); | 171 | const adapter = yield* registry.getByProvider(input.binding.provider); |
| 253 | const activeSessions = yield* adapter.listSessions(); | ||
| 254 | const resumeThreadId = input.binding.providerThreadId ?? undefined; | ||
| 255 | const hasResumeCursor = | 172 | const hasResumeCursor = |
| 256 | input.binding.resumeCursor !== null && input.binding.resumeCursor !== undefined; | 173 | input.binding.resumeCursor !== null && input.binding.resumeCursor !== undefined; |
| 257 | const existing = | 174 | const hasActiveSession = yield* adapter.hasSession(input.binding.threadId); |
| 258 | resumeThreadId === undefined | 175 | if (hasActiveSession) { |
| 259 | ? undefined | 176 | const activeSessions = yield* adapter.listSessions(); |
| 260 | : activeSessions.find((session) => session.threadId === resumeThreadId); | 177 | const existing = activeSessions.find((session) => session.threadId === input.binding.threadId); |
| 261 | if (existing) { | 178 | if (existing) { |
| 262 | const existingProviderThreadId = yield* upsertSessionBinding( | 179 | yield* upsertSessionBinding(existing, input.binding.threadId); |
| 263 | existing, | 180 | return { adapter, session: existing } as const; |
| 264 | input.binding.threadId, | ||
| 265 | ); | ||
| 266 | yield* directory.upsert({ | ||
| 267 | sessionId: input.staleSessionId, | ||
| 268 | provider: existing.provider, | ||
| 269 | threadId: input.binding.threadId, | ||
| 270 | ...(existingProviderThreadId !== undefined | ||
| 271 | ? { providerThreadId: existingProviderThreadId } | ||
| 272 | : {}), | ||
| 273 | ...(existing.resumeCursor !== undefined ? { resumeCursor: existing.resumeCursor } : {}), | ||
| 274 | }); | ||
| 275 | if (existing.sessionId !== input.staleSessionId) { | ||
| 276 | yield* setAlias(input.staleSessionId, existing.sessionId); | ||
| 277 | } else { | ||
| 278 | yield* clearAliasKey(input.staleSessionId); | ||
| 279 | } | 181 | } |
| 280 | return { | ||
| 281 | adapter, | ||
| 282 | sessionId: existing.sessionId, | ||
| 283 | } as const; | ||
| 284 | } | 182 | } |
| 285 | 183 | ||
| 286 | if (!hasResumeCursor) { | 184 | if (!hasResumeCursor) { |
| 287 | return yield* toValidationError( | 185 | return yield* toValidationError( |
| 288 | input.operation, | 186 | input.operation, |
| 289 | `Cannot recover stale session '${input.staleSessionId}' because no provider resume state is persisted.`, | 187 | `Cannot recover thread '${input.binding.threadId}' because no provider resume state is persisted.`, |
| 290 | ); | 188 | ); |
| 291 | } | 189 | } |
| 292 | 190 | ||
| 293 | const persistedCwd = readPersistedCwd(input.binding.runtimePayload); | 191 | const persistedCwd = readPersistedCwd(input.binding.runtimePayload); |
| 294 | 192 | ||
| 295 | const resumed = yield* adapter.startSession({ | 193 | const resumed = yield* adapter.startSession({ |
| 194 | threadId: input.binding.threadId, | ||
| 296 | provider: input.binding.provider, | 195 | provider: input.binding.provider, |
| 297 | ...(persistedCwd ? { cwd: persistedCwd } : {}), | 196 | ...(persistedCwd ? { cwd: persistedCwd } : {}), |
| 298 | ...(resumeThreadId ? { resumeThreadId } : {}), | ||
| 299 | ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}), | 197 | ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}), |
| 300 | runtimeMode: input.binding.runtimeMode ?? "full-access", | 198 | runtimeMode: input.binding.runtimeMode ?? "full-access", |
| 301 | }); | 199 | }); |
| 302 | if (resumed.provider !== adapter.provider) { | 200 | if (resumed.provider !== adapter.provider) { |
| 303 | return yield* toValidationError( | 201 | return yield* toValidationError( |
| 304 | input.operation, | 202 | input.operation, |
| 305 | `Adapter/provider mismatch while recovering stale session '${input.staleSessionId}'. Expected '${adapter.provider}', received '${resumed.provider}'.`, | 203 | `Adapter/provider mismatch while recovering thread '${input.binding.threadId}'. Expected '${adapter.provider}', received '${resumed.provider}'.`, |
| 306 | ); | 204 | ); |
| 307 | } | 205 | } |
| 308 | 206 | ||
| 309 | const resumedProviderThreadId = yield* upsertSessionBinding( | 207 | yield* upsertSessionBinding(resumed, input.binding.threadId); |
| 310 | resumed, | 208 | return { adapter, session: resumed } as const; |
| 311 | input.binding.threadId, | ||
| 312 | ); | ||
| 313 | |||
| 314 | yield* directory.upsert({ | ||
| 315 | sessionId: input.staleSessionId, | ||
| 316 | provider: resumed.provider, | ||
| 317 | threadId: input.binding.threadId, | ||
| 318 | ...(resumedProviderThreadId !== undefined | ||
| 319 | ? { providerThreadId: resumedProviderThreadId } | ||
| 320 | : {}), | ||
| 321 | ...(resumed.resumeCursor !== undefined ? { resumeCursor: resumed.resumeCursor } : {}), | ||
| 322 | }); | ||
| 323 | |||
| 324 | if (resumed.sessionId !== input.staleSessionId) { | ||
| 325 | yield* setAlias(input.staleSessionId, resumed.sessionId); | ||
| 326 | } else { | ||
| 327 | yield* clearAliasKey(input.staleSessionId); | ||
| 328 | } | ||
| 329 | |||
| 330 | return { | ||
| 331 | adapter, | ||
| 332 | sessionId: resumed.sessionId, | ||
| 333 | } as const; | ||
| 334 | }); | 209 | }); |
| 335 | 210 | ||
| 336 | const resolveRoutableSession = (input: { | 211 | const resolveRoutableSession = (input: { |
| 337 | readonly sessionId: ProviderSessionId; | 212 | readonly threadId: ThreadId; |
| 338 | readonly operation: string; | 213 | readonly operation: string; |
| 339 | readonly allowRecovery: boolean; | 214 | readonly allowRecovery: boolean; |
| 340 | }) => | 215 | }) => |
| 341 | Effect.gen(function* () { | 216 | Effect.gen(function* () { |
| 342 | const bindingOption = yield* directory.getBinding(input.sessionId); | 217 | const bindingOption = yield* directory.getBinding(input.threadId); |
| 343 | const binding = Option.getOrUndefined(bindingOption); | 218 | const binding = Option.getOrUndefined(bindingOption); |
| 344 | if (!binding) { | 219 | if (!binding) { |
| 345 | return yield* new ProviderSessionNotFoundError({ | ||
| 346 | sessionId: input.sessionId, | ||
| 347 | }); | ||
| 348 | } | ||
| 349 | if (!binding.threadId) { | ||
| 350 | return yield* toValidationError( | 220 | return yield* toValidationError( |
| 351 | input.operation, | 221 | input.operation, |
| 352 | `Cannot route session '${input.sessionId}' because no orchestration thread id is persisted.`, | 222 | `Cannot route thread '${input.threadId}' because no persisted provider binding exists.`, |
| 353 | ); | 223 | ); |
| 354 | } | 224 | } |
| 355 | const bindingWithThreadId: ProviderSessionBinding & { | ||
| 356 | readonly threadId: ThreadId; | ||
| 357 | } = { | ||
| 358 | ...binding, | ||
| 359 | threadId: binding.threadId, | ||
| 360 | }; | ||
| 361 | const adapter = yield* registry.getByProvider(binding.provider); | 225 | const adapter = yield* registry.getByProvider(binding.provider); |
| 362 | 226 | ||
| 363 | const hasRequestedSession = yield* adapter.hasSession(input.sessionId); | 227 | const hasRequestedSession = yield* adapter.hasSession(input.threadId); |
| 364 | if (hasRequestedSession) { | 228 | if (hasRequestedSession) { |
| 365 | yield* clearAliasKey(input.sessionId); | 229 | return { adapter, threadId: input.threadId, isActive: true } as const; |
| 366 | return { | ||
| 367 | adapter, | ||
| 368 | sessionId: input.sessionId, | ||
| 369 | isActive: true, | ||
| 370 | } as const; | ||
| 371 | } | ||
| 372 | |||
| 373 | const alias = yield* Ref.get(routedSessionAliasesRef).pipe( | ||
| 374 | Effect.map((aliases) => aliases.get(input.sessionId)), | ||
| 375 | ); | ||
| 376 | if (alias) { | ||
| 377 | const aliasIsActive = yield* adapter.hasSession(alias); | ||
| 378 | if (aliasIsActive) { | ||
| 379 | return { | ||
| 380 | adapter, | ||
| 381 | sessionId: alias, | ||
| 382 | isActive: true, | ||
| 383 | } as const; | ||
| 384 | } | ||
| 385 | yield* clearAliasKey(input.sessionId); | ||
| 386 | } | 230 | } |
| 387 | 231 | ||
| 388 | if (!input.allowRecovery) { | 232 | if (!input.allowRecovery) { |
| 389 | return { | 233 | return { adapter, threadId: input.threadId, isActive: false } as const; |
| 390 | adapter, | ||
| 391 | sessionId: input.sessionId, | ||
| 392 | isActive: false, | ||
| 393 | } as const; | ||
| 394 | } | 234 | } |
| 395 | 235 | ||
| 396 | const recovered = yield* recoverSessionForThread({ | 236 | const recovered = yield* recoverSessionForThread({ binding, operation: input.operation }); |
| 397 | staleSessionId: input.sessionId, | 237 | return { adapter: recovered.adapter, threadId: input.threadId, isActive: true } as const; |
| 398 | binding: bindingWithThreadId, | ||
| 399 | operation: input.operation, | ||
| 400 | }); | ||
| 401 | |||
| 402 | return { | ||
| 403 | adapter: recovered.adapter, | ||
| 404 | sessionId: recovered.sessionId, | ||
| 405 | isActive: true, | ||
| 406 | } as const; | ||
| 407 | }); | 238 | }); |
| 408 | 239 | ||
| 409 | const startSession: ProviderServiceShape["startSession"] = (threadId, rawInput) => | 240 | const startSession: ProviderServiceShape["startSession"] = (threadId, rawInput) => |
| ... | @@ -416,6 +247,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -416,6 +247,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 416 | 247 | ||
| 417 | const input = { | 248 | const input = { |
| 418 | ...parsed, | 249 | ...parsed, |
| 250 | threadId, | ||
| 419 | provider: parsed.provider ?? "codex", | 251 | provider: parsed.provider ?? "codex", |
| 420 | }; | 252 | }; |
| 421 | const adapter = yield* registry.getByProvider(input.provider); | 253 | const adapter = yield* registry.getByProvider(input.provider); |
| ... | @@ -452,28 +284,14 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -452,28 +284,14 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 452 | ); | 284 | ); |
| 453 | } | 285 | } |
| 454 | const routed = yield* resolveRoutableSession({ | 286 | const routed = yield* resolveRoutableSession({ |
| 455 | sessionId: input.sessionId, | 287 | threadId: input.threadId, |
| 456 | operation: "ProviderService.sendTurn", | 288 | operation: "ProviderService.sendTurn", |
| 457 | allowRecovery: true, | 289 | allowRecovery: true, |
| 458 | }); | 290 | }); |
| 459 | const turn = yield* routed.adapter.sendTurn({ | 291 | const turn = yield* routed.adapter.sendTurn(input); |
| 460 | ...input, | ||
| 461 | sessionId: routed.sessionId, | ||
| 462 | }); | ||
| 463 | const threadId = yield* directory | ||
| 464 | .getThreadId(input.sessionId) | ||
| 465 | .pipe(Effect.map(Option.getOrUndefined)); | ||
| 466 | if (!threadId) { | ||
| 467 | return yield* toValidationError( | ||
| 468 | "ProviderService.sendTurn", | ||
| 469 | `No thread id is tracked for provider session '${input.sessionId}'.`, | ||
| 470 | ); | ||
| 471 | } | ||
| 472 | yield* directory.upsert({ | 292 | yield* directory.upsert({ |
| 473 | sessionId: input.sessionId, | 293 | threadId: input.threadId, |
| 474 | provider: routed.adapter.provider, | 294 | provider: routed.adapter.provider, |
| 475 | threadId, | ||
| 476 | ...(turn.threadId !== undefined ? { providerThreadId: turn.threadId } : {}), | ||
| 477 | status: "running", | 295 | status: "running", |
| 478 | ...(turn.resumeCursor !== undefined ? { resumeCursor: turn.resumeCursor } : {}), | 296 | ...(turn.resumeCursor !== undefined ? { resumeCursor: turn.resumeCursor } : {}), |
| 479 | runtimePayload: { | 297 | runtimePayload: { |
| ... | @@ -493,11 +311,11 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -493,11 +311,11 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 493 | payload: rawInput, | 311 | payload: rawInput, |
| 494 | }); | 312 | }); |
| 495 | const routed = yield* resolveRoutableSession({ | 313 | const routed = yield* resolveRoutableSession({ |
| 496 | sessionId: input.sessionId, | 314 | threadId: input.threadId, |
| 497 | operation: "ProviderService.interruptTurn", | 315 | operation: "ProviderService.interruptTurn", |
| 498 | allowRecovery: true, | 316 | allowRecovery: true, |
| 499 | }); | 317 | }); |
| 500 | yield* routed.adapter.interruptTurn(routed.sessionId, input.turnId); | 318 | yield* routed.adapter.interruptTurn(routed.threadId, input.turnId); |
| 501 | }); | 319 | }); |
| 502 | 320 | ||
| 503 | const respondToRequest: ProviderServiceShape["respondToRequest"] = (rawInput) => | 321 | const respondToRequest: ProviderServiceShape["respondToRequest"] = (rawInput) => |
| ... | @@ -508,11 +326,11 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -508,11 +326,11 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 508 | payload: rawInput, | 326 | payload: rawInput, |
| 509 | }); | 327 | }); |
| 510 | const routed = yield* resolveRoutableSession({ | 328 | const routed = yield* resolveRoutableSession({ |
| 511 | sessionId: input.sessionId, | 329 | threadId: input.threadId, |
| 512 | operation: "ProviderService.respondToRequest", | 330 | operation: "ProviderService.respondToRequest", |
| 513 | allowRecovery: true, | 331 | allowRecovery: true, |
| 514 | }); | 332 | }); |
| 515 | yield* routed.adapter.respondToRequest(routed.sessionId, input.requestId, input.decision); | 333 | yield* routed.adapter.respondToRequest(routed.threadId, input.requestId, input.decision); |
| 516 | }); | 334 | }); |
| 517 | 335 | ||
| 518 | const stopSession: ProviderServiceShape["stopSession"] = (rawInput) => | 336 | const stopSession: ProviderServiceShape["stopSession"] = (rawInput) => |
| ... | @@ -523,19 +341,14 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -523,19 +341,14 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 523 | payload: rawInput, | 341 | payload: rawInput, |
| 524 | }); | 342 | }); |
| 525 | const routed = yield* resolveRoutableSession({ | 343 | const routed = yield* resolveRoutableSession({ |
| 526 | sessionId: input.sessionId, | 344 | threadId: input.threadId, |
| 527 | operation: "ProviderService.stopSession", | 345 | operation: "ProviderService.stopSession", |
| 528 | allowRecovery: false, | 346 | allowRecovery: false, |
| 529 | }); | 347 | }); |
| 530 | if (routed.isActive) { | 348 | if (routed.isActive) { |
| 531 | yield* routed.adapter.stopSession(routed.sessionId); | 349 | yield* routed.adapter.stopSession(routed.threadId); |
| 532 | } | ||
| 533 | if (routed.sessionId !== input.sessionId) { | ||
| 534 | yield* directory.remove(routed.sessionId); | ||
| 535 | yield* clearAliasesReferencing(routed.sessionId); | ||
| 536 | } | 350 | } |
| 537 | yield* directory.remove(input.sessionId); | 351 | yield* directory.remove(input.threadId); |
| 538 | yield* clearAliasesReferencing(input.sessionId); | ||
| 539 | }); | 352 | }); |
| 540 | 353 | ||
| 541 | const listSessions: ProviderServiceShape["listSessions"] = () => | 354 | const listSessions: ProviderServiceShape["listSessions"] = () => |
| ... | @@ -543,39 +356,36 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -543,39 +356,36 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 543 | const sessionsByProvider = yield* Effect.forEach(adapters, (adapter) => adapter.listSessions()); | 356 | const sessionsByProvider = yield* Effect.forEach(adapters, (adapter) => adapter.listSessions()); |
| 544 | const activeSessions = sessionsByProvider.flatMap((sessions) => sessions); | 357 | const activeSessions = sessionsByProvider.flatMap((sessions) => sessions); |
| 545 | const persistedBindings = yield* directory | 358 | const persistedBindings = yield* directory |
| 546 | .listSessionIds() | 359 | .listThreadIds() |
| 547 | .pipe( | 360 | .pipe( |
| 548 | Effect.flatMap((sessionIds) => | 361 | Effect.flatMap((threadIds) => |
| 549 | Effect.forEach( | 362 | Effect.forEach( |
| 550 | sessionIds, | 363 | threadIds, |
| 551 | (sessionId) => | 364 | (threadId) => |
| 552 | directory.getBinding(sessionId).pipe( | 365 | directory.getBinding(threadId).pipe( |
| 553 | Effect.orElseSucceed(() => Option.none<ProviderSessionBinding>()), | 366 | Effect.orElseSucceed(() => Option.none<ProviderRuntimeBinding>()), |
| 554 | ), | 367 | ), |
| 555 | { concurrency: "unbounded" }, | 368 | { concurrency: "unbounded" }, |
| 556 | ), | 369 | ), |
| 557 | ), | 370 | ), |
| 558 | Effect.orElseSucceed(() => [] as Array<Option.Option<ProviderSessionBinding>>), | 371 | Effect.orElseSucceed(() => [] as Array<Option.Option<ProviderRuntimeBinding>>), |
| 559 | ); | 372 | ); |
| 560 | const bindingsBySessionId = new Map<ProviderSessionId, ProviderSessionBinding>(); | 373 | const bindingsByThreadId = new Map<ThreadId, ProviderRuntimeBinding>(); |
| 561 | for (const bindingOption of persistedBindings) { | 374 | for (const bindingOption of persistedBindings) { |
| 562 | const binding = Option.getOrUndefined(bindingOption); | 375 | const binding = Option.getOrUndefined(bindingOption); |
| 563 | if (binding) { | 376 | if (binding) { |
| 564 | bindingsBySessionId.set(binding.sessionId, binding); | 377 | bindingsByThreadId.set(binding.threadId, binding); |
| 565 | } | 378 | } |
| 566 | } | 379 | } |
| 567 | 380 | ||
| 568 | return activeSessions.map((session) => { | 381 | return activeSessions.map((session) => { |
| 569 | const binding = bindingsBySessionId.get(session.sessionId); | 382 | const binding = bindingsByThreadId.get(session.threadId); |
| 570 | if (!binding) { | 383 | if (!binding) { |
| 571 | return session; | 384 | return session; |
| 572 | } | 385 | } |
| 573 | 386 | ||
| 574 | return { | 387 | return { |
| 575 | ...session, | 388 | ...session, |
| 576 | ...(session.threadId === undefined && binding.providerThreadId | ||
| 577 | ? { threadId: binding.providerThreadId } | ||
| 578 | : {}), | ||
| 579 | ...(session.resumeCursor === undefined && binding.resumeCursor !== undefined | 389 | ...(session.resumeCursor === undefined && binding.resumeCursor !== undefined |
| 580 | ? { resumeCursor: binding.resumeCursor } | 390 | ? { resumeCursor: binding.resumeCursor } |
| 581 | : {}), | 391 | : {}), |
| ... | @@ -598,22 +408,22 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -598,22 +408,22 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 598 | return; | 408 | return; |
| 599 | } | 409 | } |
| 600 | const routed = yield* resolveRoutableSession({ | 410 | const routed = yield* resolveRoutableSession({ |
| 601 | sessionId: input.sessionId, | 411 | threadId: input.threadId, |
| 602 | operation: "ProviderService.rollbackConversation", | 412 | operation: "ProviderService.rollbackConversation", |
| 603 | allowRecovery: true, | 413 | allowRecovery: true, |
| 604 | }); | 414 | }); |
| 605 | yield* routed.adapter.rollbackThread(routed.sessionId, input.numTurns); | 415 | yield* routed.adapter.rollbackThread(routed.threadId, input.numTurns); |
| 606 | }); | 416 | }); |
| 607 | 417 | ||
| 608 | const stopAll: ProviderServiceShape["stopAll"] = () => | 418 | const stopAll: ProviderServiceShape["stopAll"] = () => |
| 609 | Effect.gen(function* () { | 419 | Effect.gen(function* () { |
| 610 | const sessionIds = yield* directory.listSessionIds(); | 420 | const threadIds = yield* directory.listThreadIds(); |
| 611 | yield* Effect.forEach(adapters, (adapter) => adapter.stopAll()).pipe(Effect.asVoid); | 421 | yield* Effect.forEach(adapters, (adapter) => adapter.stopAll()).pipe(Effect.asVoid); |
| 612 | yield* Effect.forEach(sessionIds, (sessionId) => | 422 | yield* Effect.forEach(threadIds, (threadId) => |
| 613 | directory.getProvider(sessionId).pipe( | 423 | directory.getProvider(threadId).pipe( |
| 614 | Effect.flatMap((provider) => | 424 | Effect.flatMap((provider) => |
| 615 | directory.upsert({ | 425 | directory.upsert({ |
| 616 | sessionId, | 426 | threadId, |
| 617 | provider, | 427 | provider, |
| 618 | status: "stopped", | 428 | status: "stopped", |
| 619 | runtimePayload: { | 429 | runtimePayload: { |
| ... | @@ -625,9 +435,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -625,9 +435,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 625 | ), | 435 | ), |
| 626 | ), | 436 | ), |
| 627 | ).pipe(Effect.asVoid); | 437 | ).pipe(Effect.asVoid); |
| 628 | // Keep persisted session bindings so stale sessions can be resumed after | ||
| 629 | // process restart via providerThreadId. | ||
| 630 | yield* Ref.set(routedSessionAliasesRef, new Map()); | ||
| 631 | }); | 438 | }); |
| 632 | 439 | ||
| 633 | return { | 440 | return { |
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts+44-91| ... | @@ -3,7 +3,7 @@ import os from "node:os"; | ... | @@ -3,7 +3,7 @@ import os from "node:os"; |
| 3 | import path from "node:path"; | 3 | import path from "node:path"; |
| 4 | 4 | ||
| 5 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 5 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 6 | import { ProviderSessionId, ProviderThreadId, ThreadId } from "@t3tools/contracts"; | 6 | import { ThreadId } from "@t3tools/contracts"; |
| 7 | import { it, assert } from "@effect/vitest"; | 7 | import { it, assert } from "@effect/vitest"; |
| 8 | import { assertFailure, assertSome } from "@effect/vitest/utils"; | 8 | import { assertFailure, assertSome } from "@effect/vitest/utils"; |
| 9 | import { Effect, Layer, Option } from "effect"; | 9 | import { Effect, Layer, Option } from "effect"; |
| ... | @@ -15,7 +15,7 @@ import { | ... | @@ -15,7 +15,7 @@ import { |
| 15 | } from "../../persistence/Layers/Sqlite.ts"; | 15 | } from "../../persistence/Layers/Sqlite.ts"; |
| 16 | import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts"; | 16 | import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts"; |
| 17 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | 17 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; |
| 18 | import { ProviderSessionNotFoundError, ProviderValidationError } from "../Errors.ts"; | 18 | import { ProviderSessionDirectoryPersistenceError } from "../Errors.ts"; |
| 19 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; | 19 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; |
| 20 | import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; | 20 | import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; |
| 21 | 21 | ||
| ... | @@ -31,74 +31,59 @@ function makeDirectoryLayer<E, R>(persistenceLayer: Layer.Layer<SqlClient.SqlCli | ... | @@ -31,74 +31,59 @@ function makeDirectoryLayer<E, R>(persistenceLayer: Layer.Layer<SqlClient.SqlCli |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryLive", (it) => { | 33 | it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryLive", (it) => { |
| 34 | it("upserts, reads, and removes session bindings", () => | 34 | it("upserts, reads, and removes thread bindings", () => |
| 35 | Effect.gen(function* () { | 35 | Effect.gen(function* () { |
| 36 | const directory = yield* ProviderSessionDirectory; | 36 | const directory = yield* ProviderSessionDirectory; |
| 37 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | 37 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; |
| 38 | 38 | ||
| 39 | const sessionId = ProviderSessionId.makeUnsafe("sess-1"); | ||
| 40 | const initialThreadId = ThreadId.makeUnsafe("thread-1"); | 39 | const initialThreadId = ThreadId.makeUnsafe("thread-1"); |
| 41 | 40 | ||
| 42 | yield* directory.upsert({ | 41 | yield* directory.upsert({ |
| 43 | sessionId, | ||
| 44 | provider: "codex", | 42 | provider: "codex", |
| 45 | threadId: initialThreadId, | 43 | threadId: initialThreadId, |
| 46 | }); | 44 | }); |
| 47 | 45 | ||
| 48 | const provider = yield* directory.getProvider(sessionId); | 46 | const provider = yield* directory.getProvider(initialThreadId); |
| 49 | assert.equal(provider, "codex"); | 47 | assert.equal(provider, "codex"); |
| 50 | const resolvedThreadId = yield* directory.getThreadId(sessionId); | 48 | const resolvedBinding = yield* directory.getBinding(initialThreadId); |
| 51 | assertSome(resolvedThreadId, initialThreadId); | 49 | assertSome(resolvedBinding, { |
| 50 | threadId: initialThreadId, | ||
| 51 | provider: "codex", | ||
| 52 | }); | ||
| 53 | if (Option.isSome(resolvedBinding)) { | ||
| 54 | assert.equal(resolvedBinding.value.threadId, initialThreadId); | ||
| 55 | } | ||
| 52 | 56 | ||
| 53 | const nextThreadId = ThreadId.makeUnsafe("thread-2"); | 57 | const nextThreadId = ThreadId.makeUnsafe("thread-2"); |
| 54 | 58 | ||
| 55 | yield* directory.upsert({ | 59 | yield* directory.upsert({ |
| 56 | sessionId: sessionId, | ||
| 57 | provider: "codex", | 60 | provider: "codex", |
| 58 | threadId: nextThreadId, | 61 | threadId: nextThreadId, |
| 59 | }); | 62 | }); |
| 60 | const updatedThreadId = yield* directory.getThreadId(sessionId); | 63 | const updatedBinding = yield* directory.getBinding(nextThreadId); |
| 61 | assertSome(updatedThreadId, nextThreadId); | ||
| 62 | const updatedBinding = yield* directory.getBinding(sessionId); | ||
| 63 | assert.equal(Option.isSome(updatedBinding), true); | 64 | assert.equal(Option.isSome(updatedBinding), true); |
| 64 | if (Option.isSome(updatedBinding)) { | 65 | if (Option.isSome(updatedBinding)) { |
| 65 | assert.equal(updatedBinding.value.threadId, nextThreadId); | 66 | assert.equal(updatedBinding.value.threadId, nextThreadId); |
| 66 | assert.equal(updatedBinding.value.providerThreadId, null); | ||
| 67 | } | 67 | } |
| 68 | 68 | ||
| 69 | const runtime = yield* runtimeRepository.getBySessionId({ | 69 | const runtime = yield* runtimeRepository.getByThreadId({ threadId: nextThreadId }); |
| 70 | providerSessionId: sessionId, | ||
| 71 | }); | ||
| 72 | assert.equal(Option.isSome(runtime), true); | 70 | assert.equal(Option.isSome(runtime), true); |
| 73 | if (Option.isSome(runtime)) { | 71 | if (Option.isSome(runtime)) { |
| 74 | assert.equal(runtime.value.threadId, nextThreadId); | 72 | assert.equal(runtime.value.threadId, nextThreadId); |
| 75 | assert.equal(runtime.value.providerThreadId, null); | ||
| 76 | assert.equal(runtime.value.status, "running"); | 73 | assert.equal(runtime.value.status, "running"); |
| 77 | assert.equal(runtime.value.adapterKey, "codex"); | 74 | assert.equal(runtime.value.providerName, "codex"); |
| 78 | } | 75 | } |
| 79 | 76 | ||
| 80 | const sessionIds = yield* directory.listSessionIds(); | 77 | const threadIds = yield* directory.listThreadIds(); |
| 81 | assert.deepEqual(sessionIds, [sessionId]); | 78 | assert.deepEqual(threadIds, [nextThreadId]); |
| 82 | 79 | ||
| 83 | yield* directory.remove(sessionId); | 80 | yield* directory.remove(nextThreadId); |
| 84 | const missingProvider = yield* directory.getProvider(sessionId).pipe(Effect.result); | 81 | const missingProvider = yield* directory.getProvider(nextThreadId).pipe(Effect.result); |
| 85 | assertFailure(missingProvider, new ProviderSessionNotFoundError({ sessionId: "sess-1" })); | ||
| 86 | })); | ||
| 87 | |||
| 88 | it("fails upsert when thread id is unavailable", () => | ||
| 89 | Effect.gen(function* () { | ||
| 90 | const directory = yield* ProviderSessionDirectory; | ||
| 91 | const result = yield* Effect.result( | ||
| 92 | directory.upsert({ | ||
| 93 | sessionId: ProviderSessionId.makeUnsafe("sess-no-thread"), | ||
| 94 | provider: "codex", | ||
| 95 | }), | ||
| 96 | ); | ||
| 97 | assertFailure( | 82 | assertFailure( |
| 98 | result, | 83 | missingProvider, |
| 99 | new ProviderValidationError({ | 84 | new ProviderSessionDirectoryPersistenceError({ |
| 100 | operation: "ProviderSessionDirectory.upsert", | 85 | operation: "ProviderSessionDirectory.getProvider", |
| 101 | issue: "threadId must be a non-empty string.", | 86 | detail: `No persisted provider binding found for thread '${nextThreadId}'.`, |
| 102 | }), | 87 | }), |
| 103 | ); | 88 | ); |
| 104 | })); | 89 | })); |
| ... | @@ -108,15 +93,11 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -108,15 +93,11 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 108 | const directory = yield* ProviderSessionDirectory; | 93 | const directory = yield* ProviderSessionDirectory; |
| 109 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | 94 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; |
| 110 | 95 | ||
| 111 | const sessionId = ProviderSessionId.makeUnsafe("sess-runtime"); | ||
| 112 | const threadId = ThreadId.makeUnsafe("thread-runtime"); | 96 | const threadId = ThreadId.makeUnsafe("thread-runtime"); |
| 113 | const providerThreadId = ProviderThreadId.makeUnsafe("provider-thread-runtime"); | ||
| 114 | 97 | ||
| 115 | yield* directory.upsert({ | 98 | yield* directory.upsert({ |
| 116 | sessionId, | ||
| 117 | provider: "codex", | 99 | provider: "codex", |
| 118 | threadId, | 100 | threadId, |
| 119 | providerThreadId, | ||
| 120 | status: "starting", | 101 | status: "starting", |
| 121 | resumeCursor: { | 102 | resumeCursor: { |
| 122 | threadId: "provider-thread-runtime", | 103 | threadId: "provider-thread-runtime", |
| ... | @@ -128,24 +109,21 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -128,24 +109,21 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 128 | }); | 109 | }); |
| 129 | 110 | ||
| 130 | yield* directory.upsert({ | 111 | yield* directory.upsert({ |
| 131 | sessionId, | ||
| 132 | provider: "codex", | 112 | provider: "codex", |
| 113 | threadId, | ||
| 133 | status: "running", | 114 | status: "running", |
| 134 | runtimePayload: { | 115 | runtimePayload: { |
| 135 | activeTurnId: "turn-1", | 116 | activeTurnId: "turn-1", |
| 136 | }, | 117 | }, |
| 137 | }); | 118 | }); |
| 138 | 119 | ||
| 139 | const runtime = yield* runtimeRepository.getBySessionId({ | 120 | const runtime = yield* runtimeRepository.getByThreadId({ threadId }); |
| 140 | providerSessionId: sessionId, | ||
| 141 | }); | ||
| 142 | assert.equal(Option.isSome(runtime), true); | 121 | assert.equal(Option.isSome(runtime), true); |
| 143 | if (Option.isSome(runtime)) { | 122 | if (Option.isSome(runtime)) { |
| 144 | assert.equal(runtime.value.threadId, threadId); | 123 | assert.equal(runtime.value.threadId, threadId); |
| 145 | assert.equal(runtime.value.providerThreadId, providerThreadId); | ||
| 146 | assert.equal(runtime.value.status, "running"); | 124 | assert.equal(runtime.value.status, "running"); |
| 147 | assert.deepEqual(runtime.value.resumeCursor, { | 125 | assert.deepEqual(runtime.value.resumeCursor, { |
| 148 | threadId: providerThreadId, | 126 | threadId: "provider-thread-runtime", |
| 149 | }); | 127 | }); |
| 150 | assert.deepEqual(runtime.value.runtimePayload, { | 128 | assert.deepEqual(runtime.value.runtimePayload, { |
| 151 | cwd: "/tmp/project", | 129 | cwd: "/tmp/project", |
| ... | @@ -155,52 +133,17 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -155,52 +133,17 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 155 | } | 133 | } |
| 156 | })); | 134 | })); |
| 157 | 135 | ||
| 158 | it("clears providerThreadId when explicitly set to null", () => | ||
| 159 | Effect.gen(function* () { | ||
| 160 | const directory = yield* ProviderSessionDirectory; | ||
| 161 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | ||
| 162 | |||
| 163 | const sessionId = ProviderSessionId.makeUnsafe("sess-clear-provider-thread-id"); | ||
| 164 | const threadId = ThreadId.makeUnsafe("thread-clear-provider-thread-id"); | ||
| 165 | const providerThreadId = ProviderThreadId.makeUnsafe("provider-thread-to-clear"); | ||
| 166 | |||
| 167 | yield* directory.upsert({ | ||
| 168 | sessionId, | ||
| 169 | provider: "codex", | ||
| 170 | threadId, | ||
| 171 | adapterKey: "custom-adapter", | ||
| 172 | providerThreadId, | ||
| 173 | }); | ||
| 174 | |||
| 175 | yield* directory.upsert({ | ||
| 176 | sessionId, | ||
| 177 | provider: "codex", | ||
| 178 | providerThreadId: null, | ||
| 179 | }); | ||
| 180 | |||
| 181 | const runtime = yield* runtimeRepository.getBySessionId({ | ||
| 182 | providerSessionId: sessionId, | ||
| 183 | }); | ||
| 184 | assert.equal(Option.isSome(runtime), true); | ||
| 185 | if (Option.isSome(runtime)) { | ||
| 186 | assert.equal(runtime.value.providerThreadId, null); | ||
| 187 | assert.equal(runtime.value.adapterKey, "custom-adapter"); | ||
| 188 | } | ||
| 189 | })); | ||
| 190 | |||
| 191 | it("rehydrates persisted mappings across layer restart", () => | 136 | it("rehydrates persisted mappings across layer restart", () => |
| 192 | Effect.gen(function* () { | 137 | Effect.gen(function* () { |
| 193 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-directory-")); | 138 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-directory-")); |
| 194 | const dbPath = path.join(tempDir, "orchestration.sqlite"); | 139 | const dbPath = path.join(tempDir, "orchestration.sqlite"); |
| 195 | const directoryLayer = makeDirectoryLayer(makeSqlitePersistenceLive(dbPath)); | 140 | const directoryLayer = makeDirectoryLayer(makeSqlitePersistenceLive(dbPath)); |
| 196 | 141 | ||
| 197 | const sessionId = ProviderSessionId.makeUnsafe("sess-restart"); | ||
| 198 | const threadId = ThreadId.makeUnsafe("thread-restart"); | 142 | const threadId = ThreadId.makeUnsafe("thread-restart"); |
| 199 | 143 | ||
| 200 | yield* Effect.gen(function* () { | 144 | yield* Effect.gen(function* () { |
| 201 | const directory = yield* ProviderSessionDirectory; | 145 | const directory = yield* ProviderSessionDirectory; |
| 202 | yield* directory.upsert({ | 146 | yield* directory.upsert({ |
| 203 | sessionId, | ||
| 204 | provider: "codex", | 147 | provider: "codex", |
| 205 | threadId, | 148 | threadId, |
| 206 | }); | 149 | }); |
| ... | @@ -209,11 +152,17 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -209,11 +152,17 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 209 | yield* Effect.gen(function* () { | 152 | yield* Effect.gen(function* () { |
| 210 | const directory = yield* ProviderSessionDirectory; | 153 | const directory = yield* ProviderSessionDirectory; |
| 211 | const sql = yield* SqlClient.SqlClient; | 154 | const sql = yield* SqlClient.SqlClient; |
| 212 | const provider = yield* directory.getProvider(sessionId); | 155 | const provider = yield* directory.getProvider(threadId); |
| 213 | assert.equal(provider, "codex"); | 156 | assert.equal(provider, "codex"); |
| 214 | 157 | ||
| 215 | const resolvedThreadId = yield* directory.getThreadId(sessionId); | 158 | const resolvedBinding = yield* directory.getBinding(threadId); |
| 216 | assertSome(resolvedThreadId, threadId); | 159 | assertSome(resolvedBinding, { |
| 160 | threadId, | ||
| 161 | provider: "codex", | ||
| 162 | }); | ||
| 163 | if (Option.isSome(resolvedBinding)) { | ||
| 164 | assert.equal(resolvedBinding.value.threadId, threadId); | ||
| 165 | } | ||
| 217 | 166 | ||
| 218 | const legacyTableRows = yield* sql<{ readonly name: string }>` | 167 | const legacyTableRows = yield* sql<{ readonly name: string }>` |
| 219 | SELECT name | 168 | SELECT name |
| ... | @@ -229,18 +178,22 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -229,18 +178,22 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 229 | it("accepts cursor provider bindings", () => | 178 | it("accepts cursor provider bindings", () => |
| 230 | Effect.gen(function* () { | 179 | Effect.gen(function* () { |
| 231 | const directory = yield* ProviderSessionDirectory; | 180 | const directory = yield* ProviderSessionDirectory; |
| 232 | const sessionId = ProviderSessionId.makeUnsafe("sess-cursor"); | ||
| 233 | const threadId = ThreadId.makeUnsafe("thread-cursor"); | 181 | const threadId = ThreadId.makeUnsafe("thread-cursor"); |
| 234 | 182 | ||
| 235 | yield* directory.upsert({ | 183 | yield* directory.upsert({ |
| 236 | sessionId, | ||
| 237 | provider: "cursor", | 184 | provider: "cursor", |
| 238 | threadId, | 185 | threadId, |
| 239 | }); | 186 | }); |
| 240 | 187 | ||
| 241 | const provider = yield* directory.getProvider(sessionId); | 188 | const provider = yield* directory.getProvider(threadId); |
| 242 | assert.equal(provider, "cursor"); | 189 | assert.equal(provider, "cursor"); |
| 243 | const resolvedThreadId = yield* directory.getThreadId(sessionId); | 190 | const resolvedBinding = yield* directory.getBinding(threadId); |
| 244 | assertSome(resolvedThreadId, threadId); | 191 | assertSome(resolvedBinding, { |
| 192 | threadId, | ||
| 193 | provider: "cursor", | ||
| 194 | }); | ||
| 195 | if (Option.isSome(resolvedBinding)) { | ||
| 196 | assert.equal(resolvedBinding.value.threadId, threadId); | ||
| 197 | } | ||
| 245 | })); | 198 | })); |
| 246 | }); | 199 | }); |
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+27-64| ... | @@ -1,15 +1,14 @@ | ... | @@ -1,15 +1,14 @@ |
| 1 | import { ProviderSessionId, type ProviderKind } from "@t3tools/contracts"; | 1 | import { type ProviderKind, type ThreadId } from "@t3tools/contracts"; |
| 2 | import { Effect, Layer, Option } from "effect"; | 2 | import { Effect, Layer, Option } from "effect"; |
| 3 | 3 | ||
| 4 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | 4 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; |
| 5 | import { | 5 | import { |
| 6 | ProviderSessionDirectoryPersistenceError, | 6 | ProviderSessionDirectoryPersistenceError, |
| 7 | ProviderSessionNotFoundError, | ||
| 8 | ProviderValidationError, | 7 | ProviderValidationError, |
| 9 | } from "../Errors.ts"; | 8 | } from "../Errors.ts"; |
| 10 | import { | 9 | import { |
| 11 | ProviderSessionDirectory, | 10 | ProviderSessionDirectory, |
| 12 | type ProviderSessionBinding, | 11 | type ProviderRuntimeBinding, |
| 13 | type ProviderSessionDirectoryShape, | 12 | type ProviderSessionDirectoryShape, |
| 14 | } from "../Services/ProviderSessionDirectory.ts"; | 13 | } from "../Services/ProviderSessionDirectory.ts"; |
| 15 | 14 | ||
| ... | @@ -49,10 +48,7 @@ function mergeRuntimePayload( | ... | @@ -49,10 +48,7 @@ function mergeRuntimePayload( |
| 49 | return existing ?? null; | 48 | return existing ?? null; |
| 50 | } | 49 | } |
| 51 | if (isRecord(existing) && isRecord(next)) { | 50 | if (isRecord(existing) && isRecord(next)) { |
| 52 | return { | 51 | return { ...existing, ...next }; |
| 53 | ...existing, | ||
| 54 | ...next, | ||
| 55 | }; | ||
| 56 | } | 52 | } |
| 57 | return next; | 53 | return next; |
| 58 | } | 54 | } |
| ... | @@ -60,21 +56,19 @@ function mergeRuntimePayload( | ... | @@ -60,21 +56,19 @@ function mergeRuntimePayload( |
| 60 | const makeProviderSessionDirectory = Effect.gen(function* () { | 56 | const makeProviderSessionDirectory = Effect.gen(function* () { |
| 61 | const repository = yield* ProviderSessionRuntimeRepository; | 57 | const repository = yield* ProviderSessionRuntimeRepository; |
| 62 | 58 | ||
| 63 | const getBinding = (sessionId: ProviderSessionId) => | 59 | const getBinding = (threadId: ThreadId) => |
| 64 | repository.getBySessionId({ providerSessionId: sessionId }).pipe( | 60 | repository.getByThreadId({ threadId }).pipe( |
| 65 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.getBinding:getBySessionId")), | 61 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.getBinding:getByThreadId")), |
| 66 | Effect.flatMap((runtime) => | 62 | Effect.flatMap((runtime) => |
| 67 | Option.match(runtime, { | 63 | Option.match(runtime, { |
| 68 | onNone: () => Effect.succeed(Option.none<ProviderSessionBinding>()), | 64 | onNone: () => Effect.succeed(Option.none<ProviderRuntimeBinding>()), |
| 69 | onSome: (value) => | 65 | onSome: (value) => |
| 70 | decodeProviderKind(value.providerName, "ProviderSessionDirectory.getBinding").pipe( | 66 | decodeProviderKind(value.providerName, "ProviderSessionDirectory.getBinding").pipe( |
| 71 | Effect.map((provider) => | 67 | Effect.map((provider) => |
| 72 | Option.some({ | 68 | Option.some({ |
| 73 | sessionId: value.providerSessionId, | ||
| 74 | provider, | ||
| 75 | threadId: value.threadId, | 69 | threadId: value.threadId, |
| 70 | provider, | ||
| 76 | adapterKey: value.adapterKey, | 71 | adapterKey: value.adapterKey, |
| 77 | providerThreadId: value.providerThreadId, | ||
| 78 | runtimeMode: value.runtimeMode, | 72 | runtimeMode: value.runtimeMode, |
| 79 | status: value.status, | 73 | status: value.status, |
| 80 | resumeCursor: value.resumeCursor, | 74 | resumeCursor: value.resumeCursor, |
| ... | @@ -88,10 +82,8 @@ const makeProviderSessionDirectory = Effect.gen(function* () { | ... | @@ -88,10 +82,8 @@ const makeProviderSessionDirectory = Effect.gen(function* () { |
| 88 | 82 | ||
| 89 | const upsert: ProviderSessionDirectoryShape["upsert"] = Effect.fn(function* (binding) { | 83 | const upsert: ProviderSessionDirectoryShape["upsert"] = Effect.fn(function* (binding) { |
| 90 | const existing = yield* repository | 84 | const existing = yield* repository |
| 91 | .getBySessionId({ | 85 | .getByThreadId({ threadId: binding.threadId }) |
| 92 | providerSessionId: binding.sessionId, | 86 | .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:getByThreadId"))); |
| 93 | }) | ||
| 94 | .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:getBySessionId"))); | ||
| 95 | 87 | ||
| 96 | const existingRuntime = Option.getOrUndefined(existing); | 88 | const existingRuntime = Option.getOrUndefined(existing); |
| 97 | const resolvedThreadId = binding.threadId ?? existingRuntime?.threadId; | 89 | const resolvedThreadId = binding.threadId ?? existingRuntime?.threadId; |
| ... | @@ -105,14 +97,9 @@ const makeProviderSessionDirectory = Effect.gen(function* () { | ... | @@ -105,14 +97,9 @@ const makeProviderSessionDirectory = Effect.gen(function* () { |
| 105 | const now = new Date().toISOString(); | 97 | const now = new Date().toISOString(); |
| 106 | yield* repository | 98 | yield* repository |
| 107 | .upsert({ | 99 | .upsert({ |
| 108 | providerSessionId: binding.sessionId, | ||
| 109 | threadId: resolvedThreadId, | 100 | threadId: resolvedThreadId, |
| 110 | providerName: binding.provider, | 101 | providerName: binding.provider, |
| 111 | adapterKey: binding.adapterKey ?? existingRuntime?.adapterKey ?? binding.provider, | 102 | adapterKey: binding.adapterKey ?? existingRuntime?.adapterKey ?? binding.provider, |
| 112 | providerThreadId: | ||
| 113 | binding.providerThreadId !== undefined | ||
| 114 | ? binding.providerThreadId | ||
| 115 | : (existingRuntime?.providerThreadId ?? null), | ||
| 116 | runtimeMode: binding.runtimeMode ?? existingRuntime?.runtimeMode ?? "full-access", | 103 | runtimeMode: binding.runtimeMode ?? existingRuntime?.runtimeMode ?? "full-access", |
| 117 | status: binding.status ?? existingRuntime?.status ?? "running", | 104 | status: binding.status ?? existingRuntime?.status ?? "running", |
| 118 | lastSeenAt: now, | 105 | lastSeenAt: now, |
| ... | @@ -128,63 +115,39 @@ const makeProviderSessionDirectory = Effect.gen(function* () { | ... | @@ -128,63 +115,39 @@ const makeProviderSessionDirectory = Effect.gen(function* () { |
| 128 | .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:upsert"))); | 115 | .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:upsert"))); |
| 129 | }); | 116 | }); |
| 130 | 117 | ||
| 131 | const getProvider: ProviderSessionDirectoryShape["getProvider"] = (sessionId) => | 118 | const getProvider: ProviderSessionDirectoryShape["getProvider"] = (threadId) => |
| 132 | getBinding(sessionId).pipe( | 119 | getBinding(threadId).pipe( |
| 133 | Effect.flatMap((binding) => | 120 | Effect.flatMap((binding) => |
| 134 | Option.match(binding, { | 121 | Option.match(binding, { |
| 135 | onSome: (value) => Effect.succeed(value.provider), | 122 | onSome: (value) => Effect.succeed(value.provider), |
| 136 | onNone: () => Effect.fail(new ProviderSessionNotFoundError({ sessionId })), | 123 | onNone: () => |
| 137 | }), | 124 | Effect.fail( |
| 138 | ), | 125 | new ProviderSessionDirectoryPersistenceError({ |
| 139 | ); | 126 | operation: "ProviderSessionDirectory.getProvider", |
| 140 | 127 | detail: `No persisted provider binding found for thread '${threadId}'.`, | |
| 141 | const getBindingBySessionId: ProviderSessionDirectoryShape["getBinding"] = (sessionId) => | 128 | }), |
| 142 | getBinding(sessionId); | 129 | ), |
| 143 | |||
| 144 | const getThreadId: ProviderSessionDirectoryShape["getThreadId"] = (sessionId) => | ||
| 145 | getBinding(sessionId).pipe( | ||
| 146 | Effect.flatMap((binding) => | ||
| 147 | Option.match(binding, { | ||
| 148 | onSome: (value) => Effect.succeed(Option.fromNullishOr(value.threadId)), | ||
| 149 | onNone: () => Effect.fail(new ProviderSessionNotFoundError({ sessionId })), | ||
| 150 | }), | 130 | }), |
| 151 | ), | 131 | ), |
| 152 | ); | 132 | ); |
| 153 | 133 | ||
| 154 | const remove: ProviderSessionDirectoryShape["remove"] = (sessionId) => | 134 | const remove: ProviderSessionDirectoryShape["remove"] = (threadId) => |
| 155 | repository | 135 | repository |
| 156 | .deleteBySessionId({ providerSessionId: sessionId }) | 136 | .deleteByThreadId({ threadId }) |
| 157 | .pipe( | 137 | .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.remove:deleteByThreadId"))); |
| 158 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.remove:deleteBySessionId")), | ||
| 159 | ); | ||
| 160 | 138 | ||
| 161 | const listSessionIds: ProviderSessionDirectoryShape["listSessionIds"] = () => | 139 | const listThreadIds: ProviderSessionDirectoryShape["listThreadIds"] = () => |
| 162 | repository.list().pipe( | 140 | repository.list().pipe( |
| 163 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.listSessionIds:list")), | 141 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.listThreadIds:list")), |
| 164 | Effect.flatMap((rows) => | 142 | Effect.map((rows) => rows.map((row) => row.threadId)), |
| 165 | Effect.forEach( | ||
| 166 | rows, | ||
| 167 | (row) => | ||
| 168 | decodeProviderKind(row.providerName, "ProviderSessionDirectory.listSessionIds").pipe( | ||
| 169 | Effect.map((provider) => ({ | ||
| 170 | sessionId: row.providerSessionId, | ||
| 171 | provider, | ||
| 172 | threadId: row.threadId, | ||
| 173 | })), | ||
| 174 | ), | ||
| 175 | { concurrency: "unbounded" }, | ||
| 176 | ), | ||
| 177 | ), | ||
| 178 | Effect.map((bindings) => bindings.map((binding) => binding.sessionId)), | ||
| 179 | ); | 143 | ); |
| 180 | 144 | ||
| 181 | return { | 145 | return { |
| 182 | upsert, | 146 | upsert, |
| 183 | getProvider, | 147 | getProvider, |
| 184 | getBinding: getBindingBySessionId, | 148 | getBinding, |
| 185 | getThreadId, | ||
| 186 | remove, | 149 | remove, |
| 187 | listSessionIds, | 150 | listThreadIds, |
| 188 | } satisfies ProviderSessionDirectoryShape; | 151 | } satisfies ProviderSessionDirectoryShape; |
| 189 | }); | 152 | }); |
| 190 | 153 |
apps/server/src/provider/Services/ProviderAdapter.ts+11-12| ... | @@ -14,11 +14,10 @@ import type { | ... | @@ -14,11 +14,10 @@ import type { |
| 14 | ProviderRuntimeEvent, | 14 | ProviderRuntimeEvent, |
| 15 | ProviderSendTurnInput, | 15 | ProviderSendTurnInput, |
| 16 | ProviderSession, | 16 | ProviderSession, |
| 17 | ProviderSessionId, | ||
| 18 | ProviderSessionStartInput, | 17 | ProviderSessionStartInput, |
| 19 | ProviderThreadId, | 18 | ThreadId, |
| 20 | ProviderTurnId, | ||
| 21 | ProviderTurnStartResult, | 19 | ProviderTurnStartResult, |
| 20 | TurnId, | ||
| 22 | } from "@t3tools/contracts"; | 21 | } from "@t3tools/contracts"; |
| 23 | import type { Effect } from "effect"; | 22 | import type { Effect } from "effect"; |
| 24 | import type { Stream } from "effect"; | 23 | import type { Stream } from "effect"; |
| ... | @@ -33,12 +32,12 @@ export interface ProviderAdapterCapabilities { | ... | @@ -33,12 +32,12 @@ export interface ProviderAdapterCapabilities { |
| 33 | } | 32 | } |
| 34 | 33 | ||
| 35 | export interface ProviderThreadTurnSnapshot { | 34 | export interface ProviderThreadTurnSnapshot { |
| 36 | readonly id: ProviderTurnId; | 35 | readonly id: TurnId; |
| 37 | readonly items: ReadonlyArray<unknown>; | 36 | readonly items: ReadonlyArray<unknown>; |
| 38 | } | 37 | } |
| 39 | 38 | ||
| 40 | export interface ProviderThreadSnapshot { | 39 | export interface ProviderThreadSnapshot { |
| 41 | readonly threadId: ProviderThreadId; | 40 | readonly threadId: ThreadId; |
| 42 | readonly turns: ReadonlyArray<ProviderThreadTurnSnapshot>; | 41 | readonly turns: ReadonlyArray<ProviderThreadTurnSnapshot>; |
| 43 | } | 42 | } |
| 44 | 43 | ||
| ... | @@ -67,15 +66,15 @@ export interface ProviderAdapterShape<TError> { | ... | @@ -67,15 +66,15 @@ export interface ProviderAdapterShape<TError> { |
| 67 | * Interrupt an active turn. | 66 | * Interrupt an active turn. |
| 68 | */ | 67 | */ |
| 69 | readonly interruptTurn: ( | 68 | readonly interruptTurn: ( |
| 70 | sessionId: ProviderSessionId, | 69 | threadId: ThreadId, |
| 71 | turnId?: ProviderTurnId, | 70 | turnId?: TurnId, |
| 72 | ) => Effect.Effect<void, TError>; | 71 | ) => Effect.Effect<void, TError>; |
| 73 | 72 | ||
| 74 | /** | 73 | /** |
| 75 | * Respond to an interactive approval request. | 74 | * Respond to an interactive approval request. |
| 76 | */ | 75 | */ |
| 77 | readonly respondToRequest: ( | 76 | readonly respondToRequest: ( |
| 78 | sessionId: ProviderSessionId, | 77 | threadId: ThreadId, |
| 79 | requestId: ApprovalRequestId, | 78 | requestId: ApprovalRequestId, |
| 80 | decision: ProviderApprovalDecision, | 79 | decision: ProviderApprovalDecision, |
| 81 | ) => Effect.Effect<void, TError>; | 80 | ) => Effect.Effect<void, TError>; |
| ... | @@ -83,7 +82,7 @@ export interface ProviderAdapterShape<TError> { | ... | @@ -83,7 +82,7 @@ export interface ProviderAdapterShape<TError> { |
| 83 | /** | 82 | /** |
| 84 | * Stop one provider session. | 83 | * Stop one provider session. |
| 85 | */ | 84 | */ |
| 86 | readonly stopSession: (sessionId: ProviderSessionId) => Effect.Effect<void, TError>; | 85 | readonly stopSession: (threadId: ThreadId) => Effect.Effect<void, TError>; |
| 87 | 86 | ||
| 88 | /** | 87 | /** |
| 89 | * List currently active provider sessions for this adapter. | 88 | * List currently active provider sessions for this adapter. |
| ... | @@ -93,20 +92,20 @@ export interface ProviderAdapterShape<TError> { | ... | @@ -93,20 +92,20 @@ export interface ProviderAdapterShape<TError> { |
| 93 | /** | 92 | /** |
| 94 | * Check whether this adapter owns an active session id. | 93 | * Check whether this adapter owns an active session id. |
| 95 | */ | 94 | */ |
| 96 | readonly hasSession: (sessionId: ProviderSessionId) => Effect.Effect<boolean>; | 95 | readonly hasSession: (threadId: ThreadId) => Effect.Effect<boolean>; |
| 97 | 96 | ||
| 98 | /** | 97 | /** |
| 99 | * Read a provider thread snapshot. | 98 | * Read a provider thread snapshot. |
| 100 | */ | 99 | */ |
| 101 | readonly readThread: ( | 100 | readonly readThread: ( |
| 102 | sessionId: ProviderSessionId, | 101 | threadId: ThreadId, |
| 103 | ) => Effect.Effect<ProviderThreadSnapshot, TError>; | 102 | ) => Effect.Effect<ProviderThreadSnapshot, TError>; |
| 104 | 103 | ||
| 105 | /** | 104 | /** |
| 106 | * Roll back a provider thread by N turns. | 105 | * Roll back a provider thread by N turns. |
| 107 | */ | 106 | */ |
| 108 | readonly rollbackThread: ( | 107 | readonly rollbackThread: ( |
| 109 | sessionId: ProviderSessionId, | 108 | threadId: ThreadId, |
| 110 | numTurns: number, | 109 | numTurns: number, |
| 111 | ) => Effect.Effect<ProviderThreadSnapshot, TError>; | 110 | ) => Effect.Effect<ProviderThreadSnapshot, TError>; |
| 112 | 111 |
apps/server/src/provider/Services/ProviderService.ts+1-2| ... | @@ -18,7 +18,6 @@ import type { | ... | @@ -18,7 +18,6 @@ import type { |
| 18 | ProviderRuntimeEvent, | 18 | ProviderRuntimeEvent, |
| 19 | ProviderSendTurnInput, | 19 | ProviderSendTurnInput, |
| 20 | ProviderSession, | 20 | ProviderSession, |
| 21 | ProviderSessionId, | ||
| 22 | ProviderSessionStartInput, | 21 | ProviderSessionStartInput, |
| 23 | ProviderStopSessionInput, | 22 | ProviderStopSessionInput, |
| 24 | ThreadId, | 23 | ThreadId, |
| ... | @@ -88,7 +87,7 @@ export interface ProviderServiceShape { | ... | @@ -88,7 +87,7 @@ export interface ProviderServiceShape { |
| 88 | * Roll back provider conversation state by a number of turns. | 87 | * Roll back provider conversation state by a number of turns. |
| 89 | */ | 88 | */ |
| 90 | readonly rollbackConversation: (input: { | 89 | readonly rollbackConversation: (input: { |
| 91 | readonly sessionId: ProviderSessionId; | 90 | readonly threadId: ThreadId; |
| 92 | readonly numTurns: number; | 91 | readonly numTurns: number; |
| 93 | }) => Effect.Effect<void, ProviderServiceError>; | 92 | }) => Effect.Effect<void, ProviderServiceError>; |
| 94 | 93 |
apps/server/src/provider/Services/ProviderSessionDirectory.ts+10-57| ... | @@ -1,17 +1,6 @@ | ... | @@ -1,17 +1,6 @@ |
| 1 | /** | ||
| 2 | * ProviderSessionDirectory - Session ownership index across provider adapters. | ||
| 3 | * | ||
| 4 | * Tracks which provider owns each `sessionId` so `ProviderService` can route | ||
| 5 | * session-scoped calls to the correct adapter. It is metadata only and does not | ||
| 6 | * perform provider RPC or checkpoint operations. | ||
| 7 | * | ||
| 8 | * @module ProviderSessionDirectory | ||
| 9 | */ | ||
| 10 | import type { | 1 | import type { |
| 11 | ProviderKind, | 2 | ProviderKind, |
| 12 | ProviderSessionId, | ||
| 13 | ProviderSessionRuntimeStatus, | 3 | ProviderSessionRuntimeStatus, |
| 14 | ProviderThreadId, | ||
| 15 | RuntimeMode, | 4 | RuntimeMode, |
| 16 | ThreadId, | 5 | ThreadId, |
| 17 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| ... | @@ -20,84 +9,48 @@ import type { Effect } from "effect"; | ... | @@ -20,84 +9,48 @@ import type { Effect } from "effect"; |
| 20 | 9 | ||
| 21 | import type { | 10 | import type { |
| 22 | ProviderSessionDirectoryPersistenceError, | 11 | ProviderSessionDirectoryPersistenceError, |
| 23 | ProviderSessionNotFoundError, | ||
| 24 | ProviderValidationError, | 12 | ProviderValidationError, |
| 25 | } from "../Errors.ts"; | 13 | } from "../Errors.ts"; |
| 26 | 14 | ||
| 27 | export interface ProviderSessionBinding { | 15 | export interface ProviderRuntimeBinding { |
| 28 | readonly sessionId: ProviderSessionId; | 16 | readonly threadId: ThreadId; |
| 29 | readonly provider: ProviderKind; | 17 | readonly provider: ProviderKind; |
| 30 | readonly threadId?: ThreadId; | ||
| 31 | readonly adapterKey?: string; | 18 | readonly adapterKey?: string; |
| 32 | readonly providerThreadId?: ProviderThreadId | null; | ||
| 33 | readonly status?: ProviderSessionRuntimeStatus; | 19 | readonly status?: ProviderSessionRuntimeStatus; |
| 34 | readonly resumeCursor?: unknown | null; | 20 | readonly resumeCursor?: unknown | null; |
| 35 | readonly runtimePayload?: unknown | null; | 21 | readonly runtimePayload?: unknown | null; |
| 36 | readonly runtimeMode?: RuntimeMode; | 22 | readonly runtimeMode?: RuntimeMode; |
| 37 | } | 23 | } |
| 38 | 24 | ||
| 39 | export type ProviderSessionDirectoryReadError = | 25 | export type ProviderSessionDirectoryReadError = ProviderSessionDirectoryPersistenceError; |
| 40 | | ProviderSessionNotFoundError | ||
| 41 | | ProviderSessionDirectoryPersistenceError; | ||
| 42 | 26 | ||
| 43 | export type ProviderSessionDirectoryWriteError = | 27 | export type ProviderSessionDirectoryWriteError = |
| 44 | | ProviderValidationError | 28 | | ProviderValidationError |
| 45 | | ProviderSessionDirectoryPersistenceError; | 29 | | ProviderSessionDirectoryPersistenceError; |
| 46 | 30 | ||
| 47 | /** | ||
| 48 | * ProviderSessionDirectoryShape - Service API for provider session ownership metadata. | ||
| 49 | */ | ||
| 50 | export interface ProviderSessionDirectoryShape { | 31 | export interface ProviderSessionDirectoryShape { |
| 51 | /** | ||
| 52 | * Record or update ownership for one provider session. | ||
| 53 | * | ||
| 54 | * Preserves existing persisted fields when omitted and shallow-merges | ||
| 55 | * runtime payload objects. | ||
| 56 | */ | ||
| 57 | readonly upsert: ( | 32 | readonly upsert: ( |
| 58 | binding: ProviderSessionBinding, | 33 | binding: ProviderRuntimeBinding, |
| 59 | ) => Effect.Effect<void, ProviderSessionDirectoryWriteError>; | 34 | ) => Effect.Effect<void, ProviderSessionDirectoryWriteError>; |
| 60 | 35 | ||
| 61 | /** | ||
| 62 | * Resolve the provider owner for a session id. | ||
| 63 | */ | ||
| 64 | readonly getProvider: ( | 36 | readonly getProvider: ( |
| 65 | sessionId: ProviderSessionId, | 37 | threadId: ThreadId, |
| 66 | ) => Effect.Effect<ProviderKind, ProviderSessionDirectoryReadError>; | 38 | ) => Effect.Effect<ProviderKind, ProviderSessionDirectoryReadError>; |
| 67 | 39 | ||
| 68 | /** | ||
| 69 | * Resolve the full tracked binding for a session id. | ||
| 70 | */ | ||
| 71 | readonly getBinding: ( | 40 | readonly getBinding: ( |
| 72 | sessionId: ProviderSessionId, | 41 | threadId: ThreadId, |
| 73 | ) => Effect.Effect<Option.Option<ProviderSessionBinding>, ProviderSessionDirectoryReadError>; | 42 | ) => Effect.Effect<Option.Option<ProviderRuntimeBinding>, ProviderSessionDirectoryReadError>; |
| 74 | |||
| 75 | /** | ||
| 76 | * Resolve the tracked thread id for a session, if known. | ||
| 77 | */ | ||
| 78 | readonly getThreadId: ( | ||
| 79 | sessionId: ProviderSessionId, | ||
| 80 | ) => Effect.Effect<Option.Option<ThreadId>, ProviderSessionDirectoryReadError>; | ||
| 81 | 43 | ||
| 82 | /** | ||
| 83 | * Remove a session binding. | ||
| 84 | */ | ||
| 85 | readonly remove: ( | 44 | readonly remove: ( |
| 86 | sessionId: ProviderSessionId, | 45 | threadId: ThreadId, |
| 87 | ) => Effect.Effect<void, ProviderSessionDirectoryPersistenceError>; | 46 | ) => Effect.Effect<void, ProviderSessionDirectoryPersistenceError>; |
| 88 | 47 | ||
| 89 | /** | 48 | readonly listThreadIds: () => Effect.Effect< |
| 90 | * List tracked session ids. | 49 | ReadonlyArray<ThreadId>, |
| 91 | */ | ||
| 92 | readonly listSessionIds: () => Effect.Effect< | ||
| 93 | ReadonlyArray<ProviderSessionId>, | ||
| 94 | ProviderSessionDirectoryPersistenceError | 50 | ProviderSessionDirectoryPersistenceError |
| 95 | >; | 51 | >; |
| 96 | } | 52 | } |
| 97 | 53 | ||
| 98 | /** | ||
| 99 | * ProviderSessionDirectory - Service tag for session ownership lookup. | ||
| 100 | */ | ||
| 101 | export class ProviderSessionDirectory extends ServiceMap.Service< | 54 | export class ProviderSessionDirectory extends ServiceMap.Service< |
| 102 | ProviderSessionDirectory, | 55 | ProviderSessionDirectory, |
| 103 | ProviderSessionDirectoryShape | 56 | ProviderSessionDirectoryShape |
apps/server/src/serverLayers.ts+9-7| ... | @@ -58,14 +58,16 @@ export function makeServerProviderLayer(): Layer.Layer< | ... | @@ -58,14 +58,16 @@ export function makeServerProviderLayer(): Layer.Layer< |
| 58 | const codexAdapterLayer = makeCodexAdapterLive( | 58 | const codexAdapterLayer = makeCodexAdapterLive( |
| 59 | nativeEventLogger ? { nativeEventLogger } : undefined, | 59 | nativeEventLogger ? { nativeEventLogger } : undefined, |
| 60 | ); | 60 | ); |
| 61 | const claudeAdapterLayer = makeClaudeCodeAdapterLive({ | 61 | const claudeAdapterLayer = makeClaudeCodeAdapterLive( |
| 62 | nativeEventLogPath: path.join(providerLogsDir, "provider-native.ndjson"), | 62 | nativeEventLogger ? { nativeEventLogger } : undefined, |
| 63 | }); | 63 | ); |
| 64 | const cursorAdapterLayer = makeCursorAdapterLive({ | 64 | const cursorAdapterLayer = makeCursorAdapterLive( |
| 65 | nativeEventLogPath: path.join(providerLogsDir, "provider-native.ndjson"), | 65 | nativeEventLogger ? { nativeEventLogger } : undefined, |
| 66 | }); | 66 | ); |
| 67 | const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe( | 67 | const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe( |
| 68 | Layer.provide(Layer.mergeAll(codexAdapterLayer, claudeAdapterLayer, cursorAdapterLayer)), | 68 | Layer.provide(codexAdapterLayer), |
| 69 | Layer.provide(claudeAdapterLayer), | ||
| 70 | Layer.provide(cursorAdapterLayer), | ||
| 69 | Layer.provideMerge(providerSessionDirectoryLayer), | 71 | Layer.provideMerge(providerSessionDirectoryLayer), |
| 70 | ); | 72 | ); |
| 71 | return makeProviderServiceLive( | 73 | return makeProviderServiceLive( |
apps/server/src/wsServer.test.ts+11-16| ... | @@ -18,9 +18,8 @@ import { | ... | @@ -18,9 +18,8 @@ import { |
| 18 | ORCHESTRATION_WS_CHANNELS, | 18 | ORCHESTRATION_WS_CHANNELS, |
| 19 | ORCHESTRATION_WS_METHODS, | 19 | ORCHESTRATION_WS_METHODS, |
| 20 | ProviderItemId, | 20 | ProviderItemId, |
| 21 | ProviderSessionId, | 21 | ThreadId, |
| 22 | ProviderThreadId, | 22 | TurnId, |
| 23 | ProviderTurnId, | ||
| 24 | WS_CHANNELS, | 23 | WS_CHANNELS, |
| 25 | WS_METHODS, | 24 | WS_METHODS, |
| 26 | type WebSocketResponse, | 25 | type WebSocketResponse, |
| ... | @@ -60,11 +59,9 @@ interface PendingMessages { | ... | @@ -60,11 +59,9 @@ interface PendingMessages { |
| 60 | const pendingBySocket = new WeakMap<WebSocket, PendingMessages>(); | 59 | const pendingBySocket = new WeakMap<WebSocket, PendingMessages>(); |
| 61 | 60 | ||
| 62 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); | 61 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 63 | const asProviderSessionId = (value: string): ProviderSessionId => | ||
| 64 | ProviderSessionId.makeUnsafe(value); | ||
| 65 | const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId.makeUnsafe(value); | ||
| 66 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); | ||
| 67 | const asProviderItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(value); | 62 | const asProviderItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(value); |
| 63 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); | ||
| 64 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); | ||
| 68 | 65 | ||
| 69 | const defaultOpenService: OpenShape = { | 66 | const defaultOpenService: OpenShape = { |
| 70 | openBrowser: () => Effect.void, | 67 | openBrowser: () => Effect.void, |
| ... | @@ -1146,26 +1143,24 @@ describe("WebSocket Server", () => { | ... | @@ -1146,26 +1143,24 @@ describe("WebSocket Server", () => { |
| 1146 | 1143 | ||
| 1147 | it("keeps orchestration domain push behavior for provider runtime events", async () => { | 1144 | it("keeps orchestration domain push behavior for provider runtime events", async () => { |
| 1148 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); | 1145 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| 1149 | const sessionId = asProviderSessionId("sess-test"); | ||
| 1150 | const emitRuntimeEvent = (event: ProviderRuntimeEvent) => { | 1146 | const emitRuntimeEvent = (event: ProviderRuntimeEvent) => { |
| 1151 | Effect.runSync(PubSub.publish(runtimeEventPubSub, event)); | 1147 | Effect.runSync(PubSub.publish(runtimeEventPubSub, event)); |
| 1152 | }; | 1148 | }; |
| 1153 | const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never; | 1149 | const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never; |
| 1154 | const providerService: ProviderServiceShape = { | 1150 | const providerService: ProviderServiceShape = { |
| 1155 | startSession: () => | 1151 | startSession: (threadId) => |
| 1156 | Effect.succeed({ | 1152 | Effect.succeed({ |
| 1157 | sessionId, | ||
| 1158 | provider: "codex", | 1153 | provider: "codex", |
| 1159 | status: "ready", | 1154 | status: "ready", |
| 1160 | runtimeMode: "full-access", | 1155 | runtimeMode: "full-access", |
| 1161 | threadId: asProviderThreadId("provider-thread-1"), | 1156 | threadId, |
| 1162 | createdAt: new Date().toISOString(), | 1157 | createdAt: new Date().toISOString(), |
| 1163 | updatedAt: new Date().toISOString(), | 1158 | updatedAt: new Date().toISOString(), |
| 1164 | }), | 1159 | }), |
| 1165 | sendTurn: () => | 1160 | sendTurn: ({ threadId }) => |
| 1166 | Effect.succeed({ | 1161 | Effect.succeed({ |
| 1167 | threadId: asProviderThreadId("provider-thread-1"), | 1162 | threadId, |
| 1168 | turnId: asProviderTurnId("provider-turn-1"), | 1163 | turnId: asTurnId("provider-turn-1"), |
| 1169 | }), | 1164 | }), |
| 1170 | interruptTurn: () => unsupported(), | 1165 | interruptTurn: () => unsupported(), |
| 1171 | respondToRequest: () => unsupported(), | 1166 | respondToRequest: () => unsupported(), |
| ... | @@ -1239,9 +1234,9 @@ describe("WebSocket Server", () => { | ... | @@ -1239,9 +1234,9 @@ describe("WebSocket Server", () => { |
| 1239 | type: "content.delta", | 1234 | type: "content.delta", |
| 1240 | eventId: asEventId("evt-ws-runtime-message-delta"), | 1235 | eventId: asEventId("evt-ws-runtime-message-delta"), |
| 1241 | provider: "codex", | 1236 | provider: "codex", |
| 1242 | sessionId, | 1237 | threadId: asThreadId("thread-1"), |
| 1243 | createdAt: new Date().toISOString(), | 1238 | createdAt: new Date().toISOString(), |
| 1244 | turnId: asProviderTurnId("turn-1"), | 1239 | turnId: asTurnId("turn-1"), |
| 1245 | itemId: asProviderItemId("item-1"), | 1240 | itemId: asProviderItemId("item-1"), |
| 1246 | payload: { | 1241 | payload: { |
| 1247 | streamKind: "assistant_text", | 1242 | streamKind: "assistant_text", |
apps/web/src/components/BranchToolbar.tsx+2-3| ... | @@ -52,8 +52,7 @@ export default function BranchToolbar({ | ... | @@ -52,8 +52,7 @@ export default function BranchToolbar({ |
| 52 | const api = readNativeApi(); | 52 | const api = readNativeApi(); |
| 53 | // If the effective cwd is about to change, stop the running session so the | 53 | // If the effective cwd is about to change, stop the running session so the |
| 54 | // next message creates a new one with the correct cwd. | 54 | // next message creates a new one with the correct cwd. |
| 55 | const sessionId = serverThread?.session?.sessionId; | 55 | if (serverThread?.session && worktreePath !== activeWorktreePath && api) { |
| 56 | if (sessionId && worktreePath !== activeWorktreePath && api) { | ||
| 57 | void api.orchestration | 56 | void api.orchestration |
| 58 | .dispatchCommand({ | 57 | .dispatchCommand({ |
| 59 | type: "thread.session.stop", | 58 | type: "thread.session.stop", |
| ... | @@ -89,7 +88,7 @@ export default function BranchToolbar({ | ... | @@ -89,7 +88,7 @@ export default function BranchToolbar({ |
| 89 | }, | 88 | }, |
| 90 | [ | 89 | [ |
| 91 | activeThreadId, | 90 | activeThreadId, |
| 92 | serverThread?.session?.sessionId, | 91 | serverThread?.session, |
| 93 | activeWorktreePath, | 92 | activeWorktreePath, |
| 94 | hasServerThread, | 93 | hasServerThread, |
| 95 | setThreadBranchAction, | 94 | setThreadBranchAction, |
apps/web/src/components/ChatView.tsx+2| ... | @@ -3324,6 +3324,8 @@ const PendingApprovalsPanel = memo(function PendingApprovalsPanel({ | ... | @@ -3324,6 +3324,8 @@ const PendingApprovalsPanel = memo(function PendingApprovalsPanel({ |
| 3324 | <AlertTitle className="text-xs"> | 3324 | <AlertTitle className="text-xs"> |
| 3325 | {approval.requestKind === "command" | 3325 | {approval.requestKind === "command" |
| 3326 | ? "Command approval requested" | 3326 | ? "Command approval requested" |
| 3327 | : approval.requestKind === "file-read" | ||
| 3328 | ? "File-read approval requested" | ||
| 3327 | : "File-change approval requested"} | 3329 | : "File-change approval requested"} |
| 3328 | </AlertTitle> | 3330 | </AlertTitle> |
| 3329 | <AlertDescription | 3331 | <AlertDescription |
apps/web/src/session-logic.ts+8-3| ... | @@ -28,7 +28,7 @@ export interface WorkLogEntry { | ... | @@ -28,7 +28,7 @@ export interface WorkLogEntry { |
| 28 | 28 | ||
| 29 | export interface PendingApproval { | 29 | export interface PendingApproval { |
| 30 | requestId: ApprovalRequestId; | 30 | requestId: ApprovalRequestId; |
| 31 | requestKind: "command" | "file-change"; | 31 | requestKind: "command" | "file-read" | "file-change"; |
| 32 | createdAt: string; | 32 | createdAt: string; |
| 33 | detail?: string; | 33 | detail?: string; |
| 34 | } | 34 | } |
| ... | @@ -95,6 +95,8 @@ function requestKindFromRequestType( | ... | @@ -95,6 +95,8 @@ function requestKindFromRequestType( |
| 95 | case "command_execution_approval": | 95 | case "command_execution_approval": |
| 96 | case "exec_command_approval": | 96 | case "exec_command_approval": |
| 97 | return "command"; | 97 | return "command"; |
| 98 | case "file_read_approval": | ||
| 99 | return "file-read"; | ||
| 98 | case "file_change_approval": | 100 | case "file_change_approval": |
| 99 | case "apply_patch_approval": | 101 | case "apply_patch_approval": |
| 100 | return "file-change"; | 102 | return "file-change"; |
| ... | @@ -119,11 +121,14 @@ export function derivePendingApprovals( | ... | @@ -119,11 +121,14 @@ export function derivePendingApprovals( |
| 119 | ? ApprovalRequestId.makeUnsafe(payload.requestId) | 121 | ? ApprovalRequestId.makeUnsafe(payload.requestId) |
| 120 | : null; | 122 | : null; |
| 121 | const requestKind = | 123 | const requestKind = |
| 122 | payload && (payload.requestKind === "command" || payload.requestKind === "file-change") | 124 | payload && |
| 125 | (payload.requestKind === "command" || | ||
| 126 | payload.requestKind === "file-read" || | ||
| 127 | payload.requestKind === "file-change") | ||
| 123 | ? payload.requestKind | 128 | ? payload.requestKind |
| 124 | : payload | 129 | : payload |
| 125 | ? requestKindFromRequestType(payload.requestType) | 130 | ? requestKindFromRequestType(payload.requestType) |
| 126 | : null; | 131 | : null; |
| 127 | const detail = payload && typeof payload.detail === "string" ? payload.detail : undefined; | 132 | const detail = payload && typeof payload.detail === "string" ? payload.detail : undefined; |
| 128 | 133 | ||
| 129 | if (activity.kind === "approval.requested" && requestId && requestKind) { | 134 | if (activity.kind === "approval.requested" && requestId && requestKind) { |
apps/web/src/store.test.ts-4| ... | @@ -147,8 +147,6 @@ describe("store read model sync", () => { | ... | @@ -147,8 +147,6 @@ describe("store read model sync", () => { |
| 147 | threadId: ThreadId.makeUnsafe("thread-1"), | 147 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 148 | status: "ready", | 148 | status: "ready", |
| 149 | providerName: "claudeCode", | 149 | providerName: "claudeCode", |
| 150 | providerSessionId: null, | ||
| 151 | providerThreadId: null, | ||
| 152 | runtimeMode: "approval-required", | 150 | runtimeMode: "approval-required", |
| 153 | activeTurnId: null, | 151 | activeTurnId: null, |
| 154 | lastError: null, | 152 | lastError: null, |
| ... | @@ -171,8 +169,6 @@ describe("store read model sync", () => { | ... | @@ -171,8 +169,6 @@ describe("store read model sync", () => { |
| 171 | threadId: ThreadId.makeUnsafe("thread-1"), | 169 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 172 | status: "ready", | 170 | status: "ready", |
| 173 | providerName: "cursor", | 171 | providerName: "cursor", |
| 174 | providerSessionId: null, | ||
| 175 | providerThreadId: null, | ||
| 176 | runtimeMode: "approval-required", | 172 | runtimeMode: "approval-required", |
| 177 | activeTurnId: null, | 173 | activeTurnId: null, |
| 178 | lastError: null, | 174 | lastError: null, |
apps/web/src/store.ts+1-6| ... | @@ -4,7 +4,6 @@ import { | ... | @@ -4,7 +4,6 @@ import { |
| 4 | getModelOptions, | 4 | getModelOptions, |
| 5 | normalizeModelSlug, | 5 | normalizeModelSlug, |
| 6 | type ProviderKind, | 6 | type ProviderKind, |
| 7 | ProviderSessionId, | ||
| 8 | ThreadId, | 7 | ThreadId, |
| 9 | type OrchestrationReadModel, | 8 | type OrchestrationReadModel, |
| 10 | type OrchestrationSessionStatus, | 9 | type OrchestrationSessionStatus, |
| ... | @@ -247,7 +246,7 @@ export function syncServerReadModel( | ... | @@ -247,7 +246,7 @@ export function syncServerReadModel( |
| 247 | const existing = existingThreadById.get(thread.id); | 246 | const existing = existingThreadById.get(thread.id); |
| 248 | return { | 247 | return { |
| 249 | id: thread.id, | 248 | id: thread.id, |
| 250 | codexThreadId: thread.session?.providerThreadId ?? null, | 249 | codexThreadId: null, |
| 251 | projectId: thread.projectId, | 250 | projectId: thread.projectId, |
| 252 | title: thread.title, | 251 | title: thread.title, |
| 253 | model: resolveModelSlugForProvider( | 252 | model: resolveModelSlugForProvider( |
| ... | @@ -260,13 +259,9 @@ export function syncServerReadModel( | ... | @@ -260,13 +259,9 @@ export function syncServerReadModel( |
| 260 | runtimeMode: thread.runtimeMode, | 259 | runtimeMode: thread.runtimeMode, |
| 261 | session: thread.session | 260 | session: thread.session |
| 262 | ? { | 261 | ? { |
| 263 | sessionId: | ||
| 264 | thread.session.providerSessionId ?? | ||
| 265 | ProviderSessionId.makeUnsafe(`thread:${thread.id}`), | ||
| 266 | provider: toLegacyProvider(thread.session.providerName), | 262 | provider: toLegacyProvider(thread.session.providerName), |
| 267 | status: toLegacySessionStatus(thread.session.status), | 263 | status: toLegacySessionStatus(thread.session.status), |
| 268 | orchestrationStatus: thread.session.status, | 264 | orchestrationStatus: thread.session.status, |
| 269 | threadId: thread.session.providerThreadId, | ||
| 270 | activeTurnId: thread.session.activeTurnId ?? undefined, | 265 | activeTurnId: thread.session.activeTurnId ?? undefined, |
| 271 | createdAt: thread.session.updatedAt, | 266 | createdAt: thread.session.updatedAt, |
| 272 | updatedAt: thread.session.updatedAt, | 267 | updatedAt: thread.session.updatedAt, |
apps/web/src/types.ts+1-5| ... | @@ -8,8 +8,6 @@ import type { | ... | @@ -8,8 +8,6 @@ import type { |
| 8 | TurnId, | 8 | TurnId, |
| 9 | MessageId, | 9 | MessageId, |
| 10 | CheckpointRef, | 10 | CheckpointRef, |
| 11 | ProviderThreadId, | ||
| 12 | ProviderSessionId, | ||
| 13 | ProviderKind, | 11 | ProviderKind, |
| 14 | } from "@t3tools/contracts"; | 12 | } from "@t3tools/contracts"; |
| 15 | 13 | ||
| ... | @@ -75,7 +73,7 @@ export interface Project { | ... | @@ -75,7 +73,7 @@ export interface Project { |
| 75 | 73 | ||
| 76 | export interface Thread { | 74 | export interface Thread { |
| 77 | id: ThreadId; | 75 | id: ThreadId; |
| 78 | codexThreadId: ProviderThreadId | null; | 76 | codexThreadId: string | null; |
| 79 | projectId: ProjectId; | 77 | projectId: ProjectId; |
| 80 | title: string; | 78 | title: string; |
| 81 | model: string; | 79 | model: string; |
| ... | @@ -93,10 +91,8 @@ export interface Thread { | ... | @@ -93,10 +91,8 @@ export interface Thread { |
| 93 | } | 91 | } |
| 94 | 92 | ||
| 95 | export interface ThreadSession { | 93 | export interface ThreadSession { |
| 96 | sessionId: ProviderSessionId; | ||
| 97 | provider: ProviderKind; | 94 | provider: ProviderKind; |
| 98 | status: SessionPhase | "error" | "closed"; | 95 | status: SessionPhase | "error" | "closed"; |
| 99 | threadId: ProviderThreadId | null; | ||
| 100 | activeTurnId?: TurnId | undefined; | 96 | activeTurnId?: TurnId | undefined; |
| 101 | createdAt: string; | 97 | createdAt: string; |
| 102 | updatedAt: string; | 98 | updatedAt: string; |
packages/contracts/src/baseSchemas.ts-6| ... | @@ -28,12 +28,6 @@ export type MessageId = typeof MessageId.Type; | ... | @@ -28,12 +28,6 @@ export type MessageId = typeof MessageId.Type; |
| 28 | export const TurnId = makeEntityId("TurnId"); | 28 | export const TurnId = makeEntityId("TurnId"); |
| 29 | export type TurnId = typeof TurnId.Type; | 29 | export type TurnId = typeof TurnId.Type; |
| 30 | 30 | ||
| 31 | export const ProviderSessionId = makeEntityId("ProviderSessionId"); | ||
| 32 | export type ProviderSessionId = typeof ProviderSessionId.Type; | ||
| 33 | export const ProviderThreadId = makeEntityId("ProviderThreadId"); | ||
| 34 | export type ProviderThreadId = typeof ProviderThreadId.Type; | ||
| 35 | export const ProviderTurnId = makeEntityId("ProviderTurnId"); | ||
| 36 | export type ProviderTurnId = typeof ProviderTurnId.Type; | ||
| 37 | export const ProviderItemId = makeEntityId("ProviderItemId"); | 31 | export const ProviderItemId = makeEntityId("ProviderItemId"); |
| 38 | export type ProviderItemId = typeof ProviderItemId.Type; | 32 | export type ProviderItemId = typeof ProviderItemId.Type; |
| 39 | export const RuntimeSessionId = makeEntityId("RuntimeSessionId"); | 33 | export const RuntimeSessionId = makeEntityId("RuntimeSessionId"); |
packages/contracts/src/orchestration.ts+2-9| ... | @@ -9,9 +9,6 @@ import { | ... | @@ -9,9 +9,6 @@ import { |
| 9 | NonNegativeInt, | 9 | NonNegativeInt, |
| 10 | ProjectId, | 10 | ProjectId, |
| 11 | ProviderItemId, | 11 | ProviderItemId, |
| 12 | ProviderSessionId, | ||
| 13 | ProviderThreadId, | ||
| 14 | ProviderTurnId, | ||
| 15 | ThreadId, | 12 | ThreadId, |
| 16 | TrimmedNonEmptyString, | 13 | TrimmedNonEmptyString, |
| 17 | TurnId, | 14 | TurnId, |
| ... | @@ -35,7 +32,7 @@ export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; | ... | @@ -35,7 +32,7 @@ export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; |
| 35 | export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); | 32 | export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); |
| 36 | export type RuntimeMode = typeof RuntimeMode.Type; | 33 | export type RuntimeMode = typeof RuntimeMode.Type; |
| 37 | export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; | 34 | export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| 38 | export const ProviderRequestKind = Schema.Literals(["command", "file-change"]); | 35 | export const ProviderRequestKind = Schema.Literals(["command", "file-read", "file-change"]); |
| 39 | export type ProviderRequestKind = typeof ProviderRequestKind.Type; | 36 | export type ProviderRequestKind = typeof ProviderRequestKind.Type; |
| 40 | export const AssistantDeliveryMode = Schema.Literals(["buffered", "streaming"]); | 37 | export const AssistantDeliveryMode = Schema.Literals(["buffered", "streaming"]); |
| 41 | export type AssistantDeliveryMode = typeof AssistantDeliveryMode.Type; | 38 | export type AssistantDeliveryMode = typeof AssistantDeliveryMode.Type; |
| ... | @@ -149,8 +146,6 @@ export const OrchestrationSession = Schema.Struct({ | ... | @@ -149,8 +146,6 @@ export const OrchestrationSession = Schema.Struct({ |
| 149 | threadId: ThreadId, | 146 | threadId: ThreadId, |
| 150 | status: OrchestrationSessionStatus, | 147 | status: OrchestrationSessionStatus, |
| 151 | providerName: Schema.NullOr(TrimmedNonEmptyString), | 148 | providerName: Schema.NullOr(TrimmedNonEmptyString), |
| 152 | providerSessionId: Schema.NullOr(ProviderSessionId), | ||
| 153 | providerThreadId: Schema.NullOr(ProviderThreadId), | ||
| 154 | runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), | 149 | runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), |
| 155 | activeTurnId: Schema.NullOr(TurnId), | 150 | activeTurnId: Schema.NullOr(TurnId), |
| 156 | lastError: Schema.NullOr(TrimmedNonEmptyString), | 151 | lastError: Schema.NullOr(TrimmedNonEmptyString), |
| ... | @@ -637,9 +632,7 @@ export const ThreadActivityAppendedPayload = Schema.Struct({ | ... | @@ -637,9 +632,7 @@ export const ThreadActivityAppendedPayload = Schema.Struct({ |
| 637 | }); | 632 | }); |
| 638 | 633 | ||
| 639 | export const OrchestrationEventMetadata = Schema.Struct({ | 634 | export const OrchestrationEventMetadata = Schema.Struct({ |
| 640 | providerSessionId: Schema.optional(ProviderSessionId), | 635 | providerTurnId: Schema.optional(TrimmedNonEmptyString), |
| 641 | providerThreadId: Schema.optional(ProviderThreadId), | ||
| 642 | providerTurnId: Schema.optional(ProviderTurnId), | ||
| 643 | providerItemId: Schema.optional(ProviderItemId), | 636 | providerItemId: Schema.optional(ProviderItemId), |
| 644 | adapterKey: Schema.optional(TrimmedNonEmptyString), | 637 | adapterKey: Schema.optional(TrimmedNonEmptyString), |
| 645 | requestId: Schema.optional(ApprovalRequestId), | 638 | requestId: Schema.optional(ApprovalRequestId), |
packages/contracts/src/provider.ts+16-19| ... | @@ -6,10 +6,8 @@ import { | ... | @@ -6,10 +6,8 @@ import { |
| 6 | IsoDateTime, | 6 | IsoDateTime, |
| 7 | NonNegativeInt, | 7 | NonNegativeInt, |
| 8 | ProviderItemId, | 8 | ProviderItemId, |
| 9 | ProviderSessionId, | ||
| 10 | ProviderThreadId, | ||
| 11 | ProviderTurnId, | ||
| 12 | ThreadId, | 9 | ThreadId, |
| 10 | TurnId, | ||
| 13 | } from "./baseSchemas"; | 11 | } from "./baseSchemas"; |
| 14 | import { | 12 | import { |
| 15 | ChatAttachment, | 13 | ChatAttachment, |
| ... | @@ -34,15 +32,14 @@ export const ProviderSessionStatus = Schema.Literals([ | ... | @@ -34,15 +32,14 @@ export const ProviderSessionStatus = Schema.Literals([ |
| 34 | export type ProviderSessionStatus = typeof ProviderSessionStatus.Type; | 32 | export type ProviderSessionStatus = typeof ProviderSessionStatus.Type; |
| 35 | 33 | ||
| 36 | export const ProviderSession = Schema.Struct({ | 34 | export const ProviderSession = Schema.Struct({ |
| 37 | sessionId: ProviderSessionId, | ||
| 38 | provider: ProviderKind, | 35 | provider: ProviderKind, |
| 39 | status: ProviderSessionStatus, | 36 | status: ProviderSessionStatus, |
| 40 | runtimeMode: RuntimeMode, | 37 | runtimeMode: RuntimeMode, |
| 41 | cwd: Schema.optional(TrimmedNonEmptyStringSchema), | 38 | cwd: Schema.optional(TrimmedNonEmptyStringSchema), |
| 42 | model: Schema.optional(TrimmedNonEmptyStringSchema), | 39 | model: Schema.optional(TrimmedNonEmptyStringSchema), |
| 43 | threadId: Schema.optional(ProviderThreadId), | 40 | threadId: ThreadId, |
| 44 | resumeCursor: Schema.optional(Schema.Unknown), | 41 | resumeCursor: Schema.optional(Schema.Unknown), |
| 45 | activeTurnId: Schema.optional(ProviderTurnId), | 42 | activeTurnId: Schema.optional(TurnId), |
| 46 | createdAt: IsoDateTime, | 43 | createdAt: IsoDateTime, |
| 47 | updatedAt: IsoDateTime, | 44 | updatedAt: IsoDateTime, |
| 48 | lastError: Schema.optional(TrimmedNonEmptyStringSchema), | 45 | lastError: Schema.optional(TrimmedNonEmptyStringSchema), |
| ... | @@ -75,6 +72,7 @@ export const ProviderStartOptions = Schema.Struct({ | ... | @@ -75,6 +72,7 @@ export const ProviderStartOptions = Schema.Struct({ |
| 75 | export type ProviderStartOptions = typeof ProviderStartOptions.Type; | 72 | export type ProviderStartOptions = typeof ProviderStartOptions.Type; |
| 76 | 73 | ||
| 77 | export const ProviderSessionStartInput = Schema.Struct({ | 74 | export const ProviderSessionStartInput = Schema.Struct({ |
| 75 | threadId: ThreadId, | ||
| 78 | provider: Schema.optional(ProviderKind), | 76 | provider: Schema.optional(ProviderKind), |
| 79 | cwd: Schema.optional(TrimmedNonEmptyStringSchema), | 77 | cwd: Schema.optional(TrimmedNonEmptyStringSchema), |
| 80 | model: Schema.optional(TrimmedNonEmptyStringSchema), | 78 | model: Schema.optional(TrimmedNonEmptyStringSchema), |
| ... | @@ -85,7 +83,7 @@ export const ProviderSessionStartInput = Schema.Struct({ | ... | @@ -85,7 +83,7 @@ export const ProviderSessionStartInput = Schema.Struct({ |
| 85 | export type ProviderSessionStartInput = typeof ProviderSessionStartInput.Type; | 83 | export type ProviderSessionStartInput = typeof ProviderSessionStartInput.Type; |
| 86 | 84 | ||
| 87 | export const ProviderSendTurnInput = Schema.Struct({ | 85 | export const ProviderSendTurnInput = Schema.Struct({ |
| 88 | sessionId: ProviderSessionId, | 86 | threadId: ThreadId, |
| 89 | input: Schema.optional( | 87 | input: Schema.optional( |
| 90 | TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_INPUT_CHARS)), | 88 | TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_INPUT_CHARS)), |
| 91 | ), | 89 | ), |
| ... | @@ -98,25 +96,25 @@ export const ProviderSendTurnInput = Schema.Struct({ | ... | @@ -98,25 +96,25 @@ export const ProviderSendTurnInput = Schema.Struct({ |
| 98 | export type ProviderSendTurnInput = typeof ProviderSendTurnInput.Type; | 96 | export type ProviderSendTurnInput = typeof ProviderSendTurnInput.Type; |
| 99 | 97 | ||
| 100 | export const ProviderTurnStartResult = Schema.Struct({ | 98 | export const ProviderTurnStartResult = Schema.Struct({ |
| 101 | threadId: Schema.optional(ProviderThreadId), | 99 | threadId: ThreadId, |
| 102 | turnId: ProviderTurnId, | 100 | turnId: TurnId, |
| 103 | resumeCursor: Schema.optional(Schema.Unknown), | 101 | resumeCursor: Schema.optional(Schema.Unknown), |
| 104 | }); | 102 | }); |
| 105 | export type ProviderTurnStartResult = typeof ProviderTurnStartResult.Type; | 103 | export type ProviderTurnStartResult = typeof ProviderTurnStartResult.Type; |
| 106 | 104 | ||
| 107 | export const ProviderInterruptTurnInput = Schema.Struct({ | 105 | export const ProviderInterruptTurnInput = Schema.Struct({ |
| 108 | sessionId: ProviderSessionId, | 106 | threadId: ThreadId, |
| 109 | turnId: Schema.optional(ProviderTurnId), | 107 | turnId: Schema.optional(TurnId), |
| 110 | }); | 108 | }); |
| 111 | export type ProviderInterruptTurnInput = typeof ProviderInterruptTurnInput.Type; | 109 | export type ProviderInterruptTurnInput = typeof ProviderInterruptTurnInput.Type; |
| 112 | 110 | ||
| 113 | export const ProviderStopSessionInput = Schema.Struct({ | 111 | export const ProviderStopSessionInput = Schema.Struct({ |
| 114 | sessionId: ProviderSessionId, | 112 | threadId: ThreadId, |
| 115 | }); | 113 | }); |
| 116 | export type ProviderStopSessionInput = typeof ProviderStopSessionInput.Type; | 114 | export type ProviderStopSessionInput = typeof ProviderStopSessionInput.Type; |
| 117 | 115 | ||
| 118 | export const ProviderListCheckpointsInput = Schema.Struct({ | 116 | export const ProviderListCheckpointsInput = Schema.Struct({ |
| 119 | sessionId: ProviderSessionId, | 117 | threadId: ThreadId, |
| 120 | }); | 118 | }); |
| 121 | export type ProviderListCheckpointsInput = typeof ProviderListCheckpointsInput.Type; | 119 | export type ProviderListCheckpointsInput = typeof ProviderListCheckpointsInput.Type; |
| 122 | 120 | ||
| ... | @@ -137,7 +135,7 @@ export const ProviderListCheckpointsResult = Schema.Struct({ | ... | @@ -137,7 +135,7 @@ export const ProviderListCheckpointsResult = Schema.Struct({ |
| 137 | export type ProviderListCheckpointsResult = typeof ProviderListCheckpointsResult.Type; | 135 | export type ProviderListCheckpointsResult = typeof ProviderListCheckpointsResult.Type; |
| 138 | 136 | ||
| 139 | export const ProviderRevertToCheckpointInput = Schema.Struct({ | 137 | export const ProviderRevertToCheckpointInput = Schema.Struct({ |
| 140 | sessionId: ProviderSessionId, | 138 | threadId: ThreadId, |
| 141 | turnCount: NonNegativeInt, | 139 | turnCount: NonNegativeInt, |
| 142 | }); | 140 | }); |
| 143 | export type ProviderRevertToCheckpointInput = typeof ProviderRevertToCheckpointInput.Type; | 141 | export type ProviderRevertToCheckpointInput = typeof ProviderRevertToCheckpointInput.Type; |
| ... | @@ -152,7 +150,7 @@ export const ProviderRevertToCheckpointResult = Schema.Struct({ | ... | @@ -152,7 +150,7 @@ export const ProviderRevertToCheckpointResult = Schema.Struct({ |
| 152 | export type ProviderRevertToCheckpointResult = typeof ProviderRevertToCheckpointResult.Type; | 150 | export type ProviderRevertToCheckpointResult = typeof ProviderRevertToCheckpointResult.Type; |
| 153 | 151 | ||
| 154 | export const ProviderGetCheckpointDiffInput = Schema.Struct({ | 152 | export const ProviderGetCheckpointDiffInput = Schema.Struct({ |
| 155 | sessionId: ProviderSessionId, | 153 | threadId: ThreadId, |
| 156 | ...TurnCountRange.fields, | 154 | ...TurnCountRange.fields, |
| 157 | }); | 155 | }); |
| 158 | export type ProviderGetCheckpointDiffInput = typeof ProviderGetCheckpointDiffInput.Type; | 156 | export type ProviderGetCheckpointDiffInput = typeof ProviderGetCheckpointDiffInput.Type; |
| ... | @@ -165,7 +163,7 @@ export const ProviderGetCheckpointDiffResult = Schema.Struct({ | ... | @@ -165,7 +163,7 @@ export const ProviderGetCheckpointDiffResult = Schema.Struct({ |
| 165 | export type ProviderGetCheckpointDiffResult = typeof ProviderGetCheckpointDiffResult.Type; | 163 | export type ProviderGetCheckpointDiffResult = typeof ProviderGetCheckpointDiffResult.Type; |
| 166 | 164 | ||
| 167 | export const ProviderRespondToRequestInput = Schema.Struct({ | 165 | export const ProviderRespondToRequestInput = Schema.Struct({ |
| 168 | sessionId: ProviderSessionId, | 166 | threadId: ThreadId, |
| 169 | requestId: ApprovalRequestId, | 167 | requestId: ApprovalRequestId, |
| 170 | decision: ProviderApprovalDecision, | 168 | decision: ProviderApprovalDecision, |
| 171 | }); | 169 | }); |
| ... | @@ -178,12 +176,11 @@ export const ProviderEvent = Schema.Struct({ | ... | @@ -178,12 +176,11 @@ export const ProviderEvent = Schema.Struct({ |
| 178 | id: EventId, | 176 | id: EventId, |
| 179 | kind: ProviderEventKind, | 177 | kind: ProviderEventKind, |
| 180 | provider: ProviderKind, | 178 | provider: ProviderKind, |
| 181 | sessionId: ProviderSessionId, | 179 | threadId: ThreadId, |
| 182 | createdAt: IsoDateTime, | 180 | createdAt: IsoDateTime, |
| 183 | method: TrimmedNonEmptyStringSchema, | 181 | method: TrimmedNonEmptyStringSchema, |
| 184 | message: Schema.optional(TrimmedNonEmptyStringSchema), | 182 | message: Schema.optional(TrimmedNonEmptyStringSchema), |
| 185 | threadId: Schema.optional(ProviderThreadId), | 183 | turnId: Schema.optional(TurnId), |
| 186 | turnId: Schema.optional(ProviderTurnId), | ||
| 187 | itemId: Schema.optional(ProviderItemId), | 184 | itemId: Schema.optional(ProviderItemId), |
| 188 | requestId: Schema.optional(ApprovalRequestId), | 185 | requestId: Schema.optional(ApprovalRequestId), |
| 189 | requestKind: Schema.optional(ProviderRequestKind), | 186 | requestKind: Schema.optional(ProviderRequestKind), |
packages/contracts/src/providerRuntime.ts+10-11| ... | @@ -3,12 +3,8 @@ import { | ... | @@ -3,12 +3,8 @@ import { |
| 3 | EventId, | 3 | EventId, |
| 4 | IsoDateTime, | 4 | IsoDateTime, |
| 5 | ProviderItemId, | 5 | ProviderItemId, |
| 6 | ProviderSessionId, | ||
| 7 | ProviderThreadId, | ||
| 8 | ProviderTurnId, | ||
| 9 | RuntimeItemId, | 6 | RuntimeItemId, |
| 10 | RuntimeRequestId, | 7 | RuntimeRequestId, |
| 11 | RuntimeSessionId, | ||
| 12 | RuntimeTaskId, | 8 | RuntimeTaskId, |
| 13 | ThreadId, | 9 | ThreadId, |
| 14 | TrimmedNonEmptyString, | 10 | TrimmedNonEmptyString, |
| ... | @@ -44,9 +40,7 @@ export const ProviderRequestId = TrimmedNonEmptyStringSchema; | ... | @@ -44,9 +40,7 @@ export const ProviderRequestId = TrimmedNonEmptyStringSchema; |
| 44 | export type ProviderRequestId = typeof ProviderRequestId.Type; | 40 | export type ProviderRequestId = typeof ProviderRequestId.Type; |
| 45 | 41 | ||
| 46 | export const ProviderRefs = Schema.Struct({ | 42 | export const ProviderRefs = Schema.Struct({ |
| 47 | providerSessionId: Schema.optional(ProviderSessionId), | 43 | providerTurnId: Schema.optional(TrimmedNonEmptyStringSchema), |
| 48 | providerThreadId: Schema.optional(ProviderThreadId), | ||
| 49 | providerTurnId: Schema.optional(ProviderTurnId), | ||
| 50 | providerItemId: Schema.optional(ProviderItemId), | 44 | providerItemId: Schema.optional(ProviderItemId), |
| 51 | providerRequestId: Schema.optional(ProviderRequestId), | 45 | providerRequestId: Schema.optional(ProviderRequestId), |
| 52 | }); | 46 | }); |
| ... | @@ -131,6 +125,7 @@ export type CanonicalItemType = typeof CanonicalItemType.Type; | ... | @@ -131,6 +125,7 @@ export type CanonicalItemType = typeof CanonicalItemType.Type; |
| 131 | 125 | ||
| 132 | export const CanonicalRequestType = Schema.Literals([ | 126 | export const CanonicalRequestType = Schema.Literals([ |
| 133 | "command_execution_approval", | 127 | "command_execution_approval", |
| 128 | "file_read_approval", | ||
| 134 | "file_change_approval", | 129 | "file_change_approval", |
| 135 | "apply_patch_approval", | 130 | "apply_patch_approval", |
| 136 | "exec_command_approval", | 131 | "exec_command_approval", |
| ... | @@ -239,9 +234,8 @@ export const RuntimeErrorType = Schema.Literal("runtime.error"); | ... | @@ -239,9 +234,8 @@ export const RuntimeErrorType = Schema.Literal("runtime.error"); |
| 239 | export const ProviderRuntimeEventBase = Schema.Struct({ | 234 | export const ProviderRuntimeEventBase = Schema.Struct({ |
| 240 | eventId: EventId, | 235 | eventId: EventId, |
| 241 | provider: ProviderKind, | 236 | provider: ProviderKind, |
| 242 | sessionId: RuntimeSessionId, | 237 | threadId: ThreadId, |
| 243 | createdAt: IsoDateTime, | 238 | createdAt: IsoDateTime, |
| 244 | threadId: Schema.optional(ThreadId), | ||
| 245 | turnId: Schema.optional(TurnId), | 239 | turnId: Schema.optional(TurnId), |
| 246 | itemId: Schema.optional(RuntimeItemId), | 240 | itemId: Schema.optional(RuntimeItemId), |
| 247 | requestId: Schema.optional(RuntimeRequestId), | 241 | requestId: Schema.optional(RuntimeRequestId), |
| ... | @@ -276,7 +270,7 @@ export const SessionExitedPayload = Schema.Struct({ | ... | @@ -276,7 +270,7 @@ export const SessionExitedPayload = Schema.Struct({ |
| 276 | export type SessionExitedPayload = typeof SessionExitedPayload.Type; | 270 | export type SessionExitedPayload = typeof SessionExitedPayload.Type; |
| 277 | 271 | ||
| 278 | export const ThreadStartedPayload = Schema.Struct({ | 272 | export const ThreadStartedPayload = Schema.Struct({ |
| 279 | providerThreadId: Schema.optional(ProviderThreadId), | 273 | providerThreadId: Schema.optional(TrimmedNonEmptyStringSchema), |
| 280 | }); | 274 | }); |
| 281 | export type ThreadStartedPayload = typeof ThreadStartedPayload.Type; | 275 | export type ThreadStartedPayload = typeof ThreadStartedPayload.Type; |
| 282 | 276 | ||
| ... | @@ -948,7 +942,12 @@ export const ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolv | ... | @@ -948,7 +942,12 @@ export const ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolv |
| 948 | export type ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent; | 942 | export type ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent; |
| 949 | 943 | ||
| 950 | // Legacy helper aliases retained for adapters/tests. | 944 | // Legacy helper aliases retained for adapters/tests. |
| 951 | export const ProviderRuntimeToolKind = Schema.Literals(["command", "file-change", "other"]); | 945 | export const ProviderRuntimeToolKind = Schema.Literals([ |
| 946 | "command", | ||
| 947 | "file-read", | ||
| 948 | "file-change", | ||
| 949 | "other", | ||
| 950 | ]); | ||
| 952 | export type ProviderRuntimeToolKind = typeof ProviderRuntimeToolKind.Type; | 951 | export type ProviderRuntimeToolKind = typeof ProviderRuntimeToolKind.Type; |
| 953 | 952 | ||
| 954 | export const ProviderRuntimeTurnStatus = RuntimeTurnState; | 953 | export const ProviderRuntimeTurnStatus = RuntimeTurnState; |
provider-threadless-runtime-migration-plan.md created+281| ... | @@ -0,0 +1,281 @@ | ||
| 1 | # Provider Runtime Refactor Plan | ||
| 2 | |||
| 3 | ## Goal | ||
| 4 | |||
| 5 | Remove `providerSessionId` and `providerThreadId` from the core application model and make | ||
| 6 | provider continuity depend only on: | ||
| 7 | |||
| 8 | - canonical `threadId` | ||
| 9 | - provider kind | ||
| 10 | - runtime mode | ||
| 11 | - opaque `resumeCursor` | ||
| 12 | |||
| 13 | Live provider session/process state becomes ephemeral and provider-private. | ||
| 14 | |||
| 15 | ## Desired Architecture | ||
| 16 | |||
| 17 | ### Durable state | ||
| 18 | |||
| 19 | Persist only the data required to reconstruct a provider conversation after process restart: | ||
| 20 | |||
| 21 | - `threadId` | ||
| 22 | - `provider` | ||
| 23 | - `runtimeMode` | ||
| 24 | - `resumeCursor` (opaque) | ||
| 25 | - `status` | ||
| 26 | - `activeTurnId` / `lastError` / `updatedAt` if needed for UX | ||
| 27 | - optional provider runtime payload only if it is required for resume or useful UX | ||
| 28 | |||
| 29 | Do **not** persist: | ||
| 30 | |||
| 31 | - `providerSessionId` | ||
| 32 | - `providerThreadId` | ||
| 33 | - live child-process/session handles | ||
| 34 | - adapter-private routing aliases | ||
| 35 | |||
| 36 | ### Ephemeral state | ||
| 37 | |||
| 38 | Each adapter keeps an in-memory map keyed by canonical `threadId`: | ||
| 39 | |||
| 40 | - `threadId -> live provider context` | ||
| 41 | |||
| 42 | Examples: | ||
| 43 | |||
| 44 | - Codex child process context | ||
| 45 | - Cursor ACP live session | ||
| 46 | - Claude Code live subprocess context | ||
| 47 | |||
| 48 | These are disposable and rebuilt from persisted `resumeCursor`. | ||
| 49 | |||
| 50 | ### Observability | ||
| 51 | |||
| 52 | Provider-native identifiers should live in logs/runtime events only, not in the canonical DB model. | ||
| 53 | |||
| 54 | Examples: | ||
| 55 | |||
| 56 | - Codex thread id | ||
| 57 | - Cursor ACP thread/session id | ||
| 58 | - Claude conversation/thread id | ||
| 59 | |||
| 60 | ## High-Level Refactor Sequence | ||
| 61 | |||
| 62 | ### 1. Contracts cleanup | ||
| 63 | |||
| 64 | Remove provider identity fields from shared contracts: | ||
| 65 | |||
| 66 | - `packages/contracts/src/provider.ts` | ||
| 67 | - `packages/contracts/src/orchestration.ts` | ||
| 68 | - `packages/contracts/src/providerRuntime.ts` | ||
| 69 | - related tests | ||
| 70 | |||
| 71 | Planned changes: | ||
| 72 | |||
| 73 | - remove `ProviderSessionId` from cross-layer service payloads where possible | ||
| 74 | - remove `providerSessionId` / `providerThreadId` from thread session snapshot contracts | ||
| 75 | - keep `resumeCursor` opaque | ||
| 76 | |||
| 77 | ### 2. Redefine thread runtime/session projection model | ||
| 78 | |||
| 79 | Replace thread session projection as a provider-identity carrier with a pure UX/runtime status view. | ||
| 80 | |||
| 81 | Possible projected shape: | ||
| 82 | |||
| 83 | - `status` | ||
| 84 | - `providerName` | ||
| 85 | - `runtimeMode` | ||
| 86 | - `activeTurnId` | ||
| 87 | - `lastError` | ||
| 88 | - `updatedAt` | ||
| 89 | |||
| 90 | No provider-native ids in the projection. | ||
| 91 | |||
| 92 | Files likely affected: | ||
| 93 | |||
| 94 | - `apps/server/src/persistence/Services/ProjectionThreadSessions.ts` | ||
| 95 | - `apps/server/src/persistence/Layers/ProjectionThreadSessions.ts` | ||
| 96 | - `apps/server/src/orchestration/projector.ts` | ||
| 97 | - `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` | ||
| 98 | - `apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts` | ||
| 99 | - `apps/web/src/store.ts` | ||
| 100 | |||
| 101 | ### 3. Make provider service thread-keyed | ||
| 102 | |||
| 103 | Refactor provider service APIs to route by canonical `threadId`, not `providerSessionId`. | ||
| 104 | |||
| 105 | Methods to change: | ||
| 106 | |||
| 107 | - `startSession` | ||
| 108 | - `sendTurn` | ||
| 109 | - `interruptTurn` | ||
| 110 | - `respondToRequest` | ||
| 111 | - `rollbackConversation` | ||
| 112 | - `stopSession` / equivalent | ||
| 113 | - `listSessions` | ||
| 114 | |||
| 115 | New mental model: | ||
| 116 | |||
| 117 | - orchestration calls provider service with `threadId` | ||
| 118 | - provider service resolves/creates live adapter context for that canonical thread | ||
| 119 | - adapter handles provider-native resume semantics internally | ||
| 120 | |||
| 121 | Files likely affected: | ||
| 122 | |||
| 123 | - `apps/server/src/provider/Services/ProviderService.ts` | ||
| 124 | - `apps/server/src/provider/Layers/ProviderService.ts` | ||
| 125 | - `apps/server/src/provider/Services/ProviderAdapter.ts` | ||
| 126 | - all adapter implementations/tests | ||
| 127 | |||
| 128 | ### 4. Replace session directory with thread-keyed runtime directory | ||
| 129 | |||
| 130 | Current `ProviderSessionDirectory` is centered on session ids. | ||
| 131 | Replace with a thread-keyed directory/repository. | ||
| 132 | |||
| 133 | Persist per canonical thread: | ||
| 134 | |||
| 135 | - `threadId` | ||
| 136 | - `provider` | ||
| 137 | - `runtimeMode` | ||
| 138 | - `resumeCursor` | ||
| 139 | - `status` | ||
| 140 | - optional runtime payload | ||
| 141 | |||
| 142 | Likely replacement: | ||
| 143 | |||
| 144 | - `ProviderThreadRuntimeDirectory` or similar | ||
| 145 | |||
| 146 | Files likely affected: | ||
| 147 | |||
| 148 | - `apps/server/src/provider/Services/ProviderSessionDirectory.ts` | ||
| 149 | - `apps/server/src/provider/Layers/ProviderSessionDirectory.ts` | ||
| 150 | - `apps/server/src/persistence/Services/ProviderSessionRuntime.ts` | ||
| 151 | - `apps/server/src/persistence/Layers/ProviderSessionRuntime.ts` | ||
| 152 | |||
| 153 | ### 5. Database migration | ||
| 154 | |||
| 155 | Perform a clean-slate schema migration to remove provider ids from persistence. | ||
| 156 | |||
| 157 | Targets: | ||
| 158 | |||
| 159 | - remove `provider_session_id` | ||
| 160 | - remove `provider_thread_id` | ||
| 161 | - make canonical thread id the primary runtime key where appropriate | ||
| 162 | |||
| 163 | Likely tables impacted: | ||
| 164 | |||
| 165 | - `provider_session_runtime` | ||
| 166 | - `projection_thread_sessions` | ||
| 167 | |||
| 168 | Because SQLite column removal is awkward, and app is still early. A destructive migration is preferred. | ||
| 169 | Existing users will need to reset their db so migrations run from scratch. | ||
| 170 | |||
| 171 | Migration should also preserve: | ||
| 172 | |||
| 173 | - `resume_cursor_json` | ||
| 174 | - `runtime_mode` | ||
| 175 | - `runtime_payload_json` | ||
| 176 | - status metadata | ||
| 177 | |||
| 178 | ### 6. Orchestration refactor | ||
| 179 | |||
| 180 | Remove provider-id assumptions from orchestration. | ||
| 181 | |||
| 182 | Affected areas: | ||
| 183 | |||
| 184 | - `ProviderCommandReactor` | ||
| 185 | - `ProviderRuntimeIngestion` | ||
| 186 | - `CheckpointReactor` | ||
| 187 | - decider/projector event payloads | ||
| 188 | |||
| 189 | Rules: | ||
| 190 | |||
| 191 | - orchestration should only know canonical `threadId` | ||
| 192 | - restart/reconcile should use provider service APIs keyed by canonical thread | ||
| 193 | - no orchestration logic should depend on provider-native ids | ||
| 194 | |||
| 195 | ### 7. Adapter refactor | ||
| 196 | |||
| 197 | Each adapter should: | ||
| 198 | |||
| 199 | - keep a `Map<ThreadId, LiveContext>` (LiveContext is a provider-specific context object, typed independently for each adapter) | ||
| 200 | - use persisted opaque `resumeCursor` to recreate live state | ||
| 201 | - emit provider-native ids only in runtime/log events if desired | ||
| 202 | |||
| 203 | Specific adapters: | ||
| 204 | |||
| 205 | - `CodexAdapter` | ||
| 206 | - `CursorAdapter` | ||
| 207 | - `ClaudeCodeAdapter` | ||
| 208 | |||
| 209 | ### 8. Runtime events and logs | ||
| 210 | |||
| 211 | Keep provider-native ids in logs and runtime events only when useful for debugging. | ||
| 212 | |||
| 213 | Need to review: | ||
| 214 | |||
| 215 | - `packages/contracts/src/providerRuntime.ts` | ||
| 216 | - `apps/server/src/provider/Layers/*Adapter.ts` | ||
| 217 | - log sinks / canonical event logging | ||
| 218 | |||
| 219 | Goal: | ||
| 220 | |||
| 221 | - provider-native ids optional and observational | ||
| 222 | - not required by orchestration/persistence correctness | ||
| 223 | |||
| 224 | ### 9. Web/client cleanup | ||
| 225 | |||
| 226 | Web should consume only canonical thread status. | ||
| 227 | |||
| 228 | Remove use of: | ||
| 229 | |||
| 230 | - `providerSessionId` | ||
| 231 | - `providerThreadId` | ||
| 232 | |||
| 233 | Likely files: | ||
| 234 | |||
| 235 | - `apps/web/src/store.ts` | ||
| 236 | - `apps/web/src/types.ts` | ||
| 237 | - related tests | ||
| 238 | |||
| 239 | ### 10. Test migration | ||
| 240 | |||
| 241 | Update and simplify tests across: | ||
| 242 | |||
| 243 | - provider service tests | ||
| 244 | - orchestration tests | ||
| 245 | - integration harnesses | ||
| 246 | - checkpoint/runtime ingestion tests | ||
| 247 | |||
| 248 | Tests should validate: | ||
| 249 | |||
| 250 | - thread continuity across restart using persisted opaque `resumeCursor` | ||
| 251 | - runtime mode switches after reopen | ||
| 252 | - no correctness dependency on provider-native ids | ||
| 253 | |||
| 254 | ## Suggested Execution Order | ||
| 255 | |||
| 256 | 1. Add/adjust plan-approved contracts | ||
| 257 | 2. Introduce new thread-keyed provider runtime persistence alongside old code temporarily in implementation | ||
| 258 | 3. Refactor provider service/adapters to use canonical `threadId` | ||
| 259 | 4. Switch orchestration to thread-keyed APIs | ||
| 260 | 5. Switch projections/web contracts | ||
| 261 | 6. Remove old provider-id fields and dead code | ||
| 262 | 7. Run migration and clean tests | ||
| 263 | |||
| 264 | ## Key Invariants After Refactor | ||
| 265 | |||
| 266 | - Canonical `threadId` is the only cross-layer routing key | ||
| 267 | - `resumeCursor` is the only persisted continuity primitive | ||
| 268 | - provider-native ids are never required for correctness | ||
| 269 | - live provider session/process state is always reconstructible and disposable | ||
| 270 | - restart/reopen behavior is first-class, not a stale-session special case | ||
| 271 | |||
| 272 | ## Open Questions | ||
| 273 | |||
| 274 | 1. Should `thread.session` remain in snapshots at all, or be folded into thread runtime fields? | ||
| 275 | 2. Should provider runtime payload remain persisted if not required for resume? | ||
| 276 | 3. Do we want one provider runtime row per canonical thread, or a more generic runtime-state table keyed by thread? | ||
| 277 | |||
| 278 | ## Recommendation | ||
| 279 | |||
| 280 | Proceed with a full clean-slate migration rather than incremental compatibility shims. | ||
| 281 | The current model is carrying too much provider-native identity into layers that should only reason about canonical threads and opaque resume state. | ||