diff --git a/.plans/19-thread-lineage-context-transfer.md b/.plans/19-thread-lineage-context-transfer.md new file mode 100644 index 00000000000..0b59cc38548 --- /dev/null +++ b/.plans/19-thread-lineage-context-transfer.md @@ -0,0 +1,246 @@ +# Plan: Thread Lineage And Lazy Context Transfer + +## Summary + +Implement the shared backend foundation for forking, provider handoff, merge-back, and future subagents without forcing provider selection at fork time. The first slice should create cheap app-level fork lineage and defer expensive context materialization until a run starts and the target provider is known. + +Architecture reference: `docs/orchestration-v2/thread-lineage-and-context-transfer.md`. + +## Goals + +- Support user-created fork threads for exploration. +- Keep fork creation provider-neutral and cheap. +- Reuse the same source-point/context-transfer model for provider switching, merge-back, and future subagents. +- Prefer native same-provider fork at first dispatch when available. +- Fall back to portable context only when native transfer is unavailable or target provider differs. +- Keep all behavior replayable through V2 events/projections. + +## Non-Goals For First Slice + +- Full portable context generation quality. +- Cross-provider adapter implementation beyond the data/runtime hook. +- Merge-back UI. +- Native or custom subagent orchestration. +- Forking from actively streaming provider state. +- Backward compatibility with old dev databases. + +## Core Data Model + +Add schema/contracts for: + +```ts +type ContextTransferType = + | "fork" + | "provider_handoff" + | "merge_back" + | "subagent_spawn" + | "subagent_result"; + +type ContextSourcePoint = { + threadId: ThreadId; + runId?: RunId; + checkpointId?: CheckpointId; + turnItemId?: TurnItemId; + providerThreadRef?: NativeThreadRef; + providerTurnRef?: NativeTurnRef; +}; + +type ContextTransferStatus = + | "pending" + | "resolved_native" + | "resolved_portable" + | "failed" + | "consumed" + | "superseded"; +``` + +Persist: + +- `orchestration_v2_projection_context_transfers` +- optional later portable handoff payload columns/table if the existing context handoff projection is not enough + +For the first slice, `ContextTransfer` is required. Materialized `ContextHandoff` payloads can be deferred until portable context is implemented. + +## Events + +Add V2 events: + +- `context-transfer.created` +- `context-transfer.updated` +- possibly `context-handoff.created` later + +Do not emit provider-native fork details as app identity. Native fork refs belong in the transfer resolution payload and provider thread refs. + +## Commands + +Add: + +```ts +type ThreadForkCommand = { + type: "thread.fork"; + commandId: CommandId; + sourceThreadId: ThreadId; + targetThreadId: ThreadId; + sourcePoint: + | { type: "latest_stable" } + | { type: "run"; runId: RunId } + | { type: "checkpoint"; checkpointId: CheckpointId }; + title?: string; + createdAt: string; +}; +``` + +First-slice policy: + +- `latest_stable` resolves to latest completed run checkpoint. +- explicit completed run/checkpoint is accepted. +- active run source is rejected or resolved to latest stable checkpoint; choose one policy and test it. +- target thread starts idle. +- no provider session/thread is created by `thread.fork`. + +## Runtime Hook + +Add a provider-neutral start-context resolver used by run startup: + +```ts +resolveStartContext({ + threadId, + runId, + provider, + modelSelection, + userMessage, +}): Effect +``` + +Resolution order: + +1. No pending transfer targeting the thread/run: normal run start. +2. Pending fork transfer and same provider with native fork support: resolve native fork. +3. Pending transfer requiring portable context: return explicit unsupported/stub result in first slice, or build minimal portable context if cheap. +4. Failed resolution: fail command before provider side effects. + +The resolver should be called before provider thread creation/startTurn so the adapter can decide whether to create, resume, or native-fork provider state. + +## Codex Native Fork Path + +Investigate the app-server native fork API in `effect-codex-app-server` and Codex docs/probes. + +Implementation target: + +- use native fork only when source provider is Codex and source native refs are strong; +- create or bind a new `ProviderThread` for the forked native thread; +- when the source point is an earlier completed Codex provider turn, fork the latest native thread first and then rollback the forked native thread by later terminal provider turns; +- mark `ContextTransfer.status = resolved_native` and then `consumed` when the first run starts; +- preserve source app thread lineage separately from native thread refs. + +If native API details are not clean, land the data model and resolver first with native fork marked unsupported, then add Codex native fork in a follow-up. + +## Portable Context Fallback + +The first slice should define the fallback boundary even if it does not fully implement high-quality summarization. + +Minimum viable fallback: + +- detect cross-provider fork/handoff need; +- create failed/unsupported transfer resolution with a clear command error, or +- generate a deterministic minimal context handoff from app projection if we want a usable MVP. + +Do not silently concatenate hidden summaries into the user message without a `ContextTransfer`/`ContextHandoff` record. + +## Merge-Back Preparation + +Do not implement merge-back in the first slice, but keep model support: + +- `ContextTransfer.type = "merge_back"` +- `basePoint` on transfer +- source thread may differ from target thread + +The later merge-back command should create a transfer from fork latest stable point back to source thread and consume it with the next user message. + +## Subagent Preparation + +Do not implement subagents in this slice, but keep model support: + +- `createdBy: "agent"` +- `type: "subagent_spawn"` and `"subagent_result"` +- source/target thread relationship fields + +Native subagent ingestion can later map provider-native child refs into the same graph. Cross-provider app-owned subagents can later create child app threads using the same transfer primitive. + +## Projection And Debugger + +Backend projection first: + +- expose thread lineage in thread projection; +- expose context transfers in debug projection; +- expose transfer status and resolution strategy. + +Debugger after backend: + +- fork button/control from stable source point; +- show thread lineage/source point; +- show pending/resolved/failed transfers; +- show native vs portable resolution. + +Do not build debugger controls before the backend projection is authoritative. + +## Tests + +Use `bun run test`, not `bun test`. + +Required backend tests: + +- `thread.fork` from latest completed checkpoint creates target thread and pending transfer. +- fork command is idempotent through command receipts. +- fork does not create provider session/thread eagerly. +- first run on same-provider fork invokes native resolution path when capability exists. +- unsupported native fork falls back or fails according to explicit policy. +- active-run fork policy is deterministic. +- replay/recovery preserves target thread lineage and pending transfer. + +Add test provider layers only at external service boundaries. Do not mock core orchestration policy. + +## Implementation Order + +1. Add contract schemas and ids for context transfers/source points. +2. Add migration/projection table for context transfers. +3. Add event store/projection handling for transfer events. +4. Add `thread.fork` command policy and orchestrator path. +5. Add tests for cheap idle fork creation and recovery. +6. Add start-context resolver interface. +7. Wire resolver into run startup with no-op behavior. +8. Add Codex native fork capability investigation and adapter path. +9. Add tests for same-provider native fork resolution. +10. Add debug projection surface after backend behavior is stable. + +## Validation + +Before considering the slice complete: + +- `bun fmt` +- `bun lint` +- `bun typecheck` +- targeted backend tests with `bun run test` + +If backend code changes are included, tests must cover the new orchestration behavior before the task is considered done. + +## Implementation Status + +Implemented in this slice: + +- context transfer ids, schemas, JSON codecs, events, and projection storage +- app-thread lineage on thread creation and fork creation +- `thread.fork` command using `latest_stable`, explicit run, or explicit checkpoint source points +- cheap idle fork creation with no eager provider session/thread/run +- Codex same-provider native `thread/fork` resolution on first target dispatch +- Codex native fork-from-earlier-run resolution with `thread/fork` plus fork-local `thread/rollback` +- transfer status progression through `pending`, `resolved_native`, and `consumed` +- command receipt idempotency for duplicate fork commands +- replay-backed integration coverage for lazy fork creation and native Codex fork consumption + +Explicitly deferred: + +- portable `ContextHandoff` materialization for cross-provider forks/provider handoff +- merge-back command/runtime +- subagent spawn/result runtime +- debugger controls for creating/inspecting transfers diff --git a/.plans/21-orchestration-v2-application-integration.md b/.plans/21-orchestration-v2-application-integration.md new file mode 100644 index 00000000000..b5005f69bc2 --- /dev/null +++ b/.plans/21-orchestration-v2-application-integration.md @@ -0,0 +1,1085 @@ +# Plan: Orchestration V2 Application Integration + +## Summary + +Make Orchestration V2 the only orchestration system used by the server, web app, and mobile app. Preserve reusable platform infrastructure, remove V1 orchestration as each replacement becomes authoritative, and leave legacy persistence untouched until a separate state-migration plan is chosen. + +In this plan, "Orchestration V2" means the agent orchestrator: provider sessions, runs, attempts, runtime requests, and external agent effects. It does not replace the application event-sourcing data plane. Projects and threads remain first-class application aggregates in one durable, globally ordered event source, and the shell remains a projection of that source. + +The in-scope work is split into five sequential architectural shapes: 1, 2, 3, 4.0, and 4.5. The frontend cutover is intentionally divided into Shape 4.0 and Shape 4.5: first restore the existing product on V2, then expose the richer V2-native information. Finish and validate one shape before beginning the next. These shapes are implementation checkpoints only: they exist to keep the work reviewable and easier to reason about, not to define rollout stages or supported compatibility windows. Stage 5 is recorded only as an out-of-scope follow-up boundary. + +There will be no rollout while Shapes 1 through 4.5 are in progress. Shape 4.0 produces an internally usable V2 application, but Shape 4.5 is the completion boundary for the new frontend. Migrating installations that already contain V1 threads is a separate Stage 5 and is explicitly outside this plan. + +Execution rules: + +- Do not dual-write V1 and V2 state. +- Do not mirror production commands into both runtimes. +- Do not write core entity projection tables directly. Project and thread mutations commit application events first; projections are derived transactionally and remain replayable. +- Preserve one application event cursor across project and thread shell changes. +- Shapes 1 and 2 may keep V1 runnable as a development reference while V2 is completed through direct and replay-backed tests; this is not a supported user path or rollout strategy. +- Shape 3 performs the backend hard cut and intentionally breaks the old client protocol. +- Shape 4.0 updates web and mobile to the final protocol while preserving the current product experience and component structure. +- Shape 4.5 enriches that working application with V2-native graph, execution, tool, and context information. +- Do not create a server-side V2-to-V1 compatibility projection. Shape 4.0 may use temporary parity adapters around existing component props, but Shape 4.5 must not grow those adapters into a second complete thread/projection model. +- Keep React bindings platform-owned. Web and mobile may have intentionally similar binding and presentation modules; shared client-runtime ownership applies to projection state and domain semantics, not to React hooks or native UI policy. +- Only the completed Shape 4.5 system is eligible for a fresh-state user-facing release. Shipping an upgrade to an installation with V1 threads additionally requires the separate Stage 5 migration decision. +- Do not drop V1 tables, migrations, provider runtime rows, or attachment files during this plan. +- V1 removal happens inside the shape that replaces it; there is no deferred general cleanup phase. + +## Alignment With The Current Production Path + +At the start of this plan, the application path was V1 at the orchestration boundary. Between completed Shapes 3 and 4.0, the server was V2-only while the ordinary web and mobile clients still called the removed V1 protocol. Shape 4.0 has now closed that temporary gap. Several provider and selection concepts beneath that boundary were already provider-neutral and have been retained. Shape 4.5 must continue reusing them rather than recreating the existing model, mode, and provider-instance UX. + +The historical pre-cut flow was: + +```text +Web/mobile model and mode selection + -> V1 thread command and projection + -> ProviderCommandReactor + -> ProviderService.getByInstance(instanceId) + -> ProviderInstanceRegistry + -> driver-specific legacy adapter +``` + +The current post-Shape-4.0 boundary is: + +```text +ordinary web/mobile state and commands + -> shared client-runtime V2 shell and thread projections + -> final orchestration.* RPCs / V2 application services + -> shared application event transaction + -> Agent Orchestrator V2 + -> provider execution + +V2 debug client + -> direct V2 projection and item inspection + -> reference presentation for Shape 4.5 production enrichment +``` + +Shape 4.0 closed the intentional client/server gap. Shape 4.5 replaces temporary parity-oriented chat shaping with direct, scoped consumption of the complete V2 projection and its server-authoritative visible item sequence. + +Current ownership and intended treatment: + +| Concept | Current ownership | Decision | +| --- | --- | --- | +| `ProviderDriverKind` | Neutral `packages/contracts/src/providerInstance.ts` | Keep unchanged. It identifies the protocol implementation. | +| `ProviderInstanceId` | Neutral `packages/contracts/src/providerInstance.ts` | Keep unchanged. It remains the routing key for configured instances. | +| `ProviderInstanceRegistry` | Server provider platform | Keep its configuration, lifecycle, hot reload, unavailable-instance handling, continuation identity, and instance lookup behavior. Replace its legacy adapter surface with V2 during the backend transition. | +| `ModelSelection` | Physically defined in V1 `packages/contracts/src/orchestration.ts`, but used by V1, V2, providers, web, and mobile | Move essentially unchanged to a neutral provider/model contract. Do not redesign the client-facing shape. | +| `RuntimeMode` | Physically defined in V1 `packages/contracts/src/orchestration.ts` | Move unchanged to a neutral runtime contract. | +| `ProviderInteractionMode` | Physically defined in V1 `packages/contracts/src/orchestration.ts` | Move unchanged to a neutral runtime contract. | +| Model and mode UI | Existing web/mobile production code | Preserve the concepts and controls. Shape 4.0 rewires their commands and state source to V2; Shape 4.5 may enrich the surrounding runtime detail. | +| Instance-aware provider routing | `ProviderService`, `ProviderCommandReactor`, and `ProviderInstanceRegistry` | Preserve the behavior as policy, but replace the V1 service/reactor implementations. | + +`ModelSelection` is already instance-based: + +```ts +{ + instanceId: ProviderInstanceId; + model: string; + options?: ProviderOptionSelections; +} +``` + +Moving this type in Shape 1 is dependency and ownership cleanup required to delete the V1 contract. It is not a new model-selection feature. + +The production provider path already performs useful behavior that V2 must preserve: + +- resolve `modelSelection.instanceId` through the instance registry +- discover the corresponding driver and capabilities +- reject missing or unavailable instances +- decide whether a model can switch within the current session +- restart a session when runtime mode, workspace, instance, or an unsupported model change requires it +- verify continuation compatibility when changing configured instances +- apply interaction mode to each provider turn + +V2 currently diverges from this model: + +- V2's generic `ProviderKind` stores an instance ID on app threads, runs, and attempts, but stores a driver kind on provider sessions, provider threads, events, and native references. +- V2 compares some of these incompatible values directly, which only appears correct for default instances where the driver and instance slugs are identical. +- V2 constructs a separate settings-backed adapter registry instead of consuming one canonical provider-instance lifecycle. +- Some V2 adapter paths still return hardcoded default instance IDs even when created for a custom instance. +- V2 stores model and mode values on thread creation but lacks production thread mutation commands for changing them. +- `message.dispatch` can use a run-specific model selection without updating the thread's default selection. +- `provider.switch` exists in the contract but is not handled by the orchestrator dispatcher. + +Therefore: + +- Shape 1 relocates existing shared contracts, aligns V2 with the existing driver/instance identity model, ports thread mutation semantics, and converges on one provider-instance registry. +- Shape 2 replaces the operational behavior currently hidden in `ProviderCommandReactor` and other V1 reactors with explicit V2 policies, services, and durable effects. +- Neither shape rebuilds the existing model picker, runtime-mode picker, interaction-mode picker, or provider-instance configuration UX. + +## Shape 1: Production-Ready V2 Foundation + +### Goal + +Make V2 contracts, persistence, command handling, projections, and streams stable enough that application services can depend on them without inheriting temporary semantics. + +### 1. Separate shared contracts from V1 + +Move non-orchestration concepts out of `packages/contracts/src/orchestration.ts`: + +- `ModelSelection` +- runtime and interaction modes +- message attachments +- approval and input-request payloads +- project identifiers and metadata +- common timestamps and provenance types + +`ProviderDriverKind` and `ProviderInstanceId` already live in the neutral `providerInstance.ts` contract. Reuse them directly; do not redefine or relocate them as part of this work. + +Use neutral contract modules such as: + +- `provider.ts` +- `projects.ts` +- `messages.ts` +- `runtime.ts` + +V1 may temporarily import these neutral contracts until its removal. New V2 code must not import anything from the V1 orchestration contract. Preserve the current `ModelSelection`, runtime-mode, and interaction-mode wire semantics unless a deliberate final-contract simplification is made while updating all callers together. + +### 2. Align V2 with the existing provider identity model + +The current production provider-instance model is the target. This step fixes V2's inconsistent use of it rather than inventing another identity system. + +Replace ambiguous `provider` fields with explicit routing identity: + +```ts +{ + driver: ProviderDriverKind; + providerInstanceId: ProviderInstanceId; +} +``` + +Keep provider-native identity separate: + +```ts +{ + nativeThreadId: string | null; + nativeTurnId: string | null; +} +``` + +Apply the split consistently to: + +- app threads +- runs and attempts +- provider sessions +- provider threads and turns +- context transfers and handoffs +- adapter resolution +- provider switching + +Verify that multiple instances of the same driver route and resume independently. Remove hardcoded default-instance assumptions from adapters. + +Converge on one provider-instance lifecycle: + +- retain the existing registry's configuration map, scoped instance lifecycle, hot reload, unavailable-instance representation, and continuation identity +- make the final materialized provider instance expose the V2 orchestration adapter +- make `ProviderAdapterRegistryV2` a thin facade over the canonical instance registry or remove it +- remove the duplicate settings watcher and independently materialized V2 instance map +- remove the legacy adapter field when V1 provider execution is disconnected + +Use these field rules: + +- app threads and runs route by `providerInstanceId` +- provider sessions and provider threads carry `providerInstanceId` and `driver` +- provider-native refs carry `driver` plus the native ID and strength +- adapter lookup always uses `providerInstanceId` +- capability and protocol-specific behavior uses `driver` only at adapter and presentation boundaries + +### Provider process residency and thread multiplexing + +Make process sharing a provider capability, not an orchestrator special case: + +- when `supportsMultipleProviderThreadsPerSession` is true, derive one stable provider-session ID per configured provider instance +- when it is false, allocate an isolated provider session per app thread +- Codex enables sharing now; ACP, Claude, Cursor, and OpenCode remain isolated until their adapters are independently proven multiplex-safe +- model selection, cwd, runtime policy, and MCP authorization are applied on thread start/resume/fork rather than process launch +- MCP bearer credentials have no idle or maximum lifetime; reissue, thread detachment, provider-session release, and server shutdown revoke them explicitly +- the manager tracks the loaded native thread configuration per app thread, so reattachment or a model/cwd/policy change reapplies thread-scoped settings without redundantly resuming unchanged threads +- provider-session projections use explicit many-to-many thread bindings +- archiving, deleting, or switching one thread detaches that binding; it does not release a shared runtime used by sibling threads +- the manager owns one consumer of each provider process event stream and broadcasts events to run subscribers +- each run filters broadcast events by app thread, run attempt, provider thread, provider turn, and subagent lineage before ingestion +- reaping uses aggregate activity: a shared process stays resident while any attached thread is active, then closes after the normal idle timeout +- full release remains an internal runtime lifecycle operation for process failure, server shutdown, or idle reap + +This is an orchestration capability even when only one adapter enables it. Adding ACP or Claude sharing later must require only adapter capability and multiplex-safety work, not a new orchestrator path. + +### 3. Complete the V2 command and event vocabulary + +Add V2-native domain commands and events for: + +- archive and unarchive +- delete/tombstone +- thread title and metadata changes +- branch and worktree metadata +- runtime mode +- interaction mode +- model-selection changes +- explicit thread detachment from a provider session +- provider switching + +Extend thread and shell projections with: + +- archive and delete state +- branch and worktree information +- current model and provider instance +- active run status +- pending runtime-request summary +- latest visible message and update timestamp + +These commands commit domain state only. Filesystem, terminal, checkpoint, and provider side effects are implemented as durable effects in Shape 2. + +### 4. Make command commits atomic + +Replace per-event writes with one command transaction containing: + +- all domain events +- all affected projection updates +- the accepted or rejected command receipt +- required durable effect requests +- the final committed event sequence + +Duplicate command IDs must return the stored receipt without reapplying projections or repeating external effects. + +Introduce a durable orchestration-effect outbox for operations such as: + +- provider turn start +- provider interrupt +- runtime-request response +- provider rollback and fork +- provider-session detach +- checkpoint capture +- terminal cleanup +- attachment cleanup + +External effects execute only after their originating domain transaction commits. + +### 5. Fix projection and stream semantics + +Implement: + +- atomic `{ snapshot, sequence }` reads +- paginated event catch-up +- a race-free transition from stored events to live subscription +- shell snapshot cursors +- active, archive, update, and removal shell deltas +- projection schema versions +- projection replay and rebuild verification +- consistent thread `updatedAt` updates + +Replace adapter-generated turn-item positions with a durable per-thread allocator. The first event for an item allocates its position; later updates retain it. + +### 6. Decompose the orchestrator + +Use the existing intended service boundaries instead of continuing to grow `Orchestrator.ts`: + +- implement `ProviderSwitchService` +- implement `ThreadForkService` around existing fork and merge behavior +- implement `RuntimeRequestService` +- extract command transaction planning from effect execution +- delete `CorrelationStore` if no concrete consumer emerges + +The orchestrator coordinates command handlers and transaction commits. Provider processes, filesystem work, and other external effects remain outside the domain transaction. + +### Shape 1 tests + +Add coverage for: + +- two custom instances of the same provider driver +- accepted and rejected command idempotency +- crash or failure during multi-event command commits +- crash after commit but before provider-effect execution +- duplicate effect-worker claims +- snapshot/subscription races +- catch-up exceeding the current event-read limit +- shell archive and deletion deltas +- more than 100 turn items in one run +- projection replay equivalence +- lifecycle command projection behavior +- capability-driven shared versus isolated provider-session identity +- two concurrent app threads sharing one Codex runtime without cross-run event ingestion +- detaching one thread without closing the sibling runtime, followed by idle reap after the last detach + +### Shape 1 exit gate + +- Custom provider instances route correctly. +- Command retries cannot duplicate provider effects. +- Fault injection cannot produce partial projections. +- Snapshot reconnect tests prove no missing or duplicated events. +- Turn-item ordering is collision-free. +- Projection rebuild produces the same state as incremental projection. +- Lifecycle commands work through direct V2 tests. +- No new V2 code imports V1 orchestration contracts. +- V1 remains the only production application writer; no dual-write path exists. +- `vp check`, `vp run typecheck`, and `vp test` pass. + +## Shape 2: Application Services Around V2 + +Implementation status: complete. Shape 3 completed the backend protocol cut; the production web/mobile cutover was subsequently completed in Shape 4.0. + +### Goal + +Recreate the application behavior currently hidden in V1 reactors and WebSocket handlers using explicit V2 services and durable workflows. + +### 1. Project domain service + +Create a `ProjectService` outside the agent orchestrator that owns project validation and application commands for: + +- create, update, and delete +- lookup by ID and workspace root +- project lookup and snapshot queries over the event-derived projection +- repository identity +- setup-script configuration +- favicon metadata +- startup auto-bootstrap +- project event planning + +Reuse `projection_projects` initially instead of migrating project data. Project commands are application-domain commands, not agent-orchestrator commands, but they still commit to the shared application event source. `ProjectService` must not mutate `projection_projects` directly or own a standalone in-memory change stream. + +Reuse existing project infrastructure: + +- `ProjectSetupScriptRunner` +- `RepositoryIdentityResolver` +- `ProjectFaviconResolver` +- workspace and VCS services + +Update project CLI operations to target this service in tests, but do not switch the production route until Shape 3. + +### 2. Thread launch workflow + +Create `ThreadLaunchService` with an input equivalent to: + +```ts +{ + commandId: CommandId; + projectId: ProjectId; + title: string; + modelSelection: ModelSelection; + runtimeMode: RuntimeMode; + interactionMode: InteractionMode; + workspaceStrategy: WorkspaceStrategy; + initialMessage?: InitialMessage; +} +``` + +The service owns: + +1. Project resolution and validation. +2. Worktree and branch provisioning. +3. Setup-script execution. +4. V2 `thread.create` dispatch. +5. Optional initial-message dispatch. +6. First-turn title and branch generation. +7. Compensation for partially completed launches. + +Launches involving filesystem work need a durable workflow record so a restart can resume or compensate instead of leaving orphaned worktrees. + +Reuse: + +- Git and worktree services +- `ProjectSetupScriptRunner` +- text generation +- repository identity +- VCS status broadcasting + +Do not reuse the V1 WebSocket bootstrap as an orchestration layer. + +### 3. Thread lifecycle service + +Create `ThreadLifecycleService` around the Shape 1 lifecycle commands. + +Archive behavior: + +- commit archive state +- enqueue detachment from live provider sessions +- enqueue terminal closure +- remove the thread from the active shell projection + +Unarchive behavior: + +- commit active state +- restore it to the active shell projection +- do not open a provider session eagerly + +Delete behavior: + +- commit a tombstone first +- cancel queued or running work +- detach the deleted thread from provider sessions +- close terminal history according to current product semantics +- clean unreferenced attachments +- revoke MCP credentials +- remove the thread from active and archived shell projections + +Metadata and mode changes should be pure V2 commands without hidden V1 events. + +### 4. Run finalization service + +Create `RunFinalizationService` to replace the V1 checkpoint reactor. + +On root-run start: + +- establish the checkpoint scope +- capture or verify the pre-run baseline + +On root-run completion: + +- capture the filesystem checkpoint +- calculate the file summary +- commit checkpoint projection events +- refresh workspace entries +- refresh VCS status +- trigger relevant diff and index updates + +Effect IDs must be deterministic per run and scope so replay or restart cannot create duplicate checkpoints. Child or subagent completion must not finalize the root run. + +Keep and reuse: + +- `CheckpointStore` +- checkpoint diff parsing +- VCS and Git infrastructure +- workspace indexing infrastructure + +### 5. Provider session transition policy + +Extract the useful decision logic currently embedded in `ProviderCommandReactor` into a pure V2 `ProviderSessionTransitionPolicy`. + +Inputs should include: + +- current thread and active provider-thread state +- current provider session, if any +- requested `ModelSelection` +- requested runtime and interaction modes +- target workspace +- current and target provider instance metadata +- provider capabilities and continuation identity + +The policy should return an explicit decision: + +```ts +type ProviderSessionTransition = + | { type: "reuse" } + | { type: "switch_model_in_session" } + | { type: "restart_and_resume" } + | { type: "create_with_handoff" } + | { type: "reject"; reason: string }; +``` + +Required semantics: + +- reuse a compatible live session when nothing relevant changed +- switch models in-session only when the adapter advertises support +- restart and resume when runtime mode, workspace, or unsupported model changes require it +- treat a different configured instance as an instance transition even when the driver is unchanged +- use continuation identity to determine whether native resume state is compatible +- use explicit context handoff for cross-driver or incompatible-instance transitions +- apply interaction mode to the next run without treating it as provider identity +- never compare a driver kind directly with a provider instance ID + +`ProviderSessionManagerV2` and the durable effect worker execute the selected transition. Do not retain `ProviderCommandReactor` as an intermediary. + +### 6. Provider runtime recovery + +Create `ProviderRuntimeRecoveryService` and run it before command readiness. + +It must: + +- reconcile provider sessions left active by a crash +- expire or cancel orphaned runtime requests +- reclaim pending or running outbox effects +- resume provider threads where supported +- retry recoverable attempts within policy +- terminalize unrecoverable runs +- release idle sessions +- prevent one logical effect from executing concurrently twice + +Recovery decisions must be driven by capabilities and runtime policy, not provider-name checks. + +### 7. Portable provider fallback + +Complete portable context handoff before V2 becomes the application runtime: + +- use native resume when a strong provider reference exists +- create a replacement provider thread when resume fails +- inject portable history exactly once +- record the handoff explicitly +- keep rollback and fork capability-gated when correlations are weak + +This is also the continuation mechanism a future legacy importer can reuse. + +### Shape 2 tests + +Direct service-level integration tests must cover: + +- project create, update, lookup, and delete +- root-workspace and worktree thread launch +- setup-script success and failure compensation +- first-run title and branch generation +- model changes supported in-session +- model changes requiring session restart +- runtime-mode changes requiring session restart +- interaction-mode changes applied to the next run +- same-driver compatible instance transitions +- incompatible and cross-driver instance transitions using handoff +- send, queue, steer, restart, reorder, and interrupt +- approval and user-input response +- archive, unarchive, and delete cleanup +- checkpoint capture, diff, and rollback +- process restart during each durable external effect +- provider-native resume +- failed native resume followed by portable fallback + +### Shape 2 exit gate + +- Application services reproduce all required V1 product behavior without V1 orchestration dependencies. +- Provider session transition decisions preserve current production model, mode, workspace, and instance-routing semantics. +- External side effects are durable and idempotent. +- Restart recovery leaves no permanently active run or request. +- Portable fallback can continue a thread after native resume failure. +- No WebSocket or production client cutover has occurred yet. +- `vp check`, `vp run typecheck`, and `vp test` pass. + +## Shape 3: V2-Only Backend Ownership + +Implementation status: complete. The revision removes the V1 agent runtime, restores the existing application event-sourcing data plane, and installs V2 behind it instead of replacing it. + +### Goal + +Make V2 the only live agent orchestration runtime while preserving the event-sourced application backend. + +This shape intentionally breaks the old client protocol. That is acceptable because the shapes are implementation chunks and there are no users or releases between them. + +### 1. Production orchestration gateway + +Replace V1 and debug V2 endpoints with one production API containing: + +- one shell snapshot and subscription containing projects, active threads, and archived threads +- archived-thread query and subscription +- full thread projection snapshot and subscription +- thread launch +- message dispatch +- queue, reorder, and promote +- interrupt, restart, and steer +- runtime-request response +- metadata and mode updates +- archive, unarchive, and delete +- rollback +- fork and merge-back +- provider switch +- provider-session detach + +Use final unversioned method names. Do not retain compatibility aliases. + +The shell stream uses one durable application-event cursor. Project and thread deltas cannot be split into independently ordered streams. + +The project HTTP API may remain as a CLI command/query transport, but it must call the same event-sourced `ProjectService`. It is not a second project state source, and the WebSocket protocol must not expose a separate project snapshot or subscription. + +Move the debug route's projection reducer into a shared pure client-runtime module. Backend behavior must not depend on debugger state. + +### 2. Replace server runtime composition + +In `apps/server/src/server.ts`: + +- remove the V1 agent reactors and provider runtime from the composition, but retain the existing `OrchestrationLayerLive` application event/projection infrastructure +- retain `orchestration_events` and `orchestration_command_receipts` as the generic application event log and receipt store; their historical names do not make them V1-only tables +- migrate the isolated `orchestration_v2_events` and `orchestration_v2_command_receipts` rows into that shared log, then treat the V2-specific tables as read-only legacy storage +- preserve or rename the generic projection, replay, and shell-query infrastructure +- remove only V1 agent/provider reactor composition +- remove the V1 provider runtime layer +- install V2 application services +- install the durable effect worker +- install runtime recovery +- retain shared SQLite, provider-instance, VCS, terminal, workspace, project, and diagnostic infrastructure + +In `apps/server/src/serverRuntimeStartup.ts`, establish this startup order: + +1. Start settings and platform services. +2. Verify or rebuild V2 projections when required. +3. Run provider and durable-effect recovery. +4. Start effect workers. +5. Auto-bootstrap the standalone project domain. +6. Signal command readiness. +7. Publish welcome and readiness events. + +No V1 reactor or provider-session reaper starts. + +The resulting ownership boundary is: + +```text +application commands + -> application event transaction + -> project/shell/thread projections + -> Agent Orchestrator V2 durable effects + -> provider execution + -> additional application events +``` + +The event transaction remains responsible for ordered append, command idempotency, projection updates, effect-outbox enqueue, and the committed sequence. + +This is an integration, not a replacement data plane. Restore and adapt the existing components: + +- `OrchestrationEventStore` remains the one durable event source and gains V2 thread-event codecs; V2 `EventStore` is an adapter over it, not a second SQL implementation. +- `OrchestrationCommandReceiptRepository` remains the command-idempotency store; the V2 receipt service adapts to it. +- `OrchestrationEngine` retains its serialized project-command transaction, decider, and projector path. Its production command surface is narrowed to project commands instead of restoring V1 agent execution. +- `OrchestrationProjectionPipeline` remains responsible for project projection replay and cursor advancement. +- `ProjectionSnapshotQuery` remains the source of project shell rows and is composed with V2 thread projections for the unified shell. + +Do not introduce parallel `ApplicationEventStore`, `ProjectProjection`, or `ApplicationShellService` replacements for these components. + +### 3. Update backend consumers + +Rewrite or redirect: + +- `CheckpointDiffQuery` to V2 checkpoint scopes and runs +- `AgentAwarenessRelay` to V2 shell, run, and runtime-request state +- project CLI to `ProjectService` +- startup heartbeat to `ProjectService` and V2 projection counts +- welcome auto-bootstrap to `ProjectService` and `ThreadLaunchService` +- orchestration HTTP API to the new gateway, or remove it when redundant +- MCP orchestration to the production `ThreadManagementService` + +### 4. Remove V1 server code + +Delete: + +- V1-only files under `apps/server/src/orchestration/**`, while keeping the existing event transaction, project decider/projector, projection pipeline, and snapshot query +- V1 orchestration HTTP routes +- V1 WebSocket handlers +- V1 reactor and startup wiring +- V1 projection queries +- V1 provider command and runtime ingestion +- V1 provider session directory and reaper +- V1 high-level provider adapters and services after import analysis confirms no remaining consumers +- corresponding tests and duplicate service interfaces + +Do not delete infrastructure merely because it lived under the old `orchestration` namespace. Retain or relocate code whose responsibility is generic application event append/replay, command receipts, project/shell projection, cursor catch-up, or projection rebuild. + +Retain low-level provider infrastructure imported by V2, including provider instance configuration, SDK and ACP transports, environment setup, logging, and runtime utilities. + +Add an import-boundary test preventing production code from importing the deleted V1 agent reactors and provider services. Imports of the retained application event/projection services are expected. + +### 5. Preserve legacy storage + +Do not drop: + +- V1 event or projection tables +- old migrations +- provider-session runtime rows +- existing attachment files + +Move only the minimal V1 projection decoders potentially needed by migration into an isolated `legacy/orchestration-v1` module. That module must not be installed in the server runtime layer and must have no write API. + +### Shape 3 tests + +Add backend integration coverage for: + +- startup ordering and command readiness +- V2-only provider session creation +- shell and thread snapshot reconnects +- project and thread changes sharing one ordered shell cursor +- project command idempotency and projection replay +- proof that project commands cannot bypass the application event store +- CLI project operations +- MCP send, wait, interrupt, fork, and merge flows +- agent-awareness output +- checkpoint diff queries +- terminal and attachment cleanup +- server shutdown and restart +- static import-boundary enforcement + +### Shape 3 exit gate + +- Server startup does not construct any V1 orchestration or provider runtime. +- Every production orchestration endpoint targets V2. +- Projects and threads are committed through one application event source and exposed through one shell stream. +- `projection_projects` and core thread projections are only changed by event projection/rebuild code. +- CLI, MCP, checkpoints, awareness, startup, and telemetry use V2. +- No production import references the deleted V1 agent reactors, provider runtime ingestion, provider command handling, or session-reaper services. +- New writes go only to the retained application event/receipt tables and event-derived projections; the retired V1-only projection/runtime tables and isolated `orchestration_v2_*` event/receipt tables remain untouched. +- Backend integration and restart tests pass. +- Old clients are expected to be incompatible at this boundary. +- `vp check`, `vp run typecheck`, and `vp test` pass. + +## Shape 4.0: V2 Frontend Cutover And Product Parity + +Implementation status: complete. + +Implementation outcome: + +- Web and mobile now consume the unified V2 shell and full V2 thread projections through the shared client runtime, including reconnect/cursor handling and versioned V2-only caches. +- Ordinary project, thread, message, run-control, runtime-request, settings, archive, delete, checkpoint, attachment, and plan-implementation commands now use the final event-sourced project and V2 orchestration transports. +- Existing root-workspace, newly provisioned worktree, origin-based worktree, and already-prepared worktree launch flows are preserved, including promotion of an empty server thread into a worktree before its first message. +- Existing web and mobile product components consume V2-native parity models. The complete projection and structured turn-item payload remain available, while unsupported rich V2 visualization stays deferred to Shape 4.5. +- The parity models are an explicit temporary bridge for existing product components. They are not the intended Shape 4.5 thread API and may be removed incrementally as direct item consumers replace them. +- Production V1 client reducers, subscriptions, command paths, and persistence writers have been removed. Legacy server state remains untouched for the separate Stage 5 migration decision. + +### Goal + +Make the existing web and mobile application work end to end against the V2-only backend. Preserve the current product structure and the frontend pieces that already work well. This shape changes the source of truth and command path; it is not a broad visual redesign. + +At the end of Shape 4.0, the ordinary application—not only the V2 debug route—must be usable for normal project and thread work on fresh V2 state. + +### Shape 4.0 boundary + +- Treat the current V1 frontend as the behavioral and interaction reference, not as a data-model contract. +- Reuse existing sidebar, chat, composer, approval, archive, diff, settings, and model-picker components where their behavior remains correct. +- Generalize component props around V2-native view models instead of manufacturing V1 commands, events, activities, or thread projections. +- Retain the complete V2 projection in client state even when the parity UI initially presents only a subset of it. +- Preserve shell and thread-detail state as separately subscribable streams. Shape 4.0 components may compose them locally where required, but the client runtime must not make detail traffic a dependency of shell/sidebar state. +- Keep web and mobile React bindings separate. Similar binding code is intentional and is not a Shape 4.0 deduplication target. +- Provide a safe generic renderer for V2 turn-item types that do not yet have a rich presentation. Do not drop structured tool or execution data merely because Shape 4.5 owns the final renderer. +- Defer graph visualization, rich execution inspection, and provider-native tool presentation to Shape 4.5. +- Do not import V1 server rows or migrate existing V1 threads in this shape. + +### 1. Cut client-runtime state over to V2 + +Replace the production V1 shell and thread stores with V2-backed state for: + +- the unified project, active-thread, and archived-thread shell +- full V2 thread projections +- shell and thread committed cursors +- snapshot-to-live catch-up +- connection, synchronization, reconnect, and error state +- optimistic command state only where the committed receipt is not fast enough for interaction feedback + +The server's committed application sequence is the shell reconnect boundary. A thread subscription uses its committed V2 event sequence. Connection readiness must not be reported until the initial snapshot and replay window have been applied. + +Bump web and mobile orchestration-cache versions. Discard cached V1 shell/thread projections while preserving drafts, credentials, environment configuration, preferences, and unrelated local state. Cache invalidation is not the Stage 5 server-state migration. + +### 2. Add a temporary parity-focused V2 presentation adapter + +Create shared, V2-native client view models for the existing application surfaces, including: + +- project and thread shell rows +- conversation messages +- basic reasoning and work-log entries +- generic tool/execution entries with status, summary, and retained structured payload +- active-run and queued-run state +- pending approval and user-input requests +- plan and checkpoint summaries +- thread capabilities and available actions + +Derive these models from the V2 thread projection, `visibleTurnItems`, runs, attempts, runtime requests, and checkpoints. Existing components may be adapted to consume these models, but the adapter must not recreate an `OrchestrationThread`, `OrchestrationThreadActivity`, V1 `latestTurn`, or a V1 event stream. + +Web and mobile share the projection reducer and retain the complete structured V2 data. Platform-specific components may render temporary parity views differently. Shape 4.5 owns replacing full-thread parity shaping with direct, scoped item consumption; Shape 4.0 does not need to pre-design that final API. + +### 3. Replace production client commands + +Move all ordinary app operations off `orchestrationV1.*`: + +- route project create, update, and delete through the existing event-sourced `ProjectService` transport +- launch root-workspace and worktree threads through `ThreadLaunchService` +- send messages with explicit auto, queue, steer, or restart behavior +- interrupt active runs +- respond to approvals and user-input requests +- update title, branch/worktree metadata, model selection, runtime mode, and interaction mode +- archive, unarchive, and delete threads +- request checkpoint rollback +- retain current project setup-script and new-thread behavior + +Use V2 command IDs and receipt semantics directly. Retrying a client command must not duplicate messages, runs, external effects, or project mutations. + +Commands that only exist to expose new V2 capabilities—fork, merge-back, provider switch, provider-session detach, advanced queue reordering, and graph navigation—may remain debug-only until Shape 4.5 unless they are needed to preserve an existing production workflow. + +### 4. Restore web product behavior + +Rewire the existing web application for: + +- project grouping and thread navigation in the sidebar +- project creation, editing, and deletion +- new-thread creation for root workspaces and worktrees +- opening and continuing a thread +- message rendering and composer submission +- queued-message state and active-run controls +- approval and user-input controls +- plan display and implementation entry points +- diff, checkpoint, and rollback views +- archive, unarchive, and deletion flows +- provider, model, runtime-mode, and interaction-mode controls +- reconnecting while a run is active + +Preserve the current layout and interaction patterns unless a V2 semantic makes the old behavior incorrect. Keep the V2 debug route available during this shape as an inspection oracle, but production behavior must not depend on it. + +### 5. Restore mobile product behavior + +Rewire the existing mobile application for: + +- project and thread lists +- new-task creation +- thread detail and message timeline +- composer and queued-message behavior +- run status and interruption +- approval and user-input interactions +- archive management +- diff and Git actions +- provider, model, runtime-mode, and interaction-mode selectors +- reconnecting and restoring the selected thread + +Mobile consumes the same V2 state and parity derivation as web. Do not maintain a second V1-shaped mobile reducer. + +### 6. Remove production V1 client dependencies + +After web and mobile are cut over: + +- remove production calls to `ORCHESTRATION_WS_METHODS` and every `orchestrationV1.*` method +- remove V1 shell/thread subscriptions, command builders, reducers, and persistence writers +- remove V1 types from production component props and application state +- keep any still-needed legacy schemas isolated and read-only for the future Stage 5 analysis +- do not delete retained application project/event contracts merely because their historical module name contains `orchestration` + +### Shape 4.0 tests + +Add shared client-runtime coverage for: + +- unified shell snapshot and ordered project/thread deltas +- incremental V2 thread projection updates +- shell and thread reconnect from persisted cursors +- cache-version invalidation from V1 to V2 +- duplicate command delivery and receipt handling +- active, queued, steering, restarted, interrupted, and terminal run state +- approval and user-input requests across reconnect +- generic fallback presentation for every unrecognized or not-yet-enriched turn item + +Add web and mobile integration coverage for: + +- project and thread creation +- opening, sending to, and continuing a thread +- root-workspace and worktree launch +- active-run interaction and interrupt +- archive, unarchive, and delete +- provider/model and mode selection +- approvals and input requests +- checkpoint diff and rollback +- reconnect during a live run + +### Shape 4.0 exit gate + +- The ordinary web and mobile applications use V2 shell, thread, and command APIs exclusively. +- A user can create a project and thread, converse with an agent, reconnect, respond to requests, inspect diffs, archive, and delete. +- Existing V1-era product behavior that remains supported by V2 is present without routing through V1 data shapes. +- Full V2 projections and structured turn-item payloads are retained even when rendered generically. +- Server-projected `visibleTurnItems`, including inherited and synthetic rows, remain intact in client state even when Shape 4.0 surfaces do not render them richly. +- Shell/sidebar state remains independently subscribable from full thread detail. +- Temporary full-thread parity models and separate web/mobile React binding modules are allowed at this boundary; their final cleanup belongs to Shape 4.5. +- No production code calls an `orchestrationV1.*` endpoint. +- No V2-to-V1 server or persistence compatibility projection exists. +- The V2 debug route is no longer the only functional V2 client. +- `vp check`, `vp run typecheck`, and `vp test` pass. +- `vp run lint:mobile` passes for mobile changes. + +## Shape 4.5: V2-Native Frontend Enrichment + +Implementation status: in progress. Sections 1 and 2 are implemented for web; mobile cutover and the deeper supporting-state, graph, workflow, and parity-removal sections remain. + +### Goal + +Enrich the now-working application with information and workflows that V2 models explicitly but the V1 UI could not represent well. The production chat must move from the temporary Shape 4.0 parity reconstruction to the server-authoritative ordered V2 item sequence without introducing a second complete projection or a shared React presentation layer. + +The V2 projection remains the client semantic source of truth. Client enrichment adds environment scope and narrowly derived relationships; it does not copy every projection table into another thread-shaped model. + +### Shape 4.5 implementation order + +Complete the following sections in order. The client-runtime substrate is intentionally small; web proves the first production presentation, mobile follows from the same projection semantics, and only then are demonstrated shared domain selectors extracted. + +### 1. Establish the scoped projection and item atom boundary + +Keep shell and detail as independent sources: + +- `EnvironmentThreadShell` remains the environment-scoped, list-oriented view of the server shell stream. +- Introduce a detail value containing `environmentId` and the pristine `OrchestrationV2ThreadProjection`. Use a temporary migration name such as `ScopedThreadProjection` while the Shape 4.0 parity facade still owns `EnvironmentThread`; after that facade is removed, the contextual wrapper may take the unversioned `EnvironmentThread` name. +- Preserve `wrapper.projection === projectionState.data` and the identities of every nested projection collection. Do not spread or translate the complete projection into a second object graph. +- Do not attach `environmentId` to every nested message, run, node, or item. Values that leave their scoped thread context or participate in cross-thread relationships must carry an explicit `ScopedThreadRef` or equivalent environment-qualified reference. +- Shell consumers read shell atoms. Detail consumers read `projection.thread` and detail atoms. Do not synthesize a shell from detail, globally merge the streams, or make sidebar state depend on detail traffic. A component that truly needs both subscribes to both explicitly. + +The stable client-runtime access surface begins with: + +- the full thread state atom, including cached, synchronizing, live, deleted, and error state +- a scoped whole-thread atom for debugger, graph, and other whole-projection consumers +- a `visibleTurnItems` atom exposing the server-projected ordered item rows +- status/error or an equivalently stable synchronization atom that does not invalidate merely because projection content changed + +`visibleTurnItems` is the production conversation source. It already carries the discriminated union for messages, reasoning, plans, tools, approvals, input requests, checkpoints, interruption request/result items, compaction, handoff, fork, subagent, and dynamic tools. It also carries local, inherited, and synthetic visibility metadata. Production chat must not reconstruct this sequence by merging separate messages, plans, and work-entry arrays. + +Do not publish one atom merely because each projection table exists. Add a derived atom or an entity-by-ID lookup only when a concrete row, control, graph, or inspector needs state absent from its turn item, for example: + +- runtime-request response capability for an approval or user-input row +- current checkpoint/ref state for rollback +- run, attempt, or node state for execution inspection +- context-transfer lifecycle for merge-back or provider handoff + +These lookups must retain the original V2 entity types and identities. A whole-thread atom is a firehose and must not be the default subscription for granular timeline components. + +Keep web and mobile React bindings separate. Do not introduce a shared React package, a shared binding factory, or a `createClientStateGraph` abstraction merely because the two bindings are currently similar. + +#### `visibleTurnItems` structural-sharing prerequisite + +Fix the incremental client reducer before the production rich timeline depends on it: + +- inherited and synthetic rows remain server-authoritative and are never locally reconstructed +- a run or attempt update that does not change visible membership returns the previous `visibleTurnItems` array reference +- a turn-item update with unchanged membership replaces only the affected row and preserves every unaffected row object and position +- positions are recomputed only when visible membership changes +- membership equality alone must not suppress a streaming item-content update + +This is a reducer allocation and correctness requirement, not an atom-level equality workaround. + +### 2. Move the web conversation to authoritative V2 items + +Implement the first Shape 4.5 production vertical slice in web: + +- render the conversation directly from `visibleTurnItems` +- preserve item order across interruption request, intervening provider activity, and interruption result +- preserve inherited items and synthetic fork markers +- render user and assistant messages, reasoning, plans, tool activity, requests, checkpoints, compaction, handoffs, forks, subagents, and dynamic tools from the discriminated union +- retain complete structured/provider-native input and output behind a safe fallback for every item type +- join a row to targeted supporting state by ID only when the item itself is insufficient for interaction or inspection + +Reuse the proven presentation work in the V2 debugger where it fits, especially its direct `visibleTurnItems` path and item renderers. Do not reuse the debugger's local subscriptions, local projection reducer, log reconstruction fallback, or debug-only state management. Production consumes the client-runtime thread state established in section 1. + +Web owns visual row types, icons, labels, folding, adjacent visual grouping, expansion, and copy behavior. Those policies are not client-runtime domain models. + +### 3. Move the mobile conversation to the same projection semantics + +After the web item path is working, move mobile from its Shape 4.0 parity feed to the same authoritative `visibleTurnItems` sequence and targeted entity lookups. + +Mobile keeps its own React bindings and form-factor-specific presentation, including native list behavior, expansion, composer layout, offline/outbox policy, and navigation. It does not need to share web row interfaces, fold policy, labels, or visual grouping. + +Promote logic into client-runtime only when both platforms need the same nontrivial domain interpretation and the relationship is not already explicit in V2. Examples may include environment-scoped cross-thread references or capability-safe entity linkage. Do not extract presentation logic merely because two current implementations happen to look alike. + +### 4. Enrich tools, execution, and supporting state + +Add item renderers and targeted inspectors for: + +- command execution with input, working directory where available, lifecycle, output, duration, and exit state +- file changes with affected paths and patch/diff navigation +- file, code, and web search with query and result provenance +- MCP, dynamic, and provider-native tool calls with complete structured fallback +- nested tool work associated with execution nodes or subagents +- queued, steering, restarting, active, interrupted, failed, retried, superseded, and recovered execution +- attempt history, execution-node progress, and retry reasons +- provider session/thread/turn identity where diagnostically useful +- native resume versus portable context handoff +- context-window and token indicators +- plans, todos, approvals, user input, checkpoints, and rollback state + +The ordered chat remains item-based. Runs, attempts, nodes, provider state, runtime requests, checkpoints, and transfers are supporting relational state; they enrich specific rows, controls, and inspectors rather than becoming additional reconstructed chat feeds. + +Keep primary chat status simple. Detailed execution state belongs in targeted or expandable surfaces. Rendering must preserve live updates without duplicating rows as attempts or provider items change, and large output must remain inspectable without overwhelming the timeline. + +### 5. Expose relationships and V2-native workflows + +Build environment-scoped, cycle-safe graph/lineage derivation from shell threads, thread lineage, execution nodes, subagents, forks, and context transfers. It must support: + +- root, parent, child, fork, and subagent relationships +- navigation between related threads using environment-qualified references +- per-node run and terminal status +- merge-back and context-transfer state +- archived or deleted related threads +- missing-parent and partial-replay fallback + +Web should provide an efficient tree or graph surface without replacing the ordinary project/thread sidebar. Mobile should expose the same domain relationships through a form-factor-appropriate drill-down rather than copying the desktop visualization. + +Integrate capability-gated workflows for: + +- thread fork and merge-back +- provider switching and provider-session detach +- advanced queue reorder and promotion +- subagent navigation and delegated-task state +- plan/todo progress +- checkpoint scope, diff, and rollback +- context handoff and transfer visibility + +Actions derive from V2 capability and runtime policy. They must not use provider-name conditionals. + +### 6. Retire parity state and finish cleanup + +After web and mobile no longer depend on the Shape 4.0 full-thread parity facade: + +- remove `presentThread`-style whole-projection remapping and the shell/detail reconciliation it required +- give the environment-scoped projection wrapper the final unversioned client name if doing so improves clarity +- remove message/work/plan feed reconstruction and other obsolete transformations from `session-logic.ts`, mobile `threadActivity.ts`, and equivalent modules while retaining genuinely platform-specific presentation helpers +- remove unused V1 client RPC contracts and compatibility exports +- split still-required project application contracts and frozen legacy thread decoders into clear ownership boundaries instead of deleting reusable application infrastructure +- delete the V2 debug route after its useful inspection and rendering surfaces have production homes + +No production client should end with both a complete `OrchestrationV2ThreadProjection` and a second complete thread presentation object containing copied messages, work entries, runs, plans, requests, and checkpoints. + +### Shape 4.5 tests + +Add client-runtime state and reducer coverage for: + +- environment-scoped detail identity without copying or mutating the server projection +- cached, synchronizing, live, deleted, and error state independent of projection content +- preservation of local, inherited, and synthetic `visibleTurnItems` +- unchanged `visibleTurnItems` identity for run/attempt updates that do not change visibility +- single-row replacement and unaffected-row identity for streaming item updates +- membership changes, removal, and position renumbering without losing inherited or synthetic rows +- any targeted entity lookup or shared domain selector introduced by a concrete consumer +- environment-scoped graph construction for roots, forks, subagents, merges, archives, missing nodes, and cycles +- capability-gated action availability + +Add web and mobile integration coverage for: + +- every known V2 turn-item type and the structured dynamic fallback +- interruption request, intervening activity, and interruption result in committed order +- inherited conversation items and synthetic fork markers +- rich tool expansion and live updates without duplicate rows +- request response capability, checkpoint rollback, and other targeted item/entity joins +- related-thread navigation +- fork and merge-back +- subagent and delegated-task inspection +- queue reorder and promotion +- provider switch and detach +- context handoff +- reconnect while item, graph, tool, request, or attempt state is changing +- all existing Shape 4.0 product flows + +### Shape 4.5 exit gate + +- Web and mobile remain fully usable for every Shape 4.0 flow. +- Production web and mobile conversations render from server-authoritative `visibleTurnItems`; neither reconstructs the chat by merging separate message, plan, and work-entry collections. +- Thread detail values carry explicit environment scope while retaining the pristine V2 projection and nested entity identities. +- Shell/sidebar and thread-detail state remain independently subscribable; ordinary detail traffic does not invalidate the project/thread shell. +- Granular timeline components consume the item atom and targeted entity lookups rather than subscribing to the whole projection. +- Run/attempt updates with unchanged visibility preserve `visibleTurnItems` identity, while streaming item updates replace only the affected row and remain visible. +- Local, inherited, and synthetic items survive snapshot, incremental update, cache restore, and reconnect. +- Typed tool calls and structured results are visible without losing provider-native fallback data. +- Interruption request, intervening activity, and interruption result remain independently visible and correctly ordered. +- Runs, attempts, retries, recovery, plans, checkpoints, requests, handoffs, and context transfers have coherent targeted presentation without becoming a second reconstructed chat model. +- Thread lineage, forks, subagents, merge relationships, and environment-qualified navigation are available on web and mobile in platform-appropriate forms. +- New V2 capabilities are capability-gated and do not rely on provider-name conditionals. +- Web and mobile retain separate React bindings and presentation policy while consuming the same V2 projection/reducer semantics. +- No production client maintains a complete parallel thread presentation model beside `OrchestrationV2ThreadProjection`; the Shape 4.0 parity facade is removed from production consumers. +- No production TypeScript import references a V1 client orchestration type or endpoint. +- The V2 debug route is removed after its useful production surfaces are integrated. +- `vp check`, `vp run typecheck`, and `vp test` pass. +- `vp run lint:mobile` passes for mobile changes. + +## Stage 5: Existing-User State Migration (Out Of Scope) + +Stage 5 is deliberately not implemented or decided by this plan. Shapes 4.0 and 4.5 target a fully working V2 application on fresh V2 state while preserving legacy rows and files untouched. + +A later migration plan must separately decide: + +- which V1 thread, message, project, checkpoint, attachment, and provider-session state is imported +- how provider continuation identifiers are validated and mapped for native resume +- what fidelity is required for historical activities and tool calls that have no exact V2 representation +- whether migration is eager, lazy-on-open, or an explicit user action +- backup, rollback, idempotency, partial-failure, and retry behavior +- how migrated state is verified before legacy storage becomes removable + +Nothing in Shape 4.0 or 4.5 may destroy, rewrite, or make assumptions that close off those options. Shipping an upgrade to an installation containing V1 threads requires a separate Stage 5 decision and validation gate. + +## Final Outcome + +At the end of Shape 4.5: + +- V2 is the only live agent orchestration system from provider process through web and mobile UI. +- The existing product experience works on V2, and production conversations render the authoritative environment-scoped V2 item sequence without a parallel full-thread presentation model. +- V2-native graph, execution, tool, request, interruption, and context information is available through targeted platform-appropriate surfaces. +- V1 runtime, server endpoints, production client state, and production client operations are removed. +- Reusable application event-sourcing and platform infrastructure remain. +- Legacy database rows, provider continuation data, attachments, and other migration inputs remain untouched and read-only. +- Fresh V2 state is fully supported; migration of existing V1 user state remains the explicitly separate Stage 5. diff --git a/.plans/README.md b/.plans/README.md index 379158d4efd..b2c6d025036 100644 --- a/.plans/README.md +++ b/.plans/README.md @@ -12,3 +12,4 @@ 10. `10-unify-process-session-abstraction.md` 19. `19-version-control-phase-1-vcs-driver-foundation.md` 20. `20-version-control-phase-2-source-control-provider-foundation.md` +21. `21-orchestration-v2-application-integration.md` diff --git a/apps/desktop/src/backend/tailscaleEndpointProvider.ts b/apps/desktop/src/backend/tailscaleEndpointProvider.ts index 0b48adc308c..4f53a7e6f54 100644 --- a/apps/desktop/src/backend/tailscaleEndpointProvider.ts +++ b/apps/desktop/src/backend/tailscaleEndpointProvider.ts @@ -121,7 +121,7 @@ export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAd input.readMagicDnsName ?? readTailscaleStatus.pipe( Effect.map((status) => status.magicDnsName), - Effect.orElseSucceed(() => null), + Effect.orElseSucceed((): string | null => null), ); const dnsName = input.statusJson === undefined diff --git a/apps/mobile/src/connection/storage.ts b/apps/mobile/src/connection/storage.ts index 276ea3c5c08..1c3bead420e 100644 --- a/apps/mobile/src/connection/storage.ts +++ b/apps/mobile/src/connection/storage.ts @@ -3,6 +3,10 @@ import { ConnectionRegistrationStore, ConnectionTargetStore, EnvironmentCacheStore, + ORCHESTRATION_CACHE_SCHEMA_VERSION, + StoredOrchestrationShellSnapshot, + StoredOrchestrationThreadSnapshot, + decodeOrDiscardOrchestrationCache, registerConnectionInCatalog, removeConnectionFromCatalog, removeCatalogValue, @@ -14,12 +18,7 @@ import { CredentialStore, ProfileStore, } from "@t3tools/client-runtime/connection"; -import { - EnvironmentId, - OrchestrationThread, - OrchestrationShellSnapshot, - ThreadId, -} from "@t3tools/contracts"; +import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -29,31 +28,16 @@ import * as SecureStore from "expo-secure-store"; import { makeCatalogStore, type SecureCatalogStorage } from "./catalog-store"; -const SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION = 1; const SHELL_SNAPSHOT_CACHE_DIRECTORY = "connection-shell-snapshots"; const LEGACY_SHELL_SNAPSHOT_CACHE_DIRECTORY = "shell-snapshots"; -const THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION = 1; const THREAD_SNAPSHOT_CACHE_DIRECTORY = "connection-thread-snapshots"; -const StoredShellSnapshot = Schema.Struct({ - schemaVersion: Schema.Literal(SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION), - environmentId: EnvironmentId, - snapshot: OrchestrationShellSnapshot, -}); - -const StoredThreadSnapshot = Schema.Struct({ - schemaVersion: Schema.Literal(THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION), - environmentId: EnvironmentId, - threadId: ThreadId, - thread: OrchestrationThread, -}); - -const LegacyStoredShellSnapshot = Schema.Struct({ - schemaVersion: Schema.Literal(1), - environmentId: EnvironmentId, - snapshotReceivedAt: Schema.String, - snapshot: OrchestrationShellSnapshot, -}); +const StoredShellSnapshot = StoredOrchestrationShellSnapshot; +const StoredThreadSnapshot = StoredOrchestrationThreadSnapshot; +const decodeStoredShellSnapshot = Schema.decodeUnknownResult(StoredShellSnapshot); +const encodeStoredShellSnapshot = Schema.encodeUnknownResult(StoredShellSnapshot); +const decodeStoredThreadSnapshot = Schema.decodeUnknownResult(StoredThreadSnapshot); +const encodeStoredThreadSnapshot = Schema.encodeUnknownResult(StoredThreadSnapshot); function catalogError(operation: string, cause: unknown) { return new ConnectionTransientError({ @@ -289,44 +273,50 @@ export const connectionStorageLayer = Layer.effectContext( try: () => JSON.parse(raw) as unknown, catch: (cause) => shellPersistenceError("load-shell", cause), }); - const stored = yield* Effect.fromResult( - Schema.decodeUnknownResult(StoredShellSnapshot)(parsed), - ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause))); + const stored = yield* Effect.fromResult(decodeStoredShellSnapshot(parsed)).pipe( + Effect.mapError((cause) => shellPersistenceError("load-shell", cause)), + ); return stored.environmentId === environmentId ? Option.some(stored.snapshot) : Option.none(); } const legacyFile = yield* legacyShellSnapshotFile(environmentId, "load-shell"); - if (!legacyFile.exists) { - return Option.none(); + if (legacyFile.exists) { + yield* Effect.try({ + try: () => legacyFile.delete(), + catch: (cause) => shellPersistenceError("load-shell", cause), + }); } - const legacyRaw = yield* Effect.tryPromise({ - try: () => legacyFile.text(), - catch: (cause) => shellPersistenceError("load-shell", cause), - }); - const legacyParsed = yield* Effect.try({ - try: () => JSON.parse(legacyRaw) as unknown, - catch: (cause) => shellPersistenceError("load-shell", cause), - }); - const legacyStored = yield* Effect.fromResult( - Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed), - ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause))); - return legacyStored.environmentId === environmentId - ? Option.some(legacyStored.snapshot) - : Option.none(); - }), + + return Option.none(); + }).pipe((decode) => + decodeOrDiscardOrchestrationCache( + decode, + shellSnapshotFile(environmentId, "load-shell").pipe( + Effect.flatMap((file) => + Effect.try({ + try: () => { + if (file.exists) file.delete(); + }, + catch: (cause) => shellPersistenceError("load-shell", cause), + }), + ), + Effect.catch(() => Effect.void), + ), + ), + ), saveShell: (environmentId, snapshot) => Effect.gen(function* () { const file = yield* shellSnapshotFile(environmentId, "save-shell"); const stored = { - schemaVersion: SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION, + schemaVersion: ORCHESTRATION_CACHE_SCHEMA_VERSION, environmentId, snapshot, } as const; - const encoded = yield* Effect.fromResult( - Schema.encodeUnknownResult(StoredShellSnapshot)(stored), - ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause))); + const encoded = yield* Effect.fromResult(encodeStoredShellSnapshot(stored)).pipe( + Effect.mapError((cause) => shellPersistenceError("save-shell", cause)), + ); yield* Effect.try({ try: () => { if (!file.exists) { @@ -351,21 +341,36 @@ export const connectionStorageLayer = Layer.effectContext( try: () => JSON.parse(raw) as unknown, catch: (cause) => shellPersistenceError("load-thread", cause), }); - const stored = yield* Effect.fromResult( - Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed), - ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause))); + const stored = yield* Effect.fromResult(decodeStoredThreadSnapshot(parsed)).pipe( + Effect.mapError((cause) => shellPersistenceError("load-thread", cause)), + ); return stored.environmentId === environmentId && stored.threadId === threadId ? Option.some(stored.thread) : Option.none(); - }), + }).pipe((decode) => + decodeOrDiscardOrchestrationCache( + decode, + threadSnapshotFile(environmentId, threadId, "load-thread").pipe( + Effect.flatMap((file) => + Effect.try({ + try: () => { + if (file.exists) file.delete(); + }, + catch: (cause) => shellPersistenceError("load-thread", cause), + }), + ), + Effect.catch(() => Effect.void), + ), + ), + ), saveThread: (environmentId, thread) => Effect.gen(function* () { - const file = yield* threadSnapshotFile(environmentId, thread.id, "save-thread"); + const file = yield* threadSnapshotFile(environmentId, thread.thread.id, "save-thread"); const encoded = yield* Effect.fromResult( - Schema.encodeUnknownResult(StoredThreadSnapshot)({ - schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION, + encodeStoredThreadSnapshot({ + schemaVersion: ORCHESTRATION_CACHE_SCHEMA_VERSION, environmentId, - threadId: thread.id, + threadId: thread.thread.id, thread, }), ).pipe(Effect.mapError((cause) => shellPersistenceError("save-thread", cause))); diff --git a/apps/mobile/src/features/archive/archivedThreadList.test.ts b/apps/mobile/src/features/archive/archivedThreadList.test.ts index 6cd530ab37d..77e45963b83 100644 --- a/apps/mobile/src/features/archive/archivedThreadList.test.ts +++ b/apps/mobile/src/features/archive/archivedThreadList.test.ts @@ -1,9 +1,11 @@ import type { ArchivedSnapshotEntry } from "@t3tools/client-runtime/state/threads"; -import type { OrchestrationProjectShell, OrchestrationThreadShell } from "@t3tools/contracts"; -import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; +import type { OrchestrationProjectShell, OrchestrationV2ThreadShell } from "@t3tools/contracts"; +import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; +import * as DateTime from "effect/DateTime"; import { buildArchivedThreadGroups } from "./archivedThreadList"; +import { makeRawThreadShell } from "../../test-fixtures"; const environmentId = EnvironmentId.make("environment-1"); @@ -22,40 +24,30 @@ function makeProject( } function makeThread( - input: Partial & - Pick, -): OrchestrationThreadShell { - return { - modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - latestTurn: null, - createdAt: "2026-06-01T00:00:00.000Z", - updatedAt: "2026-06-01T00:00:00.000Z", - archivedAt: "2026-06-02T00:00:00.000Z", - session: null, - latestUserMessageAt: null, - hasPendingApprovals: false, - hasPendingUserInput: false, - hasActionableProposedPlan: false, + input: Pick & { + readonly branch?: string | null; + readonly archivedAt?: string | null; + }, +): OrchestrationV2ThreadShell { + const archivedAt = input.archivedAt === undefined ? "2026-06-02T00:00:00.000Z" : input.archivedAt; + return makeRawThreadShell({ ...input, - }; + archivedAt: archivedAt === null ? null : DateTime.makeUnsafe(archivedAt), + }); } function makeSnapshot( projects: ReadonlyArray, - threads: ReadonlyArray, + threads: ReadonlyArray, targetEnvironmentId = environmentId, ): ArchivedSnapshotEntry { return { environmentId: targetEnvironmentId, snapshot: { + schemaVersion: 1, snapshotSequence: 1, projects, threads, - updatedAt: "2026-06-04T00:00:00.000Z", }, }; } diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 7ee5660edf1..f1e120a38c8 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -60,14 +60,16 @@ interface HomeScreenProps { /* ─── Status indicator colors ────────────────────────────────────────── */ function statusColors(thread: EnvironmentThreadShell): { bg: string; fg: string } { - switch (thread.session?.status) { + switch (thread.runtime?.status) { case "running": + case "waiting": return { bg: "rgba(249,115,22,0.14)", fg: "#f97316" }; - case "ready": + case "completed": return { bg: "rgba(34,197,94,0.14)", fg: "#22c55e" }; + case "queued": case "starting": return { bg: "rgba(59,130,246,0.14)", fg: "#3b82f6" }; - case "error": + case "failed": return { bg: "rgba(239,68,68,0.14)", fg: "#ef4444" }; default: return { bg: "rgba(163,163,163,0.10)", fg: "#a3a3a3" }; @@ -161,7 +163,7 @@ function deriveEmptyState(props: { return { title: "No threads yet", - detail: "Create a task to start a new coding session in one of your connected projects.", + detail: "Create a task to start a new coding runtime in one of your connected projects.", loading: false, }; } @@ -544,7 +546,7 @@ export function HomeScreen(props: HomeScreenProps) { ) : !hasResults ? ( ) : ( projectGroups.map((group) => { diff --git a/apps/mobile/src/features/home/homeThreadList.test.ts b/apps/mobile/src/features/home/homeThreadList.test.ts index cf9b0824aa4..50be9a68a77 100644 --- a/apps/mobile/src/features/home/homeThreadList.test.ts +++ b/apps/mobile/src/features/home/homeThreadList.test.ts @@ -6,6 +6,7 @@ import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools import { describe, expect, it } from "vite-plus/test"; import { buildHomeThreadGroups } from "./homeThreadList"; +import { makeThreadShellFixture } from "../../test-fixtures"; function makeProject( input: Partial & Pick, @@ -25,23 +26,21 @@ function makeThread( input: Partial & Pick, ): EnvironmentThreadShell { - return { + return makeThreadShellFixture({ modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, - latestTurn: null, createdAt: "2026-06-01T00:00:00.000Z", updatedAt: "2026-06-01T00:00:00.000Z", archivedAt: null, - session: null, latestUserMessageAt: null, hasPendingApprovals: false, hasPendingUserInput: false, hasActionableProposedPlan: false, ...input, - }; + }); } function buildGroups( diff --git a/apps/mobile/src/features/review/reviewModel.test.ts b/apps/mobile/src/features/review/reviewModel.test.ts index 3390afd9ff2..3543b4de894 100644 --- a/apps/mobile/src/features/review/reviewModel.test.ts +++ b/apps/mobile/src/features/review/reviewModel.test.ts @@ -1,11 +1,7 @@ import { describe, expect, it } from "vite-plus/test"; -import { - MessageId, - TurnId, - type OrchestrationCheckpointSummary, - type ReviewDiffPreviewSource, -} from "@t3tools/contracts"; +import { MessageId, RunId, type ReviewDiffPreviewSource } from "@t3tools/contracts"; +import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/shell"; import { buildReviewListItems, @@ -18,9 +14,9 @@ import { } from "./reviewModel"; function makeCheckpoint( - input: Partial & - Pick, -): OrchestrationCheckpointSummary { + input: Partial & + Pick, +): ThreadCheckpointSummary { return { checkpointRef: `refs/t3/checkpoints/thread/${input.checkpointTurnCount}` as any, status: "ready", @@ -52,12 +48,12 @@ describe("buildReviewSectionItems", () => { it("keeps one chip per checkpoint and appends git sources", () => { const checkpoints = [ makeCheckpoint({ - turnId: TurnId.make("turn-1"), + runId: RunId.make("run-1"), checkpointTurnCount: 1, completedAt: "2026-04-01T00:00:00.000Z", }), makeCheckpoint({ - turnId: TurnId.make("turn-2"), + runId: RunId.make("run-2"), checkpointTurnCount: 2, completedAt: "2026-04-02T00:00:00.000Z", }), diff --git a/apps/mobile/src/features/review/reviewModel.ts b/apps/mobile/src/features/review/reviewModel.ts index 9459d41872d..1704f9c52a8 100644 --- a/apps/mobile/src/features/review/reviewModel.ts +++ b/apps/mobile/src/features/review/reviewModel.ts @@ -1,6 +1,7 @@ import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; import type { ChangeTypes, FileDiffMetadata } from "@pierre/diffs/types"; -import type { OrchestrationCheckpointSummary, ReviewDiffPreviewSource } from "@t3tools/contracts"; +import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/shell"; +import type { ReviewDiffPreviewSource } from "@t3tools/contracts"; import * as Arr from "effect/Array"; import { pipe } from "effect/Function"; import * as Order from "effect/Order"; @@ -128,11 +129,11 @@ export type ReviewParsedDiff = readonly notice: string | null; }; -function checkpointTitle(checkpoint: OrchestrationCheckpointSummary): string { +function checkpointTitle(checkpoint: ThreadCheckpointSummary): string { return `Turn ${checkpoint.checkpointTurnCount}`; } -function checkpointSubtitle(checkpoint: OrchestrationCheckpointSummary): string { +function checkpointSubtitle(checkpoint: ThreadCheckpointSummary): string { const fileCount = checkpoint.files.length; if (checkpoint.status !== "ready") { return `Diff ${checkpoint.status}`; @@ -141,8 +142,8 @@ function checkpointSubtitle(checkpoint: OrchestrationCheckpointSummary): string } function compareCheckpointTurnCountDescending( - left: OrchestrationCheckpointSummary, - right: OrchestrationCheckpointSummary, + left: ThreadCheckpointSummary, + right: ThreadCheckpointSummary, ): -1 | 0 | 1 { if (left.checkpointTurnCount === right.checkpointTurnCount) { return 0; @@ -151,7 +152,7 @@ function compareCheckpointTurnCountDescending( return left.checkpointTurnCount > right.checkpointTurnCount ? -1 : 1; } -const readyCheckpointOrder = Order.make( +const readyCheckpointOrder = Order.make( compareCheckpointTurnCountDescending, ); @@ -510,14 +511,14 @@ function mapRenderableFile(file: FileDiffMetadata): ReviewRenderableFile { } export function getReviewSectionIdForCheckpoint( - checkpoint: Pick, + checkpoint: Pick, ): string { return `turn:${checkpoint.checkpointTurnCount}`; } export function getReadyReviewCheckpoints( - checkpoints: ReadonlyArray, -): ReadonlyArray { + checkpoints: ReadonlyArray, +): ReadonlyArray { return pipe( checkpoints, Arr.filter((checkpoint) => checkpoint.status === "ready"), @@ -526,7 +527,7 @@ export function getReadyReviewCheckpoints( } export function buildReviewSectionItems(input: { - readonly checkpoints: ReadonlyArray; + readonly checkpoints: ReadonlyArray; readonly gitSections: ReadonlyArray; readonly turnDiffById: Readonly>; readonly loadingTurnIds: Readonly>; diff --git a/apps/mobile/src/features/review/useReviewSections.ts b/apps/mobile/src/features/review/useReviewSections.ts index 87325490990..7c725e25830 100644 --- a/apps/mobile/src/features/review/useReviewSections.ts +++ b/apps/mobile/src/features/review/useReviewSections.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo } from "react"; -import type { EnvironmentId, OrchestrationCheckpointSummary, ThreadId } from "@t3tools/contracts"; +import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/shell"; +import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useCheckpointDiff } from "../../state/queries"; import { useEnvironmentQuery } from "../../state/query"; @@ -59,7 +60,7 @@ export function useReviewSections(input: { getReviewSectionIdForCheckpoint(checkpoint), checkpoint, ]), - ) as Record, + ) as Record, [readyCheckpoints], ); const reviewSections = useMemo( diff --git a/apps/mobile/src/features/threads/PendingApprovalCard.tsx b/apps/mobile/src/features/threads/PendingApprovalCard.tsx index 0617ef1cbcf..2906bcd7d12 100644 --- a/apps/mobile/src/features/threads/PendingApprovalCard.tsx +++ b/apps/mobile/src/features/threads/PendingApprovalCard.tsx @@ -1,4 +1,4 @@ -import type { ApprovalRequestId, ProviderApprovalDecision } from "@t3tools/contracts"; +import type { ProviderApprovalDecision, RuntimeRequestId } from "@t3tools/contracts"; import { Pressable, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; @@ -6,14 +6,16 @@ import type { PendingApproval } from "../../lib/threadActivity"; export interface PendingApprovalCardProps { readonly approval: PendingApproval; - readonly respondingApprovalId: ApprovalRequestId | null; + readonly respondingApprovalId: RuntimeRequestId | null; readonly onRespond: ( - requestId: ApprovalRequestId, + requestId: RuntimeRequestId, decision: ProviderApprovalDecision, ) => Promise; } export function PendingApprovalCard(props: PendingApprovalCardProps) { + const canRespond = props.approval.responseCapability === "live"; + const disabled = !canRespond || props.respondingApprovalId === props.approval.requestId; return ( @@ -27,17 +29,23 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) { {props.approval.detail} ) : null} + {!canRespond ? ( + + The provider process for this request is no longer available. Interrupt or restart the run + to continue. + + ) : null} void props.onRespond(props.approval.requestId, "accept")} > Allow once void props.onRespond(props.approval.requestId, "acceptForSession")} > @@ -46,7 +54,7 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) { void props.onRespond(props.approval.requestId, "decline")} > Decline diff --git a/apps/mobile/src/features/threads/PendingUserInputCard.tsx b/apps/mobile/src/features/threads/PendingUserInputCard.tsx index c9e01777214..cdff7a3b96d 100644 --- a/apps/mobile/src/features/threads/PendingUserInputCard.tsx +++ b/apps/mobile/src/features/threads/PendingUserInputCard.tsx @@ -1,4 +1,4 @@ -import type { ApprovalRequestId } from "@t3tools/contracts"; +import type { RuntimeRequestId } from "@t3tools/contracts"; import { Pressable, View } from "react-native"; import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; @@ -9,14 +9,10 @@ export interface PendingUserInputCardProps { readonly pendingUserInput: PendingUserInput; readonly drafts: Record; readonly answers: Record | null; - readonly respondingUserInputId: ApprovalRequestId | null; - readonly onSelectOption: ( - requestId: ApprovalRequestId, - questionId: string, - label: string, - ) => void; + readonly respondingUserInputId: RuntimeRequestId | null; + readonly onSelectOption: (requestId: RuntimeRequestId, questionId: string, label: string) => void; readonly onChangeCustomAnswer: ( - requestId: ApprovalRequestId, + requestId: RuntimeRequestId, questionId: string, customAnswer: string, ) => void; @@ -24,6 +20,7 @@ export interface PendingUserInputCardProps { } export function PendingUserInputCard(props: PendingUserInputCardProps) { + const canRespond = props.pendingUserInput.responseCapability === "live"; return ( @@ -32,6 +29,12 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { Fill in the pending answers + {!canRespond ? ( + + The provider process for this request is no longer available. Interrupt or restart the run + to continue. + + ) : null} {props.pendingUserInput.questions.map((question) => { const draft = props.drafts[question.id]; return ( @@ -49,6 +52,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { return ( props.onChangeCustomAnswer(props.pendingUserInput.requestId, question.id, value) @@ -94,7 +99,9 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { props.answers ? "bg-blue-500" : "bg-neutral-200 dark:bg-neutral-700/60", )} disabled={ - props.answers === null || props.respondingUserInputId === props.pendingUserInput.requestId + !canRespond || + props.answers === null || + props.respondingUserInputId === props.pendingUserInput.requestId } onPress={() => void props.onSubmit()} > diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 0050eb923be..46d10b0c182 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -1,8 +1,11 @@ import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass"; +import { + threadRuntimeIsActive, + type EnvironmentThreadShell, +} from "@t3tools/client-runtime/state/shell"; import type { EnvironmentId, ModelSelection, - OrchestrationThreadShell, ProviderInteractionMode, RuntimeMode, ServerConfig as T3ServerConfig, @@ -79,7 +82,7 @@ export interface ThreadComposerProps { readonly connectionState: RemoteClientConnectionState; readonly connectionError: string | null; readonly environmentLabel: string | null; - readonly selectedThread: OrchestrationThreadShell; + readonly selectedThread: EnvironmentThreadShell; readonly serverConfig: T3ServerConfig | null; readonly queueCount: number; readonly activeThreadBusy: boolean; @@ -238,9 +241,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer setIsFocused(false); onExpandedChange?.(false); }, [onExpandedChange]); - const showStopAction = - props.selectedThread.session?.status === "running" || - props.selectedThread.session?.status === "starting"; + const showStopAction = threadRuntimeIsActive(props.selectedThread.runtime); const sendLabel = props.connectionState !== "connected" || props.activeThreadBusy || props.queueCount > 0 diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 624e8fe14fe..bcad1c503a1 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -1,12 +1,12 @@ import { type EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; +import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import type { - ApprovalRequestId, EnvironmentId, ModelSelection, - OrchestrationThreadShell, ProviderApprovalDecision, ProviderInteractionMode, RuntimeMode, + RuntimeRequestId, ServerConfig as T3ServerConfig, ThreadId, } from "@t3tools/contracts"; @@ -43,7 +43,7 @@ import { ThreadFeed } from "./ThreadFeed"; import type { ThreadContentPresentation } from "./threadContentPresentation"; export interface ThreadDetailScreenProps { - readonly selectedThread: OrchestrationThreadShell; + readonly selectedThread: EnvironmentThreadShell; readonly contentPresentation: ThreadContentPresentation; readonly screenTone: StatusTone; readonly connectionError: string | null; @@ -51,11 +51,11 @@ export interface ThreadDetailScreenProps { readonly selectedThreadFeed: ReadonlyArray; readonly activeWorkStartedAt: string | null; readonly activePendingApproval: PendingApproval | null; - readonly respondingApprovalId: ApprovalRequestId | null; + readonly respondingApprovalId: RuntimeRequestId | null; readonly activePendingUserInput: PendingUserInput | null; readonly activePendingUserInputDrafts: Record; readonly activePendingUserInputAnswers: Record | null; - readonly respondingUserInputId: ApprovalRequestId | null; + readonly respondingUserInputId: RuntimeRequestId | null; readonly draftMessage: string; readonly draftAttachments: ReadonlyArray; readonly connectionStateLabel: EnvironmentConnectionPhase; @@ -79,16 +79,16 @@ export interface ThreadDetailScreenProps { readonly onUpdateThreadRuntimeMode: (runtimeMode: RuntimeMode) => void; readonly onUpdateThreadInteractionMode: (interactionMode: ProviderInteractionMode) => void; readonly onRespondToApproval: ( - requestId: ApprovalRequestId, + requestId: RuntimeRequestId, decision: ProviderApprovalDecision, ) => Promise; readonly onSelectUserInputOption: ( - requestId: ApprovalRequestId, + requestId: RuntimeRequestId, questionId: string, label: string, ) => void; readonly onChangeUserInputCustomAnswer: ( - requestId: ApprovalRequestId, + requestId: RuntimeRequestId, questionId: string, customAnswer: string, ) => void; @@ -314,7 +314,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread feed={props.selectedThreadFeed} contentPresentation={props.contentPresentation} agentLabel={agentLabel} - latestTurn={props.selectedThread.latestTurn} + latestRun={props.selectedThread.latestRun} contentTopInset={headerHeight} contentBottomInset={feedBottomInset} layoutVariant={layoutVariant} diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 4f8ca9c747d..f2d1b99fe35 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -1,7 +1,7 @@ import * as Haptics from "expo-haptics"; import { KeyboardAvoidingLegendList } from "@legendapp/list/keyboard"; import { type LegendListRef } from "@legendapp/list/react-native"; -import type { EnvironmentId, ThreadId, TurnId } from "@t3tools/contracts"; +import type { EnvironmentId, ThreadId, RunId } from "@t3tools/contracts"; import { SymbolView } from "expo-symbols"; import { useRouter } from "expo-router"; import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; @@ -62,7 +62,7 @@ import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/l import { deriveThreadFeedPresentation, type ThreadFeedEntry, - type ThreadFeedLatestTurn, + type ThreadFeedLatestRun, } from "../../lib/threadActivity"; import { isThreadFeedNearEnd } from "../../lib/threadFeedLayout"; import { relativeTime } from "../../lib/time"; @@ -92,7 +92,7 @@ export interface ThreadFeedProps { readonly feed: ReadonlyArray; readonly contentPresentation: ThreadContentPresentation; readonly agentLabel: string; - readonly latestTurn: ThreadFeedLatestTurn | null; + readonly latestRun: ThreadFeedLatestRun | null; readonly contentTopInset?: number; readonly contentBottomInset?: number; readonly layoutVariant?: LayoutVariant; @@ -649,11 +649,11 @@ function renderFeedEntry( readonly expandedWorkGroups: Record; readonly expandedWorkRows: Record; readonly terminalAssistantMessageIds: ReadonlySet; - readonly unsettledTurnId: TurnId | null; + readonly unsettledTurnId: RunId | null; readonly onCopyWorkRow: (rowId: string, value: string) => void; readonly onToggleWorkGroup: (groupId: string) => void; readonly onToggleWorkRow: (rowId: string) => void; - readonly onToggleTurnFold: (turnId: TurnId) => void; + readonly onToggleTurnFold: (runId: RunId) => void; readonly onPressImage: (uri: string, headers?: Record) => void; readonly onMarkdownLinkPress: (href: string) => void; readonly iconSubtleColor: string | import("react-native").ColorValue; @@ -667,12 +667,12 @@ function renderFeedEntry( const entry = info.item; const { markdownStyles, iconSubtleColor, userBubbleColor } = props; - if (entry.type === "turn-fold") { + if (entry.type === "run-fold") { return ( props.onToggleTurnFold(entry.turnId)} + onPress={() => props.onToggleTurnFold(entry.runId)} hitSlop={4} className="mb-3 min-h-11 flex-row items-center gap-2 border-b border-neutral-200/80 px-2 dark:border-white/[0.08]" > @@ -699,7 +699,7 @@ function renderFeedEntry( const assistantTurnStillInProgress = message.role === "assistant" && props.unsettledTurnId !== null && - message.turnId === props.unsettledTurnId; + message.runId === props.unsettledTurnId; const showAssistantMeta = message.role === "assistant" && props.terminalAssistantMessageIds.has(message.id) && @@ -1128,7 +1128,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const foldSettleFrameRef = useRef(null); const foldSettleSecondFrameRef = useRef(null); const suppressAutoFollowRef = useRef(false); - const previousLatestTurnRef = useRef(props.latestTurn); + const previousLatestTurnRef = useRef(props.latestRun); const isNearEndRef = useRef(true); const initialScrollReadyRef = useRef(false); const lastContentHeightRef = useRef(0); @@ -1137,7 +1137,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { readonly copiedRowId: string | null; readonly expandedWorkGroups: Record; readonly expandedWorkRows: Record; - readonly expandedTurnIds: ReadonlySet; + readonly expandedTurnIds: ReadonlySet; }>({ copiedRowId: null, expandedWorkGroups: {}, @@ -1211,22 +1211,22 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { ], ); const presentedFeed = useMemo( - () => deriveThreadFeedPresentation(props.feed, props.latestTurn, expandedTurnIds), - [expandedTurnIds, props.feed, props.latestTurn], + () => deriveThreadFeedPresentation(props.feed, props.latestRun, expandedTurnIds), + [expandedTurnIds, props.feed, props.latestRun], ); const terminalAssistantMessageIds = useMemo(() => { - const terminalIdsByTurn = new Map(); + const terminalIdsByTurn = new Map(); for (const entry of props.feed) { - if (entry.type === "message" && entry.message.role === "assistant" && entry.message.turnId) { - terminalIdsByTurn.set(entry.message.turnId, entry.message.id); + if (entry.type === "message" && entry.message.role === "assistant" && entry.message.runId) { + terminalIdsByTurn.set(entry.message.runId, entry.message.id); } } return new Set(terminalIdsByTurn.values()); }, [props.feed]); const unsettledTurnId = - props.latestTurn && - (props.latestTurn.completedAt === null || props.latestTurn.state === "running") - ? props.latestTurn.turnId + props.latestRun && + (props.latestRun.completedAt === null || props.latestRun.status === "running") + ? props.latestRun.runId : null; const scrollToEnd = useCallback(() => { @@ -1279,13 +1279,13 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { useEffect(() => { const previous = previousLatestTurnRef.current; - previousLatestTurnRef.current = props.latestTurn; - if (!props.latestTurn || !previous) { + previousLatestTurnRef.current = props.latestRun; + if (!props.latestRun || !previous) { return; } - if (props.latestTurn.turnId === previous.turnId) { - if (previous.state === "running" && props.latestTurn.state === "interrupted") { - const interruptedTurnId = props.latestTurn.turnId; + if (props.latestRun.runId === previous.runId) { + if (previous.status === "running" && props.latestRun.status === "interrupted") { + const interruptedTurnId = props.latestRun.runId; setInteractionState((current) => ({ ...current, expandedTurnIds: new Set(current.expandedTurnIds).add(interruptedTurnId), @@ -1294,14 +1294,14 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { return; } setInteractionState((current) => { - if (!current.expandedTurnIds.has(previous.turnId)) { + if (!current.expandedTurnIds.has(previous.runId)) { return current; } const next = new Set(current.expandedTurnIds); - next.delete(previous.turnId); + next.delete(previous.runId); return { ...current, expandedTurnIds: next }; }); - }, [props.latestTurn]); + }, [props.latestRun]); useEffect(() => { return () => { @@ -1357,7 +1357,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { })); }, []); - const onToggleTurnFold = useCallback((turnId: TurnId) => { + const onToggleTurnFold = useCallback((runId: RunId) => { suppressAutoFollowRef.current = true; if (foldSettleFrameRef.current !== null) { cancelAnimationFrame(foldSettleFrameRef.current); @@ -1367,10 +1367,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { } setInteractionState((current) => { const next = new Set(current.expandedTurnIds); - if (next.has(turnId)) { - next.delete(turnId); + if (next.has(runId)) { + next.delete(runId); } else { - next.add(turnId); + next.add(runId); } return { ...current, expandedTurnIds: next }; }); diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 7bb74ae88ff..975cdb4c2ca 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1,4 +1,5 @@ import { Stack, useLocalSearchParams, useRouter } from "expo-router"; +import { threadRuntimeIsActive } from "@t3tools/client-runtime/state/shell"; import { useCallback, useMemo, useState } from "react"; import * as Option from "effect/Option"; import { @@ -203,20 +204,15 @@ export function ThreadRouteScreen() { [selectedThread, setThreadInteractionMode], ); const handleStopThread = useCallback(() => { - if ( - !selectedThread || - (selectedThread.session?.status !== "running" && - selectedThread.session?.status !== "starting") - ) { + const runtime = selectedThread?.runtime; + if (!selectedThread || !runtime || !threadRuntimeIsActive(runtime)) { return; } return interruptThreadTurn({ environmentId: selectedThread.environmentId, input: { threadId: selectedThread.id, - ...(selectedThread.session.activeTurnId - ? { turnId: selectedThread.session.activeTurnId } - : {}), + ...(runtime.activeRunId ? { runId: runtime.activeRunId } : {}), }, }); }, [interruptThreadTurn, selectedThread]); @@ -242,7 +238,7 @@ export function ThreadRouteScreen() { terminalDebugLog("terminal-menu:open-new", { hasThread: Boolean(selectedThread), hasWorkspaceRoot: Boolean(selectedThreadProject?.workspaceRoot), - listedTerminalIds: terminalMenuSessions.map((session) => session.terminalId), + listedTerminalIds: terminalMenuSessions.map((runtime) => runtime.terminalId), }); if (!selectedThread || !selectedThreadProject?.workspaceRoot) { @@ -250,7 +246,7 @@ export function ThreadRouteScreen() { } const nextId = nextOpenTerminalId({ - listedTerminalIds: terminalMenuSessions.map((session) => session.terminalId), + listedTerminalIds: terminalMenuSessions.map((runtime) => runtime.terminalId), }); void router.push(buildThreadTerminalNavigation(selectedThread, nextId)); }, [router, selectedThread, selectedThreadProject?.workspaceRoot, terminalMenuSessions]); @@ -273,9 +269,9 @@ export function ThreadRouteScreen() { } const targetTerminalId = resolveProjectScriptTerminalId({ - existingTerminalIds: terminalMenuSessions.map((session) => session.terminalId), + existingTerminalIds: terminalMenuSessions.map((runtime) => runtime.terminalId), hasRunningTerminal: terminalMenuSessions.some( - (session) => session.status === "running" || session.status === "starting", + (runtime) => runtime.status === "running" || runtime.status === "starting", ), }); const preferredWorktreePath = resolvePreferredThreadWorktreePath({ diff --git a/apps/mobile/src/features/threads/threadPresentation.ts b/apps/mobile/src/features/threads/threadPresentation.ts index cf5eb1817a4..8fe661f798f 100644 --- a/apps/mobile/src/features/threads/threadPresentation.ts +++ b/apps/mobile/src/features/threads/threadPresentation.ts @@ -7,29 +7,29 @@ export function threadSortValue(thread: EnvironmentThreadShell): number { } export function threadStatusTone(thread: EnvironmentThreadShell): StatusTone { - const status = thread.session?.status; - if (status === "running") { + const status = thread.runtime?.status; + if (status === "running" || status === "waiting") { return { label: "Running", pillClassName: "bg-orange-500/12 dark:bg-orange-500/16", textClassName: "text-orange-700 dark:text-orange-300", }; } - if (status === "ready") { + if (status === "completed") { return { label: "Ready", pillClassName: "bg-emerald-500/12 dark:bg-emerald-500/16", textClassName: "text-emerald-700 dark:text-emerald-300", }; } - if (status === "starting") { + if (status === "queued" || status === "starting") { return { label: "Starting", pillClassName: "bg-sky-500/12 dark:bg-sky-500/16", textClassName: "text-sky-700 dark:text-sky-300", }; } - if (status === "error") { + if (status === "failed") { return { label: "Error", pillClassName: "bg-rose-500/12 dark:bg-rose-500/16", diff --git a/apps/mobile/src/features/threads/use-project-actions.ts b/apps/mobile/src/features/threads/use-project-actions.ts index 9531567f447..96008e4d6d1 100644 --- a/apps/mobile/src/features/threads/use-project-actions.ts +++ b/apps/mobile/src/features/threads/use-project-actions.ts @@ -70,6 +70,7 @@ export function useCreateProjectThread() { environmentId: input.project.environmentId, input: { commandId: CommandId.make(metadata.commandId), + creationSource: "mobile", threadId, message: { messageId: MessageId.make(metadata.messageId), diff --git a/apps/mobile/src/lib/repositoryGroups.test.ts b/apps/mobile/src/lib/repositoryGroups.test.ts index 8cea5df2307..1e0c83c90f8 100644 --- a/apps/mobile/src/lib/repositoryGroups.test.ts +++ b/apps/mobile/src/lib/repositoryGroups.test.ts @@ -4,6 +4,7 @@ import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools import { groupProjectsByRepository } from "./repositoryGroups"; import { EnvironmentProject, EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import { makeThreadShellFixture } from "../test-fixtures"; function makeProject( input: Partial & Pick, @@ -23,22 +24,20 @@ function makeThread( input: Partial & Pick, ): EnvironmentThreadShell { - return { + return makeThreadShellFixture({ runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, - latestTurn: null, createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", archivedAt: null, - session: null, latestUserMessageAt: null, hasPendingApprovals: false, hasPendingUserInput: false, hasActionableProposedPlan: false, ...input, - }; + }); } describe("groupProjectsByRepository", () => { diff --git a/apps/mobile/src/lib/scopedEntities.ts b/apps/mobile/src/lib/scopedEntities.ts index 34709957fd4..6464b356191 100644 --- a/apps/mobile/src/lib/scopedEntities.ts +++ b/apps/mobile/src/lib/scopedEntities.ts @@ -1,4 +1,4 @@ -import { ApprovalRequestId, EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts"; +import { EnvironmentId, ProjectId, RuntimeRequestId, ThreadId } from "@t3tools/contracts"; export function scopedProjectKey(environmentId: EnvironmentId, projectId: ProjectId): string { return `${environmentId}:${projectId}`; @@ -10,7 +10,7 @@ export function scopedThreadKey(environmentId: EnvironmentId, threadId: ThreadId export function scopedRequestKey( environmentId: EnvironmentId, - requestId: ApprovalRequestId, + requestId: RuntimeRequestId, ): string { return `${environmentId}:${requestId}`; } diff --git a/apps/mobile/src/lib/threadActivity.test.ts b/apps/mobile/src/lib/threadActivity.test.ts index f5d8f4bdf11..572894760e7 100644 --- a/apps/mobile/src/lib/threadActivity.test.ts +++ b/apps/mobile/src/lib/threadActivity.test.ts @@ -1,409 +1,111 @@ +import type { ThreadWorkEntry } from "@t3tools/client-runtime/state/shell"; +import { MessageId, RunId } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; -import { - EventId, - MessageId, - ProjectId, - ProviderInstanceId, - ThreadId, - TurnId, - type OrchestrationThread, - type OrchestrationThreadActivity, -} from "@t3tools/contracts"; - +import { makeThreadFixture } from "../test-fixtures"; import { buildThreadFeed, deriveThreadFeedPresentation } from "./threadActivity"; -function makeActivity( - input: Partial & - Pick, -): OrchestrationThreadActivity { +const runId = RunId.make("run-1"); + +function message(role: "user" | "assistant", text: string, createdAt: string, id: string) { return { - tone: "info", - payload: {}, - turnId: null, - ...input, - }; + id: MessageId.make(id), + role, + text, + attachments: [], + runId: role === "assistant" ? runId : null, + streaming: false, + createdAt, + updatedAt: createdAt, + } as const; } -function makeThread( - input: Partial & Pick, -): OrchestrationThread { +function commandEntry(overrides: Partial = {}): ThreadWorkEntry { + const structuredPayload = { + type: "command_execution", + input: "vp check", + output: "ok", + } as ThreadWorkEntry["structuredPayload"]; return { - modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - latestTurn: null, - createdAt: "2026-04-01T00:00:00.000Z", - updatedAt: "2026-04-01T00:00:00.000Z", - archivedAt: null, - deletedAt: null, - messages: [], - proposedPlans: [], - activities: [], - checkpoints: [], - session: null, - ...input, + id: "item-1", + createdAt: "2026-06-20T00:00:02.000Z", + runId, + label: "Ran command", + command: "vp check", + detail: "ok", + tone: "tool", + itemType: "command_execution", + toolLifecycleStatus: "completed", + structuredPayload, + ...overrides, }; } describe("buildThreadFeed", () => { - it("keeps historic work entries attributed to their turns", () => { - const thread = makeThread({ - id: ThreadId.make("thread-1"), - projectId: ProjectId.make("project-1"), - title: "Runtime warning thread", - latestTurn: { - turnId: TurnId.make("turn-latest"), - state: "running", - requestedAt: "2026-04-01T00:00:00.000Z", - startedAt: "2026-04-01T00:00:01.000Z", - completedAt: null, - assistantMessageId: null, - }, - activities: [ - makeActivity({ - id: EventId.make("activity-old"), - kind: "runtime.warning", - summary: "Runtime warning", - createdAt: "2026-04-01T00:00:02.000Z", - turnId: TurnId.make("turn-old"), - payload: { - message: "Old warning", - }, - }), - makeActivity({ - id: EventId.make("activity-latest"), - kind: "runtime.warning", - summary: "Runtime warning", - createdAt: "2026-04-01T00:00:03.000Z", - turnId: TurnId.make("turn-latest"), - payload: { - message: "Latest warning", - }, - }), - ], - }); - - const feed = buildThreadFeed(thread, [], null); - expect(feed).toMatchObject([ - { - type: "activity-group", - turnId: "turn-old", - activities: [{ id: "activity-old", turnId: "turn-old" }], - }, - { - type: "activity-group", - turnId: "turn-latest", - activities: [{ id: "activity-latest", turnId: "turn-latest" }], - }, - ]); - }); - - it("collapses matching tool lifecycle rows like desktop", () => { - const thread = makeThread({ - id: ThreadId.make("thread-2"), - projectId: ProjectId.make("project-1"), - title: "Collapsed tools", - latestTurn: { - turnId: TurnId.make("turn-1"), - state: "completed", - requestedAt: "2026-04-01T00:00:00.000Z", - startedAt: "2026-04-01T00:00:01.000Z", - completedAt: "2026-04-01T00:00:03.000Z", - assistantMessageId: null, - }, - activities: [ - makeActivity({ - id: EventId.make("tool-updated"), - kind: "tool.updated", - tone: "tool", - summary: "Run tests", - createdAt: "2026-04-01T00:00:01.000Z", - turnId: TurnId.make("turn-1"), - payload: { - title: "Run tests", - itemType: "command_execution", - detail: "/bin/zsh -lc 'bun run test'", - }, - }), - makeActivity({ - id: EventId.make("tool-completed"), - kind: "tool.completed", - tone: "tool", - summary: "Run tests completed", - createdAt: "2026-04-01T00:00:02.000Z", - turnId: TurnId.make("turn-1"), - payload: { - title: "Run tests", - itemType: "command_execution", - detail: "/bin/zsh -lc 'bun run test'", - }, - }), + it("orders V2 messages and work entries while retaining structured tool data", () => { + const thread = makeThreadFixture({ + messages: [ + message("user", "Run checks", "2026-06-20T00:00:01.000Z", "message-user"), + message("assistant", "Done", "2026-06-20T00:00:03.000Z", "message-assistant"), ], + workEntries: [commandEntry()], }); const feed = buildThreadFeed(thread, [], null); - const group = feed[0]; - - expect(group).toMatchObject({ - type: "activity-group", - }); - if (!group || group.type !== "activity-group") { - return; - } - - expect(group.activities).toEqual([ - { - id: "tool-completed", - createdAt: "2026-04-01T00:00:02.000Z", - turnId: "turn-1", - summary: "Run tests", - detail: "bun run test", - fullDetail: "/bin/zsh -lc 'bun run test'", - copyText: "Run tests\nbun run test\n/bin/zsh -lc 'bun run test'", - icon: "command", - toolLike: true, - status: "success", - }, - ]); - }); - - it("keeps MCP inputs available to expanded mobile work rows", () => { - const turnId = TurnId.make("turn-mcp"); - const thread = makeThread({ - id: ThreadId.make("thread-mcp"), - projectId: ProjectId.make("project-1"), - title: "Expandable MCP call", - latestTurn: { - turnId, - state: "completed", - requestedAt: "2026-04-01T00:00:00.000Z", - startedAt: "2026-04-01T00:00:01.000Z", - completedAt: "2026-04-01T00:00:03.000Z", - assistantMessageId: null, - }, - activities: [ - makeActivity({ - id: EventId.make("mcp-completed"), - kind: "tool.completed", - tone: "tool", - summary: "Call repository tool", - createdAt: "2026-04-01T00:00:02.000Z", - turnId, - payload: { - title: "Call repository tool", - itemType: "mcp_tool_call", - detail: "repository.search", - status: "completed", - data: { - item: { - server: "repository", - tool: "search", - arguments: { query: "work log" }, - }, - }, - }, - }), - ], - }); - - const group = buildThreadFeed(thread, [], null)[0]; - expect(group).toMatchObject({ type: "activity-group" }); - if (!group || group.type !== "activity-group") { - return; - } - - expect(group.activities[0]?.icon).toBe("wrench"); - expect(group.activities[0]?.fullDetail).toContain('"query": "work log"'); - expect(group.activities[0]?.fullDetail).toContain("repository.search"); + expect(feed.map((entry) => entry.type)).toEqual(["message", "activity-group", "message"]); + const activity = feed.find((entry) => entry.type === "activity-group")?.activities[0]; + expect(activity?.runId).toBe(runId); + expect(activity?.fullDetail).toContain('"input": "vp check"'); }); - it("folds settled turn work while leaving the terminal answer visible", () => { - const turnId = TurnId.make("turn-1"); - const thread = makeThread({ - id: ThreadId.make("thread-3"), - projectId: ProjectId.make("project-1"), - title: "Folded work", - latestTurn: { - turnId, - state: "completed", - requestedAt: "2026-04-01T00:00:00.000Z", - startedAt: "2026-04-01T00:00:01.000Z", - completedAt: "2026-04-01T00:00:18.000Z", - assistantMessageId: MessageId.make("assistant-final"), - }, + it("folds settled V2 run work while keeping the terminal assistant message visible", () => { + const thread = makeThreadFixture({ messages: [ - { - id: MessageId.make("assistant-commentary"), - role: "assistant", - text: "I am checking.", - turnId, - streaming: false, - createdAt: "2026-04-01T00:00:02.000Z", - updatedAt: "2026-04-01T00:00:03.000Z", - }, - { - id: MessageId.make("assistant-final"), - role: "assistant", - text: "Done.", - turnId, - streaming: false, - createdAt: "2026-04-01T00:00:17.000Z", - updatedAt: "2026-04-01T00:00:18.000Z", - }, - ], - activities: [ - makeActivity({ - id: EventId.make("tool-completed"), - kind: "tool.completed", - tone: "tool", - summary: "Read files", - createdAt: "2026-04-01T00:00:05.000Z", - turnId, - payload: { - title: "Read files", - itemType: "file_read", - status: "completed", - }, - }), + message("user", "Run checks", "2026-06-20T00:00:01.000Z", "message-user"), + message("assistant", "Done", "2026-06-20T00:00:03.000Z", "message-assistant"), ], + workEntries: [commandEntry()], }); - const feed = buildThreadFeed(thread, [], null); - const collapsed = deriveThreadFeedPresentation(feed, thread.latestTurn, new Set()); - expect(collapsed.map((entry) => entry.id)).toEqual(["turn-fold:turn-1", "assistant-final"]); - expect(collapsed[0]).toMatchObject({ - type: "turn-fold", - label: "Worked for 17s", - expanded: false, - }); - - const expanded = deriveThreadFeedPresentation(feed, thread.latestTurn, new Set([turnId])); - expect(expanded.map((entry) => entry.id)).toEqual([ - "turn-fold:turn-1", - "assistant-commentary", - "tool-completed", - "assistant-final", + const latestRun = { + runId, + status: "completed" as const, + startedAt: "2026-06-20T00:00:01.000Z", + completedAt: "2026-06-20T00:00:03.000Z", + }; + + const collapsed = deriveThreadFeedPresentation(feed, latestRun, new Set()); + expect(collapsed.map((entry) => entry.type)).toEqual(["message", "run-fold", "message"]); + + const expanded = deriveThreadFeedPresentation(feed, latestRun, new Set([runId])); + expect(expanded.map((entry) => entry.type)).toEqual([ + "message", + "run-fold", + "activity-group", + "message", ]); }); - it("measures a steer-superseded turn from its user boundary through trailing work", () => { - const firstTurnId = TurnId.make("turn-1"); - const secondTurnId = TurnId.make("turn-2"); - const thread = makeThread({ - id: ThreadId.make("thread-steered"), - projectId: ProjectId.make("project-1"), - title: "Steered work", - latestTurn: { - turnId: secondTurnId, - state: "running", - requestedAt: "2026-04-01T00:00:14.000Z", - startedAt: "2026-04-01T00:00:14.000Z", - completedAt: null, - assistantMessageId: MessageId.make("assistant-next"), - }, - messages: [ - { - id: MessageId.make("user-1"), - role: "user", - text: "Do it once more.", - turnId: null, - streaming: false, - createdAt: "2026-04-01T00:00:00.000Z", - updatedAt: "2026-04-01T00:00:00.000Z", - }, - { - id: MessageId.make("assistant-commentary"), - role: "assistant", - text: "Kicking off call 1.", - turnId: firstTurnId, - streaming: false, - createdAt: "2026-04-01T00:00:09.000Z", - updatedAt: "2026-04-01T00:00:09.000Z", - }, - { - id: MessageId.make("user-2"), - role: "user", - text: "Actually do 15.", - turnId: null, - streaming: false, - createdAt: "2026-04-01T00:00:14.000Z", - updatedAt: "2026-04-01T00:00:14.000Z", - }, - { - id: MessageId.make("assistant-next"), - role: "assistant", - text: "One down - adjusting.", - turnId: secondTurnId, - streaming: true, - createdAt: "2026-04-01T00:00:17.000Z", - updatedAt: "2026-04-01T00:00:17.000Z", - }, - ], - activities: [ - makeActivity({ - id: EventId.make("work-1"), - kind: "tool.completed", - tone: "tool", - summary: "Ran command", - createdAt: "2026-04-01T00:00:12.000Z", - turnId: firstTurnId, - payload: { - title: "Ran command", - itemType: "command_execution", - status: "completed", - }, - }), - ], + it("keeps an active run expanded and marks failed tools as failures", () => { + const thread = makeThreadFixture({ + messages: [message("user", "Run checks", "2026-06-20T00:00:01.000Z", "message-user")], + workEntries: [commandEntry({ tone: "error", toolLifecycleStatus: "failed" })], }); - const feed = buildThreadFeed(thread, [], null); - const collapsed = deriveThreadFeedPresentation(feed, thread.latestTurn, new Set()); - expect(collapsed.find((entry) => entry.type === "turn-fold")).toMatchObject({ - turnId: firstTurnId, - label: "Worked for 12s", - }); - }); - - it("keeps an active turn expanded and classifies error-shaped tool output", () => { - const turnId = TurnId.make("turn-running"); - const thread = makeThread({ - id: ThreadId.make("thread-4"), - projectId: ProjectId.make("project-1"), - title: "Running work", - latestTurn: { - turnId, - state: "running", - requestedAt: "2026-04-01T00:00:00.000Z", - startedAt: "2026-04-01T00:00:01.000Z", + const presented = deriveThreadFeedPresentation( + feed, + { + runId, + status: "running", + startedAt: "2026-06-20T00:00:01.000Z", completedAt: null, - assistantMessageId: null, }, - activities: [ - makeActivity({ - id: EventId.make("tool-failed"), - kind: "tool.completed", - tone: "tool", - summary: "Run command", - createdAt: "2026-04-01T00:00:05.000Z", - turnId, - payload: { - title: "Run command", - itemType: "command_execution", - detail: "zsh: command not found: nope", - status: "completed", - }, - }), - ], - }); + new Set(), + ); - const feed = buildThreadFeed(thread, [], null); - expect(deriveThreadFeedPresentation(feed, thread.latestTurn, new Set())).toEqual(feed); - expect(feed[0]).toMatchObject({ - type: "activity-group", - activities: [{ status: "failure" }], - }); + expect(presented.some((entry) => entry.type === "run-fold")).toBe(false); + expect(presented.find((entry) => entry.type === "activity-group")?.activities[0]?.status).toBe( + "failure", + ); }); }); diff --git a/apps/mobile/src/lib/threadActivity.ts b/apps/mobile/src/lib/threadActivity.ts index bef46e46e6e..713b921b484 100644 --- a/apps/mobile/src/lib/threadActivity.ts +++ b/apps/mobile/src/lib/threadActivity.ts @@ -1,31 +1,19 @@ -import { ApprovalRequestId, isToolLifecycleItemType } from "@t3tools/contracts"; import type { - MessageId, - OrchestrationLatestTurn, - OrchestrationThread, - OrchestrationThreadActivity, - ToolLifecycleItemType, - TurnId, - UserInputQuestion, -} from "@t3tools/contracts"; + EnvironmentThread, + ThreadConversationMessage, + ThreadPendingApproval, + ThreadPendingUserInput, + ThreadRunSummary, + ThreadUserInputQuestion, + ThreadWorkEntry, +} from "@t3tools/client-runtime/state/shell"; +import type { MessageId, RunId } from "@t3tools/contracts"; import { formatDuration } from "@t3tools/shared/orchestrationTiming"; import type { QueuedThreadMessage } from "../state/thread-outbox-model"; -import * as Arr from "effect/Array"; -import * as Order from "effect/Order"; -export interface PendingApproval { - readonly requestId: ApprovalRequestId; - readonly requestKind: "command" | "file-read" | "file-change"; - readonly createdAt: string; - readonly detail?: string; -} - -export interface PendingUserInput { - readonly requestId: ApprovalRequestId; - readonly createdAt: string; - readonly questions: ReadonlyArray; -} +export type PendingApproval = ThreadPendingApproval; +export type PendingUserInput = ThreadPendingUserInput; export interface PendingUserInputDraftAnswer { readonly selectedOptionLabel?: string; @@ -35,7 +23,7 @@ export interface PendingUserInputDraftAnswer { export interface ThreadFeedActivity { readonly id: string; readonly createdAt: string; - readonly turnId: TurnId | null; + readonly runId: RunId | null; readonly summary: string; readonly detail: string | null; readonly fullDetail: string | null; @@ -57,36 +45,12 @@ export interface ThreadFeedActivity { readonly status: "success" | "failure" | "neutral" | null; } -type WorkLogToolLifecycleStatus = "inProgress" | "completed" | "failed" | "declined" | "stopped"; - -interface WorkLogEntry { - id: string; - createdAt: string; - turnId: TurnId | null; - label: string; - detail?: string; - command?: string; - rawCommand?: string; - changedFiles?: ReadonlyArray; - tone: "thinking" | "tool" | "info" | "error"; - toolTitle?: string; - itemType?: ToolLifecycleItemType; - requestKind?: PendingApproval["requestKind"]; - toolLifecycleStatus?: WorkLogToolLifecycleStatus; - toolData?: unknown; -} - -interface DerivedWorkLogEntry extends WorkLogEntry { - activityKind: OrchestrationThreadActivity["kind"]; - collapseKey?: string; -} - type RawThreadFeedEntry = | { readonly type: "message"; readonly id: string; readonly createdAt: string; - readonly message: OrchestrationThread["messages"][number]; + readonly message: ThreadConversationMessage; } | { readonly type: "queued-message"; @@ -99,7 +63,7 @@ type RawThreadFeedEntry = readonly type: "activity"; readonly id: string; readonly createdAt: string; - readonly turnId: TurnId | null; + readonly runId: RunId | null; readonly activity: ThreadFeedActivity; }; @@ -109,109 +73,25 @@ export type ThreadFeedEntry = readonly type: "activity-group"; readonly id: string; readonly createdAt: string; - readonly turnId: TurnId | null; + readonly runId: RunId | null; readonly activities: ReadonlyArray; } | { - readonly type: "turn-fold"; + readonly type: "run-fold"; readonly id: string; readonly createdAt: string; - readonly turnId: TurnId; + readonly runId: RunId; readonly label: string; readonly expanded: boolean; }; -export type ThreadFeedLatestTurn = Pick< - OrchestrationLatestTurn, - "turnId" | "state" | "startedAt" | "completedAt" +export type ThreadFeedLatestRun = Pick< + ThreadRunSummary, + "runId" | "status" | "startedAt" | "completedAt" >; -function requestKindFromRequestType(requestType: unknown): PendingApproval["requestKind"] | null { - switch (requestType) { - case "command_execution_approval": - case "exec_command_approval": - return "command"; - case "file_read_approval": - return "file-read"; - case "file_change_approval": - case "apply_patch_approval": - return "file-change"; - default: - return null; - } -} - -function isStalePendingRequestFailureDetail(detail: string | undefined): boolean { - const normalized = detail?.toLowerCase(); - if (!normalized) { - return false; - } - return ( - normalized.includes("stale pending approval request") || - normalized.includes("stale pending user-input request") || - normalized.includes("unknown pending approval request") || - normalized.includes("unknown pending permission request") || - normalized.includes("unknown pending user-input request") - ); -} - -function parseApprovalRequestId(value: unknown): ApprovalRequestId | null { - return typeof value === "string" && value.length > 0 ? ApprovalRequestId.make(value) : null; -} - -function parseUserInputQuestions( - payload: Record | null, -): ReadonlyArray | null { - const questions = payload?.questions; - if (!Array.isArray(questions)) { - return null; - } - - const parsed = questions - .map((entry) => { - if (!entry || typeof entry !== "object") return null; - const question = entry as Record; - if ( - typeof question.id !== "string" || - typeof question.header !== "string" || - typeof question.question !== "string" || - !Array.isArray(question.options) - ) { - return null; - } - const options = question.options - .map((option) => { - if (!option || typeof option !== "object") return null; - const record = option as Record; - if (typeof record.label !== "string" || typeof record.description !== "string") { - return null; - } - return { - label: record.label, - description: record.description, - }; - }) - .filter((option): option is UserInputQuestion["options"][number] => option !== null); - if (options.length === 0) { - return null; - } - return { - id: question.id, - header: question.header, - question: question.question, - options, - multiSelect: question.multiSelect === true, - }; - }) - .filter((question): question is UserInputQuestion => question !== null); - - return parsed.length > 0 ? parsed : null; -} - function normalizeDraftAnswer(value: string | undefined): string | null { - if (typeof value !== "string") { - return null; - } + if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } @@ -219,857 +99,246 @@ function normalizeDraftAnswer(value: string | undefined): string | null { function resolvePendingUserInputAnswer( draft: PendingUserInputDraftAnswer | undefined, ): string | null { - const customAnswer = normalizeDraftAnswer(draft?.customAnswer); - if (customAnswer) { - return customAnswer; - } - return normalizeDraftAnswer(draft?.selectedOptionLabel); -} - -function deriveWorkLogEntries( - activities: ReadonlyArray, -): DerivedWorkLogEntry[] { - const ordered = Arr.sort(activities, activityOrder); - const entries: DerivedWorkLogEntry[] = []; - for (const activity of ordered) { - if (activity.kind === "tool.started") continue; - if (activity.kind === "task.started") continue; - if (activity.kind === "context-window.updated") continue; - if (activity.summary === "Checkpoint captured") continue; - if (isPlanBoundaryToolActivity(activity)) continue; - entries.push(toDerivedWorkLogEntry(activity)); - } - return collapseDerivedWorkLogEntries(entries); -} - -function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { - if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { - return false; - } - - const payload = - activity.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : null; - return typeof payload?.detail === "string" && payload.detail.startsWith("ExitPlanMode:"); -} - -function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWorkLogEntry { - const payload = - activity.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : null; - const commandPreview = extractToolCommand(payload); - const changedFiles = extractChangedFiles(payload); - const title = extractToolTitle(payload); - const isTaskActivity = activity.kind === "task.progress" || activity.kind === "task.completed"; - const taskSummary = - isTaskActivity && typeof payload?.summary === "string" && payload.summary.length > 0 - ? payload.summary - : null; - const taskDetailAsLabel = - isTaskActivity && - !taskSummary && - typeof payload?.detail === "string" && - payload.detail.length > 0 - ? payload.detail - : null; - const taskLabel = taskSummary || taskDetailAsLabel; - const entry: DerivedWorkLogEntry = { - id: activity.id, - createdAt: activity.createdAt, - turnId: activity.turnId, - label: taskLabel || activity.summary, - tone: - activity.kind === "task.progress" - ? "thinking" - : activity.tone === "approval" - ? "info" - : activity.tone, - activityKind: activity.kind, - }; - const itemType = extractWorkLogItemType(payload); - const requestKind = extractWorkLogRequestKind(payload); - if ( - !taskDetailAsLabel && - payload && - typeof payload.detail === "string" && - payload.detail.length > 0 - ) { - const detail = stripTrailingExitCode(payload.detail).output; - if (detail) { - entry.detail = detail; - } - } - if (commandPreview.command) { - entry.command = commandPreview.command; - } - if (commandPreview.rawCommand) { - entry.rawCommand = commandPreview.rawCommand; - } - if (changedFiles.length > 0) { - entry.changedFiles = changedFiles; - } - if (title) { - entry.toolTitle = title; - } - if (itemType === "mcp_tool_call") { - const data = asRecord(payload?.data); - if (data?.item !== undefined) { - entry.toolData = data.item; - } - } - if (itemType) { - entry.itemType = itemType; - } - if (requestKind) { - entry.requestKind = requestKind; - } - let toolLifecycleStatus = extractWorkLogToolLifecycleStatus(payload); - if (!toolLifecycleStatus && activity.kind === "tool.completed") { - toolLifecycleStatus = "completed"; - } - if (toolLifecycleStatus) { - entry.toolLifecycleStatus = toolLifecycleStatus; - } - const collapseKey = deriveToolLifecycleCollapseKey(entry); - if (collapseKey) { - entry.collapseKey = collapseKey; - } - return entry; -} - -function collapseDerivedWorkLogEntries( - entries: ReadonlyArray, -): DerivedWorkLogEntry[] { - const collapsed: DerivedWorkLogEntry[] = []; - for (const entry of entries) { - const previous = collapsed.at(-1); - if (previous && shouldCollapseToolLifecycleEntries(previous, entry)) { - collapsed[collapsed.length - 1] = mergeDerivedWorkLogEntries(previous, entry); - continue; - } - collapsed.push(entry); - } - return collapsed; -} - -function shouldCollapseToolLifecycleEntries( - previous: DerivedWorkLogEntry, - next: DerivedWorkLogEntry, -): boolean { - if (previous.activityKind !== "tool.updated" && previous.activityKind !== "tool.completed") { - return false; - } - if (next.activityKind !== "tool.updated" && next.activityKind !== "tool.completed") { - return false; - } - if (previous.activityKind === "tool.completed") { - return false; - } - return previous.collapseKey !== undefined && previous.collapseKey === next.collapseKey; -} - -function mergeDerivedWorkLogEntries( - previous: DerivedWorkLogEntry, - next: DerivedWorkLogEntry, -): DerivedWorkLogEntry { - const changedFiles = mergeChangedFiles(previous.changedFiles, next.changedFiles); - const detail = next.detail ?? previous.detail; - const command = next.command ?? previous.command; - const rawCommand = next.rawCommand ?? previous.rawCommand; - const toolTitle = next.toolTitle ?? previous.toolTitle; - const itemType = next.itemType ?? previous.itemType; - const requestKind = next.requestKind ?? previous.requestKind; - const collapseKey = next.collapseKey ?? previous.collapseKey; - const toolLifecycleStatus = next.toolLifecycleStatus ?? previous.toolLifecycleStatus; - const toolData = next.toolData ?? previous.toolData; - return { - ...previous, - ...next, - ...(detail ? { detail } : {}), - ...(command ? { command } : {}), - ...(rawCommand ? { rawCommand } : {}), - ...(changedFiles.length > 0 ? { changedFiles } : {}), - ...(toolTitle ? { toolTitle } : {}), - ...(itemType ? { itemType } : {}), - ...(requestKind ? { requestKind } : {}), - ...(collapseKey ? { collapseKey } : {}), - ...(toolLifecycleStatus ? { toolLifecycleStatus } : {}), - ...(toolData !== undefined ? { toolData } : {}), - }; -} - -function mergeChangedFiles( - previous: ReadonlyArray | undefined, - next: ReadonlyArray | undefined, -): string[] { - const merged = [...(previous ?? []), ...(next ?? [])]; - if (merged.length === 0) { - return []; - } - return [...new Set(merged)]; -} - -function deriveToolLifecycleCollapseKey(entry: DerivedWorkLogEntry): string | undefined { - if (entry.activityKind !== "tool.updated" && entry.activityKind !== "tool.completed") { - return undefined; - } - const normalizedLabel = normalizeCompactToolLabel(entry.toolTitle ?? entry.label); - const detail = entry.detail?.trim() ?? ""; - const itemType = entry.itemType ?? ""; - if (normalizedLabel.length === 0 && detail.length === 0 && itemType.length === 0) { - return undefined; - } - return [itemType, normalizedLabel, detail].join("\u001f"); -} - -function normalizeCompactToolLabel(value: string): string { - return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim(); -} - -function workLogEntryIsToolLike(entry: WorkLogEntry): boolean { - if (entry.tone === "tool" || entry.tone === "thinking" || entry.tone === "error") { - return true; - } - if (entry.command !== undefined && entry.command.trim().length > 0) { - return true; - } - if (entry.requestKind !== undefined) { - return true; - } - return entry.itemType !== undefined && isToolLifecycleItemType(entry.itemType); -} - -function toolDetailTextLooksLikeFailure(text: string): boolean { - const normalized = text.toLowerCase(); return ( - normalized.includes("file not found") || - normalized.includes("no files found") || - normalized.includes("enoent") || - normalized.includes("no such file or directory") || - normalized.includes("no such file") || - normalized.includes("commandnotfoundexception") || - normalized.includes("command not found") || - (normalized.includes("cannot find path") && normalized.includes("because it does not exist")) || - (normalized.includes("is not recognized") && normalized.includes("the term '")) || - //i.test(text) || - /exit(?:ed)? with exit code\s+[1-9]\d*/i.test(text) || - /exit code\s*[:\s]\s*[1-9]\d*\b/i.test(text) + normalizeDraftAnswer(draft?.customAnswer) ?? normalizeDraftAnswer(draft?.selectedOptionLabel) ); } -function workEntryIndicatesToolFailure(entry: WorkLogEntry): boolean { - if (entry.tone === "error") { - return true; - } - if (entry.toolLifecycleStatus === "failed" || entry.toolLifecycleStatus === "declined") { - return true; - } - if (!workLogEntryIsToolLike(entry)) { - return false; - } - return toolDetailTextLooksLikeFailure([entry.detail, entry.command].filter(Boolean).join("\n")); +function capitalizePhrase(value: string): string { + const trimmed = value.trim(); + return trimmed.length === 0 ? value : `${trimmed.charAt(0).toUpperCase()}${trimmed.slice(1)}`; } -function workEntryIndicatesToolSuccess(entry: WorkLogEntry): boolean { - if (!workLogEntryIsToolLike(entry) || workEntryIndicatesToolFailure(entry)) { - return false; - } - if (entry.tone === "thinking") { - return false; - } +function workEntryIsToolLike(entry: ThreadWorkEntry): boolean { return ( - entry.toolLifecycleStatus !== "inProgress" && - entry.toolLifecycleStatus !== "stopped" && - entry.toolLifecycleStatus !== "failed" && - entry.toolLifecycleStatus !== "declined" + entry.tone === "tool" || + entry.tone === "thinking" || + entry.tone === "error" || + entry.command !== undefined || + entry.requestKind !== undefined ); } -function workEntryStatus(entry: WorkLogEntry): ThreadFeedActivity["status"] { - if (!workLogEntryIsToolLike(entry)) { - return null; - } - if (workEntryIndicatesToolFailure(entry)) { - return "failure"; - } - if (workEntryIndicatesToolSuccess(entry)) { - return "success"; - } - return "neutral"; -} - -function workEntryIcon(entry: DerivedWorkLogEntry): ThreadFeedActivity["icon"] { +function workEntryStatus(entry: ThreadWorkEntry): ThreadFeedActivity["status"] { + if (!workEntryIsToolLike(entry)) return null; if ( - entry.activityKind === "user-input.requested" || - entry.activityKind === "user-input.resolved" + entry.tone === "error" || + entry.toolLifecycleStatus === "failed" || + entry.toolLifecycleStatus === "declined" ) { - return "message"; - } - if (entry.activityKind === "runtime.warning") return "warning"; - if (entry.requestKind === "command") return "command"; - if (entry.requestKind === "file-read") return "eye"; - if (entry.requestKind === "file-change") return "edit"; - if (entry.itemType === "command_execution" || entry.command) return "command"; - if (entry.itemType === "file_change" || (entry.changedFiles?.length ?? 0) > 0) return "edit"; - if (entry.itemType === "web_search") return "globe"; - if (entry.itemType === "image_view") return "eye"; - if (entry.itemType === "mcp_tool_call") return "wrench"; - if (entry.itemType === "dynamic_tool_call" || entry.itemType === "collab_agent_tool_call") { - return "hammer"; + return "failure"; } - if (entry.tone === "error") return "alert"; - if (entry.tone === "thinking") return "agent"; - if (entry.tone === "info") return "check"; - return "zap"; + return entry.toolLifecycleStatus === "completed" ? "success" : "neutral"; } -function buildWorkEntryExpandedBody(entry: WorkLogEntry): string | null { - const blocks: string[] = []; - const appendUniqueBlock = (value: string | null | undefined) => { - const trimmed = value?.trim(); - if (trimmed && !blocks.includes(trimmed)) { - blocks.push(trimmed); - } - }; - - if (entry.itemType === "mcp_tool_call" && entry.toolData !== undefined) { - appendUniqueBlock(`MCP call\n${JSON.stringify(entry.toolData, null, 2)}`); - } - appendUniqueBlock(entry.rawCommand ?? entry.command); - appendUniqueBlock(entry.detail); - if ((entry.changedFiles?.length ?? 0) > 0) { - appendUniqueBlock(entry.changedFiles!.join("\n")); +function workEntryIcon(entry: ThreadWorkEntry): ThreadFeedActivity["icon"] { + switch (entry.itemType) { + case "reasoning": + return "agent"; + case "command_execution": + return "command"; + case "file_change": + return "edit"; + case "file_search": + return "eye"; + case "web_search": + return "globe"; + case "approval_request": + case "user_input_request": + return "message"; + case "dynamic_tool": + return "wrench"; + case "subagent": + return "hammer"; + case "run_interrupt_request": + case "run_interrupt_result": + return "warning"; + case "checkpoint": + return "check"; + default: + if (entry.tone === "error") return "alert"; + if (entry.tone === "thinking") return "agent"; + if (entry.tone === "info") return "check"; + return "zap"; } - - return blocks.length > 0 ? blocks.join("\n\n") : null; } -function workEntryPreview( - workEntry: Pick, -): string | null { - if (workEntry.command) return workEntry.command; - if (workEntry.detail) return workEntry.detail; - if ((workEntry.changedFiles?.length ?? 0) === 0) return null; - const [firstPath] = workEntry.changedFiles ?? []; +function workEntryPreview(entry: ThreadWorkEntry): string | null { + if (entry.command) return entry.command; + if (entry.detail) return entry.detail; + const firstPath = entry.changedFiles?.[0]; if (!firstPath) return null; - return workEntry.changedFiles!.length === 1 + return entry.changedFiles?.length === 1 ? firstPath - : `${firstPath} +${workEntry.changedFiles!.length - 1} more`; + : `${firstPath} +${(entry.changedFiles?.length ?? 1) - 1} more`; } -function capitalizePhrase(value: string): string { - const trimmed = value.trim(); - if (trimmed.length === 0) { - return value; - } - return `${trimmed.charAt(0).toUpperCase()}${trimmed.slice(1)}`; -} - -function workEntryHeading(workEntry: WorkLogEntry): string { - if (!workEntry.toolTitle) { - return capitalizePhrase(normalizeCompactToolLabel(workEntry.label)); - } - return capitalizePhrase(normalizeCompactToolLabel(workEntry.toolTitle)); -} - -function asRecord(value: unknown): Record | null { - return value && typeof value === "object" ? (value as Record) : null; -} - -function asTrimmedString(value: unknown): string | null { - if (typeof value !== "string") { - return null; - } - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function trimMatchingOuterQuotes(value: string): string { - const trimmed = value.trim(); - if ( - (trimmed.startsWith("'") && trimmed.endsWith("'")) || - (trimmed.startsWith('"') && trimmed.endsWith('"')) - ) { - const unquoted = trimmed.slice(1, -1).trim(); - return unquoted.length > 0 ? unquoted : trimmed; - } - return trimmed; -} - -function executableBasename(value: string): string | null { - const trimmed = trimMatchingOuterQuotes(value); - if (trimmed.length === 0) { - return null; - } - const normalized = trimmed.replace(/\\/g, "/"); - const segments = normalized.split("/"); - const last = segments.at(-1)?.trim() ?? ""; - return last.length > 0 ? last.toLowerCase() : null; -} - -function splitExecutableAndRest(value: string): { executable: string; rest: string } | null { - const trimmed = value.trim(); - if (trimmed.length === 0) { - return null; - } - - if (trimmed.startsWith('"') || trimmed.startsWith("'")) { - const quote = trimmed.charAt(0); - const closeIndex = trimmed.indexOf(quote, 1); - if (closeIndex <= 0) { - return null; - } - return { - executable: trimmed.slice(0, closeIndex + 1), - rest: trimmed.slice(closeIndex + 1).trim(), - }; - } - - const firstWhitespace = trimmed.search(/\s/); - if (firstWhitespace < 0) { - return { - executable: trimmed, - rest: "", - }; - } - - return { - executable: trimmed.slice(0, firstWhitespace), - rest: trimmed.slice(firstWhitespace).trim(), - }; -} - -const SHELL_WRAPPER_SPECS = [ - { - executables: ["pwsh", "pwsh.exe", "powershell", "powershell.exe"], - wrapperFlagPattern: /(?:^|\s)-command\s+/i, - }, - { - executables: ["cmd", "cmd.exe"], - wrapperFlagPattern: /(?:^|\s)\/c\s+/i, - }, - { - executables: ["bash", "sh", "zsh"], - wrapperFlagPattern: /(?:^|\s)-(?:l)?c\s+/i, - }, -] as const; - -function findShellWrapperSpec(shell: string) { - return SHELL_WRAPPER_SPECS.find((spec) => - (spec.executables as ReadonlyArray).includes(shell), - ); -} - -function unwrapCommandRemainder(value: string, wrapperFlagPattern: RegExp): string | null { - const match = wrapperFlagPattern.exec(value); - if (!match) { - return null; - } - - const command = value.slice(match.index + match[0].length).trim(); - if (command.length === 0) { - return null; - } - - const unwrapped = trimMatchingOuterQuotes(command); - return unwrapped.length > 0 ? unwrapped : null; -} - -function unwrapKnownShellCommandWrapper(value: string): string { - const split = splitExecutableAndRest(value); - if (!split || split.rest.length === 0) { - return value; - } - - const shell = executableBasename(split.executable); - if (!shell) { - return value; - } - - const spec = findShellWrapperSpec(shell); - if (!spec) { - return value; - } - - return unwrapCommandRemainder(split.rest, spec.wrapperFlagPattern) ?? value; -} - -function formatCommandArrayPart(value: string): string { - return /[\s"'`]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value; -} - -function formatCommandValue(value: unknown): string | null { - const direct = asTrimmedString(value); - if (direct) { - return direct; - } - if (!Array.isArray(value)) { - return null; - } - const parts: Array = []; - for (const entry of value) { - const part = asTrimmedString(entry); - if (part !== null) { - parts.push(part); - } - } - if (parts.length === 0) { - return null; - } - return parts.map((part) => formatCommandArrayPart(part)).join(" "); -} - -function normalizeCommandValue(value: unknown): string | null { - const formatted = formatCommandValue(value); - return formatted ? unwrapKnownShellCommandWrapper(formatted) : null; -} - -function toRawToolCommand(value: unknown, normalizedCommand: string | null): string | null { - const formatted = formatCommandValue(value); - if (!formatted || normalizedCommand === null) { - return null; - } - return formatted === normalizedCommand ? null : formatted; -} - -function extractToolCommand(payload: Record | null): { - command: string | null; - rawCommand: string | null; -} { - const data = asRecord(payload?.data); - const item = asRecord(data?.item); - const itemResult = asRecord(item?.result); - const itemInput = asRecord(item?.input); - const itemType = asTrimmedString(payload?.itemType); - const detail = asTrimmedString(payload?.detail); - const candidates: unknown[] = [ - item?.command, - itemInput?.command, - itemResult?.command, - data?.command, - itemType === "command_execution" && detail ? stripTrailingExitCode(detail).output : null, - ]; - - for (const candidate of candidates) { - const command = normalizeCommandValue(candidate); - if (!command) { - continue; - } - return { - command, - rawCommand: toRawToolCommand(candidate, command), - }; - } - - return { - command: null, - rawCommand: null, +function buildWorkEntryExpandedBody(entry: ThreadWorkEntry): string | null { + const blocks: string[] = []; + const append = (value: string | null | undefined) => { + const trimmed = value?.trim(); + if (trimmed && !blocks.includes(trimmed)) blocks.push(trimmed); }; + append(entry.rawCommand ?? entry.command); + append(entry.detail); + if (entry.changedFiles?.length) append(entry.changedFiles.join("\n")); + append(JSON.stringify(entry.structuredPayload, null, 2)); + return blocks.length === 0 ? null : blocks.join("\n\n"); } -function extractToolTitle(payload: Record | null): string | null { - return asTrimmedString(payload?.title); -} - -function extractWorkLogToolLifecycleStatus( - payload: Record | null, -): WorkLogToolLifecycleStatus | undefined { - const status = payload?.status; - if ( - status === "inProgress" || - status === "completed" || - status === "failed" || - status === "declined" || - status === "stopped" - ) { - return status; - } - return undefined; -} - -function stripTrailingExitCode(value: string): { - output: string | null; - exitCode?: number | undefined; -} { - const trimmed = value.trim(); - const match = /^(?[\s\S]*?)(?:\s*\d+)>)\s*$/i.exec( - trimmed, - ); - if (!match?.groups) { - return { - output: trimmed.length > 0 ? trimmed : null, - }; - } - const exitCode = Number.parseInt(match.groups.code ?? "", 10); - const normalizedOutput = match.groups.output?.trim() ?? ""; +function toFeedActivity(entry: ThreadWorkEntry): ThreadFeedActivity { + const summary = capitalizePhrase(entry.toolTitle ?? entry.label); + const detail = workEntryPreview(entry); + const fullDetail = buildWorkEntryExpandedBody(entry); return { - output: normalizedOutput.length > 0 ? normalizedOutput : null, - ...(Number.isInteger(exitCode) ? { exitCode } : {}), + id: entry.id, + createdAt: entry.createdAt, + runId: entry.runId, + summary, + detail, + fullDetail, + icon: workEntryIcon(entry), + copyText: [summary, detail, fullDetail] + .filter( + (value, index, values): value is string => + Boolean(value) && values.indexOf(value) === index, + ) + .join("\n"), + toolLike: workEntryIsToolLike(entry), + status: workEntryStatus(entry), }; } -function extractWorkLogItemType( - payload: Record | null, -): WorkLogEntry["itemType"] | undefined { - if (typeof payload?.itemType === "string" && isToolLifecycleItemType(payload.itemType)) { - return payload.itemType; - } - return undefined; -} - -function extractWorkLogRequestKind( - payload: Record | null, -): WorkLogEntry["requestKind"] | undefined { - if ( - payload?.requestKind === "command" || - payload?.requestKind === "file-read" || - payload?.requestKind === "file-change" - ) { - return payload.requestKind; - } - return requestKindFromRequestType(payload?.requestType) ?? undefined; -} - -function pushChangedFile(target: string[], seen: Set, value: unknown) { - const normalized = asTrimmedString(value); - if (!normalized || seen.has(normalized)) { - return; - } - seen.add(normalized); - target.push(normalized); +function byCreatedAt(left: A, right: A): number { + return left.createdAt.localeCompare(right.createdAt); } -function collectChangedFiles(value: unknown, target: string[], seen: Set, depth: number) { - if (depth > 4 || target.length >= 12) { - return; - } - if (Array.isArray(value)) { - for (const entry of value) { - collectChangedFiles(entry, target, seen, depth + 1); - if (target.length >= 12) { - return; - } - } - return; - } - - const record = asRecord(value); - if (!record) { - return; - } - - pushChangedFile(target, seen, record.path); - pushChangedFile(target, seen, record.filePath); - pushChangedFile(target, seen, record.relativePath); - pushChangedFile(target, seen, record.filename); - pushChangedFile(target, seen, record.newPath); - pushChangedFile(target, seen, record.oldPath); - - for (const nestedKey of [ - "item", - "result", - "input", - "data", - "changes", - "files", - "edits", - "patch", - "patches", - "operations", - ]) { - if (!(nestedKey in record)) { - continue; - } - collectChangedFiles(record[nestedKey], target, seen, depth + 1); - if (target.length >= 12) { - return; - } - } -} - -function extractChangedFiles(payload: Record | null): string[] { - const changedFiles: string[] = []; - const seen = new Set(); - collectChangedFiles(asRecord(payload?.data), changedFiles, seen, 0); - return changedFiles; -} - -function compareActivityLifecycleRank(kind: string): number { - if (kind.endsWith(".started") || kind === "tool.started") { - return 0; - } - if (kind.endsWith(".progress") || kind.endsWith(".updated")) { - return 1; - } - if (kind.endsWith(".completed") || kind.endsWith(".resolved")) { - return 2; - } - return 1; -} - -const activityOrder = Order.combineAll([ - Order.mapInput(Order.Number, (activity) => activity.sequence ?? Number.MAX_SAFE_INTEGER), - Order.mapInput(Order.String, (activity) => activity.createdAt), - Order.mapInput(Order.Number, (activity) => compareActivityLifecycleRank(activity.kind)), - Order.mapInput(Order.String, (activity) => activity.id), -]); - function isEmptyMessage(entry: RawThreadFeedEntry): boolean { - if (entry.type !== "message") { - return false; - } - const hasText = entry.message.text.trim().length > 0; - const hasAttachments = (entry.message.attachments ?? []).length > 0; - return !hasText && !hasAttachments; + return ( + entry.type === "message" && + entry.message.text.trim().length === 0 && + (entry.message.attachments ?? []).length === 0 + ); } function groupAdjacentActivities(entries: ReadonlyArray): ThreadFeedEntry[] { const grouped: ThreadFeedEntry[] = []; - for (const entry of entries) { - // Skip empty messages so they don't break activity grouping. - if (isEmptyMessage(entry)) { - continue; - } - + if (isEmptyMessage(entry)) continue; if (entry.type !== "activity") { grouped.push(entry); continue; } - const previous = grouped.at(-1); - if (previous?.type === "activity-group" && previous.turnId === entry.turnId) { + if (previous?.type === "activity-group" && previous.runId === entry.runId) { grouped[grouped.length - 1] = { ...previous, activities: [...previous.activities, entry.activity], }; continue; } - grouped.push({ type: "activity-group", id: entry.id, createdAt: entry.createdAt, - turnId: entry.turnId, + runId: entry.runId, activities: [entry.activity], }); } - return grouped; } function computeElapsedMs(startIso: string, endIso: string): number | null { const start = Date.parse(startIso); const end = Date.parse(endIso); - if (!Number.isFinite(start) || !Number.isFinite(end)) { - return null; - } - return Math.max(0, end - start); + return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null; } -function maxIsoTimestamp(a: string | null, b: string | null): string | null { - if (a === null) return b; - if (b === null) return a; - const aMs = Date.parse(a); - const bMs = Date.parse(b); - if (!Number.isFinite(aMs)) return b; - if (!Number.isFinite(bMs)) return a; - return bMs > aMs ? b : a; +function maxIsoTimestamp(left: string | null, right: string | null): string | null { + if (left === null) return right; + if (right === null) return left; + return Date.parse(right) > Date.parse(left) ? right : left; } -function deriveUnsettledTurnId(latestTurn: ThreadFeedLatestTurn | null): TurnId | null { - if (!latestTurn) { - return null; - } - const settled = latestTurn.completedAt !== null && latestTurn.state !== "running"; - return settled ? null : latestTurn.turnId; +function unsettledRunId(latestRun: ThreadFeedLatestRun | null): RunId | null { + if (!latestRun) return null; + return latestRun.completedAt === null || + latestRun.status === "starting" || + latestRun.status === "running" || + latestRun.status === "waiting" + ? latestRun.runId + : null; } -interface ThreadFeedTurnFold { - readonly turnId: TurnId; +interface ThreadFeedRunFold { + readonly runId: RunId; readonly createdAt: string; readonly hiddenEntryIds: ReadonlySet; readonly label: string; } -function deriveThreadFeedTurnFolds( +function deriveThreadFeedRunFolds( feed: ReadonlyArray, - latestTurn: ThreadFeedLatestTurn | null, -): ReadonlyMap { - const terminalAssistantMessageIdByTurn = new Map(); + latestRun: ThreadFeedLatestRun | null, +): ReadonlyMap { + const terminalAssistantMessageIdByRun = new Map(); for (const entry of feed) { - if (entry.type === "message" && entry.message.role === "assistant" && entry.message.turnId) { - terminalAssistantMessageIdByTurn.set(entry.message.turnId, entry.id); + if (entry.type === "message" && entry.message.role === "assistant" && entry.message.runId) { + terminalAssistantMessageIdByRun.set(entry.message.runId, entry.id); } } - interface TurnGroup { - readonly entries: ThreadFeedEntry[]; - readonly startBoundary: string | null; - } - const groupsByTurnId = new Map(); + const groupsByRunId = new Map< + RunId, + { entries: ThreadFeedEntry[]; startBoundary: string | null } + >(); let pendingUserBoundary: string | null = null; for (const entry of feed) { if (entry.type === "message" && entry.message.role === "user") { pendingUserBoundary = entry.message.createdAt; continue; } - const turnId = + const runId = entry.type === "message" && entry.message.role === "assistant" - ? entry.message.turnId + ? entry.message.runId : entry.type === "activity-group" - ? entry.turnId + ? entry.runId : null; - if (!turnId) { - continue; - } - let group = groupsByTurnId.get(turnId); + if (!runId) continue; + let group = groupsByRunId.get(runId); if (!group) { - group = { - entries: [], - startBoundary: pendingUserBoundary, - }; + group = { entries: [], startBoundary: pendingUserBoundary }; pendingUserBoundary = null; - groupsByTurnId.set(turnId, group); + groupsByRunId.set(runId, group); } group.entries.push(entry); } - const unsettledTurnId = deriveUnsettledTurnId(latestTurn); - const foldsByAnchorId = new Map(); - for (const [turnId, group] of groupsByTurnId) { - const { entries } = group; - if (turnId === unsettledTurnId) { - continue; - } - if (entries.some((entry) => entry.type === "message" && entry.message.streaming)) { + const activeRunId = unsettledRunId(latestRun); + const foldsByAnchorId = new Map(); + for (const [runId, group] of groupsByRunId) { + if ( + runId === activeRunId || + group.entries.some((entry) => entry.type === "message" && entry.message.streaming) + ) { continue; } - - const terminalAssistantMessageId = terminalAssistantMessageIdByTurn.get(turnId); + const terminalAssistantId = terminalAssistantMessageIdByRun.get(runId); const hiddenEntryIds = new Set( - entries.filter((entry) => entry.id !== terminalAssistantMessageId).map((entry) => entry.id), + group.entries.filter((entry) => entry.id !== terminalAssistantId).map((entry) => entry.id), ); - if (hiddenEntryIds.size === 0) { - continue; - } - - const firstEntry = entries[0]; - const lastEntry = entries.at(-1); - if (!firstEntry || !lastEntry) { - continue; - } - const terminalEntry = terminalAssistantMessageId - ? entries.find((entry) => entry.id === terminalAssistantMessageId) + const firstEntry = group.entries[0]; + const lastEntry = group.entries.at(-1); + if (hiddenEntryIds.size === 0 || !firstEntry || !lastEntry) continue; + const terminalEntry = terminalAssistantId + ? group.entries.find((entry) => entry.id === terminalAssistantId) : null; - const latestTurnMatches = latestTurn?.turnId === turnId; + const latestRunMatches = latestRun?.runId === runId; const lastEntryEnd = lastEntry.type === "message" ? lastEntry.message.updatedAt : lastEntry.createdAt; const elapsedMs = - latestTurnMatches && latestTurn.startedAt && latestTurn.completedAt - ? computeElapsedMs(latestTurn.startedAt, latestTurn.completedAt) + latestRunMatches && latestRun.startedAt && latestRun.completedAt + ? computeElapsedMs(latestRun.startedAt, latestRun.completedAt) : computeElapsedMs( group.startBoundary ?? firstEntry.createdAt, maxIsoTimestamp( @@ -1078,20 +347,19 @@ function deriveThreadFeedTurnFolds( ) ?? lastEntryEnd, ); const duration = elapsedMs === null ? null : formatDuration(elapsedMs); - const interrupted = latestTurnMatches && latestTurn.state === "interrupted"; - const label = interrupted - ? duration - ? `You stopped after ${duration}` - : "You stopped this response" - : duration - ? `Worked for ${duration}` - : "Worked"; - + const interrupted = + latestRunMatches && (latestRun.status === "interrupted" || latestRun.status === "cancelled"); foldsByAnchorId.set(firstEntry.id, { - turnId, + runId, createdAt: firstEntry.createdAt, hiddenEntryIds, - label, + label: interrupted + ? duration + ? `You stopped after ${duration}` + : "You stopped this response" + : duration + ? `Worked for ${duration}` + : "Worked", }); } return foldsByAnchorId; @@ -1099,227 +367,92 @@ function deriveThreadFeedTurnFolds( export function deriveThreadFeedPresentation( feed: ReadonlyArray, - latestTurn: ThreadFeedLatestTurn | null, - expandedTurnIds: ReadonlySet, + latestRun: ThreadFeedLatestRun | null, + expandedRunIds: ReadonlySet, ): ThreadFeedEntry[] { - const sourceFeed = feed.filter((entry) => entry.type !== "turn-fold"); - const foldsByAnchorId = deriveThreadFeedTurnFolds(sourceFeed, latestTurn); + const sourceFeed = feed.filter((entry) => entry.type !== "run-fold"); + const foldsByAnchorId = deriveThreadFeedRunFolds(sourceFeed, latestRun); const collapsedEntryIds = new Set(); for (const fold of foldsByAnchorId.values()) { - if (!expandedTurnIds.has(fold.turnId)) { - for (const entryId of fold.hiddenEntryIds) { - collapsedEntryIds.add(entryId); - } + if (!expandedRunIds.has(fold.runId)) { + for (const entryId of fold.hiddenEntryIds) collapsedEntryIds.add(entryId); } } - const result: ThreadFeedEntry[] = []; for (const entry of sourceFeed) { const fold = foldsByAnchorId.get(entry.id); if (fold) { result.push({ - type: "turn-fold", - id: `turn-fold:${fold.turnId}`, + type: "run-fold", + id: `run-fold:${fold.runId}`, createdAt: fold.createdAt, - turnId: fold.turnId, + runId: fold.runId, label: fold.label, - expanded: expandedTurnIds.has(fold.turnId), + expanded: expandedRunIds.has(fold.runId), }); } - if (!collapsedEntryIds.has(entry.id)) { - result.push(entry); - } + if (!collapsedEntryIds.has(entry.id)) result.push(entry); } return result; } -export function derivePendingApprovals( - activities: ReadonlyArray, -): PendingApproval[] { - const openByRequestId = new Map(); - const ordered = Arr.sort(activities, activityOrder); - - for (const activity of ordered) { - const payload = - activity.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : null; - const requestId = parseApprovalRequestId(payload?.requestId); - const requestKind = - payload?.requestKind === "command" || - payload?.requestKind === "file-read" || - payload?.requestKind === "file-change" - ? payload.requestKind - : requestKindFromRequestType(payload?.requestType); - const detail = typeof payload?.detail === "string" ? payload.detail : undefined; - - if (activity.kind === "approval.requested" && requestId && requestKind) { - openByRequestId.set(requestId, { - requestId, - requestKind, - createdAt: activity.createdAt, - ...(detail ? { detail } : {}), - }); - continue; - } - - if (activity.kind === "approval.resolved" && requestId) { - openByRequestId.delete(requestId); - continue; - } - - if ( - activity.kind === "provider.approval.respond.failed" && - requestId && - isStalePendingRequestFailureDetail(detail) - ) { - openByRequestId.delete(requestId); - } - } - - return Arr.sortWith([...openByRequestId.values()], (s) => new Date(s.createdAt), Order.Date); -} - -export function derivePendingUserInputs( - activities: ReadonlyArray, -): PendingUserInput[] { - const openByRequestId = new Map(); - const ordered = Arr.sort(activities, activityOrder); - - for (const activity of ordered) { - const payload = - activity.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : null; - const requestId = parseApprovalRequestId(payload?.requestId); - const detail = typeof payload?.detail === "string" ? payload.detail : undefined; - - if (activity.kind === "user-input.requested" && requestId) { - const questions = parseUserInputQuestions(payload); - if (!questions) { - continue; - } - openByRequestId.set(requestId, { - requestId, - createdAt: activity.createdAt, - questions, - }); - continue; - } - - if (activity.kind === "user-input.resolved" && requestId) { - openByRequestId.delete(requestId); - continue; - } - - if ( - activity.kind === "provider.user-input.respond.failed" && - requestId && - isStalePendingRequestFailureDetail(detail) - ) { - openByRequestId.delete(requestId); - } - } - - return Arr.sortWith(openByRequestId.values(), (s) => new Date(s.createdAt), Order.Date); -} - export function setPendingUserInputCustomAnswer( draft: PendingUserInputDraftAnswer | undefined, customAnswer: string, ): PendingUserInputDraftAnswer { const selectedOptionLabel = customAnswer.trim().length > 0 ? undefined : draft?.selectedOptionLabel; - return { - customAnswer, - ...(selectedOptionLabel ? { selectedOptionLabel } : {}), - }; + return { customAnswer, ...(selectedOptionLabel ? { selectedOptionLabel } : {}) }; } export function buildPendingUserInputAnswers( - questions: ReadonlyArray, + questions: ReadonlyArray, draftAnswers: Record, ): Record | null { const answers: Record = {}; - for (const question of questions) { const answer = resolvePendingUserInputAnswer(draftAnswers[question.id]); - if (!answer) { - return null; - } + if (!answer) return null; answers[question.id] = answer; } - return answers; } export function buildThreadFeed( - thread: OrchestrationThread, + thread: EnvironmentThread, queuedMessages: ReadonlyArray, dispatchingQueuedMessageId: MessageId | null, - options?: { - readonly loadedMessages?: ReadonlyArray; - }, + options?: { readonly loadedMessages?: ReadonlyArray }, ): ThreadFeedEntry[] { const loadedMessages = options?.loadedMessages ?? thread.messages; const oldestLoadedMessageCreatedAt = - options?.loadedMessages !== undefined ? (loadedMessages[0]?.createdAt ?? null) : null; - const workLogEntries = deriveWorkLogEntries(thread.activities); - const entries = Arr.sortWith( - [ - ...loadedMessages.map((message) => ({ - type: "message", - id: message.id, - createdAt: message.createdAt, - message, - })), - ...queuedMessages.map((queuedMessage) => ({ - type: "queued-message", - id: queuedMessage.messageId, - createdAt: queuedMessage.createdAt, - queuedMessage, - sending: queuedMessage.messageId === dispatchingQueuedMessageId, + options?.loadedMessages === undefined ? null : (loadedMessages[0]?.createdAt ?? null); + const entries: RawThreadFeedEntry[] = [ + ...loadedMessages.map((message) => ({ + type: "message" as const, + id: message.id, + createdAt: message.createdAt, + message, + })), + ...queuedMessages.map((queuedMessage) => ({ + type: "queued-message" as const, + id: queuedMessage.messageId, + createdAt: queuedMessage.createdAt, + queuedMessage, + sending: queuedMessage.messageId === dispatchingQueuedMessageId, + })), + ...thread.workEntries + .filter( + (entry) => + oldestLoadedMessageCreatedAt === null || entry.createdAt >= oldestLoadedMessageCreatedAt, + ) + .map((entry) => ({ + type: "activity" as const, + id: entry.id, + createdAt: entry.createdAt, + runId: entry.runId, + activity: toFeedActivity(entry), })), - ...workLogEntries - .filter((entry) => { - if (options?.loadedMessages === undefined) { - return true; - } - return ( - oldestLoadedMessageCreatedAt === null || entry.createdAt >= oldestLoadedMessageCreatedAt - ); - }) - .map((entry) => { - const summary = workEntryHeading(entry); - const detail = workEntryPreview(entry); - const fullDetail = buildWorkEntryExpandedBody(entry); - return { - type: "activity", - id: entry.id, - createdAt: entry.createdAt, - turnId: entry.turnId, - activity: { - id: entry.id, - createdAt: entry.createdAt, - turnId: entry.turnId, - summary, - detail, - fullDetail, - icon: workEntryIcon(entry), - copyText: [summary, detail, fullDetail] - .filter((value, index, values): value is string => { - return Boolean(value) && values.indexOf(value) === index; - }) - .join("\n"), - toolLike: workLogEntryIsToolLike(entry), - status: workEntryStatus(entry), - }, - }; - }), - ], - (s) => new Date(s.createdAt), - Order.Date, - ); - - return groupAdjacentActivities(entries); + ]; + return groupAdjacentActivities(entries.toSorted(byCreatedAt)); } diff --git a/apps/mobile/src/state/queries.ts b/apps/mobile/src/state/queries.ts index ea625995928..32912450061 100644 --- a/apps/mobile/src/state/queries.ts +++ b/apps/mobile/src/state/queries.ts @@ -1,4 +1,5 @@ -import type { EnvironmentId, OrchestrationThread, ThreadId } from "@t3tools/contracts"; +import type { EnvironmentThread } from "@t3tools/client-runtime/state/shell"; +import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import * as Option from "effect/Option"; import { useEffect, useMemo, useState } from "react"; @@ -18,7 +19,7 @@ const COMPOSER_PATH_SEARCH_LIMIT = 20; const VCS_REF_LIST_LIMIT = 100; export interface ThreadDetailView { - readonly data: OrchestrationThread | null; + readonly data: EnvironmentThread | null; readonly error: string | null; readonly isPending: boolean; readonly isDeleted: boolean; diff --git a/apps/mobile/src/state/threads.ts b/apps/mobile/src/state/threads.ts index 7f247123051..09aa68a6feb 100644 --- a/apps/mobile/src/state/threads.ts +++ b/apps/mobile/src/state/threads.ts @@ -7,6 +7,7 @@ import { type EnvironmentThreadState, createThreadEnvironmentAtoms, } from "@t3tools/client-runtime/state/threads"; +import { presentThread, type EnvironmentThread } from "@t3tools/client-runtime/state/shell"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import * as Option from "effect/Option"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; @@ -32,14 +33,21 @@ const EMPTY_THREAD_STATE_ATOM = Atom.make(AsyncResult.success(EMPTY_ENVIRONMENT_ export function useEnvironmentThread( environmentId: EnvironmentId | null, threadId: ThreadId | null, -): EnvironmentThreadState { +): Omit & { readonly data: Option.Option } { const result = useAtomValue( environmentId !== null && threadId !== null ? environmentThreads.stateAtom(environmentId, threadId) : EMPTY_THREAD_STATE_ATOM, ); - return Option.getOrElse( + const state = Option.getOrElse( AsyncResult.value(result), () => EMPTY_ENVIRONMENT_THREAD_STATE, ) as EnvironmentThreadState; + return { + ...state, + data: + environmentId === null + ? Option.none() + : Option.map(state.data, (projection) => presentThread(environmentId, projection)), + }; } diff --git a/apps/mobile/src/state/use-selected-thread-requests.ts b/apps/mobile/src/state/use-selected-thread-requests.ts index c9e9db12530..2845fd7baa2 100644 --- a/apps/mobile/src/state/use-selected-thread-requests.ts +++ b/apps/mobile/src/state/use-selected-thread-requests.ts @@ -1,15 +1,13 @@ import { useAtomValue } from "@effect/atom-react"; import { useCallback, useMemo, useState } from "react"; -import { ApprovalRequestId, type ProviderApprovalDecision } from "@t3tools/contracts"; +import { type ProviderApprovalDecision, type RuntimeRequestId } from "@t3tools/contracts"; import { Atom } from "effect/unstable/reactivity"; import { threadEnvironment } from "../state/threads"; import { scopedRequestKey } from "../lib/scopedEntities"; import { buildPendingUserInputAnswers, - derivePendingApprovals, - derivePendingUserInputs, setPendingUserInputCustomAnswer, type PendingUserInputDraftAnswer, } from "../lib/threadActivity"; @@ -65,18 +63,16 @@ export function useSelectedThreadRequests() { const { selectedThread: selectedThreadShell } = useThreadSelection(); const selectedThread = useSelectedThreadDetail(); const userInputDraftsByRequestKey = useAtomValue(userInputDraftsByRequestKeyAtom); - const [respondingApprovalId, setRespondingApprovalId] = useState(null); - const [respondingUserInputId, setRespondingUserInputId] = useState( - null, - ); + const [respondingApprovalId, setRespondingApprovalId] = useState(null); + const [respondingUserInputId, setRespondingUserInputId] = useState(null); const activePendingApprovals = useMemo( - () => (selectedThread ? derivePendingApprovals(selectedThread.activities) : []), + () => selectedThread?.pendingApprovals ?? [], [selectedThread], ); const activePendingApproval = activePendingApprovals[0] ?? null; const activePendingUserInputs = useMemo( - () => (selectedThread ? derivePendingUserInputs(selectedThread.activities) : []), + () => selectedThread?.pendingUserInputs ?? [], [selectedThread], ); const activePendingUserInput = activePendingUserInputs[0] ?? null; @@ -91,7 +87,7 @@ export function useSelectedThreadRequests() { : null; const onSelectUserInputOption = useCallback( - (requestId: ApprovalRequestId, questionId: string, label: string) => { + (requestId: RuntimeRequestId, questionId: string, label: string) => { if (!selectedThreadShell) { return; } @@ -103,7 +99,7 @@ export function useSelectedThreadRequests() { ); const onChangeUserInputCustomAnswer = useCallback( - (requestId: ApprovalRequestId, questionId: string, customAnswer: string) => { + (requestId: RuntimeRequestId, questionId: string, customAnswer: string) => { if (!selectedThreadShell) { return; } @@ -115,10 +111,16 @@ export function useSelectedThreadRequests() { ); const onRespondToApproval = useCallback( - async (requestId: ApprovalRequestId, decision: ProviderApprovalDecision) => { + async (requestId: RuntimeRequestId, decision: ProviderApprovalDecision) => { if (!selectedThreadShell) { return; } + if ( + activePendingApprovals.find((approval) => approval.requestId === requestId) + ?.responseCapability !== "live" + ) { + return; + } setRespondingApprovalId(requestId); const result = await respondToApproval({ @@ -132,11 +134,16 @@ export function useSelectedThreadRequests() { setRespondingApprovalId((current) => (current === requestId ? null : current)); return result; }, - [respondToApproval, selectedThreadShell], + [activePendingApprovals, respondToApproval, selectedThreadShell], ); const onSubmitUserInput = useCallback(async () => { - if (!selectedThreadShell || !activePendingUserInput || !activePendingUserInputAnswers) { + if ( + !selectedThreadShell || + !activePendingUserInput || + activePendingUserInput.responseCapability !== "live" || + !activePendingUserInputAnswers + ) { return; } diff --git a/apps/mobile/src/state/use-thread-composer-state.ts b/apps/mobile/src/state/use-thread-composer-state.ts index 60970b32a4d..df999f6e54a 100644 --- a/apps/mobile/src/state/use-thread-composer-state.ts +++ b/apps/mobile/src/state/use-thread-composer-state.ts @@ -1,4 +1,5 @@ import { useAtomValue } from "@effect/atom-react"; +import { threadRuntimeIsActive } from "@t3tools/client-runtime/state/shell"; import { useCallback, useEffect, useMemo } from "react"; import { CommandId, MessageId, type EnvironmentId, type ThreadId } from "@t3tools/contracts"; @@ -101,13 +102,13 @@ export function useThreadComposerState() { const selectedThreadSessionActivity = useMemo(() => { const selectedThread = selectedThreadDetail ?? selectedThreadShell; - if (!selectedThread?.session) { + if (!selectedThread?.runtime) { return null; } return { - orchestrationStatus: selectedThread.session.status, - activeTurnId: selectedThread.session.activeTurnId ?? undefined, + orchestrationStatus: selectedThread.runtime.status, + activeRunId: selectedThread.runtime.activeRunId ?? undefined, }; }, [selectedThreadDetail, selectedThreadShell]); @@ -119,7 +120,7 @@ export function useThreadComposerState() { } return deriveActiveWorkStartedAt( - selectedThread.latestTurn, + selectedThread.latestRun, selectedThreadSessionActivity, queuedSendStartedAt, ); @@ -131,9 +132,7 @@ export function useThreadComposerState() { ]); const selectedThread = selectedThreadDetail ?? selectedThreadShell; - const activeThreadBusy = - !!selectedThread && - (selectedThread.session?.status === "running" || selectedThread.session?.status === "starting"); + const activeThreadBusy = !!selectedThread && threadRuntimeIsActive(selectedThread.runtime); const onSendMessage = useCallback(async () => { if (!selectedThreadShell) { diff --git a/apps/mobile/src/state/use-thread-outbox-drain.ts b/apps/mobile/src/state/use-thread-outbox-drain.ts index 840456e2d1c..31fee2b1bc5 100644 --- a/apps/mobile/src/state/use-thread-outbox-drain.ts +++ b/apps/mobile/src/state/use-thread-outbox-drain.ts @@ -1,5 +1,8 @@ import { useAtomValue } from "@effect/atom-react"; -import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import { + threadRuntimeIsActive, + type EnvironmentThreadShell, +} from "@t3tools/client-runtime/state/shell"; import { type MessageId } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; @@ -72,6 +75,7 @@ export function useThreadOutboxDrain(): void { environmentId: queuedMessage.environmentId, input: { commandId: queuedMessage.commandId, + creationSource: "mobile", threadId: queuedMessage.threadId, message: { messageId: queuedMessage.messageId, @@ -142,7 +146,7 @@ export function useThreadOutboxDrain(): void { threadExists: thread !== undefined, shellStatus: shellStatuses.get(nextQueuedMessage.environmentId) ?? "empty", environmentConnected: environment?.connectionState === "connected", - threadBusy: thread?.session?.status === "running" || thread?.session?.status === "starting", + threadBusy: threadRuntimeIsActive(thread?.runtime), }); if (deliveryAction === "wait") { continue; diff --git a/apps/mobile/src/test-fixtures.ts b/apps/mobile/src/test-fixtures.ts new file mode 100644 index 00000000000..9b6b6ee08fa --- /dev/null +++ b/apps/mobile/src/test-fixtures.ts @@ -0,0 +1,130 @@ +import { + presentThread, + presentThreadShell, + type EnvironmentThread, + type EnvironmentThreadShell, +} from "@t3tools/client-runtime/state/shell"; +import { + EnvironmentId, + ProjectId, + ProviderInstanceId, + ThreadId, + type OrchestrationV2ThreadProjection, + type OrchestrationV2ThreadShell, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; + +const DEFAULT_TIMESTAMP = "2026-01-01T00:00:00.000Z"; + +export function makeRawThreadShell( + input: Partial = {}, +): OrchestrationV2ThreadShell { + const id = input.id ?? ThreadId.make("thread-test"); + const providerInstanceId = input.providerInstanceId ?? ProviderInstanceId.make("codex"); + const now = DateTime.makeUnsafe(DEFAULT_TIMESTAMP); + return { + id, + projectId: ProjectId.make("project-test"), + title: "Thread", + providerInstanceId, + modelSelection: { instanceId: providerInstanceId, model: "gpt-5.4" }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: null, + lineage: { rootThreadId: id, parentThreadId: null, relationshipToParent: null }, + forkedFrom: null, + createdBy: "user", + creationSource: "mobile", + latestRunId: null, + activeRunId: null, + status: "idle", + pendingRuntimeRequest: null, + latestVisibleMessage: null, + latestUserMessageAt: null, + hasActionableProposedPlan: false, + itemCount: 0, + visibleItemCount: 0, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + ...input, + }; +} + +export function makeThreadShellFixture( + overrides: Partial = {}, +): EnvironmentThreadShell { + const environmentId = overrides.environmentId ?? EnvironmentId.make("environment-test"); + const raw = makeRawThreadShell({ + id: overrides.id, + projectId: overrides.projectId, + title: overrides.title, + providerInstanceId: overrides.providerInstanceId, + modelSelection: overrides.modelSelection, + runtimeMode: overrides.runtimeMode, + interactionMode: overrides.interactionMode, + branch: overrides.branch, + worktreePath: overrides.worktreePath, + }); + return { ...presentThreadShell(environmentId, raw), ...overrides }; +} + +export function makeThreadFixture(overrides: Partial = {}): EnvironmentThread { + const shell = makeRawThreadShell({ + id: overrides.id, + projectId: overrides.projectId, + title: overrides.title, + providerInstanceId: overrides.providerInstanceId, + modelSelection: overrides.modelSelection, + runtimeMode: overrides.runtimeMode, + interactionMode: overrides.interactionMode, + branch: overrides.branch, + worktreePath: overrides.worktreePath, + }); + const projection: OrchestrationV2ThreadProjection = { + thread: { + id: shell.id, + projectId: shell.projectId, + title: shell.title, + providerInstanceId: shell.providerInstanceId, + modelSelection: shell.modelSelection, + runtimeMode: shell.runtimeMode, + interactionMode: shell.interactionMode, + branch: shell.branch, + worktreePath: shell.worktreePath, + activeProviderThreadId: shell.activeProviderThreadId, + lineage: shell.lineage, + forkedFrom: shell.forkedFrom, + createdBy: shell.createdBy, + creationSource: shell.creationSource, + createdAt: shell.createdAt, + updatedAt: shell.updatedAt, + archivedAt: shell.archivedAt, + deletedAt: shell.deletedAt, + }, + runs: [], + attempts: [], + nodes: [], + subagents: [], + providerSessions: [], + providerThreads: [], + providerTurns: [], + runtimeRequests: [], + messages: [], + plans: [], + turnItems: [], + checkpointScopes: [], + checkpoints: [], + contextHandoffs: [], + contextTransfers: [], + visibleTurnItems: [], + updatedAt: shell.updatedAt, + }; + return { + ...presentThread(overrides.environmentId ?? EnvironmentId.make("environment-test"), projection), + ...overrides, + }; +} diff --git a/apps/server/README.md b/apps/server/README.md new file mode 100644 index 00000000000..5864e80b033 --- /dev/null +++ b/apps/server/README.md @@ -0,0 +1,3 @@ +# Server + +Node.js WebSocket server for the T3 Code app. diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts deleted file mode 100644 index ebc4f984b86..00000000000 --- a/apps/server/integration/OrchestrationEngineHarness.integration.ts +++ /dev/null @@ -1,560 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodeChildProcess from "node:child_process"; - -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { - ApprovalRequestId, - CodexSettings, - ProviderDriverKind, - type OrchestrationEvent, - type OrchestrationThread, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as FileSystem from "effect/FileSystem"; -import * as Layer from "effect/Layer"; -import * as ManagedRuntime from "effect/ManagedRuntime"; -import * as Option from "effect/Option"; -import * as Path from "effect/Path"; -import * as Ref from "effect/Ref"; -import * as Schedule from "effect/Schedule"; -import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; - -import * as CheckpointStore from "../src/checkpointing/CheckpointStore.ts"; -import { TextGeneration, type TextGenerationShape } from "../src/textGeneration/TextGeneration.ts"; -import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/Layers/OrchestrationCommandReceipts.ts"; -import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts"; -import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts"; -import { ProjectionPendingApprovalRepositoryLive } from "../src/persistence/Layers/ProjectionPendingApprovals.ts"; -import { ProviderSessionRuntimeRepositoryLive } from "../src/persistence/Layers/ProviderSessionRuntime.ts"; -import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts"; -import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts"; -import { ProjectionPendingApprovalRepository } from "../src/persistence/Services/ProjectionPendingApprovals.ts"; -import { makeAdapterRegistryMock } from "../src/provider/testUtils/providerAdapterRegistryMock.ts"; -import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts"; -import { makeProviderRegistryLayer } from "../src/provider/testUtils/providerRegistryMock.ts"; -import { ProviderSessionDirectoryLive } from "../src/provider/Layers/ProviderSessionDirectory.ts"; -import { ServerSettingsService } from "../src/serverSettings.ts"; -import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.ts"; -import { makeCodexAdapter } from "../src/provider/Layers/CodexAdapter.ts"; -import { - NoOpProviderEventLoggers, - ProviderEventLoggers, -} from "../src/provider/Layers/ProviderEventLoggers.ts"; -import { ProviderService } from "../src/provider/Services/ProviderService.ts"; -import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts"; -import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts"; -import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts"; -import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts"; -import { OrchestrationProjectionPipelineLive } from "../src/orchestration/Layers/ProjectionPipeline.ts"; -import { OrchestrationProjectionSnapshotQueryLive } from "../src/orchestration/Layers/ProjectionSnapshotQuery.ts"; -import { RuntimeReceiptBusTest } from "../src/orchestration/Layers/RuntimeReceiptBus.ts"; -import { OrchestrationReactorLive } from "../src/orchestration/Layers/OrchestrationReactor.ts"; -import { ProviderCommandReactorLive } from "../src/orchestration/Layers/ProviderCommandReactor.ts"; -import { ProviderRuntimeIngestionLive } from "../src/orchestration/Layers/ProviderRuntimeIngestion.ts"; -import { - OrchestrationEngineService, - type OrchestrationEngineShape, -} from "../src/orchestration/Services/OrchestrationEngine.ts"; -import { ThreadDeletionReactor } from "../src/orchestration/Services/ThreadDeletionReactor.ts"; -import { OrchestrationReactor } from "../src/orchestration/Services/OrchestrationReactor.ts"; -import { ProjectionSnapshotQuery } from "../src/orchestration/Services/ProjectionSnapshotQuery.ts"; -import { - RuntimeReceiptBus, - type OrchestrationRuntimeReceipt, -} from "../src/orchestration/Services/RuntimeReceiptBus.ts"; - -import { - makeTestProviderAdapterHarness, - type TestProviderAdapterHarness, -} from "./TestProviderAdapter.integration.ts"; -import { deriveServerPaths, ServerConfig } from "../src/config.ts"; -import * as WorkspaceEntries from "../src/workspace/WorkspaceEntries.ts"; -import * as WorkspacePaths from "../src/workspace/WorkspacePaths.ts"; -import * as VcsDriverRegistry from "../src/vcs/VcsDriverRegistry.ts"; -import { VcsStatusBroadcaster } from "../src/vcs/VcsStatusBroadcaster.ts"; -import { GitWorkflowService } from "../src/git/GitWorkflowService.ts"; -import * as VcsProcess from "../src/vcs/VcsProcess.ts"; -import * as AgentAwarenessRelay from "../src/relay/AgentAwarenessRelay.ts"; - -const decodeCodexSettings = Schema.decodeEffect(CodexSettings); - -function runGit(cwd: string, args: ReadonlyArray) { - return NodeChildProcess.execFileSync("git", args, { - cwd, - stdio: ["ignore", "pipe", "pipe"], - encoding: "utf8", - }); -} - -const initializeGitWorkspace = Effect.fn(function* (cwd: string) { - runGit(cwd, ["init", "--initial-branch=main"]); - runGit(cwd, ["config", "user.email", "test@example.com"]); - runGit(cwd, ["config", "user.name", "Test User"]); - const fileSystem = yield* FileSystem.FileSystem; - const { join } = yield* Path.Path; - yield* fileSystem.writeFileString(join(cwd, "README.md"), "v1\n"); - runGit(cwd, ["add", "."]); - runGit(cwd, ["commit", "-m", "Initial"]); -}); - -export function gitRefExists(cwd: string, ref: string): boolean { - try { - runGit(cwd, ["show-ref", "--verify", "--quiet", ref]); - return true; - } catch { - return false; - } -} - -export function gitShowFileAtRef(cwd: string, ref: string, filePath: string): string { - return runGit(cwd, ["show", `${ref}:${filePath}`]); -} - -class WaitForTimeoutError extends Schema.TaggedErrorClass()( - "WaitForTimeoutError", - { - description: Schema.String, - }, -) {} - -function waitFor( - read: Effect.Effect, - predicate: (value: A) => boolean, - description: string, - timeoutMs?: number, -): Effect.Effect; -function waitFor( - read: Effect.Effect, - predicate: (value: A) => value is B, - description: string, - timeoutMs?: number, -): Effect.Effect; -function waitFor( - read: Effect.Effect, - predicate: (value: A) => boolean, - description: string, - timeoutMs = 40_000, -): Effect.Effect { - const RETRY_SIGNAL = "wait_for_retry"; - const retryIntervalMs = 10; - const maxRetries = Math.max(0, Math.floor(timeoutMs / retryIntervalMs)); - const retrySchedule = Schedule.spaced(`${retryIntervalMs} millis`); - - return read.pipe( - Effect.filterOrFail(predicate, () => RETRY_SIGNAL), - Effect.retry({ - schedule: retrySchedule, - times: maxRetries, - while: (error) => error === RETRY_SIGNAL, - }), - Effect.mapError((error) => - error === RETRY_SIGNAL ? new WaitForTimeoutError({ description }) : error, - ), - Effect.orDie, - ); -} - -class OrchestrationHarnessRuntimeError extends Schema.TaggedErrorClass()( - "OrchestrationHarnessRuntimeError", - { - operation: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) {} - -const tryRuntimePromise = (operation: string, run: () => Promise) => - Effect.tryPromise({ - try: run, - catch: (cause) => new OrchestrationHarnessRuntimeError({ operation, cause }), - }); - -export interface OrchestrationIntegrationHarness { - readonly rootDir: string; - readonly workspaceDir: string; - readonly dbPath: string; - readonly adapterHarness: TestProviderAdapterHarness | null; - readonly engine: OrchestrationEngineShape; - readonly snapshotQuery: ProjectionSnapshotQuery["Service"]; - readonly providerService: ProviderService["Service"]; - readonly checkpointStore: CheckpointStore.CheckpointStore["Service"]; - readonly checkpointRepository: ProjectionCheckpointRepository["Service"]; - readonly pendingApprovalRepository: ProjectionPendingApprovalRepository["Service"]; - readonly waitForThread: ( - threadId: string, - predicate: (thread: OrchestrationThread) => boolean, - timeoutMs?: number, - ) => Effect.Effect; - readonly waitForDomainEvent: ( - predicate: (event: OrchestrationEvent) => boolean, - timeoutMs?: number, - ) => Effect.Effect, never>; - readonly waitForPendingApproval: ( - requestId: string, - predicate: (row: { - readonly status: "pending" | "resolved"; - readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; - readonly resolvedAt: string | null; - }) => boolean, - timeoutMs?: number, - ) => Effect.Effect< - { - readonly status: "pending" | "resolved"; - readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; - readonly resolvedAt: string | null; - }, - never - >; - readonly waitForReceipt: { - ( - predicate: (receipt: OrchestrationRuntimeReceipt) => boolean, - timeoutMs?: number, - ): Effect.Effect; - ( - predicate: (receipt: OrchestrationRuntimeReceipt) => receipt is Receipt, - timeoutMs?: number, - ): Effect.Effect; - }; - readonly dispose: Effect.Effect; -} - -interface MakeOrchestrationIntegrationHarnessOptions { - readonly provider?: ProviderDriverKind; - readonly realCodex?: boolean; -} - -export const makeOrchestrationIntegrationHarness = ( - options?: MakeOrchestrationIntegrationHarnessOptions, -) => - Effect.gen(function* () { - const path = yield* Path.Path; - const fileSystem = yield* FileSystem.FileSystem; - - const provider = options?.provider ?? ProviderDriverKind.make("codex"); - const useRealCodex = options?.realCodex === true; - const adapterHarness = useRealCodex - ? null - : yield* makeTestProviderAdapterHarness({ - provider, - }); - const fakeRegistry = adapterHarness - ? Layer.succeed( - ProviderAdapterRegistry, - makeAdapterRegistryMock({ [adapterHarness.provider]: adapterHarness.adapter }), - ) - : null; - const rootDir = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-orchestration-integration-", - }); - const workspaceDir = path.join(rootDir, "workspace"); - const { stateDir, dbPath } = yield* deriveServerPaths(rootDir, undefined).pipe( - Effect.provideService(Path.Path, path), - ); - yield* fileSystem.makeDirectory(workspaceDir, { recursive: true }); - yield* fileSystem.makeDirectory(stateDir, { recursive: true }); - yield* initializeGitWorkspace(workspaceDir); - - const persistenceLayer = makeSqlitePersistenceLive(dbPath); - const orchestrationLayer = OrchestrationEngineLive.pipe( - Layer.provide(OrchestrationProjectionPipelineLive), - Layer.provide(OrchestrationEventStoreLive), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - ); - const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(ProviderSessionRuntimeRepositoryLive), - ); - const realCodexRegistry = Layer.effect( - ProviderAdapterRegistry, - Effect.gen(function* () { - const codexSettings = yield* decodeCodexSettings({}); - const codexAdapter = yield* makeCodexAdapter(codexSettings); - return makeAdapterRegistryMock({ - [ProviderDriverKind.make("codex")]: codexAdapter, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)), - Layer.provideMerge(NodeServices.layer), - Layer.provideMerge(providerSessionDirectoryLayer), - ); - const providerEventLoggersLayer = Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers); - const providerLayer = useRealCodex - ? makeProviderServiceLive().pipe( - Layer.provide(providerSessionDirectoryLayer), - Layer.provide(realCodexRegistry), - Layer.provide(AnalyticsService.layerTest), - Layer.provide(providerEventLoggersLayer), - ) - : makeProviderServiceLive().pipe( - Layer.provide(providerSessionDirectoryLayer), - Layer.provide(fakeRegistry!), - Layer.provide(AnalyticsService.layerTest), - Layer.provide(providerEventLoggersLayer), - ); - const providerRegistryLayer = makeProviderRegistryLayer(); - - const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer)); - const projectionSnapshotQueryLayer = OrchestrationProjectionSnapshotQueryLive; - const runtimeServicesLayer = Layer.mergeAll( - projectionSnapshotQueryLayer, - orchestrationLayer.pipe(Layer.provide(projectionSnapshotQueryLayer)), - ProjectionCheckpointRepositoryLive, - ProjectionPendingApprovalRepositoryLive, - checkpointStoreLayer, - providerLayer, - RuntimeReceiptBusTest, - ); - const serverSettingsLayer = ServerSettingsService.layerTest(); - const runtimeIngestionLayer = ProviderRuntimeIngestionLive.pipe( - Layer.provideMerge(runtimeServicesLayer), - Layer.provideMerge(serverSettingsLayer), - ); - const gitWorkflowLayer = Layer.mock(GitWorkflowService)({ - renameBranch: (input: { - readonly cwd: string; - readonly oldBranch: string; - readonly newBranch: string; - }) => Effect.succeed({ branch: input.newBranch }), - }); - const textGenerationLayer = Layer.succeed(TextGeneration, { - generateBranchName: () => Effect.succeed({ branch: "update" }), - generateThreadTitle: () => Effect.succeed({ title: "New thread" }), - } as unknown as TextGenerationShape); - const providerCommandReactorLayer = ProviderCommandReactorLive.pipe( - Layer.provideMerge(runtimeServicesLayer), - Layer.provideMerge(gitWorkflowLayer), - Layer.provideMerge(textGenerationLayer), - Layer.provideMerge(serverSettingsLayer), - ); - const checkpointReactorLayer = CheckpointReactorLive.pipe( - Layer.provideMerge(runtimeServicesLayer), - Layer.provideMerge( - Layer.succeed(VcsStatusBroadcaster, { - getStatus: () => Effect.die("getStatus should not be called in this test"), - refreshLocalStatus: () => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: false, - isDefaultRef: true, - refName: "main", - hasWorkingTreeChanges: false, - workingTree: { files: [], insertions: 0, deletions: 0 }, - }), - refreshStatus: () => Effect.die("refreshStatus should not be called in this test"), - streamStatus: () => Stream.empty, - }), - ), - Layer.provideMerge( - WorkspaceEntries.layer.pipe( - Layer.provide(WorkspacePaths.layer), - Layer.provideMerge(VcsDriverRegistry.layer), - Layer.provide(NodeServices.layer), - ), - ), - Layer.provideMerge(WorkspacePaths.layer), - Layer.provideMerge(VcsProcess.layer), - ); - const orchestrationReactorLayer = OrchestrationReactorLive.pipe( - Layer.provideMerge(runtimeIngestionLayer), - Layer.provideMerge(providerCommandReactorLayer), - Layer.provideMerge(checkpointReactorLayer), - Layer.provideMerge( - Layer.succeed(ThreadDeletionReactor, { - start: () => Effect.void, - drain: Effect.void, - }), - ), - Layer.provideMerge( - Layer.succeed(AgentAwarenessRelay.AgentAwarenessRelay, { - publishThread: () => Effect.void, - start: () => Effect.void, - }), - ), - ); - const layer = Layer.empty.pipe( - Layer.provideMerge(runtimeServicesLayer), - Layer.provideMerge(orchestrationReactorLayer), - Layer.provideMerge(providerRegistryLayer), - Layer.provide(persistenceLayer), - Layer.provideMerge(RepositoryIdentityResolver.layer), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)), - Layer.provideMerge(NodeServices.layer), - ); - - const runtime = ManagedRuntime.make(layer); - const engine = yield* tryRuntimePromise("load OrchestrationEngine service", () => - runtime.runPromise(Effect.service(OrchestrationEngineService)), - ).pipe(Effect.orDie); - const reactor = yield* tryRuntimePromise("load OrchestrationReactor service", () => - runtime.runPromise(Effect.service(OrchestrationReactor)), - ).pipe(Effect.orDie); - const snapshotQuery = yield* tryRuntimePromise("load ProjectionSnapshotQuery service", () => - runtime.runPromise(Effect.service(ProjectionSnapshotQuery)), - ).pipe(Effect.orDie); - const providerService = yield* tryRuntimePromise("load ProviderService service", () => - runtime.runPromise(Effect.service(ProviderService)), - ).pipe(Effect.orDie); - const checkpointStore = yield* tryRuntimePromise("load CheckpointStore service", () => - runtime.runPromise(Effect.service(CheckpointStore.CheckpointStore)), - ).pipe(Effect.orDie); - const checkpointRepository = yield* tryRuntimePromise( - "load ProjectionCheckpointRepository service", - () => runtime.runPromise(Effect.service(ProjectionCheckpointRepository)), - ).pipe(Effect.orDie); - const pendingApprovalRepository = yield* tryRuntimePromise( - "load ProjectionPendingApprovalRepository service", - () => runtime.runPromise(Effect.service(ProjectionPendingApprovalRepository)), - ).pipe(Effect.orDie); - const runtimeReceiptBus = yield* tryRuntimePromise("load RuntimeReceiptBus service", () => - runtime.runPromise(Effect.service(RuntimeReceiptBus)), - ).pipe(Effect.orDie); - - const scope = yield* Scope.make("sequential"); - yield* tryRuntimePromise("start OrchestrationReactor", () => - runtime.runPromise(reactor.start().pipe(Scope.provide(scope))), - ).pipe(Effect.orDie); - const receiptHistory = yield* Ref.make>([]); - yield* Stream.runForEach(runtimeReceiptBus.streamEventsForTest, (receipt) => - Ref.update(receiptHistory, (history) => [...history, receipt]).pipe(Effect.asVoid), - ).pipe(Effect.forkIn(scope)); - yield* Effect.sleep(10); - - const waitForThread: OrchestrationIntegrationHarness["waitForThread"] = ( - threadId, - predicate, - timeoutMs, - ) => - waitFor( - snapshotQuery - .getSnapshot() - .pipe( - Effect.map( - (snapshot) => snapshot.threads.find((thread) => thread.id === threadId) ?? null, - ), - ), - (thread): thread is OrchestrationThread => thread !== null && predicate(thread), - `projected thread '${threadId}'`, - timeoutMs, - ) as Effect.Effect; - - const waitForDomainEvent: OrchestrationIntegrationHarness["waitForDomainEvent"] = ( - predicate, - timeoutMs, - ) => - waitFor( - Stream.runCollect(engine.readEvents(0)).pipe( - Effect.map((chunk): ReadonlyArray => Array.from(chunk)), - ), - (events) => events.some(predicate), - "domain event", - timeoutMs, - ); - - const waitForPendingApproval: OrchestrationIntegrationHarness["waitForPendingApproval"] = ( - requestId, - predicate, - timeoutMs, - ) => - waitFor( - pendingApprovalRepository - .getByRequestId({ requestId: ApprovalRequestId.make(requestId) }) - .pipe( - Effect.map((row) => - Option.match(row, { - onNone: () => null, - onSome: (value) => ({ - status: value.status, - decision: value.decision, - resolvedAt: value.resolvedAt, - }), - }), - ), - ), - ( - row, - ): row is { - readonly status: "pending" | "resolved"; - readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; - readonly resolvedAt: string | null; - } => row !== null && predicate(row), - `pending approval '${requestId}'`, - timeoutMs, - ) as Effect.Effect< - { - readonly status: "pending" | "resolved"; - readonly decision: "accept" | "acceptForSession" | "decline" | "cancel" | null; - readonly resolvedAt: string | null; - }, - never - >; - - function waitForReceipt( - predicate: (receipt: OrchestrationRuntimeReceipt) => boolean, - timeoutMs?: number, - ): Effect.Effect; - function waitForReceipt( - predicate: (receipt: OrchestrationRuntimeReceipt) => receipt is Receipt, - timeoutMs?: number, - ): Effect.Effect; - function waitForReceipt( - predicate: (receipt: OrchestrationRuntimeReceipt) => boolean, - timeoutMs?: number, - ) { - const readMatchingReceipt = Ref.get(receiptHistory).pipe( - Effect.map((history) => history.find(predicate)), - ); - - return waitFor( - readMatchingReceipt, - (receipt): receipt is OrchestrationRuntimeReceipt => receipt !== undefined, - "runtime receipt", - timeoutMs, - ); - } - - let disposed = false; - const dispose = Effect.gen(function* () { - if (disposed) { - return; - } - disposed = true; - - const shutdown = Effect.gen(function* () { - const closeScopeExit = yield* Effect.exit(Scope.close(scope, Exit.void)); - const disposeRuntimeExit = yield* Effect.exit(Effect.promise(() => runtime.dispose())); - - const failureCause = Exit.isFailure(closeScopeExit) - ? closeScopeExit.cause - : Exit.isFailure(disposeRuntimeExit) - ? disposeRuntimeExit.cause - : null; - - if (failureCause) { - return yield* Effect.failCause(failureCause); - } - }); - - yield* shutdown; - }); - - return { - rootDir, - workspaceDir, - dbPath, - adapterHarness, - engine, - snapshotQuery, - providerService, - checkpointStore, - checkpointRepository, - pendingApprovalRepository, - waitForThread, - waitForDomainEvent, - waitForPendingApproval, - waitForReceipt, - dispose, - } satisfies OrchestrationIntegrationHarness; - }); diff --git a/apps/server/integration/TestProviderAdapter.integration.ts b/apps/server/integration/TestProviderAdapter.integration.ts deleted file mode 100644 index 0e64699de97..00000000000 --- a/apps/server/integration/TestProviderAdapter.integration.ts +++ /dev/null @@ -1,577 +0,0 @@ -import { - ApprovalRequestId, - EventId, - ProviderApprovalDecision, - ProviderRuntimeEvent, - RuntimeSessionId, - ProviderSession, - ProviderTurnStartResult, - ThreadId, - TurnId, - ProviderDriverKind, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import * as Crypto from "effect/Crypto"; -import * as Queue from "effect/Queue"; -import * as Stream from "effect/Stream"; - -import { - ProviderAdapterSessionNotFoundError, - ProviderAdapterValidationError, - type ProviderAdapterError, -} from "../src/provider/Errors.ts"; -import type { - ProviderAdapterShape, - ProviderThreadSnapshot, - ProviderThreadTurnSnapshot, -} from "../src/provider/Services/ProviderAdapter.ts"; - -export interface TestTurnResponse { - readonly events: ReadonlyArray; - readonly mutateWorkspace?: (input: { - readonly cwd: string; - readonly turnCount: number; - }) => Effect.Effect; -} - -export type FixtureProviderRuntimeEvent = { - readonly type: string; - readonly eventId: EventId; - readonly provider: ProviderDriverKind; - readonly createdAt: string; - readonly threadId: string; - readonly turnId?: string | undefined; - readonly itemId?: string | undefined; - readonly requestId?: string | undefined; - readonly payload?: unknown | undefined; - readonly [key: string]: unknown; -}; - -// Temporary alias while fixtures migrate to the new name. -export type LegacyProviderRuntimeEvent = FixtureProviderRuntimeEvent; - -interface SessionState { - readonly session: ProviderSession; - snapshot: ProviderThreadSnapshot; - turnCount: number; - readonly queuedResponses: Array; - readonly rollbackCalls: Array; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function normalizeTurnState(value: unknown): "completed" | "failed" | "interrupted" | "cancelled" { - if ( - value === "completed" || - value === "failed" || - value === "interrupted" || - value === "cancelled" - ) { - return value; - } - return "completed"; -} - -function mapRequestType( - requestKind: unknown, -): "command_execution_approval" | "file_change_approval" | "unknown" { - if (requestKind === "command") { - return "command_execution_approval"; - } - if (requestKind === "file-change") { - return "file_change_approval"; - } - return "unknown"; -} - -function mapItemType(toolKind: unknown): "command_execution" | "file_change" | "unknown" { - if (toolKind === "command") { - return "command_execution"; - } - if (toolKind === "file-change") { - return "file_change"; - } - return "unknown"; -} - -function normalizeFixtureEvent(rawEvent: Record): ProviderRuntimeEvent { - const type = typeof rawEvent.type === "string" ? rawEvent.type : ""; - switch (type) { - case "turn.started": - return { - ...rawEvent, - type: "turn.started", - payload: isRecord(rawEvent.payload) ? rawEvent.payload : {}, - } as ProviderRuntimeEvent; - case "turn.completed": - return { - ...rawEvent, - type: "turn.completed", - payload: isRecord(rawEvent.payload) - ? rawEvent.payload - : { - state: normalizeTurnState(rawEvent.status), - }, - } as ProviderRuntimeEvent; - case "message.delta": - return { - ...rawEvent, - type: "content.delta", - payload: { - streamKind: "assistant_text", - delta: typeof rawEvent.delta === "string" ? rawEvent.delta : "", - }, - } as ProviderRuntimeEvent; - case "message.completed": - return { - ...rawEvent, - type: "item.completed", - payload: { - itemType: "assistant_message", - ...(typeof rawEvent.detail === "string" ? { detail: rawEvent.detail } : {}), - }, - } as ProviderRuntimeEvent; - case "tool.started": - return { - ...rawEvent, - type: "item.started", - payload: { - itemType: mapItemType(rawEvent.toolKind), - ...(typeof rawEvent.title === "string" ? { title: rawEvent.title } : {}), - ...(typeof rawEvent.detail === "string" ? { detail: rawEvent.detail } : {}), - }, - } as ProviderRuntimeEvent; - case "tool.completed": - return { - ...rawEvent, - type: "item.completed", - payload: { - itemType: mapItemType(rawEvent.toolKind), - status: "completed", - ...(typeof rawEvent.title === "string" ? { title: rawEvent.title } : {}), - ...(typeof rawEvent.detail === "string" ? { detail: rawEvent.detail } : {}), - }, - } as ProviderRuntimeEvent; - case "approval.requested": - return { - ...rawEvent, - type: "request.opened", - payload: { - requestType: mapRequestType(rawEvent.requestKind), - ...(typeof rawEvent.detail === "string" ? { detail: rawEvent.detail } : {}), - }, - } as ProviderRuntimeEvent; - case "approval.resolved": - return { - ...rawEvent, - type: "request.resolved", - payload: { - requestType: mapRequestType(rawEvent.requestKind), - ...(typeof rawEvent.decision === "string" ? { decision: rawEvent.decision } : {}), - }, - } as ProviderRuntimeEvent; - default: - return rawEvent as ProviderRuntimeEvent; - } -} - -export interface TestProviderAdapterHarness { - readonly adapter: ProviderAdapterShape; - readonly provider: ProviderDriverKind; - readonly queueTurnResponse: ( - threadId: ThreadId, - response: TestTurnResponse, - ) => Effect.Effect; - readonly queueTurnResponseForNextSession: ( - response: TestTurnResponse, - ) => Effect.Effect; - readonly getStartCount: () => number; - readonly getRollbackCalls: (threadId: ThreadId) => ReadonlyArray; - readonly getInterruptCalls: (threadId: ThreadId) => ReadonlyArray; - readonly listActiveSessionIds: () => ReadonlyArray; - readonly getApprovalResponses: (threadId: ThreadId) => ReadonlyArray<{ - readonly threadId: ThreadId; - readonly requestId: ApprovalRequestId; - readonly decision: ProviderApprovalDecision; - }>; -} - -interface MakeTestProviderAdapterHarnessOptions { - readonly provider?: ProviderDriverKind; -} - -function nowIso(): string { - return "2026-01-01T00:00:00.000Z"; -} - -function sessionNotFound( - provider: ProviderDriverKind, - threadId: ThreadId, -): ProviderAdapterSessionNotFoundError { - return new ProviderAdapterSessionNotFoundError({ - provider, - threadId: String(threadId), - }); -} - -function missingSessionEffect( - provider: ProviderDriverKind, - threadId: ThreadId, -): Effect.Effect { - return Effect.fail(sessionNotFound(provider, threadId)); -} - -export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapterHarnessOptions) => - Effect.gen(function* () { - const provider = options?.provider ?? ProviderDriverKind.make("codex"); - const crypto = yield* Crypto.Crypto; - const runtimeEvents = yield* Queue.unbounded(); - let sessionCount = 0; - const sessions = new Map(); - const queuedResponsesForNextSession: TestTurnResponse[] = []; - const interruptCallsBySession = new Map>(); - const approvalResponsesBySession = new Map< - ThreadId, - Array<{ - readonly threadId: ThreadId; - readonly requestId: ApprovalRequestId; - readonly decision: ProviderApprovalDecision; - }> - >(); - - const emit = (event: ProviderRuntimeEvent) => Queue.offer(runtimeEvents, event); - const randomUUIDv4 = (threadId: ThreadId) => - crypto.randomUUIDv4.pipe( - Effect.mapError( - (cause) => - new ProviderAdapterValidationError({ - provider, - operation: "crypto/randomUUIDv4", - issue: `Failed to generate test runtime identifier for thread '${threadId}'.`, - cause, - }), - ), - ); - - const startSession: ProviderAdapterShape["startSession"] = (input) => - Effect.gen(function* () { - if (input.provider !== undefined && input.provider !== provider) { - return yield* new ProviderAdapterValidationError({ - provider, - operation: "startSession", - issue: `Expected provider '${provider}' but received '${input.provider}'.`, - }); - } - - sessionCount += 1; - const threadId = input.threadId; - const createdAt = nowIso(); - - const session: ProviderSession = { - provider, - ...(input.providerInstanceId !== undefined - ? { providerInstanceId: input.providerInstanceId } - : {}), - status: "ready", - runtimeMode: input.runtimeMode, - threadId, - cwd: input.cwd, - resumeCursor: input.resumeCursor ?? { threadId: String(threadId), seed: sessionCount }, - createdAt, - updatedAt: createdAt, - }; - - sessions.set(threadId, { - session, - snapshot: { - threadId, - turns: [], - }, - turnCount: 0, - queuedResponses: queuedResponsesForNextSession.splice(0), - rollbackCalls: [], - }); - - return session; - }); - - const sendTurn: ProviderAdapterShape["sendTurn"] = (input) => - Effect.gen(function* () { - const state = sessions.get(input.threadId); - if (!state) { - return yield* missingSessionEffect(provider, input.threadId); - } - - state.turnCount += 1; - const turnCount = state.turnCount; - const turnId = TurnId.make(`turn-${turnCount}`); - - const response = state.queuedResponses.shift(); - if (!response) { - return yield* new ProviderAdapterValidationError({ - provider, - operation: "sendTurn", - issue: `No queued turn response for thread ${input.threadId}.`, - }); - } - - const assistantDeltas: string[] = []; - const deferredTurnCompletedEvents: ProviderRuntimeEvent[] = []; - for (const fixtureEvent of response.events) { - const rawEvent: Record = { - ...(fixtureEvent as Record), - eventId: yield* randomUUIDv4(input.threadId), - provider, - sessionId: RuntimeSessionId.make(String(input.threadId)), - }; - rawEvent.threadId = state.snapshot.threadId; - if (Object.hasOwn(rawEvent, "turnId")) { - rawEvent.turnId = turnId; - } - - const runtimeEvent = normalizeFixtureEvent(rawEvent); - const runtimeType = (runtimeEvent as { type: string }).type; - if (runtimeType === "content.delta") { - const payload = runtimeEvent.payload as { delta?: unknown } | undefined; - if (typeof payload?.delta === "string") { - assistantDeltas.push(payload.delta); - } - } else if (runtimeType === "message.delta") { - const legacyDelta = (runtimeEvent as { delta?: unknown }).delta; - if (typeof legacyDelta === "string") { - assistantDeltas.push(legacyDelta); - } - } - if (runtimeEvent.type === "turn.completed") { - deferredTurnCompletedEvents.push(runtimeEvent); - continue; - } - - yield* emit(runtimeEvent); - } - - if (response.mutateWorkspace && state.session.cwd) { - yield* response.mutateWorkspace({ cwd: state.session.cwd!, turnCount }); - } - - const userItem = { - type: "userMessage", - content: [{ type: "text", text: input.input }], - } as const; - const assistantText = assistantDeltas.join(""); - const nextItems: Array = - assistantText.length > 0 - ? [userItem, { type: "agentMessage", text: assistantText }] - : [userItem]; - - const nextTurn: ProviderThreadTurnSnapshot = { - id: turnId, - items: nextItems, - }; - - state.snapshot = { - threadId: state.snapshot.threadId, - turns: [...state.snapshot.turns, nextTurn], - }; - - if (deferredTurnCompletedEvents.length === 0) { - yield* emit({ - type: "turn.completed", - eventId: EventId.make(yield* randomUUIDv4(input.threadId)), - provider, - createdAt: nowIso(), - threadId: state.snapshot.threadId, - turnId, - payload: { - state: "completed", - }, - }); - } else { - for (const completedEvent of deferredTurnCompletedEvents) { - yield* emit(completedEvent); - } - } - - return { - threadId: state.snapshot.threadId, - turnId, - } satisfies ProviderTurnStartResult; - }); - - const interruptTurn: ProviderAdapterShape["interruptTurn"] = ( - threadId, - turnId, - ) => - sessions.has(threadId) - ? Effect.sync(() => { - const existing = interruptCallsBySession.get(threadId) ?? []; - existing.push(turnId); - interruptCallsBySession.set(threadId, existing); - }) - : missingSessionEffect(provider, threadId); - - const respondToRequest: ProviderAdapterShape["respondToRequest"] = ( - threadId, - requestId, - decision, - ) => - sessions.has(threadId) - ? Effect.sync(() => { - const existing = approvalResponsesBySession.get(threadId) ?? []; - existing.push({ - threadId, - requestId, - decision, - }); - approvalResponsesBySession.set(threadId, existing); - }) - : missingSessionEffect(provider, threadId); - - const respondToUserInput: ProviderAdapterShape["respondToUserInput"] = ( - threadId, - _requestId, - _answers, - ) => (sessions.has(threadId) ? Effect.void : missingSessionEffect(provider, threadId)); - - const stopSession: ProviderAdapterShape["stopSession"] = (threadId) => - Effect.sync(() => { - sessions.delete(threadId); - }); - - const listSessions: ProviderAdapterShape["listSessions"] = () => - Effect.sync(() => Array.from(sessions.values(), (state) => state.session)); - - const hasSession: ProviderAdapterShape["hasSession"] = (threadId) => - Effect.succeed(sessions.has(threadId)); - - const readThread: ProviderAdapterShape["readThread"] = (threadId) => { - const state = sessions.get(threadId); - if (!state) { - return missingSessionEffect(provider, threadId); - } - return Effect.succeed(state.snapshot); - }; - - const rollbackThread: ProviderAdapterShape["rollbackThread"] = ( - threadId, - numTurns, - ) => { - const state = sessions.get(threadId); - if (!state) { - return missingSessionEffect(provider, threadId); - } - if (!Number.isInteger(numTurns) || numTurns < 0 || numTurns > state.snapshot.turns.length) { - return Effect.fail( - new ProviderAdapterValidationError({ - provider, - operation: "rollbackThread", - issue: "numTurns must be an integer between 0 and current turn count.", - }), - ); - } - - return Effect.sync(() => { - state.rollbackCalls.push(numTurns); - state.snapshot = { - threadId: state.snapshot.threadId, - turns: state.snapshot.turns.slice(0, state.snapshot.turns.length - numTurns), - }; - state.turnCount = state.snapshot.turns.length; - return state.snapshot; - }); - }; - - const stopAll: ProviderAdapterShape["stopAll"] = () => - Effect.sync(() => { - sessions.clear(); - }); - - const adapter: ProviderAdapterShape = { - provider, - capabilities: { - sessionModelSwitch: "in-session", - }, - startSession, - sendTurn, - interruptTurn, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - readThread, - rollbackThread, - stopAll, - streamEvents: Stream.fromQueue(runtimeEvents), - }; - - const queueTurnResponse = ( - threadId: ThreadId, - response: TestTurnResponse, - ): Effect.Effect => - Effect.sync(() => sessions.get(threadId)).pipe( - Effect.flatMap((state) => - state - ? Effect.sync(() => { - state.queuedResponses.push(response); - }) - : Effect.fail(sessionNotFound(provider, threadId)), - ), - ); - - const queueTurnResponseForNextSession = ( - response: TestTurnResponse, - ): Effect.Effect => - Effect.sync(() => { - queuedResponsesForNextSession.push(response); - }); - - const getRollbackCalls = (threadId: ThreadId): ReadonlyArray => { - const state = sessions.get(threadId); - if (!state) { - return []; - } - return [...state.rollbackCalls]; - }; - - const getStartCount = (): number => sessionCount; - - const getInterruptCalls = (threadId: ThreadId): ReadonlyArray => { - const calls = interruptCallsBySession.get(threadId); - if (!calls) { - return []; - } - return [...calls]; - }; - - const listActiveSessionIds = (): ReadonlyArray => - Array.from(sessions.values(), (state) => state.session.threadId); - - const getApprovalResponses = ( - threadId: ThreadId, - ): ReadonlyArray<{ - readonly threadId: ThreadId; - readonly requestId: ApprovalRequestId; - readonly decision: ProviderApprovalDecision; - }> => { - const responses = approvalResponsesBySession.get(threadId); - if (!responses) { - return []; - } - return [...responses]; - }; - - return { - adapter, - provider, - queueTurnResponse, - queueTurnResponseForNextSession, - getStartCount, - getRollbackCalls, - getInterruptCalls, - listActiveSessionIds, - getApprovalResponses, - } satisfies TestProviderAdapterHarness; - }); diff --git a/apps/server/integration/fixtures/providerRuntime.ts b/apps/server/integration/fixtures/providerRuntime.ts deleted file mode 100644 index e1258c4cc62..00000000000 --- a/apps/server/integration/fixtures/providerRuntime.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { EventId, ProviderDriverKind, RuntimeRequestId } from "@t3tools/contracts"; -import type { LegacyProviderRuntimeEvent } from "../TestProviderAdapter.integration.ts"; - -const PROVIDER = ProviderDriverKind.make("codex"); -const SESSION_ID = "fixture-session"; -const THREAD_ID = "fixture-thread"; -const TURN_ID = "fixture-turn"; -const REQUEST_ID = RuntimeRequestId.make("req-1"); - -function baseEvent( - eventId: string, - createdAt: string, -): Pick { - return { - eventId: EventId.make(eventId), - provider: PROVIDER, - sessionId: SESSION_ID, - createdAt, - }; -} - -export const codexTurnTextFixture = [ - { - type: "turn.started", - ...baseEvent("evt-1", "2026-02-23T00:00:00.000Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: {}, - }, - { - type: "content.delta", - ...baseEvent("evt-2", "2026-02-23T00:00:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: { - streamKind: "assistant_text", - delta: "I will make a small update.\n", - }, - }, - { - type: "content.delta", - ...baseEvent("evt-3", "2026-02-23T00:00:00.200Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: { - streamKind: "assistant_text", - delta: "Done.\n", - }, - }, - { - type: "turn.completed", - ...baseEvent("evt-4", "2026-02-23T00:00:00.300Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: { - state: "completed", - }, - }, -] satisfies ReadonlyArray; - -export const codexTurnToolFixture = [ - { - type: "turn.started", - ...baseEvent("evt-11", "2026-02-23T00:01:00.000Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: {}, - }, - { - type: "item.started", - ...baseEvent("evt-12", "2026-02-23T00:01:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: { - itemType: "command_execution", - title: "Ran command", - detail: "echo integration", - }, - }, - { - type: "item.completed", - ...baseEvent("evt-13", "2026-02-23T00:01:00.200Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: { - itemType: "command_execution", - status: "completed", - title: "Ran command", - detail: "echo integration", - }, - }, - { - type: "content.delta", - ...baseEvent("evt-14", "2026-02-23T00:01:00.300Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: { - streamKind: "assistant_text", - delta: "Applied the requested edit.\n", - }, - }, - { - type: "turn.completed", - ...baseEvent("evt-15", "2026-02-23T00:01:00.400Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: { - state: "completed", - }, - }, -] satisfies ReadonlyArray; - -export const codexTurnApprovalFixture = [ - { - type: "turn.started", - ...baseEvent("evt-21", "2026-02-23T00:02:00.000Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: {}, - }, - { - type: "request.opened", - ...baseEvent("evt-22", "2026-02-23T00:02:00.100Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - requestId: REQUEST_ID, - payload: { - requestType: "command_execution_approval", - detail: "Please approve command", - }, - }, - { - type: "request.resolved", - ...baseEvent("evt-23", "2026-02-23T00:02:00.200Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - requestId: REQUEST_ID, - payload: { - requestType: "command_execution_approval", - decision: "accept", - }, - }, - { - type: "content.delta", - ...baseEvent("evt-24", "2026-02-23T00:02:00.300Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: { - streamKind: "assistant_text", - delta: "Approval received and command executed.\n", - }, - }, - { - type: "turn.completed", - ...baseEvent("evt-25", "2026-02-23T00:02:00.400Z"), - threadId: THREAD_ID, - turnId: TURN_ID, - payload: { - state: "completed", - }, - }, -] satisfies ReadonlyArray; diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts deleted file mode 100644 index ccfb9c46742..00000000000 --- a/apps/server/integration/orchestrationEngine.integration.test.ts +++ /dev/null @@ -1,1441 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodeFS from "node:fs"; -import * as NodePath from "node:path"; - -import { - ApprovalRequestId, - CommandId, - defaultInstanceIdForDriver, - DEFAULT_PROVIDER_INTERACTION_MODE, - DEFAULT_MODEL, - DEFAULT_MODEL_BY_PROVIDER, - EventId, - MessageId, - ProjectId, - ProviderDriverKind, - ThreadId, - ModelSelection, - ProviderInstanceId, -} from "@t3tools/contracts"; -import { assert, it } from "@effect/vitest"; -import * as Clock from "effect/Clock"; -import * as Effect from "effect/Effect"; -import * as Option from "effect/Option"; -import * as Schema from "effect/Schema"; - -import type { TestTurnResponse } from "./TestProviderAdapter.integration.ts"; -import { - gitRefExists, - gitShowFileAtRef, - makeOrchestrationIntegrationHarness, - type OrchestrationIntegrationHarness, -} from "./OrchestrationEngineHarness.integration.ts"; -import { checkpointRefForThreadTurn } from "../src/checkpointing/Utils.ts"; -import type { - CheckpointDiffFinalizedReceipt, - TurnProcessingQuiescedReceipt, -} from "../src/orchestration/Services/RuntimeReceiptBus.ts"; -import * as NodeServices from "@effect/platform-node/NodeServices"; - -const asMessageId = (value: string): MessageId => MessageId.make(value); -const asProjectId = (value: string): ProjectId => ProjectId.make(value); -const asEventId = (value: string): EventId => EventId.make(value); -const asApprovalRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make(value); - -const PROJECT_ID = asProjectId("project-1"); -const THREAD_ID = ThreadId.make("thread-1"); -const FIXTURE_TURN_ID = "fixture-turn"; -const APPROVAL_REQUEST_ID = asApprovalRequestId("req-approval-1"); -type IntegrationProvider = ProviderDriverKind; -const CODEX_PROVIDER = ProviderDriverKind.make("codex"); -const CLAUDE_AGENT_PROVIDER = ProviderDriverKind.make("claudeAgent"); - -function nowIso() { - return "2026-05-01T00:00:00.000Z"; -} - -class IntegrationWaitTimeoutError extends Schema.TaggedErrorClass()( - "IntegrationWaitTimeoutError", - { - description: Schema.String, - }, -) {} - -function waitForSync( - read: () => A, - predicate: (value: A) => boolean, - description: string, - timeoutMs = 10_000, -): Effect.Effect { - return Effect.gen(function* () { - const deadline = (yield* Clock.currentTimeMillis) + timeoutMs; - - while (true) { - const value = read(); - if (predicate(value)) { - return value; - } - if ((yield* Clock.currentTimeMillis) >= deadline) { - return yield* Effect.die(new IntegrationWaitTimeoutError({ description })); - } - yield* Effect.sleep(10); - } - }); -} - -function runtimeBase( - eventId: string, - createdAt: string, - provider: IntegrationProvider = CODEX_PROVIDER, -) { - return { - eventId: asEventId(eventId), - provider, - createdAt, - }; -} - -function withHarness( - use: (harness: OrchestrationIntegrationHarness) => Effect.Effect, - provider: IntegrationProvider = CODEX_PROVIDER, -) { - return Effect.acquireUseRelease( - makeOrchestrationIntegrationHarness({ provider }), - use, - (harness) => harness.dispose, - ).pipe(Effect.provide(NodeServices.layer)); -} - -function withRealCodexHarness( - use: (harness: OrchestrationIntegrationHarness) => Effect.Effect, -) { - return Effect.acquireUseRelease( - makeOrchestrationIntegrationHarness({ provider: CODEX_PROVIDER, realCodex: true }), - use, - (harness) => harness.dispose, - ).pipe(Effect.provide(NodeServices.layer)); -} - -const seedProjectAndThread = (harness: OrchestrationIntegrationHarness) => - Effect.gen(function* () { - const createdAt = nowIso(); - const provider = harness.adapterHarness?.provider ?? CODEX_PROVIDER; - const defaultModel = DEFAULT_MODEL_BY_PROVIDER[provider] ?? DEFAULT_MODEL; - const instanceId = defaultInstanceIdForDriver(provider); - - yield* harness.engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-create"), - projectId: PROJECT_ID, - title: "Integration Project", - workspaceRoot: harness.workspaceDir, - defaultModelSelection: { - instanceId, - model: defaultModel, - }, - createdAt, - }); - - yield* harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create"), - threadId: THREAD_ID, - projectId: PROJECT_ID, - title: "Integration Thread", - modelSelection: { - instanceId, - model: defaultModel, - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: harness.workspaceDir, - createdAt, - }); - }); - -const startTurn = (input: { - readonly harness: OrchestrationIntegrationHarness; - readonly commandId: string; - readonly messageId: string; - readonly text: string; - readonly modelSelection?: ModelSelection; - readonly createdAt?: string; -}) => - input.harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make(input.commandId), - threadId: THREAD_ID, - message: { - messageId: asMessageId(input.messageId), - role: "user", - text: input.text, - attachments: [], - }, - ...(input.modelSelection !== undefined - ? { - modelSelection: input.modelSelection, - } - : {}), - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: input.createdAt ?? nowIso(), - }); - -it.live("runs a single turn end-to-end and persists checkpoint state in sqlite + git", () => - withHarness((harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - const turnResponse: TestTurnResponse = { - events: [ - { - type: "turn.started", - ...runtimeBase("evt-single-1", "2026-02-24T10:00:00.000Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "message.delta", - ...runtimeBase("evt-single-2", "2026-02-24T10:00:00.100Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "Single turn response.\n", - }, - { - type: "turn.completed", - ...runtimeBase("evt-single-3", "2026-02-24T10:00:00.200Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - }; - - yield* harness.adapterHarness!.queueTurnResponseForNextSession(turnResponse); - yield* startTurn({ - harness, - commandId: "cmd-turn-start-single", - messageId: "msg-user-single", - text: "Say hello", - }); - const finalizedReceipt = yield* harness.waitForReceipt( - (receipt): receipt is CheckpointDiffFinalizedReceipt => - receipt.type === "checkpoint.diff.finalized" && - receipt.threadId === THREAD_ID && - receipt.checkpointTurnCount === 1, - ); - if (finalizedReceipt.type !== "checkpoint.diff.finalized") { - throw new Error("Expected checkpoint.diff.finalized receipt."); - } - assert.equal(finalizedReceipt.status, "ready"); - yield* harness.waitForReceipt( - (receipt): receipt is TurnProcessingQuiescedReceipt => - receipt.type === "turn.processing.quiesced" && - receipt.threadId === THREAD_ID && - receipt.checkpointTurnCount === 1, - ); - - const thread = yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.session?.status === "ready" && - entry.messages.some( - (message) => message.role === "assistant" && message.streaming === false, - ) && - entry.checkpoints.length === 1, - ); - assert.equal(thread.checkpoints[0]?.status, "ready"); - assert.equal(thread.checkpoints[0]?.checkpointTurnCount, 1); - - const checkpointRows = yield* harness.checkpointRepository.listByThreadId({ - threadId: THREAD_ID, - }); - assert.equal(checkpointRows.length, 1); - assert.equal(checkpointRows[0]?.checkpointTurnCount, 1); - assert.equal(checkpointRows[0]?.status, "ready"); - assert.deepEqual(checkpointRows[0]?.files, []); - - const ref0 = checkpointRefForThreadTurn(THREAD_ID, 0); - const ref1 = checkpointRefForThreadTurn(THREAD_ID, 1); - assert.equal(gitRefExists(harness.workspaceDir, ref0), true); - assert.equal(gitRefExists(harness.workspaceDir, ref1), true); - assert.equal(gitShowFileAtRef(harness.workspaceDir, ref0, "README.md"), "v1\n"); - assert.equal(gitShowFileAtRef(harness.workspaceDir, ref1, "README.md"), "v1\n"); - }), - ), -); - -it.live.skipIf(!process.env.CODEX_BINARY_PATH)( - "keeps the same Codex provider thread across runtime mode switches", - () => - withRealCodexHarness((harness) => - Effect.gen(function* () { - const createdAt = nowIso(); - - yield* harness.engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-create-real-codex"), - projectId: PROJECT_ID, - title: "Integration Project", - workspaceRoot: harness.workspaceDir, - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5.3-codex", - }, - createdAt, - }); - - yield* harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create-real-codex"), - threadId: THREAD_ID, - projectId: PROJECT_ID, - title: "Integration Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5.3-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: harness.workspaceDir, - createdAt, - }); - - yield* harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-real-codex-1"), - threadId: THREAD_ID, - message: { - messageId: asMessageId("msg-real-codex-1"), - role: "user", - text: "Reply with exactly ALPHA.", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - createdAt: nowIso(), - }); - - const firstThread = yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.session?.status === "ready" && - entry.session.providerName === "codex" && - entry.messages.some( - (message) => message.role === "assistant" && message.streaming === false, - ), - 180_000, - ); - assert.equal(firstThread.session?.threadId, "thread-1"); - - yield* harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-real-codex-2"), - threadId: THREAD_ID, - message: { - messageId: asMessageId("msg-real-codex-2"), - role: "user", - text: "Reply with exactly BETA.", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: nowIso(), - }); - - const secondThread = yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.session?.status === "ready" && - entry.session.providerName === "codex" && - entry.session.runtimeMode === "approval-required" && - entry.messages.some( - (message) => message.role === "assistant" && message.text.includes("BETA"), - ), - 180_000, - ); - assert.equal(secondThread.session?.threadId, "thread-1"); - }), - ), -); - -it.live("runs multi-turn file edits and persists checkpoint diffs", () => - withHarness((harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase("evt-multi-1", "2026-02-24T10:01:00.000Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "tool.started", - ...runtimeBase("evt-multi-2", "2026-02-24T10:01:00.100Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - toolKind: "command", - title: "Edit file", - detail: "README.md", - }, - { - type: "tool.completed", - ...runtimeBase("evt-multi-3", "2026-02-24T10:01:00.200Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - toolKind: "command", - title: "Edit file", - detail: "README.md", - }, - { - type: "message.delta", - ...runtimeBase("evt-multi-4", "2026-02-24T10:01:00.300Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "Updated README to v2.\n", - }, - { - type: "turn.completed", - ...runtimeBase("evt-multi-5", "2026-02-24T10:01:00.400Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - mutateWorkspace: ({ cwd }) => - Effect.sync(() => { - NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), "v2\n", "utf8"); - }), - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-multi-1", - messageId: "msg-user-multi-1", - text: "Make first edit", - }); - yield* harness.waitForReceipt( - (receipt): receipt is CheckpointDiffFinalizedReceipt => - receipt.type === "checkpoint.diff.finalized" && - receipt.threadId === THREAD_ID && - receipt.checkpointTurnCount === 1, - ); - - yield* harness.waitForThread( - THREAD_ID, - (entry) => entry.checkpoints.length === 1 && entry.session?.threadId === "thread-1", - ); - - yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, { - events: [ - { - type: "turn.started", - ...runtimeBase("evt-multi-6", "2026-02-24T10:02:00.000Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "message.delta", - ...runtimeBase("evt-multi-7", "2026-02-24T10:02:00.100Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "Updated README to v3.\n", - }, - { - type: "turn.completed", - ...runtimeBase("evt-multi-8", "2026-02-24T10:02:00.200Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - mutateWorkspace: ({ cwd }) => - Effect.sync(() => { - NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), "v3\n", "utf8"); - }), - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-multi-2", - messageId: "msg-user-multi-2", - text: "Make second edit", - }); - const secondReceipt = yield* harness.waitForReceipt( - (receipt): receipt is CheckpointDiffFinalizedReceipt => - receipt.type === "checkpoint.diff.finalized" && - receipt.threadId === THREAD_ID && - receipt.checkpointTurnCount === 2, - ); - if (secondReceipt.type !== "checkpoint.diff.finalized") { - throw new Error("Expected checkpoint.diff.finalized receipt."); - } - assert.equal(secondReceipt.status, "ready"); - yield* harness.waitForReceipt( - (receipt): receipt is TurnProcessingQuiescedReceipt => - receipt.type === "turn.processing.quiesced" && - receipt.threadId === THREAD_ID && - receipt.checkpointTurnCount === 2, - ); - - const secondTurnThread = yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.latestTurn?.turnId === "turn-2" && - entry.checkpoints.length === 2 && - entry.checkpoints.some((checkpoint) => checkpoint.checkpointTurnCount === 2), - ); - const secondCheckpoint = secondTurnThread.checkpoints.find( - (checkpoint) => checkpoint.checkpointTurnCount === 2, - ); - assert.equal( - secondCheckpoint?.files.some((file) => file.path === "README.md"), - true, - ); - - const checkpointRows = yield* harness.checkpointRepository.listByThreadId({ - threadId: THREAD_ID, - }); - assert.deepEqual( - checkpointRows.map((row) => row.checkpointTurnCount), - [1, 2], - ); - - const incrementalDiff = yield* harness.checkpointStore.diffCheckpoints({ - cwd: harness.workspaceDir, - fromCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 1), - toCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 2), - fallbackFromToHead: false, - ignoreWhitespace: false, - }); - assert.equal(incrementalDiff.includes("README.md"), true); - - const fullDiff = yield* harness.checkpointStore.diffCheckpoints({ - cwd: harness.workspaceDir, - fromCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 0), - toCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 2), - fallbackFromToHead: false, - ignoreWhitespace: false, - }); - assert.equal(fullDiff.includes("README.md"), true); - - assert.equal( - gitShowFileAtRef( - harness.workspaceDir, - checkpointRefForThreadTurn(THREAD_ID, 1), - "README.md", - ), - "v2\n", - ); - assert.equal( - gitShowFileAtRef( - harness.workspaceDir, - checkpointRefForThreadTurn(THREAD_ID, 2), - "README.md", - ), - "v3\n", - ); - }), - ), -); - -it.live("tracks approval requests and resolves pending approvals on user response", () => - withHarness((harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase("evt-approval-1", "2026-02-24T10:03:00.000Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "approval.requested", - ...runtimeBase("evt-approval-2", "2026-02-24T10:03:00.100Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - requestId: APPROVAL_REQUEST_ID, - requestKind: "command", - detail: "Approve command execution", - }, - { - type: "turn.completed", - ...runtimeBase("evt-approval-3", "2026-02-24T10:03:00.200Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-approval", - messageId: "msg-user-approval", - text: "Run command needing approval", - }); - - const thread = yield* harness.waitForThread(THREAD_ID, (entry) => - entry.activities.some((activity) => activity.kind === "approval.requested"), - ); - assert.equal( - thread.activities.some((activity) => activity.kind === "approval.requested"), - true, - ); - - const pendingRow = yield* harness.waitForPendingApproval( - "req-approval-1", - (row) => row.status === "pending" && row.decision === null, - ); - assert.equal(pendingRow.status, "pending"); - - yield* harness.engine.dispatch({ - type: "thread.approval.respond", - commandId: CommandId.make("cmd-approval-respond"), - threadId: THREAD_ID, - requestId: APPROVAL_REQUEST_ID, - decision: "accept", - createdAt: nowIso(), - }); - - const resolvedRow = yield* harness.waitForPendingApproval( - "req-approval-1", - (row) => row.status === "resolved" && row.decision === "accept", - ); - assert.equal(resolvedRow.status, "resolved"); - assert.equal(resolvedRow.decision, "accept"); - - const approvalResponses = yield* waitForSync( - () => harness.adapterHarness!.getApprovalResponses(THREAD_ID), - (responses) => responses.length === 1, - "provider approval response", - ); - assert.equal(approvalResponses.length, 1); - assert.equal(approvalResponses[0]?.requestId, "req-approval-1"); - assert.equal(approvalResponses[0]?.decision, "accept"); - }), - ), -); - -it.live("records failed turn runtime state and checkpoint status as error", () => - withHarness((harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase("evt-failure-1", "2026-02-24T10:04:00.000Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "content.delta", - ...runtimeBase("evt-failure-2", "2026-02-24T10:04:00.100Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - payload: { - streamKind: "assistant_text", - delta: "Partial output before failure.\n", - }, - }, - { - type: "runtime.error", - ...runtimeBase("evt-failure-3", "2026-02-24T10:04:00.200Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - payload: { - message: "Sandbox command failed.", - }, - }, - { - type: "turn.completed", - ...runtimeBase("evt-failure-4", "2026-02-24T10:04:00.300Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - payload: { - state: "failed", - errorMessage: "Sandbox command failed.", - }, - }, - ], - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-failure", - messageId: "msg-user-failure", - text: "Run risky command", - }); - - const thread = yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.session?.status === "error" && - entry.session?.lastError === "Sandbox command failed." && - entry.activities.some((activity) => activity.kind === "runtime.error") && - entry.checkpoints.length === 1, - ); - assert.equal(thread.session?.status, "error"); - assert.equal(thread.checkpoints[0]?.status, "error"); - - const checkpointRow = yield* harness.checkpointRepository.getByThreadAndTurnCount({ - threadId: THREAD_ID, - checkpointTurnCount: 1, - }); - assert.equal(Option.isSome(checkpointRow), true); - if (Option.isSome(checkpointRow)) { - assert.equal(checkpointRow.value.status, "error"); - } - assert.equal( - gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 1)), - true, - ); - }), - ), -); - -it.live("reverts to an earlier checkpoint and trims checkpoint projections + git refs", () => - withHarness((harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase("evt-revert-1", "2026-02-24T10:05:00.000Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "tool.started", - ...runtimeBase("evt-revert-1-tool-started", "2026-02-24T10:05:00.025Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - toolKind: "command", - title: "Edit file", - detail: "README.md", - }, - { - type: "tool.completed", - ...runtimeBase("evt-revert-1-tool-completed", "2026-02-24T10:05:00.035Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - toolKind: "command", - title: "Edit file", - detail: "README.md", - }, - { - type: "message.delta", - ...runtimeBase("evt-revert-1a", "2026-02-24T10:05:00.050Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "Updated README to v2.\n", - }, - { - type: "turn.completed", - ...runtimeBase("evt-revert-2", "2026-02-24T10:05:00.100Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - mutateWorkspace: ({ cwd }) => - Effect.sync(() => { - NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), "v2\n", "utf8"); - }), - }); - yield* startTurn({ - harness, - commandId: "cmd-turn-start-revert-1", - messageId: "msg-user-revert-1", - text: "First edit", - createdAt: "2026-02-24T10:04:59.900Z", - }); - - yield* harness.waitForThread( - THREAD_ID, - (entry) => entry.session?.threadId === "thread-1" && entry.checkpoints.length === 1, - ); - - yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, { - events: [ - { - type: "turn.started", - ...runtimeBase("evt-revert-3", "2026-02-24T10:05:01.000Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "tool.started", - ...runtimeBase("evt-revert-3-tool-started", "2026-02-24T10:05:01.025Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - toolKind: "command", - title: "Edit file", - detail: "README.md", - }, - { - type: "tool.completed", - ...runtimeBase("evt-revert-3-tool-completed", "2026-02-24T10:05:01.035Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - toolKind: "command", - title: "Edit file", - detail: "README.md", - }, - { - type: "message.delta", - ...runtimeBase("evt-revert-3a", "2026-02-24T10:05:01.050Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "Updated README to v3.\n", - }, - { - type: "turn.completed", - ...runtimeBase("evt-revert-4", "2026-02-24T10:05:01.100Z"), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - mutateWorkspace: ({ cwd }) => - Effect.sync(() => { - NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), "v3\n", "utf8"); - }), - }); - yield* startTurn({ - harness, - commandId: "cmd-turn-start-revert-2", - messageId: "msg-user-revert-2", - text: "Second edit", - createdAt: "2026-02-24T10:05:00.900Z", - }); - - yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.latestTurn?.turnId === "turn-2" && - entry.checkpoints.length === 2 && - entry.activities.some((activity) => activity.turnId === "turn-2"), - 8000, - ); - - yield* harness.engine.dispatch({ - type: "thread.checkpoint.revert", - commandId: CommandId.make("cmd-checkpoint-revert"), - threadId: THREAD_ID, - turnCount: 1, - createdAt: nowIso(), - }); - - yield* harness.waitForDomainEvent((event) => event.type === "thread.reverted"); - const revertedThread = yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.checkpoints.length === 1 && entry.checkpoints[0]?.checkpointTurnCount === 1, - ); - assert.equal(revertedThread.checkpoints[0]?.checkpointTurnCount, 1); - assert.deepEqual( - revertedThread.messages.map((message) => ({ role: message.role, text: message.text })), - [ - { role: "user", text: "First edit" }, - { role: "assistant", text: "Updated README to v2.\n" }, - ], - ); - assert.equal( - revertedThread.activities.some((activity) => activity.turnId === "turn-2"), - false, - ); - assert.equal( - revertedThread.activities.some( - (activity) => activity.turnId === "turn-1" && activity.kind === "tool.started", - ), - true, - ); - assert.equal( - revertedThread.activities.some( - (activity) => activity.turnId === "turn-1" && activity.kind === "tool.completed", - ), - true, - ); - assert.equal( - NodeFS.readFileSync(NodePath.join(harness.workspaceDir, "README.md"), "utf8"), - "v2\n", - ); - assert.equal( - gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)), - false, - ); - assert.deepEqual(harness.adapterHarness!.getRollbackCalls(THREAD_ID), [1]); - - const checkpointRows = yield* harness.checkpointRepository.listByThreadId({ - threadId: THREAD_ID, - }); - assert.equal(checkpointRows.length, 1); - }), - ), -); - -it.live( - "appends checkpoint.revert.failed activity when revert is requested without an active session", - () => - withHarness((harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.engine.dispatch({ - type: "thread.checkpoint.revert", - commandId: CommandId.make("cmd-checkpoint-revert-no-session"), - threadId: THREAD_ID, - turnCount: 0, - createdAt: nowIso(), - }); - - const thread = yield* harness.waitForThread(THREAD_ID, (entry) => - entry.activities.some( - (activity) => - activity.kind === "checkpoint.revert.failed" && - typeof activity.payload === "object" && - activity.payload !== null, - ), - ); - const failureActivity = thread.activities.find( - (activity) => activity.kind === "checkpoint.revert.failed", - ); - assert.equal(failureActivity !== undefined, true); - assert.equal( - String( - (failureActivity?.payload as { readonly detail?: string } | undefined)?.detail, - ).includes("No active provider session"), - true, - ); - }), - ), -); - -it.live("starts a claudeAgent session on first turn when provider is requested", () => - withHarness( - (harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase( - "evt-claude-start-1", - "2026-02-24T10:10:00.000Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "message.delta", - ...runtimeBase( - "evt-claude-start-2", - "2026-02-24T10:10:00.050Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "Claude first turn.\n", - }, - { - type: "turn.completed", - ...runtimeBase( - "evt-claude-start-3", - "2026-02-24T10:10:00.100Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-claude-initial", - messageId: "msg-user-claude-initial", - text: "Use Claude", - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-6", - }, - }); - - const thread = yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.session?.providerName === "claudeAgent" && - entry.session.status === "ready" && - entry.messages.some( - (message) => message.role === "assistant" && message.text === "Claude first turn.\n", - ), - ); - assert.equal(thread.session?.providerName, "claudeAgent"); - }), - CLAUDE_AGENT_PROVIDER, - ), -); - -it.live("recovers claudeAgent sessions after provider stopAll using persisted resume state", () => - withHarness( - (harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase( - "evt-claude-recover-1", - "2026-02-24T10:11:00.000Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "message.delta", - ...runtimeBase( - "evt-claude-recover-2", - "2026-02-24T10:11:00.050Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "Turn before restart.\n", - }, - { - type: "turn.completed", - ...runtimeBase( - "evt-claude-recover-3", - "2026-02-24T10:11:00.100Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-claude-recover-1", - messageId: "msg-user-claude-recover-1", - text: "Before restart", - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-6", - }, - }); - - yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", - ); - - yield* harness.adapterHarness!.adapter.stopAll(); - yield* waitForSync( - () => harness.adapterHarness!.listActiveSessionIds(), - (sessionIds) => sessionIds.length === 0, - "provider stopAll", - ); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase( - "evt-claude-recover-4", - "2026-02-24T10:11:01.000Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "message.delta", - ...runtimeBase( - "evt-claude-recover-5", - "2026-02-24T10:11:01.050Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "Turn after restart.\n", - }, - { - type: "turn.completed", - ...runtimeBase( - "evt-claude-recover-6", - "2026-02-24T10:11:01.100Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-claude-recover-2", - messageId: "msg-user-claude-recover-2", - text: "After restart", - }); - yield* waitForSync( - () => harness.adapterHarness!.getStartCount(), - (count) => count === 2, - "claude provider recovery start", - ); - - const recoveredThread = yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.session?.providerName === "claudeAgent" && - entry.messages.some( - (message) => message.role === "user" && message.text === "After restart", - ) && - !entry.activities.some((activity) => activity.kind === "provider.turn.start.failed"), - ); - assert.equal(recoveredThread.session?.providerName, "claudeAgent"); - assert.equal(recoveredThread.session?.threadId, "thread-1"); - }), - CLAUDE_AGENT_PROVIDER, - ), -); - -it.live("forwards claudeAgent approval responses to the provider session", () => - withHarness( - (harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase( - "evt-claude-approval-1", - "2026-02-24T10:12:00.000Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "approval.requested", - ...runtimeBase( - "evt-claude-approval-2", - "2026-02-24T10:12:00.050Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - requestId: APPROVAL_REQUEST_ID, - requestKind: "command", - detail: "Approve Claude tool call", - }, - { - type: "turn.completed", - ...runtimeBase( - "evt-claude-approval-3", - "2026-02-24T10:12:00.100Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-claude-approval", - messageId: "msg-user-claude-approval", - text: "Need approval", - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-6", - }, - }); - - const thread = yield* harness.waitForThread(THREAD_ID, (entry) => - entry.activities.some((activity) => activity.kind === "approval.requested"), - ); - assert.equal(thread.session?.threadId, "thread-1"); - - yield* harness.engine.dispatch({ - type: "thread.approval.respond", - commandId: CommandId.make("cmd-claude-approval-respond"), - threadId: THREAD_ID, - requestId: APPROVAL_REQUEST_ID, - decision: "accept", - createdAt: nowIso(), - }); - - yield* harness.waitForPendingApproval( - "req-approval-1", - (row) => row.status === "resolved" && row.decision === "accept", - ); - - const approvalResponses = yield* waitForSync( - () => harness.adapterHarness!.getApprovalResponses(THREAD_ID), - (responses) => responses.length === 1, - "claude provider approval response", - ); - assert.equal(approvalResponses[0]?.decision, "accept"); - }), - CLAUDE_AGENT_PROVIDER, - ), -); - -it.live("forwards thread.turn.interrupt to claudeAgent provider sessions", () => - withHarness( - (harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase( - "evt-claude-interrupt-1", - "2026-02-24T10:13:00.000Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "message.delta", - ...runtimeBase( - "evt-claude-interrupt-2", - "2026-02-24T10:13:00.050Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "Long running output.\n", - }, - { - type: "turn.completed", - ...runtimeBase( - "evt-claude-interrupt-3", - "2026-02-24T10:13:00.100Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-claude-interrupt", - messageId: "msg-user-claude-interrupt", - text: "Start long turn", - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-6", - }, - }); - - const thread = yield* harness.waitForThread( - THREAD_ID, - (entry) => entry.session?.threadId === "thread-1", - ); - assert.equal(thread.session?.threadId, "thread-1"); - - yield* harness.engine.dispatch({ - type: "thread.turn.interrupt", - commandId: CommandId.make("cmd-turn-interrupt-claude"), - threadId: THREAD_ID, - createdAt: nowIso(), - }); - yield* harness.waitForDomainEvent( - (event) => event.type === "thread.turn-interrupt-requested", - ); - - const interruptCalls = yield* waitForSync( - () => harness.adapterHarness!.getInterruptCalls(THREAD_ID), - (calls) => calls.length === 1, - "claude provider interrupt call", - ); - assert.equal(interruptCalls.length, 1); - }), - CLAUDE_AGENT_PROVIDER, - ), -); - -it.live("reverts claudeAgent turns and rolls back provider conversation state", () => - withHarness( - (harness) => - Effect.gen(function* () { - yield* seedProjectAndThread(harness); - - yield* harness.adapterHarness!.queueTurnResponseForNextSession({ - events: [ - { - type: "turn.started", - ...runtimeBase( - "evt-claude-revert-1", - "2026-02-24T10:14:00.000Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "message.delta", - ...runtimeBase( - "evt-claude-revert-2", - "2026-02-24T10:14:00.050Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "README -> v2\n", - }, - { - type: "turn.completed", - ...runtimeBase( - "evt-claude-revert-3", - "2026-02-24T10:14:00.100Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - mutateWorkspace: ({ cwd }) => - Effect.sync(() => { - NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), "v2\n", "utf8"); - }), - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-claude-revert-1", - messageId: "msg-user-claude-revert-1", - text: "First Claude edit", - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-6", - }, - }); - - yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", - ); - - yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, { - events: [ - { - type: "turn.started", - ...runtimeBase( - "evt-claude-revert-4", - "2026-02-24T10:14:01.000Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - }, - { - type: "message.delta", - ...runtimeBase( - "evt-claude-revert-5", - "2026-02-24T10:14:01.050Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - delta: "README -> v3\n", - }, - { - type: "turn.completed", - ...runtimeBase( - "evt-claude-revert-6", - "2026-02-24T10:14:01.100Z", - CLAUDE_AGENT_PROVIDER, - ), - threadId: THREAD_ID, - turnId: FIXTURE_TURN_ID, - status: "completed", - }, - ], - mutateWorkspace: ({ cwd }) => - Effect.sync(() => { - NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), "v3\n", "utf8"); - }), - }); - - yield* startTurn({ - harness, - commandId: "cmd-turn-start-claude-revert-2", - messageId: "msg-user-claude-revert-2", - text: "Second Claude edit", - }); - - yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.latestTurn?.turnId === "turn-2" && - entry.checkpoints.length === 2 && - entry.session?.providerName === "claudeAgent", - ); - - yield* harness.engine.dispatch({ - type: "thread.checkpoint.revert", - commandId: CommandId.make("cmd-checkpoint-revert-claude"), - threadId: THREAD_ID, - turnCount: 1, - createdAt: nowIso(), - }); - - const revertedThread = yield* harness.waitForThread( - THREAD_ID, - (entry) => - entry.checkpoints.length === 1 && entry.checkpoints[0]?.checkpointTurnCount === 1, - ); - assert.equal(revertedThread.checkpoints[0]?.checkpointTurnCount, 1); - assert.equal( - gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 1)), - true, - ); - assert.equal( - gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)), - false, - ); - assert.deepEqual(harness.adapterHarness!.getRollbackCalls(THREAD_ID), [1]); - }), - CLAUDE_AGENT_PROVIDER, - ), -); diff --git a/apps/server/integration/providerService.integration.test.ts b/apps/server/integration/providerService.integration.test.ts deleted file mode 100644 index e703af4b1f4..00000000000 --- a/apps/server/integration/providerService.integration.test.ts +++ /dev/null @@ -1,304 +0,0 @@ -import type { ProviderRuntimeEvent } from "@t3tools/contracts"; -import { ProviderDriverKind, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; -import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts/settings"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { it, assert } from "@effect/vitest"; -import * as Effect from "effect/Effect"; -import * as FileSystem from "effect/FileSystem"; -import * as Layer from "effect/Layer"; -import * as Path from "effect/Path"; -import * as Queue from "effect/Queue"; -import * as Stream from "effect/Stream"; - -import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts"; -import { makeAdapterRegistryMock } from "../src/provider/testUtils/providerAdapterRegistryMock.ts"; -import { ProviderSessionDirectoryLive } from "../src/provider/Layers/ProviderSessionDirectory.ts"; -import { - NoOpProviderEventLoggers, - ProviderEventLoggers, -} from "../src/provider/Layers/ProviderEventLoggers.ts"; -import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.ts"; -import { - ProviderService, - type ProviderServiceShape, -} from "../src/provider/Services/ProviderService.ts"; -import { ServerSettingsService } from "../src/serverSettings.ts"; -import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts"; -import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts"; -import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts"; - -import { - makeTestProviderAdapterHarness, - type TestProviderAdapterHarness, - type TestTurnResponse, -} from "./TestProviderAdapter.integration.ts"; -import { - codexTurnApprovalFixture, - codexTurnToolFixture, - codexTurnTextFixture, -} from "./fixtures/providerRuntime.ts"; - -const codexInstanceId = ProviderInstanceId.make("codex"); - -const makeWorkspaceDirectory = Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const pathService = yield* Path.Path; - const cwd = yield* fs.makeTempDirectory(); - yield* fs.writeFileString(pathService.join(cwd, "README.md"), "v1\n"); - return cwd; -}).pipe(Effect.provide(NodeServices.layer)); - -interface IntegrationFixture { - readonly cwd: string; - readonly harness: TestProviderAdapterHarness; - readonly layer: Layer.Layer; -} - -const makeIntegrationFixture = Effect.gen(function* () { - const cwd = yield* makeWorkspaceDirectory; - const harness = yield* makeTestProviderAdapterHarness(); - - const registry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("codex")]: harness.adapter, - }); - - const directoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(ProviderSessionRuntime.layer), - ); - - const shared = Layer.mergeAll( - directoryLayer, - Layer.succeed(ProviderAdapterRegistry, registry), - ServerSettingsService.layerTest(DEFAULT_SERVER_SETTINGS), - AnalyticsService.layerTest, - Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers), - ).pipe(Layer.provide(SqlitePersistenceMemory)); - - const layer = makeProviderServiceLive().pipe(Layer.provide(shared)); - - return { - cwd, - harness, - layer, - } satisfies IntegrationFixture; -}); - -const collectEventsDuring = ( - stream: Stream.Stream, - count: number, - action: Effect.Effect, -) => - Effect.gen(function* () { - const queue = yield* Queue.unbounded(); - yield* Stream.runForEach(stream, (event) => Queue.offer(queue, event).pipe(Effect.asVoid)).pipe( - Effect.forkScoped, - ); - - yield* Effect.sleep("50 millis"); - yield* action; - - return yield* Effect.forEach( - Array.from({ length: count }, () => undefined), - () => Queue.take(queue), - { discard: false }, - ); - }); - -const runTurn = (input: { - readonly provider: ProviderServiceShape; - readonly harness: TestProviderAdapterHarness; - readonly threadId: ThreadId; - readonly userText: string; - readonly response: TestTurnResponse; -}) => - Effect.gen(function* () { - yield* input.harness.queueTurnResponse(input.threadId, input.response); - return yield* collectEventsDuring( - input.provider.streamEvents, - input.response.events.length, - input.provider.sendTurn({ - threadId: input.threadId, - input: input.userText, - attachments: [], - }), - ); - }); - -it.live("replays typed runtime fixture events", () => - Effect.gen(function* () { - const fixture = yield* makeIntegrationFixture; - - yield* Effect.gen(function* () { - const provider = yield* ProviderService; - const session = yield* provider.startSession(ThreadId.make("thread-integration-typed"), { - threadId: ThreadId.make("thread-integration-typed"), - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - cwd: fixture.cwd, - runtimeMode: "full-access", - }); - assert.equal((session.threadId ?? "").length > 0, true); - - const observedEvents = yield* runTurn({ - provider, - harness: fixture.harness, - threadId: session.threadId, - userText: "hello", - response: { events: codexTurnTextFixture }, - }); - - assert.deepEqual( - observedEvents.map((event) => event.type), - codexTurnTextFixture.map((event) => event.type), - ); - assert.deepEqual( - observedEvents.map((event) => event.providerInstanceId), - codexTurnTextFixture.map(() => codexInstanceId), - ); - }).pipe(Effect.provide(fixture.layer)); - }).pipe(Effect.provide(NodeServices.layer)), -); - -it.live("replays file-changing fixture turn events", () => - Effect.gen(function* () { - const fixture = yield* makeIntegrationFixture; - const { join } = yield* Path.Path; - const { writeFileString } = yield* FileSystem.FileSystem; - - yield* Effect.gen(function* () { - const provider = yield* ProviderService; - const session = yield* provider.startSession(ThreadId.make("thread-integration-tools"), { - threadId: ThreadId.make("thread-integration-tools"), - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - cwd: fixture.cwd, - runtimeMode: "full-access", - }); - assert.equal((session.threadId ?? "").length > 0, true); - - const observedEvents = yield* runTurn({ - provider, - harness: fixture.harness, - threadId: session.threadId, - userText: "make a small change", - response: { - events: codexTurnToolFixture, - mutateWorkspace: ({ cwd }) => - writeFileString(join(cwd, "README.md"), "v2\n").pipe(Effect.asVoid, Effect.ignore), - }, - }); - - assert.deepEqual( - observedEvents.map((event) => event.type), - codexTurnToolFixture.map((event) => event.type), - ); - }).pipe(Effect.provide(fixture.layer)); - }).pipe(Effect.provide(NodeServices.layer)), -); - -it.live("runs multi-turn tool/approval flow", () => - Effect.gen(function* () { - const fixture = yield* makeIntegrationFixture; - const { join } = yield* Path.Path; - const { writeFileString } = yield* FileSystem.FileSystem; - - yield* Effect.gen(function* () { - const provider = yield* ProviderService; - const session = yield* provider.startSession(ThreadId.make("thread-integration-multi"), { - threadId: ThreadId.make("thread-integration-multi"), - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - cwd: fixture.cwd, - runtimeMode: "full-access", - }); - assert.equal((session.threadId ?? "").length > 0, true); - - const firstTurnEvents = yield* runTurn({ - provider, - harness: fixture.harness, - threadId: session.threadId, - userText: "turn 1", - response: { - events: codexTurnToolFixture, - mutateWorkspace: ({ cwd }) => - writeFileString(join(cwd, "README.md"), "v2\n").pipe(Effect.asVoid, Effect.ignore), - }, - }); - assert.deepEqual( - firstTurnEvents.map((event) => event.type), - codexTurnToolFixture.map((event) => event.type), - ); - - const secondTurnEvents = yield* runTurn({ - provider, - harness: fixture.harness, - threadId: session.threadId, - userText: "turn 2 approval", - response: { - events: codexTurnApprovalFixture, - mutateWorkspace: ({ cwd }) => - writeFileString(join(cwd, "README.md"), "v3\n").pipe(Effect.asVoid, Effect.ignore), - }, - }); - assert.deepEqual( - secondTurnEvents.map((event) => event.type), - codexTurnApprovalFixture.map((event) => event.type), - ); - }).pipe(Effect.provide(fixture.layer)); - }).pipe(Effect.provide(NodeServices.layer)), -); - -it.live("rolls back provider conversation state only", () => - Effect.gen(function* () { - const fixture = yield* makeIntegrationFixture; - const { join } = yield* Path.Path; - const { writeFileString, readFileString } = yield* FileSystem.FileSystem; - - yield* Effect.gen(function* () { - const provider = yield* ProviderService; - const session = yield* provider.startSession(ThreadId.make("thread-integration-rollback"), { - threadId: ThreadId.make("thread-integration-rollback"), - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - cwd: fixture.cwd, - runtimeMode: "full-access", - }); - assert.equal((session.threadId ?? "").length > 0, true); - - yield* runTurn({ - provider, - harness: fixture.harness, - threadId: session.threadId, - userText: "turn 1", - response: { - events: codexTurnToolFixture, - mutateWorkspace: ({ cwd }) => - writeFileString(join(cwd, "README.md"), "v2\n").pipe(Effect.asVoid, Effect.ignore), - }, - }); - - yield* runTurn({ - provider, - harness: fixture.harness, - threadId: session.threadId, - userText: "turn 2 approval", - response: { - events: codexTurnApprovalFixture, - mutateWorkspace: ({ cwd }) => - writeFileString(join(cwd, "README.md"), "v3\n").pipe(Effect.asVoid, Effect.ignore), - }, - }); - - yield* provider.rollbackConversation({ - threadId: session.threadId, - numTurns: 1, - }); - - const rollbackCalls = fixture.harness.getRollbackCalls(session.threadId); - assert.deepEqual(rollbackCalls, [1]); - - const readme = yield* readFileString(join(fixture.cwd, "README.md")); - assert.equal(readme, "v3\n"); - }).pipe(Effect.provide(fixture.layer)); - }).pipe(Effect.provide(NodeServices.layer)), -); diff --git a/apps/server/package.json b/apps/server/package.json index 01003d7c176..4946b963b1c 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -16,6 +16,9 @@ "type": "module", "scripts": { "dev": "node --watch src/bin.ts", + "record:codex-replay": "bun scripts/record-codex-app-server-replay-fixture.ts", + "record:claude-replay": "bun scripts/record-claude-agent-sdk-replay-fixture.ts", + "record:cursor-replay": "node --env-file-if-exists=../../.env --experimental-strip-types scripts/record-cursor-agent-sdk-replay-fixture.ts", "build:bundle": "vp pack", "start": "node dist/bin.mjs", "typecheck": "tsgo --noEmit", @@ -23,6 +26,9 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.170", + "@connectrpc/connect": "1.7.0", + "@connectrpc/connect-node": "1.7.0", + "@cursor/sdk": "1.0.19", "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", diff --git a/apps/server/scripts/acp-mock-agent.ts b/apps/server/scripts/acp-mock-agent.ts index 0d89775844d..b0067f34d75 100644 --- a/apps/server/scripts/acp-mock-agent.ts +++ b/apps/server/scripts/acp-mock-agent.ts @@ -23,6 +23,9 @@ const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1"; const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1"; const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT; const promptDelayMs = Number(process.env.T3_ACP_PROMPT_DELAY_MS ?? "0"); +const supportsSessionLifecycle = process.env.T3_ACP_SESSION_LIFECYCLE === "1"; +const advertisedAuthMethodId = process.env.T3_ACP_AUTH_METHOD_ID?.trim(); +const requiresAuthentication = process.env.T3_ACP_REQUIRE_AUTH === "1"; const permissionOptionIds = { allowOnce: process.env.T3_ACP_ALLOW_ONCE_OPTION_ID ?? "allow-once", allowAlways: process.env.T3_ACP_ALLOW_ALWAYS_OPTION_ID ?? "allow-always", @@ -36,6 +39,7 @@ let parameterizedModelPicker = false; let currentReasoning = "medium"; let currentContext = "272k"; let currentFast = false; +let authenticated = !requiresAuthentication; const cancelledSessions = new Set(); function logExit(reason: string): void { @@ -268,19 +272,56 @@ const program = Effect.gen(function* () { request.clientCapabilities?._meta?.parameterizedModelPicker === true; return { protocolVersion: 1, - agentCapabilities: { loadSession: true }, + agentCapabilities: { + loadSession: true, + ...(supportsSessionLifecycle + ? { + sessionCapabilities: { + list: {}, + fork: {}, + resume: {}, + close: {}, + }, + } + : {}), + }, + ...(advertisedAuthMethodId + ? { + authMethods: [ + { + id: advertisedAuthMethodId, + name: "Mock agent authentication", + }, + ], + } + : {}), }; }), ); - yield* agent.handleAuthenticate(() => Effect.succeed({})); + yield* agent.handleAuthenticate((request) => + Effect.gen(function* () { + if (advertisedAuthMethodId && request.methodId !== advertisedAuthMethodId) { + return yield* AcpError.AcpRequestError.invalidParams( + `Unknown mock authentication method: ${request.methodId}`, + ); + } + authenticated = true; + return {}; + }), + ); yield* agent.handleCreateSession(() => - Effect.succeed({ - sessionId, - modes: modeState(), - models: modelState(), - configOptions: configOptions(), + Effect.gen(function* () { + if (!authenticated) { + return yield* AcpError.AcpRequestError.authRequired(); + } + return { + sessionId, + modes: modeState(), + models: modelState(), + configOptions: configOptions(), + }; }), ); @@ -302,6 +343,38 @@ const program = Effect.gen(function* () { ), ); + yield* agent.handleListSessions((request) => + Effect.succeed({ + sessions: [ + { + sessionId, + cwd: request.cwd ?? process.cwd(), + title: "Mock session", + updatedAt: "1970-01-01T00:00:00.000Z", + }, + ], + }), + ); + + yield* agent.handleForkSession((request) => + Effect.succeed({ + sessionId: `${request.sessionId}-fork`, + modes: modeState(), + models: modelState(), + configOptions: configOptions(), + }), + ); + + yield* agent.handleResumeSession(() => + Effect.succeed({ + modes: modeState(), + models: modelState(), + configOptions: configOptions(), + }), + ); + + yield* agent.handleCloseSession(() => Effect.succeed({})); + yield* agent.handleSetSessionModel((request) => Effect.gen(function* () { if (!grokAcpModels.some((model) => model.modelId === request.modelId)) { diff --git a/apps/server/scripts/acp-replay-agent.ts b/apps/server/scripts/acp-replay-agent.ts new file mode 100644 index 00000000000..705d25681c1 --- /dev/null +++ b/apps/server/scripts/acp-replay-agent.ts @@ -0,0 +1,285 @@ +#!/usr/bin/env node +// @effect-diagnostics nodeBuiltinImport:off +import * as NodeFS from "node:fs"; +import * as NodeReadline from "node:readline"; + +interface ReplayEntry { + readonly type: "emit_inbound" | "expect_outbound" | "runtime_exit"; + readonly label?: string; + readonly frame?: unknown; + readonly status?: "success" | "error" | "cancelled"; + readonly error?: unknown; +} + +interface ReplayTranscript { + readonly scenario: string; + readonly entries: ReadonlyArray; +} + +interface LogicalFrame { + readonly kind: "notification" | "request" | "response"; + readonly method: string; + readonly params?: unknown; + readonly result?: unknown; + readonly error?: unknown; +} + +interface JsonRpcMessage { + readonly jsonrpc?: string; + readonly id?: string | number | null; + readonly method?: string; + readonly params?: unknown; + readonly result?: unknown; + readonly error?: unknown; + readonly headers?: ReadonlyArray; +} + +const encodedTranscript = process.env.T3_ACP_REPLAY_TRANSCRIPT; +const statusPath = process.env.T3_ACP_REPLAY_STATUS_PATH; +const replayWorkspace = process.env.T3_ACP_REPLAY_WORKSPACE ?? process.cwd(); + +if (encodedTranscript === undefined || statusPath === undefined) { + process.stderr.write("ACP replay requires transcript and status environment variables.\n"); + process.exit(2); +} + +const replayStatusPath = statusPath; +const transcript = JSON.parse( + Buffer.from(encodedTranscript, "base64").toString("utf8"), +) as ReplayTranscript; +let cursor = 0; +let stopped = false; +let nextAgentRequestId = 1; +const pendingClientRequestIds = new Map(); +const pendingAgentRequestMethods = new Map(); + +function writeStatus(failure?: unknown): void { + NodeFS.writeFileSync( + replayStatusPath, + JSON.stringify({ + scenario: transcript.scenario, + cursor, + total: transcript.entries.length, + ...(failure === undefined ? {} : { failure }), + }), + "utf8", + ); +} + +function stableStringify(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (typeof value === "object" && value !== null) { + const record = value as Record; + return `{${Object.keys(record) + .toSorted() + .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function expandExpectedString(value: string): string { + return value.replaceAll("", replayWorkspace); +} + +function matchesExpected(expected: unknown, actual: unknown): boolean { + if (expected === "") return true; + if (typeof expected === "string" && typeof actual === "string") { + const expanded = expandExpectedString(expected); + if (!expanded.includes("")) return expanded === actual; + const parts = expanded.split(""); + let offset = 0; + for (const part of parts) { + const index = actual.indexOf(part, offset); + if (index === -1) return false; + offset = index + part.length; + } + return true; + } + if (Array.isArray(expected)) { + return ( + Array.isArray(actual) && + expected.length === actual.length && + expected.every((entry, index) => matchesExpected(entry, actual[index])) + ); + } + if (typeof expected === "object" && expected !== null) { + if (typeof actual !== "object" || actual === null || Array.isArray(actual)) return false; + const expectedRecord = expected as Record; + const actualRecord = actual as Record; + const expectedKeys = Object.keys(expectedRecord).toSorted(); + const actualKeys = Object.keys(actualRecord).toSorted(); + return ( + stableStringify(expectedKeys) === stableStringify(actualKeys) && + expectedKeys.every((key) => matchesExpected(expectedRecord[key], actualRecord[key])) + ); + } + return Object.is(expected, actual); +} + +function stopWithFailure(detail: string, actual?: unknown): void { + if (stopped) return; + stopped = true; + const entry = transcript.entries[cursor]; + const failure = { + detail, + cursor, + expected: entry, + ...(actual === undefined ? {} : { actual }), + }; + writeStatus(failure); + process.stderr.write(`ACP replay mismatch: ${JSON.stringify(failure)}\n`); + process.exitCode = 1; + process.stdin.pause(); +} + +function advance(): void { + cursor += 1; + writeStatus(); +} + +function send(message: JsonRpcMessage): void { + process.stdout.write(`${JSON.stringify(message)}\n`); +} + +function pendingClientRequestId(method: string): string | number | undefined { + return pendingClientRequestIds.get(method); +} + +function logicalIncoming(message: JsonRpcMessage): LogicalFrame | undefined { + if (typeof message.method === "string") { + return { + kind: + message.id === undefined || message.id === null || message.id === "" + ? "notification" + : "request", + method: message.method, + ...(message.params === undefined ? {} : { params: message.params }), + }; + } + if (message.id === undefined || message.id === null) return undefined; + const method = pendingAgentRequestMethods.get(String(message.id)); + if (method === undefined) return undefined; + return { + kind: "response", + method, + ...(message.result === undefined ? {} : { result: message.result }), + ...(message.error === undefined ? {} : { error: message.error }), + }; +} + +function emitInbound(frame: LogicalFrame): void { + switch (frame.kind) { + case "notification": + send({ + jsonrpc: "2.0", + method: frame.method, + ...(frame.params === undefined ? {} : { params: frame.params }), + }); + return; + case "request": { + const id = nextAgentRequestId; + nextAgentRequestId += 1; + pendingAgentRequestMethods.set(String(id), frame.method); + send({ + jsonrpc: "2.0", + id, + method: frame.method, + ...(frame.params === undefined ? {} : { params: frame.params }), + headers: [], + }); + return; + } + case "response": { + const id = pendingClientRequestId(frame.method); + if (id === undefined) { + stopWithFailure(`No pending client request for ${frame.method}`, frame); + return; + } + pendingClientRequestIds.delete(frame.method); + send({ + jsonrpc: "2.0", + id, + ...(frame.result === undefined ? {} : { result: frame.result }), + ...(frame.error === undefined ? {} : { error: frame.error }), + }); + } + } +} + +function flushInbound(): void { + while (!stopped) { + const entry = transcript.entries[cursor]; + if (entry === undefined || entry.type === "expect_outbound") return; + if (entry.type === "runtime_exit") { + if (entry.status !== "success") { + stopWithFailure(`Recorded runtime exit was ${entry.status ?? "unknown"}`, entry.error); + return; + } + advance(); + continue; + } + const frame = entry.frame as LogicalFrame; + if ( + typeof frame !== "object" || + frame === null || + !["notification", "request", "response"].includes(frame.kind) || + typeof frame.method !== "string" + ) { + stopWithFailure("Invalid emit_inbound logical ACP frame", entry.frame); + return; + } + emitInbound(frame); + if (stopped) return; + advance(); + } +} + +function handleMessage(message: JsonRpcMessage): void { + if (stopped) return; + const actual = logicalIncoming(message); + if (actual === undefined) { + stopWithFailure("Could not identify outbound ACP frame", message); + return; + } + const entry = transcript.entries[cursor]; + if (entry?.type !== "expect_outbound" || !matchesExpected(entry.frame, actual)) { + if (actual.kind === "request" && message.id !== undefined && message.id !== null) { + send({ + jsonrpc: "2.0", + id: message.id, + error: { code: -32603, message: "ACP replay frame mismatch" }, + }); + } + stopWithFailure("Unexpected outbound ACP frame", actual); + return; + } + if (actual.kind === "request" && message.id !== undefined && message.id !== null) { + pendingClientRequestIds.set(actual.method, message.id); + } else if (actual.kind === "response" && message.id !== undefined && message.id !== null) { + pendingAgentRequestMethods.delete(String(message.id)); + } + advance(); + flushInbound(); +} + +writeStatus(); +flushInbound(); + +const input = NodeReadline.createInterface({ input: process.stdin, crlfDelay: Infinity }); +input.on("line", (line) => { + if (stopped || line.trim().length === 0) return; + try { + handleMessage(JSON.parse(line) as JsonRpcMessage); + } catch (cause) { + stopWithFailure("Failed to decode outbound ACP JSON-RPC", String(cause)); + } +}); + +input.on("close", () => { + if (!stopped && cursor !== transcript.entries.length) { + stopWithFailure("ACP replay input closed before transcript completion"); + } +}); diff --git a/apps/server/scripts/probe-claude-fork-local-rollback-replay.ts b/apps/server/scripts/probe-claude-fork-local-rollback-replay.ts new file mode 100644 index 00000000000..e5a233dcc5c --- /dev/null +++ b/apps/server/scripts/probe-claude-fork-local-rollback-replay.ts @@ -0,0 +1,358 @@ +import { + forkSession, + query, + type SDKAssistantMessage, + type SDKMessage, +} from "@anthropic-ai/claude-agent-sdk"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { ProviderInstanceId, type ProviderReplayEntry } from "@t3tools/contracts"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; + +import { + makeClaudeQueryOptions, + makeClaudeUserMessage, + type ClaudeAgentSdkQueryOptions, +} from "../src/orchestration-v2/Adapters/ClaudeAdapterV2.ts"; +import { randomUuidV4 } from "../src/orchestration-v2/RandomUuid.ts"; +import { makeCheckpointWorkspace } from "../src/orchestration-v2/testkit/ReplayFixtureWorkspace.ts"; + +const SCENARIO = "thread_fork_native_fork_local_rollback"; +const DEFAULT_OUTPUT = new URL( + "../src/orchestration-v2/testkit/fixtures/thread_fork_native_fork_local_rollback/claude_transcript.ndjson", + import.meta.url, +).pathname; +const CLAUDE_PROVIDER = "claudeAgent" as const; +const PROTOCOL = "claude-agent-sdk.query" as const; +const MODEL_SELECTION = { + instanceId: ProviderInstanceId.make(CLAUDE_PROVIDER), + model: process.env.T3_CLAUDE_REPLAY_MODEL ?? "claude-sonnet-4-6", +} as const; +const SOURCE_PROMPT = + "For this fork-local rollback fixture, respond with exactly: fork local source alpha"; +const FORK_FIRST_PROMPT = + "For this fork-local rollback fixture, respond with exactly: fork local first"; +const FORK_SECOND_PROMPT = + "For this fork-local rollback fixture, respond with exactly: fork local second"; +const FORK_AFTER_ROLLBACK_PROMPT = + "Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content."; + +class PromptQueue implements AsyncIterable> { + readonly #items: Array> = []; + readonly #waiters: Array< + (value: IteratorResult>) => void + > = []; + #closed = false; + + offer(message: ReturnType): void { + const waiter = this.#waiters.shift(); + if (waiter !== undefined) { + waiter({ done: false, value: message }); + return; + } + this.#items.push(message); + } + + close(): void { + this.#closed = true; + for (const waiter of this.#waiters.splice(0)) { + waiter({ done: true, value: undefined }); + } + } + + [Symbol.asyncIterator](): AsyncIterator> { + return { + next: () => { + const value = this.#items.shift(); + if (value !== undefined) { + return Promise.resolve({ done: false, value }); + } + if (this.#closed) { + return Promise.resolve({ done: true, value: undefined }); + } + return new Promise((resolve) => this.#waiters.push(resolve)); + }, + }; + } +} + +function readArgValue(name: string): string | undefined { + const args = process.argv.slice(2); + const index = args.indexOf(name); + return index >= 0 ? args[index + 1] : undefined; +} + +function dirname(filePath: string): string { + const normalized = filePath.replace(/\/+$/u, ""); + const lastSlash = normalized.lastIndexOf("/"); + if (lastSlash < 0) { + return "."; + } + return lastSlash === 0 ? "/" : normalized.slice(0, lastSlash); +} + +function sanitizeValue( + value: unknown, + pathFragments: ReadonlyArray, +): unknown { + if (typeof value === "string") { + return pathFragments.reduce( + (sanitized, [actual, replacement]) => + actual.length === 0 ? sanitized : sanitized.split(actual).join(replacement), + value, + ); + } + if (Array.isArray(value)) { + return value.map((entry) => sanitizeValue(entry, pathFragments)); + } + if (typeof value === "object" && value !== null) { + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [key, sanitizeValue(entry, pathFragments)]), + ); + } + return value; +} + +function stableOptions(options: ClaudeAgentSdkQueryOptions): ClaudeAgentSdkQueryOptions { + const stable = { + model: options.model, + tools: options.tools, + permissionMode: options.permissionMode, + ...(options.allowedTools === undefined ? {} : { allowedTools: options.allowedTools }), + ...(options.disallowedTools === undefined ? {} : { disallowedTools: options.disallowedTools }), + ...(options.settings === undefined ? {} : { settings: options.settings }), + ...(options.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + ...(options.resumeSessionAt === undefined ? {} : { resumeSessionAt: options.resumeSessionAt }), + ...(options.forkSession === true ? { forkSession: true } : {}), + }; + return options.resume === undefined + ? { ...stable, sessionId: options.sessionId } + : { ...stable, resume: options.resume }; +} + +function queryOptions(input: { + readonly cwd: string; + readonly nativeThreadId: string; + readonly resume: boolean; + readonly resumeSessionAt?: string; +}): ClaudeAgentSdkQueryOptions { + return { + ...makeClaudeQueryOptions({ + modelSelection: MODEL_SELECTION, + cwd: input.cwd, + nativeThreadId: input.nativeThreadId, + resume: input.resume, + tools: { type: "preset", preset: "claude_code" }, + permissionMode: "bypassPermissions", + allowDangerouslySkipPermissions: true, + }), + ...(input.resumeSessionAt === undefined ? {} : { resumeSessionAt: input.resumeSessionAt }), + }; +} + +async function recordTurn(input: { + readonly entries: Array; + readonly iterator: AsyncIterator; + readonly promptQueue: PromptQueue; + readonly prompt: string; + readonly label: string; + readonly pathFragments: ReadonlyArray; +}): Promise { + const message = makeClaudeUserMessage({ text: input.prompt }); + input.entries.push({ + type: "expect_outbound", + label: input.label, + frame: { type: "prompt.offer", message }, + }); + input.promptQueue.offer(message); + + let assistantMessageUuid: SDKAssistantMessage["uuid"] | null = null; + while (true) { + const next = await input.iterator.next(); + if (next.done === true) { + throw new Error(`Claude query ended before ${input.label} completed.`); + } + const frame = sanitizeValue(next.value, input.pathFragments); + input.entries.push({ + type: "emit_inbound", + label: next.value.type, + frame, + }); + if (next.value.type === "assistant") { + assistantMessageUuid = next.value.uuid; + } + if (next.value.type === "result") { + if (assistantMessageUuid === null) { + throw new Error(`Claude query completed ${input.label} without assistant UUID.`); + } + return assistantMessageUuid; + } + } +} + +function encodeTranscript(input: { + readonly entries: ReadonlyArray; + readonly metadata: Record; +}): string { + return [ + JSON.stringify({ + type: "transcript_start", + provider: CLAUDE_PROVIDER, + protocol: PROTOCOL, + version: "0.2.111", + scenario: SCENARIO, + metadata: input.metadata, + }), + ...input.entries.map((entry) => JSON.stringify(entry)), + "", + ].join("\n"); +} + +const outputPath = readArgValue("--out") ?? DEFAULT_OUTPUT; +const cwd = + process.env.T3_CLAUDE_REPLAY_CWD ?? + (await makeCheckpointWorkspace(`claude-agent-sdk-record-${SCENARIO}`)); +const cwdRealPath = await Effect.runPromise( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + return yield* fs.realPath(cwd); + }).pipe(Effect.provide(NodeServices.layer)), +); +const pathFragments = [ + [cwd, `/tmp/claude-replay-${SCENARIO}`], + [cwdRealPath, `/tmp/claude-replay-${SCENARIO}`], + [process.env.HOME ?? "", "/home/replay-user"], +] as const; +const sessionId = + process.env.T3_CLAUDE_REPLAY_SESSION_ID ?? (await Effect.runPromise(randomUuidV4)); +const entries: Array = []; +const metadata: Record = { + prompts: [SOURCE_PROMPT, FORK_FIRST_PROMPT, FORK_SECOND_PROMPT, FORK_AFTER_ROLLBACK_PROMPT], + model: MODEL_SELECTION.model, + nativeSessionId: sessionId, + queryMode: "fork_session_resume_at_fork_cursor", + tools: "claude_code", + permissionMode: "bypassPermissions", + generatedBy: "probeClaudeForkLocalRollbackReplay", +}; + +const sourceQueue = new PromptQueue(); +const sourceOptions = queryOptions({ cwd, nativeThreadId: sessionId, resume: false }); +entries.push({ + type: "expect_outbound", + label: "query.open:source", + frame: { type: "query.open", options: stableOptions(sourceOptions) }, +}); +const sourceRuntime = query({ prompt: sourceQueue, options: sourceOptions }); +try { + const sourceCursor = await recordTurn({ + entries, + iterator: sourceRuntime[Symbol.asyncIterator](), + promptQueue: sourceQueue, + prompt: SOURCE_PROMPT, + label: "prompt.offer:source", + pathFragments, + }); + sourceQueue.close(); + sourceRuntime.close(); + entries.push({ type: "runtime_exit", status: "success" }); + + entries.push({ + type: "expect_outbound", + label: "session.fork", + frame: { + type: "session.fork", + sessionId, + options: { + dir: `/tmp/claude-replay-${SCENARIO}`, + upToMessageId: sourceCursor, + }, + }, + }); + const forked = await forkSession(sessionId, { dir: cwd, upToMessageId: sourceCursor }); + metadata.sourceAssistantMessageUuids = [sourceCursor]; + metadata.forkUpToMessageId = sourceCursor; + metadata.forkedNativeSessionId = forked.sessionId; + entries.push({ + type: "emit_inbound", + label: "session.forked", + frame: { type: "session.forked", sessionId: forked.sessionId }, + }); + + const forkQueue = new PromptQueue(); + const forkOptions = queryOptions({ cwd, nativeThreadId: forked.sessionId, resume: true }); + entries.push({ + type: "expect_outbound", + label: "query.open:fork", + frame: { type: "query.open", options: stableOptions(forkOptions) }, + }); + const forkRuntime = query({ prompt: forkQueue, options: forkOptions }); + const forkIterator = forkRuntime[Symbol.asyncIterator](); + const forkFirstCursor = await recordTurn({ + entries, + iterator: forkIterator, + promptQueue: forkQueue, + prompt: FORK_FIRST_PROMPT, + label: "prompt.offer:fork-first", + pathFragments, + }); + const forkSecondCursor = await recordTurn({ + entries, + iterator: forkIterator, + promptQueue: forkQueue, + prompt: FORK_SECOND_PROMPT, + label: "prompt.offer:fork-second", + pathFragments, + }); + forkQueue.close(); + forkRuntime.close(); + entries.push({ type: "runtime_exit", status: "success" }); + metadata.forkAssistantMessageUuids = [forkFirstCursor, forkSecondCursor]; + metadata.resumeSessionAt = forkFirstCursor; + + const resumedQueue = new PromptQueue(); + const resumedOptions = queryOptions({ + cwd, + nativeThreadId: forked.sessionId, + resume: true, + resumeSessionAt: forkFirstCursor, + }); + entries.push({ + type: "expect_outbound", + label: "query.open:fork-resume-at-cursor", + frame: { type: "query.open", options: stableOptions(resumedOptions) }, + }); + const resumedRuntime = query({ prompt: resumedQueue, options: resumedOptions }); + await recordTurn({ + entries, + iterator: resumedRuntime[Symbol.asyncIterator](), + promptQueue: resumedQueue, + prompt: FORK_AFTER_ROLLBACK_PROMPT, + label: "prompt.offer:fork-after-rollback", + pathFragments, + }); + resumedQueue.close(); + resumedRuntime.close(); + entries.push({ type: "runtime_exit", status: "success" }); +} catch (error) { + sourceQueue.close(); + sourceRuntime.close(); + entries.push({ + type: "runtime_exit", + status: "error", + error: error instanceof Error ? error.message : String(error), + }); + throw error; +} + +await Effect.runPromise( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.makeDirectory(dirname(outputPath), { recursive: true }); + yield* fs.writeFileString(outputPath, encodeTranscript({ entries, metadata })); + yield* Console.log(`Wrote ${entries.length} ${PROTOCOL} replay entries to ${outputPath}`); + }).pipe(Effect.provide(NodeServices.layer)), +); diff --git a/apps/server/scripts/record-claude-agent-sdk-replay-fixture.ts b/apps/server/scripts/record-claude-agent-sdk-replay-fixture.ts new file mode 100644 index 00000000000..77f9e88d2a4 --- /dev/null +++ b/apps/server/scripts/record-claude-agent-sdk-replay-fixture.ts @@ -0,0 +1,444 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Schema from "effect/Schema"; + +import { + recordClaudeAgentSdkReplayTranscript, + CLAUDE_AGENT_SDK_REPLAY_PROTOCOL, +} from "../src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts"; +import { claudeRuntimeQueryPolicyForRuntimePolicy } from "../src/orchestration-v2/Adapters/ClaudeAdapterV2.ts"; +import { + ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2RuntimePolicy as ProviderAdapterV2RuntimePolicyType, +} from "../src/orchestration-v2/ProviderAdapter.ts"; +import type { RuntimePolicyV2Override } from "../src/orchestration-v2/RuntimePolicy.ts"; +import { makeCheckpointWorkspace } from "../src/orchestration-v2/testkit/ReplayFixtureWorkspace.ts"; +import { CLAUDE_MODEL_SELECTION } from "../src/orchestration-v2/testkit/fixtures/shared.ts"; +import { + MESSAGE_STEERING_INITIAL_PROMPT, + MULTI_TURN_FIRST_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + READ_ONLY_NEVER_POLICY, + READ_ONLY_ON_REQUEST_POLICY, + RESTRICTED_GRANULAR_POLICY, + MULTI_TURN_SECOND_PROMPT, + SIMPLE_PROMPT, + SUBAGENT_PROMPT, + THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT, + THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT, + THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT, + THREAD_FORK_NATIVE_CONTINUE_FIRST_PROMPT, + THREAD_FORK_NATIVE_CONTINUE_SECOND_PROMPT, + THREAD_FORK_NATIVE_CONTINUE_SOURCE_PROMPT, + THREAD_FORK_NATIVE_SIBLINGS_FIRST_PROMPT, + THREAD_FORK_NATIVE_SIBLINGS_SECOND_PROMPT, + THREAD_FORK_NATIVE_SIBLINGS_SOURCE_PROMPT, + THREAD_FORK_NATIVE_SOURCE_PROMPT, + THREAD_FORK_NATIVE_TARGET_PROMPT, + THREAD_MERGE_BACK_FORK_PROMPT, + THREAD_MERGE_BACK_HANDOFF_PROMPT, + THREAD_MERGE_BACK_RECALL_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT, + THREAD_MERGE_BACK_SOURCE_PROMPT, + THREAD_ROLLBACK_AFTER_PROMPT, + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_SECOND_PROMPT, + TOOL_CALL_READ_ONLY_PROMPT, + TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, + TOOL_CALL_WRITE_PROMPT, + TURN_INTERRUPT_MID_TOOL_PROMPT, + TURN_INTERRUPT_PROMPT, + TURN_INTERRUPT_RECOVERY_PROMPT, + WORKSPACE_NEVER_POLICY, + WEB_SEARCH_PROMPT, +} from "../src/orchestration-v2/testkit/fixtures/shared.ts"; + +const CLAUDE_RECORDINGS = { + simple: { + prompts: [SIMPLE_PROMPT], + defaultTranscriptFile: "fixtures/simple/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + }, + multi_turn: { + prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT], + defaultTranscriptFile: "fixtures/multi_turn/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + }, + multi_turn_restart: { + prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT], + defaultTranscriptFile: "fixtures/multi_turn_restart/claude_transcript.ndjson", + queryMode: "restart", + enableTools: true, + }, + queued_turn: { + prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT], + defaultTranscriptFile: "fixtures/queued_turn/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + }, + message_steering: { + prompts: [MESSAGE_STEERING_INITIAL_PROMPT, MESSAGE_STEERING_STEER_PROMPT], + defaultTranscriptFile: "fixtures/message_steering/claude_transcript.ndjson", + queryMode: "active_steering", + enableTools: true, + }, + turn_interrupt_mid_tool: { + prompts: [TURN_INTERRUPT_MID_TOOL_PROMPT], + defaultTranscriptFile: "fixtures/turn_interrupt_mid_tool/claude_transcript.ndjson", + queryMode: "interrupt", + enableTools: true, + interruptAfter: "tool_use", + }, + turn_interrupt: { + prompts: [TURN_INTERRUPT_PROMPT], + defaultTranscriptFile: "fixtures/turn_interrupt/claude_transcript.ndjson", + queryMode: "interrupt", + enableTools: true, + }, + turn_interrupt_restart: { + prompts: [TURN_INTERRUPT_MID_TOOL_PROMPT, TURN_INTERRUPT_RECOVERY_PROMPT], + defaultTranscriptFile: "fixtures/turn_interrupt_restart/claude_transcript.ndjson", + queryMode: "interrupt_restart", + enableTools: true, + interruptAfter: "tool_use", + }, + tool_call_read_only: { + prompts: [TOOL_CALL_READ_ONLY_PROMPT], + defaultTranscriptFile: "fixtures/tool_call_read_only/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + }, + tool_call_read_only_on_request: { + prompts: [TOOL_CALL_WRITE_PROMPT], + defaultTranscriptFile: "fixtures/tool_call_read_only_on_request/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + runtimePolicyOverride: READ_ONLY_ON_REQUEST_POLICY, + }, + tool_call_workspace_never: { + prompts: [TOOL_CALL_WRITE_PROMPT], + defaultTranscriptFile: "fixtures/tool_call_workspace_never/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + }, + tool_call_restricted_granular: { + prompts: [TOOL_CALL_WRITE_PROMPT], + defaultTranscriptFile: "fixtures/tool_call_restricted_granular/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + runtimePolicyOverride: RESTRICTED_GRANULAR_POLICY, + }, + web_search: { + prompts: [WEB_SEARCH_PROMPT], + defaultTranscriptFile: "fixtures/web_search/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + }, + subagent: { + prompts: [SUBAGENT_PROMPT], + defaultTranscriptFile: "fixtures/subagent/claude_transcript.ndjson", + queryMode: "streaming", + enableTools: true, + }, + thread_rollback: { + prompts: [ + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_SECOND_PROMPT, + THREAD_ROLLBACK_AFTER_PROMPT, + ], + defaultTranscriptFile: "fixtures/thread_rollback/claude_transcript.ndjson", + queryMode: "resume_at_cursor", + enableTools: true, + }, + thread_fork_native: { + prompts: [THREAD_FORK_NATIVE_SOURCE_PROMPT, THREAD_FORK_NATIVE_TARGET_PROMPT], + defaultTranscriptFile: "fixtures/thread_fork_native/claude_transcript.ndjson", + queryMode: "fork_session", + enableTools: true, + }, + thread_fork_native_prior_turn: { + prompts: [ + THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT, + THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT, + THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT, + ], + defaultTranscriptFile: "fixtures/thread_fork_native_prior_turn/claude_transcript.ndjson", + queryMode: "fork_session_prior_turn", + enableTools: true, + }, + thread_fork_native_continue: { + prompts: [ + THREAD_FORK_NATIVE_CONTINUE_SOURCE_PROMPT, + THREAD_FORK_NATIVE_CONTINUE_FIRST_PROMPT, + THREAD_FORK_NATIVE_CONTINUE_SECOND_PROMPT, + ], + defaultTranscriptFile: "fixtures/thread_fork_native_continue/claude_transcript.ndjson", + queryMode: "fork_session_continue", + enableTools: true, + }, + thread_fork_native_siblings: { + prompts: [ + THREAD_FORK_NATIVE_SIBLINGS_SOURCE_PROMPT, + THREAD_FORK_NATIVE_SIBLINGS_FIRST_PROMPT, + THREAD_FORK_NATIVE_SIBLINGS_SECOND_PROMPT, + ], + defaultTranscriptFile: "fixtures/thread_fork_native_siblings/claude_transcript.ndjson", + queryMode: "fork_session_siblings", + enableTools: true, + }, + thread_merge_back_continue: { + prompts: [ + THREAD_MERGE_BACK_SOURCE_PROMPT, + THREAD_MERGE_BACK_FORK_PROMPT, + THREAD_MERGE_BACK_HANDOFF_PROMPT, + THREAD_MERGE_BACK_RECALL_PROMPT, + ], + defaultTranscriptFile: "fixtures/thread_merge_back_continue/claude_transcript.ndjson", + queryMode: "fork_session_merge_back", + enableTools: true, + }, + thread_merge_back_siblings: { + prompts: [ + THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT, + ], + defaultTranscriptFile: "fixtures/thread_merge_back_siblings/claude_transcript.ndjson", + queryMode: "fork_session_merge_back_siblings", + enableTools: true, + }, +} as const; + +function readArgValue(name: string): string | undefined { + const args = process.argv.slice(2); + const index = args.indexOf(name); + return index >= 0 ? args[index + 1] : undefined; +} + +type ClaudeRecordingQueryMode = + | "streaming" + | "restart" + | "resume_at_cursor" + | "fork_session" + | "fork_session_prior_turn" + | "fork_session_continue" + | "fork_session_siblings" + | "fork_session_merge_back" + | "fork_session_merge_back_siblings" + | "active_steering" + | "interrupt" + | "interrupt_restart"; + +function selectedQueryMode(defaultMode: ClaudeRecordingQueryMode): ClaudeRecordingQueryMode { + const raw = readArgValue("--query-mode") ?? process.env.T3_CLAUDE_REPLAY_QUERY_MODE; + if (raw === undefined) { + return defaultMode; + } + if ( + raw === "streaming" || + raw === "restart" || + raw === "resume_at_cursor" || + raw === "fork_session" || + raw === "fork_session_prior_turn" || + raw === "fork_session_continue" || + raw === "fork_session_siblings" || + raw === "fork_session_merge_back" || + raw === "fork_session_merge_back_siblings" || + raw === "active_steering" || + raw === "interrupt" || + raw === "interrupt_restart" + ) { + return raw; + } + throw new Error( + `Unsupported Claude replay query mode '${raw}'. Use 'streaming', 'restart', 'resume_at_cursor', 'fork_session', 'fork_session_prior_turn', 'fork_session_continue', 'fork_session_siblings', 'fork_session_merge_back', 'fork_session_merge_back_siblings', 'active_steering', 'interrupt', or 'interrupt_restart'.`, + ); +} + +const scenario = readArgValue("--scenario") ?? process.env.T3_CLAUDE_REPLAY_SCENARIO ?? "simple"; +const recording = CLAUDE_RECORDINGS[scenario as keyof typeof CLAUDE_RECORDINGS]; +const encodeUnknownJsonString = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); + +if (recording === undefined) { + throw new Error( + `Claude replay fixture '${scenario}' is not configured. ` + + "TODO: approval fixtures need permission callback recording before they can be generated.", + ); +} + +const positionalOutputPath = process.argv[2]?.startsWith("--") ? undefined : process.argv[2]; +const outputPath = + readArgValue("--out") ?? + positionalOutputPath ?? + new URL(`../src/orchestration-v2/testkit/${recording.defaultTranscriptFile}`, import.meta.url) + .pathname; + +function encodeTranscriptNdjson( + transcript: Awaited>, +): string { + const { entries, ...metadata } = transcript; + return [ + JSON.stringify({ type: "transcript_start", ...metadata }), + ...entries.map((entry) => JSON.stringify(entry)), + "", + ].join("\n"); +} + +function dirname(filePath: string): string { + const normalized = filePath.replace(/\/+$/u, ""); + const lastSlash = normalized.lastIndexOf("/"); + if (lastSlash < 0) { + return "."; + } + return lastSlash === 0 ? "/" : normalized.slice(0, lastSlash); +} + +function joinPath(directory: string, fileName: string): string { + return `${directory.replace(/\/+$/u, "")}/${fileName.replace(/^\/+/u, "")}`; +} + +function runFileSystem(effect: Effect.Effect): Promise { + return Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))); +} + +function selectedPrompts(): ReadonlyArray { + if (process.env.T3_CLAUDE_REPLAY_PROMPTS !== undefined) { + return process.env.T3_CLAUDE_REPLAY_PROMPTS.split("\n---\n").filter( + (prompt) => prompt.length > 0, + ); + } + if (process.env.T3_CLAUDE_REPLAY_PROMPT !== undefined) { + return [process.env.T3_CLAUDE_REPLAY_PROMPT]; + } + return recording.prompts; +} + +function runtimePolicyForRecording(input: { + readonly cwd: string; + readonly override?: RuntimePolicyV2Override; +}): ProviderAdapterV2RuntimePolicyType { + return ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: input.override?.cwd ?? input.cwd, + ...(input.override?.approvalPolicy === undefined + ? {} + : { approvalPolicy: input.override.approvalPolicy }), + ...(input.override?.sandboxPolicy === undefined + ? {} + : { sandboxPolicy: input.override.sandboxPolicy }), + ...(input.override?.reasoningEffort === undefined + ? {} + : { reasoningEffort: input.override.reasoningEffort }), + }); +} + +async function makeToolCallReadOnlyRecordingWorkspace(): Promise { + await runFileSystem( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, { recursive: true, force: true }); + yield* fs.makeDirectory(TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, { recursive: true }); + }), + ); + return TOOL_CALL_READ_ONLY_WORKSPACE_ROOT; +} + +const cwd = + process.env.T3_CLAUDE_REPLAY_CWD ?? + (scenario === "tool_call_read_only" + ? await makeToolCallReadOnlyRecordingWorkspace() + : await makeCheckpointWorkspace(`claude-agent-sdk-record-${scenario}`)); +const shouldRemoveCwd = process.env.T3_CLAUDE_REPLAY_CWD === undefined; + +if (shouldRemoveCwd && (scenario === "tool_call_read_only" || scenario === "subagent")) { + await runFileSystem( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.writeFileString( + joinPath(cwd, "package.json"), + encodeUnknownJsonString({ + name: "claude-read-only-fixture", + private: true, + scripts: { typecheck: "tsc --noEmit" }, + }), + ); + yield* fs.writeFileString( + joinPath(cwd, "tsconfig.json"), + encodeUnknownJsonString({ + compilerOptions: { + module: "ESNext", + strict: true, + target: "ES2022", + }, + }), + ); + }), + ); +} + +try { + const runtimePolicy = runtimePolicyForRecording({ + cwd, + ...("runtimePolicyOverride" in recording ? { override: recording.runtimePolicyOverride } : {}), + }); + const queryPolicy = claudeRuntimeQueryPolicyForRuntimePolicy(runtimePolicy); + + const transcript = await recordClaudeAgentSdkReplayTranscript({ + scenario, + prompts: selectedPrompts(), + modelSelection: { + ...CLAUDE_MODEL_SELECTION, + model: process.env.T3_CLAUDE_REPLAY_MODEL ?? CLAUDE_MODEL_SELECTION.model, + }, + cwd, + ...(process.env.T3_CLAUDE_REPLAY_SESSION_ID === undefined + ? {} + : { sessionId: process.env.T3_CLAUDE_REPLAY_SESSION_ID }), + queryMode: selectedQueryMode(recording.queryMode), + ...("enableTools" in recording && recording.enableTools === true ? { enableTools: true } : {}), + ...(queryPolicy.tools === undefined ? {} : { tools: queryPolicy.tools }), + permissionMode: queryPolicy.permissionMode, + ...(queryPolicy.allowedTools === undefined ? {} : { allowedTools: queryPolicy.allowedTools }), + ...(queryPolicy.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: queryPolicy.allowDangerouslySkipPermissions }), + ...(queryPolicy.installPermissionCallback ? { enablePermissionCallback: true } : {}), + ...("interruptAfter" in recording ? { interruptAfter: recording.interruptAfter } : {}), + }); + await runFileSystem( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.makeDirectory(dirname(outputPath), { recursive: true }); + yield* fs.writeFileString(outputPath, encodeTranscriptNdjson(transcript)); + }), + ); + await Effect.runPromise( + Console.log( + `Wrote ${transcript.entries.length} ${CLAUDE_AGENT_SDK_REPLAY_PROTOCOL} replay entries to ${outputPath}`, + ), + ); +} finally { + if (shouldRemoveCwd) { + await runFileSystem( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(cwd, { recursive: true, force: true }); + }), + ); + } +} diff --git a/apps/server/scripts/record-codex-app-server-replay-fixture.ts b/apps/server/scripts/record-codex-app-server-replay-fixture.ts new file mode 100644 index 00000000000..026703d1115 --- /dev/null +++ b/apps/server/scripts/record-codex-app-server-replay-fixture.ts @@ -0,0 +1,1428 @@ +import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { resolveSpawnCommand } from "@t3tools/shared/shell"; +import * as Console from "effect/Console"; +import * as Deferred from "effect/Deferred"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; +import * as Effect from "effect/Effect"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; + +import * as CodexClient from "effect-codex-app-server/client"; +import type * as CodexSchema from "effect-codex-app-server/schema"; + +import { + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + MULTI_TURN_FIRST_PROMPT, + MULTI_TURN_SECOND_PROMPT, + PLAN_QUESTIONS_PROMPT, + PROPOSED_PLAN_PROMPT, + PROVIDER_THREAD_RESUME_FIRST_PROMPT, + PROVIDER_THREAD_RESUME_SECOND_PROMPT, + SIMPLE_PROMPT, + SUBAGENT_CONTINUE_PARENT_PROMPT, + SUBAGENT_CONTINUE_PROMPT, + SUBAGENT_PROMPT, + THREAD_ROLLBACK_AFTER_PROMPT, + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_SECOND_PROMPT, + THREAD_FORK_NATIVE_CONTINUE_FIRST_PROMPT, + THREAD_FORK_NATIVE_CONTINUE_SECOND_PROMPT, + THREAD_FORK_NATIVE_CONTINUE_SOURCE_PROMPT, + THREAD_FORK_NATIVE_SIBLINGS_FIRST_PROMPT, + THREAD_FORK_NATIVE_SIBLINGS_SECOND_PROMPT, + THREAD_FORK_NATIVE_SIBLINGS_SOURCE_PROMPT, + THREAD_MERGE_BACK_FORK_PROMPT, + THREAD_MERGE_BACK_HANDOFF_PROMPT, + THREAD_MERGE_BACK_RECALL_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT, + THREAD_MERGE_BACK_SOURCE_PROMPT, + TODO_LIST_PROMPT, + TOOL_CALL_WRITE_PROMPT, + TURN_INTERRUPT_MID_TOOL_PROMPT, + TURN_INTERRUPT_PROMPT, +} from "../src/orchestration-v2/testkit/fixtures/shared.ts"; + +const CODEX_REPLAY_PLAN_MODE_DEVELOPER_INSTRUCTIONS = + process.env.T3_CODEX_REPLAY_PLAN_DEVELOPER_INSTRUCTIONS ?? + "You are in Plan mode. Prefer request_user_input for clarifying questions. When presenting a complete plan, wrap it in and ."; +const CODEX_CLIENT_INFO = { + name: "t3code_desktop", + title: "T3 Code Desktop", + version: "0.1.0", +} as const; +const CODEX_CLIENT_CAPABILITIES = { + experimentalApi: true, +} as const; + +const SCENARIO_NAMES = [ + "simple", + "tool_call_read_only_on_request", + "tool_call_workspace_never", + "tool_call_restricted_granular", + "subagent", + "subagent_continue", + "multi_turn", + "provider_thread_resume", + "todo_list", + "plan_questions", + "proposed_plan", + "message_steering", + "turn_interrupt", + "turn_interrupt_mid_tool", + "thread_rollback", + "thread_fork_native_continue", + "thread_fork_native_siblings", + "thread_merge_back_continue", + "thread_merge_back_siblings", +] as const; + +type ScenarioName = (typeof SCENARIO_NAMES)[number]; +type TurnStartParams = CodexSchema.ClientRequestParamsByMethod["turn/start"] & { + readonly collaborationMode?: CodexSchema.V2TurnStartParams__CollaborationMode; +}; +type TurnStartInput = TurnStartParams["input"]; +type TurnStartResponse = CodexSchema.ClientRequestResponsesByMethod["turn/start"]; +type SandboxPolicy = NonNullable; +type ApprovalPolicy = NonNullable; + +interface ReplayRun { + readonly name: string; + readonly prompt?: string; + readonly description: string; + readonly steps: ReadonlyArray; + readonly turnDefaults?: Omit; +} + +type ReplayStep = + | { + readonly type: "turn"; + readonly label: string; + readonly prompt: string; + readonly thread?: string; + readonly turnOverrides?: Omit; + } + | { + readonly type: "steeredTurn"; + readonly label: string; + readonly prompt: string; + readonly steer: string; + readonly turnOverrides?: Omit; + } + | { + readonly type: "interruptedTurn"; + readonly label: string; + readonly prompt: string; + readonly interruptAfterMs?: number; + readonly interruptAfterCommandExecutionStarted?: boolean; + readonly turnOverrides?: Omit; + } + | { + readonly type: "rollback"; + readonly label: string; + readonly numTurns: number; + } + | { + readonly type: "fork"; + readonly label: string; + readonly from?: string; + readonly as?: string; + }; +type TurnReplayStep = Exclude< + ReplayStep, + { readonly type: "rollback" } | { readonly type: "fork" } +>; + +interface ReplayScenario { + readonly name: ScenarioName; + readonly fileName: `${ScenarioName}.ndjson`; + readonly description: string; + readonly runs: ReadonlyArray; +} + +interface Recorder { + readonly path: string; + readonly setVersion: (version: string) => Effect.Effect; + readonly writeRecord: ( + record: Record, + ) => Effect.Effect; + readonly flush: () => Effect.Effect; +} + +function readArgValue(name: string): string | undefined { + const args = process.argv.slice(2); + const index = args.indexOf(name); + return index >= 0 ? args[index + 1] : undefined; +} + +function readArgValues(name: string): ReadonlyArray { + const args = process.argv.slice(2); + return args.flatMap((arg, index) => (arg === name && args[index + 1] ? [args[index + 1]!] : [])); +} + +function defaultTranscriptPath(scenario: ReplayScenario): string { + return new URL( + `../src/orchestration-v2/testkit/fixtures/${scenario.name}/codex_transcript.ndjson`, + import.meta.url, + ).pathname; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function parseScenarios(): ReadonlyArray { + const rawValues = [ + ...readArgValues("--scenario"), + ...(process.env.T3_CODEX_REPLAY_SCENARIOS ? [process.env.T3_CODEX_REPLAY_SCENARIOS] : []), + ]; + const requested = rawValues.length > 0 ? rawValues : ["simple"]; + const names = requested.flatMap((value) => + value + .split(",") + .map((part) => part.trim()) + .filter((part) => part.length > 0), + ); + + if (names.includes("all")) { + return SCENARIO_NAMES; + } + + const expandedNames = names.flatMap((name) => + name === "tool_call" + ? [ + "tool_call_read_only_on_request", + "tool_call_workspace_never", + "tool_call_restricted_granular", + ] + : [name], + ); + + const invalid = expandedNames.filter( + (name): name is string => !SCENARIO_NAMES.includes(name as ScenarioName), + ); + if (invalid.length > 0) { + throw new Error(`Unknown scenario(s): ${invalid.join(", ")}`); + } + + return [...new Set(expandedNames)] as ReadonlyArray; +} + +function classifyJsonRpcPayload(payload: unknown): string { + if (!isRecord(payload)) { + return "unknown"; + } + if (typeof payload.method === "string" && "id" in payload) { + return "request"; + } + if (typeof payload.method === "string") { + return "notification"; + } + if ("id" in payload && "error" in payload) { + return "error_response"; + } + if ("id" in payload && "result" in payload) { + return "response"; + } + return "unknown"; +} + +function protocolMethod(payload: unknown): string | undefined { + return isRecord(payload) && typeof payload.method === "string" ? payload.method : undefined; +} + +function protocolId(payload: unknown): string | number | undefined { + if (!isRecord(payload)) { + return undefined; + } + return typeof payload.id === "string" || typeof payload.id === "number" ? payload.id : undefined; +} + +function protocolResult(payload: unknown): unknown { + return isRecord(payload) && "result" in payload ? payload.result : undefined; +} + +function inferCodexVersion(payload: unknown): string | undefined { + const result = protocolResult(payload); + if (!isRecord(result)) { + return undefined; + } + + if (typeof result.userAgent === "string") { + const match = result.userAgent.match(/\/([0-9][^\s)]*)/u); + if (match?.[1]) { + return match[1]; + } + } + + const thread = result.thread; + return isRecord(thread) && typeof thread.cliVersion === "string" ? thread.cliVersion : undefined; +} + +function turnInput(prompt: string): TurnStartInput { + return [{ type: "text", text: prompt }]; +} + +function getTurnId(response: TurnStartResponse): string { + return response.turn.id; +} + +function readOnlyFullAccessSandbox(): SandboxPolicy { + return { + networkAccess: false, + type: "readOnly", + }; +} + +function readOnlyRestrictedSandbox(): SandboxPolicy { + return { + networkAccess: false, + type: "readOnly", + }; +} + +function workspaceWriteSandbox(): SandboxPolicy { + return { + networkAccess: false, + type: "workspaceWrite", + writableRoots: [], + }; +} + +function granularApprovalPolicy(): ApprovalPolicy { + return { + granular: { + mcp_elicitations: true, + request_permissions: true, + rules: true, + sandbox_approval: true, + skill_approval: true, + }, + }; +} + +function collaborationMode( + mode: Extract, +): CodexSchema.V2TurnStartParams__CollaborationMode { + return { + mode, + settings: { + developer_instructions: CODEX_REPLAY_PLAN_MODE_DEVELOPER_INSTRUCTIONS, + model: "gpt-5.4", + reasoning_effort: "medium", + }, + }; +} + +function scenarios(): ReadonlyArray { + return [ + { + name: "simple", + fileName: "simple.ndjson", + description: "One thread and one turn with a deterministic text-only response.", + runs: [ + { + name: "simple", + description: "Single text-only turn.", + prompt: SIMPLE_PROMPT, + steps: [{ type: "turn", label: "simple", prompt: SIMPLE_PROMPT }], + }, + ], + }, + { + name: "tool_call_read_only_on_request", + fileName: "tool_call_read_only_on_request.ndjson", + description: + "Write a small fixture file with read-only full filesystem visibility and on-request approvals.", + runs: [ + { + name: "read-only-on-request", + description: + "Write action under read-only full filesystem visibility with on-request approvals.", + prompt: TOOL_CALL_WRITE_PROMPT, + turnDefaults: { + approvalPolicy: "on-request", + sandboxPolicy: readOnlyFullAccessSandbox(), + }, + steps: [{ type: "turn", label: "write-fixture-file", prompt: TOOL_CALL_WRITE_PROMPT }], + }, + ], + }, + { + name: "tool_call_workspace_never", + fileName: "tool_call_workspace_never.ndjson", + description: + "Write a small fixture file with workspace-write sandbox policy and never approvals.", + runs: [ + { + name: "workspace-never", + description: + "Write action under workspace-write policy with never approvals for baseline no-prompt behavior.", + prompt: TOOL_CALL_WRITE_PROMPT, + turnDefaults: { + approvalPolicy: "never", + sandboxPolicy: workspaceWriteSandbox(), + }, + steps: [{ type: "turn", label: "write-fixture-file", prompt: TOOL_CALL_WRITE_PROMPT }], + }, + ], + }, + { + name: "tool_call_restricted_granular", + fileName: "tool_call_restricted_granular.ndjson", + description: + "Write a small fixture file with restricted read access and granular approval flags enabled.", + runs: [ + { + name: "restricted-granular", + description: + "Write action under restricted read access with granular approval flags enabled, intended to capture permission request flows when Codex escalates.", + prompt: TOOL_CALL_WRITE_PROMPT, + turnDefaults: { + approvalPolicy: granularApprovalPolicy(), + sandboxPolicy: readOnlyRestrictedSandbox(), + }, + steps: [ + { + type: "turn", + label: "write-fixture-file", + prompt: TOOL_CALL_WRITE_PROMPT, + }, + ], + }, + ], + }, + { + name: "subagent", + fileName: "subagent.ndjson", + description: "One root turn that asks Codex to spawn two collab agents.", + runs: [ + { + name: "two-subagents", + description: "Root turn asks for two subagents reading different files.", + prompt: SUBAGENT_PROMPT, + turnDefaults: { + approvalPolicy: "on-request", + sandboxPolicy: readOnlyFullAccessSandbox(), + }, + steps: [{ type: "turn", label: "spawn-two-subagents", prompt: SUBAGENT_PROMPT }], + }, + ], + }, + { + name: "subagent_continue", + fileName: "subagent_continue.ndjson", + description: + "A root turn spawns one native Codex subagent, then a later root turn resumes and messages the same child thread.", + runs: [ + { + name: "continued-subagent", + description: + "The second root turn asks Codex to continue the subagent created by the first root turn.", + steps: [ + { + type: "turn", + label: "spawn-subagent", + prompt: SUBAGENT_CONTINUE_PROMPT, + }, + { + type: "turn", + label: "continue-subagent", + prompt: SUBAGENT_CONTINUE_PARENT_PROMPT, + }, + ], + }, + ], + }, + { + name: "multi_turn", + fileName: "multi_turn.ndjson", + description: "One thread with two sequential user turns.", + runs: [ + { + name: "two-turns-same-thread", + description: "Second turn starts after the first root turn completes.", + steps: [ + { + type: "turn", + label: "first", + prompt: MULTI_TURN_FIRST_PROMPT, + }, + { + type: "turn", + label: "second", + prompt: MULTI_TURN_SECOND_PROMPT, + }, + ], + }, + ], + }, + { + name: "provider_thread_resume", + fileName: "provider_thread_resume.ndjson", + description: + "One provider-native thread is started, completed, then resumed by thread id in a fresh app-server session.", + runs: [ + { + name: "resume-provider-thread", + description: + "First turn completes, the app-server runtime is restarted, thread/resume loads the existing provider thread, then a second turn completes.", + steps: [ + { + type: "turn", + label: "first-before-resume", + prompt: PROVIDER_THREAD_RESUME_FIRST_PROMPT, + }, + { + type: "turn", + label: "second-after-resume", + prompt: PROVIDER_THREAD_RESUME_SECOND_PROMPT, + }, + ], + }, + ], + }, + { + name: "todo_list", + fileName: "todo_list.ndjson", + description: "One turn that asks Codex to emit progress through update_plan.", + runs: [ + { + name: "todo-list", + description: "Default-mode turn that should surface turn/plan/updated notifications.", + prompt: TODO_LIST_PROMPT, + turnDefaults: { + approvalPolicy: "never", + sandboxPolicy: readOnlyFullAccessSandbox(), + }, + steps: [{ type: "turn", label: "todo-list", prompt: TODO_LIST_PROMPT }], + }, + ], + }, + { + name: "plan_questions", + fileName: "plan_questions.ndjson", + description: "One plan-mode turn that asks a structured clarifying question.", + runs: [ + { + name: "plan-questions", + description: "Plan-mode turn intended to surface item/tool/requestUserInput.", + prompt: PLAN_QUESTIONS_PROMPT, + turnDefaults: { + approvalPolicy: "never", + collaborationMode: collaborationMode("plan"), + sandboxPolicy: readOnlyFullAccessSandbox(), + }, + steps: [{ type: "turn", label: "plan-questions", prompt: PLAN_QUESTIONS_PROMPT }], + }, + ], + }, + { + name: "proposed_plan", + fileName: "proposed_plan.ndjson", + description: "One plan-mode turn that emits a proposed plan document.", + runs: [ + { + name: "proposed-plan", + description: + "Plan-mode turn intended to surface item/plan/delta and completed plan item.", + prompt: PROPOSED_PLAN_PROMPT, + turnDefaults: { + approvalPolicy: "never", + collaborationMode: collaborationMode("plan"), + sandboxPolicy: readOnlyFullAccessSandbox(), + }, + steps: [{ type: "turn", label: "proposed-plan", prompt: PROPOSED_PLAN_PROMPT }], + }, + ], + }, + { + name: "message_steering", + fileName: "message_steering.ndjson", + description: "One active turn receives an immediate turn/steer request.", + runs: [ + { + name: "immediate-steer", + description: "Start a turn, then immediately steer the active root turn.", + steps: [ + { + type: "steeredTurn", + label: "steered", + prompt: MESSAGE_STEERING_INITIAL_PROMPT, + steer: MESSAGE_STEERING_STEER_PROMPT, + }, + ], + }, + ], + }, + { + name: "turn_interrupt", + fileName: "turn_interrupt.ndjson", + description: "One active turn is interrupted before it finishes naturally.", + runs: [ + { + name: "interrupt-active-turn", + description: "Start a long-running turn, then send turn/interrupt.", + prompt: TURN_INTERRUPT_PROMPT, + turnDefaults: { + approvalPolicy: "never", + sandboxPolicy: workspaceWriteSandbox(), + }, + steps: [ + { + type: "interruptedTurn", + label: "interrupt-active-turn", + prompt: TURN_INTERRUPT_PROMPT, + interruptAfterMs: 1_500, + }, + ], + }, + ], + }, + { + name: "turn_interrupt_mid_tool", + fileName: "turn_interrupt_mid_tool.ndjson", + description: + "One active turn is interrupted after Codex has already executed a local command.", + runs: [ + { + name: "interrupt-after-command-execution", + description: "Start a turn, wait for command execution, then send turn/interrupt.", + prompt: TURN_INTERRUPT_MID_TOOL_PROMPT, + turnDefaults: { + approvalPolicy: "never", + sandboxPolicy: workspaceWriteSandbox(), + }, + steps: [ + { + type: "interruptedTurn", + label: "interrupt-after-command-execution", + prompt: TURN_INTERRUPT_MID_TOOL_PROMPT, + interruptAfterCommandExecutionStarted: true, + }, + ], + }, + ], + }, + { + name: "thread_rollback", + fileName: "thread_rollback.ndjson", + description: + "One thread completes two turns, rolls back the most recent turn, then starts another turn.", + runs: [ + { + name: "rollback-one-turn", + description: + "Two completed turns, thread/rollback numTurns=1, then a post-rollback turn.", + steps: [ + { + type: "turn", + label: "first-before-rollback", + prompt: THREAD_ROLLBACK_FIRST_PROMPT, + }, + { + type: "turn", + label: "second-before-rollback", + prompt: THREAD_ROLLBACK_SECOND_PROMPT, + }, + { + type: "rollback", + label: "rollback-latest-turn", + numTurns: 1, + }, + { + type: "turn", + label: "post-rollback", + prompt: THREAD_ROLLBACK_AFTER_PROMPT, + }, + ], + }, + ], + }, + { + name: "thread_fork_native_continue", + fileName: "thread_fork_native_continue.ndjson", + description: + "One source marker turn, a native thread fork, a fork-local marker turn, and a recall turn that requires both contexts.", + runs: [ + { + name: "continue-native-fork", + description: + "Proves the native fork inherits source context and retains its own first turn into a second fork-local turn.", + steps: [ + { + type: "turn", + label: "source", + prompt: THREAD_FORK_NATIVE_CONTINUE_SOURCE_PROMPT, + }, + { + type: "fork", + label: "fork-source", + }, + { + type: "turn", + label: "fork-first-delta", + prompt: THREAD_FORK_NATIVE_CONTINUE_FIRST_PROMPT, + }, + { + type: "turn", + label: "fork-second-delta", + prompt: THREAD_FORK_NATIVE_CONTINUE_SECOND_PROMPT, + }, + ], + }, + ], + }, + { + name: "thread_fork_native_siblings", + fileName: "thread_fork_native_siblings.ndjson", + description: + "One source marker turn and two independent native forks that each recall the source plus only their own fork marker.", + runs: [ + { + name: "native-sibling-forks", + description: + "Creates two native forks from the same source provider thread and proves their contexts remain isolated.", + steps: [ + { + type: "turn", + label: "source", + prompt: THREAD_FORK_NATIVE_SIBLINGS_SOURCE_PROMPT, + thread: "source", + }, + { + type: "fork", + label: "fork-first", + from: "source", + as: "first", + }, + { + type: "turn", + label: "first-recall", + prompt: THREAD_FORK_NATIVE_SIBLINGS_FIRST_PROMPT, + thread: "first", + }, + { + type: "fork", + label: "fork-second", + from: "source", + as: "second", + }, + { + type: "turn", + label: "second-recall", + prompt: THREAD_FORK_NATIVE_SIBLINGS_SECOND_PROMPT, + thread: "second", + }, + ], + }, + ], + }, + { + name: "thread_merge_back_continue", + fileName: "thread_merge_back_continue.ndjson", + description: + "A source thread consumes one fork-delta handoff and later recalls source and transferred context.", + runs: [ + { + name: "merge-native-fork", + description: + "Proves a merge-back handoff becomes durable context on the original provider thread.", + steps: [ + { + type: "turn", + label: "source", + prompt: THREAD_MERGE_BACK_SOURCE_PROMPT, + thread: "source", + }, + { + type: "fork", + label: "fork", + from: "source", + as: "fork", + }, + { + type: "turn", + label: "fork-delta", + prompt: THREAD_MERGE_BACK_FORK_PROMPT, + thread: "fork", + }, + { + type: "turn", + label: "merge-back", + prompt: THREAD_MERGE_BACK_HANDOFF_PROMPT, + thread: "source", + }, + { + type: "turn", + label: "source-recall", + prompt: THREAD_MERGE_BACK_RECALL_PROMPT, + thread: "source", + }, + ], + }, + ], + }, + { + name: "thread_merge_back_siblings", + fileName: "thread_merge_back_siblings.ndjson", + description: + "Two sibling fork deltas are merged sequentially into one source provider thread and recalled together.", + runs: [ + { + name: "merge-native-sibling-forks", + description: + "Proves repeated merge-back handoffs accumulate on the source while native sibling forks remain independent.", + steps: [ + { + type: "turn", + label: "source", + prompt: THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT, + thread: "source", + }, + { + type: "fork", + label: "fork-first", + from: "source", + as: "first", + }, + { + type: "turn", + label: "first-fork-delta", + prompt: THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT, + thread: "first", + }, + { + type: "fork", + label: "fork-second", + from: "source", + as: "second", + }, + { + type: "turn", + label: "second-fork-delta", + prompt: THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT, + thread: "second", + }, + { + type: "turn", + label: "merge-first", + prompt: THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT, + thread: "source", + }, + { + type: "turn", + label: "merge-second", + prompt: THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT, + thread: "source", + }, + { + type: "turn", + label: "source-recall", + prompt: THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT, + thread: "source", + }, + ], + }, + ], + }, + ]; +} + +function makeRecorder({ + outPath, + scenario, +}: { + readonly outPath: string; + readonly scenario: ReplayScenario; +}): Effect.Effect { + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + let version = "unknown"; + const records: Array> = []; + const setVersion = (nextVersion: string) => + Effect.sync(() => { + version = nextVersion; + }); + const writeRecord = (record: Record) => + Effect.sync(() => { + records.push(record); + }); + const flush = () => { + const outputRecords = sessionizeForkReplayRecords({ + scenario: scenario.name, + records, + }); + return fs.writeFileString( + outPath, + `${[ + { + type: "transcript_start", + provider: "codex", + protocol: "codex.app-server", + version, + scenario: scenario.name, + metadata: { + source: "record-codex-app-server-replay-fixture", + fileName: scenario.fileName, + description: scenario.description, + }, + }, + ...outputRecords, + ] + .map((record) => JSON.stringify(record)) + .join("\n")}\n`, + ); + }; + + return { path: outPath, setVersion, writeRecord, flush }; + }); +} + +function sessionizeForkReplayRecords(input: { + readonly scenario: ScenarioName; + readonly records: ReadonlyArray>; +}): ReadonlyArray> { + if ( + input.scenario !== "thread_merge_back_continue" && + input.scenario !== "thread_merge_back_siblings" + ) { + return input.records; + } + const initializeRequest = input.records.find( + (record) => record.type === "expect_outbound" && record.label === "initialize", + ); + const initializeResponse = input.records.find( + (record) => record.type === "emit_inbound" && record.label === "initialize", + ); + const initializedNotification = input.records.find( + (record) => record.type === "expect_outbound" && record.label === "initialized", + ); + if ( + initializeRequest === undefined || + initializeResponse === undefined || + initializedNotification === undefined + ) { + throw new Error(`Scenario ${input.scenario} is missing initialization records.`); + } + + let nextRequestId = 1; + let forkSessionOrdinal = 0; + const requestIdMap = new Map(); + const output: Array> = []; + const appendRequest = (record: Record, id: number) => { + const frame = record.frame as Record; + output.push({ ...record, frame: { ...frame, id } }); + }; + const appendResponse = (record: Record, id: number) => { + const frame = record.frame as Record; + output.push({ ...record, frame: { ...frame, id } }); + }; + + for (const record of input.records) { + const frame = record.frame; + if (record.type === "expect_outbound" && isRecord(frame) && frame.method === "thread/fork") { + forkSessionOrdinal += 1; + const initializeId = nextRequestId++; + appendRequest( + { ...initializeRequest, label: `initialize/fork:${forkSessionOrdinal}` }, + initializeId, + ); + appendResponse( + { ...initializeResponse, label: `initialize/fork:${forkSessionOrdinal}` }, + initializeId, + ); + output.push({ + ...initializedNotification, + label: `initialized/fork:${forkSessionOrdinal}`, + }); + } + + if ( + record.type === "expect_outbound" && + isRecord(frame) && + typeof frame.id === "number" && + typeof frame.method === "string" + ) { + const requestId = nextRequestId++; + requestIdMap.set(frame.id, requestId); + appendRequest(record, requestId); + continue; + } + if ( + record.type === "emit_inbound" && + isRecord(frame) && + typeof frame.id === "number" && + !("method" in frame) + ) { + const responseId = requestIdMap.get(frame.id); + if (responseId === undefined) { + throw new Error( + `Scenario ${input.scenario} has an unmatched response id ${String(frame.id)}.`, + ); + } + appendResponse(record, responseId); + continue; + } + output.push(record); + } + return output; +} + +function makeCodexLayer({ recorder }: { readonly recorder: Recorder }) { + const clientRequestMethodById = new Map(); + const serverRequestMethodById = new Map(); + const clientOptions: CodexClient.CodexAppServerClientOptions = { + logIncoming: true, + logOutgoing: true, + logger: (event) => { + if (event.stage === "raw") { + return Effect.void; + } + + const id = protocolId(event.payload); + const idKey = id === undefined ? undefined : String(id); + const method = protocolMethod(event.payload); + const messageKind = classifyJsonRpcPayload(event.payload); + let correlatedRequestMethod: string | undefined; + + if (messageKind === "request" && idKey && method) { + if (event.direction === "outgoing") { + clientRequestMethodById.set(idKey, method); + } else { + serverRequestMethodById.set(idKey, method); + } + } + + if (messageKind === "response" || messageKind === "error_response") { + if (event.direction === "incoming" && idKey) { + correlatedRequestMethod = clientRequestMethodById.get(idKey); + clientRequestMethodById.delete(idKey); + } + if (event.direction === "outgoing" && idKey) { + correlatedRequestMethod = serverRequestMethodById.get(idKey); + serverRequestMethodById.delete(idKey); + } + } + + const version = inferCodexVersion(event.payload); + const updateVersion = version ? recorder.setVersion(version) : Effect.void; + + const label = method ?? correlatedRequestMethod; + const record = + event.direction === "outgoing" + ? { + type: "expect_outbound", + ...(label ? { label } : {}), + frame: event.payload, + } + : { + type: "emit_inbound", + ...(label ? { label } : {}), + frame: event.payload, + }; + + return Effect.gen(function* () { + yield* updateVersion; + yield* recorder.writeRecord(record); + }).pipe(Effect.ignore); + }, + }; + + return Layer.effect( + CodexClient.CodexAppServerClient, + Effect.gen(function* () { + const environment = yield* HostProcessEnvironment; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const commandName = environment.T3_CODEX_BIN ?? environment.CODEX_BIN ?? "codex"; + const spawnCommand = yield* resolveSpawnCommand(commandName, ["app-server"], { + env: environment, + }); + const handle = yield* spawner.spawn( + ChildProcess.make(spawnCommand.command, spawnCommand.args, { + cwd: process.cwd(), + env: environment, + shell: spawnCommand.shell, + }), + ); + const context = yield* Layer.build(CodexClient.layerChildProcess(handle, clientOptions)); + return yield* Effect.service(CodexClient.CodexAppServerClient).pipe(Effect.provide(context)); + }), + ); +} + +function installReplayHandlers({ + client, + startTurn, + completeTurn, + startCommandExecution, + beforeApprovalResponse, +}: { + readonly client: CodexClient.CodexAppServerClient["Service"]; + readonly startTurn: (turnId: string) => Effect.Effect; + readonly completeTurn: (turnId: string) => Effect.Effect; + readonly startCommandExecution: (turnId: string) => Effect.Effect; + readonly beforeApprovalResponse: () => Effect.Effect; +}) { + return Effect.all( + [ + client.handleServerRequest("item/tool/requestUserInput", (payload) => + Effect.succeed({ + answers: Object.fromEntries( + payload.questions.map((question) => [ + question.id, + { + answers: + question.options && question.options.length > 0 + ? [question.options[0]!.label] + : ["ok"], + }, + ]), + ), + }), + ), + client.handleServerRequest("item/commandExecution/requestApproval", () => + beforeApprovalResponse().pipe(Effect.as({ decision: "accept" })), + ), + client.handleServerRequest("item/fileChange/requestApproval", () => + beforeApprovalResponse().pipe(Effect.as({ decision: "accept" })), + ), + client.handleServerRequest("item/permissions/requestApproval", (payload) => + beforeApprovalResponse().pipe( + Effect.as({ + permissions: payload.permissions, + scope: "turn" as const, + }), + ), + ), + client.handleServerRequest("mcpServer/elicitation/request", () => + Effect.succeed({ action: "accept" }), + ), + client.handleServerRequest("item/tool/call", (payload) => + Effect.succeed({ + contentItems: [ + { + text: `Replay dynamic tool handler did not execute external tool: ${payload.tool}`, + type: "inputText" as const, + }, + ], + success: false, + }), + ), + client.handleServerRequest("applyPatchApproval", () => + Effect.succeed({ decision: "approved" }), + ), + client.handleServerRequest("execCommandApproval", () => + Effect.succeed({ decision: "approved" }), + ), + client.handleUnknownServerRequest((method) => + Effect.die(new Error(`Unhandled Codex app-server request in replay recorder: ${method}`)), + ), + client.handleServerNotification("turn/started", (payload) => + startTurn(payload.turn.id).pipe(Effect.ignore), + ), + client.handleServerNotification("turn/completed", (payload) => + completeTurn(payload.turn.id).pipe(Effect.ignore), + ), + client.handleServerNotification("item/completed", (payload) => + isRecord(payload.item) && payload.item.type === "commandExecution" + ? startCommandExecution(payload.turnId).pipe(Effect.ignore) + : Effect.void, + ), + client.handleServerNotification("item/started", (payload) => + isRecord(payload.item) && payload.item.type === "commandExecution" + ? startCommandExecution(payload.turnId).pipe(Effect.ignore) + : Effect.void, + ), + ], + { discard: true }, + ); +} + +function runReplaySession({ + scenario, + run, + recorder, +}: { + readonly scenario: ReplayScenario; + readonly run: ReplayRun; + readonly recorder: Recorder; +}) { + return Effect.gen(function* () { + const startedTurns = new Map>(); + const completedTurns = new Map>(); + const startedCommandExecutions = new Map>(); + let approvalGate: Deferred.Deferred | undefined; + const getStarted = (turnId: string) => { + const existing = startedTurns.get(turnId); + if (existing) { + return Effect.succeed(existing); + } + return Deferred.make().pipe( + Effect.tap((deferred) => Effect.sync(() => startedTurns.set(turnId, deferred))), + ); + }; + const getCompletion = (turnId: string) => { + const existing = completedTurns.get(turnId); + if (existing) { + return Effect.succeed(existing); + } + return Deferred.make().pipe( + Effect.tap((deferred) => Effect.sync(() => completedTurns.set(turnId, deferred))), + ); + }; + const getCommandExecutionStarted = (turnId: string) => { + const existing = startedCommandExecutions.get(turnId); + if (existing) { + return Effect.succeed(existing); + } + return Deferred.make().pipe( + Effect.tap((deferred) => Effect.sync(() => startedCommandExecutions.set(turnId, deferred))), + ); + }; + const startTurn = (turnId: string) => + getStarted(turnId).pipe(Effect.flatMap((deferred) => Deferred.succeed(deferred, void 0))); + const completeTurn = (turnId: string) => + getCompletion(turnId).pipe(Effect.flatMap((deferred) => Deferred.succeed(deferred, void 0))); + const startCommandExecution = (turnId: string) => + getCommandExecutionStarted(turnId).pipe( + Effect.flatMap((deferred) => Deferred.succeed(deferred, void 0)), + ); + const beforeApprovalResponse = () => + approvalGate ? Deferred.await(approvalGate) : Effect.void; + + const initializeClient = Effect.gen(function* () { + const client = yield* CodexClient.CodexAppServerClient; + + yield* installReplayHandlers({ + client, + startTurn, + completeTurn, + startCommandExecution, + beforeApprovalResponse, + }); + + yield* client.request("initialize", { + clientInfo: CODEX_CLIENT_INFO, + capabilities: CODEX_CLIENT_CAPABILITIES, + }); + + yield* client.notify("initialized", undefined); + + return client; + }); + + const runTurnStep = ( + client: CodexClient.CodexAppServerClient["Service"], + threadId: string, + step: TurnReplayStep, + ) => + Effect.gen(function* () { + const turnParams: TurnStartParams = { + ...run.turnDefaults, + ...step.turnOverrides, + input: turnInput(step.prompt), + threadId, + }; + + if (step.type === "steeredTurn") { + approvalGate = yield* Deferred.make(); + } + + const turn = yield* client.request("turn/start", turnParams); + const turnId = getTurnId(turn); + const started = yield* getStarted(turnId); + yield* getCompletion(turnId); + + if (step.type === "steeredTurn") { + yield* Deferred.await(started); + yield* client.request("turn/steer", { + expectedTurnId: turnId, + input: turnInput(step.steer), + threadId, + }); + if (approvalGate) { + yield* Deferred.succeed(approvalGate, void 0); + approvalGate = undefined; + } + } + + if (step.type === "interruptedTurn") { + if (step.interruptAfterCommandExecutionStarted === true) { + const commandExecutionStarted = yield* getCommandExecutionStarted(turnId); + yield* Deferred.await(commandExecutionStarted); + } else { + yield* Effect.sleep(`${step.interruptAfterMs ?? 1_500} millis`); + } + yield* client.request("turn/interrupt", { + threadId, + turnId, + }); + } + + const completed = yield* getCompletion(turnId); + yield* Deferred.await(completed); + }); + + if (scenario.name === "provider_thread_resume") { + const firstStep = run.steps[0]; + const secondStep = run.steps[1]; + if ( + !firstStep || + !secondStep || + firstStep.type === "rollback" || + firstStep.type === "fork" || + secondStep.type === "rollback" || + secondStep.type === "fork" + ) { + throw new Error("provider_thread_resume replay recording requires two turn steps."); + } + + const firstThread = yield* Effect.gen(function* () { + const client = yield* initializeClient; + const thread = yield* client.request("thread/start", {}); + yield* runTurnStep(client, thread.thread.id, firstStep); + return thread; + }).pipe( + Effect.provide( + makeCodexLayer({ + recorder, + }), + ), + ); + + yield* recorder.writeRecord({ + type: "runtime_exit", + status: "success", + }); + + yield* Effect.gen(function* () { + const client = yield* initializeClient; + const thread = yield* client.request("thread/resume", { + threadId: firstThread.thread.id, + }); + yield* runTurnStep(client, thread.thread.id, secondStep); + }).pipe( + Effect.provide( + makeCodexLayer({ + recorder, + }), + ), + ); + return; + } + + yield* Effect.gen(function* () { + const client = yield* initializeClient; + const thread = yield* client.request("thread/start", {}); + let activeThreadId = thread.thread.id; + const threadIds = new Map([["source", thread.thread.id]]); + + for (const [stepIndex, step] of run.steps.entries()) { + if (step.type === "rollback") { + yield* client.request("thread/rollback", { + threadId: activeThreadId, + numTurns: step.numTurns, + }); + continue; + } + if (step.type === "fork") { + const sourceThreadId = + step.from === undefined ? activeThreadId : threadIds.get(step.from); + if (sourceThreadId === undefined) { + throw new Error(`Unknown replay thread alias '${step.from}'.`); + } + const forked = yield* client.request("thread/fork", { + threadId: sourceThreadId, + }); + activeThreadId = forked.thread.id; + if (step.as !== undefined) { + threadIds.set(step.as, forked.thread.id); + } + continue; + } + + const threadAlias = "thread" in step ? step.thread : undefined; + const turnThreadId = + threadAlias === undefined ? activeThreadId : threadIds.get(threadAlias); + if (turnThreadId === undefined) { + throw new Error(`Unknown replay thread alias '${threadAlias}'.`); + } + yield* runTurnStep(client, turnThreadId, step); + void stepIndex; + } + }).pipe( + Effect.provide( + makeCodexLayer({ + recorder, + }), + ), + ); + }); +} + +function runScenario({ + scenario, + outPath, +}: { + readonly scenario: ReplayScenario; + readonly outPath: string; +}) { + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const recorder = yield* makeRecorder({ outPath, scenario }); + + yield* fs.makeDirectory(path.dirname(outPath), { recursive: true }); + yield* Console.log(`Writing ${scenario.name} Codex replay events to ${recorder.path}`); + + yield* Effect.forEach(scenario.runs, (run) => runReplaySession({ scenario, run, recorder }), { + concurrency: 1, + }); + + yield* recorder.writeRecord({ + type: "runtime_exit", + status: "success", + }); + yield* recorder.flush(); + }); +} + +const program = Effect.gen(function* () { + const path = yield* Path.Path; + const requestedScenarios = parseScenarios(); + const allScenarios = scenarios(); + const outDir = readArgValue("--out-dir") ?? process.env.T3_CODEX_REPLAY_OUT_DIR; + const singleOutPath = readArgValue("--out") ?? process.env.T3_CODEX_REPLAY_OUT; + const selected = allScenarios.filter((scenario) => requestedScenarios.includes(scenario.name)); + + if (selected.length === 0) { + throw new Error("No replay scenarios selected."); + } + if (singleOutPath && selected.length !== 1) { + throw new Error("--out / T3_CODEX_REPLAY_OUT can only be used with exactly one --scenario."); + } + + yield* Effect.forEach( + selected, + (scenario) => + runScenario({ + scenario, + outPath: + singleOutPath ?? + (outDir ? path.join(outDir, scenario.fileName) : defaultTranscriptPath(scenario)), + }), + { concurrency: 1 }, + ); +}); + +program.pipe(Effect.scoped, Effect.provide(NodeServices.layer), NodeRuntime.runMain); diff --git a/apps/server/scripts/record-cursor-agent-sdk-replay-fixture.ts b/apps/server/scripts/record-cursor-agent-sdk-replay-fixture.ts new file mode 100644 index 00000000000..95a97321c8a --- /dev/null +++ b/apps/server/scripts/record-cursor-agent-sdk-replay-fixture.ts @@ -0,0 +1,222 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; + +import { recordCursorAgentSdkReplayTranscript } from "../src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts"; +import { makeCheckpointWorkspace } from "../src/orchestration-v2/testkit/ReplayFixtureWorkspace.ts"; +import { + CURSOR_MODEL_SELECTION, + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + MULTI_TURN_FIRST_PROMPT, + MULTI_TURN_SECOND_PROMPT, + PROVIDER_THREAD_RESUME_FIRST_PROMPT, + PROVIDER_THREAD_RESUME_SECOND_PROMPT, + PROPOSED_PLAN_PROMPT, + SIMPLE_PROMPT, + SUBAGENT_PROMPT, + TODO_LIST_PROMPT, + TOOL_CALL_READ_ONLY_PROMPT, + TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, + TURN_INTERRUPT_MID_TOOL_PROMPT, +} from "../src/orchestration-v2/testkit/fixtures/shared.ts"; + +const RECORDINGS = { + simple: { + prompts: [SIMPLE_PROMPT], + output: "../src/orchestration-v2/testkit/fixtures/simple/cursor_transcript.ndjson", + }, + multi_turn: { + prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT], + output: "../src/orchestration-v2/testkit/fixtures/multi_turn/cursor_transcript.ndjson", + }, + message_steering: { + prompts: [MESSAGE_STEERING_INITIAL_PROMPT, MESSAGE_STEERING_STEER_PROMPT], + output: "../src/orchestration-v2/testkit/fixtures/message_steering/cursor_transcript.ndjson", + interruptAfterRunStartPromptIndex: 0, + }, + provider_thread_resume: { + prompts: [PROVIDER_THREAD_RESUME_FIRST_PROMPT, PROVIDER_THREAD_RESUME_SECOND_PROMPT], + output: + "../src/orchestration-v2/testkit/fixtures/provider_thread_resume/cursor_transcript.ndjson", + restartBeforePromptIndex: 1, + }, + queued_turn: { + prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT], + output: "../src/orchestration-v2/testkit/fixtures/queued_turn/cursor_transcript.ndjson", + }, + proposed_plan: { + prompts: [PROPOSED_PLAN_PROMPT], + output: "../src/orchestration-v2/testkit/fixtures/proposed_plan/cursor_transcript.ndjson", + interactionMode: "plan", + }, + todo_list: { + prompts: [TODO_LIST_PROMPT], + output: "../src/orchestration-v2/testkit/fixtures/todo_list/cursor_transcript.ndjson", + }, + subagent: { + prompts: [SUBAGENT_PROMPT], + output: "../src/orchestration-v2/testkit/fixtures/subagent/cursor_transcript.ndjson", + }, + tool_call_read_only: { + prompts: [TOOL_CALL_READ_ONLY_PROMPT], + output: "../src/orchestration-v2/testkit/fixtures/tool_call_read_only/cursor_transcript.ndjson", + }, + turn_interrupt_mid_tool: { + prompts: [TURN_INTERRUPT_MID_TOOL_PROMPT], + output: + "../src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_transcript.ndjson", + interruptAfterToolStart: true, + }, +} as const; + +type RecordingName = keyof typeof RECORDINGS; + +const encodeUnknownJsonString = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); + +function readArgValue(name: string): string | undefined { + const index = process.argv.indexOf(name); + return index === -1 ? undefined : process.argv[index + 1]; +} + +function encodeTranscriptNdjson( + transcript: Awaited>, +): string { + const { entries, ...metadata } = transcript; + return [ + encodeUnknownJsonString({ type: "transcript_start", ...metadata }), + ...entries.map((entry) => encodeUnknownJsonString(entry)), + "", + ].join("\n"); +} + +const runFileSystem = ( + effect: Effect.Effect, +): Promise => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))); + +async function prepareWorkspace(scenario: RecordingName): Promise<{ + readonly cwd: string; + readonly remove: boolean; +}> { + if (process.env.T3_CURSOR_REPLAY_CWD !== undefined) { + return { + cwd: process.env.T3_CURSOR_REPLAY_CWD, + remove: false, + }; + } + if (scenario === "tool_call_read_only") { + await runFileSystem( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, { + recursive: true, + force: true, + }); + yield* fs.makeDirectory(TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, { + recursive: true, + }); + }), + ); + return { + cwd: TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, + remove: true, + }; + } + return { + cwd: await makeCheckpointWorkspace(`cursor-agent-sdk-record-${scenario}`), + remove: true, + }; +} + +async function writeFixtureFiles(cwd: string): Promise { + await runFileSystem( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.writeFileString( + path.join(cwd, "package.json"), + encodeUnknownJsonString({ + name: "cursor-read-only-fixture", + private: true, + scripts: { typecheck: "tsc --noEmit" }, + }), + ); + yield* fs.writeFileString( + path.join(cwd, "tsconfig.json"), + encodeUnknownJsonString({ + compilerOptions: { + module: "ESNext", + strict: true, + target: "ES2022", + }, + }), + ); + }), + ); +} + +const scenario = (readArgValue("--scenario") ?? process.env.T3_CURSOR_REPLAY_SCENARIO) as + | RecordingName + | undefined; +if (scenario === undefined || RECORDINGS[scenario] === undefined) { + throw new Error(`Pass --scenario with one of: ${Object.keys(RECORDINGS).join(", ")}`); +} + +const apiKey = process.env.CURSOR_API_KEY?.trim(); +if (!apiKey) { + throw new Error("CURSOR_API_KEY is required to record Cursor SDK replay fixtures."); +} + +const recording = RECORDINGS[scenario]; +const workspace = await prepareWorkspace(scenario); +if (scenario === "subagent" || scenario === "tool_call_read_only" || scenario === "todo_list") { + await writeFixtureFiles(workspace.cwd); +} + +const outputPath = readArgValue("--out") ?? new URL(recording.output, import.meta.url).pathname; + +try { + const transcript = await recordCursorAgentSdkReplayTranscript({ + scenario, + prompts: recording.prompts, + modelSelection: { + ...CURSOR_MODEL_SELECTION, + model: process.env.T3_CURSOR_REPLAY_MODEL ?? CURSOR_MODEL_SELECTION.model, + }, + cwd: workspace.cwd, + apiKey, + ...("interactionMode" in recording ? { interactionMode: recording.interactionMode } : {}), + ...("interruptAfterToolStart" in recording + ? { interruptAfterToolStart: recording.interruptAfterToolStart } + : {}), + ...("interruptAfterRunStartPromptIndex" in recording + ? { interruptAfterRunStartPromptIndex: recording.interruptAfterRunStartPromptIndex } + : {}), + ...("restartBeforePromptIndex" in recording + ? { restartBeforePromptIndex: recording.restartBeforePromptIndex } + : {}), + }); + await runFileSystem( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.dirname(outputPath), { recursive: true }); + yield* fs.writeFileString(outputPath, encodeTranscriptNdjson(transcript)); + }), + ); + await Effect.runPromise( + Console.log(`Wrote ${transcript.entries.length} Cursor SDK replay entries to ${outputPath}`), + ); +} finally { + if (workspace.remove) { + await runFileSystem( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(workspace.cwd, { recursive: true, force: true }); + }), + ); + } +} diff --git a/apps/server/src/attachmentStore.test.ts b/apps/server/src/attachmentStore.test.ts index e21d9cf62cf..3de9a8081c8 100644 --- a/apps/server/src/attachmentStore.test.ts +++ b/apps/server/src/attachmentStore.test.ts @@ -7,11 +7,22 @@ import { describe, expect, it } from "vite-plus/test"; import { createAttachmentId, + createDeterministicAttachmentId, parseThreadSegmentFromAttachmentId, resolveAttachmentPathById, } from "./attachmentStore.ts"; describe("attachmentStore", () => { + it("derives stable attachment ids for idempotent message retries", () => { + const first = createDeterministicAttachmentId("thread-1", "message-1:0"); + const retry = createDeterministicAttachmentId("thread-1", "message-1:0"); + const next = createDeterministicAttachmentId("thread-1", "message-1:1"); + + expect(first).toBe(retry); + expect(next).not.toBe(first); + expect(first && parseThreadSegmentFromAttachmentId(first)).toBe("thread-1"); + }); + it("sanitizes thread ids when creating attachment ids", () => { const attachmentId = createAttachmentId("thread.folder/unsafe space"); expect(attachmentId).toBeTruthy(); diff --git a/apps/server/src/attachmentStore.ts b/apps/server/src/attachmentStore.ts index 3d5b531db21..597c75dd91b 100644 --- a/apps/server/src/attachmentStore.ts +++ b/apps/server/src/attachmentStore.ts @@ -42,6 +42,17 @@ export function createAttachmentId(threadId: string): string | null { return `${threadSegment}-${NodeCrypto.randomUUID()}`; } +export function createDeterministicAttachmentId( + threadId: string, + stableKey: string, +): string | null { + const threadSegment = toSafeThreadAttachmentSegment(threadId); + if (!threadSegment) return null; + const hash = NodeCrypto.createHash("sha256").update(stableKey).digest("hex").slice(0, 32); + const uuid = `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20)}`; + return `${threadSegment}-${uuid}`; +} + export function parseThreadSegmentFromAttachmentId(attachmentId: string): string | null { const normalizedId = normalizeAttachmentRelativePath(attachmentId); if (!normalizedId || normalizedId.includes("/") || normalizedId.includes(".")) { diff --git a/apps/server/src/bin.test.ts b/apps/server/src/bin.test.ts deleted file mode 100644 index 5c713ff2be7..00000000000 --- a/apps/server/src/bin.test.ts +++ /dev/null @@ -1,522 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off - CLI integration exercises Node HTTP and filesystem boundaries. -import * as NodeHttp from "node:http"; -import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; - -import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { EnvironmentOrchestrationHttpApi } from "@t3tools/contracts"; -import * as NetService from "@t3tools/shared/Net"; -import { assert, it } from "@effect/vitest"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as HttpRouter from "effect/unstable/http/HttpRouter"; -import * as HttpServer from "effect/unstable/http/HttpServer"; -import * as HttpApi from "effect/unstable/httpapi/HttpApi"; -import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; -import * as CliError from "effect/unstable/cli/CliError"; -import * as TestConsole from "effect/testing/TestConsole"; -import { Command } from "effect/unstable/cli"; - -import { cli, makeCli } from "./bin.ts"; -import * as ServerConfig from "./config.ts"; -import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; -import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts"; -import { orchestrationHttpApiLayer } from "./orchestration/http.ts"; -import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; -import * as RepositoryIdentityResolver from "./project/RepositoryIdentityResolver.ts"; -import { - makePersistedServerRuntimeState, - persistServerRuntimeState, -} from "./serverRuntimeState.ts"; -import * as WorkspacePaths from "./workspace/WorkspacePaths.ts"; -import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; -import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; -import { environmentAuthenticatedAuthLayer } from "./auth/http.ts"; - -const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); -class ProjectCliHttpApi extends HttpApi.make("environment").add(EnvironmentOrchestrationHttpApi) {} - -const connectCli = makeCli({ cloudEnabled: true }); -const noConnectCli = makeCli({ cloudEnabled: false }); -const runCli = (args: ReadonlyArray, command = cli) => - Command.runWith(command, { version: "0.0.0" })(args); -const runConnectCli = (args: ReadonlyArray) => runCli(args, connectCli); -const runCliWithRuntime = (args: ReadonlyArray) => - runCli(args).pipe(Effect.provide(CliRuntimeLayer)); - -const captureStdout = (effect: Effect.Effect) => - Effect.gen(function* () { - const result = yield* effect; - const output = - (yield* TestConsole.logLines).findLast((line): line is string => typeof line === "string") ?? - ""; - return { result, output }; - }).pipe(Effect.provide(Layer.mergeAll(CliRuntimeLayer, TestConsole.layer))); - -const makeCliTestServerConfig = (baseDir: string) => - Effect.gen(function* () { - const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined); - return { - logLevel: "Info", - traceMinLevel: "Info", - traceTimingEnabled: true, - traceBatchWindowMs: 200, - traceMaxBytes: 10 * 1024 * 1024, - traceMaxFiles: 10, - otlpTracesUrl: undefined, - otlpMetricsUrl: undefined, - otlpExportIntervalMs: 10_000, - otlpServiceName: "t3-server", - mode: "web", - port: 0, - host: "127.0.0.1", - cwd: process.cwd(), - baseDir, - ...derivedPaths, - staticDir: undefined, - devUrl: undefined, - noBrowser: true, - startupPresentation: "browser", - desktopBootstrapToken: undefined, - autoBootstrapProjectFromCwd: false, - logWebSocketEvents: false, - tailscaleServeEnabled: false, - tailscaleServePort: 443, - } satisfies ServerConfig.ServerConfig["Service"]; - }); - -const makeProjectPersistenceLayer = (config: ServerConfig.ServerConfig["Service"]) => - Layer.mergeAll( - OrchestrationLayerLive.pipe( - Layer.provideMerge(RepositoryIdentityResolver.layer), - Layer.provideMerge(SqlitePersistenceLayerLive), - ), - WorkspacePaths.layer, - ).pipe(Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config))); - -const readPersistedSnapshot = (baseDir: string) => - Effect.gen(function* () { - const config = yield* makeCliTestServerConfig(baseDir); - return yield* Effect.gen(function* () { - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; - return yield* projectionSnapshotQuery.getSnapshot(); - }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); - }); - -const withLiveProjectCliServer = (baseDir: string, run: () => Effect.Effect) => - Effect.gen(function* () { - const config = yield* makeCliTestServerConfig(baseDir); - const routesLayer = HttpApiBuilder.layer(ProjectCliHttpApi).pipe( - Layer.provide(orchestrationHttpApiLayer), - Layer.provide(environmentAuthenticatedAuthLayer), - ); - const appLayer = HttpRouter.serve(routesLayer, { - disableListenLog: true, - disableLogger: true, - }).pipe( - Layer.provideMerge( - EnvironmentAuth.layer.pipe( - Layer.provideMerge(SqlitePersistenceLayerLive), - Layer.provide(ServerSecretStore.layer), - ), - ), - Layer.provideMerge(makeProjectPersistenceLayer(config)), - Layer.provideMerge( - NodeHttpServer.layer(NodeHttp.createServer, { - host: "127.0.0.1", - port: 0, - }), - ), - Layer.provideMerge(NodeServices.layer), - Layer.provide(ServerConfig.layer(config)), - ); - - return yield* Effect.scoped( - Effect.gen(function* () { - const server = yield* HttpServer.HttpServer; - const address = server.address; - if (typeof address === "string" || !("port" in address)) { - assert.fail(`Expected TCP address, got ${address}`); - } - yield* persistServerRuntimeState({ - path: config.serverRuntimeStatePath, - state: yield* makePersistedServerRuntimeState({ - config, - port: address.port, - }), - }); - return yield* run(); - }).pipe(Effect.provide(Layer.mergeAll(appLayer, NodeServices.layer))), - ); - }); - -it.layer(NodeServices.layer)("bin cli parsing", (it) => { - it.effect("accepts the built-in lowercase log-level flag values", () => - runCliWithRuntime(["--log-level", "debug", "--version"]), - ); - - it.effect("accepts canonical --no- boolean negation", () => - runCliWithRuntime(["--no-log-websocket-events", "--version"]), - ); - - it.effect("rejects invalid log-level casing before launching the server", () => - Effect.gen(function* () { - const error = yield* runCliWithRuntime(["--log-level", "Debug"]).pipe(Effect.flip); - - if (!CliError.isCliError(error)) { - assert.fail(`Expected CliError, got ${String(error)}`); - } - if (error._tag !== "InvalidValue") { - assert.fail(`Expected InvalidValue, got ${error._tag}`); - } - assert.equal(error.option, "log-level"); - assert.equal(error.value, "Debug"); - }), - ); - - it.effect("rejects connect commands when public configuration is missing", () => - Effect.gen(function* () { - const error = yield* runCli(["connect", "status"], noConnectCli).pipe(Effect.flip); - - if (!CliError.isCliError(error)) { - assert.fail(`Expected CliError, got ${String(error)}`); - } - if (error._tag !== "ShowHelp") { - assert.fail(`Expected ShowHelp, got ${error._tag}`); - } - assert.deepEqual(error.commandPath, ["t3", "connect"]); - assert.include(error.errors[0]?.message ?? "", "missing T3 Connect public configuration"); - - const output = (yield* TestConsole.errorLines).join("\n"); - assert.include(output, "ERROR"); - assert.include(output, "missing T3 Connect public configuration"); - }).pipe(Effect.provide(Layer.mergeAll(CliRuntimeLayer, TestConsole.layer))), - ); - - it.effect("reports fresh headless connect state without requiring local configuration", () => - Effect.gen(function* () { - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-status-test-"), - ); - const { output } = yield* captureStdout( - runConnectCli(["connect", "status", "--base-dir", baseDir, "--json"]), - ); - // @effect-diagnostics-next-line preferSchemaOverJson:off - CLI JSON output is decoded as a presentation DTO. - const status = JSON.parse(output) as { - readonly desired: boolean; - readonly authenticated: boolean; - readonly linked: boolean; - readonly cloudUserId: string | null; - readonly relayUrl: string | null; - }; - - assert.equal(status.desired, false); - assert.equal(status.authenticated, false); - assert.equal(status.linked, false); - assert.equal(status.cloudUserId, null); - assert.equal(status.relayUrl, null); - }), - ); - - it.effect("reports actionable human-readable headless connect state", () => - Effect.gen(function* () { - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-status-human-test-"), - ); - const { output } = yield* captureStdout( - runConnectCli(["connect", "status", "--base-dir", baseDir]), - ); - - assert.include(output, "T3 Connect\n Exposure: disabled"); - assert.include(output, " Authorization: missing"); - assert.include(output, " Environment link: not provisioned"); - assert.include(output, "Next: Run `t3 connect link` to authorize and enable T3 Connect."); - }), - ); - - it.effect("logs in to headless connect without enabling access", () => - Effect.gen(function* () { - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-login-test-"), - ); - const { secretsDir } = yield* ServerConfig.deriveServerPaths(baseDir, undefined); - NodeFS.mkdirSync(secretsDir, { recursive: true }); - NodeFS.writeFileSync( - NodePath.join(secretsDir, "cloud-cli-oauth-token.bin"), - // @effect-diagnostics-next-line preferSchemaOverJson:off - Test fixture matches the persisted CLI token representation. - JSON.stringify({ - accessToken: "access-token", - refreshToken: "refresh-token", - expiresAtEpochMs: Number.MAX_SAFE_INTEGER, - }), - ); - - const login = yield* captureStdout( - runConnectCli(["connect", "login", "--base-dir", baseDir]), - ); - const status = yield* captureStdout( - runConnectCli(["connect", "status", "--base-dir", baseDir, "--json"]), - ); - // @effect-diagnostics-next-line preferSchemaOverJson:off - CLI JSON output is decoded as a presentation DTO. - const decoded = JSON.parse(status.output) as { - readonly desired: boolean; - readonly authenticated: boolean; - }; - - assert.equal(login.output, "Signed in to T3 Connect."); - assert.isFalse(decoded.desired); - assert.isTrue(decoded.authenticated); - }), - ); - - it.effect("disables headless connect without a running server", () => - Effect.gen(function* () { - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-unlink-test-"), - ); - const { output } = yield* captureStdout( - runConnectCli(["connect", "unlink", "--base-dir", baseDir]), - ); - - assert.equal(output, "T3 Connect is disabled locally."); - }), - ); - - it.effect("logs out of headless connect and removes the stored CLI authorization", () => - Effect.gen(function* () { - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-logout-test-"), - ); - const { secretsDir } = yield* ServerConfig.deriveServerPaths(baseDir, undefined); - const tokenPath = NodePath.join(secretsDir, "cloud-cli-oauth-token.bin"); - NodeFS.mkdirSync(secretsDir, { recursive: true }); - NodeFS.writeFileSync(tokenPath, "invalid persisted token"); - - const { output } = yield* captureStdout( - runConnectCli(["connect", "logout", "--base-dir", baseDir]), - ); - - assert.equal(output, "Signed out of T3 Connect locally."); - assert.isFalse(NodeFS.existsSync(tokenPath)); - }), - ); - - it.effect("executes auth pairing subcommands and redacts secrets from list output", () => - Effect.gen(function* () { - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-auth-pairing-test-"), - ); - - const createdOutput = yield* captureStdout( - runCli(["auth", "pairing", "create", "--base-dir", baseDir, "--json"]), - ); - // @effect-diagnostics-next-line preferSchemaOverJson:off - const created = JSON.parse(createdOutput.output) as { - readonly id: string; - readonly credential: string; - }; - const listedOutput = yield* captureStdout( - runCli(["auth", "pairing", "list", "--base-dir", baseDir, "--json"]), - ); - // @effect-diagnostics-next-line preferSchemaOverJson:off - const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{ - readonly id: string; - readonly credential?: string; - }>; - - assert.equal(typeof created.id, "string"); - assert.equal(typeof created.credential, "string"); - assert.equal(created.credential.length > 0, true); - assert.equal(listed.length, 1); - assert.equal(listed[0]?.id, created.id); - assert.equal("credential" in (listed[0] ?? {}), false); - }), - ); - - it.effect("executes auth session subcommands and redacts secrets from list output", () => - Effect.gen(function* () { - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-auth-session-test-"), - ); - - const issuedOutput = yield* captureStdout( - runCli(["auth", "session", "issue", "--base-dir", baseDir, "--json"]), - ); - // @effect-diagnostics-next-line preferSchemaOverJson:off - const issued = JSON.parse(issuedOutput.output) as { - readonly sessionId: string; - readonly token: string; - readonly scopes: ReadonlyArray; - }; - const listedOutput = yield* captureStdout( - runCli(["auth", "session", "list", "--base-dir", baseDir, "--json"]), - ); - // @effect-diagnostics-next-line preferSchemaOverJson:off - const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{ - readonly sessionId: string; - readonly token?: string; - readonly scopes: ReadonlyArray; - }>; - - assert.equal(typeof issued.sessionId, "string"); - assert.equal(typeof issued.token, "string"); - assert.deepEqual(issued.scopes, [ - "orchestration:read", - "orchestration:operate", - "terminal:operate", - "review:write", - "relay:read", - "access:read", - "access:write", - "relay:write", - ]); - assert.equal(listed.length, 1); - assert.equal(listed[0]?.sessionId, issued.sessionId); - assert.deepEqual(listed[0]?.scopes, [ - "orchestration:read", - "orchestration:operate", - "terminal:operate", - "review:write", - "relay:read", - "access:read", - "access:write", - "relay:write", - ]); - assert.equal("token" in (listed[0] ?? {}), false); - }), - ); - - it.effect("rejects invalid ttl values before running auth commands", () => - Effect.gen(function* () { - const error = yield* runCliWithRuntime(["auth", "pairing", "create", "--ttl", "soon"]).pipe( - Effect.flip, - ); - - if (!CliError.isCliError(error)) { - assert.fail(`Expected CliError, got ${String(error)}`); - } - if (error._tag !== "ShowHelp") { - assert.fail(`Expected ShowHelp, got ${error._tag}`); - } - assert.deepEqual(error.commandPath, ["t3", "auth", "pairing", "create"]); - const ttlError = error.errors[0] as CliError.CliError | undefined; - if (!ttlError || ttlError._tag !== "InvalidValue") { - assert.fail(`Expected InvalidValue, got ${String(ttlError?._tag)}`); - } - assert.equal(ttlError.option, "ttl"); - assert.equal(ttlError.value, "soon"); - assert.isTrue(ttlError.message.includes("Invalid duration")); - assert.isTrue(ttlError.message.includes("5m, 1h, 30d, or 15 minutes")); - }), - ); - - it.effect("adds, renames, and removes projects offline through the orchestration engine", () => - Effect.gen(function* () { - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-projects-offline-test-"), - ); - const workspaceRoot = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-projects-workspace-"), - ); - - yield* runCliWithRuntime([ - "project", - "add", - workspaceRoot, - "--title", - "Alpha", - "--base-dir", - baseDir, - ]); - const afterAdd = yield* readPersistedSnapshot(baseDir); - const addedProject = afterAdd.projects.find( - (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null, - ); - assert.isTrue(addedProject !== undefined); - assert.equal(addedProject?.title, "Alpha"); - - yield* runCliWithRuntime(["project", "rename", workspaceRoot, "Beta", "--base-dir", baseDir]); - const afterRename = yield* readPersistedSnapshot(baseDir); - const renamedProject = afterRename.projects.find( - (project) => project.id === addedProject?.id, - ); - assert.equal(renamedProject?.title, "Beta"); - assert.equal(renamedProject?.deletedAt, null); - - yield* runCliWithRuntime([ - "project", - "remove", - addedProject?.id ?? "", - "--base-dir", - baseDir, - ]); - const afterRemove = yield* readPersistedSnapshot(baseDir); - const removedProject = afterRemove.projects.find( - (project) => project.id === addedProject?.id, - ); - assert.isTrue((removedProject?.deletedAt ?? null) !== null); - }), - ); - - it.effect("routes project commands through a running server when runtime state is present", () => - Effect.gen(function* () { - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-projects-live-test-"), - ); - const workspaceRoot = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-projects-live-workspace-"), - ); - - yield* withLiveProjectCliServer(baseDir, () => - Effect.gen(function* () { - yield* runCliWithRuntime([ - "project", - "add", - workspaceRoot, - "--title", - "Live Project", - "--base-dir", - baseDir, - ]); - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; - const readModel = yield* projectionSnapshotQuery.getSnapshot(); - const addedProject = readModel.projects.find( - (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null, - ); - assert.isTrue(addedProject !== undefined); - assert.equal(addedProject?.title, "Live Project"); - }), - ); - }), - ); - - it.effect("rejects dev-url on project commands", () => - Effect.gen(function* () { - const workspaceRoot = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-cli-projects-unknown-option-workspace-"), - ); - const error = yield* runCliWithRuntime([ - "project", - "add", - workspaceRoot, - "--dev-url", - "http://127.0.0.1:5173", - ]).pipe(Effect.flip); - - if (!CliError.isCliError(error)) { - assert.fail(`Expected CliError, got ${String(error)}`); - } - if (error._tag !== "ShowHelp") { - assert.fail(`Expected ShowHelp, got ${error._tag}`); - } - assert.deepEqual(error.commandPath, ["t3", "project", "add"]); - const optionError = error.errors[0] as CliError.CliError | undefined; - if (!optionError || optionError._tag !== "UnrecognizedOption") { - assert.fail(`Expected UnrecognizedOption, got ${String(optionError?._tag)}`); - } - assert.equal(optionError.option, "--dev-url"); - }), - ); -}); diff --git a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts index 8654fa0fec1..6196b1e5a82 100644 --- a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts +++ b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts @@ -1,418 +1,72 @@ -import { CheckpointRef, ProjectId, ThreadId, TurnId } from "@t3tools/contracts"; -import { it } from "@effect/vitest"; +import { assert, it, vi } from "@effect/vitest"; +import { + CheckpointRef, + CheckpointScopeId, + RunId, + ThreadId, + type OrchestrationV2ThreadProjection, +} from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import { describe, expect } from "vite-plus/test"; -import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; -import { checkpointRefForThreadTurn } from "./Utils.ts"; +import { checkpointRefForScopeOrdinal } from "../orchestration-v2/CheckpointService.ts"; +import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts"; import * as CheckpointDiffQuery from "./CheckpointDiffQuery.ts"; import * as CheckpointStore from "./CheckpointStore.ts"; -function makeThreadCheckpointContext(input: { - readonly projectId: ProjectId; - readonly threadId: ThreadId; - readonly workspaceRoot: string; - readonly worktreePath: string | null; - readonly checkpointTurnCount: number; - readonly checkpointRef: CheckpointRef; -}): ProjectionSnapshotQuery.ProjectionThreadCheckpointContext { - return { - threadId: input.threadId, - projectId: input.projectId, - workspaceRoot: input.workspaceRoot, - worktreePath: input.worktreePath, +it.effect("computes V2 run diffs from projected checkpoint scopes", () => { + const threadId = ThreadId.make("thread:checkpoint-diff-v2"); + const firstRunId = RunId.make("run:checkpoint-diff-v2:1"); + const secondRunId = RunId.make("run:checkpoint-diff-v2:2"); + const firstScopeId = CheckpointScopeId.make("scope:checkpoint-diff-v2:1"); + const secondScopeId = CheckpointScopeId.make("scope:checkpoint-diff-v2:2"); + const secondRef = CheckpointRef.make("refs/t3/test/second"); + const projection = { + runs: [ + { id: firstRunId, ordinal: 1 }, + { id: secondRunId, ordinal: 2 }, + ], + checkpointScopes: [ + { id: firstScopeId, runId: firstRunId, kind: "root_run", cwd: "/repo" }, + { id: secondScopeId, runId: secondRunId, kind: "root_run", cwd: "/repo" }, + ], checkpoints: [ { - turnId: TurnId.make("turn-1"), - checkpointTurnCount: input.checkpointTurnCount, - checkpointRef: input.checkpointRef, + scopeId: secondScopeId, + appRunOrdinal: 2, status: "ready", - files: [], - assistantMessageId: null, - completedAt: "2026-01-01T00:00:00.000Z", + ref: secondRef, }, ], - }; -} - -describe("CheckpointDiffQuery.layer", () => { - it.effect("uses the narrow full-thread context lookup for all-turns diffs", () => - Effect.gen(function* () { - const projectId = ProjectId.make("project-full-thread"); - const threadId = ThreadId.make("thread-full-thread"); - const toCheckpointRef = checkpointRefForThreadTurn(threadId, 4); - let getThreadCheckpointContextCalls = 0; - let getFullThreadDiffContextCalls = 0; - const diffCheckpointsCalls: Array<{ - readonly fromCheckpointRef: CheckpointRef; - readonly toCheckpointRef: CheckpointRef; - readonly cwd: string; - readonly ignoreWhitespace: boolean; - }> = []; - - const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { - isGitRepository: () => Effect.succeed(true), - captureCheckpoint: () => Effect.void, - hasCheckpointRef: () => Effect.succeed(true), - restoreCheckpoint: () => Effect.succeed(true), - diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) => - Effect.sync(() => { - diffCheckpointsCalls.push({ - fromCheckpointRef, - toCheckpointRef, - cwd, - ignoreWhitespace, - }); - return "full thread diff patch"; - }), - deleteCheckpointRefs: () => Effect.void, - }; - - const layer = CheckpointDiffQuery.layer.pipe( - Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), - Layer.provideMerge( - Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => - Effect.die("CheckpointDiffQuery should not request the command read model"), - getSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), - getShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), - getArchivedShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), - getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), - getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getProjectShellById: () => Effect.succeed(Option.none()), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => - Effect.sync(() => { - getThreadCheckpointContextCalls += 1; - return Option.none(); - }), - getFullThreadDiffContext: () => - Effect.sync(() => { - getFullThreadDiffContextCalls += 1; - return Option.some({ - threadId, - projectId, - workspaceRoot: "/tmp/workspace", - worktreePath: "/tmp/worktree", - latestCheckpointTurnCount: 4, - toCheckpointRef, - }); - }), - getThreadShellById: () => Effect.succeed(Option.none()), - getThreadDetailById: () => Effect.succeed(Option.none()), - }), - ), - ); - - const result = yield* Effect.gen(function* () { - const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; - return yield* query.getFullThreadDiff({ - threadId, - toTurnCount: 4, - ignoreWhitespace: true, - }); - }).pipe(Effect.provide(layer)); - - expect(getThreadCheckpointContextCalls).toBe(0); - expect(getFullThreadDiffContextCalls).toBe(1); - expect(diffCheckpointsCalls).toEqual([ - { - cwd: "/tmp/worktree", - fromCheckpointRef: checkpointRefForThreadTurn(threadId, 0), - toCheckpointRef, - ignoreWhitespace: true, - }, - ]); - expect(result).toEqual({ - threadId, - fromTurnCount: 0, - toTurnCount: 4, - diff: "full thread diff patch", - }); - }), - ); - - it.effect("computes diffs using canonical turn-0 checkpoint refs", () => - Effect.gen(function* () { - const projectId = ProjectId.make("project-1"); - const threadId = ThreadId.make("thread-1"); - const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); - const diffCheckpointsCalls: Array<{ - readonly fromCheckpointRef: CheckpointRef; - readonly toCheckpointRef: CheckpointRef; - readonly cwd: string; - readonly ignoreWhitespace: boolean; - }> = []; - - const threadCheckpointContext = makeThreadCheckpointContext({ - projectId, - threadId, - workspaceRoot: "/tmp/workspace", - worktreePath: null, - checkpointTurnCount: 1, - checkpointRef: toCheckpointRef, - }); - - const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { - isGitRepository: () => Effect.succeed(true), - captureCheckpoint: () => Effect.void, - hasCheckpointRef: () => Effect.succeed(true), - restoreCheckpoint: () => Effect.succeed(true), - diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) => - Effect.sync(() => { - diffCheckpointsCalls.push({ - fromCheckpointRef, - toCheckpointRef, - cwd, - ignoreWhitespace, - }); - return "diff patch"; - }), - deleteCheckpointRefs: () => Effect.void, - }; - - const layer = CheckpointDiffQuery.layer.pipe( - Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), - Layer.provideMerge( - Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => - Effect.die("CheckpointDiffQuery should not request the command read model"), - getSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), - getShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), - getArchivedShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), - getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), - getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getProjectShellById: () => Effect.succeed(Option.none()), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), - getFullThreadDiffContext: () => Effect.die("unused"), - getThreadShellById: () => Effect.succeed(Option.none()), - getThreadDetailById: () => Effect.succeed(Option.none()), - }), - ), - ); - - const result = yield* Effect.gen(function* () { - const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; - return yield* query.getTurnDiff({ - threadId, - fromTurnCount: 0, - toTurnCount: 1, - ignoreWhitespace: true, - }); - }).pipe(Effect.provide(layer)); - - const expectedFromRef = checkpointRefForThreadTurn(threadId, 0); - expect(diffCheckpointsCalls).toEqual([ - { - cwd: "/tmp/workspace", - fromCheckpointRef: expectedFromRef, - toCheckpointRef, - ignoreWhitespace: true, - }, - ]); - expect(result).toEqual({ - threadId, - fromTurnCount: 0, - toTurnCount: 1, - diff: "diff patch", - }); - }), - ); - - it.effect("defaults to hide whitespace changes", () => - Effect.gen(function* () { - const projectId = ProjectId.make("project-default-whitespace"); - const threadId = ThreadId.make("thread-default-whitespace"); - const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); - const diffCheckpointsCalls: Array<{ readonly ignoreWhitespace: boolean }> = []; - - const threadCheckpointContext = makeThreadCheckpointContext({ - projectId, - threadId, - workspaceRoot: "/tmp/workspace", - worktreePath: null, - checkpointTurnCount: 1, - checkpointRef: toCheckpointRef, - }); - - const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { - isGitRepository: () => Effect.succeed(true), - captureCheckpoint: () => Effect.void, - hasCheckpointRef: () => Effect.succeed(true), - restoreCheckpoint: () => Effect.succeed(true), - diffCheckpoints: ({ ignoreWhitespace }) => - Effect.sync(() => { - diffCheckpointsCalls.push({ ignoreWhitespace }); - return "diff patch"; - }), - deleteCheckpointRefs: () => Effect.void, - }; - - const layer = CheckpointDiffQuery.layer.pipe( - Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), - Layer.provideMerge( - Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => - Effect.die("CheckpointDiffQuery should not request the command read model"), - getSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), - getShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), - getArchivedShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), - getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), - getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getProjectShellById: () => Effect.succeed(Option.none()), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), - getFullThreadDiffContext: () => Effect.die("unused"), - getThreadShellById: () => Effect.succeed(Option.none()), - getThreadDetailById: () => Effect.succeed(Option.none()), - }), - ), - ); - - yield* Effect.gen(function* () { - const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; - return yield* query.getTurnDiff({ - threadId, - fromTurnCount: 0, - toTurnCount: 1, - }); - }).pipe(Effect.provide(layer)); - - expect(diffCheckpointsCalls).toEqual([{ ignoreWhitespace: true }]); - }), + } as unknown as OrchestrationV2ThreadProjection; + const diffCheckpoints = vi.fn((_input: CheckpointStore.DiffCheckpointsInput) => + Effect.succeed("diff --git a/file b/file"), ); - - it.effect("does not preflight checkpoint refs before diffing", () => - Effect.gen(function* () { - const projectId = ProjectId.make("project-no-preflight"); - const threadId = ThreadId.make("thread-no-preflight"); - const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); - let hasCheckpointRefCallCount = 0; - - const threadCheckpointContext = makeThreadCheckpointContext({ - projectId, - threadId, - workspaceRoot: "/tmp/workspace", - worktreePath: null, - checkpointTurnCount: 1, - checkpointRef: toCheckpointRef, - }); - - const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { - isGitRepository: () => Effect.succeed(true), - captureCheckpoint: () => Effect.void, - hasCheckpointRef: () => - Effect.sync(() => { - hasCheckpointRefCallCount += 1; - return true; - }), - restoreCheckpoint: () => Effect.succeed(true), - diffCheckpoints: () => Effect.succeed("diff patch"), - deleteCheckpointRefs: () => Effect.void, - }; - - const layer = CheckpointDiffQuery.layer.pipe( - Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), - Layer.provideMerge( - Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => - Effect.die("CheckpointDiffQuery should not request the command read model"), - getSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), - getShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), - getArchivedShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), - getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), - getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getProjectShellById: () => Effect.succeed(Option.none()), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), - getFullThreadDiffContext: () => Effect.die("unused"), - getThreadShellById: () => Effect.succeed(Option.none()), - getThreadDetailById: () => Effect.succeed(Option.none()), - }), - ), - ); - - yield* Effect.gen(function* () { - const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; - return yield* query.getTurnDiff({ - threadId, - fromTurnCount: 0, - toTurnCount: 1, - ignoreWhitespace: true, - }); - }).pipe(Effect.provide(layer)); - - expect(hasCheckpointRefCallCount).toBe(0); - }), + const layer = CheckpointDiffQuery.layer.pipe( + Layer.provide( + Layer.mergeAll( + Layer.mock(ThreadManagement.ThreadManagementService)({ + getThreadProjection: () => Effect.succeed(projection), + }), + Layer.mock(CheckpointStore.CheckpointStore)({ diffCheckpoints }), + ), + ), ); - it.effect("fails when the thread is missing from the snapshot", () => - Effect.gen(function* () { - const threadId = ThreadId.make("thread-missing"); - - const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { - isGitRepository: () => Effect.succeed(true), - captureCheckpoint: () => Effect.void, - hasCheckpointRef: () => Effect.succeed(true), - restoreCheckpoint: () => Effect.succeed(true), - diffCheckpoints: () => Effect.succeed(""), - deleteCheckpointRefs: () => Effect.void, - }; - - const layer = CheckpointDiffQuery.layer.pipe( - Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), - Layer.provideMerge( - Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => - Effect.die("CheckpointDiffQuery should not request the command read model"), - getSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), - getShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), - getArchivedShellSnapshot: () => - Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), - getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), - getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getProjectShellById: () => Effect.succeed(Option.none()), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => Effect.succeed(Option.none()), - getFullThreadDiffContext: () => Effect.succeed(Option.none()), - getThreadShellById: () => Effect.succeed(Option.none()), - getThreadDetailById: () => Effect.succeed(Option.none()), - }), - ), - ); - - const error = yield* Effect.gen(function* () { - const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; - return yield* query.getTurnDiff({ - threadId, - fromTurnCount: 0, - toTurnCount: 1, - }); - }).pipe(Effect.provide(layer), Effect.flip); - - expect(error.message).toContain("Thread 'thread-missing' not found."); - }), - ); + return Effect.gen(function* () { + const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; + const result = yield* query.getFullThreadDiff({ threadId, toTurnCount: 2 }); + + assert.equal(result.diff, "diff --git a/file b/file"); + assert.deepEqual(diffCheckpoints.mock.calls[0]?.[0], { + cwd: "/repo", + fromCheckpointRef: checkpointRefForScopeOrdinal({ + scopeId: firstScopeId, + ordinalWithinScope: 0, + }), + toCheckpointRef: secondRef, + fallbackFromToHead: false, + ignoreWhitespace: true, + }); + }).pipe(Effect.provide(layer)); }); diff --git a/apps/server/src/checkpointing/CheckpointDiffQuery.ts b/apps/server/src/checkpointing/CheckpointDiffQuery.ts index d42c58dfff3..5629d12797e 100644 --- a/apps/server/src/checkpointing/CheckpointDiffQuery.ts +++ b/apps/server/src/checkpointing/CheckpointDiffQuery.ts @@ -7,7 +7,6 @@ * @module CheckpointDiffQuery */ import { - type CheckpointRef, OrchestrationGetTurnDiffResult, type OrchestrationGetFullThreadDiffInput, type OrchestrationGetFullThreadDiffResult, @@ -18,13 +17,12 @@ import { import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; -import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; +import { checkpointRefForScopeOrdinal } from "../orchestration-v2/CheckpointService.ts"; +import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts"; import { CheckpointInvariantError, CheckpointUnavailableError } from "./Errors.ts"; import type { CheckpointServiceError } from "./Errors.ts"; -import { checkpointRefForThreadTurn } from "./Utils.ts"; import * as CheckpointStore from "./CheckpointStore.ts"; /** Service tag for checkpoint diff queries. */ @@ -70,7 +68,7 @@ function buildTurnDiffResult( } export const make = Effect.gen(function* () { - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; + const threads = yield* ThreadManagement.ThreadManagementService; const checkpointStore = yield* CheckpointStore.CheckpointStore; const getTurnDiff: CheckpointDiffQuery["Service"]["getTurnDiff"] = Effect.fn("getTurnDiff")( @@ -100,18 +98,21 @@ export const make = Effect.gen(function* () { return emptyDiff; } - const threadContext = yield* projectionSnapshotQuery - .getThreadCheckpointContext(input.threadId) - .pipe(Effect.withSpan("checkpoint.turnDiff.lookupContext")); - if (Option.isNone(threadContext)) { - return yield* new CheckpointInvariantError({ - operation, - detail: `Thread '${input.threadId}' not found.`, - }); - } - - const maxTurnCount = threadContext.value.checkpoints.reduce( - (max, checkpoint) => Math.max(max, checkpoint.checkpointTurnCount), + const projection = yield* threads.getThreadProjection(input.threadId).pipe( + Effect.mapError( + (cause) => + new CheckpointInvariantError({ + operation, + detail: `Thread '${input.threadId}' not found: ${String(cause)}`, + }), + ), + Effect.withSpan("checkpoint.turnDiff.lookupProjection"), + ); + const readyCheckpoints = projection.checkpoints.filter( + (checkpoint) => checkpoint.status === "ready" && checkpoint.appRunOrdinal !== null, + ); + const maxTurnCount = readyCheckpoints.reduce( + (max, checkpoint) => Math.max(max, checkpoint.appRunOrdinal ?? 0), 0, ); if (input.toTurnCount > maxTurnCount) { @@ -122,20 +123,42 @@ export const make = Effect.gen(function* () { }); } - const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot; - if (!workspaceCwd) { + const toCheckpoint = readyCheckpoints.find( + (checkpoint) => checkpoint.appRunOrdinal === input.toTurnCount, + ); + if (toCheckpoint === undefined) { + return yield* new CheckpointUnavailableError({ + threadId: input.threadId, + turnCount: input.toTurnCount, + detail: `Checkpoint ref is unavailable for run ${input.toTurnCount}.`, + }); + } + const toScope = projection.checkpointScopes.find( + (scope) => scope.id === toCheckpoint.scopeId, + ); + if (toScope === undefined) { return yield* new CheckpointInvariantError({ operation, - detail: `Workspace path missing for thread '${input.threadId}' when computing turn diff.`, + detail: `Checkpoint scope '${toCheckpoint.scopeId}' is missing for thread '${input.threadId}'.`, }); } const fromCheckpointRef = input.fromTurnCount === 0 - ? checkpointRefForThreadTurn(input.threadId, 0) - : threadContext.value.checkpoints.find( - (checkpoint) => checkpoint.checkpointTurnCount === input.fromTurnCount, - )?.checkpointRef; + ? (() => { + const firstRun = projection.runs.find((run) => run.ordinal === 1); + const firstScope = projection.checkpointScopes.find( + (scope) => scope.runId === firstRun?.id && scope.kind === "root_run", + ); + return firstScope === undefined + ? undefined + : checkpointRefForScopeOrdinal({ + scopeId: firstScope.id, + ordinalWithinScope: 0, + }); + })() + : readyCheckpoints.find((checkpoint) => checkpoint.appRunOrdinal === input.fromTurnCount) + ?.ref; if (!fromCheckpointRef) { return yield* new CheckpointUnavailableError({ threadId: input.threadId, @@ -144,22 +167,11 @@ export const make = Effect.gen(function* () { }); } - const toCheckpointRef = threadContext.value.checkpoints.find( - (checkpoint) => checkpoint.checkpointTurnCount === input.toTurnCount, - )?.checkpointRef; - if (!toCheckpointRef) { - return yield* new CheckpointUnavailableError({ - threadId: input.threadId, - turnCount: input.toTurnCount, - detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`, - }); - } - const diff = yield* checkpointStore .diffCheckpoints({ - cwd: workspaceCwd, + cwd: toScope.cwd, fromCheckpointRef, - toCheckpointRef, + toCheckpointRef: toCheckpoint.ref, fallbackFromToHead: false, ignoreWhitespace, }) @@ -208,59 +220,12 @@ export const make = Effect.gen(function* () { return emptyDiff satisfies OrchestrationGetFullThreadDiffResult; } - const threadContext = yield* projectionSnapshotQuery - .getFullThreadDiffContext(input.threadId, input.toTurnCount) - .pipe(Effect.withSpan("checkpoint.fullThread.lookupContext")); - - if (Option.isNone(threadContext)) { - return yield* new CheckpointInvariantError({ - operation, - detail: `Thread '${input.threadId}' not found.`, - }); - } - - if (input.toTurnCount > threadContext.value.latestCheckpointTurnCount) { - return yield* new CheckpointUnavailableError({ - threadId: input.threadId, - turnCount: input.toTurnCount, - detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${threadContext.value.latestCheckpointTurnCount}.`, - }); - } - - const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot; - if (!workspaceCwd) { - return yield* new CheckpointInvariantError({ - operation, - detail: `Workspace path missing for thread '${input.threadId}' when computing full thread diff.`, - }); - } - - if (!threadContext.value.toCheckpointRef) { - return yield* new CheckpointUnavailableError({ - threadId: input.threadId, - turnCount: input.toTurnCount, - detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`, - }); - } - - const diff = yield* checkpointStore - .diffCheckpoints({ - cwd: workspaceCwd, - fromCheckpointRef: checkpointRefForThreadTurn(input.threadId, 0), - toCheckpointRef: threadContext.value.toCheckpointRef as CheckpointRef, - fallbackFromToHead: false, - ignoreWhitespace, - }) - .pipe(Effect.withSpan("checkpoint.fullThread.diffCheckpoints")); - - const turnDiff = buildTurnDiffResult( - { - threadId: input.threadId, - fromTurnCount: 0, - toTurnCount: input.toTurnCount, - }, - diff, - ); + const turnDiff = yield* getTurnDiff({ + threadId: input.threadId, + fromTurnCount: 0, + toTurnCount: input.toTurnCount, + ignoreWhitespace, + }); if (!isTurnDiffResult(turnDiff)) { return yield* new CheckpointInvariantError({ operation, diff --git a/apps/server/src/cli/project.test.ts b/apps/server/src/cli/project.test.ts index 4d7e47ce541..0d51d5f72a3 100644 --- a/apps/server/src/cli/project.test.ts +++ b/apps/server/src/cli/project.test.ts @@ -1,32 +1,95 @@ +// @effect-diagnostics nodeBuiltinImport:off - CLI integration uses temporary Node paths. +import * as NodeFS from "node:fs"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; + +import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; +import * as NetService from "@t3tools/shared/Net"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as References from "effect/References"; +import { Command } from "effect/unstable/cli"; -import { EnvironmentInternalError } from "@t3tools/contracts"; +import { cli } from "../bin.ts"; +import * as ServerConfig from "../config.ts"; +import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts"; +import { ProjectServiceLayerLive } from "../orchestration-v2/runtimeLayer.ts"; +import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts"; +import * as ProjectService from "../project/ProjectService.ts"; +import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; +import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; -import { ProjectCommandError } from "./project.ts"; +const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); +const runCli = (args: ReadonlyArray) => + Command.runWith(cli, { version: "0.0.0" })(args).pipe(Effect.provide(CliRuntimeLayer)); -it("maps declared server failures into structural project command errors", () => { - const cause = new EnvironmentInternalError({ - code: "internal_error", - reason: "orchestration_snapshot_failed", - traceId: "trace-123", +const makeConfig = (baseDir: string) => + Effect.gen(function* () { + const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined); + return { + logLevel: "Info", + traceMinLevel: "Info", + traceTimingEnabled: true, + traceBatchWindowMs: 200, + traceMaxBytes: 10 * 1024 * 1024, + traceMaxFiles: 10, + otlpTracesUrl: undefined, + otlpMetricsUrl: undefined, + otlpExportIntervalMs: 10_000, + otlpServiceName: "t3-server", + mode: "web", + port: 0, + host: "127.0.0.1", + cwd: process.cwd(), + baseDir, + ...derivedPaths, + staticDir: undefined, + devUrl: undefined, + noBrowser: true, + startupPresentation: "browser", + desktopBootstrapToken: undefined, + autoBootstrapProjectFromCwd: false, + logWebSocketEvents: false, + tailscaleServeEnabled: false, + tailscaleServePort: 443, + } satisfies ServerConfig.ServerConfig["Service"]; }); - const error = ProjectCommandError.fromLiveServerRequest(cause); +const readProjects = (baseDir: string) => + Effect.gen(function* () { + const config = yield* makeConfig(baseDir); + const layer = ProjectServiceLayerLive.pipe( + Layer.provideMerge(RepositoryIdentityResolver.layer), + Layer.provideMerge(ProjectFaviconResolver.layer), + Layer.provideMerge(WorkspacePaths.layer), + Layer.provideMerge(SqlitePersistenceLayerLive), + Layer.provideMerge(NodeServices.layer), + Layer.provide(ServerConfig.layer(config)), + Layer.provide(Layer.succeed(References.MinimumLogLevel, config.logLevel)), + ); + return yield* ProjectService.ProjectService.pipe( + Effect.flatMap((projects) => projects.snapshot), + Effect.provide(layer), + ); + }); - assert.strictEqual(error.operation, "callLiveServer"); - assert.strictEqual(error.code, "internal_error"); - assert.strictEqual(error.traceId, "trace-123"); - assert.strictEqual(error.message, "Server request failed (internal_error, trace trace-123)."); - assert.strictEqual(error.cause, cause); -}); +it.effect("adds, renames, and removes projects through the V2 project CLI domain", () => + Effect.gen(function* () { + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-v2-project-cli-")); + const workspaceRoot = NodeFS.mkdtempSync( + NodePath.join(NodeOS.tmpdir(), "t3-v2-project-workspace-"), + ); -it("preserves unexpected server failures without deriving the message from them", () => { - const cause = new Error("credential abc123 was rejected"); + yield* runCli(["project", "add", workspaceRoot, "--title", "Alpha", "--base-dir", baseDir]); + const added = (yield* readProjects(baseDir)).projects[0]; + assert.equal(added?.title, "Alpha"); + assert.equal(added?.workspaceRoot, workspaceRoot); - const error = ProjectCommandError.fromLiveServerRequest(cause); + yield* runCli(["project", "rename", workspaceRoot, "Beta", "--base-dir", baseDir]); + assert.equal((yield* readProjects(baseDir)).projects[0]?.title, "Beta"); - assert.strictEqual(error.operation, "callLiveServer"); - assert.strictEqual(error.detail, "Failed to call the running server."); - assert.strictEqual(error.message, "Failed to call the running server."); - assert.strictEqual(error.cause, cause); -}); + yield* runCli(["project", "remove", added?.id ?? "", "--base-dir", baseDir]); + assert.deepEqual((yield* readProjects(baseDir)).projects, []); + }).pipe(Effect.provide(NodeServices.layer)), +); diff --git a/apps/server/src/cli/project.ts b/apps/server/src/cli/project.ts index 16f1f0e14d7..cf5dc5da749 100644 --- a/apps/server/src/cli/project.ts +++ b/apps/server/src/cli/project.ts @@ -1,15 +1,14 @@ import { - CommandId, AuthAdministrativeScopes, + CommandId, EnvironmentHttpApi, EnvironmentHttpCommonError, - type OrchestrationReadModel, + type ProjectMutation, + type ProjectSnapshot, ProjectId, - type ClientOrchestrationCommand, } from "@t3tools/contracts"; import * as Console from "effect/Console"; import * as Crypto from "effect/Crypto"; -import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -25,11 +24,11 @@ import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; import * as ServerConfig from "../config.ts"; -import * as OrchestrationEngine from "../orchestration/Services/OrchestrationEngine.ts"; -import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; -import { OrchestrationLayerLive } from "../orchestration/runtimeLayer.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts"; +import { ProjectServiceLayerLive } from "../orchestration-v2/runtimeLayer.ts"; +import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts"; import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; +import * as ProjectService from "../project/ProjectService.ts"; import * as ServerRuntimeStartup from "../serverRuntimeStartup.ts"; import { clearPersistedServerRuntimeState, @@ -45,10 +44,7 @@ type ProjectMutationTarget = { }; type ProjectCommandExecutionMode = "live" | "offline"; -type ProjectCliDispatchCommand = Extract< - ClientOrchestrationCommand, - { type: "project.create" | "project.meta.update" | "project.delete" } ->; +type ProjectCliDispatchCommand = ProjectMutation; const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); const ProjectCommandOperation = Schema.Literals([ @@ -112,12 +108,11 @@ const projectCommandUuid = Crypto.Crypto.pipe( ), ); -const ProjectCliRuntimeLive = Layer.mergeAll( - WorkspacePaths.layer, - OrchestrationLayerLive.pipe( - Layer.provideMerge(RepositoryIdentityResolver.layer), - Layer.provideMerge(SqlitePersistenceLayerLive), - ), +const ProjectCliRuntimeLive = ProjectServiceLayerLive.pipe( + Layer.provideMerge(RepositoryIdentityResolver.layer), + Layer.provideMerge(ProjectFaviconResolver.layer.pipe(Layer.provide(WorkspacePaths.layer))), + Layer.provideMerge(WorkspacePaths.layer), + Layer.provideMerge(SqlitePersistenceLayerLive), ); const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1); @@ -170,7 +165,7 @@ const resolveProjectTitle = Effect.fn("resolveProjectTitle")(function* ( }); const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* (input: { - readonly snapshot: OrchestrationReadModel; + readonly snapshot: ProjectSnapshot; readonly identifier: string; }) { const trimmedIdentifier = input.identifier.trim(); @@ -223,7 +218,7 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( const fetchLiveOrchestrationSnapshot = (origin: string, bearerToken: string) => Effect.gen(function* () { const client = yield* makeLiveServerClient(origin); - return yield* client.orchestration.snapshot({ + return yield* client.projects.snapshot({ headers: { authorization: `Bearer ${bearerToken}` }, }); }).pipe( @@ -238,18 +233,18 @@ const dispatchLiveOrchestrationCommand = ( ) => Effect.gen(function* () { const client = yield* makeLiveServerClient(origin); - yield* client.orchestration.dispatch({ + yield* client.projects.mutate({ headers: { authorization: `Bearer ${bearerToken}` }, payload: command, - } as Parameters[0]); + } as Parameters[0]); }).pipe( withProjectCliLiveServerTimeout, Effect.mapError(ProjectCommandError.fromLiveServerRequest), ); const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () { - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; - return yield* projectionSnapshotQuery.getSnapshot(); + const projects = yield* ProjectService.ProjectService; + return yield* projects.snapshot; }); const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")( @@ -287,7 +282,7 @@ const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecu const runProjectMutation = Effect.fn("runProjectMutation")(function* ( flags: CliAuthLocationFlags, run: (input: { - readonly snapshot: OrchestrationReadModel; + readonly snapshot: ProjectSnapshot; readonly dispatch: ( command: ProjectCliDispatchCommand, ) => Effect.Effect; @@ -332,10 +327,41 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( return yield* Effect.gen(function* () { const snapshot = yield* getOfflineSnapshot(); - const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService; + const projects = yield* ProjectService.ProjectService; const output = yield* run({ snapshot, - dispatch: (command) => orchestrationEngine.dispatch(command), + dispatch: (command) => + command.type === "project.create" + ? projects + .create({ + commandId: command.commandId, + projectId: command.projectId, + title: command.title, + workspaceRoot: command.workspaceRoot, + ...(command.defaultModelSelection === undefined + ? {} + : { defaultModelSelection: command.defaultModelSelection }), + ...(command.scripts === undefined ? {} : { scripts: command.scripts }), + }) + .pipe(Effect.asVoid) + : command.type === "project.update" + ? projects + .update({ + commandId: command.commandId, + projectId: command.projectId, + ...(command.title === undefined ? {} : { title: command.title }), + ...(command.workspaceRoot === undefined + ? {} + : { workspaceRoot: command.workspaceRoot }), + ...(command.defaultModelSelection === undefined + ? {} + : { defaultModelSelection: command.defaultModelSelection }), + ...(command.scripts === undefined ? {} : { scripts: command.scripts }), + }) + .pipe(Effect.asVoid) + : projects + .delete({ commandId: command.commandId, projectId: command.projectId }) + .pipe(Effect.asVoid), mode: "offline", }); yield* Console.log(output); @@ -366,7 +392,7 @@ const projectAddCommand = Command.make("add", { snapshot, dispatch, }: { - readonly snapshot: OrchestrationReadModel; + readonly snapshot: ProjectSnapshot; readonly dispatch: ( command: ProjectCliDispatchCommand, ) => Effect.Effect; @@ -391,7 +417,6 @@ const projectAddCommand = Command.make("add", { title, workspaceRoot, defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), - createdAt: DateTime.formatIso(yield* DateTime.now), }); return `Added project ${projectId} (${title}) at ${workspaceRoot}.`; }), @@ -413,7 +438,7 @@ const projectRemoveCommand = Command.make("remove", { snapshot, dispatch, }: { - readonly snapshot: OrchestrationReadModel; + readonly snapshot: ProjectSnapshot; readonly dispatch: ( command: ProjectCliDispatchCommand, ) => Effect.Effect; @@ -448,7 +473,7 @@ const projectRenameCommand = Command.make("rename", { snapshot, dispatch, }: { - readonly snapshot: OrchestrationReadModel; + readonly snapshot: ProjectSnapshot; readonly dispatch: ( command: ProjectCliDispatchCommand, ) => Effect.Effect; @@ -463,7 +488,7 @@ const projectRenameCommand = Command.make("rename", { } yield* dispatch({ - type: "project.meta.update", + type: "project.update", commandId: CommandId.make(yield* projectCommandUuid), projectId: project.id, title: nextTitle, diff --git a/apps/server/src/mcp/McpHttpServer.test.ts b/apps/server/src/mcp/McpHttpServer.test.ts index f550396c660..74ac2931434 100644 --- a/apps/server/src/mcp/McpHttpServer.test.ts +++ b/apps/server/src/mcp/McpHttpServer.test.ts @@ -21,7 +21,6 @@ const invocation = { providerInstanceId: ProviderInstanceId.make("codex"), capabilities: new Set(["preview"] as const), issuedAt: 1, - expiresAt: Number.MAX_SAFE_INTEGER, }; const client = McpSchema.McpServerClient.of({ clientId: 1, diff --git a/apps/server/src/mcp/McpHttpServer.ts b/apps/server/src/mcp/McpHttpServer.ts index e95662a30f8..9e04ec5f987 100644 --- a/apps/server/src/mcp/McpHttpServer.ts +++ b/apps/server/src/mcp/McpHttpServer.ts @@ -11,8 +11,11 @@ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstab import packageJson from "../../package.json" with { type: "json" }; import * as McpInvocationContext from "./McpInvocationContext.ts"; +import * as OrchestratorMcpService from "./OrchestratorMcpService.ts"; import * as McpSessionRegistry from "./McpSessionRegistry.ts"; import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; +import { OrchestratorToolkitHandlersLive } from "./toolkits/orchestrator/handlers.ts"; +import { OrchestratorToolkit } from "./toolkits/orchestrator/tools.ts"; import { PreviewSnapshotToolkitHandlersLive, PreviewStandardToolkitHandlersLive, @@ -208,13 +211,18 @@ export const PreviewToolkitRegistrationLive = Layer.mergeAll( PreviewSnapshotRegistrationLive, ); +export const OrchestratorToolkitRegistrationLive = McpServer.toolkit(OrchestratorToolkit).pipe( + Layer.provide(OrchestratorToolkitHandlersLive), + Layer.provide(OrchestratorMcpService.layer), +); + const McpTransportLive = McpServer.layerHttp({ name: "T3 Code", version: packageJson.version, path: "/mcp", }).pipe(Layer.provide(McpAuthMiddlewareLive)); -export const layer = PreviewToolkitRegistrationLive.pipe( - Layer.provideMerge(McpTransportLive), - Layer.provide(PreviewAutomationBroker.layer), -); +export const layer = Layer.mergeAll( + PreviewToolkitRegistrationLive, + OrchestratorToolkitRegistrationLive, +).pipe(Layer.provideMerge(McpTransportLive), Layer.provide(PreviewAutomationBroker.layer)); diff --git a/apps/server/src/mcp/McpInvocationContext.ts b/apps/server/src/mcp/McpInvocationContext.ts index 0d3f84df42c..b2de56b4fcb 100644 --- a/apps/server/src/mcp/McpInvocationContext.ts +++ b/apps/server/src/mcp/McpInvocationContext.ts @@ -3,7 +3,7 @@ import { PreviewAutomationUnavailableError } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; -export type McpCapability = "preview"; +export type McpCapability = "preview" | "orchestration"; export interface McpInvocationScope { readonly environmentId: EnvironmentId; @@ -12,7 +12,6 @@ export interface McpInvocationScope { readonly providerInstanceId: ProviderInstanceId; readonly capabilities: ReadonlySet; readonly issuedAt: number; - readonly expiresAt: number; } export class McpInvocationContext extends Context.Service< diff --git a/apps/server/src/mcp/McpSessionRegistry.test.ts b/apps/server/src/mcp/McpSessionRegistry.test.ts index a91d98febd8..4fb273889df 100644 --- a/apps/server/src/mcp/McpSessionRegistry.test.ts +++ b/apps/server/src/mcp/McpSessionRegistry.test.ts @@ -21,11 +21,7 @@ const fakeEnvironment = ServerEnvironment.ServerEnvironment.of({ const makeRegistry = (now: () => number, httpServer = fakeHttpServer) => McpSessionRegistry.__testing - .make({ - now, - idleTimeoutMs: 100, - maximumLifetimeMs: 1_000, - }) + .make({ now }) .pipe( Effect.provideService(HttpServer.HttpServer, httpServer), Effect.provideService(ServerEnvironment.ServerEnvironment, fakeEnvironment), @@ -47,6 +43,7 @@ it.effect("stores only a token hash, resolves the bearer token, and revokes by t const resolved = yield* registry.resolve(token); expect(resolved?.threadId).toBe(threadId); + expect(resolved?.capabilities).toEqual(new Set(["preview", "orchestration"])); yield* registry.revokeThread(threadId); expect(yield* registry.resolve(token)).toBeUndefined(); @@ -75,7 +72,7 @@ it.effect("builds MCP endpoints from the bound server host", () => }), ); -it.effect("expires credentials after inactivity", () => +it.effect("keeps credentials valid until they are explicitly revoked", () => Effect.gen(function* () { let timestamp = 1_000; const registry = yield* makeRegistry(() => timestamp); @@ -84,7 +81,12 @@ it.effect("expires credentials after inactivity", () => providerInstanceId: ProviderInstanceId.make("claude"), }); const token = issued.config.authorizationHeader.replace(/^Bearer\s+/, ""); - timestamp += 101; + const resolved = yield* registry.resolve(token); + expect(resolved?.providerSessionId).toBe(issued.config.providerSessionId); + timestamp += 365 * 24 * 60 * 60 * 1_000; + expect(yield* registry.resolve(token)).toEqual(resolved); + + yield* registry.revokeProviderSession(issued.config.providerSessionId); expect(yield* registry.resolve(token)).toBeUndefined(); }), ); diff --git a/apps/server/src/mcp/McpSessionRegistry.testkit.ts b/apps/server/src/mcp/McpSessionRegistry.testkit.ts new file mode 100644 index 00000000000..71b0913274b --- /dev/null +++ b/apps/server/src/mcp/McpSessionRegistry.testkit.ts @@ -0,0 +1,26 @@ +import { EnvironmentId } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import { McpSessionRegistry } from "./McpSessionRegistry.ts"; + +export const layer = Layer.succeed( + McpSessionRegistry, + McpSessionRegistry.of({ + issue: ({ threadId, providerInstanceId }) => + Effect.succeed({ + config: { + environmentId: EnvironmentId.make("environment:mcp-test"), + threadId, + providerSessionId: `mcp-test:${threadId}`, + providerInstanceId, + endpoint: "http://127.0.0.1/mcp", + authorizationHeader: `Bearer mcp-test:${threadId}`, + }, + }), + resolve: () => Effect.succeed(undefined), + revokeProviderSession: () => Effect.void, + revokeThread: () => Effect.void, + revokeAll: Effect.succeed(undefined), + }), +); diff --git a/apps/server/src/mcp/McpSessionRegistry.ts b/apps/server/src/mcp/McpSessionRegistry.ts index 67c4f2f0ff0..c79685f79c9 100644 --- a/apps/server/src/mcp/McpSessionRegistry.ts +++ b/apps/server/src/mcp/McpSessionRegistry.ts @@ -18,10 +18,10 @@ export interface McpCredentialRequest { export interface McpIssuedCredential { readonly config: McpProviderSession.McpProviderSessionConfig; - readonly expiresAt: number; } export interface McpSessionRegistryShape { + /** Credentials have no time-based expiry and must be explicitly revoked by their owner. */ readonly issue: (request: McpCredentialRequest) => Effect.Effect; readonly resolve: ( rawToken: string, @@ -37,9 +37,7 @@ export class McpSessionRegistry extends Context.Service< >()("t3/mcp/McpSessionRegistry") {} interface CredentialRecord { - readonly tokenHash: string; readonly scope: McpInvocationContext.McpInvocationScope; - readonly lastUsedAt: number; } interface RegistryState { @@ -47,14 +45,9 @@ interface RegistryState { } export interface McpSessionRegistryOptions { - readonly idleTimeoutMs?: number; - readonly maximumLifetimeMs?: number; readonly now?: () => number; } -const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1_000; -const DEFAULT_MAXIMUM_LIFETIME_MS = 8 * 60 * 60 * 1_000; - const bytesToHex = (bytes: Uint8Array): string => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); @@ -80,8 +73,6 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* ( const httpServer = yield* HttpServer.HttpServer; const state = yield* SynchronizedRef.make({ records: new Map() }); const currentTimeMillis = options.now ? Effect.sync(options.now) : Clock.currentTimeMillis; - const idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS; - const maximumLifetimeMs = options.maximumLifetimeMs ?? DEFAULT_MAXIMUM_LIFETIME_MS; const endpoint = httpServer.address._tag === "TcpAddress" ? `http://${getHttpMcpEndpointHost(httpServer.address.hostname)}:${httpServer.address.port}/mcp` @@ -92,35 +83,23 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* ( .digest("SHA-256", new TextEncoder().encode(token)) .pipe(Effect.map(bytesToHex), Effect.orDie); - const pruneExpired = (records: ReadonlyMap, timestamp: number) => { - const next = new Map( - Array.from(records).filter( - ([, record]) => - timestamp <= record.scope.expiresAt && timestamp - record.lastUsedAt <= idleTimeoutMs, - ), - ); - return next.size === records.size ? records : next; - }; - const issue: McpSessionRegistryShape["issue"] = Effect.fn("McpSessionRegistry.issue")( function* (request) { const issuedAt = yield* currentTimeMillis; const providerSessionId = yield* crypto.randomUUIDv4.pipe(Effect.orDie); const rawToken = yield* crypto.randomBytes(32).pipe(Effect.map(tokenFromBytes), Effect.orDie); const tokenHash = yield* hashToken(rawToken); - const expiresAt = issuedAt + maximumLifetimeMs; const scope: McpInvocationContext.McpInvocationScope = { environmentId, threadId: ThreadId.make(request.threadId), providerSessionId, providerInstanceId: ProviderInstanceId.make(request.providerInstanceId), - capabilities: new Set(["preview"]), + capabilities: new Set(["preview", "orchestration"]), issuedAt, - expiresAt, }; yield* SynchronizedRef.update(state, ({ records }) => { - const next = new Map(pruneExpired(records, issuedAt)); - next.set(tokenHash, { tokenHash, scope, lastUsedAt: issuedAt }); + const next = new Map(records); + next.set(tokenHash, { scope }); return { records: next }; }); return { @@ -132,7 +111,6 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* ( endpoint, authorizationHeader: `Bearer ${rawToken}`, }, - expiresAt, }; }, ); @@ -141,15 +119,8 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* ( function* (rawToken) { if (rawToken.length === 0) return undefined; const tokenHash = yield* hashToken(rawToken); - const timestamp = yield* currentTimeMillis; - return yield* SynchronizedRef.modify(state, ({ records }) => { - const current = pruneExpired(records, timestamp); - const record = current.get(tokenHash); - if (!record) return [undefined, { records: current }] as const; - const next = new Map(current); - next.set(tokenHash, { ...record, lastUsedAt: timestamp }); - return [record.scope, { records: next }] as const; - }); + const record = (yield* SynchronizedRef.get(state)).records.get(tokenHash); + return record?.scope; }, ); diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts new file mode 100644 index 00000000000..9001cd78da0 --- /dev/null +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -0,0 +1,1154 @@ +import { + CommandId, + isProviderAvailable, + MessageId, + type ModelSelection, + NodeId, + type OrchestrationV2Run, + type OrchestrationV2ThreadProjection, + type OrchestrationV2ThreadShell, + type OrchestrationV2TurnItem, + OrchestratorMcpFailure, + type OrchestratorMcpCapabilitiesResult, + type OrchestratorMcpCreateThreadsInput, + type OrchestratorMcpCreateThreadsResult, + type OrchestratorMcpCreatedThread, + type OrchestratorMcpDelegateTaskInput, + type OrchestratorMcpDelegateTaskResult, + type OrchestratorMcpInteractionMode, + type OrchestratorMcpRuntimeMode, + type OrchestratorMcpTarget, + type OrchestratorMcpTaskCancelInput, + type OrchestratorMcpTaskCancelResult, + type OrchestratorMcpThreadDetail, + type OrchestratorMcpThreadInterruptInput, + type OrchestratorMcpThreadInterruptResult, + type OrchestratorMcpThreadListInput, + type OrchestratorMcpThreadListItem, + type OrchestratorMcpThreadListResult, + type OrchestratorMcpThreadReadInput, + type OrchestratorMcpThreadReadResult, + type OrchestratorMcpThreadRun, + type OrchestratorMcpThreadSendInput, + type OrchestratorMcpThreadSendResult, + type OrchestratorMcpThreadTimelineItem, + type OrchestratorMcpThreadWaitInput, + type OrchestratorMcpThreadWaitResult, + type ProviderInteractionMode, + ProviderInstanceId, + type RuntimeMode, + type ServerProvider, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +import { isBuiltInProviderAdapterDriverV2 } from "../orchestration-v2/builtInProviderAdapterDrivers.ts"; +import { subagentResultForRun } from "../orchestration-v2/SubagentProjection.ts"; +import { + isActiveRun, + latestActiveRun, + latestRun, + ThreadManagementError, + ThreadManagementService, +} from "../orchestration-v2/ThreadManagementService.ts"; +import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts"; +import type { McpInvocationScope } from "./McpInvocationContext.ts"; + +const DEFAULT_WAIT_TIMEOUT_MS = 10 * 60 * 1_000; +const MAX_WAIT_TIMEOUT_MS = 60 * 60 * 1_000; +const TASK_POLL_INTERVAL_MS = 50; +const DEFAULT_THREAD_LIST_LIMIT = 50; +const DEFAULT_THREAD_READ_LIMIT = 50; +const DEFAULT_THREAD_RUN_LIMIT = 10; +const DEFAULT_THREAD_ITEM_MAX_CHARS = 20_000; + +interface ResolvedTarget { + readonly modelSelection: ModelSelection; +} + +type TerminalTaskStatus = Extract< + OrchestratorMcpDelegateTaskResult["status"], + "completed" | "failed" | "cancelled" | "interrupted" +>; + +export interface OrchestratorMcpServiceShape { + readonly capabilities: ( + scope: McpInvocationScope, + ) => Effect.Effect; + readonly delegateTask: ( + scope: McpInvocationScope, + input: OrchestratorMcpDelegateTaskInput, + ) => Effect.Effect; + readonly taskStatus: ( + scope: McpInvocationScope, + taskId: NodeId, + ) => Effect.Effect; + readonly cancelTask: ( + scope: McpInvocationScope, + input: OrchestratorMcpTaskCancelInput, + ) => Effect.Effect; + readonly createThreads: ( + scope: McpInvocationScope, + input: OrchestratorMcpCreateThreadsInput, + ) => Effect.Effect; + readonly listThreads: ( + scope: McpInvocationScope, + input: OrchestratorMcpThreadListInput, + ) => Effect.Effect; + readonly readThread: ( + scope: McpInvocationScope, + input: OrchestratorMcpThreadReadInput, + ) => Effect.Effect; + readonly sendToThread: ( + scope: McpInvocationScope, + input: OrchestratorMcpThreadSendInput, + ) => Effect.Effect; + readonly waitForThread: ( + scope: McpInvocationScope, + input: OrchestratorMcpThreadWaitInput, + ) => Effect.Effect; + readonly interruptThread: ( + scope: McpInvocationScope, + input: OrchestratorMcpThreadInterruptInput, + ) => Effect.Effect; +} + +export class OrchestratorMcpService extends Context.Service< + OrchestratorMcpService, + OrchestratorMcpServiceShape +>()("t3/mcp/OrchestratorMcpService") {} + +const isThreadManagementError = Schema.is(ThreadManagementError); + +function failure(code: OrchestratorMcpFailure["code"], message: string): OrchestratorMcpFailure { + return new OrchestratorMcpFailure({ code, message }); +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function providerConstraints( + provider: ServerProvider | undefined, + supportsOrchestrationV2: boolean, +): ReadonlyArray { + const constraints: Array = []; + if (!supportsOrchestrationV2) { + constraints.push("No V2 provider adapter is registered."); + } + if (provider === undefined) return constraints; + if (!provider.enabled) constraints.push("Provider instance is disabled."); + if (!provider.installed) constraints.push("Provider executable is not installed."); + if (!isProviderAvailable(provider)) { + constraints.push(provider.unavailableReason ?? "Provider driver is unavailable."); + } + if (provider.status === "error" || provider.status === "disabled") { + constraints.push(provider.message ?? `Provider status is ${provider.status}.`); + } + if (provider.auth.status === "unauthenticated") { + constraints.push("Provider is not authenticated."); + } + return constraints; +} + +function taskStatusForRun( + run: OrchestrationV2Run | undefined, +): OrchestratorMcpDelegateTaskResult["status"] { + switch (run?.status) { + case "queued": + return "queued"; + case "waiting": + return "waiting"; + case "completed": + return "completed"; + case "failed": + return "failed"; + case "cancelled": + case "rolled_back": + return "cancelled"; + case "interrupted": + return "interrupted"; + case "starting": + case "running": + case undefined: + return "running"; + } +} + +function isTerminalTaskStatus( + status: OrchestratorMcpDelegateTaskResult["status"], +): status is TerminalTaskStatus { + return ( + status === "completed" || + status === "failed" || + status === "cancelled" || + status === "interrupted" + ); +} + +function runtimeModeRank(mode: RuntimeMode): number { + switch (mode) { + case "approval-required": + return 0; + case "auto-accept-edits": + return 1; + case "full-access": + return 2; + } +} + +function interactionModeRank(mode: ProviderInteractionMode): number { + return mode === "plan" ? 0 : 1; +} + +function resolveRuntimeMode( + parentMode: RuntimeMode, + requested: OrchestratorMcpRuntimeMode | undefined, +): Effect.Effect { + const resolved = requested === undefined || requested === "inherit" ? parentMode : requested; + return runtimeModeRank(resolved) > runtimeModeRank(parentMode) + ? Effect.fail( + failure( + "runtime_mode_escalation_denied", + `Child runtime mode ${resolved} is broader than parent mode ${parentMode}.`, + ), + ) + : Effect.succeed(resolved); +} + +function resolveInteractionMode( + parentMode: ProviderInteractionMode, + requested: OrchestratorMcpInteractionMode | undefined, +): Effect.Effect { + const resolved = requested === undefined || requested === "inherit" ? parentMode : requested; + return interactionModeRank(resolved) > interactionModeRank(parentMode) + ? Effect.fail( + failure( + "interaction_mode_escalation_denied", + `Child interaction mode ${resolved} is broader than parent mode ${parentMode}.`, + ), + ) + : Effect.succeed(resolved); +} + +function stablePart(value: string): string { + return encodeURIComponent(value); +} + +function stableCommandId(input: { + readonly scope: McpInvocationScope; + readonly requestKey: string; + readonly operation: string; + readonly index?: number; +}): CommandId { + return CommandId.make( + [ + "command", + "mcp", + stablePart(input.scope.providerSessionId), + stablePart(input.operation), + stablePart(input.requestKey), + ...(input.index === undefined ? [] : [String(input.index)]), + ].join(":"), + ); +} + +function stableThreadId(input: { + readonly scope: McpInvocationScope; + readonly requestKey: string; + readonly index: number; +}): ThreadId { + return ThreadId.make( + [ + "thread", + "mcp", + stablePart(input.scope.providerSessionId), + stablePart(input.requestKey), + String(input.index), + ].join(":"), + ); +} + +function stableMessageId(input: { + readonly scope: McpInvocationScope; + readonly requestKey: string; + readonly index: number; +}): MessageId { + return MessageId.make( + [ + "message", + "mcp", + stablePart(input.scope.providerSessionId), + stablePart(input.requestKey), + String(input.index), + ].join(":"), + ); +} + +function stableOperationMessageId(input: { + readonly scope: McpInvocationScope; + readonly requestKey: string; + readonly operation: string; +}): MessageId { + return MessageId.make( + [ + "message", + "mcp", + stablePart(input.scope.providerSessionId), + stablePart(input.operation), + stablePart(input.requestKey), + ].join(":"), + ); +} + +function threadTitle(input: { + readonly parentTitle: string; + readonly prompt: string | undefined; + readonly title: string | undefined; + readonly index: number; +}): string { + const detail = input.title?.trim() || input.prompt?.trim(); + if (!detail) return `${input.parentTitle} thread ${input.index + 1}`; + return detail.length > 80 ? `${detail.slice(0, 77)}...` : detail; +} + +function taskPrompt(input: OrchestratorMcpDelegateTaskInput): string { + return input.role === undefined || input.role === "general" + ? input.task + : `Act as the ${input.role} sub-agent for this task.\n\n${input.task}`; +} + +function listItemFromShell(shell: OrchestrationV2ThreadShell): OrchestratorMcpThreadListItem { + return { + threadId: shell.id, + title: shell.title, + createdBy: shell.createdBy, + creationSource: shell.creationSource, + status: shell.status, + latestRunId: shell.latestRunId, + providerInstanceId: shell.modelSelection.instanceId, + model: shell.modelSelection.model, + runtimeMode: shell.runtimeMode, + interactionMode: shell.interactionMode, + parentThreadId: shell.lineage.parentThreadId, + relationshipToParent: shell.lineage.relationshipToParent, + itemCount: shell.visibleItemCount, + createdAt: DateTime.formatIso(shell.createdAt), + updatedAt: DateTime.formatIso(shell.updatedAt), + }; +} + +function threadDetail(projection: OrchestrationV2ThreadProjection): OrchestratorMcpThreadDetail { + const latest = latestRun(projection); + const active = latestActiveRun(projection); + return { + threadId: projection.thread.id, + projectId: projection.thread.projectId, + title: projection.thread.title, + createdBy: projection.thread.createdBy, + creationSource: projection.thread.creationSource, + status: latest?.status ?? "idle", + latestRunId: latest?.id ?? null, + activeRunId: active?.id ?? null, + providerInstanceId: projection.thread.modelSelection.instanceId, + model: projection.thread.modelSelection.model, + runtimeMode: projection.thread.runtimeMode, + interactionMode: projection.thread.interactionMode, + branch: projection.thread.branch, + worktreePath: projection.thread.worktreePath, + parentThreadId: projection.thread.lineage.parentThreadId, + relationshipToParent: projection.thread.lineage.relationshipToParent, + runCount: projection.runs.length, + itemCount: projection.visibleTurnItems.length, + pendingRequestCount: projection.runtimeRequests.filter( + (request) => request.status === "pending", + ).length, + archived: projection.thread.archivedAt !== null, + createdAt: DateTime.formatIso(projection.thread.createdAt), + updatedAt: DateTime.formatIso(projection.updatedAt), + }; +} + +function threadRun(run: OrchestrationV2Run): OrchestratorMcpThreadRun { + return { + runId: run.id, + ordinal: run.ordinal, + status: run.status, + providerInstanceId: run.modelSelection.instanceId, + model: run.modelSelection.model, + requestedAt: DateTime.formatIso(run.requestedAt), + startedAt: run.startedAt === null ? null : DateTime.formatIso(run.startedAt), + completedAt: run.completedAt === null ? null : DateTime.formatIso(run.completedAt), + }; +} + +function jsonText(value: unknown): string { + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function turnItemText(item: OrchestrationV2TurnItem): string | null { + switch (item.type) { + case "user_message": + case "assistant_message": + case "reasoning": + return item.text; + case "proposed_plan": + return item.markdown; + case "todo_list": + return [item.explanation, ...item.steps.map((step) => `[${step.status}] ${step.text}`)] + .filter((line): line is string => line !== undefined) + .join("\n"); + case "user_input_request": + return jsonText(item.questions); + case "file_change": + return [ + item.fileName, + item.additions === undefined && item.deletions === undefined + ? undefined + : `+${item.additions ?? 0} -${item.deletions ?? 0}`, + item.diffStr ?? item.newStr, + ] + .filter((line): line is string => line !== undefined) + .join("\n"); + case "command_execution": + return [`$ ${item.input}`, item.output] + .filter((line): line is string => line !== undefined) + .join("\n"); + case "file_search": + return jsonText({ pattern: item.pattern, results: item.results }); + case "web_search": + return jsonText({ patterns: item.patterns, results: item.results }); + case "approval_request": + return item.prompt ?? item.requestKind; + case "checkpoint": + return jsonText(item.files); + case "run_interrupt_request": + case "run_interrupt_result": + return item.message; + case "compaction": + return item.summary ?? null; + case "handoff": + return item.summary ?? `${item.strategy} handoff to ${item.toProviderInstanceId}`; + case "fork": + return `Forked to thread ${item.targetThreadId}.`; + case "subagent": + return item.result ?? item.prompt; + case "dynamic_tool": + return jsonText({ toolName: item.toolName, input: item.input, output: item.output }); + } +} + +function timelineItem(input: { + readonly row: OrchestrationV2ThreadProjection["visibleTurnItems"][number]; + readonly maxChars: number; + readonly projection: OrchestrationV2ThreadProjection; +}): OrchestratorMcpThreadTimelineItem { + const text = turnItemText(input.row.item); + const textTruncated = text !== null && text.length > input.maxChars; + const messageId = + input.row.item.type === "user_message" || input.row.item.type === "assistant_message" + ? input.row.item.messageId + : null; + const message = + messageId === null + ? undefined + : input.projection.messages.find((candidate) => candidate.id === messageId); + return { + position: input.row.position, + visibility: input.row.visibility, + sourceThreadId: input.row.sourceThreadId, + itemId: input.row.sourceItemId, + runId: input.row.item.runId, + messageId, + createdBy: message?.createdBy ?? null, + creationSource: message?.creationSource ?? null, + type: input.row.item.type, + status: input.row.item.status, + title: input.row.item.title, + text: textTruncated ? `${text.slice(0, input.maxChars)}\n…[truncated]` : text, + textTruncated, + updatedAt: DateTime.formatIso(input.row.item.updatedAt), + }; +} + +const make = Effect.gen(function* () { + const crypto = yield* Crypto.Crypto; + const threadManagement = yield* ThreadManagementService; + const providerRegistry = yield* ProviderRegistry; + + const requireCapability = (scope: McpInvocationScope) => + scope.capabilities.has("orchestration") + ? Effect.void + : Effect.fail( + failure( + "capability_denied", + "This MCP credential does not grant orchestration capabilities.", + ), + ); + + const loadProjection = (threadId: ThreadId) => + threadManagement + .getThreadProjection(threadId) + .pipe( + Effect.mapError((error) => + failure( + "orchestration_error", + `Unable to read thread ${threadId}: ${errorMessage(error)}`, + ), + ), + ); + + const loadProjectThread = ( + projectId: OrchestrationV2ThreadProjection["thread"]["projectId"], + threadId: ThreadId, + ): Effect.Effect => + threadManagement + .getProjectThread({ projectId, threadId }) + .pipe( + Effect.mapError(() => + failure("thread_not_found", `Thread ${threadId} was not found in the calling project.`), + ), + ); + + const loadScopedThread = (scope: McpInvocationScope, threadId: ThreadId) => + Effect.gen(function* () { + yield* requireCapability(scope); + const parent = yield* loadProjection(scope.threadId); + const target = + threadId === scope.threadId + ? parent + : yield* loadProjectThread(parent.thread.projectId, threadId); + return { parent, target } as const; + }); + + const loadProviders = providerRegistry.getProviders; + + const resolveTarget = (input: { + readonly parent: OrchestrationV2ThreadProjection; + readonly target: OrchestratorMcpTarget | undefined; + readonly providers: ReadonlyArray; + }): Effect.Effect => + Effect.gen(function* () { + const requestedInstanceId = input.target?.providerInstanceId; + const requestedDriver = input.target?.driverKind; + let instanceId = requestedInstanceId; + + if (instanceId === undefined && requestedDriver !== undefined) { + const candidates = input.providers.filter( + (provider) => + provider.driver === requestedDriver && + isBuiltInProviderAdapterDriverV2(provider.driver), + ); + if (candidates.length === 0) { + return yield* failure( + "provider_unavailable", + `No V2 provider adapter is registered for driver ${requestedDriver}.`, + ); + } + const inheritedCandidate = candidates.find( + (candidate) => candidate.instanceId === input.parent.thread.modelSelection.instanceId, + ); + const availableCandidate = candidates.find((candidate) => { + return ( + providerConstraints(candidate, isBuiltInProviderAdapterDriverV2(candidate.driver)) + .length === 0 + ); + }); + instanceId = inheritedCandidate?.instanceId ?? availableCandidate?.instanceId; + } + instanceId ??= input.parent.thread.modelSelection.instanceId; + + const provider = input.providers.find((candidate) => candidate.instanceId === instanceId); + if (provider === undefined) { + return yield* failure( + "provider_unavailable", + `Provider instance ${instanceId} is not registered.`, + ); + } + if (requestedDriver !== undefined && provider.driver !== requestedDriver) { + return yield* failure( + "invalid_request", + `Provider instance ${instanceId} uses driver ${provider.driver}, not ${requestedDriver}.`, + ); + } + const constraints = providerConstraints( + provider, + isBuiltInProviderAdapterDriverV2(provider.driver), + ); + if (constraints.length > 0) { + return yield* failure( + "provider_unavailable", + `Provider ${instanceId} cannot run a child task: ${constraints.join(" ")}`, + ); + } + + const inheritedSelection = input.parent.thread.modelSelection; + const requestedModel = input.target?.model; + const model = + requestedModel ?? + (instanceId === inheritedSelection.instanceId + ? inheritedSelection.model + : provider?.models[0]?.slug); + if (model === undefined) { + return yield* failure( + "model_unavailable", + `Provider ${instanceId} has no model available for inheritance.`, + ); + } + if ( + requestedModel !== undefined && + provider !== undefined && + provider.models.length > 0 && + !provider.models.some((candidate) => candidate.slug === requestedModel) + ) { + return yield* failure( + "model_unavailable", + `Model ${requestedModel} is not advertised by provider ${instanceId}.`, + ); + } + + return { + modelSelection: + instanceId === inheritedSelection.instanceId && model === inheritedSelection.model + ? inheritedSelection + : { instanceId, model }, + }; + }); + + const requestKey = (clientRequestId: string | undefined): Effect.Effect => + clientRequestId === undefined + ? crypto.randomUUIDv4.pipe(Effect.orDie) + : Effect.succeed(clientRequestId); + + const readTask = ( + scope: McpInvocationScope, + taskId: NodeId, + waitTimedOut = false, + ): Effect.Effect => + Effect.gen(function* () { + yield* requireCapability(scope); + const parentProjection = yield* loadProjection(scope.threadId); + const task = parentProjection.subagents.find( + (candidate) => + candidate.id === taskId && + candidate.origin === "app_owned" && + candidate.threadId === scope.threadId, + ); + if (task === undefined || task.childThreadId === null) { + return yield* failure( + "task_not_found", + `Delegated task ${taskId} does not belong to thread ${scope.threadId}.`, + ); + } + const childProjection = yield* loadProjection(task.childThreadId); + const childRun = childProjection.runs[0]; + const status = taskStatusForRun(childRun); + const derivedResult = + task.result !== null + ? task.result + : childRun !== undefined && isTerminalTaskStatus(status) + ? subagentResultForRun(childProjection, childRun).text + : null; + const resultTransfer = + parentProjection.contextTransfers.find( + (transfer) => + transfer.type === "subagent_result" && + transfer.sourceThreadId === task.childThreadId && + transfer.targetThreadId === scope.threadId, + ) ?? null; + return { + taskId: task.id, + childThreadId: task.childThreadId, + childRunId: childRun?.id ?? null, + childNodeId: task.id, + status, + providerInstanceId: ProviderInstanceId.make(task.driver), + model: task.model, + summary: derivedResult, + resultContextTransferId: resultTransfer?.id ?? null, + waitTimedOut, + }; + }); + + const waitForTask = (scope: McpInvocationScope, taskId: NodeId, timeoutMs: number) => + Effect.gen(function* () { + while (true) { + const result = yield* readTask(scope, taskId); + if (isTerminalTaskStatus(result.status)) return result; + yield* Effect.sleep(Duration.millis(TASK_POLL_INTERVAL_MS)); + } + }).pipe(Effect.timeoutOption(Duration.millis(timeoutMs))); + + return OrchestratorMcpService.of({ + capabilities: (scope) => + Effect.gen(function* () { + yield* requireCapability(scope); + const parent = yield* loadProjection(scope.threadId); + const providers = yield* loadProviders; + return { + parentThreadId: scope.threadId, + inheritedProviderInstanceId: parent.thread.modelSelection.instanceId, + inheritedModel: parent.thread.modelSelection.model, + runtimeMode: parent.thread.runtimeMode, + interactionMode: parent.thread.interactionMode, + providers: providers.map((provider) => { + const constraints = providerConstraints( + provider, + isBuiltInProviderAdapterDriverV2(provider.driver), + ); + return { + providerInstanceId: provider.instanceId, + driverKind: provider.driver, + displayName: provider?.displayName ?? null, + models: + provider?.models.map((model) => ({ + id: model.slug, + label: model.name ?? null, + })) ?? [], + canRunChildTask: constraints.length === 0, + canRunCrossProviderChildTask: constraints.length === 0, + constraints: [...constraints], + }; + }), + features: { + appOwnedSubagents: true, + asyncPolling: true, + cancellation: true, + batchThreadCreation: true, + threadManagement: true, + incrementalThreadRead: true, + maxBatchThreads: 20, + }, + }; + }), + delegateTask: (scope, input) => + Effect.gen(function* () { + yield* requireCapability(scope); + const parent = yield* loadProjection(scope.threadId); + const parentRun = parent.runs + .filter(isActiveRun) + .toSorted((left, right) => right.ordinal - left.ordinal)[0]; + if ( + parentRun === undefined || + parentRun.rootNodeId === null || + parentRun.providerInstanceId !== scope.providerInstanceId + ) { + return yield* failure( + "parent_not_active", + "Delegated tasks require an active run owned by this MCP provider session.", + ); + } + const providers = yield* loadProviders; + const target = yield* resolveTarget({ + parent, + target: input.target, + providers, + }); + const runtimeMode = yield* resolveRuntimeMode(parent.thread.runtimeMode, input.runtimeMode); + const interactionMode = yield* resolveInteractionMode( + parent.thread.interactionMode, + input.interactionMode, + ); + const key = yield* requestKey(input.clientRequestId); + const commandId = stableCommandId({ + scope, + requestKey: key, + operation: "delegate-task", + }); + const result = yield* threadManagement + .dispatch({ + type: "delegated_task.request", + createdBy: "agent", + creationSource: "mcp", + commandId, + parentThreadId: scope.threadId, + parentRunId: parentRun.id, + parentNodeId: parentRun.rootNodeId, + task: taskPrompt(input), + ...(input.title === undefined ? {} : { title: input.title }), + modelSelection: target.modelSelection, + runtimeMode, + interactionMode, + }) + .pipe( + Effect.mapError((error) => + failure( + "orchestration_error", + `Unable to create delegated task: ${errorMessage(error)}`, + ), + ), + ); + const taskEvent = result.storedEvents.find( + (stored) => + stored.event.type === "subagent.updated" && stored.event.payload.origin === "app_owned", + ); + if (taskEvent?.event.type !== "subagent.updated") { + return yield* failure( + "orchestration_error", + "Delegated task command did not produce a task projection.", + ); + } + + if (input.mode !== "wait") { + return yield* readTask(scope, taskEvent.event.payload.id); + } + const timeoutMs = Math.min( + MAX_WAIT_TIMEOUT_MS, + Math.max(1, input.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS), + ); + const waited = yield* waitForTask(scope, taskEvent.event.payload.id, timeoutMs); + return Option.isSome(waited) + ? waited.value + : yield* readTask(scope, taskEvent.event.payload.id, true); + }), + taskStatus: (scope, taskId) => readTask(scope, taskId), + cancelTask: (scope, input) => + Effect.gen(function* () { + const current = yield* readTask(scope, input.taskId); + if (isTerminalTaskStatus(current.status)) { + return { + taskId: input.taskId, + status: current.status, + } satisfies OrchestratorMcpTaskCancelResult; + } + const child = yield* loadProjection(current.childThreadId); + const activeRun = child.runs.find(isActiveRun); + if (activeRun === undefined) { + return yield* failure( + "task_not_cancellable", + `Delegated task ${input.taskId} has no interruptible child run.`, + ); + } + const key = yield* requestKey(input.clientRequestId); + yield* threadManagement + .dispatch({ + type: "run.interrupt", + commandId: stableCommandId({ + scope, + requestKey: key, + operation: "cancel-task", + }), + threadId: current.childThreadId, + runId: activeRun.id, + ...(input.reason === undefined ? {} : { reason: input.reason }), + }) + .pipe( + Effect.mapError((error) => + failure( + "task_not_cancellable", + `Unable to interrupt delegated task ${input.taskId}: ${errorMessage(error)}`, + ), + ), + ); + return { + taskId: input.taskId, + status: "cancel_requested", + }; + }), + createThreads: (scope, input) => + Effect.gen(function* () { + yield* requireCapability(scope); + const parent = yield* loadProjection(scope.threadId); + const providers = yield* loadProviders; + const key = yield* requestKey(input.clientRequestId); + const created = yield* Effect.forEach( + input.threads, + (request, index) => + Effect.gen(function* () { + const target = yield* resolveTarget({ + parent, + target: request.target, + providers, + }); + const runtimeMode = yield* resolveRuntimeMode( + parent.thread.runtimeMode, + request.runtimeMode, + ); + const interactionMode = yield* resolveInteractionMode( + parent.thread.interactionMode, + request.interactionMode, + ); + const threadId = stableThreadId({ + scope, + requestKey: key, + index, + }); + const title = threadTitle({ + parentTitle: parent.thread.title, + prompt: request.prompt, + title: request.title, + index, + }); + yield* threadManagement + .dispatch({ + type: "thread.create", + createdBy: "agent", + creationSource: "mcp", + commandId: stableCommandId({ + scope, + requestKey: key, + operation: "create-thread", + index, + }), + threadId, + projectId: parent.thread.projectId, + title, + modelSelection: target.modelSelection, + runtimeMode, + interactionMode, + branch: parent.thread.branch, + worktreePath: parent.thread.worktreePath, + }) + .pipe( + Effect.mapError((error) => + failure( + "orchestration_error", + `Unable to create thread ${index + 1}: ${errorMessage(error)}`, + ), + ), + ); + if (request.prompt !== undefined) { + yield* threadManagement + .dispatch({ + type: "message.dispatch", + createdBy: "agent", + creationSource: "mcp", + commandId: stableCommandId({ + scope, + requestKey: key, + operation: "dispatch-thread", + index, + }), + threadId, + messageId: stableMessageId({ + scope, + requestKey: key, + index, + }), + text: request.prompt, + attachments: [], + modelSelection: target.modelSelection, + dispatchMode: { type: "start_immediately" }, + }) + .pipe( + Effect.mapError((error) => + failure( + "orchestration_error", + `Unable to start thread ${index + 1}: ${errorMessage(error)}`, + ), + ), + ); + } + const projection = yield* loadProjection(threadId); + const run = projection.runs.at(-1); + return { + threadId, + runId: run?.id ?? null, + status: run?.status ?? "idle", + title: projection.thread.title, + createdBy: projection.thread.createdBy, + creationSource: projection.thread.creationSource, + providerInstanceId: target.modelSelection.instanceId, + model: target.modelSelection.model, + } satisfies OrchestratorMcpCreatedThread; + }), + { concurrency: 1 }, + ); + return { threads: created }; + }), + listThreads: (scope, input) => + Effect.gen(function* () { + yield* requireCapability(scope); + const parent = yield* loadProjection(scope.threadId); + const projectThreads = yield* threadManagement + .listProjectThreads({ + projectId: parent.thread.projectId, + includeSubagents: input.includeSubagents !== false, + }) + .pipe( + Effect.mapError((error) => + failure("orchestration_error", `Unable to list threads: ${errorMessage(error)}`), + ), + ); + const statuses = input.statuses === undefined ? null : new Set(input.statuses); + const titleContains = input.titleContains?.toLocaleLowerCase(); + const filtered = projectThreads + .filter((thread) => statuses === null || statuses.has(thread.status)) + .filter( + (thread) => + titleContains === undefined || + thread.title.toLocaleLowerCase().includes(titleContains), + ); + const cursor = input.cursor ?? 0; + const limit = input.limit ?? DEFAULT_THREAD_LIST_LIMIT; + const page = filtered.slice(cursor, cursor + limit); + const nextCursor = cursor + page.length < filtered.length ? cursor + page.length : null; + return { + projectId: parent.thread.projectId, + currentThreadId: scope.threadId, + threads: page.map(listItemFromShell), + nextCursor, + total: filtered.length, + } satisfies OrchestratorMcpThreadListResult; + }), + readThread: (scope, input) => + Effect.gen(function* () { + const { target } = yield* loadScopedThread(scope, input.threadId); + const view = input.view ?? "messages"; + const afterPosition = input.afterPosition ?? -1; + const limit = input.limit ?? DEFAULT_THREAD_READ_LIMIT; + const maxChars = input.maxCharsPerItem ?? DEFAULT_THREAD_ITEM_MAX_CHARS; + const matching = target.visibleTurnItems + .filter((row) => row.position > afterPosition) + .filter( + (row) => + view === "activity" || + row.item.type === "user_message" || + row.item.type === "assistant_message" || + row.item.type === "proposed_plan", + ); + const page = matching.slice(0, limit); + return { + thread: threadDetail(target), + recentRuns: target.runs + .toSorted((left, right) => right.ordinal - left.ordinal) + .slice(0, input.runLimit ?? DEFAULT_THREAD_RUN_LIMIT) + .map(threadRun), + items: page.map((row) => timelineItem({ row, maxChars, projection: target })), + nextPosition: page.at(-1)?.position ?? null, + hasMore: page.length < matching.length, + } satisfies OrchestratorMcpThreadReadResult; + }), + sendToThread: (scope, input) => + Effect.gen(function* () { + const { parent, target } = yield* loadScopedThread(scope, input.threadId); + yield* resolveRuntimeMode(parent.thread.runtimeMode, target.thread.runtimeMode); + yield* resolveInteractionMode(parent.thread.interactionMode, target.thread.interactionMode); + + const mode = input.mode ?? "auto"; + const key = yield* requestKey(input.clientRequestId); + const messageId = stableOperationMessageId({ + scope, + requestKey: key, + operation: "thread-send", + }); + const result = yield* threadManagement + .sendToThread({ + projectId: parent.thread.projectId, + commandId: stableCommandId({ + scope, + requestKey: key, + operation: "thread-send", + }), + threadId: input.threadId, + messageId, + text: input.message, + attachments: [], + mode, + createdBy: "agent", + creationSource: "mcp", + }) + .pipe( + Effect.mapError((error) => + failure( + "thread_not_sendable", + `Unable to send to thread ${input.threadId}: ${errorMessage(error)}`, + ), + ), + ); + return { + threadId: input.threadId, + messageId, + runId: result.run.id, + status: result.run.status, + delivery: result.delivery, + } satisfies OrchestratorMcpThreadSendResult; + }), + waitForThread: (scope, input) => + Effect.gen(function* () { + const { parent } = yield* loadScopedThread(scope, input.threadId); + const result = yield* threadManagement + .waitForThread({ + projectId: parent.thread.projectId, + threadId: input.threadId, + ...(input.runId === undefined ? {} : { runId: input.runId }), + timeoutMs: Math.min( + MAX_WAIT_TIMEOUT_MS, + Math.max(1, input.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS), + ), + }) + .pipe( + Effect.mapError((error) => + failure( + error.code === "run_not_found" ? "run_not_found" : "orchestration_error", + error.message, + ), + ), + ); + return { + threadId: input.threadId, + runId: result.run?.id ?? null, + status: result.run?.status ?? "idle", + timedOut: result.timedOut, + } satisfies OrchestratorMcpThreadWaitResult; + }), + interruptThread: (scope, input) => + Effect.gen(function* () { + const { parent } = yield* loadScopedThread(scope, input.threadId); + const key = yield* requestKey(input.clientRequestId); + const result = yield* threadManagement + .interruptThread({ + projectId: parent.thread.projectId, + commandId: stableCommandId({ + scope, + requestKey: key, + operation: "thread-interrupt", + }), + threadId: input.threadId, + ...(input.runId === undefined ? {} : { runId: input.runId }), + ...(input.reason === undefined ? {} : { reason: input.reason }), + }) + .pipe( + Effect.mapError((error) => + failure( + isThreadManagementError(error) && error.code === "run_not_found" + ? "run_not_found" + : "thread_not_interruptible", + isThreadManagementError(error) + ? error.message + : `Unable to interrupt thread ${input.threadId}: ${errorMessage(error)}`, + ), + ), + ); + if (result.type === "no_active_run") { + return { + threadId: input.threadId, + runId: null, + status: "no_active_run", + } satisfies OrchestratorMcpThreadInterruptResult; + } + return { + threadId: input.threadId, + runId: result.run.id, + status: result.type === "already_terminal" ? result.run.status : "interrupt_requested", + } satisfies OrchestratorMcpThreadInterruptResult; + }), + }); +}); + +export const layer: Layer.Layer< + OrchestratorMcpService, + never, + Crypto.Crypto | ThreadManagementService | ProviderRegistry +> = Layer.effect(OrchestratorMcpService, make); diff --git a/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts new file mode 100644 index 00000000000..146a0faa084 --- /dev/null +++ b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts @@ -0,0 +1,925 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { describe, expect, it } from "@effect/vitest"; +import { + CommandId, + EnvironmentId, + MessageId, + type ModelSelection, + type OrchestrationV2ProviderCapabilities, + type OrchestrationV2ProviderSession, + type OrchestrationV2ProviderThread, + type OrchestrationV2ThreadProjection, + OrchestratorMcpCreateThreadsResult, + OrchestratorMcpCreatedThread, + OrchestratorMcpDelegateTaskResult, + OrchestratorMcpTaskCancelResult, + OrchestratorMcpThreadInterruptResult, + OrchestratorMcpThreadListResult, + OrchestratorMcpThreadReadResult, + OrchestratorMcpThreadSendResult, + OrchestratorMcpThreadWaitResult, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + ProviderThreadId, + ProviderTurnId, + type ServerProvider, + ThreadId, + TurnItemId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as PubSub from "effect/PubSub"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import { McpSchema, McpServer } from "effect/unstable/ai"; + +import { ClaudeProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/ClaudeAdapterV2.ts"; +import { CodexProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/CodexAdapterV2.ts"; +import { OrchestratorV2, type OrchestratorV2Shape } from "../orchestration-v2/Orchestrator.ts"; +import { layer as threadManagementServiceLayer } from "../orchestration-v2/ThreadManagementService.ts"; +import { + type ProviderAdapterV2Event, + ProviderAdapterProtocolError, + type ProviderAdapterV2Shape, + type ProviderAdapterV2TurnInput, +} from "../orchestration-v2/ProviderAdapter.ts"; +import { makeLayer as makeProviderAdapterRegistryLayer } from "../orchestration-v2/ProviderAdapterRegistry.ts"; +import { checkpointWorkspace } from "../orchestration-v2/testkit/ReplayFixtureWorkspace.ts"; +import { makeOrchestratorV2ReplayLayerWithRegistry } from "../orchestration-v2/testkit/ProviderReplayHarness.ts"; +import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts"; +import * as McpHttpServer from "./McpHttpServer.ts"; +import * as McpInvocationContext from "./McpInvocationContext.ts"; + +const parentThreadId = ThreadId.make("thread:mcp-orchestrator-parent"); +const projectId = ProjectId.make("project:mcp-orchestrator"); +const codexInstanceId = ProviderInstanceId.make("codex"); +const claudeInstanceId = ProviderInstanceId.make("claudeAgent"); +const codexModel = "gpt-5.4"; +const claudeModel = "claude-sonnet-4-6"; +const parentPrompt = "Keep this parent turn active while orchestration tools are tested."; +const delegatedPrompt = "Inspect the delegated API boundary and return the result."; +const delegatedResult = "Delegated API boundary inspected."; +const cancellationPrompt = "Remain active until the parent cancels this delegated task."; +const createdThreadPrompt = "Complete the newly created ordinary thread."; + +const decodeCreateThreadsResult = Schema.decodeUnknownEffect(OrchestratorMcpCreateThreadsResult); +const decodeCreatedThread = Schema.decodeUnknownEffect(OrchestratorMcpCreatedThread); +const decodeDelegateTaskResult = Schema.decodeUnknownEffect(OrchestratorMcpDelegateTaskResult); +const decodeTaskCancelResult = Schema.decodeUnknownEffect(OrchestratorMcpTaskCancelResult); +const decodeThreadInterruptResult = Schema.decodeUnknownEffect( + OrchestratorMcpThreadInterruptResult, +); +const decodeThreadListResult = Schema.decodeUnknownEffect(OrchestratorMcpThreadListResult); +const decodeThreadReadResult = Schema.decodeUnknownEffect(OrchestratorMcpThreadReadResult); +const decodeThreadSendResult = Schema.decodeUnknownEffect(OrchestratorMcpThreadSendResult); +const decodeThreadWaitResult = Schema.decodeUnknownEffect(OrchestratorMcpThreadWaitResult); + +const codexSelection = { + instanceId: codexInstanceId, + model: codexModel, +} satisfies ModelSelection; + +const claudeSelection = { + instanceId: claudeInstanceId, + model: claudeModel, +} satisfies ModelSelection; + +interface CapturedTurn { + readonly instanceId: ProviderInstanceId; + readonly threadId: ThreadId; + readonly text: string; +} + +function unsupported(driver: ProviderDriverKind, detail: string) { + return Effect.fail(new ProviderAdapterProtocolError({ driver, detail })); +} + +function makeProviderSnapshot(input: { + readonly instanceId: ProviderInstanceId; + readonly driver: ProviderDriverKind; + readonly model: string; +}): ServerProvider { + return { + instanceId: input.instanceId, + driver: input.driver, + enabled: true, + installed: true, + version: "test", + status: "ready", + auth: { status: "authenticated" }, + checkedAt: "2026-06-17T00:00:00.000Z", + models: [ + { + slug: input.model, + name: input.model, + isCustom: false, + capabilities: null, + }, + ], + slashCommands: [], + skills: [], + }; +} + +function makeDeterministicAdapter(input: { + readonly instanceId: ProviderInstanceId; + readonly driver: ProviderDriverKind; + readonly capabilities: OrchestrationV2ProviderCapabilities; + readonly capturedTurns: Ref.Ref>; + readonly shouldComplete: (turn: ProviderAdapterV2TurnInput) => boolean; + readonly response: (turn: ProviderAdapterV2TurnInput) => string; +}): ProviderAdapterV2Shape { + return { + instanceId: input.instanceId, + driver: input.driver, + getCapabilities: () => Effect.succeed(input.capabilities), + openSession: (sessionInput) => + Effect.gen(function* () { + const events = yield* PubSub.unbounded(); + const now = yield* DateTime.now; + const providerSession: OrchestrationV2ProviderSession = { + id: sessionInput.providerSessionId, + driver: input.driver, + providerInstanceId: input.instanceId, + status: "ready", + cwd: sessionInput.runtimePolicy.cwd ?? process.cwd(), + model: sessionInput.modelSelection.model, + capabilities: input.capabilities, + createdAt: now, + updatedAt: now, + lastError: null, + }; + + const publish = (providerEvents: ReadonlyArray) => + Effect.forEach(providerEvents, (event) => PubSub.publish(events, event), { + discard: true, + }); + + return { + instanceId: input.instanceId, + driver: input.driver, + providerSessionId: sessionInput.providerSessionId, + providerSession, + rawEvents: Stream.empty, + events: Stream.fromPubSub(events), + ensureThread: (threadInput) => + Effect.gen(function* () { + const createdAt = yield* DateTime.now; + const nativeThreadId = `${input.driver}:${threadInput.threadId}`; + return { + id: ProviderThreadId.make(`provider-thread:${nativeThreadId}`), + driver: input.driver, + providerInstanceId: input.instanceId, + providerSessionId: sessionInput.providerSessionId, + appThreadId: threadInput.threadId, + ownerNodeId: null, + nativeThreadRef: { + driver: input.driver, + nativeId: nativeThreadId, + strength: "strong", + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: null, + createdAt, + updatedAt: createdAt, + } satisfies OrchestrationV2ProviderThread; + }), + resumeThread: ({ providerThread }) => Effect.succeed(providerThread), + startTurn: (turnInput) => + Effect.gen(function* () { + yield* Ref.update(input.capturedTurns, (turns) => [ + ...turns, + { + instanceId: input.instanceId, + threadId: turnInput.threadId, + text: turnInput.message.text, + }, + ]); + const eventTime = yield* DateTime.now; + const providerTurnId = ProviderTurnId.make( + `provider-turn:${input.instanceId}:${turnInput.threadId}:${turnInput.runOrdinal}`, + ); + yield* publish([ + { + type: "provider_turn.updated", + driver: input.driver, + providerTurn: { + id: providerTurnId, + providerThreadId: turnInput.providerThread.id, + nodeId: turnInput.rootNodeId, + runAttemptId: turnInput.attemptId, + nativeTurnRef: { + driver: input.driver, + nativeId: `native-turn:${turnInput.threadId}:${turnInput.runOrdinal}`, + strength: "strong", + }, + ordinal: turnInput.providerTurnOrdinal, + status: "running", + startedAt: eventTime, + completedAt: null, + }, + }, + ]); + if (!input.shouldComplete(turnInput)) { + return; + } + const response = input.response(turnInput); + yield* publish([ + { + type: "provider_turn.updated", + driver: input.driver, + providerTurn: { + id: providerTurnId, + providerThreadId: turnInput.providerThread.id, + nodeId: turnInput.rootNodeId, + runAttemptId: turnInput.attemptId, + nativeTurnRef: { + driver: input.driver, + nativeId: `native-turn:${turnInput.threadId}:${turnInput.runOrdinal}`, + strength: "strong", + }, + ordinal: turnInput.providerTurnOrdinal, + status: "completed", + startedAt: eventTime, + completedAt: eventTime, + }, + }, + { + type: "turn_item.updated", + driver: input.driver, + turnItem: { + id: TurnItemId.make( + `turn-item:${input.instanceId}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`, + ), + threadId: turnInput.threadId, + runId: turnInput.runId, + nodeId: turnInput.rootNodeId, + providerThreadId: turnInput.providerThread.id, + providerTurnId, + nativeItemRef: null, + parentItemId: null, + ordinal: turnInput.runOrdinal * 100 + 1, + status: "completed", + title: null, + startedAt: eventTime, + completedAt: eventTime, + updatedAt: eventTime, + type: "assistant_message", + messageId: MessageId.make( + `message:${input.instanceId}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`, + ), + text: response, + streaming: false, + }, + }, + { + type: "turn.terminal", + driver: input.driver, + providerTurnId, + status: "completed", + }, + ]); + }), + steerTurn: () => Effect.void, + interruptTurn: ({ providerTurnId }) => + PubSub.publish(events, { + type: "turn.terminal", + driver: input.driver, + providerTurnId, + status: "interrupted", + }).pipe(Effect.asVoid), + respondToRuntimeRequest: () => Effect.void, + readThreadSnapshot: () => + unsupported(input.driver, "readThreadSnapshot is unused in this test"), + rollbackThread: () => unsupported(input.driver, "rollbackThread is unused in this test"), + forkThread: () => unsupported(input.driver, "forkThread is unused in this test"), + }; + }), + }; +} + +function waitForProjection( + orchestrator: OrchestratorV2Shape, + threadId: ThreadId, + predicate: (projection: OrchestrationV2ThreadProjection) => boolean, +) { + return Effect.gen(function* () { + for (let attempt = 0; attempt < 1_000; attempt += 1) { + const projection = yield* orchestrator.getThreadProjection(threadId); + if (predicate(projection)) { + return projection; + } + yield* Effect.sleep("5 millis"); + } + return yield* Effect.die( + new Error(`Timed out waiting for orchestration projection ${threadId}.`), + ); + }); +} + +const client = McpSchema.McpServerClient.of({ + clientId: 1, + initializePayload: { + protocolVersion: "2025-03-26", + capabilities: {}, + clientInfo: { name: "orchestrator-mcp-test", version: "1.0.0" }, + }, + getClient: Effect.die("unused"), +}); + +describe("orchestrator MCP toolkit", () => { + it.live( + "delegates cross-provider tasks, polls and cancels children, and creates ordinary threads", + () => + Effect.scoped( + Effect.gen(function* () { + const cwd = yield* checkpointWorkspace("orchestrator-mcp-toolkit"); + const capturedTurns = yield* Ref.make>([]); + const registryLayer = makeProviderAdapterRegistryLayer([ + makeDeterministicAdapter({ + instanceId: codexInstanceId, + driver: ProviderDriverKind.make("codex"), + capabilities: CodexProviderCapabilitiesV2, + capturedTurns, + shouldComplete: (turn) => + turn.threadId !== parentThreadId && turn.message.text !== cancellationPrompt, + response: (turn) => `Codex completed: ${turn.message.text}`, + }), + makeDeterministicAdapter({ + instanceId: claudeInstanceId, + driver: ProviderDriverKind.make("claudeAgent"), + capabilities: ClaudeProviderCapabilitiesV2, + capturedTurns, + shouldComplete: () => true, + response: (turn) => + turn.message.text === delegatedPrompt + ? delegatedResult + : `Claude completed: ${turn.message.text}`, + }), + ]); + const orchestratorLayer = makeOrchestratorV2ReplayLayerWithRegistry( + { + name: "orchestrator-mcp-toolkit", + runtimePolicyOverride: { + cwd, + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }, + }, + registryLayer, + ); + const orchestrationLayer = Layer.merge( + orchestratorLayer, + threadManagementServiceLayer.pipe(Layer.provide(orchestratorLayer)), + ); + const providerRegistryLayer = makeProviderRegistryLayer([ + makeProviderSnapshot({ + instanceId: codexInstanceId, + driver: ProviderDriverKind.make("codex"), + model: codexModel, + }), + makeProviderSnapshot({ + instanceId: claudeInstanceId, + driver: ProviderDriverKind.make("claudeAgent"), + model: claudeModel, + }), + makeProviderSnapshot({ + instanceId: ProviderInstanceId.make("opencode"), + driver: ProviderDriverKind.make("opencode"), + model: "opencode/test", + }), + ]); + const testLayer = McpHttpServer.OrchestratorToolkitRegistrationLive.pipe( + Layer.provideMerge(McpServer.McpServer.layer), + Layer.provideMerge(orchestrationLayer), + Layer.provide(providerRegistryLayer), + Layer.provide(NodeServices.layer), + ); + + yield* Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + const server = yield* McpServer.McpServer; + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:mcp-parent:create"), + threadId: parentThreadId, + projectId, + title: "MCP parent", + modelSelection: codexSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: cwd, + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:mcp-parent:start"), + threadId: parentThreadId, + messageId: MessageId.make("message:mcp-parent:start"), + text: parentPrompt, + attachments: [], + modelSelection: codexSelection, + dispatchMode: { type: "start_immediately" }, + }); + const parent = yield* waitForProjection( + orchestrator, + parentThreadId, + (projection) => + projection.runs.some((run) => + ["starting", "running", "waiting"].includes(run.status), + ) && projection.providerTurns.some((turn) => turn.status === "running"), + ); + const parentRun = parent.runs[0]; + expect(parentRun?.status).toBe("running"); + + const invocation: McpInvocationContext.McpInvocationScope = { + environmentId: EnvironmentId.make("environment:mcp-orchestrator"), + threadId: parentThreadId, + providerSessionId: "mcp-provider-session-parent", + providerInstanceId: codexInstanceId, + capabilities: new Set(["orchestration"]), + issuedAt: 1, + }; + const invoke = (name: string, args: Record) => + server + .callTool({ name, arguments: args }) + .pipe( + Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), + Effect.provideService(McpSchema.McpServerClient, client), + ); + + const capabilitiesTool = server.tools.find( + ({ tool }) => tool.name === "orchestrator_capabilities", + ); + expect(capabilitiesTool?.tool.annotations?.readOnlyHint).toBe(true); + expect(capabilitiesTool?.tool.annotations?.idempotentHint).toBe(true); + const delegateTool = server.tools.find(({ tool }) => tool.name === "delegate_task"); + expect(delegateTool?.tool.annotations?.destructiveHint).toBe(true); + expect(delegateTool?.tool.annotations?.openWorldHint).toBe(true); + const createThreadsTool = server.tools.find( + ({ tool }) => tool.name === "create_threads", + ); + expect(createThreadsTool?.tool.annotations?.destructiveHint).toBe(true); + const threadListTool = server.tools.find(({ tool }) => tool.name === "t3_thread_list"); + expect(threadListTool?.tool.annotations?.readOnlyHint).toBe(true); + expect(threadListTool?.tool.annotations?.idempotentHint).toBe(true); + const threadReadTool = server.tools.find(({ tool }) => tool.name === "t3_thread_read"); + expect(threadReadTool?.tool.annotations?.readOnlyHint).toBe(true); + const threadSendTool = server.tools.find(({ tool }) => tool.name === "t3_thread_send"); + expect(threadSendTool?.tool.annotations?.destructiveHint).toBe(true); + const threadWaitTool = server.tools.find(({ tool }) => tool.name === "t3_thread_wait"); + expect(threadWaitTool?.tool.annotations?.readOnlyHint).toBe(true); + const threadInterruptTool = server.tools.find( + ({ tool }) => tool.name === "t3_thread_interrupt", + ); + expect(threadInterruptTool?.tool.annotations?.destructiveHint).toBe(true); + + const capabilities = yield* invoke("orchestrator_capabilities", {}); + expect(capabilities.isError).toBe(false); + expect(capabilities.structuredContent).toMatchObject({ + inheritedProviderInstanceId: codexInstanceId, + inheritedModel: codexModel, + features: { + appOwnedSubagents: true, + asyncPolling: true, + cancellation: true, + batchThreadCreation: true, + threadManagement: true, + incrementalThreadRead: true, + }, + providers: expect.arrayContaining([ + expect.objectContaining({ + providerInstanceId: claudeInstanceId, + canRunCrossProviderChildTask: true, + }), + expect.objectContaining({ + providerInstanceId: "opencode", + canRunChildTask: true, + }), + ]), + }); + + const delegatedCall = yield* invoke("delegate_task", { + task: delegatedPrompt, + target: { + providerInstanceId: claudeInstanceId, + model: claudeModel, + }, + mode: "wait", + timeoutMs: 10_000, + clientRequestId: "delegate-claude-1", + }); + expect(delegatedCall.isError).toBe(false); + const delegated = yield* decodeDelegateTaskResult(delegatedCall.structuredContent).pipe( + Effect.orDie, + ); + expect(delegated.status).toBe("completed"); + expect(delegated.summary).toBe(delegatedResult); + expect(delegated.providerInstanceId).toBe(claudeInstanceId); + + const completedParent = yield* waitForProjection( + orchestrator, + parentThreadId, + (projection) => + projection.subagents.some( + (task) => + task.id === delegated.taskId && + task.status === "completed" && + task.result === delegatedResult, + ) && + projection.contextTransfers.some( + (transfer) => + transfer.type === "subagent_result" && + transfer.sourceThreadId === delegated.childThreadId, + ), + ); + const completedTask = completedParent.subagents.find( + (task) => task.id === delegated.taskId, + ); + expect(completedTask).toMatchObject({ + origin: "app_owned", + createdBy: "agent", + childThreadId: delegated.childThreadId, + status: "completed", + result: delegatedResult, + }); + const child = yield* orchestrator.getThreadProjection(delegated.childThreadId); + expect(child.thread.lineage).toEqual({ + parentThreadId, + relationshipToParent: "subagent", + rootThreadId: parentThreadId, + }); + expect(child.thread).toMatchObject({ + createdBy: "agent", + creationSource: "mcp", + }); + expect(child.thread.modelSelection).toEqual(claudeSelection); + expect( + child.messages + .filter((message) => message.role === "user") + .map((message) => message.text), + ).toEqual([delegatedPrompt]); + expect( + child.contextTransfers.some( + (transfer) => + transfer.type === "subagent_spawn" && transfer.sourceThreadId === parentThreadId, + ), + ).toBe(true); + const capturedAfterDelegate = yield* Ref.get(capturedTurns); + expect( + capturedAfterDelegate.filter((turn) => turn.threadId === delegated.childThreadId), + ).toEqual([ + { + instanceId: claudeInstanceId, + threadId: delegated.childThreadId, + text: delegatedPrompt, + }, + ]); + expect( + capturedAfterDelegate.some( + (turn) => + turn.threadId === delegated.childThreadId && turn.text.includes(parentPrompt), + ), + ).toBe(false); + + const delegatedStatusCall = yield* invoke("task_status", { + taskId: delegated.taskId, + }); + const delegatedStatus = yield* decodeDelegateTaskResult( + delegatedStatusCall.structuredContent, + ).pipe(Effect.orDie); + expect(delegatedStatus.status).toBe("completed"); + expect(delegatedStatus.resultContextTransferId).not.toBeNull(); + + const repeatedDelegatedCall = yield* invoke("delegate_task", { + task: delegatedPrompt, + target: { + providerInstanceId: claudeInstanceId, + model: claudeModel, + }, + mode: "async", + clientRequestId: "delegate-claude-1", + }); + const repeatedDelegated = yield* decodeDelegateTaskResult( + repeatedDelegatedCall.structuredContent, + ).pipe(Effect.orDie); + expect(repeatedDelegated.taskId).toBe(delegated.taskId); + expect( + (yield* orchestrator.getThreadProjection(parentThreadId)).subagents.filter( + (task) => task.id === delegated.taskId, + ), + ).toHaveLength(1); + + const cancellableCall = yield* invoke("delegate_task", { + task: cancellationPrompt, + target: { + providerInstanceId: codexInstanceId, + model: codexModel, + }, + mode: "async", + clientRequestId: "delegate-cancel-1", + }); + const cancellable = yield* decodeDelegateTaskResult( + cancellableCall.structuredContent, + ).pipe(Effect.orDie); + expect(cancellable.status).toBe("running"); + yield* waitForProjection(orchestrator, cancellable.childThreadId, (projection) => + projection.providerTurns.some((turn) => turn.status === "running"), + ); + const cancelCall = yield* invoke("task_cancel", { + taskId: cancellable.taskId, + reason: "Parent no longer needs this work.", + clientRequestId: "cancel-1", + }); + const cancelResult = yield* decodeTaskCancelResult(cancelCall.structuredContent).pipe( + Effect.orDie, + ); + expect(cancelResult.status).toBe("cancel_requested"); + yield* waitForProjection(orchestrator, cancellable.childThreadId, (projection) => + projection.runs.some((run) => run.status === "interrupted"), + ); + const cancelledStatusCall = yield* invoke("task_status", { + taskId: cancellable.taskId, + }); + const cancelledStatus = yield* decodeDelegateTaskResult( + cancelledStatusCall.structuredContent, + ).pipe(Effect.orDie); + expect(cancelledStatus.status).toBe("interrupted"); + + const createInput = { + clientRequestId: "create-thread-batch-1", + threads: [ + { + title: "Inherited empty thread", + }, + { + title: "Claude ordinary thread", + prompt: createdThreadPrompt, + target: { + driverKind: "claudeAgent", + }, + }, + ], + }; + const createCall = yield* invoke("create_threads", createInput); + expect(createCall.isError).toBe(false); + const created = yield* decodeCreateThreadsResult(createCall.structuredContent).pipe( + Effect.orDie, + ); + expect(created.threads).toHaveLength(2); + const emptyThread = created.threads[0]!; + const promptedThread = created.threads[1]!; + expect(emptyThread).toMatchObject({ + status: "idle", + createdBy: "agent", + creationSource: "mcp", + providerInstanceId: codexInstanceId, + model: codexModel, + }); + expect(promptedThread).toMatchObject({ + createdBy: "agent", + creationSource: "mcp", + providerInstanceId: claudeInstanceId, + model: claudeModel, + }); + const emptyProjection = yield* orchestrator.getThreadProjection(emptyThread.threadId); + expect(emptyProjection.thread.lineage).toEqual({ + parentThreadId: null, + relationshipToParent: null, + rootThreadId: emptyThread.threadId, + }); + expect(emptyProjection.thread).toMatchObject({ + createdBy: "agent", + creationSource: "mcp", + }); + expect(emptyProjection.thread.forkedFrom).toBeNull(); + expect(emptyProjection.runs).toEqual([]); + const promptedProjection = yield* waitForProjection( + orchestrator, + promptedThread.threadId, + (projection) => projection.runs.some((run) => run.status === "completed"), + ); + expect(promptedProjection.thread.lineage.parentThreadId).toBeNull(); + expect( + promptedProjection.messages + .filter((message) => message.role === "user") + .map((message) => message.text), + ).toEqual([createdThreadPrompt]); + + const repeatedCreateCall = yield* invoke("create_threads", createInput); + const repeatedCreated = yield* decodeCreateThreadsResult( + repeatedCreateCall.structuredContent, + ).pipe(Effect.orDie); + expect(repeatedCreated.threads.map((thread) => thread.threadId)).toEqual( + created.threads.map((thread) => thread.threadId), + ); + + const promptedReadCall = yield* invoke("t3_thread_read", { + threadId: promptedThread.threadId, + limit: 1, + }); + const promptedRead = yield* decodeThreadReadResult( + promptedReadCall.structuredContent, + ).pipe(Effect.orDie); + expect(promptedRead.thread.status).toBe("completed"); + expect(promptedRead.thread).toMatchObject({ + createdBy: "agent", + creationSource: "mcp", + }); + expect(promptedRead.items.map((item) => item.type)).toEqual(["user_message"]); + expect(promptedRead.items[0]).toMatchObject({ + createdBy: "agent", + creationSource: "mcp", + }); + expect(promptedRead.hasMore).toBe(true); + const promptedReadNextCall = yield* invoke("t3_thread_read", { + threadId: promptedThread.threadId, + afterPosition: promptedRead.nextPosition, + limit: 1, + }); + const promptedReadNext = yield* decodeThreadReadResult( + promptedReadNextCall.structuredContent, + ).pipe(Effect.orDie); + expect(promptedReadNext.items.map((item) => item.type)).toEqual(["assistant_message"]); + expect(promptedReadNext.items[0]?.text).toBe( + `Claude completed: ${createdThreadPrompt}`, + ); + + const ordinaryLoopPrompt = "Run an ordinary thread loop iteration."; + const sendCall = yield* invoke("t3_thread_send", { + threadId: emptyThread.threadId, + message: ordinaryLoopPrompt, + clientRequestId: "ordinary-loop-send-1", + }); + const sent = yield* decodeThreadSendResult(sendCall.structuredContent).pipe( + Effect.orDie, + ); + expect(sent.delivery).toBe("started"); + const waitCall = yield* invoke("t3_thread_wait", { + threadId: emptyThread.threadId, + runId: sent.runId, + timeoutMs: 10_000, + }); + const waited = yield* decodeThreadWaitResult(waitCall.structuredContent).pipe( + Effect.orDie, + ); + expect(waited).toMatchObject({ + runId: sent.runId, + status: "completed", + timedOut: false, + }); + const repeatedSendCall = yield* invoke("t3_thread_send", { + threadId: emptyThread.threadId, + message: ordinaryLoopPrompt, + clientRequestId: "ordinary-loop-send-1", + }); + const repeatedSend = yield* decodeThreadSendResult( + repeatedSendCall.structuredContent, + ).pipe(Effect.orDie); + expect(repeatedSend.runId).toBe(sent.runId); + expect( + (yield* orchestrator.getThreadProjection(emptyThread.threadId)).runs, + ).toHaveLength(1); + + const activeThreadCall = yield* invoke("t3_thread_start", { + prompt: cancellationPrompt, + title: "Managed active thread", + clientRequestId: "managed-active-thread-1", + }); + const activeThread = yield* decodeCreatedThread( + activeThreadCall.structuredContent, + ).pipe(Effect.orDie); + const activeProjection = yield* waitForProjection( + orchestrator, + activeThread.threadId, + (projection) => + projection.runs.some((run) => run.status === "running") && + projection.providerTurns.some((turn) => turn.status === "running"), + ); + const activeRun = activeProjection.runs[0]!; + const activeTimeoutCall = yield* invoke("t3_thread_wait", { + threadId: activeThread.threadId, + runId: activeRun.id, + timeoutMs: 1, + }); + const activeTimeout = yield* decodeThreadWaitResult( + activeTimeoutCall.structuredContent, + ).pipe(Effect.orDie); + expect(activeTimeout).toMatchObject({ + runId: activeRun.id, + status: "running", + timedOut: true, + }); + const steerCall = yield* invoke("t3_thread_send", { + threadId: activeThread.threadId, + message: "Include the latest parent guidance before finishing.", + mode: "steer", + clientRequestId: "managed-active-steer-1", + }); + const steered = yield* decodeThreadSendResult(steerCall.structuredContent).pipe( + Effect.orDie, + ); + expect(steered).toMatchObject({ + runId: activeRun.id, + delivery: "steered", + }); + const interruptCall = yield* invoke("t3_thread_interrupt", { + threadId: activeThread.threadId, + reason: "The orchestration loop has enough evidence.", + clientRequestId: "managed-active-interrupt-1", + }); + const interrupted = yield* decodeThreadInterruptResult( + interruptCall.structuredContent, + ).pipe(Effect.orDie); + expect(interrupted).toMatchObject({ + runId: activeRun.id, + status: "interrupt_requested", + }); + const interruptedWaitCall = yield* invoke("t3_thread_wait", { + threadId: activeThread.threadId, + runId: activeRun.id, + timeoutMs: 10_000, + }); + const interruptedWait = yield* decodeThreadWaitResult( + interruptedWaitCall.structuredContent, + ).pipe(Effect.orDie); + expect(interruptedWait.status).toBe("interrupted"); + const repeatedInterruptCall = yield* invoke("t3_thread_interrupt", { + threadId: activeThread.threadId, + runId: activeRun.id, + }); + const repeatedInterrupt = yield* decodeThreadInterruptResult( + repeatedInterruptCall.structuredContent, + ).pipe(Effect.orDie); + expect(repeatedInterrupt.status).toBe("interrupted"); + + const foreignThreadId = ThreadId.make("thread:mcp-foreign-project"); + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:mcp-foreign-project:create"), + threadId: foreignThreadId, + projectId: ProjectId.make("project:mcp-foreign"), + title: "Foreign project thread", + modelSelection: codexSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: cwd, + }); + const foreignReadCall = yield* invoke("t3_thread_read", { + threadId: foreignThreadId, + }); + expect(foreignReadCall.structuredContent).toMatchObject({ + _tag: "OrchestratorMcpFailure", + code: "thread_not_found", + }); + const listCall = yield* invoke("t3_thread_list", { + includeSubagents: false, + limit: 100, + }); + const listed = yield* decodeThreadListResult(listCall.structuredContent).pipe( + Effect.orDie, + ); + expect(listed.projectId).toBe(projectId); + expect(listed.threads.map((thread) => thread.threadId)).toEqual( + expect.arrayContaining([ + parentThreadId, + emptyThread.threadId, + promptedThread.threadId, + activeThread.threadId, + ]), + ); + expect( + listed.threads.find((thread) => thread.threadId === emptyThread.threadId), + ).toMatchObject({ + createdBy: "agent", + creationSource: "mcp", + }); + expect(listed.threads.some((thread) => thread.threadId === foreignThreadId)).toBe( + false, + ); + expect( + listed.threads.some((thread) => thread.relationshipToParent === "subagent"), + ).toBe(false); + }).pipe(Effect.provide(testLayer)); + }), + ), + ); +}); diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index 9f7ef2113d7..e415620b00f 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -22,7 +22,6 @@ const scope = { providerInstanceId: ProviderInstanceId.make("codex"), capabilities: new Set(["preview"] as const), issuedAt: 1, - expiresAt: 2, }; const makeOwner = (overrides: Partial = {}): PreviewAutomationOwner => ({ diff --git a/apps/server/src/mcp/toolkits/orchestrator/handlers.ts b/apps/server/src/mcp/toolkits/orchestrator/handlers.ts new file mode 100644 index 00000000000..1c4482d04fe --- /dev/null +++ b/apps/server/src/mcp/toolkits/orchestrator/handlers.ts @@ -0,0 +1,90 @@ +import { OrchestratorToolkit } from "./tools.ts"; +import * as Effect from "effect/Effect"; + +import { McpInvocationContext } from "../../McpInvocationContext.ts"; +import { OrchestratorMcpService } from "../../OrchestratorMcpService.ts"; + +const handlers = { + orchestrator_capabilities: () => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.capabilities(scope); + }), + delegate_task: (input) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.delegateTask(scope, input); + }), + task_status: ({ taskId }) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.taskStatus(scope, taskId); + }), + task_cancel: (input) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.cancelTask(scope, input); + }), + create_threads: (input) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.createThreads(scope, input); + }), + t3_thread_start: (input) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + const result = yield* service.createThreads(scope, { + ...(input.clientRequestId === undefined ? {} : { clientRequestId: input.clientRequestId }), + threads: [ + { + prompt: input.prompt, + ...(input.title === undefined ? {} : { title: input.title }), + ...(input.target === undefined ? {} : { target: input.target }), + ...(input.runtimeMode === undefined ? {} : { runtimeMode: input.runtimeMode }), + ...(input.interactionMode === undefined + ? {} + : { interactionMode: input.interactionMode }), + }, + ], + }); + return result.threads[0]!; + }), + t3_thread_list: (input) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.listThreads(scope, input); + }), + t3_thread_read: (input) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.readThread(scope, input); + }), + t3_thread_send: (input) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.sendToThread(scope, input); + }), + t3_thread_wait: (input) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.waitForThread(scope, input); + }), + t3_thread_interrupt: (input) => + Effect.gen(function* () { + const scope = yield* McpInvocationContext; + const service = yield* OrchestratorMcpService; + return yield* service.interruptThread(scope, input); + }), +} satisfies Parameters[0]; + +export const OrchestratorToolkitHandlersLive = OrchestratorToolkit.toLayer(handlers); diff --git a/apps/server/src/mcp/toolkits/orchestrator/tools.ts b/apps/server/src/mcp/toolkits/orchestrator/tools.ts new file mode 100644 index 00000000000..a3edbcdeaa8 --- /dev/null +++ b/apps/server/src/mcp/toolkits/orchestrator/tools.ts @@ -0,0 +1,188 @@ +import { + OrchestratorMcpCapabilitiesResult, + OrchestratorMcpCreatedThread, + OrchestratorMcpCreateThreadsInput, + OrchestratorMcpCreateThreadsResult, + OrchestratorMcpDelegateTaskInput, + OrchestratorMcpDelegateTaskResult, + OrchestratorMcpFailure, + OrchestratorMcpTaskCancelInput, + OrchestratorMcpTaskCancelResult, + OrchestratorMcpTaskStatusInput, + OrchestratorMcpThreadInterruptInput, + OrchestratorMcpThreadInterruptResult, + OrchestratorMcpThreadListInput, + OrchestratorMcpThreadListResult, + OrchestratorMcpThreadReadInput, + OrchestratorMcpThreadReadResult, + OrchestratorMcpThreadSendInput, + OrchestratorMcpThreadSendResult, + OrchestratorMcpThreadStartInput, + OrchestratorMcpThreadWaitInput, + OrchestratorMcpThreadWaitResult, +} from "@t3tools/contracts"; +import { Tool, Toolkit } from "effect/unstable/ai"; + +import * as McpInvocationContext from "../../McpInvocationContext.ts"; +import { OrchestratorMcpService } from "../../OrchestratorMcpService.ts"; + +const dependencies = [McpInvocationContext.McpInvocationContext, OrchestratorMcpService]; + +export const OrchestratorCapabilitiesTool = Tool.make("orchestrator_capabilities", { + description: + "List the V2 provider instances, models, inherited runtime settings, and app-owned orchestration features available to this T3 thread.", + success: OrchestratorMcpCapabilitiesResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Get orchestration capabilities") + .annotate(Tool.Readonly, true) + .annotate(Tool.Destructive, false) + .annotate(Tool.Idempotent, true); + +export const DelegateTaskTool = Tool.make("delegate_task", { + description: + "Create a T3-owned child agent thread and run it with only the supplied task prompt, without copying parent conversation history. Provider, model, runtime mode, and interaction mode inherit from the parent unless overridden. Prefer mode='async' and poll task_status for long work; mode='wait' blocks until completion or timeout.", + parameters: OrchestratorMcpDelegateTaskInput, + success: OrchestratorMcpDelegateTaskResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Delegate a child task") + .annotate(Tool.Destructive, true) + .annotate(Tool.OpenWorld, true); + +export const TaskStatusTool = Tool.make("task_status", { + description: + "Read the latest durable state and final summary for a T3-owned delegated task created by this parent thread.", + parameters: OrchestratorMcpTaskStatusInput, + success: OrchestratorMcpDelegateTaskResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Get delegated task status") + .annotate(Tool.Readonly, true) + .annotate(Tool.Destructive, false) + .annotate(Tool.Idempotent, true); + +export const TaskCancelTool = Tool.make("task_cancel", { + description: + "Request interruption of an active T3-owned delegated task. Completed tasks are returned unchanged.", + parameters: OrchestratorMcpTaskCancelInput, + success: OrchestratorMcpTaskCancelResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Cancel delegated task") + .annotate(Tool.Destructive, true); + +export const CreateThreadsTool = Tool.make("create_threads", { + description: + "Create one or more ordinary top-level T3 V2 threads. Each entry may have its own prompt, title, provider instance or driver, model, runtime mode, and interaction mode. Omitted provider/model/settings inherit from the calling thread; entries without prompts create empty threads.", + parameters: OrchestratorMcpCreateThreadsInput, + success: OrchestratorMcpCreateThreadsResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Create T3 threads") + .annotate(Tool.Destructive, true) + .annotate(Tool.OpenWorld, true); + +export const ThreadStartTool = Tool.make("t3_thread_start", { + description: + "Create an ordinary top-level T3 thread and immediately start its first turn. The new thread inherits this thread's project, checkout, provider, model, and runtime settings unless overridden. Use t3_thread_wait and t3_thread_read to collect its result.", + parameters: OrchestratorMcpThreadStartInput, + success: OrchestratorMcpCreatedThread, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Start a T3 thread") + .annotate(Tool.Destructive, true) + .annotate(Tool.OpenWorld, true); + +export const ThreadListTool = Tool.make("t3_thread_list", { + description: + "List T3 threads in the calling thread's project, newest first. Filter by durable run status or title and paginate with the returned cursor. Threads from other projects are never exposed.", + parameters: OrchestratorMcpThreadListInput, + success: OrchestratorMcpThreadListResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "List T3 threads") + .annotate(Tool.Readonly, true) + .annotate(Tool.Destructive, false) + .annotate(Tool.Idempotent, true); + +export const ThreadReadTool = Tool.make("t3_thread_read", { + description: + "Read durable state and a paginated timeline from a T3 thread in the calling project. The default messages view returns user messages, assistant messages, and proposed plans; activity returns all summarized timeline items. Continue with afterPosition=nextPosition.", + parameters: OrchestratorMcpThreadReadInput, + success: OrchestratorMcpThreadReadResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Read a T3 thread") + .annotate(Tool.Readonly, true) + .annotate(Tool.Destructive, false) + .annotate(Tool.Idempotent, true); + +export const ThreadSendTool = Tool.make("t3_thread_send", { + description: + "Send a message to a T3 thread in the calling project. mode='auto' starts an idle thread, steers a fully active turn, or queues behind a turn that is not yet steerable. Use queue for a separate follow-up turn, steer for an in-flight update, or restart to interrupt-and-restart the active turn. clientRequestId makes retries idempotent.", + parameters: OrchestratorMcpThreadSendInput, + success: OrchestratorMcpThreadSendResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Send to a T3 thread") + .annotate(Tool.Destructive, true) + .annotate(Tool.OpenWorld, true); + +export const ThreadWaitTool = Tool.make("t3_thread_wait", { + description: + "Wait for a T3 thread run to reach a terminal durable state. Without runId, the latest run at call time is selected; an idle thread returns immediately. Timeout does not interrupt work, so call again or use t3_thread_read/list after timedOut=true.", + parameters: OrchestratorMcpThreadWaitInput, + success: OrchestratorMcpThreadWaitResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Wait for a T3 thread") + .annotate(Tool.Readonly, true) + .annotate(Tool.Destructive, false) + .annotate(Tool.Idempotent, true); + +export const ThreadInterruptTool = Tool.make("t3_thread_interrupt", { + description: + "Request interruption of a running turn in a T3 thread in the calling project. Without runId, the newest interruptible run is selected. Terminal runs and threads without an active turn return without another side effect. clientRequestId makes retries idempotent.", + parameters: OrchestratorMcpThreadInterruptInput, + success: OrchestratorMcpThreadInterruptResult, + failure: OrchestratorMcpFailure, + failureMode: "return", + dependencies, +}) + .annotate(Tool.Title, "Interrupt a T3 thread") + .annotate(Tool.Destructive, true); + +export const OrchestratorToolkit = Toolkit.make( + OrchestratorCapabilitiesTool, + DelegateTaskTool, + TaskStatusTool, + TaskCancelTool, + CreateThreadsTool, + ThreadStartTool, + ThreadListTool, + ThreadReadTool, + ThreadSendTool, + ThreadWaitTool, + ThreadInterruptTool, +); diff --git a/apps/server/src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts b/apps/server/src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts new file mode 100644 index 00000000000..0fedf0b7a3e --- /dev/null +++ b/apps/server/src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts @@ -0,0 +1,198 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import { + CommandId, + type ModelSelection, + MessageId, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + ThreadId, +} from "@t3tools/contracts"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { FetchHttpClient } from "effect/unstable/http"; +import { describe } from "vite-plus/test"; + +import * as CheckpointStore from "../checkpointing/CheckpointStore.ts"; +import { ServerConfig } from "../config.ts"; +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import { ProviderInstanceRegistryHydrationLive } from "../provider/Layers/ProviderInstanceRegistryHydration.ts"; +import { + NoOpProviderEventLoggers, + ProviderEventLoggers, +} from "../provider/Layers/ProviderEventLoggers.ts"; +import { OpenCodeRuntimeLive } from "../provider/opencodeRuntime.ts"; +import { ServerSettingsService } from "../serverSettings.ts"; +import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; +import * as VcsProcess from "../vcs/VcsProcess.ts"; +import { OrchestratorV2 } from "./Orchestrator.ts"; +import { OrchestrationV2LayerLive } from "./runtimeLayer.ts"; +import { layer as mcpSessionRegistryTestLayer } from "../mcp/McpSessionRegistry.testkit.ts"; + +const liveAgentId = process.env.T3_ACP_REGISTRY_LIVE_AGENT_ID?.trim() || "devin"; +const liveCommandPath = process.env.T3_ACP_REGISTRY_LIVE_COMMAND?.trim(); +const liveInstanceId = ProviderInstanceId.make("acpRegistry_live"); +const liveModelSelection = { + instanceId: liveInstanceId, + model: "default", +} satisfies ModelSelection; + +const serverConfigLayer = ServerConfig.layerTest(process.cwd(), { + prefix: "t3-acp-registry-v2-live-", +}); + +const vcsDriverRegistryLayer = VcsDriverRegistry.layer.pipe( + Layer.provide(VcsProcess.layer), + Layer.provide(serverConfigLayer), + Layer.provide(NodeServices.layer), +); + +const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(vcsDriverRegistryLayer)); + +const serverSettingsLayer = ServerSettingsService.layerTest({ + providerInstances: { + [liveInstanceId]: { + driver: ProviderDriverKind.make("acpRegistry"), + displayName: `ACP Registry: ${liveAgentId}`, + enabled: true, + config: { + agentId: liveAgentId, + ...(liveCommandPath ? { commandPath: liveCommandPath } : {}), + }, + }, + }, +}); +const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe( + Layer.provide( + Layer.mergeAll( + serverConfigLayer.pipe(Layer.provide(NodeServices.layer)), + serverSettingsLayer, + NodeServices.layer, + FetchHttpClient.layer, + OpenCodeRuntimeLive.pipe(Layer.provide(NodeServices.layer)), + Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers), + ), + ), +); + +const liveLayer = OrchestrationV2LayerLive.pipe( + Layer.provide(mcpSessionRegistryTestLayer), + Layer.provide(SqlitePersistenceMemory), + Layer.provide(checkpointStoreLayer), + Layer.provide(serverConfigLayer), + Layer.provide(serverSettingsLayer), + Layer.provide(providerInstanceRegistryLayer), + Layer.provide(NodeServices.layer), +); + +const waitForIdle = Effect.fn("AcpRegistryOrchestratorV2Live.waitForIdle")(function* ( + threadId: ThreadId, + expectedRunCount: number, +) { + const orchestrator = yield* OrchestratorV2; + for (let attempt = 0; attempt < 900; attempt += 1) { + const projection = yield* orchestrator.getThreadProjection(threadId); + if ( + projection.runs.length >= expectedRunCount && + projection.runs.every( + (run) => !["queued", "starting", "running", "waiting"].includes(run.status), + ) + ) { + return projection; + } + yield* Effect.sleep("500 millis"); + } + return yield* Effect.die(new Error(`Timed out waiting for ACP Registry thread ${threadId}.`)); +}); + +describe.runIf(process.env.T3_ACP_REGISTRY_LIVE_ORCHESTRATOR === "1")( + "ACP Registry V2 live orchestrator", + () => { + it.live( + "runs and resumes a real registry agent through the production V2 harness", + () => + Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + const projectId = ProjectId.make("project:acp-registry-live"); + const threadId = ThreadId.make("thread:acp-registry-live"); + const marker = "ACP_REGISTRY_LIVE_7H3Q"; + + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:acp-registry-live:create"), + threadId, + projectId, + title: `ACP Registry live: ${liveAgentId}`, + modelSelection: liveModelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + yield* Console.log( + `ACP Registry thread created for '${liveAgentId}'; dispatching first prompt.`, + ); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:acp-registry-live:first"), + threadId, + messageId: MessageId.make("message:acp-registry-live:first"), + text: `Remember this opaque marker. Respond with exactly: ${marker}`, + attachments: [], + modelSelection: liveModelSelection, + dispatchMode: { type: "start_immediately" }, + }); + const firstProjection = yield* waitForIdle(threadId, 1); + const firstAssistant = firstProjection.messages.findLast( + (message) => message.role === "assistant", + )?.text; + + assert.deepEqual( + firstProjection.runs.map((run) => [run.providerInstanceId, run.status]), + [[liveInstanceId, "completed"]], + ); + assert.include(firstAssistant ?? "", marker); + + yield* Console.log("First ACP turn completed; dispatching continuation prompt."); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:acp-registry-live:second"), + threadId, + messageId: MessageId.make("message:acp-registry-live:second"), + text: "Return the opaque marker from the previous turn. Respond with only the marker.", + attachments: [], + modelSelection: liveModelSelection, + dispatchMode: { type: "start_immediately" }, + }); + const finalProjection = yield* waitForIdle(threadId, 2); + const finalAssistant = finalProjection.messages.findLast( + (message) => message.role === "assistant", + )?.text; + + assert.deepEqual( + finalProjection.runs.map((run) => [run.providerInstanceId, run.status]), + [ + [liveInstanceId, "completed"], + [liveInstanceId, "completed"], + ], + ); + assert.include(finalAssistant ?? "", marker); + assert.deepEqual(finalProjection.providerSessions.length, 2); + assert.isAtLeast(finalProjection.providerThreads.length, 1); + assert.deepEqual( + finalProjection.providerTurns.map((turn) => turn.status), + ["completed", "completed"], + ); + }).pipe(Effect.provide(liveLayer), Effect.scoped), + 480_000, + ); + }, +); diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts new file mode 100644 index 00000000000..f4fd97a6779 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -0,0 +1,366 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import { + MessageId, + NodeId, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + ProviderSessionId, + RunAttemptId, + RunId, + ThreadId, + type OrchestrationV2ProviderThread, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Fiber from "effect/Fiber"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as Stream from "effect/Stream"; +import * as TestClock from "effect/testing/TestClock"; +import { ChildProcessSpawner } from "effect/unstable/process"; + +import { ServerConfig } from "../../config.ts"; +import * as AcpSessionRuntime from "../../provider/acp/AcpSessionRuntime.ts"; +import { layer as idAllocatorLayer, IdAllocatorV2 } from "../IdAllocator.ts"; +import { + ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2TurnInput, +} from "../ProviderAdapter.ts"; +import { + AcpProviderCapabilitiesV2, + makeAcpAdapterV2, + type AcpAdapterV2Flavor, +} from "./AcpAdapterV2.ts"; + +const serverConfigLayer = ServerConfig.layerTest(process.cwd(), { + prefix: "t3-acp-v2-adapter-", +}).pipe(Layer.provide(NodeServices.layer)); + +const testLayer = Layer.mergeAll(NodeServices.layer, idAllocatorLayer, serverConfigLayer); +const ACP_TEST_DRIVER = ProviderDriverKind.make("acp-test"); + +function makeMockRuntime(input: { + readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; + readonly mockAgentPath: string; + readonly environment?: Readonly>; +}): AcpAdapterV2Flavor["makeRuntime"] { + return (runtimeInput) => + Effect.gen(function* () { + const context = yield* Layer.build( + AcpSessionRuntime.layer({ + ...runtimeInput, + spawn: { + command: process.execPath, + args: [input.mockAgentPath], + cwd: runtimeInput.cwd, + env: { T3_ACP_SESSION_LIFECYCLE: "1", ...input.environment }, + }, + authMethodId: "test", + }).pipe( + Layer.provide( + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, input.childProcessSpawner), + ), + ), + ); + return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe( + Effect.provide(context), + ); + }); +} + +function makeTurnInput(input: { + readonly threadId: ThreadId; + readonly providerThread: OrchestrationV2ProviderThread; + readonly instanceId: ProviderInstanceId; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; + readonly now: DateTime.Utc; + readonly ordinal?: number; +}): ProviderAdapterV2TurnInput { + const ordinal = input.ordinal ?? 1; + const suffix = `${input.threadId}:${ordinal}`; + const modelSelection = { instanceId: input.instanceId, model: "default" } as const; + return { + appThread: { + createdBy: "user", + creationSource: "web", + id: input.threadId, + projectId: ProjectId.make(`project:${input.threadId}`), + title: "ACP adapter test", + providerInstanceId: input.instanceId, + modelSelection, + runtimeMode: "approval-required", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: input.providerThread.id, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: input.threadId, + }, + forkedFrom: null, + createdAt: input.now, + updatedAt: input.now, + archivedAt: null, + deletedAt: null, + }, + threadId: input.threadId, + runId: RunId.make(`run:${suffix}`), + runOrdinal: ordinal, + providerTurnOrdinal: ordinal, + attemptId: RunAttemptId.make(`attempt:${suffix}`), + rootNodeId: NodeId.make(`node:${suffix}`), + providerThread: input.providerThread, + message: { + createdBy: "user", + creationSource: "web", + messageId: MessageId.make(`message:${suffix}`), + text: "test prompt", + attachments: [], + }, + modelSelection, + runtimePolicy: input.runtimePolicy, + }; +} + +describe("AcpAdapterV2", () => { + it.effect("negotiates and executes optional native session forks through the ACP runtime", () => + Effect.gen(function* () { + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fileSystem = yield* FileSystem.FileSystem; + const idAllocator = yield* IdAllocatorV2; + const path = yield* Path.Path; + const serverConfig = yield* ServerConfig; + const mockAgentPath = yield* path.fromFileUrl( + new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), + ); + const makeRuntime = makeMockRuntime({ childProcessSpawner, mockAgentPath }); + + const instanceId = ProviderInstanceId.make("acp-test"); + const adapter = makeAcpAdapterV2({ + instanceId, + flavor: { + driver: ACP_TEST_DRIVER, + capabilities: AcpProviderCapabilitiesV2, + makeRuntime, + }, + fileSystem, + idAllocator, + serverConfig, + }); + const sourceThreadId = ThreadId.make("thread-acp-native-fork-source"); + const targetThreadId = ThreadId.make("thread-acp-native-fork-target"); + const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: process.cwd(), + }); + const modelSelection = { instanceId, model: "default" } as const; + const runtime = yield* adapter.openSession({ + threadId: sourceThreadId, + providerSessionId: ProviderSessionId.make("provider-session-acp-native-fork"), + modelSelection, + runtimePolicy, + }); + + assert.isTrue(runtime.providerSession.capabilities.threads.canForkThread); + assert.isTrue(runtime.providerSession.capabilities.threads.canReadThreadSnapshot); + + const sourceProviderThread = yield* runtime.ensureThread({ + threadId: sourceThreadId, + modelSelection, + runtimePolicy, + }); + const forkedProviderThread = yield* runtime.forkThread({ + sourceProviderThread, + targetThreadId, + }); + + assert.equal(sourceProviderThread.nativeThreadRef?.nativeId, "mock-session-1"); + assert.equal(forkedProviderThread.nativeThreadRef?.nativeId, "mock-session-1-fork"); + assert.equal(forkedProviderThread.appThreadId, targetThreadId); + assert.equal(forkedProviderThread.forkedFrom?.providerThreadId, sourceProviderThread.id); + }).pipe(Effect.provide(testLayer), Effect.scoped), + ); + + it.effect("cancels pending permission requests while interrupting an ACP turn", () => + Effect.gen(function* () { + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fileSystem = yield* FileSystem.FileSystem; + const idAllocator = yield* IdAllocatorV2; + const path = yield* Path.Path; + const serverConfig = yield* ServerConfig; + const mockAgentPath = yield* path.fromFileUrl( + new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), + ); + const instanceId = ProviderInstanceId.make("acp-test"); + const adapter = makeAcpAdapterV2({ + instanceId, + flavor: { + driver: ACP_TEST_DRIVER, + capabilities: AcpProviderCapabilitiesV2, + makeRuntime: makeMockRuntime({ + childProcessSpawner, + mockAgentPath, + environment: { T3_ACP_EMIT_TOOL_CALLS: "1" }, + }), + }, + fileSystem, + idAllocator, + serverConfig, + }); + const threadId = ThreadId.make("thread-acp-cancel-permission"); + const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "approval-required", + interactionMode: "default", + cwd: process.cwd(), + }); + const modelSelection = { instanceId, model: "default" } as const; + const runtime = yield* adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("provider-session-acp-cancel-permission"), + modelSelection, + runtimePolicy, + }); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection, + runtimePolicy, + }); + const now = yield* DateTime.now; + yield* runtime.startTurn( + makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }), + ); + + const pendingRequest = Option.getOrThrow( + yield* runtime.events.pipe( + Stream.filter( + (event) => + event.type === "runtime_request.updated" && event.runtimeRequest.status === "pending", + ), + Stream.runHead, + ), + ); + if ( + pendingRequest.type !== "runtime_request.updated" || + pendingRequest.runtimeRequest.providerTurnId === null + ) { + return yield* Effect.die("Expected a pending ACP permission request with a provider turn"); + } + + yield* runtime.interruptTurn({ + providerThread, + providerTurnId: pendingRequest.runtimeRequest.providerTurnId, + }); + + const cancelledRequest = Option.getOrThrow( + yield* runtime.events.pipe( + Stream.filter( + (event) => + event.type === "runtime_request.updated" && + event.runtimeRequest.id === pendingRequest.runtimeRequest.id && + event.runtimeRequest.status === "cancelled", + ), + Stream.runHead, + ), + ); + assert.equal(cancelledRequest.type, "runtime_request.updated"); + }).pipe(Effect.provide(testLayer), Effect.scoped), + ); + + it.effect("does not release an ACP turn when cancellation is not acknowledged", () => + Effect.gen(function* () { + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fileSystem = yield* FileSystem.FileSystem; + const idAllocator = yield* IdAllocatorV2; + const path = yield* Path.Path; + const serverConfig = yield* ServerConfig; + const mockAgentPath = yield* path.fromFileUrl( + new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), + ); + const instanceId = ProviderInstanceId.make("acp-test"); + const adapter = makeAcpAdapterV2({ + instanceId, + flavor: { + driver: ACP_TEST_DRIVER, + capabilities: AcpProviderCapabilitiesV2, + makeRuntime: makeMockRuntime({ + childProcessSpawner, + mockAgentPath, + environment: { T3_ACP_PROMPT_DELAY_MS: "5000" }, + }), + }, + fileSystem, + idAllocator, + serverConfig, + }); + const threadId = ThreadId.make("thread-acp-cancel-timeout"); + const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: process.cwd(), + }); + const modelSelection = { instanceId, model: "default" } as const; + const runtime = yield* adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("provider-session-acp-cancel-timeout"), + modelSelection, + runtimePolicy, + }); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection, + runtimePolicy, + }); + const now = yield* DateTime.now; + const firstTurn = makeTurnInput({ + threadId, + providerThread, + instanceId, + runtimePolicy, + now, + }); + yield* runtime.startTurn(firstTurn); + yield* runtime.rawEvents.pipe( + Stream.filter( + (event) => event.direction === "outgoing" && event.method === "session/prompt", + ), + Stream.runHead, + ); + const providerTurnId = idAllocator.derive.providerTurn({ + driver: ACP_TEST_DRIVER, + nativeTurnId: "mock-session-1:turn:1", + }); + const interruptFiber = yield* runtime + .interruptTurn({ providerThread, providerTurnId }) + .pipe(Effect.flip, Effect.forkScoped); + yield* runtime.rawEvents.pipe( + Stream.filter( + (event) => event.direction === "outgoing" && event.method === "session/cancel", + ), + Stream.runHead, + ); + yield* TestClock.adjust("10 seconds"); + const interruptError = yield* Fiber.join(interruptFiber); + assert.equal(interruptError._tag, "ProviderAdapterInterruptError"); + + const secondTurnError = yield* runtime + .startTurn( + makeTurnInput({ + threadId, + providerThread, + instanceId, + runtimePolicy, + now, + ordinal: 2, + }), + ) + .pipe(Effect.flip); + assert.equal(secondTurnError._tag, "ProviderAdapterTurnStartError"); + }).pipe(Effect.provide(testLayer), Effect.scoped), + ); +}); diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.testkit.ts new file mode 100644 index 00000000000..5b3c51f7874 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.testkit.ts @@ -0,0 +1,190 @@ +import { + ProviderDriverKind, + ProviderReplayEntry, + type ProviderReplayTranscript, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import type * as Scope from "effect/Scope"; +import { ChildProcessSpawner } from "effect/unstable/process"; +import * as EffectAcpErrors from "effect-acp/errors"; + +import * as AcpSessionRuntime from "../../provider/acp/AcpSessionRuntime.ts"; +import { ACP_PROTOCOL, type AcpAdapterV2RuntimeInput } from "./AcpAdapterV2.ts"; + +export const AcpReplayTranscript = Schema.Struct({ + provider: ProviderDriverKind, + protocol: Schema.Literal(ACP_PROTOCOL), + version: Schema.String, + scenario: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + entries: Schema.Array(ProviderReplayEntry), +}); +export type AcpReplayTranscript = typeof AcpReplayTranscript.Type; + +const decodeAcpReplayTranscriptSchema = Schema.decodeUnknownEffect(AcpReplayTranscript); + +export class AcpReplayTranscriptDecodeError extends Schema.TaggedErrorClass()( + "AcpReplayTranscriptDecodeError", + { + expectedProvider: ProviderDriverKind, + driver: Schema.optional(Schema.String), + protocol: Schema.optional(Schema.String), + scenario: Schema.optional(Schema.String), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to decode ${this.expectedProvider} ACP replay transcript for scenario ${this.scenario ?? ""}.`; + } +} + +const isAcpReplayTranscriptDecodeError = Schema.is(AcpReplayTranscriptDecodeError); + +function metadataFromTranscript(transcript: ProviderReplayTranscript): { + readonly provider?: string; + readonly protocol?: string; + readonly scenario?: string; +} { + return { + provider: transcript.provider, + protocol: transcript.protocol, + scenario: transcript.scenario, + }; +} + +export function decodeAcpReplayTranscript( + transcript: ProviderReplayTranscript, + expectedProvider: ProviderDriverKind, + options: { readonly retargetProvider?: boolean } = {}, +): Effect.Effect { + const candidate = options.retargetProvider + ? { ...transcript, provider: expectedProvider } + : transcript; + return decodeAcpReplayTranscriptSchema(candidate).pipe( + Effect.filterOrFail( + (decoded) => decoded.provider === expectedProvider, + () => + new AcpReplayTranscriptDecodeError({ + expectedProvider, + ...metadataFromTranscript(transcript), + cause: `Expected provider ${expectedProvider}, received ${transcript.provider}`, + }), + ), + Effect.mapError((cause) => + isAcpReplayTranscriptDecodeError(cause) + ? cause + : new AcpReplayTranscriptDecodeError({ + expectedProvider, + ...metadataFromTranscript(transcript), + cause, + }), + ), + ); +} + +interface ReplayStatus { + readonly cursor: number; + readonly total: number; + readonly failure?: unknown; +} + +function decodeReplayStatus(raw: string): ReplayStatus { + const value: unknown = JSON.parse(raw); + if ( + typeof value !== "object" || + value === null || + typeof Reflect.get(value, "cursor") !== "number" || + typeof Reflect.get(value, "total") !== "number" + ) { + throw new Error("ACP replay status is malformed."); + } + return value as ReplayStatus; +} + +export function makeAcpReplayCompletenessAssertion( + fileSystem: FileSystem.FileSystem, + statusPath: string, + transcript: AcpReplayTranscript, +): Effect.Effect { + return fileSystem.readFileString(statusPath).pipe( + Effect.mapError( + (cause) => + new EffectAcpErrors.AcpTransportError({ + detail: `Failed to read ACP replay status for ${transcript.scenario}`, + cause, + }), + ), + Effect.flatMap((raw) => + Effect.try({ + try: () => decodeReplayStatus(raw), + catch: (cause) => + new EffectAcpErrors.AcpTransportError({ + detail: `Failed to decode ACP replay status for ${transcript.scenario}`, + cause, + }), + }), + ), + Effect.flatMap((status) => { + if ( + status.failure === undefined && + status.cursor === transcript.entries.length && + status.total === transcript.entries.length + ) { + return Effect.void; + } + return Effect.fail( + new EffectAcpErrors.AcpTransportError({ + detail: `ACP replay did not consume all frames for ${transcript.scenario}`, + cause: status, + }), + ); + }), + ); +} + +export function makeAcpReplayRuntime(input: { + readonly transcript: AcpReplayTranscript; + readonly statusPath: string; + readonly scriptPath: string; + readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; +}): ( + runtimeInput: AcpAdapterV2RuntimeInput, +) => Effect.Effect< + AcpSessionRuntime.AcpSessionRuntime["Service"], + EffectAcpErrors.AcpError, + Scope.Scope +> { + const encodedTranscript = Buffer.from(JSON.stringify(input.transcript), "utf8").toString( + "base64", + ); + return (runtimeInput) => + Effect.gen(function* () { + const context = yield* Layer.build( + AcpSessionRuntime.layer({ + ...runtimeInput, + spawn: { + command: process.execPath, + args: [input.scriptPath], + cwd: runtimeInput.cwd, + env: { + ...process.env, + T3_ACP_REPLAY_TRANSCRIPT: encodedTranscript, + T3_ACP_REPLAY_STATUS_PATH: input.statusPath, + T3_ACP_REPLAY_WORKSPACE: runtimeInput.cwd, + }, + }, + authMethodId: "replay", + }).pipe( + Layer.provide( + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, input.childProcessSpawner), + ), + ), + ); + return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe( + Effect.provide(context), + ); + }); +} diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts new file mode 100644 index 00000000000..1a21d33d749 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts @@ -0,0 +1,2229 @@ +import { + type ChatAttachment, + type ModelSelection, + type OrchestrationV2ConversationMessage, + type OrchestrationV2ExecutionNode, + type OrchestrationV2PlanArtifact, + type OrchestrationV2PlanStep, + type OrchestrationV2ProviderCapabilities, + type OrchestrationV2ProviderSession, + type OrchestrationV2ProviderThread, + type OrchestrationV2ProviderTurn, + type OrchestrationV2RawProviderEvent, + type OrchestrationV2RuntimeRequest, + type OrchestrationV2TurnItem, + type OrchestrationV2UserInputQuestion, + type ProviderApprovalDecision, + type ProviderInstanceId, + type ProviderDriverKind, + type ProviderRequestKind, + type ProviderUserInputAnswers, + type RuntimeRequestId, + type ThreadId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; +import type * as Scope from "effect/Scope"; +import * as Stream from "effect/Stream"; +import * as EffectAcpErrors from "effect-acp/errors"; +import type * as EffectAcpProtocol from "effect-acp/protocol"; +import type * as EffectAcpSchema from "effect-acp/schema"; + +import { resolveAttachmentPath } from "../../attachmentStore.ts"; +import { ServerConfig } from "../../config.ts"; +import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; +import { + mergeToolCallState, + parsePermissionRequest, + parseSessionUpdateEvent, + type AcpPlanUpdate, + type AcpToolCallState, +} from "../../provider/acp/AcpRuntimeModel.ts"; +import type { + AcpSessionRuntimeOptions, + AcpSessionRuntimeStartResult, +} from "../../provider/acp/AcpSessionRuntime.ts"; +import * as AcpSessionRuntime from "../../provider/acp/AcpSessionRuntime.ts"; +import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts"; +import { + ProviderAdapterEnsureThreadError, + ProviderAdapterForkThreadError, + ProviderAdapterInterruptError, + ProviderAdapterOpenSessionError, + ProviderAdapterProtocolError, + ProviderAdapterReadThreadSnapshotError, + ProviderAdapterResumeThreadError, + ProviderAdapterRollbackThreadError, + ProviderAdapterRuntimeRequestResponseError, + ProviderAdapterSteerRunUnsupportedError, + ProviderAdapterTurnStartError, + ProviderAdapterV2, + type ProviderAdapterV2EnsureThreadInput, + type ProviderAdapterV2Event, + type ProviderAdapterV2InterruptInput, + type ProviderAdapterV2OpenSessionInput, + type ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2SessionRuntime, + type ProviderAdapterV2Shape, + type ProviderAdapterV2TurnInput, +} from "../ProviderAdapter.ts"; + +export const ACP_PROTOCOL = "acp.ndjson-jsonrpc" as const; + +export interface AcpAdapterV2RuntimeInput { + readonly cwd: string; + readonly mcpServers: ReadonlyArray; + readonly clientCapabilities: EffectAcpSchema.InitializeRequest["clientCapabilities"]; + readonly clientInfo: AcpSessionRuntimeOptions["clientInfo"]; + readonly protocolLogging: NonNullable; +} + +export interface AcpAdapterV2UserInputRequest { + readonly nativeItemId: string; + readonly nativeRequestId: string; + readonly questions: ReadonlyArray; +} + +export interface AcpAdapterV2ExtensionContext { + readonly runtime: AcpSessionRuntime.AcpSessionRuntime["Service"]; + readonly requestUserInput: ( + input: AcpAdapterV2UserInputRequest, + ) => Effect.Effect; +} + +export interface AcpAdapterV2Flavor { + readonly driver: ProviderDriverKind; + readonly capabilities: OrchestrationV2ProviderCapabilities; + readonly makeRuntime: ( + input: AcpAdapterV2RuntimeInput, + ) => Effect.Effect< + AcpSessionRuntime.AcpSessionRuntime["Service"], + EffectAcpErrors.AcpError, + Scope.Scope + >; + readonly resolveModelId?: (selection: ModelSelection) => string | undefined; + readonly registerExtensions?: ( + context: AcpAdapterV2ExtensionContext, + ) => Effect.Effect; + readonly assertComplete?: Effect.Effect; +} + +export interface AcpAdapterV2Options { + readonly instanceId: ProviderInstanceId; + readonly flavor: AcpAdapterV2Flavor; + readonly fileSystem: FileSystem.FileSystem; + readonly idAllocator: IdAllocatorV2Shape; + readonly serverConfig: ServerConfig["Service"]; +} + +export const AcpProviderCapabilitiesV2 = { + sessions: { + supportsMultipleProviderThreadsPerSession: false, + supportsModelSwitchInSession: false, + supportsProviderSwitchingViaHandoff: true, + supportsRuntimeModeSwitchInSession: false, + pendingRequestsSurviveRestart: false, + }, + threads: { + canCreateEmptyThread: true, + canReadThreadSnapshot: false, + canRollbackThread: false, + canForkThread: false, + canForkFromTurn: false, + canForkFromSubagentThread: false, + exposesNativeThreadId: true, + }, + turns: { + exposesNativeTurnId: false, + emitsTurnStarted: true, + emitsTurnCompleted: true, + supportsInterrupt: true, + supportsActiveSteering: false, + supportsSteeringByInterruptRestart: true, + supportsQueuedMessages: true, + terminalStatusQuality: "strong", + }, + streaming: { + streamsAssistantText: true, + streamsReasoning: true, + streamsToolOutput: true, + streamsPlanText: false, + emitsMessageCompleted: true, + }, + tools: { + exposesToolItemIds: true, + emitsToolStarted: true, + emitsToolCompleted: true, + emitsToolOutput: true, + supportsMcpTools: false, + supportsDynamicToolCallbacks: false, + }, + approvals: { + supportsCommandApproval: true, + supportsFileReadApproval: true, + supportsFileChangeApproval: true, + supportsApplyPatchApproval: false, + approvalsHaveNativeRequestIds: false, + approvalCallbacksAreLiveOnly: true, + approvalsCanOriginateFromSubagents: false, + }, + planning: { + emitsPlanUpdated: true, + emitsTodoList: true, + emitsProposedPlan: false, + supportsStructuredQuestions: true, + planDeltasHaveItemIds: false, + }, + subagents: { + supportsSubagents: false, + exposesSubagentThreadIds: false, + emitsSubagentLifecycle: false, + canWaitForSubagents: false, + canCloseSubagents: false, + canForkSubagentThread: false, + }, + context: { + acceptsSystemContext: false, + acceptsDeveloperContext: false, + acceptsSyntheticUserContext: true, + canGenerateSummaries: true, + canConsumeHandoffSummaries: true, + supportsDeltaHandoff: true, + supportsFullThreadHandoff: true, + maxRecommendedHandoffChars: null, + }, + checkpointing: { + appCanCheckpointFilesystem: true, + supportsNestedCheckpointScopes: true, + providerCanRollbackConversation: false, + providerRollbackReturnsSnapshot: false, + providerCanReadConversationSnapshot: false, + }, + identity: { + nativeThreadIds: "strong", + nativeTurnIds: "weak", + nativeItemIds: "weak", + nativeRequestIds: "weak", + }, +} satisfies OrchestrationV2ProviderCapabilities; + +function negotiatedCapabilities( + base: OrchestrationV2ProviderCapabilities, + started: AcpSessionRuntimeStartResult, +): OrchestrationV2ProviderCapabilities { + const agent = started.initializeResult.agentCapabilities ?? {}; + const session = agent.sessionCapabilities; + const setup = started.sessionSetupResult; + const hasModelConfig = + setup.configOptions?.some((option) => option.category === "model") === true; + const hasModeConfig = setup.configOptions?.some((option) => option.category === "mode") === true; + const supportsMcp = agent.mcpCapabilities?.http === true || agent.mcpCapabilities?.sse === true; + const canLoad = agent.loadSession === true; + const canFork = session?.fork != null; + return { + ...base, + sessions: { + ...base.sessions, + supportsModelSwitchInSession: setup.models != null || hasModelConfig, + supportsRuntimeModeSwitchInSession: setup.modes != null || hasModeConfig, + }, + threads: { + ...base.threads, + canReadThreadSnapshot: canLoad, + canForkThread: canFork, + canForkFromTurn: false, + }, + tools: { + ...base.tools, + supportsMcpTools: supportsMcp, + }, + checkpointing: { + ...base.checkpointing, + providerCanReadConversationSnapshot: canLoad, + }, + }; +} + +function acpMcpServers(threadId: ThreadId): ReadonlyArray { + const session = McpProviderSession.readMcpProviderSession(threadId); + if (session === undefined) { + return []; + } + return [ + { + type: "http", + name: "t3-code", + url: session.endpoint, + headers: [ + { + name: "Authorization", + value: session.authorizationHeader, + }, + ], + }, + ]; +} + +function nativeThreadId(driver: ProviderDriverKind, thread: OrchestrationV2ProviderThread): string { + const id = thread.nativeThreadRef?.nativeId; + if (id === null || id === undefined || id.trim().length === 0) { + throw new ProviderAdapterProtocolError({ + driver, + detail: `Provider thread ${thread.id} is missing its ACP session id`, + }); + } + return id; +} + +function makeProviderThread(input: { + readonly driver: ProviderDriverKind; + readonly providerInstanceId: ProviderInstanceId; + readonly idAllocator: IdAllocatorV2Shape; + readonly appThreadId: OrchestrationV2ProviderThread["appThreadId"]; + readonly providerSessionId: OrchestrationV2ProviderThread["providerSessionId"]; + readonly nativeThreadId: string; + readonly ownerNodeId?: OrchestrationV2ProviderThread["ownerNodeId"]; + readonly forkedFrom?: OrchestrationV2ProviderThread["forkedFrom"]; + readonly now: DateTime.Utc; +}): OrchestrationV2ProviderThread { + return { + id: input.idAllocator.derive.providerThread({ + driver: input.driver, + nativeThreadId: input.nativeThreadId, + }), + driver: input.driver, + providerInstanceId: input.providerInstanceId, + providerSessionId: input.providerSessionId, + appThreadId: input.appThreadId, + ownerNodeId: input.ownerNodeId ?? null, + nativeThreadRef: { + driver: input.driver, + nativeId: input.nativeThreadId, + strength: "strong", + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: input.forkedFrom ?? null, + createdAt: input.now, + updatedAt: input.now, + }; +} + +function unknownRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +function nonEmptyText(value: unknown, fallback: string): string { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback; +} + +function textFromUnknown(value: unknown): string | undefined { + if (typeof value === "string") { + return value; + } + if (Array.isArray(value)) { + const parts = value.flatMap((entry) => { + const text = textFromUnknown(entry); + return text === undefined || text.length === 0 ? [] : [text]; + }); + return parts.length === 0 ? undefined : parts.join("\n"); + } + const record = unknownRecord(value); + if (record === undefined) { + return undefined; + } + for (const key of ["stdout", "stderr", "output", "content", "text", "message"]) { + const text = textFromUnknown(record[key]); + if (text !== undefined && text.length > 0) { + return text; + } + } + return undefined; +} + +function commandExitCode(value: unknown): number | undefined { + const record = unknownRecord(value); + for (const key of ["exitCode", "exit_code", "code"]) { + const candidate = record?.[key]; + if (typeof candidate === "number" && Number.isInteger(candidate)) { + return candidate; + } + } + return undefined; +} + +function pathFromToolCall(toolCall: AcpToolCallState): string | undefined { + const locations = toolCall.data.locations; + if (Array.isArray(locations)) { + for (const location of locations) { + const path = unknownRecord(location)?.path; + if (typeof path === "string" && path.trim().length > 0) { + return path.trim(); + } + } + } + const rawInput = unknownRecord(toolCall.data.rawInput); + for (const key of ["path", "filePath", "file_path", "url", "query", "pattern"]) { + const candidate = rawInput?.[key]; + if (typeof candidate === "string" && candidate.trim().length > 0) { + return candidate.trim(); + } + } + return undefined; +} + +function providerRequestKind(kind: string | "unknown"): ProviderRequestKind { + switch (kind) { + case "execute": + return "command"; + case "read": + case "search": + case "fetch": + return "file-read"; + case "edit": + case "delete": + case "move": + return "file-change"; + default: + return "command"; + } +} + +function toolStatus( + status: AcpToolCallState["status"], +): "pending" | "running" | "completed" | "failed" { + switch (status) { + case "completed": + return "completed"; + case "failed": + return "failed"; + case "pending": + return "pending"; + default: + return "running"; + } +} + +function nodeStatus(status: ReturnType): OrchestrationV2ExecutionNode["status"] { + return status === "pending" ? "running" : status; +} + +function completedAtForStatus( + status: ReturnType, + now: DateTime.Utc, +): DateTime.Utc | null { + return status === "completed" || status === "failed" ? now : null; +} + +function selectPermissionOptionId( + request: EffectAcpSchema.RequestPermissionRequest, + decision: Exclude, +): string | undefined { + const kind = + decision === "acceptForSession" + ? "allow_always" + : decision === "accept" + ? "allow_once" + : "reject_once"; + return request.options.find((option) => option.kind === kind)?.optionId.trim() || undefined; +} + +function selectAutoApprovedPermissionOption( + request: EffectAcpSchema.RequestPermissionRequest, +): string | undefined { + return ( + selectPermissionOptionId(request, "acceptForSession") ?? + selectPermissionOptionId(request, "accept") + ); +} + +export type AcpPermissionDisposition = "allow" | "ask" | "deny"; + +export function acpPermissionDisposition( + runtimePolicy: ProviderAdapterV2RuntimePolicy, + request: EffectAcpSchema.RequestPermissionRequest, +): AcpPermissionDisposition { + const approvalPolicy = runtimePolicy.approvalPolicy; + const requiresApproval = + approvalPolicy === undefined + ? runtimePolicy.runtimeMode === "approval-required" + : approvalPolicy !== "never"; + if (requiresApproval) { + return "ask"; + } + + const sandboxPolicy = unknownRecord(runtimePolicy.sandboxPolicy); + const sandboxType = sandboxPolicy?.type; + const toolKind = request.toolCall.kind ?? "other"; + switch (sandboxType) { + case "readOnly": + return toolKind === "read" || toolKind === "search" || toolKind === "think" + ? "allow" + : "deny"; + case "workspaceWrite": + return toolKind === "read" || + toolKind === "search" || + toolKind === "think" || + toolKind === "edit" || + toolKind === "delete" || + toolKind === "move" + ? "allow" + : "deny"; + case "dangerFullAccess": + case "externalSandbox": + return "allow"; + case undefined: + return runtimePolicy.runtimeMode === "approval-required" ? "deny" : "allow"; + default: + return "deny"; + } +} + +function jsonRpcId(value: unknown): string | number | null { + return (typeof value === "string" && value.length > 0) || typeof value === "number" + ? value + : null; +} + +function elicitationContent( + answers: ProviderUserInputAnswers, + allowedKeys: ReadonlySet, +): Record { + const content: Record = {}; + for (const [key, value] of Object.entries(answers)) { + if (!allowedKeys.has(key)) continue; + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + content[key] = value; + } else if (Array.isArray(value)) { + content[key] = value.filter((entry): entry is string => typeof entry === "string"); + } + } + return content; +} + +function rawJsonRpcMessages(payload: unknown): ReadonlyArray> { + if (typeof payload !== "string") { + return []; + } + const messages: Array> = []; + for (const line of payload.split("\n")) { + const trimmed = line.trim(); + if (trimmed.length === 0) continue; + try { + const decoded: unknown = JSON.parse(trimmed); + if (Array.isArray(decoded)) { + for (const entry of decoded) { + const record = unknownRecord(entry); + if (record !== undefined) messages.push(record); + } + } else { + const record = unknownRecord(decoded); + if (record !== undefined) messages.push(record); + } + } catch { + messages.push({ raw: trimmed }); + } + } + return messages; +} + +interface ActiveTextSegment { + readonly nativeItemId: string; + readonly startedAt: DateTime.Utc; + text: string; +} + +interface ActiveTextStream { + current: ActiveTextSegment | null; + nextSegment: number; +} + +interface ActiveAcpTurn { + readonly input: ProviderAdapterV2TurnInput; + readonly providerTurnId: OrchestrationV2ProviderTurn["id"]; + readonly nativeTurnId: string; + readonly startedAt: DateTime.Utc; + readonly completed: Deferred.Deferred; + readonly assistant: ActiveTextStream; + readonly reasoning: ActiveTextStream; + readonly tools: Map; + readonly toolStartedAt: Map; + plan: { + readonly id: OrchestrationV2PlanArtifact["id"]; + readonly startedAt: DateTime.Utc; + } | null; + interrupted: boolean; + finalized: boolean; +} + +type PendingRuntimeRequest = { + readonly requestId: RuntimeRequestId; + readonly runtimeRequest: OrchestrationV2RuntimeRequest; + readonly node: OrchestrationV2ExecutionNode; + readonly turnItem: OrchestrationV2TurnItem; +} & ( + | { + readonly type: "approval"; + readonly decision: Deferred.Deferred; + } + | { + readonly type: "user_input"; + readonly answers: Deferred.Deferred; + } +); + +interface SnapshotMessageState { + readonly order: Array; + readonly messages: Map; + loadingRole: "user" | "assistant" | null; + loadingIndex: number; +} + +interface RawProtocolState { + readonly pendingMethods: Map; + sequence: number; +} + +function requestKey(direction: "incoming" | "outgoing", id: string | number): string { + return `${direction}:${String(id)}`; +} + +function oppositeDirection(direction: "incoming" | "outgoing"): "incoming" | "outgoing" { + return direction === "incoming" ? "outgoing" : "incoming"; +} + +export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV2Shape { + const { flavor, fileSystem, idAllocator, serverConfig } = options; + const driver = flavor.driver; + + return ProviderAdapterV2.of({ + instanceId: options.instanceId, + driver, + getCapabilities: () => Effect.succeed(flavor.capabilities), + openSession: Effect.fn("AcpAdapterV2.openSession")( + function* (input: ProviderAdapterV2OpenSessionInput) { + const sessionScope = yield* Effect.scope; + const events = yield* Queue.unbounded(); + const rawEvents = yield* Queue.unbounded(); + const rawState = yield* Ref.make({ + pendingMethods: new Map(), + sequence: 0, + }); + const activeTurn = yield* Ref.make(null); + const activeSessionId = yield* Ref.make(null); + const pendingRuntimeRequests = yield* Ref.make(new Map()); + const nextElicitationOrdinal = yield* Ref.make(0); + const itemOrdinals = yield* Ref.make(new Map()); + const nextItemOrdinalsByTurn = yield* Ref.make(new Map()); + const providerTurns = yield* Ref.make(new Map()); + const snapshot = yield* Ref.make({ + order: [], + messages: new Map(), + loadingRole: null, + loadingIndex: 0, + }); + + const emitProviderEvent = (event: ProviderAdapterV2Event) => + Queue.offer(events, event).pipe(Effect.asVoid); + + const protocolLogger = ( + event: EffectAcpProtocol.AcpProtocolLogEvent, + ): Effect.Effect => + Effect.gen(function* () { + if (event.stage !== "raw") return; + for (const message of rawJsonRpcMessages(event.payload)) { + const methodValue = message.method; + const id = jsonRpcId(message.id); + let method = typeof methodValue === "string" ? methodValue : null; + let messageKind: OrchestrationV2RawProviderEvent["messageKind"]; + if (method !== null) { + messageKind = id === null ? "notification" : "request"; + } else if ("error" in message) { + messageKind = "error"; + } else { + messageKind = "response"; + } + const state = yield* Ref.get(rawState); + if (method !== null && id !== null) { + state.pendingMethods.set(requestKey(event.direction, id), method); + } else if (id !== null) { + method = + state.pendingMethods.get(requestKey(oppositeDirection(event.direction), id)) ?? + null; + state.pendingMethods.delete(requestKey(oppositeDirection(event.direction), id)); + } + state.sequence += 1; + const now = yield* DateTime.now; + const rawEventId = yield* idAllocator.allocate.rawEvent({ + providerSessionId: input.providerSessionId, + method, + }); + yield* Queue.offer(rawEvents, { + id: rawEventId, + driver, + providerInstanceId: options.instanceId, + providerSessionId: input.providerSessionId, + sequence: state.sequence, + direction: event.direction, + messageKind, + method, + jsonRpcId: id, + payload: message, + observedAt: now, + }); + } + }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("orchestration-v2.acp-raw-event-failed", { + driver, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ); + + const runtime = yield* flavor.makeRuntime({ + cwd: input.runtimePolicy.cwd ?? process.cwd(), + mcpServers: acpMcpServers(input.threadId), + clientCapabilities: { + fs: { readTextFile: false, writeTextFile: false }, + terminal: false, + elicitation: { form: {} }, + }, + clientInfo: { name: "t3-code", version: "0.0.0" }, + protocolLogging: { + logIncoming: true, + logOutgoing: true, + logger: protocolLogger, + }, + }); + + const resolveItemOrdinal = Effect.fnUntraced(function* ( + context: ActiveAcpTurn, + nativeItemId: string, + ) { + const existing = (yield* Ref.get(itemOrdinals)).get(nativeItemId); + if (existing !== undefined) return existing; + const nextWithinTurn = yield* Ref.modify(nextItemOrdinalsByTurn, (current) => { + const next = (current.get(context.nativeTurnId) ?? 0) + 1; + const updated = new Map(current); + updated.set(context.nativeTurnId, next); + return [next, updated] as const; + }); + const ordinal = context.input.runOrdinal * 100 + nextWithinTurn; + yield* Ref.update(itemOrdinals, (current) => { + const updated = new Map(current); + updated.set(nativeItemId, ordinal); + return updated; + }); + return ordinal; + }); + + const rememberSnapshotMessage = (message: OrchestrationV2ConversationMessage) => + Ref.update(snapshot, (current) => { + const key = String(message.id); + const exists = current.messages.has(key); + const messages = new Map(current.messages); + messages.set(key, message); + return { + ...current, + order: exists ? current.order : [...current.order, key], + messages, + }; + }); + + const emitTextSegment = Effect.fnUntraced(function* ( + context: ActiveAcpTurn, + kind: "assistant" | "reasoning", + completed: boolean, + ) { + const stream = kind === "assistant" ? context.assistant : context.reasoning; + const segment = stream.current; + if (segment === null || segment.text.length === 0) return; + const now = yield* DateTime.now; + const ordinal = yield* resolveItemOrdinal(context, segment.nativeItemId); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver, + nativeItemId: segment.nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver, + nativeItemId: segment.nativeItemId, + }); + const nativeItemRef = { + driver, + nativeId: segment.nativeItemId, + strength: "weak" as const, + }; + yield* emitProviderEvent({ + type: "node.updated", + driver, + node: { + id: nodeId, + threadId: context.input.threadId, + runId: context.input.runId, + parentNodeId: context.input.rootNodeId, + rootNodeId: context.input.rootNodeId, + kind: kind === "assistant" ? "assistant_message" : "reasoning", + status: completed ? "completed" : "running", + countsForRun: false, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: segment.startedAt, + completedAt: completed ? now : null, + }, + }); + if (kind === "assistant") { + const messageId = idAllocator.derive.messageFromProviderItem({ + driver, + nativeItemId: segment.nativeItemId, + }); + const message: OrchestrationV2ConversationMessage = { + createdBy: "agent", + creationSource: "provider", + id: messageId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + role: "assistant", + text: segment.text, + attachments: [], + streaming: !completed, + createdAt: segment.startedAt, + updatedAt: now, + }; + yield* emitProviderEvent({ type: "message.updated", driver, message }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver, + turnItem: { + id: turnItemId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: completed ? "completed" : "running", + title: null, + startedAt: segment.startedAt, + completedAt: completed ? now : null, + updatedAt: now, + type: "assistant_message", + messageId, + text: segment.text, + streaming: !completed, + }, + }); + if (completed) yield* rememberSnapshotMessage(message); + return; + } + yield* emitProviderEvent({ + type: "turn_item.updated", + driver, + turnItem: { + id: turnItemId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: completed ? "completed" : "running", + title: null, + startedAt: segment.startedAt, + completedAt: completed ? now : null, + updatedAt: now, + type: "reasoning", + text: segment.text, + streaming: !completed, + }, + }); + }); + + const closeTextStream = Effect.fnUntraced(function* ( + context: ActiveAcpTurn, + kind: "assistant" | "reasoning", + ) { + const stream = kind === "assistant" ? context.assistant : context.reasoning; + if (stream.current === null) return; + yield* emitTextSegment(context, kind, true); + stream.current = null; + }); + + const closeTextStreams = Effect.fnUntraced(function* (context: ActiveAcpTurn) { + yield* closeTextStream(context, "reasoning"); + yield* closeTextStream(context, "assistant"); + }); + + const appendText = Effect.fnUntraced(function* ( + context: ActiveAcpTurn, + kind: "assistant" | "reasoning", + text: string, + ) { + if (text.length === 0) return; + const other = kind === "assistant" ? "reasoning" : "assistant"; + yield* closeTextStream(context, other); + const stream = kind === "assistant" ? context.assistant : context.reasoning; + if (stream.current === null) { + const now = yield* DateTime.now; + stream.current = { + nativeItemId: `${context.nativeTurnId}:${kind}:${stream.nextSegment}`, + startedAt: now, + text: "", + }; + stream.nextSegment += 1; + } + stream.current.text += text; + yield* emitTextSegment(context, kind, false); + }); + + const emitTool = Effect.fnUntraced(function* ( + context: ActiveAcpTurn, + incoming: AcpToolCallState, + ) { + yield* closeTextStreams(context); + const previous = context.tools.get(incoming.toolCallId); + const toolCall = mergeToolCallState(previous, incoming); + context.tools.set(toolCall.toolCallId, toolCall); + const status = toolStatus(toolCall.status); + const now = yield* DateTime.now; + const nativeItemId = `${nativeThreadId(driver, context.input.providerThread)}:tool:${toolCall.toolCallId}`; + const ordinal = yield* resolveItemOrdinal(context, nativeItemId); + const nodeId = idAllocator.derive.nodeFromProviderItem({ driver, nativeItemId }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver, + nativeItemId, + }); + const nativeItemRef = { + driver, + nativeId: toolCall.toolCallId, + strength: "strong" as const, + }; + const startedAt = context.toolStartedAt.get(toolCall.toolCallId) ?? now; + context.toolStartedAt.set(toolCall.toolCallId, startedAt); + const completedAt = completedAtForStatus(status, now); + const title = toolCall.title ?? null; + yield* emitProviderEvent({ + type: "node.updated", + driver, + node: { + id: nodeId, + threadId: context.input.threadId, + runId: context.input.runId, + parentNodeId: context.input.rootNodeId, + rootNodeId: context.input.rootNodeId, + kind: "tool_call", + status: nodeStatus(status), + countsForRun: true, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt, + completedAt, + }, + }); + + const base = { + id: turnItemId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status, + title, + startedAt, + completedAt, + updatedAt: now, + } as const; + const rawInput = toolCall.data.rawInput; + const rawOutput = toolCall.data.rawOutput ?? toolCall.data.content; + const path = pathFromToolCall(toolCall); + let turnItem: OrchestrationV2TurnItem; + switch (toolCall.kind) { + case "read": + case "search": + turnItem = { + ...base, + type: "file_search", + ...(path === undefined ? {} : { pattern: path }), + ...(path === undefined + ? {} + : { + results: [ + { + fileName: path, + ...(textFromUnknown(rawOutput) === undefined + ? {} + : { preview: textFromUnknown(rawOutput) }), + }, + ], + }), + }; + break; + case "execute": + turnItem = { + ...base, + type: "command_execution", + input: toolCall.command ?? toolCall.title ?? "Command", + ...(textFromUnknown(rawOutput) === undefined + ? {} + : { output: textFromUnknown(rawOutput) }), + ...(commandExitCode(rawOutput) === undefined + ? {} + : { exitCode: commandExitCode(rawOutput) }), + }; + break; + case "edit": + case "delete": + case "move": + turnItem = { + ...base, + type: "file_change", + fileName: path ?? toolCall.title ?? "File change", + ...(textFromUnknown(rawOutput) === undefined + ? {} + : { diffStr: textFromUnknown(rawOutput) }), + }; + break; + case "fetch": + turnItem = { + ...base, + type: "web_search", + ...(path === undefined ? {} : { patterns: [path] }), + ...(path === undefined + ? {} + : { + results: [ + { + url: path, + ...(textFromUnknown(rawOutput) === undefined + ? {} + : { snippet: textFromUnknown(rawOutput) }), + }, + ], + }), + }; + break; + default: + turnItem = { + ...base, + type: "dynamic_tool", + toolName: toolCall.title ?? toolCall.kind ?? null, + input: rawInput ?? {}, + ...(rawOutput === undefined ? {} : { output: rawOutput }), + }; + } + yield* emitProviderEvent({ type: "turn_item.updated", driver, turnItem }); + }); + + const emitPlan = Effect.fnUntraced(function* ( + context: ActiveAcpTurn, + update: AcpPlanUpdate, + ) { + yield* closeTextStreams(context); + const nativeItemId = `${context.nativeTurnId}:plan`; + const ordinal = yield* resolveItemOrdinal(context, nativeItemId); + const now = yield* DateTime.now; + const nodeId = idAllocator.derive.nodeFromProviderItem({ driver, nativeItemId }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver, + nativeItemId, + }); + if (context.plan === null) { + context.plan = { + id: yield* idAllocator.allocate.plan({ + threadId: context.input.threadId, + runId: context.input.runId, + driver, + }), + startedAt: now, + }; + } + const planId = context.plan.id; + const steps: ReadonlyArray = update.plan.map((step, index) => ({ + id: `acp-step-${index + 1}`, + text: nonEmptyText(step.step, `Step ${index + 1}`), + status: + step.status === "inProgress" + ? "running" + : step.status === "completed" + ? "completed" + : "pending", + })); + const completed = steps.length > 0 && steps.every((step) => step.status === "completed"); + const nativeItemRef = { driver, nativeId: nativeItemId, strength: "weak" as const }; + const plan: OrchestrationV2PlanArtifact = { + id: planId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + status: completed ? "completed" : "active", + kind: "todo_list", + steps, + ...(update.explanation == null ? {} : { explanation: update.explanation }), + }; + yield* emitProviderEvent({ + type: "node.updated", + driver, + node: { + id: nodeId, + threadId: context.input.threadId, + runId: context.input.runId, + parentNodeId: context.input.rootNodeId, + rootNodeId: context.input.rootNodeId, + kind: "todo_list", + status: completed ? "completed" : "running", + countsForRun: false, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: context.plan.startedAt, + completedAt: completed ? now : null, + }, + }); + yield* emitProviderEvent({ type: "plan.updated", driver, plan }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver, + turnItem: { + id: turnItemId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: completed ? "completed" : "running", + title: null, + startedAt: context.plan.startedAt, + completedAt: completed ? now : null, + updatedAt: now, + type: "todo_list", + planId, + steps, + ...(update.explanation == null ? {} : { explanation: update.explanation }), + }, + }); + }); + + const appendLoadedHistory = ( + notification: EffectAcpSchema.SessionNotification, + role: "user" | "assistant", + text: string, + ) => + Effect.gen(function* () { + if (text.length === 0) return; + const now = yield* DateTime.now; + yield* Ref.update(snapshot, (current) => { + const startsNew = current.loadingRole !== role; + const loadingIndex = startsNew ? current.loadingIndex + 1 : current.loadingIndex; + const nativeItemId = `${notification.sessionId}:history:${role}:${loadingIndex}`; + const messageId = idAllocator.derive.messageFromProviderItem({ + driver, + nativeItemId, + }); + const key = String(messageId); + const previous = current.messages.get(key); + const messages = new Map(current.messages); + messages.set(key, { + createdBy: previous?.createdBy ?? (role === "user" ? "user" : "agent"), + creationSource: previous?.creationSource ?? "provider", + id: messageId, + threadId: input.threadId, + runId: null, + nodeId: null, + role, + text: `${previous?.text ?? ""}${text}`, + attachments: [], + streaming: false, + createdAt: previous?.createdAt ?? now, + updatedAt: now, + }); + return { + order: current.order.includes(key) ? current.order : [...current.order, key], + messages, + loadingRole: role, + loadingIndex, + }; + }); + }); + + const handleSessionUpdate = Effect.fnUntraced(function* ( + notification: EffectAcpSchema.SessionNotification, + ) { + const context = yield* Ref.get(activeTurn); + const update = notification.update; + if (context === null) { + if ( + (update.sessionUpdate === "user_message_chunk" || + update.sessionUpdate === "agent_message_chunk") && + update.content.type === "text" + ) { + yield* appendLoadedHistory( + notification, + update.sessionUpdate === "user_message_chunk" ? "user" : "assistant", + update.content.text, + ); + } else if ( + update.sessionUpdate === "tool_call" || + update.sessionUpdate === "tool_call_update" || + update.sessionUpdate === "plan" + ) { + yield* Ref.update(snapshot, (current) => ({ ...current, loadingRole: null })); + } + return; + } + if (context.finalized) return; + if (notification.sessionId !== (yield* Ref.get(activeSessionId))) return; + switch (update.sessionUpdate) { + case "agent_message_chunk": + if (update.content.type === "text") { + yield* appendText(context, "assistant", update.content.text); + } + return; + case "agent_thought_chunk": + if (update.content.type === "text") { + yield* appendText(context, "reasoning", update.content.text); + } + return; + default: { + const parsed = parseSessionUpdateEvent(notification); + for (const event of parsed.events) { + if (event._tag === "ToolCallUpdated") { + yield* emitTool(context, event.toolCall); + } else if (event._tag === "PlanUpdated") { + yield* emitPlan(context, event.payload); + } + } + } + } + }); + + yield* runtime.handleSessionUpdate((notification) => + handleSessionUpdate(notification).pipe( + Effect.mapError( + (cause) => + new EffectAcpErrors.AcpTransportError({ + detail: "Failed to project an ACP session update", + cause, + }), + ), + ), + ); + + const activeContext = Effect.gen(function* () { + const context = yield* Ref.get(activeTurn); + if (context === null) { + return yield* new EffectAcpErrors.AcpTransportError({ + detail: "ACP agent requested input without an active turn", + cause: "No active ACP turn", + }); + } + return context; + }); + + const emitApprovalRequest = Effect.fnUntraced(function* ( + context: ActiveAcpTurn, + params: EffectAcpSchema.RequestPermissionRequest, + ) { + yield* closeTextStreams(context); + const parsed = parsePermissionRequest(params); + const nativeRequestId = params.toolCall.toolCallId; + const requestId = yield* idAllocator.allocate.runtimeRequest({ + driver, + providerTurnId: context.providerTurnId, + nativeRequestId, + }); + const decision = yield* Deferred.make(); + const now = yield* DateTime.now; + const nodeId = idAllocator.derive.approvalNode({ requestId }); + const requestKind = providerRequestKind(parsed.kind); + const nativeItemRef = { driver, nativeId: nativeRequestId, strength: "weak" as const }; + const ordinal = yield* resolveItemOrdinal( + context, + `${context.nativeTurnId}:approval:${nativeRequestId}`, + ); + const runtimeRequest: OrchestrationV2RuntimeRequest = { + id: requestId, + nodeId, + providerTurnId: context.providerTurnId, + nativeRequestRef: nativeItemRef, + kind: requestKind, + status: "pending", + responseCapability: { + type: "live", + providerSessionId: input.providerSessionId, + }, + createdAt: now, + resolvedAt: null, + }; + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: context.input.threadId, + runId: context.input.runId, + parentNodeId: context.input.rootNodeId, + rootNodeId: context.input.rootNodeId, + kind: "approval_request", + status: "waiting", + countsForRun: false, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + runtimeRequestId: requestId, + checkpointScopeId: null, + startedAt: now, + completedAt: null, + }; + const turnItem: OrchestrationV2TurnItem = { + id: idAllocator.derive.approvalTurnItem({ requestId }), + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: "waiting", + title: null, + startedAt: now, + completedAt: null, + updatedAt: now, + type: "approval_request", + requestId, + requestKind, + ...(parsed.detail === undefined ? {} : { prompt: parsed.detail }), + }; + yield* Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.set(String(requestId), { + type: "approval", + requestId, + decision, + runtimeRequest, + node, + turnItem, + }); + return updated; + }); + yield* emitProviderEvent({ + type: "node.updated", + driver, + node, + }); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver, + threadId: context.input.threadId, + runtimeRequest, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver, + turnItem, + }); + const resolved = yield* Deferred.await(decision).pipe( + Effect.ensuring( + Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.delete(String(requestId)); + return updated; + }), + ), + ); + return resolved; + }); + + yield* runtime.handleRequestPermission((params) => + Effect.gen(function* () { + const context = yield* activeContext; + const disposition = acpPermissionDisposition(context.input.runtimePolicy, params); + if (disposition === "allow") { + const optionId = selectAutoApprovedPermissionOption(params); + return optionId === undefined + ? ({ outcome: { outcome: "cancelled" } } as const) + : ({ outcome: { outcome: "selected", optionId } } as const); + } + if (disposition === "deny") { + const optionId = selectPermissionOptionId(params, "decline"); + return optionId === undefined + ? ({ outcome: { outcome: "cancelled" } } as const) + : ({ outcome: { outcome: "selected", optionId } } as const); + } + const decision = yield* emitApprovalRequest(context, params); + if (decision === "cancel") { + return { outcome: { outcome: "cancelled" } } as const; + } + const optionId = selectPermissionOptionId(params, decision); + return optionId === undefined + ? ({ outcome: { outcome: "cancelled" } } as const) + : ({ outcome: { outcome: "selected", optionId } } as const); + }).pipe( + Effect.mapError( + (cause) => + new EffectAcpErrors.AcpTransportError({ + detail: "Failed to handle an ACP permission request", + cause, + }), + ), + ), + ); + + const requestUserInputInternal = Effect.fnUntraced(function* ( + request: AcpAdapterV2UserInputRequest, + ) { + const context = yield* activeContext; + yield* closeTextStreams(context); + const requestId = yield* idAllocator.allocate.runtimeRequest({ + driver, + providerTurnId: context.providerTurnId, + nativeRequestId: request.nativeRequestId, + }); + const answers = yield* Deferred.make(); + const now = yield* DateTime.now; + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver, + nativeItemId: request.nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver, + nativeItemId: request.nativeItemId, + }); + const nativeItemRef = { + driver, + nativeId: request.nativeItemId, + strength: "weak" as const, + }; + const ordinal = yield* resolveItemOrdinal(context, request.nativeItemId); + const runtimeRequest: OrchestrationV2RuntimeRequest = { + id: requestId, + nodeId, + providerTurnId: context.providerTurnId, + nativeRequestRef: { + driver, + nativeId: request.nativeRequestId, + strength: "weak", + }, + kind: "user_input", + status: "pending", + responseCapability: { + type: "live", + providerSessionId: input.providerSessionId, + }, + createdAt: now, + resolvedAt: null, + }; + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: context.input.threadId, + runId: context.input.runId, + parentNodeId: context.input.rootNodeId, + rootNodeId: context.input.rootNodeId, + kind: "user_input_request", + status: "waiting", + countsForRun: false, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + runtimeRequestId: requestId, + checkpointScopeId: null, + startedAt: now, + completedAt: null, + }; + const turnItem: OrchestrationV2TurnItem = { + id: turnItemId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: "waiting", + title: null, + startedAt: now, + completedAt: null, + updatedAt: now, + type: "user_input_request", + requestId, + questions: [...request.questions], + }; + yield* Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.set(String(requestId), { + type: "user_input", + requestId, + answers, + runtimeRequest, + node, + turnItem, + }); + return updated; + }); + yield* emitProviderEvent({ + type: "node.updated", + driver, + node, + }); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver, + threadId: context.input.threadId, + runtimeRequest, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver, + turnItem, + }); + return yield* Deferred.await(answers).pipe( + Effect.ensuring( + Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.delete(String(requestId)); + return updated; + }), + ), + ); + }); + + const requestUserInput = (request: AcpAdapterV2UserInputRequest) => + requestUserInputInternal(request).pipe( + Effect.mapError( + (cause) => + new EffectAcpErrors.AcpTransportError({ + detail: "Failed to handle ACP user input request", + cause, + }), + ), + ); + + const cancelPendingRuntimeRequests = Effect.fnUntraced(function* () { + const requests = [...(yield* Ref.get(pendingRuntimeRequests)).values()]; + if (requests.length === 0) return; + + const now = yield* DateTime.now; + yield* Effect.forEach( + requests, + (request) => + Effect.gen(function* () { + const cancelled = yield* request.type === "approval" + ? Deferred.succeed(request.decision, "cancel") + : Deferred.succeed(request.answers, null); + if (!cancelled) return; + + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver, + threadId: request.node.threadId, + runtimeRequest: { + ...request.runtimeRequest, + status: "cancelled", + resolvedAt: now, + }, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver, + node: { + ...request.node, + status: "cancelled", + completedAt: now, + }, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver, + turnItem: { + ...request.turnItem, + status: "cancelled", + completedAt: now, + updatedAt: now, + }, + }); + }), + { concurrency: 1, discard: true }, + ); + }); + + yield* runtime.handleElicitation((params) => + Effect.gen(function* () { + if (params.mode === "url") { + return { action: { action: "decline" } } as const; + } + const questions = Object.entries(params.requestedSchema.properties ?? {}).map( + ([id, property], index): OrchestrationV2UserInputQuestion => { + const record = unknownRecord(property); + const enumValues = Array.isArray(record?.enum) + ? record.enum.filter((value): value is string => typeof value === "string") + : []; + const options = + enumValues.length > 0 + ? enumValues.map((value) => ({ label: value, description: value })) + : record?.type === "boolean" + ? [ + { label: "true", description: "Yes" }, + { label: "false", description: "No" }, + ] + : []; + return { + id, + header: nonEmptyText(record?.title, `Question ${index + 1}`), + question: nonEmptyText(record?.description, params.message), + options, + }; + }, + ); + const ordinal = yield* Ref.getAndUpdate( + nextElicitationOrdinal, + (current) => current + 1, + ); + const nativeRequestId = `${params.sessionId}:elicitation:${ordinal}`; + const answers = yield* requestUserInput({ + nativeItemId: nativeRequestId, + nativeRequestId, + questions, + }); + return answers === null + ? ({ action: { action: "cancel" } } as const) + : ({ + action: { + action: "accept", + content: elicitationContent( + answers, + new Set(Object.keys(params.requestedSchema.properties ?? {})), + ), + }, + } as const); + }), + ); + + if (flavor.registerExtensions !== undefined) { + yield* flavor.registerExtensions({ runtime, requestUserInput }); + } + + const started = yield* runtime.start(); + yield* Ref.set(activeSessionId, started.sessionId); + const capabilities = negotiatedCapabilities(flavor.capabilities, started); + const canLoadSession = started.initializeResult.agentCapabilities?.loadSession === true; + const canResumeSession = + started.initializeResult.agentCapabilities?.sessionCapabilities?.resume != null; + const supportsImagePrompts = + started.initializeResult.agentCapabilities?.promptCapabilities?.image === true; + + const activateSession = Effect.fnUntraced(function* (sessionId: string) { + if (canLoadSession) { + return yield* runtime.loadSession(sessionId); + } + if (canResumeSession) { + return yield* runtime.resumeSession(sessionId); + } + return yield* new ProviderAdapterProtocolError({ + driver, + detail: `ACP driver cannot load or resume session ${sessionId}`, + }); + }); + + const configureSession = Effect.fnUntraced(function* ( + startResult: AcpSessionRuntimeStartResult, + modelSelection: ModelSelection, + runtimePolicy: ProviderAdapterV2RuntimePolicy, + ) { + const requestedModel = flavor.resolveModelId?.(modelSelection) ?? modelSelection.model; + if ( + requestedModel.length > 0 && + requestedModel !== "auto" && + requestedModel !== "default" + ) { + const currentModel = startResult.sessionSetupResult.models?.currentModelId; + if (currentModel !== requestedModel) { + if (startResult.sessionSetupResult.models != null) { + yield* runtime.setSessionModel(requestedModel); + } else if ( + startResult.sessionSetupResult.configOptions?.some( + (option) => option.category === "model", + ) === true + ) { + yield* runtime.setModel(requestedModel); + } + } + } + const configOptions = yield* runtime.getConfigOptions; + for (const selection of modelSelection.options ?? []) { + if (configOptions.some((option) => option.id === selection.id)) { + yield* runtime.setConfigOption(selection.id, selection.value); + } + } + const modeState = yield* runtime.getModeState; + if (runtimePolicy.interactionMode === "plan" && modeState !== undefined) { + const planMode = modeState.availableModes.find( + (mode) => mode.id === "plan" || mode.id === "architect", + ); + if (planMode !== undefined) yield* runtime.setMode(planMode.id); + } + }); + + yield* configureSession(started, input.modelSelection, input.runtimePolicy); + const createdAt = yield* DateTime.now; + const providerSession: OrchestrationV2ProviderSession = { + id: input.providerSessionId, + driver, + providerInstanceId: options.instanceId, + status: "ready", + cwd: input.runtimePolicy.cwd ?? process.cwd(), + model: input.modelSelection.model, + capabilities, + createdAt, + updatedAt: createdAt, + lastError: null, + }; + + const providerTurnPayload = ( + context: ActiveAcpTurn, + status: OrchestrationV2ProviderTurn["status"], + completedAt: DateTime.Utc | null, + ): OrchestrationV2ProviderTurn => ({ + id: context.providerTurnId, + providerThreadId: context.input.providerThread.id, + nodeId: context.input.rootNodeId, + runAttemptId: context.input.attemptId, + nativeTurnRef: { + driver, + nativeId: context.nativeTurnId, + strength: "weak", + }, + ordinal: context.input.providerTurnOrdinal, + status, + startedAt: context.startedAt, + completedAt, + }); + + const finalizeTurn = Effect.fnUntraced(function* ( + context: ActiveAcpTurn, + status: "completed" | "interrupted" | "failed" | "cancelled", + ) { + if (context.finalized) return; + context.finalized = true; + yield* closeTextStreams(context); + const now = yield* DateTime.now; + const turn = providerTurnPayload(context, status, now); + yield* Ref.update(providerTurns, (current) => { + const updated = new Map(current); + updated.set(String(turn.id), turn); + return updated; + }); + yield* emitProviderEvent({ + type: "provider_turn.updated", + driver, + threadId: context.input.threadId, + providerTurn: turn, + }); + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver, + providerThread: { + ...context.input.providerThread, + providerSessionId: input.providerSessionId, + status: "idle", + lastRunOrdinal: context.input.runOrdinal, + firstRunOrdinal: + context.input.providerThread.firstRunOrdinal ?? context.input.runOrdinal, + updatedAt: now, + }, + }); + yield* emitProviderEvent({ + type: "turn.terminal", + driver, + providerTurnId: context.providerTurnId, + status, + }); + yield* Ref.set(activeTurn, null); + yield* Deferred.succeed(context.completed, undefined).pipe(Effect.ignore); + }); + + const resolvePromptParts = Effect.fnUntraced(function* ( + turnInput: ProviderAdapterV2TurnInput, + ) { + const prompt: Array = []; + if (turnInput.message.text.length > 0) { + prompt.push({ type: "text", text: turnInput.message.text }); + } + if (turnInput.message.attachments.length > 0 && !supportsImagePrompts) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: "ACP driver did not negotiate image prompt support", + }); + } + for (const attachment of turnInput.message.attachments) { + const path = resolveAttachmentPath({ + attachmentsDir: serverConfig.attachmentsDir, + attachment: attachment as ChatAttachment, + }); + if (path === null) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: `Invalid attachment id '${attachment.id}'`, + }); + } + const bytes = yield* fileSystem.readFile(path).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterProtocolError({ + driver, + detail: `Failed to read attachment '${attachment.id}'`, + payload: cause, + }), + ), + ); + prompt.push({ + type: "image", + data: Buffer.from(bytes).toString("base64"), + mimeType: attachment.mimeType, + }); + } + if (prompt.length === 0) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: "ACP turn requires non-empty text or attachments", + }); + } + return prompt; + }); + + const startTurn = Effect.fn("AcpAdapterV2.startTurn")( + function* (turnInput: ProviderAdapterV2TurnInput) { + const existing = yield* Ref.get(activeTurn); + if (existing !== null) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: `ACP provider turn ${existing.providerTurnId} is still active`, + }); + } + const requestedSessionId = nativeThreadId(driver, turnInput.providerThread); + if ((yield* Ref.get(activeSessionId)) !== requestedSessionId) { + const activated = yield* activateSession(requestedSessionId); + yield* Ref.set(activeSessionId, activated.sessionId); + yield* configureSession(activated, turnInput.modelSelection, turnInput.runtimePolicy); + } + const prompt = yield* resolvePromptParts(turnInput); + const startedAt = yield* DateTime.now; + const nativeTurnId = `${requestedSessionId}:turn:${turnInput.providerTurnOrdinal}`; + const providerTurnId = idAllocator.derive.providerTurn({ driver, nativeTurnId }); + const completed = yield* Deferred.make(); + const context: ActiveAcpTurn = { + input: turnInput, + providerTurnId, + nativeTurnId, + startedAt, + completed, + assistant: { current: null, nextSegment: 0 }, + reasoning: { current: null, nextSegment: 0 }, + tools: new Map(), + toolStartedAt: new Map(), + plan: null, + interrupted: false, + finalized: false, + }; + yield* Ref.set(activeTurn, context); + const runningTurn = providerTurnPayload(context, "running", null); + yield* Ref.update(providerTurns, (current) => { + const updated = new Map(current); + updated.set(String(runningTurn.id), runningTurn); + return updated; + }); + yield* emitProviderEvent({ + type: "provider_turn.updated", + driver, + threadId: turnInput.threadId, + providerTurn: runningTurn, + }); + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver, + providerThread: { + ...turnInput.providerThread, + providerSessionId: input.providerSessionId, + status: "active", + updatedAt: startedAt, + }, + }); + yield* rememberSnapshotMessage({ + createdBy: turnInput.message.createdBy, + creationSource: turnInput.message.creationSource, + id: turnInput.message.messageId, + threadId: turnInput.threadId, + runId: turnInput.runId, + nodeId: turnInput.rootNodeId, + role: "user", + text: turnInput.message.text, + attachments: [...turnInput.message.attachments], + streaming: false, + createdAt: startedAt, + updatedAt: startedAt, + }); + yield* runtime.prompt({ prompt }).pipe( + Effect.flatMap((result) => { + const status = + result.stopReason === "cancelled" + ? context.interrupted + ? "interrupted" + : "cancelled" + : "completed"; + return finalizeTurn(context, status); + }), + Effect.catchCause((cause) => + finalizeTurn(context, context.interrupted ? "interrupted" : "failed").pipe( + Effect.andThen( + Effect.logWarning("orchestration-v2.acp-prompt-failed", { + driver, + providerSessionId: input.providerSessionId, + providerThreadId: turnInput.providerThread.id, + providerTurnId, + cause, + }), + ), + ), + ), + Effect.forkIn(sessionScope), + ); + }, + (effect, turnInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterTurnStartError({ + driver, + threadId: turnInput.threadId, + providerThreadId: turnInput.providerThread.id, + runId: turnInput.runId, + cause, + }), + ), + ), + ); + + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + const requests = [...(yield* Ref.get(pendingRuntimeRequests)).values()]; + yield* Effect.forEach( + requests, + (request) => + request.type === "approval" + ? Deferred.succeed(request.decision, "cancel").pipe(Effect.ignore) + : Deferred.succeed(request.answers, null).pipe(Effect.ignore), + { discard: true }, + ); + const sessionCapabilities = + started.initializeResult.agentCapabilities?.sessionCapabilities; + if (sessionCapabilities?.close != null) { + yield* runtime.closeSession().pipe(Effect.ignore); + } + if (flavor.assertComplete !== undefined) { + yield* flavor.assertComplete.pipe(Effect.orDie); + } + }), + ); + + const sessionRuntime: ProviderAdapterV2SessionRuntime = { + instanceId: options.instanceId, + driver, + providerSessionId: input.providerSessionId, + providerSession, + rawEvents: Stream.fromEffectRepeat(Queue.take(rawEvents)), + events: Stream.fromEffectRepeat(Queue.take(events)), + ensureThread: Effect.fn("AcpAdapterV2.ensureThread")( + function* (threadInput: ProviderAdapterV2EnsureThreadInput) { + const now = yield* DateTime.now; + const sessionId = yield* Ref.get(activeSessionId); + if (sessionId === null) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: "ACP runtime did not produce a session id", + }); + } + return makeProviderThread({ + driver, + providerInstanceId: options.instanceId, + idAllocator, + appThreadId: threadInput.threadId, + providerSessionId: input.providerSessionId, + nativeThreadId: sessionId, + now, + }); + }, + (effect, threadInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterEnsureThreadError({ + driver, + threadId: threadInput.threadId, + cause, + }), + ), + ), + ), + resumeThread: Effect.fn("AcpAdapterV2.resumeThread")( + function* (threadInput: { readonly providerThread: OrchestrationV2ProviderThread }) { + const sessionId = nativeThreadId(driver, threadInput.providerThread); + if ((yield* Ref.get(activeSessionId)) !== sessionId) { + yield* Ref.set(snapshot, { + order: [], + messages: new Map(), + loadingRole: null, + loadingIndex: 0, + }); + const activated = yield* activateSession(sessionId); + yield* Ref.set(activeSessionId, activated.sessionId); + yield* configureSession(activated, input.modelSelection, input.runtimePolicy); + } + const now = yield* DateTime.now; + return { + ...threadInput.providerThread, + providerSessionId: input.providerSessionId, + status: "idle" as const, + updatedAt: now, + }; + }, + (effect, threadInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterResumeThreadError({ + driver, + providerSessionId: input.providerSessionId, + providerThreadId: threadInput.providerThread.id, + cause, + }), + ), + ), + ), + startTurn, + steerTurn: (turnInput) => + Effect.fail( + new ProviderAdapterSteerRunUnsupportedError({ + driver, + providerThreadId: turnInput.providerThread.id, + }), + ), + interruptTurn: Effect.fn("AcpAdapterV2.interruptTurn")( + function* (turnInput: ProviderAdapterV2InterruptInput) { + const context = yield* Ref.get(activeTurn); + if (context?.providerTurnId !== turnInput.providerTurnId) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: `ACP provider turn ${turnInput.providerTurnId} is not active`, + }); + } + context.interrupted = true; + yield* runtime.cancel.pipe(Effect.ensuring(cancelPendingRuntimeRequests())); + const stopped = yield* Deferred.await(context.completed).pipe( + Effect.timeoutOption("10 seconds"), + ); + if (Option.isNone(stopped)) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: `ACP provider turn ${turnInput.providerTurnId} did not acknowledge cancellation before the interrupt timeout`, + }); + } + }, + (effect, turnInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterInterruptError({ + driver, + providerThreadId: turnInput.providerThread.id, + providerTurnId: turnInput.providerTurnId, + cause, + }), + ), + ), + ), + respondToRuntimeRequest: (requestInput) => + Effect.gen(function* () { + const pending = (yield* Ref.get(pendingRuntimeRequests)).get( + String(requestInput.requestId), + ); + if (pending === undefined) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: `No pending ACP runtime request ${requestInput.requestId}`, + }); + } + if (pending.type === "user_input") { + yield* Deferred.succeed(pending.answers, requestInput.answers ?? null); + return; + } + if (requestInput.decision === undefined) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: `ACP approval request ${requestInput.requestId} requires a decision`, + }); + } + yield* Deferred.succeed(pending.decision, requestInput.decision); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterRuntimeRequestResponseError({ + driver, + requestId: requestInput.requestId, + cause, + }), + ), + ), + readThreadSnapshot: Effect.fn("AcpAdapterV2.readThreadSnapshot")( + function* (snapshotInput) { + const sessionId = nativeThreadId(driver, snapshotInput.providerThread); + if ((yield* Ref.get(activeSessionId)) !== sessionId) { + if (!capabilities.threads.canReadThreadSnapshot) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: "ACP driver does not support session/load snapshots", + }); + } + yield* Ref.set(snapshot, { + order: [], + messages: new Map(), + loadingRole: null, + loadingIndex: 0, + }); + const activated = yield* runtime.loadSession(sessionId); + yield* Ref.set(activeSessionId, activated.sessionId); + } + const state = yield* Ref.get(snapshot); + const now = yield* DateTime.now; + return { + providerThread: { + ...snapshotInput.providerThread, + providerSessionId: input.providerSessionId, + status: "idle" as const, + updatedAt: now, + }, + providerTurns: [...(yield* Ref.get(providerTurns)).values()], + messages: state.order.flatMap((key) => { + const message = state.messages.get(key); + return message === undefined ? [] : [message]; + }), + runtimeRequests: [], + providerPayload: { protocol: ACP_PROTOCOL, sessionId }, + }; + }, + (effect, snapshotInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterReadThreadSnapshotError({ + driver, + providerThreadId: snapshotInput.providerThread.id, + cause, + }), + ), + ), + ), + rollbackThread: (rollbackInput) => + Effect.fail( + new ProviderAdapterRollbackThreadError({ + driver, + providerThreadId: rollbackInput.providerThread.id, + checkpointId: rollbackInput.target.checkpointId, + cause: "ACP does not define conversation rollback.", + }), + ), + forkThread: Effect.fn("AcpAdapterV2.forkThread")( + function* (forkInput) { + if (!capabilities.threads.canForkThread) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: "ACP driver did not negotiate session/fork", + }); + } + if (forkInput.providerTurnId !== undefined) { + return yield* new ProviderAdapterProtocolError({ + driver, + detail: "ACP session/fork can only fork the current session head", + }); + } + const sourceSessionId = nativeThreadId(driver, forkInput.sourceProviderThread); + const forked = yield* runtime.forkSession(sourceSessionId); + yield* Ref.set(activeSessionId, forked.sessionId); + const now = yield* DateTime.now; + return makeProviderThread({ + driver, + providerInstanceId: options.instanceId, + idAllocator, + appThreadId: forkInput.targetThreadId, + providerSessionId: input.providerSessionId, + nativeThreadId: forked.sessionId, + ...(forkInput.ownerNodeId === undefined + ? {} + : { ownerNodeId: forkInput.ownerNodeId }), + forkedFrom: { + providerThreadId: forkInput.sourceProviderThread.id, + ...(forkInput.providerTurnId === undefined + ? {} + : { providerTurnId: forkInput.providerTurnId }), + }, + now, + }); + }, + (effect, forkInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterForkThreadError({ + driver, + providerThreadId: forkInput.sourceProviderThread.id, + cause, + }), + ), + ), + ), + }; + return sessionRuntime; + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterOpenSessionError({ + driver, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ), + ), + }); +} + +export type AcpAdapterV2Env = FileSystem.FileSystem | IdAllocatorV2; diff --git a/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts new file mode 100644 index 00000000000..acb2a48d575 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts @@ -0,0 +1,157 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import { ProviderInstanceId, ProviderSessionId, ThreadId } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import { HttpClient, HttpClientResponse } from "effect/unstable/http"; +import { ChildProcessSpawner } from "effect/unstable/process"; + +import { ServerConfig } from "../../config.ts"; +import { makeAcpRegistryResolver } from "../../provider/acp/AcpRegistrySupport.ts"; +import { layer as idAllocatorLayer, IdAllocatorV2 } from "../IdAllocator.ts"; +import { ProviderAdapterV2RuntimePolicy } from "../ProviderAdapter.ts"; +import { BUILT_IN_PROVIDER_ADAPTER_DRIVER_KINDS_V2 } from "../builtInProviderAdapterDrivers.ts"; +import { + ACP_REGISTRY_DRIVER_KIND, + AcpRegistryAdapterV2Driver, + makeAcpRegistryAdapterV2, +} from "./AcpRegistryAdapterV2.ts"; + +const registryUrl = "https://registry.test/registry.json"; +const decodeAcpRegistryAdapterSettings = Schema.decodeUnknownEffect( + AcpRegistryAdapterV2Driver.configSchema, +); + +const serverConfigLayer = ServerConfig.layerTest(process.cwd(), { + prefix: "t3-acp-registry-v2-adapter-", +}).pipe(Layer.provide(NodeServices.layer)); + +const registryLayer = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + Response.json({ + version: "1.0.0", + agents: [ + { + id: "fixture-agent", + name: "Fixture Agent", + version: "1.0.0", + description: "ACP V2 adapter fixture", + distribution: { + binary: { + "darwin-aarch64": { + archive: "https://registry.test/unused", + cmd: "fixture-agent", + args: [], + }, + "linux-x86_64": { + archive: "https://registry.test/unused", + cmd: "fixture-agent", + args: [], + }, + }, + }, + }, + ], + }), + ), + ), + ), +); + +const testLayer = Layer.mergeAll( + NodeServices.layer, + idAllocatorLayer, + serverConfigLayer, + registryLayer, +); + +describe("AcpRegistryAdapterV2", () => { + it("is registered as a generic provider driver with schema defaults", () => { + assert.isTrue(BUILT_IN_PROVIDER_ADAPTER_DRIVER_KINDS_V2.has(ACP_REGISTRY_DRIVER_KIND)); + assert.equal(AcpRegistryAdapterV2Driver.driverKind, ACP_REGISTRY_DRIVER_KIND); + assert.deepEqual(AcpRegistryAdapterV2Driver.defaultConfig(), { + enabled: true, + agentId: "", + commandPath: "", + authMethodId: "", + distribution: "auto", + customModels: [], + }); + }); + + it.effect("opens a real ACP child process resolved from registry configuration", () => + Effect.gen(function* () { + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fileSystem = yield* FileSystem.FileSystem; + const idAllocator = yield* IdAllocatorV2; + const path = yield* Path.Path; + const serverConfig = yield* ServerConfig; + const mockAgentPath = yield* path.fromFileUrl( + new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), + ); + const resolver = yield* makeAcpRegistryResolver({ + cacheDir: serverConfig.providerStatusCacheDir, + registryUrl, + }); + const settings = yield* decodeAcpRegistryAdapterSettings({ + agentId: "fixture-agent", + commandPath: process.execPath, + authMethodId: "test", + }); + const instanceId = ProviderInstanceId.make("acp-registry-fixture"); + const adapter = makeAcpRegistryAdapterV2({ + instanceId, + settings, + environment: { + T3_ACP_SESSION_LIFECYCLE: "1", + }, + childProcessSpawner, + fileSystem, + idAllocator, + resolver: { + resolve: (configuredSettings, cwd, environment) => + resolver.resolve(configuredSettings, cwd, environment).pipe( + Effect.map((resolved) => ({ + ...resolved, + spawn: { + ...resolved.spawn, + args: [mockAgentPath], + }, + })), + ), + }, + serverConfig, + }); + const threadId = ThreadId.make("thread-acp-registry-fixture"); + const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: process.cwd(), + }); + const modelSelection = { instanceId, model: "default" } as const; + const runtime = yield* adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("provider-session-acp-registry-fixture"), + modelSelection, + runtimePolicy, + }); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection, + runtimePolicy, + }); + + assert.equal(runtime.providerSession.driver, "acpRegistry"); + assert.equal(providerThread.nativeThreadRef?.nativeId, "mock-session-1"); + assert.isTrue(runtime.providerSession.capabilities.threads.canReadThreadSnapshot); + assert.isTrue(runtime.providerSession.capabilities.threads.canForkThread); + }).pipe(Effect.provide(testLayer), Effect.scoped), + ); +}); diff --git a/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.testkit.ts new file mode 100644 index 00000000000..93cbc7f93f3 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.testkit.ts @@ -0,0 +1,89 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { AcpRegistrySettings } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import { ChildProcessSpawner } from "effect/unstable/process"; + +import { ServerConfig } from "../../config.ts"; +import { layer as idAllocatorLayer, IdAllocatorV2 } from "../IdAllocator.ts"; +import { makeLayerEffect as makeProviderAdapterRegistryLayerEffect } from "../ProviderAdapterRegistry.ts"; +import type { OrchestratorV2ProviderReplayHarness } from "../testkit/ProviderReplayHarness.ts"; +import { makeReplayServerConfig } from "../testkit/ProviderReplayHarness.ts"; +import { + type AcpReplayTranscript, + AcpReplayTranscriptDecodeError, + decodeAcpReplayTranscript, + makeAcpReplayCompletenessAssertion, + makeAcpReplayRuntime, +} from "./AcpAdapterV2.testkit.ts"; +import { + ACP_REGISTRY_DEFAULT_INSTANCE_ID, + ACP_REGISTRY_PROVIDER, + makeAcpRegistryAdapterV2, +} from "./AcpRegistryAdapterV2.ts"; + +const REPLAY_SETTINGS = Schema.decodeUnknownSync(AcpRegistrySettings)({ + agentId: "replay-agent", + authMethodId: "replay", +}); + +export function makeAcpRegistryProviderAdapterRegistryReplayLayer(transcript: AcpReplayTranscript) { + const serverConfigLayer = Layer.effect( + ServerConfig, + makeReplayServerConfig(`acp-registry-${transcript.scenario}`).pipe(Effect.orDie), + ).pipe(Layer.provide(NodeServices.layer)); + + return makeProviderAdapterRegistryLayerEffect( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const idAllocator = yield* IdAllocatorV2; + const serverConfig = yield* ServerConfig; + const replayDir = yield* fileSystem + .makeTempDirectory({ + prefix: `t3-orchestration-v2-acp-registry-replay-${transcript.scenario}-`, + }) + .pipe(Effect.orDie); + const statusPath = path.join(replayDir, "status.json"); + const scriptPath = yield* path + .fromFileUrl(new URL("../../../scripts/acp-replay-agent.ts", import.meta.url)) + .pipe(Effect.orDie); + const adapter = makeAcpRegistryAdapterV2({ + instanceId: ACP_REGISTRY_DEFAULT_INSTANCE_ID, + settings: REPLAY_SETTINGS, + environment: {}, + childProcessSpawner, + fileSystem, + idAllocator, + resolver: { + resolve: () => Effect.die("ACP registry resolver must not run during replay"), + }, + serverConfig, + makeRuntime: makeAcpReplayRuntime({ + transcript, + statusPath, + scriptPath, + childProcessSpawner, + }), + assertComplete: makeAcpReplayCompletenessAssertion(fileSystem, statusPath, transcript), + }); + return [adapter]; + }), + ).pipe(Layer.provide(Layer.mergeAll(serverConfigLayer, NodeServices.layer, idAllocatorLayer))); +} + +export const AcpRegistryOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness< + AcpReplayTranscript, + AcpReplayTranscriptDecodeError +> = { + driver: ACP_REGISTRY_PROVIDER, + decodeTranscript: (transcript) => + decodeAcpReplayTranscript(transcript, ACP_REGISTRY_PROVIDER, { + retargetProvider: true, + }), + makeProviderAdapterRegistryLayer: makeAcpRegistryProviderAdapterRegistryReplayLayer, +}; diff --git a/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.ts new file mode 100644 index 00000000000..ecc59dc1629 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.ts @@ -0,0 +1,165 @@ +import { + AcpRegistrySettings, + defaultInstanceIdForDriver, + ProviderDriverKind, +} from "@t3tools/contracts"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import type * as Scope from "effect/Scope"; +import { HttpClient } from "effect/unstable/http"; +import { ChildProcessSpawner } from "effect/unstable/process"; +import * as EffectAcpErrors from "effect-acp/errors"; + +import { ServerConfig } from "../../config.ts"; +import { + makeAcpRegistryResolver, + type AcpRegistryResolverShape, +} from "../../provider/acp/AcpRegistrySupport.ts"; +import * as AcpSessionRuntime from "../../provider/acp/AcpSessionRuntime.ts"; +import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; +import { IdAllocatorV2 } from "../IdAllocator.ts"; +import { + ProviderAdapterDriverCreateError, + type ProviderAdapterDriver, + type ProviderAdapterDriverCreateInput, +} from "../ProviderAdapterDriver.ts"; +import { + AcpProviderCapabilitiesV2, + makeAcpAdapterV2, + type AcpAdapterV2Flavor, + type AcpAdapterV2RuntimeInput, +} from "./AcpAdapterV2.ts"; + +export const ACP_REGISTRY_PROVIDER = ProviderDriverKind.make("acpRegistry"); +export const ACP_REGISTRY_DRIVER_KIND = ACP_REGISTRY_PROVIDER; +export const ACP_REGISTRY_DEFAULT_INSTANCE_ID = + defaultInstanceIdForDriver(ACP_REGISTRY_DRIVER_KIND); + +const DEFAULT_ACP_REGISTRY_SETTINGS = Schema.decodeSync(AcpRegistrySettings)({}); + +export interface AcpRegistryAdapterV2Options { + readonly instanceId: Parameters[0]["instanceId"]; + readonly settings: AcpRegistrySettings; + readonly environment: NodeJS.ProcessEnv; + readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; + readonly fileSystem: FileSystem.FileSystem; + readonly idAllocator: IdAllocatorV2["Service"]; + readonly resolver: AcpRegistryResolverShape; + readonly serverConfig: ServerConfig["Service"]; + readonly makeRuntime?: ( + input: AcpAdapterV2RuntimeInput, + ) => Effect.Effect< + AcpSessionRuntime.AcpSessionRuntime["Service"], + EffectAcpErrors.AcpError, + Scope.Scope + >; + readonly assertComplete?: Effect.Effect; +} + +function makeAcpRegistryRuntime(options: AcpRegistryAdapterV2Options) { + return ( + input: AcpAdapterV2RuntimeInput, + ): Effect.Effect< + AcpSessionRuntime.AcpSessionRuntime["Service"], + EffectAcpErrors.AcpError, + Scope.Scope + > => + Effect.gen(function* () { + const resolved = yield* options.resolver + .resolve(options.settings, input.cwd, options.environment) + .pipe( + Effect.mapError( + (cause) => + new EffectAcpErrors.AcpSpawnError({ + command: options.settings.agentId || ACP_REGISTRY_PROVIDER, + cause, + }), + ), + ); + const context = yield* Layer.build( + AcpSessionRuntime.layer({ + ...input, + spawn: resolved.spawn, + ...(options.settings.authMethodId ? { authMethodId: options.settings.authMethodId } : {}), + }).pipe( + Layer.provide( + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, options.childProcessSpawner), + ), + ), + ); + return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe( + Effect.provide(context), + ); + }); +} + +export function makeAcpRegistryAdapterV2(options: AcpRegistryAdapterV2Options) { + const flavor: AcpAdapterV2Flavor = { + driver: ACP_REGISTRY_PROVIDER, + capabilities: AcpProviderCapabilitiesV2, + makeRuntime: options.makeRuntime ?? makeAcpRegistryRuntime(options), + ...(options.assertComplete === undefined ? {} : { assertComplete: options.assertComplete }), + }; + return makeAcpAdapterV2({ + instanceId: options.instanceId, + flavor, + fileSystem: options.fileSystem, + idAllocator: options.idAllocator, + serverConfig: options.serverConfig, + }); +} + +export type AcpRegistryAdapterV2DriverEnv = + | ChildProcessSpawner.ChildProcessSpawner + | FileSystem.FileSystem + | HttpClient.HttpClient + | IdAllocatorV2 + | Path.Path + | ServerConfig; + +export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver< + AcpRegistrySettings, + AcpRegistryAdapterV2DriverEnv +> = { + driverKind: ACP_REGISTRY_DRIVER_KIND, + configSchema: AcpRegistrySettings, + defaultConfig: (): AcpRegistrySettings => DEFAULT_ACP_REGISTRY_SETTINGS, + create: Effect.fn("AcpRegistryAdapterV2Driver.create")( + function* (input: ProviderAdapterDriverCreateInput) { + const hostEnvironment = yield* HostProcessEnvironment; + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fileSystem = yield* FileSystem.FileSystem; + const idAllocator = yield* IdAllocatorV2; + const serverConfig = yield* ServerConfig; + const resolver = yield* makeAcpRegistryResolver({ + cacheDir: serverConfig.providerStatusCacheDir, + }); + return makeAcpRegistryAdapterV2({ + instanceId: input.instanceId, + settings: { ...input.config, enabled: input.enabled }, + environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + childProcessSpawner, + fileSystem, + idAllocator, + resolver, + serverConfig, + }); + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterDriverCreateError({ + driver: ACP_REGISTRY_DRIVER_KIND, + instanceId: input.instanceId, + detail: "Failed to create ACP Registry adapter.", + cause, + }), + ), + ), + ), +}; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts new file mode 100644 index 00000000000..27f5d5150e4 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -0,0 +1,447 @@ +import { + ClaudeSettings, + EnvironmentId, + MessageId, + NodeId, + ProjectId, + ProviderInstanceId, + ProviderSessionId, + ProviderTurnId, + RunAttemptId, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import { assert, describe, it } from "@effect/vitest"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; + +import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; +import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts"; +import { ProviderAdapterV2RuntimePolicy } from "../ProviderAdapter.ts"; +import { + CLAUDE_AGENT_SDK_QUERY_PROTOCOL, + CLAUDE_DEFAULT_INSTANCE_ID, + CLAUDE_PROVIDER, + CLAUDE_READ_ONLY_ALLOWED_TOOLS, + ClaudeProviderCapabilitiesV2, + claudeMcpQueryOverrides, + claudeRuntimeQueryPolicyForRuntimePolicy, + loggedClaudeQueryOptions, + makeClaudeAdapterV2, + makeClaudeAgentSdkProtocolLogger, + makeClaudeQueryOptions, + type ClaudeAgentSdkQueryOptions, + type ClaudeAgentSdkQueryOpenInput, +} from "./ClaudeAdapterV2.ts"; +import { layer as idAllocatorLayer, IdAllocatorV2 } from "../IdAllocator.ts"; + +const DEFAULT_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({}); + +describe("ClaudeAdapterV2 runtime query policy", () => { + it("maps canonical read-only never policy to Claude dontAsk with read-only tools", () => { + const queryPolicy = claudeRuntimeQueryPolicyForRuntimePolicy( + ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: "/workspace", + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }), + ); + + assert.deepEqual(queryPolicy, { + permissionMode: "dontAsk", + tools: CLAUDE_READ_ONLY_ALLOWED_TOOLS, + allowedTools: CLAUDE_READ_ONLY_ALLOWED_TOOLS, + installPermissionCallback: false, + }); + }); + + it("maps canonical read-only on-request policy to Claude default with callbacks", () => { + const queryPolicy = claudeRuntimeQueryPolicyForRuntimePolicy( + ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: "/workspace", + approvalPolicy: "on-request", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }), + ); + + assert.deepEqual(queryPolicy, { + permissionMode: "default", + installPermissionCallback: true, + }); + }); + + it("does not auto-allow reads for canonical restricted read-only never policy", () => { + const queryPolicy = claudeRuntimeQueryPolicyForRuntimePolicy( + ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: "/workspace", + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { + type: "restricted", + includePlatformDefaults: false, + readableRoots: [], + }, + networkAccess: false, + }, + }), + ); + + assert.deepEqual(queryPolicy, { + permissionMode: "dontAsk", + tools: CLAUDE_READ_ONLY_ALLOWED_TOOLS, + installPermissionCallback: false, + }); + }); + + it("maps default full-access policy to Claude bypass permissions", () => { + const queryPolicy = claudeRuntimeQueryPolicyForRuntimePolicy( + ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: "/workspace", + }), + ); + + assert.deepEqual(queryPolicy, { + permissionMode: "bypassPermissions", + allowDangerouslySkipPermissions: true, + installPermissionCallback: false, + }); + }); +}); + +describe("ClaudeAdapterV2 native protocol logging", () => { + it("injects thread-scoped MCP configuration without logging the credential", () => { + const threadId = ThreadId.make("thread-claude-mcp"); + McpProviderSession.setMcpProviderSession({ + environmentId: EnvironmentId.make("environment-claude-mcp"), + threadId, + providerSessionId: "mcp-session-claude", + providerInstanceId: ProviderInstanceId.make("claudeAgent"), + endpoint: "http://127.0.0.1:43123/mcp", + authorizationHeader: "Bearer secret-claude-token", + }); + + try { + const overrides = claudeMcpQueryOverrides({ + threadId, + allowedTools: ["Read"], + }); + assert.deepEqual(overrides, { + allowedTools: ["Read", "mcp__t3-code__*"], + mcpServers: { + "t3-code": { + type: "http", + url: "http://127.0.0.1:43123/mcp", + headers: { + Authorization: "Bearer secret-claude-token", + }, + }, + }, + }); + + const options = makeClaudeQueryOptions({ + modelSelection: { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-sonnet-4-6", + }, + nativeThreadId: "native-thread-claude-mcp", + resume: false, + cwd: "/workspace", + ...overrides, + }); + const logged = loggedClaudeQueryOptions(options); + assert.equal(logged.hasMcpServers, true); + assert.notInclude(JSON.stringify(logged), "secret-claude-token"); + } finally { + McpProviderSession.clearMcpProviderSession(threadId); + } + }); + + it.effect("writes Claude Agent SDK protocol frames to the native provider log", () => + Effect.gen(function* () { + const writes: Array<{ + readonly event: unknown; + readonly threadId: ThreadId | null; + }> = []; + const logger: EventNdjsonLogger = { + filePath: "/tmp/events.log", + write: (event, threadId) => + Effect.sync(() => { + writes.push({ event, threadId }); + }), + close: () => Effect.void, + }; + const threadId = ThreadId.make("thread-1"); + const providerSessionId = ProviderSessionId.make("provider-session-1"); + const protocolLogger = makeClaudeAgentSdkProtocolLogger({ + nativeEventLogger: logger, + threadId, + providerSessionId, + }); + + assert.notEqual(protocolLogger, undefined); + if (protocolLogger === undefined) { + return; + } + + yield* protocolLogger({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "query.interrupt", + }, + }); + + assert.equal(writes.length, 1); + assert.equal(writes[0]?.threadId, threadId); + assert.deepEqual(writes[0]?.event, { + provider: "claudeAgent", + protocol: CLAUDE_AGENT_SDK_QUERY_PROTOCOL, + kind: "protocol", + providerSessionId, + event: { + direction: "outgoing", + stage: "decoded", + payload: { + type: "query.interrupt", + }, + }, + }); + }), + ); + + it("does not install a protocol logger when native logging is unavailable", () => { + const protocolLogger = makeClaudeAgentSdkProtocolLogger({ + nativeEventLogger: undefined, + threadId: ThreadId.make("thread-1"), + providerSessionId: ProviderSessionId.make("provider-session-1"), + }); + + assert.equal(protocolLogger, undefined); + }); + + it("logs query options without leaking environment values or callback functions", () => { + const options: ClaudeAgentSdkQueryOptions = { + model: "claude-sonnet-4-6", + tools: { + type: "preset", + preset: "claude_code", + }, + permissionMode: "default", + sessionId: "native-thread-1", + cwd: "/workspace", + env: { + ANTHROPIC_API_KEY: "secret", + }, + canUseTool: (_toolName, input, callbackOptions) => + Promise.resolve({ + behavior: "allow", + updatedInput: input, + toolUseID: callbackOptions.toolUseID, + decisionClassification: "user_temporary", + }), + }; + + assert.deepEqual(loggedClaudeQueryOptions(options), { + model: "claude-sonnet-4-6", + tools: { + type: "preset", + preset: "claude_code", + }, + permissionMode: "default", + sessionId: "native-thread-1", + cwd: "/workspace", + hasCanUseTool: true, + hasEnvironment: true, + }); + }); +}); + +describe("ClaudeAdapterV2 native fork", () => { + it("advertises Claude Agent SDK session forks", () => { + assert.equal(ClaudeProviderCapabilitiesV2.threads.canForkThread, true); + assert.equal(ClaudeProviderCapabilitiesV2.threads.canForkFromTurn, true); + }); + + it.effect("forks at the source assistant cursor and resumes the forked session", () => + Effect.scoped( + Effect.gen(function* () { + const idAllocator = yield* IdAllocatorV2; + const openedQueries: Array = []; + const forkCalls: Array<{ + readonly sessionId: string; + readonly options: unknown; + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + }> = []; + const adapter = makeClaudeAdapterV2({ + instanceId: CLAUDE_DEFAULT_INSTANCE_ID, + settings: DEFAULT_CLAUDE_SETTINGS, + environment: {}, + idAllocator, + queryRunner: { + allocateSessionId: Effect.succeed("source-native-session"), + open: (input) => + Effect.sync(() => { + openedQueries.push(input); + return { + messages: Stream.empty, + offer: () => Effect.void, + setModel: () => Effect.void, + interrupt: Effect.void, + close: Effect.void, + }; + }), + forkSession: (input) => + Effect.sync(() => { + forkCalls.push(input); + return { sessionId: "forked-native-session" }; + }), + assertComplete: Effect.void, + }, + }); + const providerSessionId = ProviderSessionId.make("provider-session-claude-fork"); + const sourceThreadId = ThreadId.make("thread-claude-fork-source"); + const targetThreadId = ThreadId.make("thread-claude-fork-target"); + const runtime = yield* adapter.openSession({ + threadId: sourceThreadId, + providerSessionId, + modelSelection: { + instanceId: ProviderInstanceId.make(CLAUDE_PROVIDER), + model: "claude-sonnet-4-6", + }, + runtimePolicy: ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: "/workspace", + }), + }); + const sourceProviderThread = yield* runtime.ensureThread({ + threadId: sourceThreadId, + modelSelection: { + instanceId: ProviderInstanceId.make(CLAUDE_PROVIDER), + model: "claude-sonnet-4-6", + }, + runtimePolicy: ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: "/workspace", + }), + }); + const now = yield* DateTime.now; + const providerTurnId = ProviderTurnId.make("provider-turn-claude-source"); + const forkedProviderThread = yield* runtime.forkThread({ + sourceProviderThread, + sourceProviderTurns: [ + { + id: providerTurnId, + providerThreadId: sourceProviderThread.id, + nodeId: NodeId.make("node-claude-source"), + runAttemptId: RunAttemptId.make("run-attempt-claude-source"), + nativeTurnRef: { + driver: CLAUDE_PROVIDER, + nativeId: "assistant-message-cursor", + strength: "weak", + }, + ordinal: 1, + status: "completed", + startedAt: now, + completedAt: now, + }, + ], + providerTurnId, + targetThreadId, + }); + + assert.deepEqual(forkCalls, [ + { + sessionId: "source-native-session", + options: { + dir: "/workspace", + upToMessageId: "assistant-message-cursor", + }, + threadId: targetThreadId, + providerSessionId, + }, + ]); + assert.equal(forkedProviderThread.nativeThreadRef?.nativeId, "forked-native-session"); + assert.equal(forkedProviderThread.forkedFrom?.providerThreadId, sourceProviderThread.id); + assert.equal(forkedProviderThread.forkedFrom?.providerTurnId, providerTurnId); + + yield* runtime.startTurn({ + appThread: { + createdBy: "user", + creationSource: "web", + id: targetThreadId, + projectId: ProjectId.make("project-claude-fork-target"), + title: "Claude fork target", + providerInstanceId: ProviderInstanceId.make(CLAUDE_PROVIDER), + modelSelection: { + instanceId: ProviderInstanceId.make(CLAUDE_PROVIDER), + model: "claude-sonnet-4-6", + }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: forkedProviderThread.id, + lineage: { + parentThreadId: sourceThreadId, + relationshipToParent: "fork", + rootThreadId: sourceThreadId, + }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }, + threadId: targetThreadId, + runId: RunId.make("run-claude-fork-target"), + runOrdinal: 1, + providerTurnOrdinal: 1, + attemptId: RunAttemptId.make("run-attempt-claude-fork-target"), + rootNodeId: NodeId.make("node-claude-fork-target-root"), + providerThread: forkedProviderThread, + message: { + createdBy: "user", + creationSource: "web", + messageId: MessageId.make("message-claude-fork-target"), + text: "Respond with fork ok", + attachments: [], + }, + modelSelection: { + instanceId: ProviderInstanceId.make(CLAUDE_PROVIDER), + model: "claude-sonnet-4-6", + }, + runtimePolicy: ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: "/workspace", + }), + }); + + assert.equal(openedQueries[0]?.options.resume, "forked-native-session"); + assert.equal(openedQueries[0]?.options.sessionId, undefined); + }).pipe(Effect.provide(idAllocatorLayer)), + ), + ); +}); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts new file mode 100644 index 00000000000..45d255cb2ee --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts @@ -0,0 +1,2383 @@ +import { + forkSession, + query, + type CanUseTool, + type PermissionResult, + type SDKAssistantMessage, + type SDKMessage, + type SDKUserMessage, +} from "@anthropic-ai/claude-agent-sdk"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { + ProviderReplayEntry, + type ModelSelection, + type ProviderApprovalDecision, + type ProviderReplayTranscript, +} from "@t3tools/contracts"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; + +import { + CLAUDE_PROVIDER, + CLAUDE_DEFAULT_INSTANCE_ID, + CLAUDE_DRIVER_KIND, + ClaudeAdapterV2Driver, + ClaudeAgentSdkQueryRunner, + ClaudeAgentSdkQueryRunnerError, + makeClaudeUserMessage, + makeClaudeQueryOptions, + type ClaudeAgentSdkSessionForkInput, + type ClaudeAgentSdkQueryOpenInput, + type ClaudeAgentSdkQueryOptions, + type ClaudeAgentSdkQuerySession, + type ClaudeAgentSdkQueryTools, +} from "./ClaudeAdapterV2.ts"; +import { layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { ProviderAdapterDriverCreateError } from "../ProviderAdapterDriver.ts"; +import { makeDriverLayer as makeProviderAdapterRegistryDriverLayer } from "../ProviderAdapterRegistry.ts"; +import { randomUuidV4 } from "../RandomUuid.ts"; +import type { OrchestratorV2ProviderReplayHarness } from "../testkit/ProviderReplayHarness.ts"; + +export const CLAUDE_AGENT_SDK_REPLAY_PROTOCOL = "claude-agent-sdk.query" as const; + +const ClaudeAgentSdkReplayTranscript = Schema.Struct({ + provider: Schema.Literal(CLAUDE_PROVIDER), + protocol: Schema.Literal(CLAUDE_AGENT_SDK_REPLAY_PROTOCOL), + version: Schema.String, + scenario: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + entries: Schema.Array(ProviderReplayEntry), +}); +type ClaudeAgentSdkReplayTranscript = typeof ClaudeAgentSdkReplayTranscript.Type; + +export class ClaudeReplayTranscriptDecodeError extends Schema.TaggedErrorClass()( + "ClaudeReplayTranscriptDecodeError", + { + driver: Schema.optional(Schema.String), + protocol: Schema.optional(Schema.String), + scenario: Schema.optional(Schema.String), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to decode Claude Agent SDK replay transcript for scenario ${this.scenario ?? ""}.`; + } +} + +export class ClaudeReplayExhaustedError extends Schema.TaggedErrorClass()( + "ClaudeReplayExhaustedError", + { + scenario: Schema.String, + cursor: Schema.Number, + actual: Schema.Unknown, + }, +) { + override get message(): string { + return `Claude Agent SDK replay transcript exhausted before outbound frame ${this.cursor} in scenario ${this.scenario}.`; + } +} + +export class ClaudeReplayUnexpectedOutboundError extends Schema.TaggedErrorClass()( + "ClaudeReplayUnexpectedOutboundError", + { + scenario: Schema.String, + cursor: Schema.Number, + expectedType: Schema.String, + actual: Schema.Unknown, + }, +) { + override get message(): string { + return `Unexpected outbound Claude Agent SDK frame at replay cursor ${this.cursor} in scenario ${this.scenario}.`; + } +} + +export class ClaudeReplayFrameMismatchError extends Schema.TaggedErrorClass()( + "ClaudeReplayFrameMismatchError", + { + scenario: Schema.String, + cursor: Schema.Number, + label: Schema.optional(Schema.String), + expected: Schema.Unknown, + actual: Schema.Unknown, + }, +) { + override get message(): string { + return `Outbound Claude Agent SDK frame did not match replay cursor ${this.cursor} in scenario ${this.scenario}.`; + } +} + +export class ClaudeReplayRuntimeExitError extends Schema.TaggedErrorClass()( + "ClaudeReplayRuntimeExitError", + { + scenario: Schema.String, + cursor: Schema.Number, + status: Schema.Literals(["error", "cancelled"]), + error: Schema.optional(Schema.Unknown), + }, +) { + override get message(): string { + return `Claude Agent SDK replay exited with status ${this.status} at cursor ${this.cursor} in scenario ${this.scenario}.`; + } +} + +export class ClaudeReplayIncompleteError extends Schema.TaggedErrorClass()( + "ClaudeReplayIncompleteError", + { + scenario: Schema.String, + cursor: Schema.Number, + remaining: Schema.Number, + }, +) { + override get message(): string { + return `Claude Agent SDK replay ended with ${this.remaining} unconsumed entries in scenario ${this.scenario}.`; + } +} + +export class ClaudeReplayDriverError extends Schema.TaggedErrorClass()( + "ClaudeReplayDriverError", + { + scenario: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Claude Agent SDK replay driver failed in scenario ${this.scenario}.`; + } +} + +export const ClaudeAgentSdkReplayError = Schema.Union([ + ClaudeReplayTranscriptDecodeError, + ClaudeReplayExhaustedError, + ClaudeReplayUnexpectedOutboundError, + ClaudeReplayFrameMismatchError, + ClaudeReplayRuntimeExitError, + ClaudeReplayIncompleteError, + ClaudeReplayDriverError, +]); +export type ClaudeAgentSdkReplayError = typeof ClaudeAgentSdkReplayError.Type; +export const ClaudeOrchestratorReplayHarnessError = Schema.Union([ + ClaudeAgentSdkReplayError, + ProviderAdapterDriverCreateError, +]); +export type ClaudeOrchestratorReplayHarnessError = typeof ClaudeOrchestratorReplayHarnessError.Type; + +interface ClaudeQueryOpenFrame { + readonly type: "query.open"; + readonly options: ClaudeAgentSdkQueryOptions; +} + +interface ClaudePromptOfferFrame { + readonly type: "prompt.offer"; + readonly message: SDKUserMessage; +} + +interface ClaudeQuerySetModelFrame { + readonly type: "query.set_model"; + readonly model: string; +} + +interface ClaudeQueryInterruptFrame { + readonly type: "query.interrupt"; +} + +interface ClaudePermissionRequestFrame { + readonly type: "permission.request"; + readonly toolName: string; + readonly input: Record; + readonly options: { + readonly suggestions?: Parameters[2]["suggestions"]; + readonly blockedPath?: string; + readonly decisionReason?: string; + readonly title?: string; + readonly displayName?: string; + readonly description?: string; + readonly toolUseID: string; + readonly agentID?: string; + }; +} + +interface ClaudePermissionResponseFrame { + readonly type: "permission.response"; + readonly result: PermissionResult; +} + +interface ClaudeSessionForkFrame { + readonly type: "session.fork"; + readonly sessionId: string; + readonly options: { + readonly dir?: string; + readonly upToMessageId?: string; + readonly title?: string; + }; +} + +interface ClaudeSessionForkedFrame { + readonly type: "session.forked"; + readonly sessionId: string; +} + +type ClaudeOutboundFrame = + | ClaudeQueryOpenFrame + | ClaudePromptOfferFrame + | ClaudeQuerySetModelFrame + | ClaudeQueryInterruptFrame + | ClaudePermissionResponseFrame + | ClaudeSessionForkFrame; + +interface ClaudeQueryRunner { + readonly open: (input: ClaudeAgentSdkQueryOpenInput) => ClaudeAgentSdkQuerySession; + readonly forkSession: (input: ClaudeAgentSdkSessionForkInput) => ClaudeSessionForkedFrame; + readonly assertComplete: () => void; +} + +function stableStringify(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (typeof value === "object" && value !== null) { + const record = value as Record; + return `{${Object.keys(record) + .toSorted() + .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function normalizeContextHandoffText(value: string): string { + if (!value.startsWith("Context handoff (")) { + return value; + } + const userMessageMarker = "\n\nUser message:\n"; + const userMessageIndex = value.indexOf(userMessageMarker); + const headerEndIndex = value.indexOf(":\n"); + if (headerEndIndex === -1 || userMessageIndex === -1 || headerEndIndex >= userMessageIndex) { + return value; + } + return `${value.slice(0, headerEndIndex + 2)}${value.slice(userMessageIndex)}`; +} + +function normalizeReplayFrame(value: unknown): unknown { + if (typeof value === "string") { + return normalizeContextHandoffText(value); + } + if (Array.isArray(value)) { + return value.map(normalizeReplayFrame); + } + if (typeof value !== "object" || value === null) { + return value; + } + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [key, normalizeReplayFrame(entry)]), + ); +} + +function sameFrame(left: unknown, right: unknown): boolean { + return ( + stableStringify(normalizeReplayFrame(left)) === stableStringify(normalizeReplayFrame(right)) + ); +} + +function isClaudeSdkReplayMessage(frame: unknown): frame is SDKMessage { + if (typeof frame !== "object" || frame === null) { + return false; + } + const type = Reflect.get(frame, "type"); + return ( + type === "assistant" || + type === "user" || + type === "result" || + type === "system" || + type === "rate_limit_event" + ); +} + +function sdkMessageFromReplayFrame(frame: unknown): SDKMessage { + if (!isClaudeSdkReplayMessage(frame)) { + throw new Error("Replay frame is not a Claude Agent SDK message."); + } + return frame; +} + +function isClaudePermissionRequestFrame(frame: unknown): frame is ClaudePermissionRequestFrame { + const options = + typeof frame === "object" && frame !== null ? Reflect.get(frame, "options") : undefined; + return ( + typeof frame === "object" && + frame !== null && + Reflect.get(frame, "type") === "permission.request" && + typeof Reflect.get(frame, "toolName") === "string" && + typeof Reflect.get(frame, "input") === "object" && + Reflect.get(frame, "input") !== null && + typeof options === "object" && + options !== null && + typeof Reflect.get(options, "toolUseID") === "string" + ); +} + +function makeClaudePermissionResponseFrame( + result: PermissionResult, +): ClaudePermissionResponseFrame { + return { + type: "permission.response", + result, + }; +} + +function permissionRequestOptionsFromFrame( + frame: ClaudePermissionRequestFrame, +): Parameters[2] { + const abortController = new AbortController(); + const options = frame.options; + return { + signal: abortController.signal, + ...(options.suggestions === undefined ? {} : { suggestions: options.suggestions }), + ...(options.blockedPath === undefined ? {} : { blockedPath: options.blockedPath }), + ...(options.decisionReason === undefined ? {} : { decisionReason: options.decisionReason }), + ...(options.title === undefined ? {} : { title: options.title }), + ...(options.displayName === undefined ? {} : { displayName: options.displayName }), + ...(options.description === undefined ? {} : { description: options.description }), + toolUseID: options.toolUseID, + ...(options.agentID === undefined ? {} : { agentID: options.agentID }), + }; +} + +function unresolvedCursorSignal(): void {} + +function makeCursorSignal(): { + readonly promise: Promise; + readonly resolve: () => void; +} { + let resolve: () => void = unresolvedCursorSignal; + const promise = new Promise((onResolve) => { + resolve = onResolve; + }); + return { promise, resolve }; +} + +function stableClaudeQueryOptions(options: ClaudeAgentSdkQueryOptions): ClaudeAgentSdkQueryOptions { + const stable = { + model: options.model, + tools: options.tools, + permissionMode: options.permissionMode, + ...(options.allowedTools === undefined ? {} : { allowedTools: options.allowedTools }), + ...(options.disallowedTools === undefined ? {} : { disallowedTools: options.disallowedTools }), + ...(options.settings === undefined ? {} : { settings: options.settings }), + ...(options.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + ...(options.resumeSessionAt === undefined ? {} : { resumeSessionAt: options.resumeSessionAt }), + ...(options.forkSession === true ? { forkSession: true } : {}), + }; + return options.resume === undefined + ? { ...stable, sessionId: options.sessionId } + : { ...stable, resume: options.resume }; +} + +function makeClaudeQueryOpenFrame( + input: Pick, +): ClaudeQueryOpenFrame { + return { + type: "query.open", + options: stableClaudeQueryOptions(input.options), + }; +} + +function makeClaudePromptOfferFrame(message: SDKUserMessage): ClaudePromptOfferFrame { + return { + type: "prompt.offer", + message, + }; +} + +function makeClaudeSessionForkFrame( + input: ClaudeAgentSdkSessionForkInput, + scenario: string, +): ClaudeSessionForkFrame { + return { + type: "session.fork", + sessionId: input.sessionId, + options: { + ...(input.options.dir === undefined ? {} : { dir: sanitizedReplayCwd(scenario) }), + ...(input.options.upToMessageId === undefined + ? {} + : { upToMessageId: input.options.upToMessageId }), + ...(input.options.title === undefined ? {} : { title: input.options.title }), + }, + }; +} + +function makeReplayQueryRunner(transcript: ClaudeAgentSdkReplayTranscript): ClaudeQueryRunner { + let cursor = 0; + let failure: ClaudeAgentSdkReplayError | null = null; + let cursorAdvanced = makeCursorSignal(); + let activeOptions: ClaudeAgentSdkQueryOptions | null = null; + + const fail = (error: ClaudeAgentSdkReplayError): never => { + failure = error; + throw error; + }; + + const advance = () => { + cursor += 1; + const signal = cursorAdvanced; + cursorAdvanced = makeCursorSignal(); + signal.resolve(); + }; + + async function* replayMessages(): AsyncGenerator { + while (true) { + if (failure !== null) { + throw failure; + } + + const entry = transcript.entries[cursor]; + if (entry === undefined) { + return; + } + + if (entry.type === "emit_inbound") { + if (isClaudePermissionRequestFrame(entry.frame)) { + const request = entry.frame; + const invokeCanUseTool = activeOptions?.canUseTool; + if (invokeCanUseTool === undefined) { + const error = new ClaudeReplayUnexpectedOutboundError({ + scenario: transcript.scenario, + cursor, + expectedType: "permission.request", + actual: request, + }); + failure = error; + throw error; + } + advance(); + const result = await invokeCanUseTool( + request.toolName, + request.input, + permissionRequestOptionsFromFrame(request), + ); + assertNextOutboundFrame(makeClaudePermissionResponseFrame(result)); + continue; + } + advance(); + yield sdkMessageFromReplayFrame(entry.frame); + continue; + } + + if (entry.type === "runtime_exit") { + advance(); + if (entry.status === "success" || entry.status === "cancelled") { + return; + } + fail( + new ClaudeReplayRuntimeExitError({ + scenario: transcript.scenario, + cursor: cursor - 1, + status: entry.status, + ...(entry.error === undefined ? {} : { error: entry.error }), + }), + ); + } + + if (entry.type === "expect_outbound") { + const signal = cursorAdvanced; + await signal.promise; + continue; + } + } + } + + const assertNextOutboundFrame = (actual: ClaudeOutboundFrame) => { + if (failure !== null) { + throw failure; + } + const entry = transcript.entries[cursor]; + if (entry === undefined) { + return fail( + new ClaudeReplayExhaustedError({ + scenario: transcript.scenario, + cursor, + actual, + }), + ); + } + if (entry.type !== "expect_outbound") { + return fail( + new ClaudeReplayUnexpectedOutboundError({ + scenario: transcript.scenario, + cursor, + expectedType: entry.type, + actual, + }), + ); + } + + const expected = entry.frame; + if (!sameFrame(expected, actual)) { + fail( + new ClaudeReplayFrameMismatchError({ + scenario: transcript.scenario, + cursor, + ...(entry.label === undefined ? {} : { label: entry.label }), + expected, + actual, + }), + ); + } + + advance(); + }; + + const assertNextForkedFrame = (): ClaudeSessionForkedFrame => { + const entry = transcript.entries[cursor]; + if (entry === undefined) { + return fail( + new ClaudeReplayExhaustedError({ + scenario: transcript.scenario, + cursor, + actual: { type: "session.forked" }, + }), + ); + } + if (entry.type !== "emit_inbound") { + return fail( + new ClaudeReplayUnexpectedOutboundError({ + scenario: transcript.scenario, + cursor, + expectedType: entry.type, + actual: { type: "session.forked" }, + }), + ); + } + if ( + typeof entry.frame !== "object" || + entry.frame === null || + Reflect.get(entry.frame, "type") !== "session.forked" || + typeof Reflect.get(entry.frame, "sessionId") !== "string" + ) { + return fail( + new ClaudeReplayFrameMismatchError({ + scenario: transcript.scenario, + cursor, + expected: { type: "session.forked" }, + actual: entry.frame, + }), + ); + } + + const frame = entry.frame as ClaudeSessionForkedFrame; + advance(); + return frame; + }; + + const replayEffect = (tryEffect: () => void) => + Effect.try({ + try: tryEffect, + catch: (cause) => replayQueryRunnerError(transcript, cause), + }); + + return { + open: (input) => { + assertNextOutboundFrame(makeClaudeQueryOpenFrame(input)); + activeOptions = input.options; + return { + messages: Stream.fromAsyncIterable(replayMessages(), (cause) => + replayQueryRunnerError(transcript, cause), + ), + offer: (message) => + replayEffect(() => { + assertNextOutboundFrame(makeClaudePromptOfferFrame(message)); + }), + setModel: (model) => + replayEffect(() => { + assertNextOutboundFrame({ + type: "query.set_model", + model, + }); + }), + interrupt: replayEffect(() => { + assertNextOutboundFrame({ type: "query.interrupt" }); + }), + close: Effect.void, + }; + }, + forkSession: (input) => { + assertNextOutboundFrame(makeClaudeSessionForkFrame(input, transcript.scenario)); + return assertNextForkedFrame(); + }, + assertComplete: () => { + if (failure !== null) { + throw failure; + } + if (cursor !== transcript.entries.length) { + throw new ClaudeReplayIncompleteError({ + scenario: transcript.scenario, + cursor, + remaining: transcript.entries.length - cursor, + }); + } + }, + }; +} + +function metadataFromTranscript(transcript: ProviderReplayTranscript): { + readonly provider?: string; + readonly protocol?: string; + readonly scenario?: string; +} { + return { + provider: transcript.provider, + protocol: transcript.protocol, + scenario: transcript.scenario, + }; +} + +function nativeSessionIdFor(transcript: ClaudeAgentSdkReplayTranscript): string { + const metadataSessionId = transcript.metadata?.nativeSessionId; + return typeof metadataSessionId === "string" + ? metadataSessionId + : "00000000-0000-4000-8000-000000000000"; +} + +function replayQueryRunnerError( + transcript: ClaudeAgentSdkReplayTranscript, + cause: unknown, +): ClaudeAgentSdkQueryRunnerError { + if (Schema.is(ClaudeAgentSdkQueryRunnerError)(cause)) { + return cause; + } + const replayCause = Schema.is(ClaudeAgentSdkReplayError)(cause) + ? cause + : new ClaudeReplayDriverError({ scenario: transcript.scenario, cause }); + return new ClaudeAgentSdkQueryRunnerError({ + cause: replayCause, + method: `replay-scenario:${transcript.scenario}`, + }); +} + +const makeClaudeAgentSdkReplayQueryRunner = Effect.fn("ClaudeAgentSdkReplayQueryRunner.layer")( + function* (transcript: ClaudeAgentSdkReplayTranscript) { + const queryRunner = makeReplayQueryRunner(transcript); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + queryRunner.assertComplete(); + }), + ); + + return ClaudeAgentSdkQueryRunner.of({ + allocateSessionId: Effect.succeed(nativeSessionIdFor(transcript)), + open: (input) => + Effect.try({ + try: () => queryRunner.open(input), + catch: (cause) => replayQueryRunnerError(transcript, cause), + }), + forkSession: (input) => + Effect.try({ + try: () => queryRunner.forkSession(input), + catch: (cause) => replayQueryRunnerError(transcript, cause), + }), + assertComplete: Effect.try({ + try: () => queryRunner.assertComplete(), + catch: (cause) => replayQueryRunnerError(transcript, cause), + }), + }); + }, +); + +export function makeClaudeAgentSdkReplayQueryRunnerLayer( + transcript: ClaudeAgentSdkReplayTranscript, +): Layer.Layer { + return Layer.effect(ClaudeAgentSdkQueryRunner, makeClaudeAgentSdkReplayQueryRunner(transcript)); +} + +export function makeClaudeAgentSdkReplayLayer( + transcript: ClaudeAgentSdkReplayTranscript, +): Layer.Layer { + const queryRunner = makeReplayQueryRunner(transcript); + return Layer.effect( + ClaudeAgentSdkQueryRunner, + Effect.gen(function* () { + yield* Effect.addFinalizer(() => + Effect.sync(() => { + queryRunner.assertComplete(); + }), + ); + + return ClaudeAgentSdkQueryRunner.of({ + allocateSessionId: Effect.succeed(nativeSessionIdFor(transcript)), + open: (input) => + Effect.try({ + try: () => queryRunner.open(input), + catch: (cause) => replayQueryRunnerError(transcript, cause), + }), + forkSession: (input) => + Effect.try({ + try: () => queryRunner.forkSession(input), + catch: (cause) => replayQueryRunnerError(transcript, cause), + }), + assertComplete: Effect.try({ + try: () => queryRunner.assertComplete(), + catch: (cause) => replayQueryRunnerError(transcript, cause), + }), + }); + }), + ); +} + +export function makeClaudeProviderAdapterRegistryReplayLayer( + transcript: ClaudeAgentSdkReplayTranscript, +) { + return makeProviderAdapterRegistryDriverLayer({ + drivers: [ClaudeAdapterV2Driver], + configMap: { + [CLAUDE_DEFAULT_INSTANCE_ID]: { + driver: CLAUDE_DRIVER_KIND, + }, + }, + }).pipe( + Layer.provide(makeClaudeAgentSdkReplayLayer(transcript)), + Layer.provide(idAllocatorLayer), + Layer.provide(NodeServices.layer), + ); +} + +export async function replayClaudeAgentSdkTranscript(input: { + readonly transcript: ClaudeAgentSdkReplayTranscript; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd?: string; +}): Promise> { + return input.transcript.entries.flatMap((entry) => + entry.type === "emit_inbound" && isClaudeSdkReplayMessage(entry.frame) + ? [sdkMessageFromReplayFrame(entry.frame)] + : [], + ); +} + +function serializeReplayError(error: unknown, scenario?: string): unknown { + return error instanceof Error + ? { + name: error.name, + message: + scenario === undefined + ? error.message + : sanitizeReplayText({ text: error.message, scenario }), + } + : error; +} + +function permissionResultForRecording(input: { + readonly decision: ProviderApprovalDecision; + readonly toolInput: Record; + readonly toolUseID: string; + readonly suggestions?: Parameters[2]["suggestions"]; +}): PermissionResult { + if (input.decision === "accept" || input.decision === "acceptForSession") { + return { + behavior: "allow", + updatedInput: input.toolInput, + toolUseID: input.toolUseID, + decisionClassification: + input.decision === "acceptForSession" ? "user_permanent" : "user_temporary", + ...(input.decision === "acceptForSession" && input.suggestions !== undefined + ? { updatedPermissions: input.suggestions } + : {}), + }; + } + return { + behavior: "deny", + message: + input.decision === "cancel" + ? "User cancelled tool execution." + : "User declined tool execution.", + toolUseID: input.toolUseID, + decisionClassification: "user_reject", + ...(input.decision === "cancel" ? { interrupt: true } : {}), + }; +} + +function permissionRequestFrame(input: { + readonly toolName: string; + readonly toolInput: Record; + readonly callbackOptions: Parameters[2]; +}): ClaudePermissionRequestFrame { + const { callbackOptions } = input; + return { + type: "permission.request", + toolName: input.toolName, + input: input.toolInput, + options: { + ...(callbackOptions.suggestions === undefined + ? {} + : { suggestions: callbackOptions.suggestions }), + ...(callbackOptions.blockedPath === undefined + ? {} + : { blockedPath: callbackOptions.blockedPath }), + ...(callbackOptions.decisionReason === undefined + ? {} + : { decisionReason: callbackOptions.decisionReason }), + ...(callbackOptions.title === undefined ? {} : { title: callbackOptions.title }), + ...(callbackOptions.displayName === undefined + ? {} + : { displayName: callbackOptions.displayName }), + ...(callbackOptions.description === undefined + ? {} + : { description: callbackOptions.description }), + toolUseID: callbackOptions.toolUseID, + ...(callbackOptions.agentID === undefined ? {} : { agentID: callbackOptions.agentID }), + }, + }; +} + +function sanitizedReplayCwd(scenario: string): string { + return `/tmp/claude-replay-${scenario}`; +} + +function parentDirectory(input: string): string { + const trimmed = input.replace(/\/+$/u, ""); + const lastSlash = trimmed.lastIndexOf("/"); + if (lastSlash <= 0) { + return "/"; + } + return trimmed.slice(0, lastSlash); +} + +function sanitizeReplayText(input: { readonly text: string; readonly scenario: string }): string { + const sanitizedCwd = sanitizedReplayCwd(input.scenario); + const repoRoot = parentDirectory(parentDirectory(process.cwd())); + return [repoRoot, process.cwd()] + .toSorted((left, right) => right.length - left.length) + .reduce((text, localPath) => text.replaceAll(localPath, sanitizedCwd), input.text); +} + +function sanitizeSdkMessageForReplay(input: { + readonly message: SDKMessage; + readonly scenario: string; +}): SDKMessage { + const { message } = input; + if (message.type === "system" && message.subtype === "init") { + return { + type: "system", + subtype: "init", + ...(message.agents === undefined ? {} : { agents: [] }), + apiKeySource: message.apiKeySource, + ...(message.betas === undefined ? {} : { betas: message.betas }), + claude_code_version: message.claude_code_version, + cwd: sanitizedReplayCwd(input.scenario), + tools: [], + mcp_servers: [], + model: message.model, + permissionMode: message.permissionMode, + slash_commands: [], + output_style: message.output_style, + skills: [], + plugins: [], + ...(message.fast_mode_state === undefined + ? {} + : { fast_mode_state: message.fast_mode_state }), + uuid: message.uuid, + session_id: message.session_id, + }; + } + if (message.type === "rate_limit_event") { + return { + ...message, + rate_limit_info: { + status: message.rate_limit_info.status, + }, + }; + } + if (message.type === "result" && message.subtype !== "success" && message.errors.length > 0) { + return { + ...message, + errors: message.errors.map((error) => + sanitizeReplayText({ text: error, scenario: input.scenario }), + ), + }; + } + return message; +} + +class RecordingPromptQueue implements AsyncIterable { + private readonly pending: Array> = []; + private readonly waiters: Array<(result: IteratorResult) => void> = []; + private closed = false; + + offer(message: SDKUserMessage): void { + if (this.closed) { + throw new Error("Cannot offer a prompt to a closed Claude recording queue."); + } + this.push({ done: false, value: message }); + } + + close(): void { + if (this.closed) { + return; + } + this.closed = true; + this.push({ done: true, value: undefined }); + } + + async *[Symbol.asyncIterator](): AsyncIterator { + while (true) { + const next = await this.take(); + if (next.done === true) { + return; + } + yield next.value; + } + } + + private push(result: IteratorResult): void { + const waiter = this.waiters.shift(); + if (waiter === undefined) { + this.pending.push(result); + return; + } + waiter(result); + } + + private take(): Promise> { + const next = this.pending.shift(); + if (next !== undefined) { + return Promise.resolve(next); + } + return new Promise((resolve) => { + this.waiters.push(resolve); + }); + } +} + +async function recordMessagesUntilTurnResult(input: { + readonly iterator: AsyncIterator; + readonly entries: Array; + readonly scenario: string; +}): Promise { + while (true) { + const next = await input.iterator.next(); + if (next.done === true) { + return false; + } + const replayMessage = sanitizeSdkMessageForReplay({ + message: next.value, + scenario: input.scenario, + }); + input.entries.push({ + type: "emit_inbound", + label: replayMessage.type, + frame: replayMessage, + }); + if (replayMessage.type === "result") { + return true; + } + } +} + +async function recordMessagesUntilTurnResultWithCursor(input: { + readonly iterator: AsyncIterator; + readonly entries: Array; + readonly scenario: string; +}): Promise<{ + readonly completed: boolean; + readonly assistantMessageUuid: SDKAssistantMessage["uuid"] | null; +}> { + let assistantMessageUuid: SDKAssistantMessage["uuid"] | null = null; + while (true) { + const next = await input.iterator.next(); + if (next.done === true) { + return { completed: false, assistantMessageUuid }; + } + const replayMessage = sanitizeSdkMessageForReplay({ + message: next.value, + scenario: input.scenario, + }); + input.entries.push({ + type: "emit_inbound", + label: replayMessage.type, + frame: replayMessage, + }); + if (replayMessage.type === "assistant") { + assistantMessageUuid = replayMessage.uuid; + } + if (replayMessage.type === "result") { + return { completed: true, assistantMessageUuid }; + } + } +} + +function requireAssistantCursor(input: { + readonly scenario: string; + readonly promptIndex: number; + readonly cursor: SDKAssistantMessage["uuid"] | null; +}): SDKAssistantMessage["uuid"] { + if (input.cursor !== null) { + return input.cursor; + } + throw new Error( + `Claude replay scenario ${input.scenario} prompt ${input.promptIndex} completed without an SDKAssistantMessage.uuid cursor.`, + ); +} + +async function recordMessagesUntilTurnResults(input: { + readonly iterator: AsyncIterator; + readonly entries: Array; + readonly scenario: string; + readonly resultCount: number; +}): Promise { + let seenResults = 0; + while (true) { + const next = await input.iterator.next(); + if (next.done === true) { + return false; + } + const replayMessage = sanitizeSdkMessageForReplay({ + message: next.value, + scenario: input.scenario, + }); + input.entries.push({ + type: "emit_inbound", + label: replayMessage.type, + frame: replayMessage, + }); + if (replayMessage.type === "result") { + seenResults += 1; + if (seenResults >= input.resultCount) { + return true; + } + } + } +} + +async function recordMessagesUntilIteratorDone(input: { + readonly iterator: AsyncIterator; + readonly entries: Array; + readonly scenario: string; +}): Promise { + while (true) { + const next = await input.iterator.next(); + if (next.done === true) { + return; + } + const replayMessage = sanitizeSdkMessageForReplay({ + message: next.value, + scenario: input.scenario, + }); + input.entries.push({ + type: "emit_inbound", + label: replayMessage.type, + frame: replayMessage, + }); + } +} + +function sdkMessageHasToolUse(message: SDKMessage): boolean { + return ( + message.type === "assistant" && message.message.content.some((part) => part.type === "tool_use") + ); +} + +async function recordMessagesUntilFirstToolUse(input: { + readonly iterator: AsyncIterator; + readonly entries: Array; + readonly scenario: string; +}): Promise { + while (true) { + const next = await input.iterator.next(); + if (next.done === true) { + throw new Error(`Claude query ended before ${input.scenario} started a tool use.`); + } + const replayMessage = sanitizeSdkMessageForReplay({ + message: next.value, + scenario: input.scenario, + }); + input.entries.push({ + type: "emit_inbound", + label: replayMessage.type, + frame: replayMessage, + }); + if (replayMessage.type === "result") { + throw new Error(`Claude query completed before ${input.scenario} started a tool use.`); + } + if (sdkMessageHasToolUse(replayMessage)) { + return; + } + } +} + +async function recordClaudeStreamingQuery(input: { + readonly scenario: string; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly sessionId: string; + readonly entries: Array; + readonly enableTools?: boolean; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly permissionMode?: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: boolean; + readonly enablePermissionCallback?: boolean; + readonly permissionDecision?: ProviderApprovalDecision; +}): Promise { + const promptQueue = new RecordingPromptQueue(); + const canUseTool: CanUseTool | undefined = + input.enablePermissionCallback === true + ? async (toolName, toolInput, callbackOptions) => { + const requestFrame = permissionRequestFrame({ + toolName, + toolInput, + callbackOptions, + }); + input.entries.push({ + type: "emit_inbound", + label: `permission.request:${toolName}`, + frame: requestFrame, + }); + const result = permissionResultForRecording({ + decision: input.permissionDecision ?? "accept", + toolInput, + toolUseID: callbackOptions.toolUseID, + ...(callbackOptions.suggestions === undefined + ? {} + : { suggestions: callbackOptions.suggestions }), + }); + input.entries.push({ + type: "expect_outbound", + label: `permission.response:${toolName}`, + frame: makeClaudePermissionResponseFrame(result), + }); + return result; + } + : undefined; + const options = makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: input.sessionId, + resume: false, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + ...(canUseTool === undefined ? {} : { canUseTool }), + } + : {}), + }); + input.entries.push({ + type: "expect_outbound", + label: "query.open", + frame: makeClaudeQueryOpenFrame({ options }), + }); + const queryRuntime = query({ + prompt: promptQueue, + options, + }); + const iterator = queryRuntime[Symbol.asyncIterator](); + try { + for (const [index, prompt] of input.prompts.entries()) { + const message = makeClaudeUserMessage({ text: prompt }); + input.entries.push({ + type: "expect_outbound", + label: `prompt.offer:${index + 1}`, + frame: makeClaudePromptOfferFrame(message), + }); + promptQueue.offer(message); + const completed = await recordMessagesUntilTurnResult({ + iterator, + entries: input.entries, + scenario: input.scenario, + }); + if (!completed) { + throw new Error(`Claude streaming query ended before prompt ${index + 1} completed.`); + } + } + promptQueue.close(); + queryRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "success", + }); + } catch (error) { + promptQueue.close(); + queryRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "error", + error: serializeReplayError(error, input.scenario), + }); + throw error; + } +} + +async function recordClaudeActiveSteeringQuery(input: { + readonly scenario: string; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly sessionId: string; + readonly entries: Array; + readonly enableTools?: boolean; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly permissionMode?: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: boolean; + readonly enablePermissionCallback?: boolean; + readonly permissionDecision?: ProviderApprovalDecision; +}): Promise { + if (input.prompts.length < 2) { + throw new Error("Claude active steering replay recording requires at least two prompts."); + } + + const promptQueue = new RecordingPromptQueue(); + const offeredPrompts = new Set(); + const offerPrompt = (index: number, priority?: SDKUserMessage["priority"]) => { + const message = makeClaudeUserMessage({ + text: input.prompts[index]!, + ...(priority === undefined ? {} : { priority }), + }); + input.entries.push({ + type: "expect_outbound", + label: `prompt.offer:${index + 1}`, + frame: makeClaudePromptOfferFrame(message), + }); + promptQueue.offer(message); + offeredPrompts.add(index); + }; + const offerSteeringPrompts = () => { + for (let index = 1; index < input.prompts.length; index += 1) { + if (!offeredPrompts.has(index)) { + offerPrompt(index, "now"); + } + } + }; + + const canUseTool: CanUseTool | undefined = + input.enablePermissionCallback === true + ? async (toolName, toolInput, callbackOptions) => { + const requestFrame = permissionRequestFrame({ + toolName, + toolInput, + callbackOptions, + }); + input.entries.push({ + type: "emit_inbound", + label: `permission.request:${toolName}`, + frame: requestFrame, + }); + const result = permissionResultForRecording({ + decision: input.permissionDecision ?? "accept", + toolInput, + toolUseID: callbackOptions.toolUseID, + ...(callbackOptions.suggestions === undefined + ? {} + : { suggestions: callbackOptions.suggestions }), + }); + input.entries.push({ + type: "expect_outbound", + label: `permission.response:${toolName}`, + frame: makeClaudePermissionResponseFrame(result), + }); + return result; + } + : undefined; + const options = makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: input.sessionId, + resume: false, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + ...(canUseTool === undefined ? {} : { canUseTool }), + } + : {}), + }); + input.entries.push({ + type: "expect_outbound", + label: "query.open", + frame: makeClaudeQueryOpenFrame({ options }), + }); + const queryRuntime = query({ + prompt: promptQueue, + options, + }); + const iterator = queryRuntime[Symbol.asyncIterator](); + try { + offerPrompt(0); + offerSteeringPrompts(); + const completed = await recordMessagesUntilTurnResults({ + iterator, + entries: input.entries, + scenario: input.scenario, + resultCount: input.prompts.length, + }); + if (!completed) { + throw new Error("Claude active steering query ended before the turn completed."); + } + if (offeredPrompts.size < input.prompts.length) { + throw new Error("Claude active steering prompts were not all offered before completion."); + } + promptQueue.close(); + queryRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "success", + }); + } catch (error) { + promptQueue.close(); + queryRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "error", + error: serializeReplayError(error, input.scenario), + }); + throw error; + } +} + +async function recordClaudeRestartingQueries(input: { + readonly scenario: string; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly sessionId: string; + readonly entries: Array; + readonly enableTools?: boolean; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly permissionMode?: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: boolean; +}): Promise { + for (const [index, prompt] of input.prompts.entries()) { + const promptQueue = new RecordingPromptQueue(); + const options = makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: input.sessionId, + resume: index > 0, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + } + : {}), + }); + + input.entries.push({ + type: "expect_outbound", + label: `query.open:${index + 1}`, + frame: makeClaudeQueryOpenFrame({ options }), + }); + const message = makeClaudeUserMessage({ text: prompt }); + input.entries.push({ + type: "expect_outbound", + label: `prompt.offer:${index + 1}`, + frame: makeClaudePromptOfferFrame(message), + }); + + try { + const queryRuntime = query({ + prompt: promptQueue, + options, + }); + promptQueue.offer(message); + promptQueue.close(); + const iterator = queryRuntime[Symbol.asyncIterator](); + for (;;) { + const next = await iterator.next(); + if (next.done === true) { + break; + } + const replayMessage = sanitizeSdkMessageForReplay({ + message: next.value, + scenario: input.scenario, + }); + input.entries.push({ + type: "emit_inbound", + label: replayMessage.type, + frame: replayMessage, + }); + } + input.entries.push({ + type: "runtime_exit", + status: "success", + }); + } catch (error) { + promptQueue.close(); + input.entries.push({ + type: "runtime_exit", + status: "error", + error: serializeReplayError(error, input.scenario), + }); + throw error; + } + } +} + +async function recordClaudeResumeAtCursorQuery(input: { + readonly scenario: string; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly sessionId: string; + readonly entries: Array; + readonly metadata: Record; + readonly enableTools?: boolean; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly permissionMode?: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: boolean; +}): Promise { + if (input.prompts.length !== 3) { + throw new Error( + `Claude resume-at-cursor replay scenario ${input.scenario} requires exactly three prompts.`, + ); + } + + const sourcePromptQueue = new RecordingPromptQueue(); + const sourceOptions = makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: input.sessionId, + resume: false, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + } + : {}), + }); + + input.entries.push({ + type: "expect_outbound", + label: "query.open:source", + frame: makeClaudeQueryOpenFrame({ options: sourceOptions }), + }); + const sourceRuntime = query({ + prompt: sourcePromptQueue, + options: sourceOptions, + }); + const sourceIterator = sourceRuntime[Symbol.asyncIterator](); + + try { + const sourceCursors: Array = []; + for (const [index, prompt] of input.prompts.slice(0, 2).entries()) { + const message = makeClaudeUserMessage({ text: prompt }); + input.entries.push({ + type: "expect_outbound", + label: `prompt.offer:${index + 1}`, + frame: makeClaudePromptOfferFrame(message), + }); + sourcePromptQueue.offer(message); + const result = await recordMessagesUntilTurnResultWithCursor({ + iterator: sourceIterator, + entries: input.entries, + scenario: input.scenario, + }); + if (!result.completed) { + throw new Error(`Claude source query ended before prompt ${index + 1} completed.`); + } + sourceCursors.push( + requireAssistantCursor({ + scenario: input.scenario, + promptIndex: index + 1, + cursor: result.assistantMessageUuid, + }), + ); + } + sourcePromptQueue.close(); + sourceRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "success", + }); + + const resumeSessionAt = sourceCursors[0]!; + input.metadata.resumeSessionAt = resumeSessionAt; + input.metadata.sourceAssistantMessageUuids = sourceCursors; + + const resumedPromptQueue = new RecordingPromptQueue(); + const resumedOptions = { + ...makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: input.sessionId, + resume: true, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + } + : {}), + }), + resumeSessionAt, + } satisfies ClaudeAgentSdkQueryOptions; + input.entries.push({ + type: "expect_outbound", + label: "query.open:resume_at_cursor", + frame: makeClaudeQueryOpenFrame({ options: resumedOptions }), + }); + const resumedMessage = makeClaudeUserMessage({ text: input.prompts[2]! }); + input.entries.push({ + type: "expect_outbound", + label: "prompt.offer:3", + frame: makeClaudePromptOfferFrame(resumedMessage), + }); + + const resumedRuntime = query({ + prompt: resumedPromptQueue, + options: resumedOptions, + }); + resumedPromptQueue.offer(resumedMessage); + resumedPromptQueue.close(); + const resumedIterator = resumedRuntime[Symbol.asyncIterator](); + await recordMessagesUntilIteratorDone({ + iterator: resumedIterator, + entries: input.entries, + scenario: input.scenario, + }); + resumedRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "success", + }); + } catch (error) { + sourcePromptQueue.close(); + sourceRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "error", + error: serializeReplayError(error, input.scenario), + }); + throw error; + } +} + +async function recordClaudeForkSessionQuery(input: { + readonly scenario: string; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly sessionId: string; + readonly entries: Array; + readonly metadata: Record; + readonly forkFromPromptIndex?: 1 | 2; + readonly sourcePromptCount?: number; + readonly forkPromptGroups?: ReadonlyArray>; + readonly sourceContinuationPromptCount?: number; + readonly enableTools?: boolean; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly permissionMode?: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: boolean; +}): Promise { + if (input.prompts.length < 2) { + throw new Error( + `Claude fork-session replay scenario ${input.scenario} requires at least two prompts.`, + ); + } + const sourceContinuationPromptCount = input.sourceContinuationPromptCount ?? 0; + const forkPromptEnd = input.prompts.length - sourceContinuationPromptCount; + const sourcePromptCount = input.sourcePromptCount ?? forkPromptEnd - 1; + if ( + sourceContinuationPromptCount < 0 || + sourceContinuationPromptCount >= input.prompts.length || + sourcePromptCount < 1 || + sourcePromptCount >= forkPromptEnd + ) { + throw new Error( + `Claude fork-session replay scenario ${input.scenario} requires at least one source prompt and one fork prompt.`, + ); + } + const forkFromPromptIndex = input.forkFromPromptIndex ?? sourcePromptCount; + if (forkFromPromptIndex > sourcePromptCount) { + throw new Error( + `Claude fork-session replay scenario ${input.scenario} cannot fork from prompt ${forkFromPromptIndex} after recording ${sourcePromptCount} source prompts.`, + ); + } + + const sourcePromptQueue = new RecordingPromptQueue(); + const sourceOptions = makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: input.sessionId, + resume: false, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + } + : {}), + }); + input.entries.push({ + type: "expect_outbound", + label: "query.open:source", + frame: makeClaudeQueryOpenFrame({ options: sourceOptions }), + }); + const sourceRuntime = query({ + prompt: sourcePromptQueue, + options: sourceOptions, + }); + const sourceIterator = sourceRuntime[Symbol.asyncIterator](); + + try { + const sourceCursors: Array = []; + for (const [index, prompt] of input.prompts.slice(0, sourcePromptCount).entries()) { + const message = makeClaudeUserMessage({ text: prompt }); + input.entries.push({ + type: "expect_outbound", + label: `prompt.offer:${index + 1}`, + frame: makeClaudePromptOfferFrame(message), + }); + sourcePromptQueue.offer(message); + const result = await recordMessagesUntilTurnResultWithCursor({ + iterator: sourceIterator, + entries: input.entries, + scenario: input.scenario, + }); + if (!result.completed) { + throw new Error(`Claude source query ended before prompt ${index + 1} completed.`); + } + sourceCursors.push( + requireAssistantCursor({ + scenario: input.scenario, + promptIndex: index + 1, + cursor: result.assistantMessageUuid, + }), + ); + } + sourcePromptQueue.close(); + sourceRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "success", + }); + + const upToMessageId = sourceCursors[forkFromPromptIndex - 1]!; + const forkPrompts = input.prompts.slice(sourcePromptCount, forkPromptEnd); + const forkPromptGroups = input.forkPromptGroups ?? [forkPrompts]; + if ( + forkPromptGroups.length === 0 || + forkPromptGroups.some((group) => group.length === 0) || + forkPromptGroups.flat().join("\n") !== forkPrompts.join("\n") + ) { + throw new Error( + `Claude fork-session replay scenario ${input.scenario} has invalid fork prompt groups.`, + ); + } + input.metadata.sourceAssistantMessageUuids = sourceCursors; + input.metadata.forkUpToMessageId = upToMessageId; + const forkedNativeSessionIds: Array = []; + let promptOrdinal = sourcePromptCount; + for (const [groupIndex, forkPrompts] of forkPromptGroups.entries()) { + const labelSuffix = forkPromptGroups.length === 1 ? "" : `:${groupIndex + 1}`; + input.entries.push({ + type: "expect_outbound", + label: `session.fork${labelSuffix}`, + frame: { + type: "session.fork", + sessionId: input.sessionId, + options: { + dir: sanitizedReplayCwd(input.scenario), + upToMessageId, + }, + }, + }); + const forked = await forkSession(input.sessionId, { + dir: input.cwd, + upToMessageId, + }); + forkedNativeSessionIds.push(forked.sessionId); + input.entries.push({ + type: "emit_inbound", + label: `session.forked${labelSuffix}`, + frame: { + type: "session.forked", + sessionId: forked.sessionId, + }, + }); + + const targetPromptQueue = new RecordingPromptQueue(); + const targetOptions = makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: forked.sessionId, + resume: true, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + } + : {}), + }); + input.entries.push({ + type: "expect_outbound", + label: `query.open:fork${labelSuffix}`, + frame: makeClaudeQueryOpenFrame({ options: targetOptions }), + }); + const targetRuntime = query({ + prompt: targetPromptQueue, + options: targetOptions, + }); + const targetIterator = targetRuntime[Symbol.asyncIterator](); + for (const prompt of forkPrompts) { + promptOrdinal += 1; + const targetMessage = makeClaudeUserMessage({ text: prompt }); + input.entries.push({ + type: "expect_outbound", + label: `prompt.offer:${promptOrdinal}`, + frame: makeClaudePromptOfferFrame(targetMessage), + }); + targetPromptQueue.offer(targetMessage); + const completed = await recordMessagesUntilTurnResult({ + iterator: targetIterator, + entries: input.entries, + scenario: input.scenario, + }); + if (!completed) { + throw new Error(`Claude fork query ended before prompt ${promptOrdinal} completed.`); + } + } + targetPromptQueue.close(); + targetRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "success", + }); + } + input.metadata.forkedNativeSessionId = forkedNativeSessionIds[0]; + input.metadata.forkedNativeSessionIds = forkedNativeSessionIds; + if (sourceContinuationPromptCount > 0) { + const continuationPromptQueue = new RecordingPromptQueue(); + const continuationOptions = makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: input.sessionId, + resume: true, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + } + : {}), + }); + input.entries.push({ + type: "expect_outbound", + label: "query.open:source-continuation", + frame: makeClaudeQueryOpenFrame({ options: continuationOptions }), + }); + const continuationRuntime = query({ + prompt: continuationPromptQueue, + options: continuationOptions, + }); + const continuationIterator = continuationRuntime[Symbol.asyncIterator](); + for (const prompt of input.prompts.slice(forkPromptEnd)) { + promptOrdinal += 1; + const continuationMessage = makeClaudeUserMessage({ text: prompt }); + input.entries.push({ + type: "expect_outbound", + label: `prompt.offer:${promptOrdinal}`, + frame: makeClaudePromptOfferFrame(continuationMessage), + }); + continuationPromptQueue.offer(continuationMessage); + const completed = await recordMessagesUntilTurnResult({ + iterator: continuationIterator, + entries: input.entries, + scenario: input.scenario, + }); + if (!completed) { + throw new Error( + `Claude source continuation ended before prompt ${promptOrdinal} completed.`, + ); + } + } + continuationPromptQueue.close(); + continuationRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "success", + }); + input.metadata.sourceContinuationPromptCount = sourceContinuationPromptCount; + } + } catch (error) { + sourcePromptQueue.close(); + sourceRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "error", + error: serializeReplayError(error, input.scenario), + }); + throw error; + } +} + +async function recordInterruptedClaudeQuery(input: { + readonly scenario: string; + readonly prompt: string; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly sessionId: string; + readonly resume: boolean; + readonly entries: Array; + readonly queryOpenLabel: string; + readonly promptOfferLabel: string; + readonly interruptLabel: string; + readonly interruptAfter?: "prompt_offer" | "tool_use"; + readonly enableTools?: boolean; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly permissionMode?: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: boolean; +}): Promise { + const promptQueue = new RecordingPromptQueue(); + const options = makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: input.sessionId, + resume: input.resume, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + } + : {}), + }); + input.entries.push({ + type: "expect_outbound", + label: input.queryOpenLabel, + frame: makeClaudeQueryOpenFrame({ options }), + }); + const runtime = query({ + prompt: promptQueue, + options, + }); + const iterator = runtime[Symbol.asyncIterator](); + const message = makeClaudeUserMessage({ text: input.prompt }); + input.entries.push({ + type: "expect_outbound", + label: input.promptOfferLabel, + frame: makeClaudePromptOfferFrame(message), + }); + promptQueue.offer(message); + if (input.interruptAfter === "tool_use") { + await recordMessagesUntilFirstToolUse({ + iterator, + entries: input.entries, + scenario: input.scenario, + }); + await Effect.runPromise(Effect.sleep(Duration.millis(250))); + } + input.entries.push({ + type: "expect_outbound", + label: input.interruptLabel, + frame: { type: "query.interrupt" }, + }); + + try { + let cancelledError: unknown; + try { + await runtime.interrupt(); + } catch (error) { + cancelledError = error; + } + promptQueue.close(); + runtime.close(); + try { + await recordMessagesUntilIteratorDone({ + iterator, + entries: input.entries, + scenario: input.scenario, + }); + } catch (error) { + cancelledError = error; + } + input.entries.push({ + type: "runtime_exit", + status: cancelledError === undefined ? "success" : "cancelled", + ...(cancelledError === undefined + ? {} + : { error: serializeReplayError(cancelledError, input.scenario) }), + }); + } catch (error) { + promptQueue.close(); + runtime.close(); + input.entries.push({ + type: "runtime_exit", + status: "error", + error: serializeReplayError(error, input.scenario), + }); + throw error; + } +} + +async function recordClaudeInterruptQuery(input: { + readonly scenario: string; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly sessionId: string; + readonly entries: Array; + readonly enableTools?: boolean; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly permissionMode?: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: boolean; + readonly interruptAfter?: "prompt_offer" | "tool_use"; +}): Promise { + if (input.prompts.length !== 1) { + throw new Error( + `Claude interrupt replay scenario ${input.scenario} requires exactly one prompt.`, + ); + } + + await recordInterruptedClaudeQuery({ + scenario: input.scenario, + prompt: input.prompts[0]!, + modelSelection: input.modelSelection, + cwd: input.cwd, + sessionId: input.sessionId, + resume: false, + entries: input.entries, + queryOpenLabel: "query.open", + promptOfferLabel: "prompt.offer:1", + interruptLabel: "query.interrupt:1", + ...(input.interruptAfter === undefined ? {} : { interruptAfter: input.interruptAfter }), + ...(input.enableTools === undefined ? {} : { enableTools: input.enableTools }), + ...(input.tools === undefined ? {} : { tools: input.tools }), + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: input.allowDangerouslySkipPermissions }), + }); +} + +async function recordClaudeInterruptRestartQuery(input: { + readonly scenario: string; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly sessionId: string; + readonly entries: Array; + readonly enableTools?: boolean; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly permissionMode?: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: boolean; + readonly interruptAfter?: "prompt_offer" | "tool_use"; +}): Promise { + if (input.prompts.length !== 2) { + throw new Error( + `Claude interrupt-restart replay scenario ${input.scenario} requires exactly two prompts.`, + ); + } + + await recordInterruptedClaudeQuery({ + scenario: input.scenario, + prompt: input.prompts[0]!, + modelSelection: input.modelSelection, + cwd: input.cwd, + sessionId: input.sessionId, + resume: false, + entries: input.entries, + queryOpenLabel: "query.open:1", + promptOfferLabel: "prompt.offer:1", + interruptLabel: "query.interrupt:1", + ...(input.interruptAfter === undefined ? {} : { interruptAfter: input.interruptAfter }), + ...(input.enableTools === undefined ? {} : { enableTools: input.enableTools }), + ...(input.tools === undefined ? {} : { tools: input.tools }), + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: input.allowDangerouslySkipPermissions }), + }); + + const secondPromptQueue = new RecordingPromptQueue(); + const secondOptions = makeClaudeQueryOptions({ + modelSelection: input.modelSelection, + nativeThreadId: input.sessionId, + resume: true, + cwd: input.cwd, + ...(input.enableTools === true + ? { + tools: input.tools ?? { type: "preset", preset: "claude_code" }, + permissionMode: input.permissionMode ?? "default", + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined + ? {} + : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + } + : {}), + }); + input.entries.push({ + type: "expect_outbound", + label: "query.open:2", + frame: makeClaudeQueryOpenFrame({ options: secondOptions }), + }); + const secondMessage = makeClaudeUserMessage({ text: input.prompts[1]! }); + input.entries.push({ + type: "expect_outbound", + label: "prompt.offer:2", + frame: makeClaudePromptOfferFrame(secondMessage), + }); + + try { + const secondRuntime = query({ + prompt: secondPromptQueue, + options: secondOptions, + }); + secondPromptQueue.offer(secondMessage); + secondPromptQueue.close(); + const secondIterator = secondRuntime[Symbol.asyncIterator](); + await recordMessagesUntilIteratorDone({ + iterator: secondIterator, + entries: input.entries, + scenario: input.scenario, + }); + secondRuntime.close(); + input.entries.push({ + type: "runtime_exit", + status: "success", + }); + } catch (error) { + secondPromptQueue.close(); + input.entries.push({ + type: "runtime_exit", + status: "error", + error: serializeReplayError(error, input.scenario), + }); + throw error; + } +} + +export async function recordClaudeAgentSdkReplayTranscript(input: { + readonly scenario: string; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly sessionId?: string; + readonly queryMode?: + | "streaming" + | "restart" + | "resume_at_cursor" + | "fork_session" + | "fork_session_prior_turn" + | "fork_session_continue" + | "fork_session_siblings" + | "fork_session_merge_back" + | "fork_session_merge_back_siblings" + | "active_steering" + | "interrupt" + | "interrupt_restart"; + readonly enableTools?: boolean; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly permissionMode?: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: boolean; + readonly enablePermissionCallback?: boolean; + readonly permissionDecision?: ProviderApprovalDecision; + readonly interruptAfter?: "prompt_offer" | "tool_use"; +}): Promise { + if (input.prompts.length === 0) { + throw new Error( + `Claude Agent SDK replay scenario ${input.scenario} needs at least one prompt.`, + ); + } + + const entries: Array = []; + const sessionId = input.sessionId ?? (await Effect.runPromise(randomUuidV4)); + const queryMode = input.queryMode ?? "streaming"; + const recordingMetadata: Record = {}; + if (queryMode === "streaming") { + await recordClaudeStreamingQuery({ + scenario: input.scenario, + prompts: input.prompts, + modelSelection: input.modelSelection, + cwd: input.cwd, + sessionId, + entries, + ...(input.enableTools === undefined ? {} : { enableTools: input.enableTools }), + ...(input.tools === undefined ? {} : { tools: input.tools }), + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: input.allowDangerouslySkipPermissions }), + ...(input.enablePermissionCallback === undefined + ? {} + : { enablePermissionCallback: input.enablePermissionCallback }), + ...(input.permissionDecision === undefined + ? {} + : { permissionDecision: input.permissionDecision }), + }); + } else if (queryMode === "active_steering") { + await recordClaudeActiveSteeringQuery({ + scenario: input.scenario, + prompts: input.prompts, + modelSelection: input.modelSelection, + cwd: input.cwd, + sessionId, + entries, + ...(input.enableTools === undefined ? {} : { enableTools: input.enableTools }), + ...(input.tools === undefined ? {} : { tools: input.tools }), + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: input.allowDangerouslySkipPermissions }), + ...(input.enablePermissionCallback === undefined + ? {} + : { enablePermissionCallback: input.enablePermissionCallback }), + ...(input.permissionDecision === undefined + ? {} + : { permissionDecision: input.permissionDecision }), + }); + } else if (queryMode === "restart") { + await recordClaudeRestartingQueries({ + scenario: input.scenario, + prompts: input.prompts, + modelSelection: input.modelSelection, + cwd: input.cwd, + sessionId, + entries, + ...(input.enableTools === undefined ? {} : { enableTools: input.enableTools }), + ...(input.tools === undefined ? {} : { tools: input.tools }), + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: input.allowDangerouslySkipPermissions }), + }); + } else if (queryMode === "resume_at_cursor") { + await recordClaudeResumeAtCursorQuery({ + scenario: input.scenario, + prompts: input.prompts, + modelSelection: input.modelSelection, + cwd: input.cwd, + sessionId, + entries, + metadata: recordingMetadata, + ...(input.enableTools === undefined ? {} : { enableTools: input.enableTools }), + ...(input.tools === undefined ? {} : { tools: input.tools }), + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: input.allowDangerouslySkipPermissions }), + }); + } else if ( + queryMode === "fork_session" || + queryMode === "fork_session_prior_turn" || + queryMode === "fork_session_continue" || + queryMode === "fork_session_siblings" || + queryMode === "fork_session_merge_back" || + queryMode === "fork_session_merge_back_siblings" + ) { + await recordClaudeForkSessionQuery({ + scenario: input.scenario, + prompts: input.prompts, + modelSelection: input.modelSelection, + cwd: input.cwd, + sessionId, + entries, + metadata: recordingMetadata, + ...(queryMode === "fork_session_prior_turn" ? { forkFromPromptIndex: 1 as const } : {}), + ...(queryMode === "fork_session_continue" ? { sourcePromptCount: 1 } : {}), + ...(queryMode === "fork_session_siblings" + ? { + sourcePromptCount: 1, + forkPromptGroups: [[input.prompts[1]!], [input.prompts[2]!]], + } + : {}), + ...(queryMode === "fork_session_merge_back" + ? { + sourcePromptCount: 1, + forkPromptGroups: [[input.prompts[1]!]], + sourceContinuationPromptCount: 2, + } + : {}), + ...(queryMode === "fork_session_merge_back_siblings" + ? { + sourcePromptCount: 1, + forkPromptGroups: [[input.prompts[1]!], [input.prompts[2]!]], + sourceContinuationPromptCount: 3, + } + : {}), + ...(input.enableTools === undefined ? {} : { enableTools: input.enableTools }), + ...(input.tools === undefined ? {} : { tools: input.tools }), + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: input.allowDangerouslySkipPermissions }), + }); + } else if (queryMode === "interrupt") { + await recordClaudeInterruptQuery({ + scenario: input.scenario, + prompts: input.prompts, + modelSelection: input.modelSelection, + cwd: input.cwd, + sessionId, + entries, + ...(input.enableTools === undefined ? {} : { enableTools: input.enableTools }), + ...(input.tools === undefined ? {} : { tools: input.tools }), + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: input.allowDangerouslySkipPermissions }), + ...(input.interruptAfter === undefined ? {} : { interruptAfter: input.interruptAfter }), + }); + } else { + await recordClaudeInterruptRestartQuery({ + scenario: input.scenario, + prompts: input.prompts, + modelSelection: input.modelSelection, + cwd: input.cwd, + sessionId, + entries, + ...(input.enableTools === undefined ? {} : { enableTools: input.enableTools }), + ...(input.tools === undefined ? {} : { tools: input.tools }), + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: input.allowDangerouslySkipPermissions }), + ...(input.interruptAfter === undefined ? {} : { interruptAfter: input.interruptAfter }), + }); + } + + return { + provider: CLAUDE_PROVIDER, + protocol: CLAUDE_AGENT_SDK_REPLAY_PROTOCOL, + version: "0.2.111", + scenario: input.scenario, + metadata: { + prompts: [...input.prompts], + model: input.modelSelection.model, + nativeSessionId: sessionId, + queryMode, + tools: input.enableTools === true ? (input.tools ?? "claude_code") : "none", + ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }), + ...(input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: input.disallowedTools }), + ...(input.enablePermissionCallback === undefined + ? {} + : { enablePermissionCallback: input.enablePermissionCallback }), + ...(input.permissionDecision === undefined + ? {} + : { permissionDecision: input.permissionDecision }), + ...(input.interruptAfter === undefined ? {} : { interruptAfter: input.interruptAfter }), + generatedBy: "recordClaudeAgentSdkReplayTranscript", + ...recordingMetadata, + }, + entries, + }; +} + +export const ClaudeOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness< + ClaudeAgentSdkReplayTranscript, + ClaudeOrchestratorReplayHarnessError +> = { + driver: CLAUDE_PROVIDER, + decodeTranscript: (transcript) => + Schema.decodeUnknownEffect(ClaudeAgentSdkReplayTranscript)(transcript).pipe( + Effect.mapError( + (cause) => + new ClaudeReplayTranscriptDecodeError({ + ...metadataFromTranscript(transcript), + cause, + }), + ), + ), + makeProviderAdapterRegistryLayer: (transcript) => + makeClaudeProviderAdapterRegistryReplayLayer(transcript), +}; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts new file mode 100644 index 00000000000..8ac5ff6c190 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -0,0 +1,3337 @@ +import { + type CanUseTool, + forkSession as forkClaudeSession, + type ForkSessionOptions, + type ForkSessionResult, + query, + type Options as ClaudeQueryOptions, + type PermissionMode, + type PermissionResult, + type Query as ClaudeQuery, + type Settings as ClaudeSdkSettings, + type SDKAssistantMessage, + type SDKMessage, + type SDKResultMessage, + type SDKUserMessage, +} from "@anthropic-ai/claude-agent-sdk"; +import type { WebSearchOutput } from "@anthropic-ai/claude-agent-sdk/sdk-tools"; +import { parseCliArgs } from "@t3tools/shared/cliArgs"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { + ClaudeSettings, + defaultInstanceIdForDriver, + type ModelSelection, + type OrchestrationV2ConversationMessage, + type OrchestrationV2ExecutionNode, + type OrchestrationV2ProviderCapabilities, + type OrchestrationV2ProviderSession, + type OrchestrationV2ProviderThread, + type OrchestrationV2ProviderTurn, + type OrchestrationV2RuntimeRequest, + type OrchestrationV2Subagent, + type OrchestrationV2TurnItem, + type OrchestrationV2WebSearchResult, + type ProviderApprovalDecision, + ProviderDriverKind, + type ProviderInstanceId, + type ProviderRequestKind, + type ThreadId, +} from "@t3tools/contracts"; + +import * as Cause from "effect/Cause"; +import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; +import * as DateTime from "effect/DateTime"; +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; + +import { ServerConfig } from "../../config.ts"; +import { makeClaudeEnvironment } from "../../provider/Drivers/ClaudeHome.ts"; +import { + type EventNdjsonLogger, + makeEventNdjsonLogger, +} from "../../provider/Layers/EventNdjsonLogger.ts"; +import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; +import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; +import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts"; +import { + ProviderAdapterEnsureThreadError, + ProviderAdapterForkThreadError, + ProviderAdapterInterruptError, + ProviderAdapterOpenSessionError, + ProviderAdapterProtocolError, + ProviderAdapterReadThreadSnapshotError, + ProviderAdapterResumeThreadError, + ProviderAdapterRollbackThreadError, + ProviderAdapterRuntimeRequestResponseError, + ProviderAdapterSteerRunError, + ProviderAdapterTurnStartError, + ProviderAdapterV2, + type ProviderAdapterV2EnsureThreadInput, + type ProviderAdapterV2Event, + type ProviderAdapterV2ForkThreadInput, + type ProviderAdapterV2InterruptInput, + type ProviderAdapterV2OpenSessionInput, + type ProviderAdapterV2RollbackThreadInput, + type ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2Shape, + type ProviderAdapterV2SessionRuntime, + type ProviderAdapterV2SteerInput, + type ProviderAdapterV2TurnInput, +} from "../ProviderAdapter.ts"; +import { + ProviderAdapterDriverCreateError, + type ProviderAdapterDriver, + type ProviderAdapterDriverCreateInput, +} from "../ProviderAdapterDriver.ts"; +import { + makeSubagentChildThread, + makeSubagentConversationArtifacts, + subagentThreadTitle, +} from "../SubagentProjection.ts"; + +export const CLAUDE_PROVIDER = ProviderDriverKind.make("claudeAgent"); +export const CLAUDE_AGENT_SDK_QUERY_PROTOCOL = "claude-agent-sdk.query" as const; +export const CLAUDE_DRIVER_KIND = CLAUDE_PROVIDER; +export const CLAUDE_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(CLAUDE_DRIVER_KIND); +const DEFAULT_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({}); + +export const ClaudeProviderCapabilitiesV2 = { + sessions: { + supportsMultipleProviderThreadsPerSession: false, + supportsModelSwitchInSession: true, + supportsProviderSwitchingViaHandoff: true, + supportsRuntimeModeSwitchInSession: false, + pendingRequestsSurviveRestart: false, + }, + threads: { + canCreateEmptyThread: true, + canReadThreadSnapshot: false, + canRollbackThread: true, + canForkThread: true, + canForkFromTurn: true, + canForkFromSubagentThread: false, + exposesNativeThreadId: true, + }, + turns: { + exposesNativeTurnId: false, + emitsTurnStarted: true, + emitsTurnCompleted: true, + supportsInterrupt: true, + supportsActiveSteering: true, + supportsSteeringByInterruptRestart: false, + supportsQueuedMessages: true, + terminalStatusQuality: "strong", + }, + streaming: { + streamsAssistantText: true, + streamsReasoning: false, + streamsToolOutput: false, + streamsPlanText: false, + emitsMessageCompleted: true, + }, + tools: { + exposesToolItemIds: true, + emitsToolStarted: true, + emitsToolCompleted: true, + emitsToolOutput: true, + supportsMcpTools: true, + supportsDynamicToolCallbacks: true, + }, + approvals: { + supportsCommandApproval: true, + supportsFileReadApproval: true, + supportsFileChangeApproval: true, + supportsApplyPatchApproval: false, + approvalsHaveNativeRequestIds: true, + approvalCallbacksAreLiveOnly: true, + approvalsCanOriginateFromSubagents: false, + }, + planning: { + emitsPlanUpdated: false, + emitsTodoList: false, + emitsProposedPlan: false, + supportsStructuredQuestions: false, + planDeltasHaveItemIds: false, + }, + subagents: { + supportsSubagents: true, + exposesSubagentThreadIds: false, + emitsSubagentLifecycle: true, + canWaitForSubagents: false, + canCloseSubagents: false, + canForkSubagentThread: false, + }, + context: { + acceptsSystemContext: true, + acceptsDeveloperContext: true, + acceptsSyntheticUserContext: true, + canGenerateSummaries: true, + canConsumeHandoffSummaries: true, + supportsDeltaHandoff: true, + supportsFullThreadHandoff: true, + maxRecommendedHandoffChars: null, + }, + checkpointing: { + appCanCheckpointFilesystem: true, + supportsNestedCheckpointScopes: true, + providerCanRollbackConversation: true, + providerRollbackReturnsSnapshot: true, + providerCanReadConversationSnapshot: false, + }, + identity: { + nativeThreadIds: "strong", + nativeTurnIds: "weak", + nativeItemIds: "strong", + nativeRequestIds: "strong", + }, +} satisfies OrchestrationV2ProviderCapabilities; + +const CLAUDE_CODE_PRESET_TOOLS = { + type: "preset", + preset: "claude_code", +} satisfies NonNullable; + +export type ClaudeAgentSdkQueryToolList = ReadonlyArray; +export interface ClaudeAgentSdkQueryPresetTools { + readonly type: "preset"; + readonly preset: "claude_code"; +} +export type ClaudeAgentSdkQueryTools = ClaudeAgentSdkQueryToolList | ClaudeAgentSdkQueryPresetTools; + +export const CLAUDE_READ_ONLY_ALLOWED_TOOLS = ["Read", "Glob", "Grep"] as const; + +function claudeAgentSdkQueryToolsForSdk( + tools: ClaudeAgentSdkQueryTools, +): NonNullable { + if (isClaudeAgentSdkQueryToolList(tools)) { + return [...tools]; + } + return { type: tools.type, preset: tools.preset }; +} + +function isClaudeAgentSdkQueryToolList( + tools: ClaudeAgentSdkQueryTools, +): tools is ClaudeAgentSdkQueryToolList { + return Array.isArray(tools); +} + +type ClaudeAgentSdkThreadIdentity = + | { + readonly sessionId: string; + readonly resume?: never; + } + | { + readonly sessionId?: never; + readonly resume: string; + }; + +export type ClaudeAgentSdkQueryOptions = Omit< + ClaudeQueryOptions, + "maxTurns" | "model" | "permissionMode" | "resume" | "sessionId" | "tools" +> & { + readonly model: string; + readonly tools: NonNullable; + readonly permissionMode: NonNullable; +} & ClaudeAgentSdkThreadIdentity; + +export interface ClaudeAgentSdkQueryOpenInput { + readonly options: ClaudeAgentSdkQueryOptions; + readonly threadId: ThreadId; + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; +} + +export interface ClaudeAgentSdkQuerySession { + readonly messages: Stream.Stream; + readonly offer: (message: SDKUserMessage) => Effect.Effect; + readonly setModel: (model: string) => Effect.Effect; + readonly interrupt: Effect.Effect; + readonly close: Effect.Effect; +} + +type ClaudeQueryStreamExit = Exit.Exit; + +export class ClaudeAgentSdkQueryRunnerError extends Schema.TaggedErrorClass()( + "ClaudeAgentSdkQueryRunnerError", + { + method: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return "Claude Agent SDK query failed."; + } +} + +export interface ClaudeAgentSdkQueryRunnerShape { + readonly allocateSessionId: Effect.Effect; + readonly open: ( + input: ClaudeAgentSdkQueryOpenInput, + ) => Effect.Effect; + readonly forkSession: ( + input: ClaudeAgentSdkSessionForkInput, + ) => Effect.Effect; + readonly assertComplete: Effect.Effect; +} + +export class ClaudeAgentSdkQueryRunner extends Context.Service< + ClaudeAgentSdkQueryRunner, + ClaudeAgentSdkQueryRunnerShape +>()("t3/orchestration-v2/Adapters/ClaudeAdapterV2/ClaudeAgentSdkQueryRunner") {} + +export interface ClaudeAgentSdkSessionForkInput { + readonly sessionId: string; + readonly options: ForkSessionOptions; + readonly threadId: ThreadId; + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; +} + +function queryRunnerError(cause: unknown, method: string): ClaudeAgentSdkQueryRunnerError { + return Schema.is(ClaudeAgentSdkQueryRunnerError)(cause) + ? cause + : new ClaudeAgentSdkQueryRunnerError({ cause, method }); +} + +function closeClaudeQuery(queryRuntime: ClaudeQuery) { + return Effect.try({ + try: () => queryRuntime.close(), + catch: (cause) => queryRunnerError(cause, "close"), + }); +} + +export interface ClaudeAgentSdkLoggedQueryOptions { + readonly model: ClaudeAgentSdkQueryOptions["model"]; + readonly tools: ClaudeAgentSdkQueryOptions["tools"]; + readonly permissionMode: ClaudeAgentSdkQueryOptions["permissionMode"]; + readonly sessionId?: string; + readonly resume?: string; + readonly resumeSessionAt?: ClaudeAgentSdkQueryOptions["resumeSessionAt"]; + readonly cwd?: ClaudeAgentSdkQueryOptions["cwd"]; + readonly allowedTools?: ClaudeAgentSdkQueryOptions["allowedTools"]; + readonly disallowedTools?: ClaudeAgentSdkQueryOptions["disallowedTools"]; + readonly settings?: ClaudeAgentSdkQueryOptions["settings"]; + readonly pathToClaudeCodeExecutable?: ClaudeAgentSdkQueryOptions["pathToClaudeCodeExecutable"]; + readonly extraArgs?: ClaudeAgentSdkQueryOptions["extraArgs"]; + readonly allowDangerouslySkipPermissions?: true; + readonly hasCanUseTool?: true; + readonly hasEnvironment?: true; + readonly hasMcpServers?: true; +} + +export type ClaudeAgentSdkProtocolLogEvent = + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "query.open"; + readonly options: ClaudeAgentSdkLoggedQueryOptions; + }; + } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "prompt.offer"; + readonly message: SDKUserMessage; + }; + } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "query.set_model"; + readonly model: string; + }; + } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "query.interrupt"; + }; + } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "query.close"; + }; + } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "session.fork"; + readonly sessionId: string; + readonly options: ForkSessionOptions; + }; + } + | { + readonly direction: "incoming"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "session.forked"; + readonly sessionId: string; + }; + } + | { + readonly direction: "incoming"; + readonly stage: "decoded"; + readonly payload: SDKMessage; + }; + +export type ClaudeAgentSdkProtocolLogger = ( + event: ClaudeAgentSdkProtocolLogEvent, +) => Effect.Effect; + +export function loggedClaudeQueryOptions( + options: ClaudeAgentSdkQueryOptions, +): ClaudeAgentSdkLoggedQueryOptions { + return { + model: options.model, + tools: options.tools, + permissionMode: options.permissionMode, + ...(options.sessionId === undefined ? {} : { sessionId: options.sessionId }), + ...(options.resume === undefined ? {} : { resume: options.resume }), + ...(options.resumeSessionAt === undefined ? {} : { resumeSessionAt: options.resumeSessionAt }), + ...(options.cwd === undefined ? {} : { cwd: options.cwd }), + ...(options.allowedTools === undefined ? {} : { allowedTools: options.allowedTools }), + ...(options.disallowedTools === undefined ? {} : { disallowedTools: options.disallowedTools }), + ...(options.settings === undefined ? {} : { settings: options.settings }), + ...(options.pathToClaudeCodeExecutable === undefined + ? {} + : { pathToClaudeCodeExecutable: options.pathToClaudeCodeExecutable }), + ...(options.extraArgs === undefined ? {} : { extraArgs: options.extraArgs }), + ...(options.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + ...(options.canUseTool === undefined ? {} : { hasCanUseTool: true }), + ...(options.env === undefined ? {} : { hasEnvironment: true }), + ...(options.mcpServers === undefined ? {} : { hasMcpServers: true }), + }; +} + +export function makeClaudeAgentSdkProtocolLogger(input: { + readonly nativeEventLogger: EventNdjsonLogger | undefined; + readonly threadId: ThreadId; + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; +}): ClaudeAgentSdkProtocolLogger | undefined { + const { nativeEventLogger } = input; + if (nativeEventLogger === undefined) { + return undefined; + } + + return (event) => + nativeEventLogger + .write( + { + provider: CLAUDE_PROVIDER, + protocol: CLAUDE_AGENT_SDK_QUERY_PROTOCOL, + kind: "protocol", + providerSessionId: input.providerSessionId, + event, + }, + input.threadId, + ) + .pipe(Effect.ignore); +} + +export const claudeAgentSdkQueryRunnerLiveLayer: Layer.Layer< + ClaudeAgentSdkQueryRunner, + never, + Crypto.Crypto | ServerConfig +> = Layer.effect( + ClaudeAgentSdkQueryRunner, + Effect.gen(function* () { + const { providerEventLogPath } = yield* ServerConfig; + const crypto = yield* Crypto.Crypto; + const nativeEventLogger = yield* makeEventNdjsonLogger(providerEventLogPath, { + stream: "native", + }); + + return ClaudeAgentSdkQueryRunner.of({ + allocateSessionId: crypto.randomUUIDv4.pipe( + Effect.mapError((cause) => queryRunnerError(cause, "allocateSessionId")), + ), + open: Effect.fn("ClaudeAgentSdkQueryRunner.open")(function* ( + input: ClaudeAgentSdkQueryOpenInput, + ) { + const protocolLogger = makeClaudeAgentSdkProtocolLogger({ + nativeEventLogger, + threadId: input.threadId, + providerSessionId: input.providerSessionId, + }); + const logProtocolEvent = (event: ClaudeAgentSdkProtocolLogEvent) => + protocolLogger === undefined ? Effect.void : protocolLogger(event); + const promptQueue = yield* Queue.unbounded(); + const prompt = Stream.fromQueue(promptQueue).pipe( + Stream.catchCause((cause) => + Cause.hasInterruptsOnly(cause) ? Stream.empty : Stream.failCause(cause), + ), + Stream.toAsyncIterable, + ); + const queryRuntime = yield* Effect.try({ + try: () => + query({ + prompt, + options: input.options, + }), + catch: (cause) => queryRunnerError(cause, "query"), + }); + yield* logProtocolEvent({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "query.open", + options: loggedClaudeQueryOptions(input.options), + }, + }); + + return { + messages: Stream.fromAsyncIterable(queryRuntime, (cause) => + queryRunnerError(cause, "fromAsyncIterable"), + ).pipe( + Stream.tap((message) => + logProtocolEvent({ + direction: "incoming", + stage: "decoded", + payload: message, + }), + ), + ), + offer: (message) => + Queue.offer(promptQueue, message).pipe( + Effect.asVoid, + Effect.tap(() => + logProtocolEvent({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "prompt.offer", + message, + }, + }), + ), + ), + setModel: (model) => + Effect.tryPromise({ + try: () => queryRuntime.setModel(model), + catch: (cause) => queryRunnerError(cause, "setModel"), + }).pipe( + Effect.tap(() => + logProtocolEvent({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "query.set_model", + model, + }, + }), + ), + ), + interrupt: Effect.tryPromise({ + try: () => queryRuntime.interrupt(), + catch: (cause) => queryRunnerError(cause, "interrupt"), + }).pipe( + Effect.tap(() => + logProtocolEvent({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "query.interrupt", + }, + }), + ), + ), + close: Queue.shutdown(promptQueue).pipe( + Effect.andThen(closeClaudeQuery(queryRuntime)), + Effect.tap(() => + logProtocolEvent({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "query.close", + }, + }), + ), + ), + } satisfies ClaudeAgentSdkQuerySession; + }), + forkSession: Effect.fn("ClaudeAgentSdkQueryRunner.forkSession")(function* ( + input: ClaudeAgentSdkSessionForkInput, + ) { + const protocolLogger = makeClaudeAgentSdkProtocolLogger({ + nativeEventLogger, + threadId: input.threadId, + providerSessionId: input.providerSessionId, + }); + const logProtocolEvent = (event: ClaudeAgentSdkProtocolLogEvent) => + protocolLogger === undefined ? Effect.void : protocolLogger(event); + yield* logProtocolEvent({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "session.fork", + sessionId: input.sessionId, + options: input.options, + }, + }); + const result = yield* Effect.tryPromise({ + try: () => forkClaudeSession(input.sessionId, input.options), + catch: (cause) => queryRunnerError(cause, "forkSession"), + }); + yield* logProtocolEvent({ + direction: "incoming", + stage: "decoded", + payload: { + type: "session.forked", + sessionId: result.sessionId, + }, + }); + return result; + }), + assertComplete: Effect.void, + }); + }), +); + +export function makeClaudeQueryOptions(input: { + readonly modelSelection: ModelSelection; + readonly nativeThreadId: string; + readonly resume: boolean; + readonly resumeSessionAt?: string; + readonly cwd: string | null; + readonly settings?: ClaudeSettings; + readonly sdkSettings?: string | ClaudeSdkSettings; + readonly environment?: NodeJS.ProcessEnv; + readonly mcpServers?: ClaudeQueryOptions["mcpServers"]; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly allowedTools?: ReadonlyArray; + readonly disallowedTools?: ReadonlyArray; + readonly permissionMode?: PermissionMode; + readonly canUseTool?: CanUseTool; + readonly allowDangerouslySkipPermissions?: boolean; +}): ClaudeAgentSdkQueryOptions { + const extraArgs = + input.settings === undefined ? {} : parseCliArgs(input.settings.launchArgs).flags; + const threadIdentity: ClaudeAgentSdkThreadIdentity = input.resume + ? { resume: input.nativeThreadId } + : { sessionId: input.nativeThreadId }; + const selectedTools = input.tools ?? CLAUDE_CODE_PRESET_TOOLS; + const options: ClaudeAgentSdkQueryOptions = { + model: input.modelSelection.model, + tools: claudeAgentSdkQueryToolsForSdk(selectedTools), + permissionMode: input.permissionMode ?? "default", + ...threadIdentity, + ...(input.resumeSessionAt === undefined ? {} : { resumeSessionAt: input.resumeSessionAt }), + ...(input.allowedTools === undefined ? {} : { allowedTools: [...input.allowedTools] }), + ...(input.disallowedTools === undefined ? {} : { disallowedTools: [...input.disallowedTools] }), + ...(input.canUseTool === undefined ? {} : { canUseTool: input.canUseTool }), + ...(input.allowDangerouslySkipPermissions === true + ? { allowDangerouslySkipPermissions: true } + : {}), + ...(input.sdkSettings === undefined ? {} : { settings: input.sdkSettings }), + ...(input.settings?.binaryPath + ? { pathToClaudeCodeExecutable: input.settings.binaryPath } + : {}), + ...(input.environment === undefined ? {} : { env: input.environment }), + ...(input.mcpServers === undefined ? {} : { mcpServers: input.mcpServers }), + ...(Object.keys(extraArgs).length === 0 ? {} : { extraArgs }), + }; + return input.cwd === null ? options : { ...options, cwd: input.cwd }; +} + +export function claudeMcpQueryOverrides(input: { + readonly threadId: ThreadId; + readonly allowedTools?: ReadonlyArray; +}): { + readonly allowedTools?: ReadonlyArray; + readonly mcpServers?: ClaudeQueryOptions["mcpServers"]; +} { + const session = McpProviderSession.readMcpProviderSession(input.threadId); + if (session === undefined) { + return input.allowedTools === undefined ? {} : { allowedTools: input.allowedTools }; + } + return { + allowedTools: Array.from(new Set([...(input.allowedTools ?? []), "mcp__t3-code__*"])), + mcpServers: { + "t3-code": { + type: "http", + url: session.endpoint, + headers: { + Authorization: session.authorizationHeader, + }, + }, + }, + }; +} + +function providerSession(input: { + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; + readonly providerInstanceId: ProviderInstanceId; + readonly cwd: string | null; + readonly model: string; + readonly now: DateTime.Utc; +}): OrchestrationV2ProviderSession { + return { + id: input.providerSessionId, + driver: CLAUDE_PROVIDER, + providerInstanceId: input.providerInstanceId, + status: "ready", + cwd: input.cwd ?? process.cwd(), + model: input.model, + capabilities: ClaudeProviderCapabilitiesV2, + createdAt: input.now, + updatedAt: input.now, + lastError: null, + }; +} + +function textFromClaudeContent(content: SDKAssistantMessage["message"]["content"]): string { + return content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join(""); +} + +function assistantTextFromSdkMessage( + message: SDKMessage, +): { readonly nativeItemId: string; readonly text: string } | null { + if (message.type !== "assistant") { + return null; + } + return { + nativeItemId: message.uuid, + text: textFromClaudeContent(message.message.content), + }; +} + +function resultTextFromSdkMessage( + message: SDKMessage, +): { readonly nativeItemId: string; readonly text: string } | null { + if (message.type !== "result" || message.subtype !== "success") { + return null; + } + return { + nativeItemId: message.uuid, + text: message.result, + }; +} + +function makeProviderThread(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly providerInstanceId: ProviderInstanceId; + readonly appThreadId: OrchestrationV2ProviderThread["appThreadId"]; + readonly ownerNodeId?: OrchestrationV2ProviderThread["ownerNodeId"]; + readonly providerSessionId: OrchestrationV2ProviderThread["providerSessionId"]; + readonly nativeThreadId: string; + readonly forkedFrom?: NonNullable; + readonly now: DateTime.Utc; +}): OrchestrationV2ProviderThread { + return { + id: input.idAllocator.derive.providerThread({ + driver: CLAUDE_PROVIDER, + nativeThreadId: input.nativeThreadId, + }), + driver: CLAUDE_PROVIDER, + providerInstanceId: input.providerInstanceId, + providerSessionId: input.providerSessionId, + appThreadId: input.appThreadId, + ownerNodeId: input.ownerNodeId ?? null, + nativeThreadRef: { + driver: CLAUDE_PROVIDER, + nativeId: input.nativeThreadId, + strength: "strong", + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: input.forkedFrom ?? null, + createdAt: input.now, + updatedAt: input.now, + }; +} + +const getNativeThreadId = Effect.fnUntraced(function* ( + providerThread: OrchestrationV2ProviderThread, +) { + const nativeThreadId = providerThread.nativeThreadRef?.nativeId; + if (nativeThreadId === undefined || nativeThreadId === null) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Provider thread ${providerThread.id} is missing a native Claude session id.`, + }); + } + return nativeThreadId; +}); + +const isSyntheticClaudeTurnId = (nativeTurnId: string): boolean => nativeTurnId.startsWith("turn:"); + +const isTerminalProviderTurn = (turn: OrchestrationV2ProviderTurn): boolean => + turn.status === "completed" || + turn.status === "interrupted" || + turn.status === "failed" || + turn.status === "cancelled"; + +const getNativeConversationHeadId = Effect.fnUntraced(function* ( + providerThread: OrchestrationV2ProviderThread, +) { + const nativeHeadRef = providerThread.nativeConversationHeadRef; + if (nativeHeadRef === null) { + return undefined; + } + if (nativeHeadRef.driver !== CLAUDE_PROVIDER) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Provider thread ${providerThread.id} has a non-Claude native conversation head reference.`, + }); + } + if (nativeHeadRef.nativeId === null) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Provider thread ${providerThread.id} has a Claude native conversation head reference without a native id.`, + }); + } + return nativeHeadRef.nativeId; +}); + +const resolveClaudeForkUpToMessageId = Effect.fn("ClaudeAdapterV2.resolveForkUpToMessageId")( + function* (input: ProviderAdapterV2ForkThreadInput) { + if (input.providerTurnId === undefined || input.sourceProviderTurns === undefined) { + return undefined; + } + + const sourceTurns = input.sourceProviderTurns + .filter((turn) => turn.providerThreadId === input.sourceProviderThread.id) + .toSorted((left, right) => left.ordinal - right.ordinal); + const boundaryIndex = sourceTurns.findIndex((turn) => turn.id === input.providerTurnId); + if (boundaryIndex < 0) { + return yield* new ProviderAdapterForkThreadError({ + driver: CLAUDE_PROVIDER, + providerThreadId: input.sourceProviderThread.id, + cause: `Cannot fork Claude thread from provider turn ${input.providerTurnId}: source turn was not found in provider thread ${input.sourceProviderThread.id}.`, + }); + } + + const boundaryNativeId = sourceTurns[boundaryIndex]?.nativeTurnRef?.nativeId; + if ( + boundaryNativeId !== undefined && + boundaryNativeId !== null && + !isSyntheticClaudeTurnId(boundaryNativeId) + ) { + return boundaryNativeId; + } + + const terminalTurnsAfterBoundary = sourceTurns + .slice(boundaryIndex + 1) + .filter(isTerminalProviderTurn); + if (terminalTurnsAfterBoundary.length === 0) { + return undefined; + } + + return yield* new ProviderAdapterForkThreadError({ + driver: CLAUDE_PROVIDER, + providerThreadId: input.sourceProviderThread.id, + cause: `Cannot fork Claude thread from prior provider turn ${input.providerTurnId}: no SDK assistant message cursor was recorded for that turn.`, + }); + }, +); + +const resolveClaudeRollbackResumeSessionAt = Effect.fn( + "ClaudeAdapterV2.resolveRollbackResumeSessionAt", +)(function* (input: ProviderAdapterV2RollbackThreadInput) { + switch (input.target.type) { + case "thread_start": + return null; + case "provider_turn": { + const target = input.target; + if (target.providerTurn.providerThreadId !== input.providerThread.id) { + return yield* new ProviderAdapterRollbackThreadError({ + driver: CLAUDE_PROVIDER, + providerThreadId: input.providerThread.id, + cause: `Cannot roll back Claude thread ${input.providerThread.id} to provider turn ${target.providerTurn.id}: target turn belongs to provider thread ${target.providerTurn.providerThreadId}.`, + }); + } + + const nativeTurnRef = target.providerTurn.nativeTurnRef; + if ( + nativeTurnRef !== null && + nativeTurnRef.driver === CLAUDE_PROVIDER && + nativeTurnRef.nativeId !== null && + !isSyntheticClaudeTurnId(nativeTurnRef.nativeId) + ) { + return nativeTurnRef.nativeId; + } + + const providerTurnsAfterTarget = input.providerThreadTurns.filter( + (turn) => turn.ordinal > target.providerTurn.ordinal && isTerminalProviderTurn(turn), + ); + if (providerTurnsAfterTarget.length === 0) { + return null; + } + + return yield* new ProviderAdapterRollbackThreadError({ + driver: CLAUDE_PROVIDER, + providerThreadId: input.providerThread.id, + cause: `Cannot roll back Claude thread ${input.providerThread.id} to provider turn ${target.providerTurn.id}: no SDK assistant message cursor was recorded for that turn.`, + }); + } + } +}); + +export function makeClaudeUserMessage(input: { + readonly text: string; + readonly priority?: SDKUserMessage["priority"]; +}): SDKUserMessage { + return { + type: "user", + message: { + role: "user", + content: input.text, + }, + parent_tool_use_id: null, + ...(input.priority === undefined ? {} : { priority: input.priority }), + }; +} + +type ClaudeAssistantContentBlock = SDKAssistantMessage["message"]["content"][number]; +type ClaudeToolUseContentBlock = Extract< + ClaudeAssistantContentBlock, + { + readonly id: string; + readonly name: string; + readonly input: unknown; + } +>; +type ClaudeUserContent = SDKUserMessage["message"]["content"]; +type ClaudeUserContentBlock = Exclude[number]; +type ClaudeAssistantToolResultContentBlock = Extract< + ClaudeAssistantContentBlock, + { + readonly tool_use_id: string; + } +>; +type ClaudeUserToolResultContentBlock = Extract< + ClaudeUserContentBlock, + { + readonly tool_use_id: string; + } +>; +type ClaudeToolResultContentBlock = + | ClaudeAssistantToolResultContentBlock + | ClaudeUserToolResultContentBlock; +type ClaudeTypedToolResultContentBlock = Exclude< + ClaudeToolResultContentBlock, + { readonly type: "mcp_tool_result" | "tool_result" } +>; +type ClaudeTypedToolResultContent = ClaudeTypedToolResultContentBlock["content"]; +type ClaudeToolResultOutput = + | Extract["content"] + | Extract["content"] + | ClaudeTypedToolResultContent; + +function assertNever(value: never): never { + throw new Error(`Unhandled Claude SDK variant: ${jsonStringifyForTool(value)}`); +} + +const ClaudeRuntimeSandboxPolicyKind = Schema.Struct({ + type: Schema.Literals(["dangerFullAccess", "externalSandbox", "readOnly", "workspaceWrite"]), +}); +type ClaudeRuntimeSandboxPolicy = typeof ClaudeRuntimeSandboxPolicyKind.Type; +type ClaudeRuntimeSandboxPolicyKindName = ClaudeRuntimeSandboxPolicy["type"]; + +const ClaudeRuntimeReadOnlyFullAccessSandboxPolicy = Schema.Struct({ + type: Schema.Literal("readOnly"), + access: Schema.Struct({ + type: Schema.Literal("fullAccess"), + }), +}); + +function sandboxPolicyKindForClaudeRuntimePolicy( + runtimePolicy: ProviderAdapterV2RuntimePolicy, +): ClaudeRuntimeSandboxPolicyKindName | undefined { + return runtimePolicy.sandboxPolicy !== undefined && + Schema.is(ClaudeRuntimeSandboxPolicyKind)(runtimePolicy.sandboxPolicy) + ? runtimePolicy.sandboxPolicy.type + : undefined; +} + +function readOnlyPolicyAllowsGlobalReads(runtimePolicy: ProviderAdapterV2RuntimePolicy): boolean { + return ( + runtimePolicy.sandboxPolicy !== undefined && + Schema.is(ClaudeRuntimeReadOnlyFullAccessSandboxPolicy)(runtimePolicy.sandboxPolicy) + ); +} + +function permissionModeForClaudeRuntimePolicy( + runtimePolicy: ProviderAdapterV2RuntimePolicy, +): PermissionMode { + if (runtimePolicy.interactionMode === "plan") { + return "plan"; + } + if (runtimePolicy.approvalPolicy !== undefined && runtimePolicy.approvalPolicy !== "never") { + return "default"; + } + + switch (sandboxPolicyKindForClaudeRuntimePolicy(runtimePolicy)) { + case "readOnly": + return "dontAsk"; + case "dangerFullAccess": + return "bypassPermissions"; + case "externalSandbox": + case "workspaceWrite": + case undefined: + break; + } + + switch (runtimePolicy.runtimeMode) { + case "approval-required": + return "default"; + case "auto-accept-edits": + return "acceptEdits"; + case "full-access": + return "bypassPermissions"; + } +} + +export interface ClaudeRuntimeQueryPolicy { + readonly permissionMode: PermissionMode; + readonly tools?: ClaudeAgentSdkQueryTools; + readonly allowedTools?: ReadonlyArray; + readonly allowDangerouslySkipPermissions?: true; + readonly installPermissionCallback: boolean; +} + +export function claudeRuntimeQueryPolicyForRuntimePolicy( + runtimePolicy: ProviderAdapterV2RuntimePolicy, +): ClaudeRuntimeQueryPolicy { + const permissionMode = permissionModeForClaudeRuntimePolicy(runtimePolicy); + const readOnlyTools = + permissionMode === "dontAsk" && + sandboxPolicyKindForClaudeRuntimePolicy(runtimePolicy) === "readOnly" + ? CLAUDE_READ_ONLY_ALLOWED_TOOLS + : undefined; + const allowedTools = + readOnlyTools !== undefined && readOnlyPolicyAllowsGlobalReads(runtimePolicy) + ? readOnlyTools + : undefined; + + if (permissionMode === "plan") { + return { + permissionMode, + ...(readOnlyTools === undefined ? {} : { tools: readOnlyTools }), + ...(allowedTools === undefined ? {} : { allowedTools }), + installPermissionCallback: false, + }; + } + + const installPermissionCallback = + runtimePolicy.approvalPolicy === undefined + ? runtimePolicy.runtimeMode === "approval-required" + : runtimePolicy.approvalPolicy !== "never"; + + return { + permissionMode, + ...(readOnlyTools === undefined ? {} : { tools: readOnlyTools }), + ...(allowedTools === undefined ? {} : { allowedTools }), + ...(permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}), + installPermissionCallback, + }; +} + +function shouldInstallClaudePermissionCallback(policy: ClaudeRuntimeQueryPolicy): boolean { + if (policy.permissionMode === "plan") { + return false; + } + return policy.installPermissionCallback; +} + +function claudeRuntimeQueryPolicyKey(policy: ClaudeRuntimeQueryPolicy): string { + return JSON.stringify({ + permissionMode: policy.permissionMode, + tools: policy.tools, + allowedTools: policy.allowedTools, + allowDangerouslySkipPermissions: policy.allowDangerouslySkipPermissions, + installPermissionCallback: policy.installPermissionCallback, + }); +} + +type ClaudeToolItemType = Extract< + OrchestrationV2TurnItem["type"], + "command_execution" | "file_change" | "dynamic_tool" | "web_search" +>; + +interface ClaudeToolClassification { + readonly known: boolean; + readonly normalizedName: string; + readonly itemType: ClaudeToolItemType; + readonly requestKind: ProviderRequestKind; +} + +function normalizedClaudeToolName(toolName: string): string { + return toolName.toLowerCase().replaceAll(/[\s_-]/g, ""); +} + +const CLAUDE_KNOWN_TOOL_CLASSIFICATIONS: Record< + string, + { + readonly itemType: ClaudeToolItemType; + readonly requestKind: ProviderRequestKind; + } +> = { + agent: { itemType: "dynamic_tool", requestKind: "command" }, + bash: { itemType: "command_execution", requestKind: "command" }, + edit: { itemType: "file_change", requestKind: "file-change" }, + glob: { itemType: "dynamic_tool", requestKind: "file-read" }, + grep: { itemType: "dynamic_tool", requestKind: "file-read" }, + ls: { itemType: "dynamic_tool", requestKind: "file-read" }, + multiedit: { itemType: "file_change", requestKind: "file-change" }, + notebookedit: { itemType: "file_change", requestKind: "file-change" }, + read: { itemType: "dynamic_tool", requestKind: "file-read" }, + task: { itemType: "dynamic_tool", requestKind: "command" }, + todowrite: { itemType: "dynamic_tool", requestKind: "command" }, + toolsearch: { itemType: "dynamic_tool", requestKind: "command" }, + webfetch: { itemType: "web_search", requestKind: "command" }, + websearch: { itemType: "web_search", requestKind: "command" }, + write: { itemType: "file_change", requestKind: "file-change" }, +}; + +export function classifyClaudeNativeTool(toolName: string): ClaudeToolClassification { + const normalizedName = normalizedClaudeToolName(toolName); + const known = CLAUDE_KNOWN_TOOL_CLASSIFICATIONS[normalizedName]; + return known === undefined + ? { + known: false, + normalizedName, + itemType: "dynamic_tool", + requestKind: "command", + } + : { + known: true, + normalizedName, + ...known, + }; +} + +function providerRequestKindFromClaudeTool(toolName: string): ProviderRequestKind { + return classifyClaudeNativeTool(toolName).requestKind; +} + +function isClaudeWebSearchOutput(output: unknown): output is WebSearchOutput { + return ( + typeof output === "object" && + output !== null && + typeof Reflect.get(output, "query") === "string" && + Array.isArray(Reflect.get(output, "results")) && + typeof Reflect.get(output, "durationSeconds") === "number" + ); +} + +const ClaudeNativeToolInputRecord = Schema.Record(Schema.String, Schema.Unknown); +type ClaudeNativeToolInputRecord = typeof ClaudeNativeToolInputRecord.Type; + +type ClaudeNativeToolInput = + | { + readonly type: "record"; + readonly value: ClaudeNativeToolInputRecord; + } + | { + readonly type: "non_record"; + readonly value: unknown; + }; + +const EMPTY_CLAUDE_NATIVE_TOOL_INPUT = { + type: "record", + value: {}, +} satisfies ClaudeNativeToolInput; + +function claudeNativeToolInputFromUnknown(input: unknown): ClaudeNativeToolInput { + return Schema.is(ClaudeNativeToolInputRecord)(input) + ? { type: "record", value: input } + : { type: "non_record", value: input }; +} + +function claudeNativeToolInputFromRecord(input: Record): ClaudeNativeToolInput { + return { type: "record", value: input }; +} + +function claudeNativeToolInputValue(input: ClaudeNativeToolInput): unknown { + return input.value; +} + +function inputRecordValue(input: ClaudeNativeToolInput, key: string): unknown { + return input.type === "record" ? input.value[key] : undefined; +} + +function firstStringInputField( + input: ClaudeNativeToolInput, + keys: ReadonlyArray, +): string | undefined { + for (const key of keys) { + const value = inputRecordValue(input, key); + if (typeof value === "string" && value.trim().length > 0) { + return value.trim(); + } + } + return undefined; +} + +function jsonStringifyForTool(value: unknown): string { + if (typeof value === "string") { + return value; + } + return JSON.stringify(value) ?? String(value); +} + +function commandInputFromClaudeTool(toolName: string, input: ClaudeNativeToolInput): string { + return ( + firstStringInputField(input, ["command", "cmd", "script"]) ?? + `${toolName}: ${jsonStringifyForTool(claudeNativeToolInputValue(input))}` + ); +} + +function fileNameFromClaudeTool(toolName: string, input: ClaudeNativeToolInput): string { + return ( + firstStringInputField(input, ["file_path", "path", "filename", "fileName"]) ?? + `${toolName} result` + ); +} + +type ClaudeNativeToolOutput = + | { + readonly type: "none"; + } + | { + readonly type: "content_block"; + readonly value: ClaudeToolResultOutput; + } + | { + readonly type: "structured_tool_use_result"; + readonly value: unknown; + readonly fallbackValue?: ClaudeToolResultOutput; + }; + +const NO_CLAUDE_NATIVE_TOOL_OUTPUT = { type: "none" } satisfies ClaudeNativeToolOutput; + +function claudeNativeToolOutputFromToolResult( + toolResult: ClaudeToolResultContentBlock, +): ClaudeNativeToolOutput { + const value = outputFromClaudeToolResult(toolResult); + return value === undefined ? NO_CLAUDE_NATIVE_TOOL_OUTPUT : { type: "content_block", value }; +} + +function claudeNativeToolOutputFromStructuredResult(input: { + readonly structuredOutput: unknown; + readonly fallbackValue?: ClaudeToolResultOutput; +}): ClaudeNativeToolOutput { + return { + type: "structured_tool_use_result", + value: input.structuredOutput, + ...(input.fallbackValue === undefined ? {} : { fallbackValue: input.fallbackValue }), + }; +} + +function claudeNativeToolOutputValue(output: ClaudeNativeToolOutput): unknown | undefined { + switch (output.type) { + case "none": + return undefined; + case "content_block": + case "structured_tool_use_result": + return output.value; + default: + return assertNever(output); + } +} + +function claudeNativeToolOutputText(output: ClaudeNativeToolOutput): string { + const value = claudeNativeToolOutputValue(output); + return typeof value === "string" ? value : value === undefined ? "" : jsonStringifyForTool(value); +} + +function claudeSubagentResultText(output: ClaudeNativeToolOutput): string { + const value = claudeNativeToolOutputValue(output); + if (typeof value === "object" && value !== null && "content" in value) { + const content = value.content; + if (Array.isArray(content)) { + const text = content + .flatMap((part) => + typeof part === "object" && + part !== null && + "type" in part && + part.type === "text" && + "text" in part && + typeof part.text === "string" + ? [part.text] + : [], + ) + .join("\n"); + if (text.length > 0) { + return text; + } + } + } + return claudeNativeToolOutputText(output); +} + +function webSearchPatternsFromClaudeTool(input: { + readonly toolInput: ClaudeNativeToolInput; + readonly output: ClaudeNativeToolOutput; +}): ReadonlyArray { + const output = claudeNativeToolOutputValue(input.output); + const pattern = + firstStringInputField(input.toolInput, ["query", "url", "pattern"]) ?? + (isClaudeWebSearchOutput(output) ? output.query : undefined); + return pattern === undefined || pattern.trim().length === 0 ? [] : [pattern]; +} + +function webSearchResultsFromClaudeOutput( + output: ClaudeNativeToolOutput, +): ReadonlyArray { + const value = claudeNativeToolOutputValue(output); + if (!isClaudeWebSearchOutput(value)) { + return []; + } + + return value.results.flatMap((result) => { + if (typeof result === "string") { + return []; + } + return result.content.map((content) => ({ + title: content.title, + url: content.url, + })); + }); +} + +function summarizeClaudeToolRequest(toolName: string, input: ClaudeNativeToolInput): string { + const command = firstStringInputField(input, ["command", "cmd", "script"]); + if (command !== undefined) { + return `${toolName}: ${command.slice(0, 400)}`; + } + const path = firstStringInputField(input, ["file_path", "path", "filename", "fileName"]); + if (path !== undefined) { + return `${toolName}: ${path.slice(0, 400)}`; + } + const serialized = jsonStringifyForTool(claudeNativeToolInputValue(input)); + return serialized.length <= 400 + ? `${toolName}: ${serialized}` + : `${toolName}: ${serialized.slice(0, 397)}...`; +} + +function outputFromClaudeToolResult( + toolResult: ClaudeToolResultContentBlock, +): ClaudeToolResultOutput | undefined { + switch (toolResult.type) { + case "tool_result": + return toolResult.content; + case "mcp_tool_result": + return toolResult.content; + case "bash_code_execution_tool_result": + case "code_execution_tool_result": + case "advisor_tool_result": + case "text_editor_code_execution_tool_result": + case "tool_search_tool_result": + case "web_fetch_tool_result": + case "web_search_tool_result": + return toolResult.content; + default: + return assertNever(toolResult); + } +} + +function isClaudeTypedToolResultErrorContent(content: ClaudeTypedToolResultContent): boolean { + if (Array.isArray(content)) { + return false; + } + + switch (content.type) { + case "bash_code_execution_tool_result_error": + case "code_execution_tool_result_error": + case "text_editor_code_execution_tool_result_error": + case "tool_search_tool_result_error": + case "web_fetch_tool_result_error": + case "web_search_tool_result_error": + return true; + default: + return false; + } +} + +function isClaudeToolResultError(toolResult: ClaudeToolResultContentBlock): boolean { + switch (toolResult.type) { + case "tool_result": + return toolResult.is_error === true; + case "mcp_tool_result": + return toolResult.is_error; + case "bash_code_execution_tool_result": + case "code_execution_tool_result": + case "advisor_tool_result": + case "text_editor_code_execution_tool_result": + case "tool_search_tool_result": + case "web_fetch_tool_result": + case "web_search_tool_result": + return isClaudeTypedToolResultErrorContent(toolResult.content); + default: + return assertNever(toolResult); + } +} + +function toolNameFromClaudeToolResult(toolResult: ClaudeToolResultContentBlock): string { + switch (toolResult.type) { + case "bash_code_execution_tool_result": + return "bash_code_execution"; + case "code_execution_tool_result": + return "code_execution"; + case "advisor_tool_result": + return "advisor"; + case "mcp_tool_result": + return "mcp_tool"; + case "text_editor_code_execution_tool_result": + return "text_editor_code_execution"; + case "tool_result": + return "tool"; + case "tool_search_tool_result": + return "tool_search"; + case "web_fetch_tool_result": + return "web_fetch"; + case "web_search_tool_result": + return "web_search"; + default: + return assertNever(toolResult); + } +} + +function isClaudeAssistantToolResultContentBlock( + part: ClaudeAssistantContentBlock, +): part is ClaudeAssistantToolResultContentBlock { + return "tool_use_id" in part && typeof part.tool_use_id === "string"; +} + +function isClaudeUserToolResultContentBlock( + part: ClaudeUserContentBlock, +): part is ClaudeUserToolResultContentBlock { + return "tool_use_id" in part && typeof part.tool_use_id === "string"; +} + +function isClaudeToolUseContentBlock( + part: ClaudeAssistantContentBlock, +): part is ClaudeToolUseContentBlock { + return ( + "id" in part && + typeof part.id === "string" && + "name" in part && + typeof part.name === "string" && + "input" in part + ); +} + +function claudeToolUseBlocksFromAssistantMessage( + message: SDKMessage, +): ReadonlyArray { + if (message.type !== "assistant") { + return []; + } + return message.message.content.filter(isClaudeToolUseContentBlock); +} + +function claudeToolResultBlocksFromAssistantMessage( + message: SDKMessage, +): ReadonlyArray { + if (message.type !== "assistant") { + return []; + } + return message.message.content.filter(isClaudeAssistantToolResultContentBlock); +} + +function claudeToolResultBlocksFromUserMessage( + message: SDKMessage, +): ReadonlyArray { + if (message.type !== "user" || typeof message.message.content === "string") { + return []; + } + return message.message.content.filter(isClaudeUserToolResultContentBlock); +} + +function claudeToolResultEntriesFromMessage(message: SDKMessage): ReadonlyArray<{ + readonly toolResult: ClaudeToolResultContentBlock; + readonly output: ClaudeNativeToolOutput; +}> { + const assistantResults = claudeToolResultBlocksFromAssistantMessage(message).map( + (toolResult) => ({ toolResult, output: claudeNativeToolOutputFromToolResult(toolResult) }), + ); + const userResults = claudeToolResultBlocksFromUserMessage(message); + const structuredOutput = + message.type === "user" && userResults.length === 1 ? message.tool_use_result : undefined; + return [ + ...assistantResults, + ...userResults.map((toolResult) => ({ + toolResult, + output: + structuredOutput === undefined + ? claudeNativeToolOutputFromToolResult(toolResult) + : claudeNativeToolOutputFromStructuredResult({ + structuredOutput, + fallbackValue: outputFromClaudeToolResult(toolResult), + }), + })), + ]; +} + +function parentToolUseIdFromSdkMessage(message: SDKMessage): string | null { + return message.type === "assistant" || message.type === "user" + ? message.parent_tool_use_id + : null; +} + +function permissionResultFromDecision(input: { + readonly decision: ProviderApprovalDecision; + readonly toolInput: Record; + readonly toolUseID: string; + readonly suggestions?: Parameters[2]["suggestions"]; +}): PermissionResult { + if (input.decision === "accept" || input.decision === "acceptForSession") { + return { + behavior: "allow", + updatedInput: input.toolInput, + toolUseID: input.toolUseID, + decisionClassification: + input.decision === "acceptForSession" ? "user_permanent" : "user_temporary", + ...(input.decision === "acceptForSession" && input.suggestions !== undefined + ? { updatedPermissions: input.suggestions } + : {}), + }; + } + + return { + behavior: "deny", + message: + input.decision === "cancel" + ? "User cancelled tool execution." + : "User declined tool execution.", + toolUseID: input.toolUseID, + decisionClassification: "user_reject", + ...(input.decision === "cancel" ? { interrupt: true } : {}), + }; +} + +function terminalStatusFromResult( + message: SDKResultMessage, +): Extract< + OrchestrationV2ProviderTurn["status"], + "completed" | "interrupted" | "failed" | "cancelled" +> { + if (message.subtype === "success") { + return "completed"; + } + const errorText = message.errors.join("\n").toLowerCase(); + if (errorText.includes("interrupt")) { + return "interrupted"; + } + if (errorText.includes("cancel")) { + return "cancelled"; + } + return "failed"; +} + +function isClaudeActiveSteeringAbortResult(message: SDKResultMessage): boolean { + return message.terminal_reason === "aborted_streaming"; +} + +function buildAssistantArtifacts(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly turnInput: ProviderAdapterV2TurnInput; + readonly providerTurnId: OrchestrationV2ProviderTurn["id"]; + readonly nativeItemId: string; + readonly text: string; + readonly ordinal: number; + readonly startedAt: DateTime.Utc; + readonly completedAt: DateTime.Utc; +}): { + readonly node: OrchestrationV2ExecutionNode; + readonly message: OrchestrationV2ConversationMessage; + readonly turnItem: OrchestrationV2TurnItem; +} { + const nodeId = input.idAllocator.derive.nodeFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const messageId = input.idAllocator.derive.messageFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const turnItemId = input.idAllocator.derive.turnItemFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const nativeItemRef = { + driver: CLAUDE_PROVIDER, + nativeId: input.nativeItemId, + strength: "strong" as const, + }; + + return { + node: { + id: nodeId, + threadId: input.turnInput.threadId, + runId: input.turnInput.runId, + parentNodeId: input.turnInput.rootNodeId, + rootNodeId: input.turnInput.rootNodeId, + kind: "assistant_message", + status: "completed", + countsForRun: false, + providerThreadId: input.turnInput.providerThread.id, + providerTurnId: input.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: input.startedAt, + completedAt: input.completedAt, + }, + message: { + createdBy: "agent", + creationSource: "provider", + id: messageId, + threadId: input.turnInput.threadId, + runId: input.turnInput.runId, + nodeId, + role: "assistant", + text: input.text, + attachments: [], + streaming: false, + createdAt: input.completedAt, + updatedAt: input.completedAt, + }, + turnItem: { + id: turnItemId, + threadId: input.turnInput.threadId, + runId: input.turnInput.runId, + nodeId, + providerThreadId: input.turnInput.providerThread.id, + providerTurnId: input.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal: input.ordinal, + status: "completed", + title: null, + startedAt: input.startedAt, + completedAt: input.completedAt, + updatedAt: input.completedAt, + type: "assistant_message", + messageId, + text: input.text, + streaming: false, + }, + }; +} + +interface ActiveClaudeTurnContext { + readonly input: ProviderAdapterV2TurnInput; + readonly nativeTurnId: string; + nativeMessageCursor: string | null; + readonly providerTurnId: OrchestrationV2ProviderTurn["id"]; + readonly providerTurnOrdinal: number; + readonly startedAt: DateTime.Utc; + readonly assistant: { + text: string; + nativeItemId: string; + }; + readonly toolCalls: Map; + readonly subagentsByTaskId: Map; + readonly subagentsByToolUseId: Map; + readonly subagentNodesByTaskId: Map; +} + +interface ActiveClaudeSubagent { + task: OrchestrationV2Subagent; + readonly childThreadId: ThreadId; + readonly childRootNodeId: OrchestrationV2ExecutionNode["id"]; + readonly turnItemId: OrchestrationV2TurnItem["id"]; + readonly turnItemOrdinal: number; + nextChildItemOrdinal: number; + resultItemOrdinal: number | null; +} + +interface ClaudeLiveQueryContext { + readonly nativeThreadId: string; + readonly query: ClaudeAgentSdkQuerySession; + readonly queryPolicyKey: string; + readonly closed: Deferred.Deferred; + currentModel: string; +} + +interface ActiveClaudeToolCall { + readonly nativeItemId: string; + readonly toolName: string; + readonly classification: ClaudeToolClassification; + readonly input: ClaudeNativeToolInput; + readonly threadId: ThreadId; + readonly runId: ProviderAdapterV2TurnInput["runId"] | null; + readonly rootNodeId: OrchestrationV2ExecutionNode["id"]; + readonly parentNodeId: OrchestrationV2ExecutionNode["id"]; + readonly ordinal: number; + readonly startedAt: DateTime.Utc; +} + +interface PendingClaudeRuntimeRequest { + readonly requestId: OrchestrationV2RuntimeRequest["id"]; + readonly requestKind: ProviderRequestKind; + readonly decision: Deferred.Deferred; +} + +export interface ClaudeAdapterV2Options { + readonly instanceId: ProviderInstanceId; + readonly settings: ClaudeSettings; + readonly environment: NodeJS.ProcessEnv; + readonly idAllocator: IdAllocatorV2Shape; + readonly queryRunner: ClaudeAgentSdkQueryRunnerShape; +} + +export function makeClaudeAdapterV2( + adapterOptions: ClaudeAdapterV2Options, +): ProviderAdapterV2Shape { + const { idAllocator, queryRunner } = adapterOptions; + + return ProviderAdapterV2.of({ + instanceId: adapterOptions.instanceId, + driver: CLAUDE_PROVIDER, + getCapabilities: () => Effect.succeed(ClaudeProviderCapabilitiesV2), + openSession: Effect.fn("ClaudeAdapterV2.openSession")( + function* (input: ProviderAdapterV2OpenSessionInput) { + const sessionScope = yield* Effect.scope; + const now = yield* DateTime.now; + const session = providerSession({ + providerSessionId: input.providerSessionId, + providerInstanceId: adapterOptions.instanceId, + cwd: input.runtimePolicy.cwd, + model: input.modelSelection.model, + now, + }); + const events = yield* Queue.unbounded(); + const activeTurn = yield* Ref.make(null); + const interruptedTurns = yield* Ref.make(new Set()); + const steeredTurns = yield* Ref.make(new Set()); + const queryContext = yield* Ref.make(null); + const openedNativeThreads = yield* Ref.make(new Set()); + const itemOrdinals = yield* Ref.make(new Map()); + const nextItemOrdinalsByTurn = yield* Ref.make(new Map()); + const pendingRuntimeRequests = yield* Ref.make( + new Map(), + ); + const runtimeContext = yield* Effect.context(); + const runFork = Effect.runForkWith(runtimeContext); + const runPromise = Effect.runPromiseWith(runtimeContext); + + const emitProviderEvent = (event: ProviderAdapterV2Event) => + Queue.offer(events, event).pipe(Effect.asVoid); + + const resolveItemOrdinal = Effect.fnUntraced(function* ( + context: ActiveClaudeTurnContext, + nativeItemId: string, + ) { + const existing = (yield* Ref.get(itemOrdinals)).get(nativeItemId); + if (existing !== undefined) { + return existing; + } + + const nextWithinTurn = yield* Ref.modify(nextItemOrdinalsByTurn, (current) => { + const next = (current.get(context.nativeTurnId) ?? 0) + 1; + const updated = new Map(current); + updated.set(context.nativeTurnId, next); + return [next, updated]; + }); + const nextOrdinal = context.input.runOrdinal * 100 + nextWithinTurn; + yield* Ref.update(itemOrdinals, (current) => { + const updated = new Map(current); + updated.set(nativeItemId, nextOrdinal); + return updated; + }); + return nextOrdinal; + }); + + const providerTurnPayload = (input: { + readonly context: ActiveClaudeTurnContext; + readonly status: OrchestrationV2ProviderTurn["status"]; + readonly completedAt: DateTime.Utc | null; + }): OrchestrationV2ProviderTurn => ({ + id: input.context.providerTurnId, + providerThreadId: input.context.input.providerThread.id, + nodeId: input.context.input.rootNodeId, + runAttemptId: input.context.input.attemptId, + nativeTurnRef: { + driver: CLAUDE_PROVIDER, + nativeId: input.context.nativeMessageCursor ?? input.context.nativeTurnId, + strength: "weak", + }, + ordinal: input.context.providerTurnOrdinal, + status: input.status, + startedAt: input.context.startedAt, + completedAt: input.completedAt, + }); + + const buildToolCallArtifacts = (input: { + readonly context: ActiveClaudeTurnContext; + readonly nativeItemId: string; + readonly toolName: string; + readonly classification: ClaudeToolClassification; + readonly toolInput: ClaudeNativeToolInput; + readonly threadId: ThreadId; + readonly runId: ProviderAdapterV2TurnInput["runId"] | null; + readonly rootNodeId: OrchestrationV2ExecutionNode["id"]; + readonly parentNodeId: OrchestrationV2ExecutionNode["id"]; + readonly ordinal: number; + readonly output: ClaudeNativeToolOutput; + readonly status: Extract< + OrchestrationV2TurnItem["status"], + "running" | "completed" | "failed" + >; + readonly startedAt: DateTime.Utc; + readonly updatedAt: DateTime.Utc; + }) => { + const completedAt = input.status === "running" ? null : input.updatedAt; + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const nativeItemRef = { + driver: CLAUDE_PROVIDER, + nativeId: input.nativeItemId, + strength: "strong" as const, + }; + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: input.threadId, + runId: input.runId, + parentNodeId: input.parentNodeId, + rootNodeId: input.rootNodeId, + kind: "tool_call", + status: input.status, + countsForRun: false, + providerThreadId: input.runId === null ? null : input.context.input.providerThread.id, + providerTurnId: input.runId === null ? null : input.context.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: input.startedAt, + completedAt, + }; + const itemBase = { + id: turnItemId, + threadId: input.threadId, + runId: input.runId, + nodeId, + providerThreadId: input.runId === null ? null : input.context.input.providerThread.id, + providerTurnId: input.runId === null ? null : input.context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal: input.ordinal, + status: input.status, + title: null, + startedAt: input.startedAt, + completedAt, + updatedAt: input.updatedAt, + } satisfies Pick< + OrchestrationV2TurnItem, + | "id" + | "threadId" + | "runId" + | "nodeId" + | "providerThreadId" + | "providerTurnId" + | "nativeItemRef" + | "parentItemId" + | "ordinal" + | "status" + | "title" + | "startedAt" + | "completedAt" + | "updatedAt" + >; + const itemType = input.classification.itemType; + const webSearchPatterns = webSearchPatternsFromClaudeTool({ + toolInput: input.toolInput, + output: input.output, + }); + const webSearchResults = webSearchResultsFromClaudeOutput(input.output); + const outputValue = claudeNativeToolOutputValue(input.output); + const outputText = claudeNativeToolOutputText(input.output); + const turnItem: OrchestrationV2TurnItem = + itemType === "command_execution" + ? { + ...itemBase, + type: "command_execution", + input: commandInputFromClaudeTool(input.toolName, input.toolInput), + ...(outputText.length === 0 ? {} : { output: outputText }), + } + : itemType === "file_change" + ? { + ...itemBase, + type: "file_change", + fileName: fileNameFromClaudeTool(input.toolName, input.toolInput), + ...(outputText.length === 0 ? {} : { diffStr: outputText }), + } + : itemType === "web_search" + ? { + ...itemBase, + type: "web_search", + ...(webSearchPatterns.length === 0 + ? {} + : { patterns: [...webSearchPatterns] }), + ...(webSearchResults.length === 0 ? {} : { results: [...webSearchResults] }), + } + : { + ...itemBase, + type: "dynamic_tool", + toolName: input.toolName, + input: claudeNativeToolInputValue(input.toolInput), + ...(outputValue === undefined ? {} : { output: outputValue }), + }; + return { node, turnItem }; + }; + + const emitToolCallArtifacts = Effect.fnUntraced(function* (artifacts: { + readonly node: OrchestrationV2ExecutionNode; + readonly turnItem: OrchestrationV2TurnItem; + }) { + yield* emitProviderEvent({ + type: "node.updated", + driver: CLAUDE_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CLAUDE_PROVIDER, + turnItem: artifacts.turnItem, + }); + }); + + const updateClaudeSubagentNode = Effect.fnUntraced(function* (input: { + readonly context: ActiveClaudeTurnContext; + readonly taskId: string; + readonly toolUseId?: string; + readonly prompt?: string; + readonly title?: string; + readonly result?: string; + readonly status: Extract< + OrchestrationV2ExecutionNode["status"], + "running" | "completed" | "failed" | "cancelled" + >; + }) { + const existingSubagent = + input.context.subagentsByTaskId.get(input.taskId) ?? + (input.toolUseId === undefined + ? undefined + : input.context.subagentsByToolUseId.get(input.toolUseId)); + if (existingSubagent === undefined && input.status !== "running") { + return; + } + + const now = yield* DateTime.now; + const nativeItemId = `task:${input.taskId}`; + const nodeId = + existingSubagent?.task.id ?? + idAllocator.derive.nodeFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId, + }); + const childRootNodeId = + existingSubagent?.childRootNodeId ?? + idAllocator.derive.nodeFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: `${nativeItemId}:thread-root`, + }); + const childThreadId = + existingSubagent?.childThreadId ?? + idAllocator.derive.threadFromProviderThread({ + driver: CLAUDE_PROVIDER, + nativeThreadId: `${input.context.input.providerThread.id}:${input.taskId}`, + }); + if (existingSubagent === undefined) { + input.context.subagentNodesByTaskId.set(input.taskId, nodeId); + } + const turnItemOrdinal = + existingSubagent?.turnItemOrdinal ?? + (yield* resolveItemOrdinal(input.context, `${nativeItemId}:subagent`)); + const task = { + ...(existingSubagent?.task ?? { + id: nodeId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + parentNodeId: input.context.input.rootNodeId, + origin: "provider_native" as const, + createdBy: "agent" as const, + driver: CLAUDE_PROVIDER, + providerInstanceId: input.context.input.modelSelection.instanceId, + providerThreadId: null, + childThreadId, + nativeTaskRef: { + driver: CLAUDE_PROVIDER, + nativeId: input.taskId, + strength: "strong" as const, + }, + prompt: input.prompt ?? "", + title: input.title ?? null, + model: input.context.input.modelSelection.model, + result: null, + startedAt: now, + }), + status: input.status, + ...(input.prompt === undefined ? {} : { prompt: input.prompt }), + ...(input.title === undefined ? {} : { title: input.title }), + ...(input.result === undefined ? {} : { result: input.result }), + completedAt: input.status === "running" ? null : now, + updatedAt: now, + } satisfies OrchestrationV2Subagent; + const subagent = { + task, + childThreadId, + childRootNodeId, + turnItemId: + existingSubagent?.turnItemId ?? + idAllocator.derive.turnItemFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: `${nativeItemId}:subagent`, + }), + turnItemOrdinal, + nextChildItemOrdinal: existingSubagent?.nextChildItemOrdinal ?? 100, + resultItemOrdinal: existingSubagent?.resultItemOrdinal ?? null, + } satisfies ActiveClaudeSubagent; + input.context.subagentsByTaskId.set(input.taskId, subagent); + if (input.toolUseId !== undefined) { + input.context.subagentsByToolUseId.set(input.toolUseId, subagent); + } + + if (existingSubagent === undefined) { + const childThread = makeSubagentChildThread({ + parentThread: input.context.input.appThread, + childThreadId, + parentNodeId: nodeId, + activeProviderThreadId: null, + providerInstanceId: input.context.input.modelSelection.instanceId, + modelSelection: input.context.input.modelSelection, + title: subagentThreadTitle({ + parentTitle: input.context.input.appThread.title, + prompt: task.prompt, + title: task.title, + ordinal: input.context.subagentsByTaskId.size, + }), + now, + createdBy: "agent", + creationSource: "provider", + }); + yield* emitProviderEvent({ + type: "app_thread.created", + driver: CLAUDE_PROVIDER, + appThread: childThread, + }); + } + + yield* emitProviderEvent({ + type: "node.updated", + driver: CLAUDE_PROVIDER, + node: { + id: nodeId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + parentNodeId: input.context.input.rootNodeId, + rootNodeId: input.context.input.rootNodeId, + kind: "subagent", + status: input.status, + countsForRun: false, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: { + driver: CLAUDE_PROVIDER, + nativeId: input.taskId, + strength: "strong", + }, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: task.startedAt, + completedAt: input.status === "running" ? null : now, + }, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CLAUDE_PROVIDER, + node: { + id: childRootNodeId, + threadId: childThreadId, + runId: null, + parentNodeId: null, + rootNodeId: childRootNodeId, + kind: "root_turn", + status: input.status, + countsForRun: false, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: task.nativeTaskRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: task.startedAt, + completedAt: input.status === "running" ? null : now, + }, + }); + if (existingSubagent === undefined) { + const promptNativeItemId = `${nativeItemId}:prompt`; + const promptArtifacts = makeSubagentConversationArtifacts({ + messageId: idAllocator.derive.messageFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: promptNativeItemId, + }), + turnItemId: idAllocator.derive.turnItemFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: promptNativeItemId, + }), + threadId: childThreadId, + rootNodeId: childRootNodeId, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: { + driver: CLAUDE_PROVIDER, + nativeId: promptNativeItemId, + strength: "strong", + }, + role: "user", + text: task.prompt, + ordinal: 100, + now, + }); + yield* emitProviderEvent({ + type: "message.updated", + driver: CLAUDE_PROVIDER, + message: promptArtifacts.message, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CLAUDE_PROVIDER, + turnItem: promptArtifacts.turnItem, + }); + } + yield* emitProviderEvent({ + type: "subagent.updated", + driver: CLAUDE_PROVIDER, + subagent: task, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CLAUDE_PROVIDER, + turnItem: { + id: subagent.turnItemId, + threadId: task.threadId, + runId: task.runId, + nodeId: task.id, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: task.nativeTaskRef, + parentItemId: null, + ordinal: subagent.turnItemOrdinal, + status: task.status, + title: task.title, + startedAt: task.startedAt, + completedAt: task.completedAt, + updatedAt: task.updatedAt, + type: "subagent", + subagentId: task.id, + origin: task.origin, + driver: task.driver, + providerInstanceId: task.providerInstanceId, + childThreadId: task.childThreadId, + prompt: task.prompt, + result: task.result, + }, + }); + + if ( + input.result !== undefined && + input.result.trim().length > 0 && + input.status !== "running" + ) { + const resultNativeItemId = `${nativeItemId}:result`; + const resultItemOrdinal = subagent.resultItemOrdinal ?? ++subagent.nextChildItemOrdinal; + subagent.resultItemOrdinal = resultItemOrdinal; + const resultArtifacts = makeSubagentConversationArtifacts({ + messageId: idAllocator.derive.messageFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: resultNativeItemId, + }), + turnItemId: idAllocator.derive.turnItemFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: resultNativeItemId, + }), + threadId: childThreadId, + rootNodeId: childRootNodeId, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: { + driver: CLAUDE_PROVIDER, + nativeId: resultNativeItemId, + strength: "strong", + }, + role: "assistant", + text: input.result, + ordinal: resultItemOrdinal, + now, + }); + yield* emitProviderEvent({ + type: "message.updated", + driver: CLAUDE_PROVIDER, + message: resultArtifacts.message, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CLAUDE_PROVIDER, + turnItem: resultArtifacts.turnItem, + }); + } + }); + + const ensureToolCallStarted = Effect.fnUntraced(function* (input: { + readonly context: ActiveClaudeTurnContext; + readonly nativeItemId: string; + readonly toolName: string; + readonly toolInput: ClaudeNativeToolInput; + readonly parentToolUseId: string | null; + }) { + const existing = input.context.toolCalls.get(input.nativeItemId); + if (existing !== undefined) { + return existing; + } + const startedAt = yield* DateTime.now; + const classification = classifyClaudeNativeTool(input.toolName); + const subagent = + input.parentToolUseId === null + ? undefined + : input.context.subagentsByToolUseId.get(input.parentToolUseId); + const threadId = subagent?.childThreadId ?? input.context.input.threadId; + const runId = subagent === undefined ? input.context.input.runId : null; + const rootNodeId = subagent?.childRootNodeId ?? input.context.input.rootNodeId; + const parentNodeId = rootNodeId; + const ordinal = + subagent === undefined + ? yield* resolveItemOrdinal(input.context, input.nativeItemId) + : ++subagent.nextChildItemOrdinal; + const toolCall: ActiveClaudeToolCall = { + nativeItemId: input.nativeItemId, + toolName: input.toolName, + classification, + input: input.toolInput, + threadId, + runId, + rootNodeId, + parentNodeId, + ordinal, + startedAt, + }; + input.context.toolCalls.set(input.nativeItemId, toolCall); + yield* emitToolCallArtifacts( + buildToolCallArtifacts({ + context: input.context, + nativeItemId: input.nativeItemId, + toolName: input.toolName, + classification, + toolInput: input.toolInput, + threadId, + runId, + rootNodeId, + parentNodeId, + ordinal, + output: NO_CLAUDE_NATIVE_TOOL_OUTPUT, + status: "running", + startedAt, + updatedAt: startedAt, + }), + ); + return toolCall; + }); + + const buildApprovalRequestArtifacts = Effect.fnUntraced(function* (input: { + readonly context: ActiveClaudeTurnContext; + readonly nativeItemId: string; + readonly nativeRequestId: string; + readonly requestKind: ProviderRequestKind; + readonly prompt?: string; + }) { + const createdAt = yield* DateTime.now; + const requestId = yield* idAllocator.allocate.runtimeRequest({ + driver: CLAUDE_PROVIDER, + providerTurnId: input.context.providerTurnId, + nativeRequestId: input.nativeRequestId, + }); + const nodeId = idAllocator.derive.approvalNode({ requestId }); + const providerSessionId = input.context.input.providerThread.providerSessionId; + if (providerSessionId === null) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Provider thread ${input.context.input.providerThread.id} is missing a provider session id.`, + }); + } + const ordinal = yield* resolveItemOrdinal( + input.context, + `${input.nativeItemId}:approval:${input.nativeRequestId}`, + ); + const nativeItemRef = { + driver: CLAUDE_PROVIDER, + nativeId: input.nativeRequestId, + strength: "strong" as const, + }; + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + parentNodeId: idAllocator.derive.nodeFromProviderItem({ + driver: CLAUDE_PROVIDER, + nativeItemId: input.nativeItemId, + }), + rootNodeId: input.context.input.rootNodeId, + kind: "approval_request", + status: "waiting", + countsForRun: false, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef, + runtimeRequestId: requestId, + checkpointScopeId: null, + startedAt: createdAt, + completedAt: null, + }; + const request: OrchestrationV2RuntimeRequest = { + id: requestId, + nodeId, + providerTurnId: input.context.providerTurnId, + nativeRequestRef: { + driver: CLAUDE_PROVIDER, + nativeId: input.nativeRequestId, + strength: "strong", + }, + kind: input.requestKind, + status: "pending", + responseCapability: { + type: "live", + providerSessionId, + }, + createdAt, + resolvedAt: null, + }; + const turnItem: OrchestrationV2TurnItem = { + id: idAllocator.derive.approvalTurnItem({ requestId }), + threadId: input.context.input.threadId, + runId: input.context.input.runId, + nodeId, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: "waiting", + title: null, + startedAt: createdAt, + completedAt: null, + updatedAt: createdAt, + type: "approval_request", + requestId, + requestKind: input.requestKind, + ...(input.prompt === undefined ? {} : { prompt: input.prompt }), + }; + return { node, request, turnItem }; + }); + + const finalizeActiveTurn = Effect.fnUntraced(function* (input: { + readonly context: ActiveClaudeTurnContext; + readonly status: Extract< + OrchestrationV2ProviderTurn["status"], + "completed" | "interrupted" | "failed" | "cancelled" + >; + readonly completedAt: DateTime.Utc; + }) { + for (const toolCall of input.context.toolCalls.values()) { + const artifacts = buildToolCallArtifacts({ + context: input.context, + nativeItemId: toolCall.nativeItemId, + toolName: toolCall.toolName, + classification: toolCall.classification, + toolInput: toolCall.input, + threadId: toolCall.threadId, + runId: toolCall.runId, + rootNodeId: toolCall.rootNodeId, + parentNodeId: toolCall.parentNodeId, + ordinal: toolCall.ordinal, + output: NO_CLAUDE_NATIVE_TOOL_OUTPUT, + status: "failed", + startedAt: toolCall.startedAt, + updatedAt: input.completedAt, + }); + yield* emitToolCallArtifacts(artifacts); + } + input.context.toolCalls.clear(); + + if (input.context.assistant.text.length > 0) { + const ordinal = yield* resolveItemOrdinal( + input.context, + input.context.assistant.nativeItemId, + ); + const artifacts = buildAssistantArtifacts({ + idAllocator, + turnInput: input.context.input, + providerTurnId: input.context.providerTurnId, + nativeItemId: input.context.assistant.nativeItemId, + text: input.context.assistant.text, + ordinal, + startedAt: input.context.startedAt, + completedAt: input.completedAt, + }); + yield* Effect.all( + [ + emitProviderEvent({ + type: "node.updated", + driver: CLAUDE_PROVIDER, + node: artifacts.node, + }), + emitProviderEvent({ + type: "message.updated", + driver: CLAUDE_PROVIDER, + message: artifacts.message, + }), + emitProviderEvent({ + type: "turn_item.updated", + driver: CLAUDE_PROVIDER, + turnItem: artifacts.turnItem, + }), + ], + { concurrency: 1 }, + ); + } + + yield* Effect.all( + [ + emitProviderEvent({ + type: "provider_turn.updated", + driver: CLAUDE_PROVIDER, + providerTurn: providerTurnPayload({ + context: input.context, + status: input.status, + completedAt: input.completedAt, + }), + }), + ...(input.status === "completed" && + input.context.input.providerThread.nativeConversationHeadRef !== null + ? [ + emitProviderEvent({ + type: "provider_thread.updated" as const, + driver: CLAUDE_PROVIDER, + providerThread: { + ...input.context.input.providerThread, + providerSessionId: session.id, + nativeConversationHeadRef: null, + status: "idle" as const, + firstRunOrdinal: + input.context.input.providerThread.firstRunOrdinal ?? + input.context.input.runOrdinal, + lastRunOrdinal: input.context.input.runOrdinal, + updatedAt: input.completedAt, + }, + }), + ] + : []), + emitProviderEvent({ + type: "turn.terminal", + driver: CLAUDE_PROVIDER, + providerTurnId: input.context.providerTurnId, + status: input.status, + }), + ], + { concurrency: 1 }, + ); + yield* Ref.update(activeTurn, (current) => + current?.providerTurnId === input.context.providerTurnId ? null : current, + ); + yield* Ref.update(interruptedTurns, (current) => { + const next = new Set(current); + next.delete(input.context.providerTurnId); + return next; + }); + }); + + const finalizeActiveTurnAfterQueryExit = Effect.fnUntraced(function* (cause?: unknown) { + const context = yield* Ref.get(activeTurn); + if (context === null) { + return; + } + const completedAt = yield* DateTime.now; + const interrupted = (yield* Ref.get(interruptedTurns)).has(context.providerTurnId); + yield* finalizeActiveTurn({ + context, + status: interrupted ? "interrupted" : "failed", + completedAt, + }); + yield* Ref.update(interruptedTurns, (current) => { + const next = new Set(current); + next.delete(context.providerTurnId); + return next; + }); + if (cause !== undefined) { + yield* Effect.logWarning("orchestration-v2.claude-query-stream-failed", { + providerSessionId: input.providerSessionId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + cause, + }); + } + }); + + const handleSdkMessage = Effect.fnUntraced(function* (input: { + readonly query: ClaudeAgentSdkQuerySession; + readonly message: SDKMessage; + }) { + const liveQuery = yield* Ref.get(queryContext); + if (liveQuery?.query !== input.query) { + return; + } + + const message = input.message; + const context = yield* Ref.get(activeTurn); + if (context === null) { + return; + } + + if (message.type === "assistant") { + context.nativeMessageCursor = message.uuid; + } + + if (message.type === "system" && message.subtype === "task_started") { + yield* updateClaudeSubagentNode({ + context, + taskId: message.task_id, + ...(message.tool_use_id === undefined ? {} : { toolUseId: message.tool_use_id }), + ...(message.prompt === undefined ? {} : { prompt: message.prompt }), + title: message.description, + status: "running", + }); + } + + if (message.type === "system" && message.subtype === "task_notification") { + yield* updateClaudeSubagentNode({ + context, + taskId: message.task_id, + ...(message.tool_use_id === undefined ? {} : { toolUseId: message.tool_use_id }), + result: message.summary, + status: + message.status === "completed" + ? "completed" + : message.status === "stopped" + ? "cancelled" + : "failed", + }); + } + + for (const toolUse of claudeToolUseBlocksFromAssistantMessage(message)) { + if (toolUse.name === "Agent") { + continue; + } + yield* ensureToolCallStarted({ + context, + nativeItemId: toolUse.id, + toolName: toolUse.name, + toolInput: claudeNativeToolInputFromUnknown(toolUse.input), + parentToolUseId: parentToolUseIdFromSdkMessage(message), + }); + } + + for (const { toolResult, output } of claudeToolResultEntriesFromMessage(message)) { + const subagent = context.subagentsByToolUseId.get(toolResult.tool_use_id); + if (subagent !== undefined) { + const result = claudeSubagentResultText(output); + yield* updateClaudeSubagentNode({ + context, + taskId: subagent.task.nativeTaskRef?.nativeId ?? String(subagent.task.id), + toolUseId: toolResult.tool_use_id, + ...(result.length === 0 ? {} : { result }), + status: isClaudeToolResultError(toolResult) ? "failed" : "completed", + }); + continue; + } + const parentToolUseId = parentToolUseIdFromSdkMessage(message); + const toolCall = + context.toolCalls.get(toolResult.tool_use_id) ?? + (yield* ensureToolCallStarted({ + context, + nativeItemId: toolResult.tool_use_id, + toolName: toolNameFromClaudeToolResult(toolResult), + toolInput: EMPTY_CLAUDE_NATIVE_TOOL_INPUT, + parentToolUseId, + })); + const completedAt = yield* DateTime.now; + const artifacts = buildToolCallArtifacts({ + context, + nativeItemId: toolCall.nativeItemId, + toolName: toolCall.toolName, + classification: toolCall.classification, + toolInput: toolCall.input, + threadId: toolCall.threadId, + runId: toolCall.runId, + rootNodeId: toolCall.rootNodeId, + parentNodeId: toolCall.parentNodeId, + ordinal: toolCall.ordinal, + output, + status: isClaudeToolResultError(toolResult) ? "failed" : "completed", + startedAt: toolCall.startedAt, + updatedAt: completedAt, + }); + yield* emitToolCallArtifacts(artifacts); + context.toolCalls.delete(toolCall.nativeItemId); + } + + const assistantText = assistantTextFromSdkMessage(message); + if (assistantText !== null && assistantText.text.length > 0) { + context.assistant.text += assistantText.text; + context.assistant.nativeItemId = assistantText.nativeItemId; + return; + } + + const resultText = resultTextFromSdkMessage(message); + if ( + context.assistant.text.length === 0 && + resultText !== null && + resultText.text.length > 0 + ) { + context.assistant.text = resultText.text; + context.assistant.nativeItemId = resultText.nativeItemId; + } + + if (message.type === "result") { + const completedAt = yield* DateTime.now; + const interrupted = (yield* Ref.get(interruptedTurns)).has(context.providerTurnId); + const wasSteered = (yield* Ref.get(steeredTurns)).has(context.providerTurnId); + if (!interrupted && wasSteered && isClaudeActiveSteeringAbortResult(message)) { + return; + } + yield* Ref.update(steeredTurns, (current) => { + const next = new Set(current); + next.delete(context.providerTurnId); + return next; + }); + yield* finalizeActiveTurn({ + context, + status: interrupted ? "interrupted" : terminalStatusFromResult(message), + completedAt, + }); + } + }); + + const canUseToolEffect = Effect.fn("ClaudeAdapterV2.canUseTool")(function* ( + toolName: Parameters[0], + toolInput: Parameters[1], + callbackOptions: Parameters[2], + ) { + const context = yield* Ref.get(activeTurn); + if (context === null) { + return { + behavior: "deny", + message: "Claude V2 adapter has no active turn for this tool request.", + toolUseID: callbackOptions.toolUseID, + } satisfies PermissionResult; + } + + const nativeRequestId = callbackOptions.toolUseID; + const nativeToolInput = claudeNativeToolInputFromRecord(toolInput); + if (toolName !== "Agent") { + yield* ensureToolCallStarted({ + context, + nativeItemId: nativeRequestId, + toolName, + toolInput: nativeToolInput, + parentToolUseId: null, + }); + } + + const requestKind = providerRequestKindFromClaudeTool(toolName); + const prompt = + callbackOptions.title ?? + callbackOptions.description ?? + callbackOptions.decisionReason ?? + summarizeClaudeToolRequest(toolName, nativeToolInput); + const artifacts = yield* buildApprovalRequestArtifacts({ + context, + nativeItemId: nativeRequestId, + nativeRequestId, + requestKind, + prompt, + }); + const decision = yield* Deferred.make(); + yield* Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.set(String(artifacts.request.id), { + requestId: artifacts.request.id, + requestKind, + decision, + }); + return updated; + }); + yield* Effect.all( + [ + emitProviderEvent({ + type: "node.updated", + driver: CLAUDE_PROVIDER, + node: artifacts.node, + }), + emitProviderEvent({ + type: "runtime_request.updated", + driver: CLAUDE_PROVIDER, + runtimeRequest: artifacts.request, + }), + emitProviderEvent({ + type: "turn_item.updated", + driver: CLAUDE_PROVIDER, + turnItem: artifacts.turnItem, + }), + ], + { concurrency: 1 }, + ); + + const abort = () => { + runFork(Deferred.succeed(decision, "cancel")); + }; + callbackOptions.signal.addEventListener("abort", abort, { once: true }); + const resolvedDecision = yield* Deferred.await(decision).pipe( + Effect.ensuring( + Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.delete(String(artifacts.request.id)); + return updated; + }), + ), + ); + callbackOptions.signal.removeEventListener("abort", abort); + + return permissionResultFromDecision({ + decision: resolvedDecision, + toolInput, + toolUseID: callbackOptions.toolUseID, + ...(callbackOptions.suggestions === undefined + ? {} + : { suggestions: callbackOptions.suggestions }), + }); + }); + + const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) => + runPromise(canUseToolEffect(toolName, toolInput, callbackOptions)); + + const openQuery = Effect.fnUntraced(function* ( + turnInput: ProviderAdapterV2TurnInput, + nativeThreadId: string, + ) { + const queryPolicy = claudeRuntimeQueryPolicyForRuntimePolicy(turnInput.runtimePolicy); + const mcpOverrides = claudeMcpQueryOverrides({ + threadId: turnInput.threadId, + ...(queryPolicy.allowedTools === undefined + ? {} + : { allowedTools: queryPolicy.allowedTools }), + }); + const queryPolicyKey = claudeRuntimeQueryPolicyKey(queryPolicy); + const resumeSessionAt = yield* getNativeConversationHeadId(turnInput.providerThread); + const existing = yield* Ref.get(queryContext); + if ( + existing !== null && + existing.nativeThreadId === nativeThreadId && + existing.queryPolicyKey === queryPolicyKey + ) { + if (existing.currentModel !== turnInput.modelSelection.model) { + yield* existing.query.setModel(turnInput.modelSelection.model); + existing.currentModel = turnInput.modelSelection.model; + } + return existing; + } + + if (existing !== null) { + yield* existing.query.close.pipe(Effect.ignore); + } + + const openedWithResume = yield* Ref.modify(openedNativeThreads, (current) => { + const hasOpenedThread = current.has(nativeThreadId); + if (hasOpenedThread) { + return [true, current]; + } + const updated = new Set(current); + updated.add(nativeThreadId); + return [false, updated]; + }); + const shouldResume = resumeSessionAt !== undefined || openedWithResume; + const querySession = yield* queryRunner.open({ + threadId: turnInput.threadId, + providerSessionId: input.providerSessionId, + options: makeClaudeQueryOptions({ + modelSelection: turnInput.modelSelection, + nativeThreadId, + resume: shouldResume, + ...(resumeSessionAt === undefined ? {} : { resumeSessionAt }), + cwd: turnInput.runtimePolicy.cwd, + settings: adapterOptions.settings, + environment: adapterOptions.environment, + tools: queryPolicy.tools ?? CLAUDE_CODE_PRESET_TOOLS, + ...mcpOverrides, + permissionMode: queryPolicy.permissionMode, + ...(queryPolicy.allowDangerouslySkipPermissions === undefined + ? {} + : { allowDangerouslySkipPermissions: queryPolicy.allowDangerouslySkipPermissions }), + ...(shouldInstallClaudePermissionCallback(queryPolicy) ? { canUseTool } : {}), + }), + }); + const closed = yield* Deferred.make(); + const context: ClaudeLiveQueryContext = { + nativeThreadId, + query: querySession, + queryPolicyKey, + closed, + currentModel: turnInput.modelSelection.model, + }; + yield* Ref.set(queryContext, context); + yield* querySession.messages.pipe( + Stream.runForEach((message) => handleSdkMessage({ query: querySession, message })), + Effect.exit, + Effect.flatMap( + Effect.fnUntraced(function* (exit: ClaudeQueryStreamExit) { + const ownsLiveQuery = yield* Ref.modify(queryContext, (current) => + current?.query === querySession ? [true, null] : [false, current], + ); + if (ownsLiveQuery) { + yield* finalizeActiveTurnAfterQueryExit( + exit._tag === "Failure" ? exit.cause : undefined, + ); + } + }), + ), + Effect.ensuring(Deferred.succeed(closed, undefined)), + Effect.forkIn(sessionScope), + ); + return context; + }); + + const startTurn = Effect.fn("ClaudeAdapterV2.startTurn")( + function* (turnInput: ProviderAdapterV2TurnInput) { + const startedAt = yield* DateTime.now; + const nativeThreadId = yield* getNativeThreadId(turnInput.providerThread); + const nativeTurnId = `turn:${turnInput.attemptId}`; + const providerTurnId = idAllocator.derive.providerTurn({ + driver: CLAUDE_PROVIDER, + nativeTurnId, + }); + const providerTurnOrdinal = turnInput.providerTurnOrdinal; + const currentTurn = yield* Ref.get(activeTurn); + if (currentTurn !== null) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Claude provider turn ${currentTurn.providerTurnId} is still active.`, + }); + } + const context: ActiveClaudeTurnContext = { + input: turnInput, + nativeTurnId, + nativeMessageCursor: null, + providerTurnId, + providerTurnOrdinal, + startedAt, + assistant: { + text: "", + nativeItemId: `assistant:${turnInput.runId}`, + }, + toolCalls: new Map(), + subagentsByTaskId: new Map(), + subagentsByToolUseId: new Map(), + subagentNodesByTaskId: new Map(), + }; + const querySession = yield* openQuery(turnInput, nativeThreadId); + yield* Ref.set(activeTurn, context); + yield* emitProviderEvent({ + type: "provider_turn.updated", + driver: CLAUDE_PROVIDER, + providerTurn: providerTurnPayload({ + context, + status: "running", + completedAt: null, + }), + }); + yield* querySession.query.offer( + makeClaudeUserMessage({ text: turnInput.message.text }), + ); + }, + (effect, turnInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterTurnStartError({ + driver: CLAUDE_PROVIDER, + threadId: turnInput.threadId, + providerThreadId: turnInput.providerThread.id, + runId: turnInput.runId, + cause, + }), + ), + ), + ); + + const interruptTurn = Effect.fn("ClaudeAdapterV2.interruptTurn")( + function* (turnInput: ProviderAdapterV2InterruptInput) { + const existing = yield* Ref.get(queryContext); + if (existing === null) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Claude provider thread ${turnInput.providerThread.id} has no live query.`, + }); + } + const currentTurn = yield* Ref.get(activeTurn); + if (currentTurn?.providerTurnId !== turnInput.providerTurnId) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Claude provider turn ${turnInput.providerTurnId} is not the active turn.`, + }); + } + yield* Ref.update(interruptedTurns, (current) => { + const next = new Set(current); + next.add(turnInput.providerTurnId); + return next; + }); + yield* existing.query.interrupt; + yield* existing.query.close.pipe(Effect.ignore); + const closed = yield* Deferred.await(existing.closed).pipe( + Effect.timeoutOption("10 seconds"), + ); + if (Option.isSome(closed)) { + return; + } + + const completedAt = yield* DateTime.now; + yield* Effect.logWarning("orchestration-v2.claude-query-interrupt-timeout", { + providerSessionId: input.providerSessionId, + providerThreadId: turnInput.providerThread.id, + providerTurnId: turnInput.providerTurnId, + }); + yield* Ref.update(queryContext, (current) => + current?.query === existing.query ? null : current, + ); + yield* finalizeActiveTurn({ + context: currentTurn, + status: "interrupted", + completedAt, + }); + yield* Deferred.succeed(existing.closed, undefined); + }, + (effect, turnInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterInterruptError({ + driver: CLAUDE_PROVIDER, + providerThreadId: turnInput.providerThread.id, + providerTurnId: turnInput.providerTurnId, + cause, + }), + ), + ), + ); + + const steerTurn = Effect.fn("ClaudeAdapterV2.steerTurn")( + function* (turnInput: ProviderAdapterV2SteerInput) { + const existing = yield* Ref.get(queryContext); + if (existing === null) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Claude provider thread ${turnInput.providerThread.id} has no live query.`, + }); + } + const currentTurn = yield* Ref.get(activeTurn); + if (currentTurn?.providerTurnId !== turnInput.providerTurnId) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Claude provider turn ${turnInput.providerTurnId} is not the active turn.`, + }); + } + yield* Ref.update(steeredTurns, (current) => { + const next = new Set(current); + next.add(turnInput.providerTurnId); + return next; + }); + yield* existing.query.offer( + makeClaudeUserMessage({ text: turnInput.message.text, priority: "now" }), + ); + }, + (effect, turnInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterSteerRunError({ + driver: CLAUDE_PROVIDER, + providerThreadId: turnInput.providerThread.id, + providerTurnId: turnInput.providerTurnId, + cause, + }), + ), + ), + ); + + const closeSession = Effect.fnUntraced(function* () { + const existing = yield* Ref.get(queryContext); + if (existing !== null) { + yield* existing.query.close.pipe(Effect.ignore); + } + yield* Effect.yieldNow; + yield* queryRunner.assertComplete.pipe( + Effect.catchCause((cause) => + Effect.logWarning("orchestration-v2.claude-query-runner-incomplete", { + providerSessionId: input.providerSessionId, + cause, + }), + ), + ); + }); + + const closeLiveQueryForNativeThread = Effect.fnUntraced(function* (nativeThreadId: string) { + const existing = yield* Ref.get(queryContext); + if (existing === null || existing.nativeThreadId !== nativeThreadId) { + return; + } + + yield* existing.query.close.pipe(Effect.ignore); + const closed = yield* Deferred.await(existing.closed).pipe( + Effect.timeoutOption("10 seconds"), + ); + if (Option.isSome(closed)) { + return; + } + + yield* Effect.logWarning("orchestration-v2.claude-query-close-timeout-before-fork", { + providerSessionId: input.providerSessionId, + nativeThreadId, + }); + yield* Ref.update(queryContext, (current) => + current?.query === existing.query ? null : current, + ); + yield* Deferred.succeed(existing.closed, undefined); + }); + yield* Effect.addFinalizer(() => closeSession()); + + const runtime: ProviderAdapterV2SessionRuntime = { + instanceId: adapterOptions.instanceId, + driver: CLAUDE_PROVIDER, + providerSessionId: input.providerSessionId, + providerSession: session, + rawEvents: Stream.empty, + events: Stream.fromEffectRepeat(Queue.take(events)), + ensureThread: Effect.fn("ClaudeAdapterV2.ensureThread")( + function* (threadInput: ProviderAdapterV2EnsureThreadInput) { + const createdAt = yield* DateTime.now; + const nativeThreadId = yield* queryRunner.allocateSessionId; + return makeProviderThread({ + idAllocator, + providerInstanceId: adapterOptions.instanceId, + appThreadId: threadInput.threadId, + providerSessionId: input.providerSessionId, + nativeThreadId, + now: createdAt, + }); + }, + (effect, threadInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterEnsureThreadError({ + driver: CLAUDE_PROVIDER, + threadId: threadInput.threadId, + cause, + }), + ), + ), + ), + resumeThread: Effect.fn("ClaudeAdapterV2.resumeThread")( + function* (threadInput: { readonly providerThread: OrchestrationV2ProviderThread }) { + const updatedAt = yield* DateTime.now; + return { + ...threadInput.providerThread, + providerSessionId: input.providerSessionId, + status: "idle" as const, + updatedAt, + }; + }, + (effect, threadInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterResumeThreadError({ + driver: CLAUDE_PROVIDER, + providerSessionId: input.providerSessionId, + providerThreadId: threadInput.providerThread.id, + cause, + }), + ), + ), + ), + startTurn, + steerTurn, + interruptTurn, + respondToRuntimeRequest: Effect.fn("ClaudeAdapterV2.respondToRuntimeRequest")( + function* (requestInput) { + const pending = (yield* Ref.get(pendingRuntimeRequests)).get( + String(requestInput.requestId), + ); + if (pending === undefined) { + return yield* new ProviderAdapterRuntimeRequestResponseError({ + driver: CLAUDE_PROVIDER, + requestId: requestInput.requestId, + cause: new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `No pending Claude runtime request ${requestInput.requestId}.`, + }), + }); + } + if (requestInput.decision === undefined) { + return yield* new ProviderAdapterRuntimeRequestResponseError({ + driver: CLAUDE_PROVIDER, + requestId: requestInput.requestId, + cause: new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Claude ${pending.requestKind} request ${requestInput.requestId} requires an approval decision.`, + }), + }); + } + yield* Deferred.succeed(pending.decision, requestInput.decision); + }, + (effect, requestInput) => + effect.pipe( + Effect.mapError((cause) => + Schema.is(ProviderAdapterRuntimeRequestResponseError)(cause) + ? cause + : new ProviderAdapterRuntimeRequestResponseError({ + driver: CLAUDE_PROVIDER, + requestId: requestInput.requestId, + cause, + }), + ), + ), + ), + readThreadSnapshot: (snapshotInput) => + Effect.fail( + new ProviderAdapterReadThreadSnapshotError({ + driver: CLAUDE_PROVIDER, + providerThreadId: snapshotInput.providerThread.id, + cause: "Claude V2 adapter does not implement snapshots.", + }), + ), + rollbackThread: Effect.fn("ClaudeAdapterV2.rollbackThread")( + function* (rollbackInput) { + const currentTurn = yield* Ref.get(activeTurn); + if (currentTurn !== null) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Cannot roll back Claude provider thread ${rollbackInput.providerThread.id} while provider turn ${currentTurn.providerTurnId} is active.`, + }); + } + + const nativeThreadId = yield* getNativeThreadId(rollbackInput.providerThread); + yield* closeLiveQueryForNativeThread(nativeThreadId); + const now = yield* DateTime.now; + + if (rollbackInput.target.type === "thread_start") { + const resetNativeThreadId = yield* queryRunner.allocateSessionId; + return { + providerThread: { + ...makeProviderThread({ + idAllocator, + providerInstanceId: adapterOptions.instanceId, + appThreadId: rollbackInput.providerThread.appThreadId, + ...(rollbackInput.providerThread.ownerNodeId === null + ? {} + : { ownerNodeId: rollbackInput.providerThread.ownerNodeId }), + providerSessionId: input.providerSessionId, + nativeThreadId: resetNativeThreadId, + ...(rollbackInput.providerThread.forkedFrom === null + ? {} + : { forkedFrom: rollbackInput.providerThread.forkedFrom }), + now, + }), + handoffIds: rollbackInput.providerThread.handoffIds, + }, + providerTurns: [], + messages: [], + runtimeRequests: [], + }; + } + + const resumeSessionAt = yield* resolveClaudeRollbackResumeSessionAt(rollbackInput); + return { + providerThread: { + ...rollbackInput.providerThread, + providerSessionId: input.providerSessionId, + nativeConversationHeadRef: + resumeSessionAt === null + ? null + : { + driver: CLAUDE_PROVIDER, + nativeId: resumeSessionAt, + strength: "weak" as const, + }, + status: "idle" as const, + lastRunOrdinal: rollbackInput.target.appRunOrdinal, + updatedAt: now, + }, + providerTurns: [], + messages: [], + runtimeRequests: [], + }; + }, + (effect, rollbackInput) => + effect.pipe( + Effect.mapError((cause) => + Schema.is(ProviderAdapterRollbackThreadError)(cause) + ? cause + : new ProviderAdapterRollbackThreadError({ + driver: CLAUDE_PROVIDER, + providerThreadId: rollbackInput.providerThread.id, + cause, + }), + ), + ), + ), + forkThread: Effect.fn("ClaudeAdapterV2.forkThread")( + function* (forkInput) { + const currentTurn = yield* Ref.get(activeTurn); + if (currentTurn !== null) { + return yield* new ProviderAdapterProtocolError({ + driver: CLAUDE_PROVIDER, + detail: `Cannot fork Claude provider thread ${forkInput.sourceProviderThread.id} while provider turn ${currentTurn.providerTurnId} is active.`, + }); + } + + const sourceNativeThreadId = yield* getNativeThreadId(forkInput.sourceProviderThread); + yield* closeLiveQueryForNativeThread(sourceNativeThreadId); + const upToMessageId = yield* resolveClaudeForkUpToMessageId(forkInput); + const forkOptions: ForkSessionOptions = { + ...(input.runtimePolicy.cwd === null ? {} : { dir: input.runtimePolicy.cwd }), + ...(upToMessageId === undefined ? {} : { upToMessageId }), + }; + const forked = yield* queryRunner.forkSession({ + sessionId: sourceNativeThreadId, + options: forkOptions, + threadId: forkInput.targetThreadId, + providerSessionId: input.providerSessionId, + }); + yield* Ref.update(openedNativeThreads, (current) => { + const updated = new Set(current); + updated.add(forked.sessionId); + return updated; + }); + const now = yield* DateTime.now; + return makeProviderThread({ + idAllocator, + providerInstanceId: adapterOptions.instanceId, + appThreadId: forkInput.targetThreadId, + ownerNodeId: forkInput.ownerNodeId ?? null, + providerSessionId: input.providerSessionId, + nativeThreadId: forked.sessionId, + forkedFrom: { + providerThreadId: forkInput.sourceProviderThread.id, + ...(forkInput.providerTurnId === undefined + ? {} + : { providerTurnId: forkInput.providerTurnId }), + }, + now, + }); + }, + (effect, forkInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterForkThreadError({ + driver: CLAUDE_PROVIDER, + providerThreadId: forkInput.sourceProviderThread.id, + cause, + }), + ), + ), + ), + }; + + return runtime; + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterOpenSessionError({ + driver: CLAUDE_PROVIDER, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ), + ), + }); +} + +export type ClaudeAdapterV2DriverEnv = ClaudeAgentSdkQueryRunner | IdAllocatorV2 | Path.Path; + +export const ClaudeAdapterV2Driver: ProviderAdapterDriver< + ClaudeSettings, + ClaudeAdapterV2DriverEnv +> = { + driverKind: CLAUDE_DRIVER_KIND, + configSchema: ClaudeSettings, + defaultConfig: (): ClaudeSettings => DEFAULT_CLAUDE_SETTINGS, + create: Effect.fn("ClaudeAdapterV2Driver.create")( + function* (input: ProviderAdapterDriverCreateInput) { + const { instanceId, environment, enabled, config } = input; + const hostEnvironment = yield* HostProcessEnvironment; + const idAllocator = yield* IdAllocatorV2; + const queryRunner = yield* ClaudeAgentSdkQueryRunner; + const baseEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment); + const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment); + return makeClaudeAdapterV2({ + instanceId, + settings: { ...config, enabled }, + environment: claudeEnvironment, + idAllocator, + queryRunner, + }); + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterDriverCreateError({ + driver: CLAUDE_DRIVER_KIND, + instanceId: input.instanceId, + detail: "Failed to create Claude Agent SDK adapter.", + cause, + }), + ), + ), + ), +}; + +const makeDefaultClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2.layer")(function* () { + const hostEnvironment = yield* HostProcessEnvironment; + const idAllocator = yield* IdAllocatorV2; + const queryRunner = yield* ClaudeAgentSdkQueryRunner; + + return makeClaudeAdapterV2({ + instanceId: CLAUDE_DEFAULT_INSTANCE_ID, + settings: DEFAULT_CLAUDE_SETTINGS, + environment: hostEnvironment, + idAllocator, + queryRunner, + }); +}); + +export const layer: Layer.Layer< + ProviderAdapterV2, + never, + ClaudeAgentSdkQueryRunner | IdAllocatorV2 +> = Layer.effect(ProviderAdapterV2, makeDefaultClaudeAdapterV2()); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts new file mode 100644 index 00000000000..f73ddffd4fb --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -0,0 +1,373 @@ +import { + CheckpointId, + EnvironmentId, + NodeId, + type OrchestrationV2ProviderThread, + type OrchestrationV2ProviderTurn, + ProviderInstanceId, + ProviderSessionId, + ProviderThreadId, + ProviderTurnId, + RunAttemptId, + ThreadId, +} from "@t3tools/contracts"; +import { assert, describe, it } from "@effect/vitest"; +import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { SpawnExecutableResolution } from "@t3tools/shared/shell"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import { ChildProcess } from "effect/unstable/process"; + +import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; +import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts"; +import { + buildCodexTurnStartParams, + CODEX_DRIVER_KIND, + codexThreadRuntimeParams, + makeCodexAppServerProtocolLogger, + makeCodexAppServerSpawnCommand, + projectCodexDynamicToolItem, + resolveCodexRollbackTurnCount, +} from "./CodexAdapterV2.ts"; + +describe("CodexAdapterV2 runtime policy", () => { + it.effect("derives concrete Codex turn policies from every T3 runtime mode", () => + Effect.gen(function* () { + const build = (runtimeMode: "approval-required" | "auto-accept-edits" | "full-access") => + buildCodexTurnStartParams({ + nativeThreadId: `native-${runtimeMode}`, + codexInput: [{ type: "text", text: "test" }], + runtimePolicy: { + runtimeMode, + interactionMode: "default", + cwd: null, + }, + model: "gpt-5.4", + }); + + const approvalRequired = yield* build("approval-required"); + const autoAcceptEdits = yield* build("auto-accept-edits"); + const fullAccess = yield* build("full-access"); + + assert.equal(approvalRequired.approvalPolicy, "untrusted"); + assert.equal(approvalRequired.sandboxPolicy?.type, "readOnly"); + assert.equal(autoAcceptEdits.approvalPolicy, "on-request"); + assert.equal(autoAcceptEdits.sandboxPolicy?.type, "workspaceWrite"); + assert.equal(fullAccess.approvalPolicy, "never"); + assert.equal(fullAccess.sandboxPolicy?.type, "dangerFullAccess"); + }), + ); + + it.effect("preserves explicit Codex turn policy overrides", () => + Effect.gen(function* () { + const params = yield* buildCodexTurnStartParams({ + nativeThreadId: "native-override", + codexInput: [{ type: "text", text: "test" }], + runtimePolicy: { + runtimeMode: "full-access", + interactionMode: "default", + cwd: null, + approvalPolicy: "on-request", + sandboxPolicy: { + type: "readOnly", + }, + }, + model: "gpt-5.4", + }); + + assert.equal(params.approvalPolicy, "on-request"); + assert.equal(params.sandboxPolicy?.type, "readOnly"); + }), + ); +}); + +describe("CodexAdapterV2 process spawning", () => { + it("injects cwd, model, and MCP authorization into thread-scoped params", () => { + const threadId = ThreadId.make("thread-codex-mcp"); + McpProviderSession.setMcpProviderSession({ + environmentId: EnvironmentId.make("environment-codex-mcp"), + threadId, + providerSessionId: "mcp-session-codex", + providerInstanceId: ProviderInstanceId.make("codex"), + endpoint: "http://127.0.0.1:43123/mcp", + authorizationHeader: "Bearer secret-codex-token", + }); + + try { + assert.deepEqual( + codexThreadRuntimeParams({ + threadId, + modelSelection: { model: "gpt-5.4" }, + runtimePolicy: { + runtimeMode: "full-access", + interactionMode: "default", + cwd: "/workspace/thread-codex-mcp", + }, + }), + { + cwd: "/workspace/thread-codex-mcp", + model: "gpt-5.4", + config: { + mcp_servers: { + "t3-code": { + url: "http://127.0.0.1:43123/mcp", + http_headers: { + Authorization: "Bearer secret-codex-token", + }, + }, + }, + }, + }, + ); + } finally { + McpProviderSession.clearMcpProviderSession(threadId); + } + }); + + it.effect("resolves Windows command shims through the shared spawn policy", () => + Effect.gen(function* () { + const command = yield* makeCodexAppServerSpawnCommand({ + command: "codex", + args: ["app-server", "argument with spaces"], + cwd: "C:\\workspace", + env: { CUSTOM: "1" }, + extendEnv: true, + }); + + assert.isTrue(ChildProcess.isStandardCommand(command)); + if (!ChildProcess.isStandardCommand(command)) { + return; + } + assert.equal(command.command, '^"C:\\npm\\codex.cmd^"'); + assert.deepEqual(command.args, ['^"app-server^"', '^"argument^ with^ spaces^"']); + assert.equal(command.options.shell, true); + assert.equal(command.options.cwd, "C:\\workspace"); + assert.deepEqual(command.options.env, { CUSTOM: "1" }); + assert.equal(command.options.extendEnv, true); + }).pipe( + Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(HostProcessEnvironment, { + PATH: "C:\\Windows\\System32", + HOST_ONLY: "1", + }), + Effect.provideService(SpawnExecutableResolution, (_command, _platform, environment) => { + assert.equal(environment.HOST_ONLY, "1"); + assert.equal(environment.CUSTOM, "1"); + return "C:\\npm\\codex.cmd"; + }), + ), + ); + + it.effect("uses direct execution for native executables", () => + Effect.gen(function* () { + const command = yield* makeCodexAppServerSpawnCommand({ + command: "codex.exe", + args: ["app-server"], + }); + + assert.isTrue(ChildProcess.isStandardCommand(command)); + if (!ChildProcess.isStandardCommand(command)) { + return; + } + assert.equal(command.command, "C:\\bin\\codex.exe"); + assert.deepEqual(command.args, ["app-server"]); + assert.equal(command.options.shell, false); + }).pipe( + Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(SpawnExecutableResolution, () => "C:\\bin\\codex.exe"), + ), + ); +}); + +describe("CodexAdapterV2 dynamic tool projection", () => { + it("preserves MCP arguments and prefers structured output", () => { + const projection = projectCodexDynamicToolItem({ + type: "mcpToolCall", + id: "call-create-threads", + server: "t3-code", + tool: "create_threads", + status: "completed", + arguments: { + threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], + }, + result: { + content: [{ type: "text", text: '{"threads":[{"threadId":"thread:mcp:fixture:0"}]}' }], + structuredContent: { + threads: [{ threadId: "thread:mcp:fixture:0" }], + }, + }, + }); + + assert.deepEqual(projection, { + toolName: "t3-code.create_threads", + input: { + threads: [{ title: "Fixture child", prompt: "fixture child prompt" }], + }, + output: { + threads: [{ threadId: "thread:mcp:fixture:0" }], + }, + status: "completed", + }); + }); + + it("preserves namespaced dynamic tool output", () => { + const projection = projectCodexDynamicToolItem({ + type: "dynamicToolCall", + id: "call-dynamic", + namespace: "workspace", + tool: "inspect", + status: "failed", + arguments: { path: "package.json" }, + contentItems: [{ type: "inputText", text: "inspection failed" }], + success: false, + }); + + assert.deepEqual(projection, { + toolName: "workspace.inspect", + input: { path: "package.json" }, + output: [{ type: "inputText", text: "inspection failed" }], + status: "failed", + }); + }); +}); + +describe("CodexAdapterV2 native protocol logging", () => { + it.effect("writes app-server protocol frames to the native provider log", () => + Effect.gen(function* () { + const writes: Array<{ + readonly event: unknown; + readonly threadId: ThreadId | null; + }> = []; + const logger: EventNdjsonLogger = { + filePath: "/tmp/events.log", + write: (event, threadId) => + Effect.sync(() => { + writes.push({ event, threadId }); + }), + close: () => Effect.void, + }; + const threadId = ThreadId.make("thread-1"); + const providerSessionId = ProviderSessionId.make("provider-session-1"); + const protocolLogger = makeCodexAppServerProtocolLogger({ + nativeEventLogger: logger, + threadId, + providerSessionId, + }); + + assert.notEqual(protocolLogger, undefined); + if (protocolLogger === undefined) { + return; + } + + yield* protocolLogger({ + direction: "incoming", + stage: "decoded", + payload: { + method: "thread/event", + params: { + id: "evt-1", + http_headers: { Authorization: "Bearer secret-codex-token" }, + usage: { inputTokens: 12, outputTokens: 8, totalTokens: 20 }, + }, + }, + }); + + assert.equal(writes.length, 1); + assert.equal(writes[0]?.threadId, threadId); + assert.deepEqual(writes[0]?.event, { + provider: "codex", + protocol: "codex.app-server", + kind: "protocol", + providerSessionId, + event: { + direction: "incoming", + stage: "decoded", + payload: { + method: "thread/event", + params: { + id: "evt-1", + http_headers: { Authorization: "[REDACTED]" }, + usage: { inputTokens: 12, outputTokens: 8, totalTokens: 20 }, + }, + }, + }, + }); + }), + ); + + it("does not install a protocol logger when native logging is unavailable", () => { + const protocolLogger = makeCodexAppServerProtocolLogger({ + nativeEventLogger: undefined, + threadId: ThreadId.make("thread-1"), + providerSessionId: ProviderSessionId.make("provider-session-1"), + }); + + assert.equal(protocolLogger, undefined); + }); +}); + +describe("CodexAdapterV2 rollback mapping", () => { + it.effect("derives native rollback count from durable provider turns", () => + Effect.gen(function* () { + const now = yield* DateTime.now; + const providerThreadId = ProviderThreadId.make("provider-thread-codex-rollback"); + const providerThread: OrchestrationV2ProviderThread = { + id: providerThreadId, + driver: CODEX_DRIVER_KIND, + providerInstanceId: ProviderInstanceId.make("codex"), + providerSessionId: ProviderSessionId.make("provider-session-codex-rollback"), + appThreadId: ThreadId.make("thread-codex-rollback"), + ownerNodeId: null, + nativeThreadRef: { + driver: CODEX_DRIVER_KIND, + nativeId: "native-thread-codex-rollback", + strength: "strong", + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: 1, + lastRunOrdinal: 3, + handoffIds: [], + forkedFrom: null, + createdAt: now, + updatedAt: now, + }; + const providerTurn = ( + id: string, + ordinal: number, + status: OrchestrationV2ProviderTurn["status"], + ): OrchestrationV2ProviderTurn => ({ + id: ProviderTurnId.make(id), + providerThreadId, + nodeId: NodeId.make(`node-${id}`), + runAttemptId: RunAttemptId.make(`run-attempt-${id}`), + nativeTurnRef: { + driver: CODEX_DRIVER_KIND, + nativeId: `native-${id}`, + strength: "strong", + }, + ordinal, + status, + startedAt: now, + completedAt: status === "running" || status === "pending" ? null : now, + }); + const firstTurn = providerTurn("provider-turn-first", 1, "completed"); + const secondTurn = providerTurn("provider-turn-second", 2, "completed"); + const runningTurn = providerTurn("provider-turn-running", 3, "running"); + const interruptedTurn = providerTurn("provider-turn-interrupted", 4, "interrupted"); + + const numTurns = yield* resolveCodexRollbackTurnCount({ + providerThread, + target: { + type: "provider_turn", + checkpointId: CheckpointId.make("checkpoint-first"), + appRunOrdinal: 1, + providerTurn: firstTurn, + }, + providerThreadTurns: [interruptedTurn, runningTurn, secondTurn, firstTurn], + }); + + assert.equal(numTurns, 2); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts new file mode 100644 index 00000000000..aacddc6cffc --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts @@ -0,0 +1,212 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { type ProviderReplayTranscript } from "@t3tools/contracts"; +import * as CodexClient from "effect-codex-app-server/client"; +import * as CodexReplay from "effect-codex-app-server/replay"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; +import * as Schema from "effect/Schema"; + +import { ServerConfig } from "../../config.ts"; +import { layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { ProviderAdapterOpenSessionError } from "../ProviderAdapter.ts"; +import { ProviderAdapterDriverCreateError } from "../ProviderAdapterDriver.ts"; +import { makeDriverLayer as makeProviderAdapterRegistryDriverLayer } from "../ProviderAdapterRegistry.ts"; +import type { OrchestratorV2ProviderReplayHarness } from "../testkit/ProviderReplayHarness.ts"; +import { + CODEX_DEFAULT_INSTANCE_ID, + CODEX_DRIVER_KIND, + CodexAdapterV2Driver, + CodexAppServerClientFactory, +} from "./CodexAdapterV2.ts"; + +export class CodexReplayTranscriptDecodeError extends Schema.TaggedErrorClass()( + "CodexReplayTranscriptDecodeError", + { + driver: Schema.optional(Schema.String), + protocol: Schema.optional(Schema.String), + scenario: Schema.optional(Schema.String), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to decode Codex app-server replay transcript for scenario ${this.scenario ?? ""}.`; + } +} + +export const CodexOrchestratorReplayHarnessError = Schema.Union([ + CodexReplayTranscriptDecodeError, + CodexReplay.CodexAppServerReplayError, + ProviderAdapterDriverCreateError, +]); +export type CodexOrchestratorReplayHarnessError = typeof CodexOrchestratorReplayHarnessError.Type; + +function metadataFromTranscript(transcript: ProviderReplayTranscript): { + readonly provider?: string; + readonly protocol?: string; + readonly scenario?: string; +} { + return { + provider: transcript.provider, + protocol: transcript.protocol, + scenario: transcript.scenario, + }; +} + +function makeReplayServerConfig( + scenario: string, +): Effect.Effect< + ServerConfig["Service"], + PlatformError.PlatformError, + FileSystem.FileSystem | Path.Path +> { + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = yield* fs.makeTempDirectory({ + prefix: `t3-orchestration-v2-codex-${scenario}-`, + }); + const stateDir = path.join(baseDir, "userdata"); + const logsDir = path.join(stateDir, "logs"); + const providerLogsDir = path.join(logsDir, "provider"); + const terminalLogsDir = path.join(logsDir, "terminals"); + const attachmentsDir = path.join(stateDir, "attachments"); + const worktreesDir = path.join(baseDir, "worktrees"); + const providerStatusCacheDir = path.join(baseDir, "caches"); + + for (const directory of [ + stateDir, + logsDir, + providerLogsDir, + terminalLogsDir, + attachmentsDir, + worktreesDir, + providerStatusCacheDir, + ]) { + yield* fs.makeDirectory(directory, { recursive: true }); + } + + return { + logLevel: "Error", + traceMinLevel: "Info", + traceTimingEnabled: true, + traceBatchWindowMs: 200, + traceMaxBytes: 10 * 1024 * 1024, + traceMaxFiles: 10, + otlpTracesUrl: undefined, + otlpMetricsUrl: undefined, + otlpExportIntervalMs: 10_000, + otlpServiceName: "t3-server", + mode: "web", + port: 0, + host: undefined, + cwd: process.cwd(), + baseDir, + staticDir: undefined, + devUrl: undefined, + noBrowser: false, + startupPresentation: "browser", + tailscaleServeEnabled: false, + tailscaleServePort: 443, + desktopBootstrapToken: undefined, + autoBootstrapProjectFromCwd: false, + logWebSocketEvents: false, + stateDir, + dbPath: path.join(stateDir, "state.sqlite"), + keybindingsConfigPath: path.join(stateDir, "keybindings.json"), + settingsPath: path.join(stateDir, "settings.json"), + providerStatusCacheDir, + worktreesDir, + attachmentsDir, + logsDir, + serverLogPath: path.join(logsDir, "server.log"), + serverTracePath: path.join(logsDir, "server.trace.ndjson"), + providerLogsDir, + providerEventLogPath: path.join(providerLogsDir, "events.log"), + terminalLogsDir, + anonymousIdPath: path.join(stateDir, "anonymous-id"), + environmentIdPath: path.join(stateDir, "environment-id"), + serverRuntimeStatePath: path.join(stateDir, "server-runtime.json"), + secretsDir: path.join(stateDir, "secrets"), + }; + }); +} + +export function makeCodexProviderAdapterRegistryReplayLayer(input: { + readonly transcript: CodexReplay.CodexAppServerReplayTranscript; + readonly driver?: CodexReplay.CodexAppServerReplayDriver; +}) { + const replayLayer = + input.driver === undefined + ? CodexReplay.layerReplay(input.transcript) + : CodexReplay.layerReplayWithDriver(input.driver); + const replayClientFactoryLayer = Layer.succeed(CodexAppServerClientFactory, { + open: (openInput) => + Effect.gen(function* () { + const context = yield* Layer.build(replayLayer).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterOpenSessionError({ + driver: CODEX_DRIVER_KIND, + providerSessionId: openInput.providerSessionId, + cause, + }), + ), + ); + return yield* Effect.service(CodexClient.CodexAppServerClient).pipe( + Effect.provide(context), + ); + }), + }); + const serverConfigLayer = Layer.effect( + ServerConfig, + makeReplayServerConfig(input.transcript.scenario).pipe(Effect.orDie), + ).pipe(Layer.provide(NodeServices.layer)); + const registryLayer = makeProviderAdapterRegistryDriverLayer({ + drivers: [CodexAdapterV2Driver], + configMap: { + [CODEX_DEFAULT_INSTANCE_ID]: { + driver: CODEX_DRIVER_KIND, + }, + }, + }).pipe( + Layer.provide( + Layer.mergeAll( + replayClientFactoryLayer, + serverConfigLayer, + NodeServices.layer, + idAllocatorLayer, + ), + ), + ); + + return registryLayer; +} + +export const CodexOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness< + CodexReplay.CodexAppServerReplayTranscript, + CodexOrchestratorReplayHarnessError +> = { + driver: CODEX_DRIVER_KIND, + decodeTranscript: (transcript) => + Schema.decodeUnknownEffect(CodexReplay.CodexAppServerReplayTranscript)(transcript).pipe( + Effect.mapError( + (cause) => + new CodexReplayTranscriptDecodeError({ + ...metadataFromTranscript(transcript), + cause, + }), + ), + ), + makeProviderAdapterRegistryLayer: (transcript) => { + return Layer.effectContext( + CodexReplay.makeReplayDriver(transcript).pipe( + Effect.flatMap((driver) => + Layer.build(makeCodexProviderAdapterRegistryReplayLayer({ transcript, driver })), + ), + ), + ); + }, +}; diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts new file mode 100644 index 00000000000..3805540d820 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -0,0 +1,3645 @@ +import { CodexSettings, defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { resolveSpawnCommand } from "@t3tools/shared/shell"; +import type { + ChatAttachment, + OrchestrationV2ConversationMessage, + OrchestrationV2ExecutionNode, + OrchestrationV2PlanArtifact, + OrchestrationV2ProviderCapabilities, + OrchestrationV2ProviderSession, + OrchestrationV2ProviderThread, + OrchestrationV2ProviderTurn, + OrchestrationV2PlanStep, + OrchestrationV2RuntimeRequest, + OrchestrationV2Subagent, + OrchestrationV2TurnItem, + ProviderUserInputAnswers, + ProviderApprovalDecision, + ProviderRequestKind, + ProviderTurnId, + ProviderInstanceId, + RuntimeMode, + RuntimeRequestId, + ThreadId, +} from "@t3tools/contracts"; +import * as CodexClient from "effect-codex-app-server/client"; +import * as CodexSchema from "effect-codex-app-server/schema"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Scope from "effect/Scope"; +import * as Stream from "effect/Stream"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; + +import { resolveAttachmentPath } from "../../attachmentStore.ts"; +import { ServerConfig } from "../../config.ts"; +import { CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS } from "../../provider/CodexDeveloperInstructions.ts"; +import { + materializeCodexShadowHome, + resolveCodexHomeLayout, +} from "../../provider/Drivers/CodexHomeLayout.ts"; +import { + type EventNdjsonLogger, + makeEventNdjsonLogger, +} from "../../provider/Layers/EventNdjsonLogger.ts"; +import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; +import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; +import { + ProviderAdapterDriverCreateError, + type ProviderAdapterDriver, +} from "../ProviderAdapterDriver.ts"; +import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts"; +import { + ProviderAdapterEnsureThreadError, + ProviderAdapterForkThreadError, + ProviderAdapterInterruptError, + ProviderAdapterOpenSessionError, + ProviderAdapterProtocolError, + ProviderAdapterReadThreadSnapshotError, + ProviderAdapterResumeThreadError, + ProviderAdapterRollbackThreadError, + ProviderAdapterRuntimeRequestResponseError, + ProviderAdapterSteerRunError, + ProviderAdapterTurnStartError, + ProviderAdapterV2, + type ProviderAdapterV2Shape, + type ProviderAdapterV2Event, + type ProviderAdapterV2ForkThreadInput, + type ProviderAdapterV2RollbackThreadInput, + type ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2SessionRuntime, + type ProviderAdapterV2SteerInput, + type ProviderAdapterV2TurnInput, +} from "../ProviderAdapter.ts"; +import { + makeSubagentChildThread, + makeSubagentConversationArtifacts, + subagentThreadTitle, +} from "../SubagentProjection.ts"; + +const CODEX_PROVIDER = ProviderDriverKind.make("codex"); +export const CODEX_DRIVER_KIND = CODEX_PROVIDER; +export const CODEX_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(CODEX_DRIVER_KIND); +const DEFAULT_CODEX_SETTINGS = Schema.decodeSync(CodexSettings)({}); +const CODEX_CLIENT_INFO = { + name: "t3code_desktop", + title: "T3 Code Desktop", + version: "0.1.0", +} as const; +const CODEX_CLIENT_CAPABILITIES = { + experimentalApi: true, +} as const; + +export const CodexProviderCapabilitiesV2 = { + sessions: { + supportsMultipleProviderThreadsPerSession: true, + supportsModelSwitchInSession: true, + supportsProviderSwitchingViaHandoff: true, + supportsRuntimeModeSwitchInSession: true, + pendingRequestsSurviveRestart: false, + }, + threads: { + canCreateEmptyThread: true, + canReadThreadSnapshot: true, + canRollbackThread: true, + canForkThread: true, + canForkFromTurn: true, + canForkFromSubagentThread: true, + exposesNativeThreadId: true, + }, + turns: { + exposesNativeTurnId: true, + emitsTurnStarted: true, + emitsTurnCompleted: true, + supportsInterrupt: true, + supportsActiveSteering: true, + supportsSteeringByInterruptRestart: true, + supportsQueuedMessages: true, + terminalStatusQuality: "strong", + }, + streaming: { + streamsAssistantText: true, + streamsReasoning: true, + streamsToolOutput: true, + streamsPlanText: true, + emitsMessageCompleted: true, + }, + tools: { + exposesToolItemIds: true, + emitsToolStarted: true, + emitsToolCompleted: true, + emitsToolOutput: true, + supportsMcpTools: true, + supportsDynamicToolCallbacks: true, + }, + approvals: { + supportsCommandApproval: true, + supportsFileReadApproval: true, + supportsFileChangeApproval: true, + supportsApplyPatchApproval: true, + approvalsHaveNativeRequestIds: true, + approvalCallbacksAreLiveOnly: true, + approvalsCanOriginateFromSubagents: true, + }, + planning: { + emitsPlanUpdated: true, + emitsTodoList: true, + emitsProposedPlan: true, + supportsStructuredQuestions: true, + planDeltasHaveItemIds: true, + }, + subagents: { + supportsSubagents: true, + exposesSubagentThreadIds: true, + emitsSubagentLifecycle: true, + canWaitForSubagents: true, + canCloseSubagents: true, + canForkSubagentThread: true, + }, + context: { + acceptsSystemContext: true, + acceptsDeveloperContext: true, + acceptsSyntheticUserContext: true, + canGenerateSummaries: true, + canConsumeHandoffSummaries: true, + supportsDeltaHandoff: true, + supportsFullThreadHandoff: true, + maxRecommendedHandoffChars: null, + }, + checkpointing: { + appCanCheckpointFilesystem: true, + supportsNestedCheckpointScopes: true, + providerCanRollbackConversation: true, + providerRollbackReturnsSnapshot: true, + providerCanReadConversationSnapshot: true, + }, + identity: { + nativeThreadIds: "strong", + nativeTurnIds: "strong", + nativeItemIds: "strong", + nativeRequestIds: "strong", + }, +} satisfies OrchestrationV2ProviderCapabilities; + +function toProtocolError(detail: string, payload?: unknown): ProviderAdapterProtocolError { + return new ProviderAdapterProtocolError({ + driver: CODEX_PROVIDER, + detail, + ...(payload === undefined ? {} : { payload }), + }); +} + +function normalizeCodexCause(error: unknown): unknown { + return error; +} + +function codexTimestamp(seconds: number | null | undefined): DateTime.Utc { + return seconds === null || seconds === undefined + ? DateTime.nowUnsafe() + : DateTime.makeUnsafe(seconds * 1000); +} + +function codexUserMessageText( + content: ReadonlyArray, +): string { + return content + .flatMap((item) => (item.type === "text" ? [item.text] : [])) + .join("\n") + .trim(); +} + +function mapCodexTurnStatus( + status: CodexSchema.V2TurnCompletedNotification__TurnStatus, +): OrchestrationV2ProviderTurn["status"] { + switch (status) { + case "completed": + return "completed"; + case "interrupted": + return "interrupted"; + case "failed": + return "failed"; + case "inProgress": + return "running"; + } +} + +function providerTurnStatusToTerminal( + status: OrchestrationV2ProviderTurn["status"], +): Extract["status"] { + switch (status) { + case "completed": + return "completed"; + case "interrupted": + return "interrupted"; + case "failed": + return "failed"; + case "cancelled": + return "cancelled"; + case "pending": + case "running": + return "failed"; + } +} + +function codexItemStatus(status: "inProgress" | "completed" | "failed" | "declined"): { + readonly node: OrchestrationV2ExecutionNode["status"]; + readonly turnItem: OrchestrationV2TurnItem["status"]; + readonly completed: boolean; +} { + switch (status) { + case "inProgress": + return { node: "running", turnItem: "running", completed: false }; + case "completed": + return { + node: "completed", + turnItem: "completed", + completed: true, + }; + case "failed": + return { node: "failed", turnItem: "failed", completed: true }; + case "declined": + return { + node: "cancelled", + turnItem: "cancelled", + completed: true, + }; + } +} + +export interface CodexDynamicToolProjection { + readonly toolName: string; + readonly input: unknown; + readonly output?: unknown; + readonly status: OrchestrationV2TurnItem["status"]; +} + +function codexMcpToolOutput( + item: Extract, +): unknown | undefined { + const resultOutput = + item.result === null || item.result === undefined + ? undefined + : item.result.structuredContent !== null && item.result.structuredContent !== undefined + ? item.result.structuredContent + : item.result.content; + + if (item.error === null || item.error === undefined) { + return resultOutput; + } + return resultOutput === undefined + ? { error: item.error.message } + : { error: item.error.message, result: resultOutput }; +} + +function codexDynamicToolOutput( + item: Extract, +): unknown | undefined { + if (item.contentItems !== null && item.contentItems !== undefined) { + return item.contentItems; + } + return item.success === false ? { success: false } : undefined; +} + +export function projectCodexDynamicToolItem( + item: CodexDynamicToolItem, +): CodexDynamicToolProjection { + const output = + item.type === "mcpToolCall" ? codexMcpToolOutput(item) : codexDynamicToolOutput(item); + const toolName = + item.type === "mcpToolCall" + ? `${item.server}.${item.tool}` + : [trimText(item.namespace), item.tool].filter(Boolean).join("."); + const projection: CodexDynamicToolProjection = { + toolName, + input: item.arguments, + status: codexItemStatus(item.status).turnItem, + }; + return output === undefined ? projection : { ...projection, output }; +} + +function codexNativeItemRef(nativeItemId: string) { + return { + driver: CODEX_PROVIDER, + nativeId: nativeItemId, + strength: "strong" as const, + }; +} + +function trimText(value: string | null | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed && trimmed.length > 0 ? trimmed : undefined; +} + +function nonEmptyText(value: string | null | undefined, fallback: string): string { + return trimText(value) ?? fallback; +} + +function codexPlanStepStatus( + status: CodexSchema.V2TurnPlanUpdatedNotification__TurnPlanStepStatus, +): OrchestrationV2PlanStep["status"] { + switch (status) { + case "completed": + return "completed"; + case "inProgress": + return "running"; + case "pending": + return "pending"; + } +} + +function approvalDecisionToLegacyReviewDecision( + decision: ProviderApprovalDecision, +): CodexSchema.ExecCommandApprovalResponse__ReviewDecision { + switch (decision) { + case "accept": + return "approved"; + case "acceptForSession": + return "approved_for_session"; + case "decline": + return "denied"; + case "cancel": + return "abort"; + } +} + +function providerRequestKindFromPermissions( + permissions: CodexSchema.PermissionsRequestApprovalParams["permissions"], +): ProviderRequestKind { + if ((permissions.fileSystem?.write?.length ?? 0) > 0) { + return "file-change"; + } + if ((permissions.fileSystem?.read?.length ?? 0) > 0) { + return "file-read"; + } + return "command"; +} + +function permissionsResponseFromDecision(input: { + readonly decision: ProviderApprovalDecision; + readonly permissions: CodexSchema.PermissionsRequestApprovalParams["permissions"]; +}): CodexSchema.PermissionsRequestApprovalResponse { + if (input.decision !== "accept" && input.decision !== "acceptForSession") { + return { permissions: {}, scope: "turn" }; + } + + return { + permissions: input.permissions, + scope: input.decision === "acceptForSession" ? "session" : "turn", + }; +} + +function answerValueToStrings(value: unknown): ReadonlyArray { + if (Array.isArray(value)) { + return value.map((item) => String(item)); + } + if (typeof value === "string") { + return [value]; + } + if (value === null || value === undefined) { + return []; + } + if (typeof value === "number" || typeof value === "boolean") { + return [String(value)]; + } + return [JSON.stringify(value)]; +} + +function toCodexUserInputAnswers( + answers: ProviderUserInputAnswers, + allowedQuestionIds: ReadonlySet, +): CodexSchema.ToolRequestUserInputResponse["answers"] { + return Object.fromEntries( + Object.entries(answers).flatMap(([questionId, value]) => + allowedQuestionIds.has(questionId) + ? [[questionId, { answers: [...answerValueToStrings(value)] }]] + : [], + ), + ); +} + +function compactStrings(values: ReadonlyArray): ReadonlyArray { + return values.flatMap((value) => { + const trimmed = trimText(value); + return trimmed === undefined ? [] : [trimmed]; + }); +} + +function webSearchPatterns(item: CodexWebSearchItem): ReadonlyArray { + if (item.action === null || item.action === undefined) { + return compactStrings([item.query]); + } + + switch (item.action.type) { + case "search": + return compactStrings([...(item.action.queries ?? []), item.action.query, item.query]); + case "openPage": + return compactStrings([item.action.url, item.query]); + case "findInPage": + return compactStrings([item.action.pattern, item.action.url, item.query]); + case "other": + return compactStrings([item.query]); + } +} + +const decodeTurnApprovalPolicy = Schema.decodeUnknownEffect( + Schema.Union([CodexSchema.V2TurnStartParams__AskForApproval, Schema.Null]), +); +const decodeTurnSandboxPolicy = Schema.decodeUnknownEffect( + Schema.Union([CodexSchema.V2TurnStartParams__SandboxPolicy, Schema.Null]), +); +const decodeTurnReasoningEffort = Schema.decodeUnknownEffect( + Schema.Union([CodexSchema.V2TurnStartParams__ReasoningEffort, Schema.Null]), +); + +const CodexTurnStartParamsWithCollaborationMode = CodexSchema.V2TurnStartParams.pipe( + Schema.fieldsAssign({ + collaborationMode: Schema.optionalKey(CodexSchema.ClientRequest__CollaborationMode), + }), +); +type CodexTurnStartParamsWithCollaborationMode = + typeof CodexTurnStartParamsWithCollaborationMode.Type; + +function codexRuntimeModeTurnDefaults(runtimeMode: RuntimeMode): { + readonly approvalPolicy: CodexSchema.V2TurnStartParams__AskForApproval; + readonly sandboxPolicy: CodexSchema.V2TurnStartParams__SandboxPolicy; +} { + switch (runtimeMode) { + case "approval-required": + return { + approvalPolicy: "untrusted", + sandboxPolicy: { + type: "readOnly", + }, + }; + case "auto-accept-edits": + return { + approvalPolicy: "on-request", + sandboxPolicy: { + type: "workspaceWrite", + }, + }; + case "full-access": + return { + approvalPolicy: "never", + sandboxPolicy: { + type: "dangerFullAccess", + }, + }; + } +} + +export function buildCodexTurnStartParams(input: { + readonly nativeThreadId: string; + readonly codexInput: ReadonlyArray; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; + readonly model: string; +}) { + return Effect.gen(function* () { + const runtimeModeDefaults = codexRuntimeModeTurnDefaults(input.runtimePolicy.runtimeMode); + const approvalPolicy = + input.runtimePolicy.approvalPolicy === undefined + ? runtimeModeDefaults.approvalPolicy + : yield* decodeTurnApprovalPolicy(input.runtimePolicy.approvalPolicy); + const sandboxPolicy = + input.runtimePolicy.sandboxPolicy === undefined + ? runtimeModeDefaults.sandboxPolicy + : yield* decodeTurnSandboxPolicy(input.runtimePolicy.sandboxPolicy); + const effort = + input.runtimePolicy.reasoningEffort === undefined + ? undefined + : yield* decodeTurnReasoningEffort(input.runtimePolicy.reasoningEffort); + const collaborationMode: CodexSchema.ClientRequest__CollaborationMode | undefined = + input.runtimePolicy.interactionMode === "plan" + ? { + mode: "plan", + settings: { + model: input.model, + reasoning_effort: effort ?? "medium", + developer_instructions: CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, + }, + } + : undefined; + + return yield* Schema.decodeUnknownEffect(CodexTurnStartParamsWithCollaborationMode)({ + threadId: input.nativeThreadId, + input: input.codexInput, + ...(approvalPolicy === undefined ? {} : { approvalPolicy }), + ...(sandboxPolicy === undefined ? {} : { sandboxPolicy }), + ...(effort === undefined ? {} : { effort }), + ...(collaborationMode === undefined ? {} : { collaborationMode }), + }); + }); +} + +function providerSession(input: { + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; + readonly providerInstanceId: ProviderInstanceId; + readonly cwd: string | null; + readonly model: string; + readonly now: DateTime.Utc; +}): OrchestrationV2ProviderSession { + return { + id: input.providerSessionId, + driver: CODEX_PROVIDER, + providerInstanceId: input.providerInstanceId, + status: "ready", + cwd: input.cwd ?? process.cwd(), + model: input.model, + capabilities: CodexProviderCapabilitiesV2, + createdAt: input.now, + updatedAt: input.now, + lastError: null, + }; +} + +function getNativeThreadId(providerThread: OrchestrationV2ProviderThread) { + return Effect.gen(function* () { + const nativeThreadId = providerThread.nativeThreadRef?.nativeId; + if (nativeThreadId === undefined || nativeThreadId === null) { + return yield* toProtocolError( + `Provider thread ${providerThread.id} is missing a native Codex thread id.`, + ); + } + return nativeThreadId; + }); +} + +function providerThreadFromCodexThread(input: { + readonly appThreadId: ThreadId | null; + readonly idAllocator: IdAllocatorV2Shape; + readonly ownerNodeId: OrchestrationV2ProviderThread["ownerNodeId"]; + readonly providerSessionId: OrchestrationV2ProviderThread["providerSessionId"]; + readonly providerInstanceId: ProviderInstanceId; + readonly thread: { + readonly createdAt: number; + readonly forkedFromId?: string | null; + readonly id: string; + readonly updatedAt: number; + }; + readonly forkedFrom?: OrchestrationV2ProviderThread["forkedFrom"]; +}): OrchestrationV2ProviderThread { + return { + id: input.idAllocator.derive.providerThread({ + driver: CODEX_PROVIDER, + nativeThreadId: input.thread.id, + }), + driver: CODEX_PROVIDER, + providerInstanceId: input.providerInstanceId, + providerSessionId: input.providerSessionId, + appThreadId: input.appThreadId, + ownerNodeId: input.ownerNodeId, + nativeThreadRef: { + driver: CODEX_PROVIDER, + nativeId: input.thread.id, + strength: "strong" as const, + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: input.forkedFrom ?? null, + createdAt: codexTimestamp(input.thread.createdAt), + updatedAt: codexTimestamp(input.thread.updatedAt), + }; +} + +const isTerminalProviderTurn = (turn: OrchestrationV2ProviderTurn): boolean => + turn.status === "completed" || + turn.status === "interrupted" || + turn.status === "failed" || + turn.status === "cancelled"; + +const providerTurnsForThread = ( + providerTurns: ReadonlyArray, + providerThread: OrchestrationV2ProviderThread, +): ReadonlyArray => + providerTurns.filter((turn) => turn.providerThreadId === providerThread.id); + +const countTerminalTurnsAfterBoundary = ( + providerTurns: ReadonlyArray, + providerTurnId: ProviderTurnId, +): number | null => { + const boundaryTurn = providerTurns.find((turn) => turn.id === providerTurnId); + if (boundaryTurn === undefined) { + return null; + } + + return providerTurns.filter( + (turn) => turn.ordinal > boundaryTurn.ordinal && isTerminalProviderTurn(turn), + ).length; +}; + +const resolveCodexForkRollbackTurnCount = Effect.fn("CodexAdapterV2.resolveForkRollbackTurnCount")( + function* (input: ProviderAdapterV2ForkThreadInput) { + if (input.providerTurnId === undefined || input.sourceProviderTurns === undefined) { + return 0; + } + + const rollbackTurnCount = countTerminalTurnsAfterBoundary( + providerTurnsForThread(input.sourceProviderTurns, input.sourceProviderThread), + input.providerTurnId, + ); + if (rollbackTurnCount === null) { + return yield* new ProviderAdapterForkThreadError({ + driver: CODEX_PROVIDER, + providerThreadId: input.sourceProviderThread.id, + cause: `Cannot fork Codex thread from provider turn ${input.providerTurnId}: source turn was not found in provider thread ${input.sourceProviderThread.id}.`, + }); + } + + return rollbackTurnCount; + }, +); + +export const resolveCodexRollbackTurnCount = Effect.fn("CodexAdapterV2.resolveRollbackTurnCount")( + function* (input: ProviderAdapterV2RollbackThreadInput) { + const providerTurns = input.providerThreadTurns; + switch (input.target.type) { + case "thread_start": + return providerTurns.filter(isTerminalProviderTurn).length; + case "provider_turn": { + if (input.target.providerTurn.providerThreadId !== input.providerThread.id) { + return yield* new ProviderAdapterRollbackThreadError({ + driver: CODEX_PROVIDER, + providerThreadId: input.providerThread.id, + cause: `Cannot roll back Codex thread ${input.providerThread.id} to provider turn ${input.target.providerTurn.id}: target turn belongs to provider thread ${input.target.providerTurn.providerThreadId}.`, + }); + } + + const rollbackTurnCount = countTerminalTurnsAfterBoundary( + providerTurns, + input.target.providerTurn.id, + ); + if (rollbackTurnCount === null) { + return yield* new ProviderAdapterRollbackThreadError({ + driver: CODEX_PROVIDER, + providerThreadId: input.providerThread.id, + cause: `Cannot roll back Codex thread ${input.providerThread.id} to provider turn ${input.target.providerTurn.id}: target turn was not found in durable provider turn history.`, + }); + } + + return rollbackTurnCount; + } + } + }, +); + +interface ActiveCodexTurnContext { + readonly input: ProviderAdapterV2TurnInput; + readonly projectionThreadId: ThreadId; + readonly projectionRunId: ProviderAdapterV2TurnInput["runId"] | null; + readonly nativeTurnId: string; + readonly providerThread: OrchestrationV2ProviderThread; + readonly providerTurnId: ProviderTurnId; + readonly providerTurnOrdinal: number; + readonly providerNodeId: OrchestrationV2ExecutionNode["id"]; + readonly providerNodeKind: OrchestrationV2ExecutionNode["kind"]; + readonly providerNodeStartedAt: DateTime.Utc | null; + readonly itemParentNodeId: OrchestrationV2ExecutionNode["id"]; + readonly rootNodeId: OrchestrationV2ExecutionNode["id"]; + readonly subagent: CodexSubagentThreadContext | null; + readonly startedAt: DateTime.Utc; +} + +interface CodexSubagentThreadContext { + readonly parentContext: ActiveCodexTurnContext; + readonly providerThread: OrchestrationV2ProviderThread; + readonly subagentNodeId: OrchestrationV2ExecutionNode["id"]; + readonly childRootNodeId: OrchestrationV2ExecutionNode["id"]; + readonly childThreadId: ThreadId; + readonly nativeToolCallId: string; + readonly ordinal: number; + readonly startedAt: DateTime.Utc; + readonly turnItemId: OrchestrationV2TurnItem["id"]; + readonly turnItemOrdinal: number; + task: OrchestrationV2Subagent; +} + +interface PendingCodexSubagentTurnStarted { + readonly nativeTurnId: string; + readonly startedAt: DateTime.Utc; +} + +type PendingCodexRuntimeRequest = + | { + readonly type: "approval"; + readonly requestId: RuntimeRequestId; + readonly requestKind: ProviderRequestKind; + readonly decision: Deferred.Deferred; + } + | { + readonly type: "user_input"; + readonly requestId: RuntimeRequestId; + readonly answers: Deferred.Deferred; + }; + +type CodexWebSearchItem = { + readonly id: string; + readonly type: "webSearch"; + readonly query?: string | null; + readonly action?: + | CodexSchema.V2ItemStartedNotification__WebSearchAction + | CodexSchema.V2ItemCompletedNotification__WebSearchAction + | null; +}; + +export type CodexDynamicToolItem = Extract< + | CodexSchema.V2ItemStartedNotification__ThreadItem + | CodexSchema.V2ItemCompletedNotification__ThreadItem, + { readonly type: "mcpToolCall" | "dynamicToolCall" } +>; + +export interface CodexAppServerClientFactoryShape { + readonly open: (input: { + readonly instanceId: ProviderInstanceId; + readonly threadId: ThreadId; + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; + readonly settings: CodexSettings; + readonly environment: NodeJS.ProcessEnv; + }) => Effect.Effect< + CodexClient.CodexAppServerClient["Service"], + ProviderAdapterOpenSessionError, + Scope.Scope + >; +} + +export class CodexAppServerClientFactory extends Context.Service< + CodexAppServerClientFactory, + CodexAppServerClientFactoryShape +>()("t3/orchestration-v2/Adapters/CodexAdapterV2/CodexAppServerClientFactory") {} + +export function codexThreadRuntimeParams(input: { + readonly threadId: ThreadId | null; + readonly modelSelection?: { readonly model: string }; + readonly runtimePolicy?: ProviderAdapterV2RuntimePolicy; +}): { + readonly cwd?: string; + readonly model?: string; + readonly config?: Readonly>; +} { + const mcpSession = + input.threadId === null ? undefined : McpProviderSession.readMcpProviderSession(input.threadId); + return { + ...(input.runtimePolicy?.cwd == null ? {} : { cwd: input.runtimePolicy.cwd }), + ...(input.modelSelection === undefined ? {} : { model: input.modelSelection.model }), + ...(mcpSession === undefined + ? {} + : { + config: { + mcp_servers: { + "t3-code": { + url: mcpSession.endpoint, + http_headers: { + Authorization: mcpSession.authorizationHeader, + }, + }, + }, + }, + }), + }; +} + +export const makeCodexAppServerSpawnCommand = Effect.fn( + "CodexAdapterV2.makeCodexAppServerSpawnCommand", +)(function* (input: { + readonly command: string; + readonly args: ReadonlyArray; + readonly cwd?: string | undefined; + readonly env?: NodeJS.ProcessEnv | undefined; + readonly extendEnv?: boolean | undefined; +}) { + const spawnCommand = yield* resolveSpawnCommand(input.command, input.args, { + ...(input.env === undefined ? {} : { env: input.env }), + ...(input.extendEnv === undefined ? {} : { extendEnv: input.extendEnv }), + }); + return ChildProcess.make(spawnCommand.command, spawnCommand.args, { + ...(input.cwd === undefined ? {} : { cwd: input.cwd }), + ...(input.env === undefined ? {} : { env: input.env }), + ...(input.extendEnv === undefined ? {} : { extendEnv: input.extendEnv }), + shell: spawnCommand.shell, + }); +}); + +export const makeCodexAppServerClientFactoryCommandLayer = ( + options: CodexClient.CodexAppServerClientOptions & { + readonly command: string; + readonly args?: ReadonlyArray; + readonly cwd?: string; + readonly env?: NodeJS.ProcessEnv; + }, +): Layer.Layer => + Layer.effect( + CodexAppServerClientFactory, + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + return CodexAppServerClientFactory.of({ + open: (input) => + Effect.gen(function* () { + const scope = yield* Scope.Scope; + const command = yield* makeCodexAppServerSpawnCommand({ + command: options.command, + args: [...(options.args ?? [])], + ...(options.cwd === undefined ? {} : { cwd: options.cwd }), + ...(options.env === undefined ? {} : { env: options.env, extendEnv: true }), + }); + const handle = yield* spawner.spawn(command).pipe( + Effect.provideService(Scope.Scope, scope), + Effect.mapError( + (cause) => + new ProviderAdapterOpenSessionError({ + driver: CODEX_PROVIDER, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ); + const context = yield* Layer.build(CodexClient.layerChildProcess(handle, options)); + return yield* Effect.service(CodexClient.CodexAppServerClient).pipe( + Effect.provide(context), + ); + }), + }); + }), + ); + +export function makeCodexAppServerProtocolLogger(input: { + readonly nativeEventLogger: EventNdjsonLogger | undefined; + readonly threadId: ThreadId; + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; +}): CodexClient.CodexAppServerClientOptions["logger"] | undefined { + const { nativeEventLogger } = input; + if (nativeEventLogger === undefined) { + return undefined; + } + + return (event) => + nativeEventLogger + .write( + { + provider: CODEX_PROVIDER, + protocol: "codex.app-server", + kind: "protocol", + providerSessionId: input.providerSessionId, + event: redactCodexProtocolValue(event), + }, + input.threadId, + ) + .pipe(Effect.ignore); +} + +export function redactCodexProtocolValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(redactCodexProtocolValue); + } + if (value === null || typeof value !== "object") { + return typeof value === "string" && /^Bearer\s+/i.test(value) ? "[REDACTED]" : value; + } + return Object.fromEntries( + Object.entries(value).map(([key, nested]) => [ + key, + isSensitiveCodexProtocolKey(key) ? "[REDACTED]" : redactCodexProtocolValue(nested), + ]), + ); +} + +function isSensitiveCodexProtocolKey(key: string): boolean { + const normalized = key.replace(/[^a-z0-9]/gi, "").toLowerCase(); + return ( + normalized.endsWith("authorization") || + normalized.endsWith("apikey") || + normalized.endsWith("token") || + normalized.endsWith("password") || + normalized.endsWith("secret") + ); +} + +export const codexAppServerClientFactoryFromSettingsLayer: Layer.Layer< + CodexAppServerClientFactory, + never, + ChildProcessSpawner.ChildProcessSpawner | ServerConfig +> = Layer.effect( + CodexAppServerClientFactory, + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const { providerEventLogPath } = yield* ServerConfig; + const nativeEventLogger = yield* makeEventNdjsonLogger(providerEventLogPath, { + stream: "native", + }); + + return CodexAppServerClientFactory.of({ + open: (input) => + Effect.gen(function* () { + const scope = yield* Scope.Scope; + const environment = { + ...input.environment, + ...(input.settings.homePath ? { CODEX_HOME: input.settings.homePath } : {}), + }; + const command = yield* makeCodexAppServerSpawnCommand({ + command: input.settings.binaryPath || "codex", + args: ["app-server"], + env: environment, + }); + const handle = yield* spawner.spawn(command).pipe( + Effect.provideService(Scope.Scope, scope), + Effect.mapError( + (cause) => + new ProviderAdapterOpenSessionError({ + driver: CODEX_PROVIDER, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ); + const protocolLogger = makeCodexAppServerProtocolLogger({ + nativeEventLogger, + threadId: input.threadId, + providerSessionId: input.providerSessionId, + }); + const clientOptions: CodexClient.CodexAppServerClientOptions = + protocolLogger === undefined + ? {} + : { + logIncoming: true, + logOutgoing: true, + logger: protocolLogger, + }; + const context = yield* Layer.build(CodexClient.layerChildProcess(handle, clientOptions)); + return yield* Effect.service(CodexClient.CodexAppServerClient).pipe( + Effect.provide(context), + ); + }), + }); + }), +); + +export type CodexAdapterV2DriverEnv = + | CodexAppServerClientFactory + | FileSystem.FileSystem + | IdAllocatorV2 + | Path.Path + | ServerConfig; + +export const CodexAdapterV2Driver: ProviderAdapterDriver = { + driverKind: CODEX_DRIVER_KIND, + configSchema: CodexSettings, + defaultConfig: (): CodexSettings => DEFAULT_CODEX_SETTINGS, + create: ({ instanceId, environment, enabled, config }) => + Effect.gen(function* () { + const clientFactory = yield* CodexAppServerClientFactory; + const fileSystem = yield* FileSystem.FileSystem; + const hostEnvironment = yield* HostProcessEnvironment; + const idAllocator = yield* IdAllocatorV2; + const serverConfig = yield* ServerConfig; + const homeLayout = yield* resolveCodexHomeLayout(config); + + yield* materializeCodexShadowHome(homeLayout).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterDriverCreateError({ + driver: CODEX_DRIVER_KIND, + instanceId, + detail: cause.message, + cause, + }), + ), + ); + + const settings = { + ...config, + enabled, + homePath: homeLayout.effectiveHomePath ?? "", + } satisfies CodexSettings; + + return makeCodexAdapterV2({ + instanceId, + settings, + environment: mergeProviderInstanceEnvironment(environment, hostEnvironment), + clientFactory, + fileSystem, + idAllocator, + serverConfig, + }); + }), +}; + +export const layer: Layer.Layer< + ProviderAdapterV2, + never, + CodexAppServerClientFactory | FileSystem.FileSystem | IdAllocatorV2 | ServerConfig +> = Layer.effect( + ProviderAdapterV2, + Effect.gen(function* () { + const clientFactory = yield* CodexAppServerClientFactory; + const fileSystem = yield* FileSystem.FileSystem; + const hostEnvironment = yield* HostProcessEnvironment; + const idAllocator = yield* IdAllocatorV2; + const serverConfig = yield* ServerConfig; + + return makeCodexAdapterV2({ + instanceId: CODEX_DEFAULT_INSTANCE_ID, + settings: DEFAULT_CODEX_SETTINGS, + environment: hostEnvironment, + clientFactory, + fileSystem, + idAllocator, + serverConfig, + }); + }), +); + +export interface CodexAdapterV2Options { + readonly instanceId: ProviderInstanceId; + readonly settings: CodexSettings; + readonly environment: NodeJS.ProcessEnv; + readonly clientFactory: CodexAppServerClientFactoryShape; + readonly fileSystem: FileSystem.FileSystem; + readonly idAllocator: IdAllocatorV2Shape; + readonly serverConfig: ServerConfig["Service"]; +} + +export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): ProviderAdapterV2Shape { + const { clientFactory, fileSystem, idAllocator, serverConfig } = adapterOptions; + + return ProviderAdapterV2.of({ + instanceId: adapterOptions.instanceId, + driver: CODEX_PROVIDER, + getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2), + openSession: (input) => + Effect.gen(function* () { + const client = yield* clientFactory.open({ + instanceId: adapterOptions.instanceId, + threadId: input.threadId, + providerSessionId: input.providerSessionId, + runtimePolicy: input.runtimePolicy, + settings: adapterOptions.settings, + environment: adapterOptions.environment, + }); + const initialized = yield* Ref.make(false); + const ensureInitialized = Effect.gen(function* () { + const alreadyInitialized = yield* Ref.get(initialized); + if (alreadyInitialized) { + return; + } + + yield* client.request("initialize", { + clientInfo: CODEX_CLIENT_INFO, + capabilities: CODEX_CLIENT_CAPABILITIES, + }); + yield* client.notify("initialized", undefined); + yield* Ref.set(initialized, true); + }); + const now = yield* DateTime.now; + const session = providerSession({ + providerSessionId: input.providerSessionId, + providerInstanceId: adapterOptions.instanceId, + cwd: input.runtimePolicy.cwd, + model: input.modelSelection.model, + now, + }); + const events = yield* Queue.unbounded(); + const activeTurns = yield* Ref.make(new Map()); + const pendingRootTurns = yield* Ref.make(new Map()); + const turnWaiters = yield* Ref.make(new Map>()); + const subagentThreads = yield* Ref.make(new Map()); + const pendingSubagentTurns = yield* Ref.make( + new Map>(), + ); + const nextProviderTurnOrdinals = yield* Ref.make(new Map()); + const itemOrdinals = yield* Ref.make(new Map()); + const nextItemOrdinalsByTurn = yield* Ref.make(new Map()); + const agentMessageDeltas = yield* Ref.make(new Map()); + const planDeltas = yield* Ref.make(new Map()); + const planIds = yield* Ref.make(new Map()); + const pendingRuntimeRequests = yield* Ref.make( + new Map(), + ); + + const emitProviderEvent = (event: ProviderAdapterV2Event) => + Queue.offer(events, event).pipe(Effect.asVoid); + + const registerRootTurn = (input: { + readonly turnInput: ProviderAdapterV2TurnInput; + readonly nativeTurnId: string; + readonly startedAt: DateTime.Utc; + }) => + Effect.gen(function* () { + const existing = (yield* Ref.get(activeTurns)).get(input.nativeTurnId); + if (existing !== undefined) { + return existing; + } + const providerTurnId = idAllocator.derive.providerTurn({ + driver: CODEX_PROVIDER, + nativeTurnId: input.nativeTurnId, + }); + const context: ActiveCodexTurnContext = { + input: input.turnInput, + projectionThreadId: input.turnInput.threadId, + projectionRunId: input.turnInput.runId, + nativeTurnId: input.nativeTurnId, + providerThread: input.turnInput.providerThread, + providerTurnId, + providerTurnOrdinal: input.turnInput.providerTurnOrdinal, + providerNodeId: input.turnInput.rootNodeId, + providerNodeKind: "root_turn", + providerNodeStartedAt: input.startedAt, + itemParentNodeId: input.turnInput.rootNodeId, + rootNodeId: input.turnInput.rootNodeId, + subagent: null, + startedAt: input.startedAt, + }; + yield* Ref.update(activeTurns, (current) => { + const updated = new Map(current); + updated.set(input.nativeTurnId, context); + return updated; + }); + yield* emitProviderEvent({ + type: "provider_turn.updated", + driver: CODEX_PROVIDER, + threadId: input.turnInput.threadId, + providerTurn: { + id: providerTurnId, + providerThreadId: input.turnInput.providerThread.id, + nodeId: input.turnInput.rootNodeId, + runAttemptId: input.turnInput.attemptId, + nativeTurnRef: { + driver: CODEX_PROVIDER, + nativeId: input.nativeTurnId, + strength: "strong", + }, + ordinal: input.turnInput.providerTurnOrdinal, + status: "running", + startedAt: input.startedAt, + completedAt: null, + }, + }); + return context; + }); + + const findActiveTurnByNativeThreadId = (nativeThreadId: string) => + Effect.gen(function* () { + const turns = Array.from((yield* Ref.get(activeTurns)).values()); + return turns.find( + (context) => context.providerThread.nativeThreadRef?.nativeId === nativeThreadId, + ); + }); + + const awaitActiveTurn = ( + nativeTurnId: string, + attemptsRemaining = 1_000, + ): Effect.Effect => + Effect.gen(function* () { + const context = (yield* Ref.get(activeTurns)).get(nativeTurnId); + if (context !== undefined || attemptsRemaining <= 0) { + return context; + } + yield* Effect.yieldNow; + return yield* awaitActiveTurn(nativeTurnId, attemptsRemaining - 1); + }); + + const resolveItemOrdinal = (context: ActiveCodexTurnContext, nativeItemId: string) => + Effect.gen(function* () { + const existing = (yield* Ref.get(itemOrdinals)).get(nativeItemId); + if (existing !== undefined) { + return existing; + } + + const turnKey = context.nativeTurnId; + const nextWithinTurn = yield* Ref.modify(nextItemOrdinalsByTurn, (current) => { + const next = (current.get(turnKey) ?? 0) + 1; + const updated = new Map(current); + updated.set(turnKey, next); + return [next, updated]; + }); + const nextOrdinal = context.providerTurnOrdinal * 100 + nextWithinTurn; + yield* Ref.update(itemOrdinals, (current) => { + const updated = new Map(current); + updated.set(nativeItemId, nextOrdinal); + return updated; + }); + return nextOrdinal; + }); + + const nextProviderTurnOrdinal = ( + providerThreadId: OrchestrationV2ProviderThread["id"], + minimum: number, + ) => + Ref.modify(nextProviderTurnOrdinals, (current) => { + const previous = current.get(String(providerThreadId)); + const next = previous === undefined ? minimum : Math.max(previous + 1, minimum); + const updated = new Map(current); + updated.set(String(providerThreadId), next); + return [next, updated]; + }); + + const emitSubagentTaskUpdate = (input: { + readonly subagent: CodexSubagentThreadContext; + readonly status: OrchestrationV2Subagent["status"]; + readonly result?: string | null; + readonly completedAt?: DateTime.Utc | null; + }) => + Effect.gen(function* () { + const now = yield* DateTime.now; + const terminal = + input.status === "completed" || + input.status === "failed" || + input.status === "cancelled" || + input.status === "interrupted"; + const completedAt = terminal ? (input.completedAt ?? now) : null; + const task = { + ...input.subagent.task, + status: input.status, + result: input.result === undefined ? input.subagent.task.result : input.result, + completedAt, + updatedAt: now, + } satisfies OrchestrationV2Subagent; + input.subagent.task = task; + + yield* emitProviderEvent({ + type: "subagent.updated", + driver: CODEX_PROVIDER, + subagent: task, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: { + id: input.subagent.turnItemId, + threadId: task.threadId, + runId: task.runId, + nodeId: task.id, + providerThreadId: task.providerThreadId, + providerTurnId: input.subagent.parentContext.providerTurnId, + nativeItemRef: task.nativeTaskRef, + parentItemId: null, + ordinal: input.subagent.turnItemOrdinal, + status: task.status, + title: task.title, + startedAt: task.startedAt, + completedAt: task.completedAt, + updatedAt: task.updatedAt, + type: "subagent", + subagentId: task.id, + origin: task.origin, + driver: task.driver, + providerInstanceId: task.providerInstanceId, + childThreadId: task.childThreadId, + prompt: task.prompt, + result: task.result, + }, + }); + }); + + const emitSubagentProviderTurnStarted = ( + subagent: CodexSubagentThreadContext, + turn: PendingCodexSubagentTurnStarted, + ) => + Effect.gen(function* () { + const providerTurnId = idAllocator.derive.providerTurn({ + driver: CODEX_PROVIDER, + nativeTurnId: turn.nativeTurnId, + }); + const providerTurnOrdinal = yield* nextProviderTurnOrdinal( + subagent.providerThread.id, + 1, + ); + const providerNodeId = + providerTurnOrdinal === 1 + ? subagent.childRootNodeId + : idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: `${turn.nativeTurnId}:thread-root`, + }); + const activeContext: ActiveCodexTurnContext = { + input: subagent.parentContext.input, + projectionThreadId: subagent.childThreadId, + projectionRunId: null, + nativeTurnId: turn.nativeTurnId, + providerThread: subagent.providerThread, + providerTurnId, + providerTurnOrdinal, + providerNodeId, + providerNodeKind: "root_turn", + providerNodeStartedAt: turn.startedAt, + itemParentNodeId: providerNodeId, + rootNodeId: providerNodeId, + subagent, + startedAt: turn.startedAt, + }; + yield* Ref.update(activeTurns, (current) => { + const updated = new Map(current); + updated.set(turn.nativeTurnId, activeContext); + return updated; + }); + const now = yield* DateTime.now; + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver: CODEX_PROVIDER, + providerThread: { + ...subagent.providerThread, + status: "active", + updatedAt: now, + }, + }); + yield* emitProviderEvent({ + type: "provider_turn.updated", + driver: CODEX_PROVIDER, + threadId: subagent.childThreadId, + providerTurn: { + id: providerTurnId, + providerThreadId: subagent.providerThread.id, + nodeId: providerNodeId, + runAttemptId: null, + nativeTurnRef: { + driver: CODEX_PROVIDER, + nativeId: turn.nativeTurnId, + strength: "strong", + }, + ordinal: activeContext.providerTurnOrdinal, + status: "running", + startedAt: turn.startedAt, + completedAt: null, + }, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: { + id: providerNodeId, + threadId: subagent.childThreadId, + runId: null, + parentNodeId: null, + rootNodeId: providerNodeId, + kind: "root_turn", + status: "running", + countsForRun: false, + providerThreadId: subagent.providerThread.id, + providerTurnId, + nativeItemRef: subagent.task.nativeTaskRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: turn.startedAt, + completedAt: null, + }, + }); + }); + + const rememberSubagentTurnStarted = (input: { + readonly nativeThreadId: string; + readonly nativeTurnId: string; + readonly startedAt: DateTime.Utc; + }) => + Effect.gen(function* () { + const subagent = (yield* Ref.get(subagentThreads)).get(input.nativeThreadId); + if (subagent !== undefined) { + yield* emitSubagentProviderTurnStarted(subagent, input); + return; + } + yield* Ref.update(pendingSubagentTurns, (current) => { + const updated = new Map(current); + updated.set(input.nativeThreadId, [ + ...(updated.get(input.nativeThreadId) ?? []), + { nativeTurnId: input.nativeTurnId, startedAt: input.startedAt }, + ]); + return updated; + }); + }); + + const registerSubagentThreads = (input: { + readonly context: ActiveCodexTurnContext; + readonly item: Extract< + CodexSchema.V2ItemCompletedNotification__ThreadItem, + { type: "collabAgentToolCall" } + >; + }) => + Effect.gen(function* () { + if (input.item.tool !== "spawnAgent" || input.item.receiverThreadIds.length === 0) { + return; + } + + const now = yield* DateTime.now; + for (const [index, nativeThreadId] of input.item.receiverThreadIds.entries()) { + const registeredSubagents = yield* Ref.get(subagentThreads); + if (registeredSubagents.has(nativeThreadId)) { + continue; + } + + const nativeItemId = `${input.item.id}:${nativeThreadId}`; + const subagentNodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId, + }); + const childRootNodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: `${nativeItemId}:thread-root`, + }); + const childThreadId = idAllocator.derive.threadFromProviderThread({ + driver: CODEX_PROVIDER, + nativeThreadId, + }); + const turnItemOrdinal = yield* resolveItemOrdinal(input.context, nativeItemId); + const providerThread = { + id: idAllocator.derive.providerThread({ + driver: CODEX_PROVIDER, + nativeThreadId, + }), + driver: CODEX_PROVIDER, + providerInstanceId: input.context.input.modelSelection.instanceId, + providerSessionId: input.context.providerThread.providerSessionId, + appThreadId: childThreadId, + ownerNodeId: null, + nativeThreadRef: { + driver: CODEX_PROVIDER, + nativeId: nativeThreadId, + strength: "strong" as const, + }, + nativeConversationHeadRef: null, + status: "idle" as const, + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: { + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + }, + createdAt: now, + updatedAt: now, + } satisfies OrchestrationV2ProviderThread; + const task = { + id: subagentNodeId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + parentNodeId: input.context.rootNodeId, + origin: "provider_native", + createdBy: "agent", + driver: CODEX_PROVIDER, + providerInstanceId: input.context.input.modelSelection.instanceId, + providerThreadId: providerThread.id, + childThreadId, + nativeTaskRef: codexNativeItemRef(nativeItemId), + prompt: input.item.prompt ?? "", + title: null, + model: + typeof input.item.model === "string" && input.item.model.length > 0 + ? input.item.model + : null, + status: "running", + result: null, + startedAt: now, + completedAt: null, + updatedAt: now, + } satisfies OrchestrationV2Subagent; + const subagent = { + parentContext: input.context, + providerThread, + subagentNodeId, + childRootNodeId, + childThreadId, + nativeToolCallId: input.item.id, + ordinal: index + 1, + startedAt: now, + turnItemId: idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId, + }), + turnItemOrdinal, + task, + } satisfies CodexSubagentThreadContext; + + yield* Ref.update(subagentThreads, (current) => { + const updated = new Map(current); + updated.set(nativeThreadId, subagent); + return updated; + }); + const childThread = makeSubagentChildThread({ + parentThread: input.context.input.appThread, + childThreadId, + parentNodeId: subagentNodeId, + activeProviderThreadId: providerThread.id, + providerInstanceId: input.context.input.modelSelection.instanceId, + modelSelection: { + ...input.context.input.modelSelection, + model: task.model ?? input.context.input.modelSelection.model, + }, + title: subagentThreadTitle({ + parentTitle: input.context.input.appThread.title, + prompt: task.prompt, + title: task.title, + ordinal: index + 1, + }), + now, + createdBy: "agent", + creationSource: "provider", + }); + const promptNativeItemId = `${nativeItemId}:prompt`; + const promptArtifacts = makeSubagentConversationArtifacts({ + messageId: idAllocator.derive.messageFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: promptNativeItemId, + }), + turnItemId: idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: promptNativeItemId, + }), + threadId: childThreadId, + rootNodeId: childRootNodeId, + providerThreadId: providerThread.id, + providerTurnId: null, + nativeItemRef: codexNativeItemRef(promptNativeItemId), + role: "user", + text: task.prompt, + ordinal: 100, + now, + }); + yield* emitProviderEvent({ + type: "app_thread.created", + driver: CODEX_PROVIDER, + appThread: childThread, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: { + id: subagentNodeId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + parentNodeId: input.context.rootNodeId, + rootNodeId: input.context.rootNodeId, + kind: "subagent", + status: "running", + countsForRun: false, + providerThreadId: providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.item.id), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: now, + completedAt: null, + }, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: { + id: childRootNodeId, + threadId: childThreadId, + runId: null, + parentNodeId: null, + rootNodeId: childRootNodeId, + kind: "root_turn", + status: "running", + countsForRun: false, + providerThreadId: providerThread.id, + providerTurnId: null, + nativeItemRef: codexNativeItemRef(nativeItemId), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: now, + completedAt: null, + }, + }); + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver: CODEX_PROVIDER, + providerThread, + }); + yield* emitProviderEvent({ + type: "message.updated", + driver: CODEX_PROVIDER, + message: promptArtifacts.message, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: promptArtifacts.turnItem, + }); + yield* emitSubagentTaskUpdate({ + subagent, + status: "running", + }); + + const pendingTurns = yield* Ref.modify(pendingSubagentTurns, (current) => { + const pending = current.get(nativeThreadId) ?? []; + const updated = new Map(current); + updated.delete(nativeThreadId); + return [pending, updated]; + }); + for (const pendingTurn of pendingTurns) { + yield* emitSubagentProviderTurnStarted(subagent, pendingTurn); + } + } + }); + + const updateSubagentStates = (input: { + readonly item: Extract< + CodexSchema.V2ItemCompletedNotification__ThreadItem, + { type: "collabAgentToolCall" } + >; + }) => + Effect.gen(function* () { + if (input.item.tool !== "spawnAgent") { + return; + } + + const subagents = yield* Ref.get(subagentThreads); + for (const [nativeThreadId, state] of Object.entries(input.item.agentsStates)) { + const subagent = subagents.get(nativeThreadId); + if (subagent === undefined) { + continue; + } + const nativeStatus = String(state.status); + const status: OrchestrationV2Subagent["status"] = + nativeStatus === "completed" + ? "completed" + : nativeStatus === "failed" || nativeStatus === "errored" + ? "failed" + : nativeStatus === "cancelled" || nativeStatus === "closed" + ? "cancelled" + : "running"; + yield* emitSubagentTaskUpdate({ + subagent, + status, + ...(state.message === null ? {} : { result: state.message }), + }); + } + }); + + const resolvePlanId = (context: ActiveCodexTurnContext, planKey: string) => + Effect.gen(function* () { + const existing = (yield* Ref.get(planIds)).get(planKey); + if (existing !== undefined) { + return existing; + } + const planId = yield* idAllocator.allocate.plan({ + threadId: context.projectionThreadId, + ...(context.projectionRunId === null ? {} : { runId: context.projectionRunId }), + driver: CODEX_PROVIDER, + }); + yield* Ref.update(planIds, (current) => { + const updated = new Map(current); + updated.set(planKey, planId); + return updated; + }); + return planId; + }); + + const resolveCodexAttachment = (attachment: ChatAttachment) => + Effect.gen(function* () { + const attachmentPath = resolveAttachmentPath({ + attachmentsDir: serverConfig.attachmentsDir, + attachment, + }); + if (attachmentPath === null) { + return yield* toProtocolError(`Invalid attachment id '${attachment.id}'`); + } + const bytes = yield* fileSystem + .readFile(attachmentPath) + .pipe( + Effect.mapError((cause) => + toProtocolError(`Failed to read attachment '${attachment.id}'.`, cause), + ), + ); + return { + type: "image" as const, + url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`, + } satisfies CodexSchema.V2TurnStartParams__UserInput; + }); + + const toCodexInput = ( + turnInput: Pick, + ) => + Effect.gen(function* () { + const inputItems: Array = []; + if (turnInput.message.text.length > 0) { + inputItems.push({ + type: "text", + text: turnInput.message.text, + }); + } + const attachmentItems = yield* Effect.forEach( + turnInput.message.attachments, + resolveCodexAttachment, + { concurrency: 1 }, + ); + inputItems.push(...attachmentItems); + if (inputItems.length === 0) { + return yield* toProtocolError("Turn requires non-empty text or attachments."); + } + return inputItems; + }); + + const buildAgentMessageArtifacts = ( + context: ActiveCodexTurnContext, + item: Extract< + CodexSchema.V2ItemCompletedNotification__ThreadItem, + { type: "agentMessage" } + >, + ) => + Effect.gen(function* () { + const completedAt = yield* DateTime.now; + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }); + const ordinal = yield* resolveItemOrdinal(context, item.id); + const messageId = idAllocator.derive.messageFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }); + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: context.projectionThreadId, + runId: context.projectionRunId, + parentNodeId: context.itemParentNodeId, + rootNodeId: context.rootNodeId, + kind: "assistant_message", + status: "completed", + countsForRun: false, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: codexNativeItemRef(item.id), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: context.startedAt, + completedAt, + }; + const message: OrchestrationV2ConversationMessage = { + createdBy: "agent", + creationSource: "provider", + id: messageId, + threadId: context.projectionThreadId, + runId: context.projectionRunId, + nodeId, + role: "assistant", + text: item.text, + attachments: [], + streaming: false, + createdAt: completedAt, + updatedAt: completedAt, + }; + const turnItem: OrchestrationV2TurnItem = { + id: turnItemId, + threadId: context.projectionThreadId, + runId: context.projectionRunId, + nodeId, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: codexNativeItemRef(item.id), + parentItemId: null, + ordinal, + status: "completed", + title: null, + startedAt: context.startedAt, + completedAt, + updatedAt: completedAt, + type: "assistant_message", + messageId, + text: item.text, + streaming: false, + }; + return { node, message, turnItem }; + }); + + const emitSubagentUserMessage = ( + context: ActiveCodexTurnContext, + item: Extract< + | CodexSchema.V2ItemStartedNotification__ThreadItem + | CodexSchema.V2ItemCompletedNotification__ThreadItem, + { type: "userMessage" } + >, + ) => + Effect.gen(function* () { + if (context.subagent === null || context.providerTurnOrdinal === 1) { + return false; + } + const text = codexUserMessageText(item.content); + if (text.length === 0) { + return false; + } + const now = yield* DateTime.now; + const ordinal = yield* resolveItemOrdinal(context, item.id); + const artifacts = makeSubagentConversationArtifacts({ + messageId: idAllocator.derive.messageFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }), + turnItemId: idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }), + threadId: context.projectionThreadId, + rootNodeId: context.rootNodeId, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: codexNativeItemRef(item.id), + role: "user", + text, + ordinal, + now, + }); + yield* emitProviderEvent({ + type: "message.updated", + driver: CODEX_PROVIDER, + message: artifacts.message, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + return true; + }); + + const buildCommandExecutionArtifacts = ( + context: ActiveCodexTurnContext, + item: Extract< + | CodexSchema.V2ItemStartedNotification__ThreadItem + | CodexSchema.V2ItemCompletedNotification__ThreadItem, + { type: "commandExecution" } + >, + ) => + Effect.gen(function* () { + const updatedAt = yield* DateTime.now; + const status = codexItemStatus(item.status); + const completedAt = status.completed ? updatedAt : null; + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }); + const ordinal = yield* resolveItemOrdinal(context, item.id); + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: context.projectionThreadId, + runId: context.projectionRunId, + parentNodeId: context.itemParentNodeId, + rootNodeId: context.rootNodeId, + kind: "tool_call", + status: status.node, + countsForRun: false, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: codexNativeItemRef(item.id), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: context.startedAt, + completedAt, + }; + const turnItem: OrchestrationV2TurnItem = { + id: turnItemId, + threadId: context.projectionThreadId, + runId: context.projectionRunId, + nodeId, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: codexNativeItemRef(item.id), + parentItemId: null, + ordinal, + status: status.turnItem, + title: null, + startedAt: context.startedAt, + completedAt, + updatedAt, + type: "command_execution", + input: item.command, + ...(item.aggregatedOutput === null || item.aggregatedOutput === undefined + ? {} + : { output: item.aggregatedOutput }), + ...(item.exitCode === null || item.exitCode === undefined + ? {} + : { exitCode: item.exitCode }), + }; + return { node, turnItem }; + }); + + const buildFileChangeArtifacts = ( + context: ActiveCodexTurnContext, + item: Extract< + CodexSchema.V2ItemCompletedNotification__ThreadItem, + { type: "fileChange" } + >, + ) => + Effect.gen(function* () { + const firstChange = item.changes[0]; + if (firstChange === undefined) { + return null; + } + + const updatedAt = yield* DateTime.now; + const status = codexItemStatus(item.status); + const completedAt = status.completed ? updatedAt : null; + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }); + const ordinal = yield* resolveItemOrdinal(context, item.id); + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: context.projectionThreadId, + runId: context.projectionRunId, + parentNodeId: context.itemParentNodeId, + rootNodeId: context.rootNodeId, + kind: "tool_call", + status: status.node, + countsForRun: false, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: codexNativeItemRef(item.id), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: context.startedAt, + completedAt, + }; + const turnItem: OrchestrationV2TurnItem = { + id: turnItemId, + threadId: context.projectionThreadId, + runId: context.projectionRunId, + nodeId, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: codexNativeItemRef(item.id), + parentItemId: null, + ordinal, + status: status.turnItem, + title: null, + startedAt: context.startedAt, + completedAt, + updatedAt, + type: "file_change", + fileName: firstChange.path, + diffStr: firstChange.diff, + }; + return { node, turnItem }; + }); + + const buildWebSearchArtifacts = (input: { + readonly context: ActiveCodexTurnContext; + readonly item: CodexWebSearchItem; + readonly completed: boolean; + }) => + Effect.gen(function* () { + const updatedAt = yield* DateTime.now; + const completedAt = input.completed ? updatedAt : null; + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: input.item.id, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: input.item.id, + }); + const ordinal = yield* resolveItemOrdinal(input.context, input.item.id); + const patterns = webSearchPatterns(input.item); + const status = input.completed ? "completed" : "running"; + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + parentNodeId: input.context.itemParentNodeId, + rootNodeId: input.context.rootNodeId, + kind: "tool_call", + status, + countsForRun: false, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.item.id), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: input.context.startedAt, + completedAt, + }; + const turnItem: OrchestrationV2TurnItem = { + id: turnItemId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + nodeId, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.item.id), + parentItemId: null, + ordinal, + status, + title: null, + startedAt: input.context.startedAt, + completedAt, + updatedAt, + type: "web_search", + ...(patterns.length === 0 ? {} : { patterns: [...patterns] }), + }; + return { node, turnItem }; + }); + + const buildDynamicToolArtifacts = ( + context: ActiveCodexTurnContext, + item: CodexDynamicToolItem, + ) => + Effect.gen(function* () { + const updatedAt = yield* DateTime.now; + const status = codexItemStatus(item.status); + const completedAt = status.completed ? updatedAt : null; + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: item.id, + }); + const ordinal = yield* resolveItemOrdinal(context, item.id); + const projection = projectCodexDynamicToolItem(item); + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: context.projectionThreadId, + runId: context.projectionRunId, + parentNodeId: context.itemParentNodeId, + rootNodeId: context.rootNodeId, + kind: "tool_call", + status: status.node, + countsForRun: false, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: codexNativeItemRef(item.id), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: context.startedAt, + completedAt, + }; + const turnItem: OrchestrationV2TurnItem = { + id: turnItemId, + threadId: context.projectionThreadId, + runId: context.projectionRunId, + nodeId, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: codexNativeItemRef(item.id), + parentItemId: null, + ordinal, + status: projection.status, + title: null, + startedAt: context.startedAt, + completedAt, + updatedAt, + type: "dynamic_tool", + toolName: projection.toolName, + input: projection.input, + ...(projection.output === undefined ? {} : { output: projection.output }), + }; + return { node, turnItem }; + }); + + const buildProposedPlanArtifacts = (input: { + readonly context: ActiveCodexTurnContext; + readonly nativeItemId: string; + readonly status: OrchestrationV2PlanArtifact["status"]; + readonly markdown: string; + readonly completed?: boolean; + }) => + Effect.gen(function* () { + const updatedAt = yield* DateTime.now; + const completedAt = input.completed === true ? updatedAt : null; + const planId = yield* resolvePlanId(input.context, input.nativeItemId); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const ordinal = yield* resolveItemOrdinal(input.context, input.nativeItemId); + const plan: OrchestrationV2PlanArtifact = { + id: planId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + nodeId, + kind: "proposed_plan", + status: input.status, + markdown: input.markdown, + }; + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + parentNodeId: input.context.itemParentNodeId, + rootNodeId: input.context.rootNodeId, + kind: "plan", + status: input.completed === true ? "completed" : "running", + countsForRun: false, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.nativeItemId), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: input.context.startedAt, + completedAt, + }; + const turnItem: OrchestrationV2TurnItem = { + id: turnItemId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + nodeId, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.nativeItemId), + parentItemId: null, + ordinal, + status: input.completed === true ? "completed" : "running", + title: null, + startedAt: input.context.startedAt, + completedAt, + updatedAt, + type: "proposed_plan", + planId, + markdown: input.markdown, + streaming: input.completed !== true, + }; + return { node, plan, turnItem }; + }); + + const buildTodoListArtifacts = (input: { + readonly context: ActiveCodexTurnContext; + readonly nativeItemId: string; + readonly status: OrchestrationV2PlanArtifact["status"]; + readonly steps: ReadonlyArray; + readonly explanation?: string; + readonly completed?: boolean; + }) => + Effect.gen(function* () { + const updatedAt = yield* DateTime.now; + const completedAt = input.completed === true ? updatedAt : null; + const planId = yield* resolvePlanId(input.context, input.nativeItemId); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const ordinal = yield* resolveItemOrdinal(input.context, input.nativeItemId); + const plan: OrchestrationV2PlanArtifact = { + id: planId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + nodeId, + kind: "todo_list", + status: input.status, + steps: [...input.steps], + ...(input.explanation === undefined ? {} : { explanation: input.explanation }), + }; + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + parentNodeId: input.context.itemParentNodeId, + rootNodeId: input.context.rootNodeId, + kind: "todo_list", + status: input.completed === true ? "completed" : "running", + countsForRun: false, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.nativeItemId), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: input.context.startedAt, + completedAt, + }; + const turnItem: OrchestrationV2TurnItem = { + id: turnItemId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + nodeId, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.nativeItemId), + parentItemId: null, + ordinal, + status: input.completed === true ? "completed" : "running", + title: null, + startedAt: input.context.startedAt, + completedAt, + updatedAt, + type: "todo_list", + planId, + steps: [...input.steps], + ...(input.explanation === undefined ? {} : { explanation: input.explanation }), + }; + return { node, plan, turnItem }; + }); + + const buildApprovalRequestArtifacts = (input: { + readonly context: ActiveCodexTurnContext; + readonly nativeItemId: string; + readonly nativeRequestId: string; + readonly requestKind: ProviderRequestKind; + readonly prompt?: string | null; + }) => + Effect.gen(function* () { + const createdAt = yield* DateTime.now; + const parentNodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const ordinal = yield* resolveItemOrdinal( + input.context, + `${input.nativeItemId}:approval:${input.nativeRequestId}`, + ); + const requestId = yield* idAllocator.allocate.runtimeRequest({ + driver: CODEX_PROVIDER, + providerTurnId: input.context.providerTurnId, + nativeRequestId: input.nativeRequestId, + }); + const nodeId = idAllocator.derive.approvalNode({ requestId }); + const providerSessionId = input.context.input.providerThread.providerSessionId; + if (providerSessionId === null) { + return yield* toProtocolError( + `Provider thread ${input.context.providerThread.id} is missing a provider session id.`, + ); + } + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + parentNodeId, + rootNodeId: input.context.rootNodeId, + kind: "approval_request", + status: "waiting", + countsForRun: false, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.nativeItemId), + runtimeRequestId: requestId, + checkpointScopeId: null, + startedAt: createdAt, + completedAt: null, + }; + const request: OrchestrationV2RuntimeRequest = { + id: requestId, + nodeId, + providerTurnId: input.context.providerTurnId, + nativeRequestRef: { + driver: CODEX_PROVIDER, + nativeId: input.nativeRequestId, + strength: "strong", + }, + kind: input.requestKind, + status: "pending", + responseCapability: { + type: "live", + providerSessionId, + }, + createdAt, + resolvedAt: null, + }; + const turnItem: OrchestrationV2TurnItem = { + id: idAllocator.derive.approvalTurnItem({ requestId }), + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + nodeId, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.nativeItemId), + parentItemId: null, + ordinal, + status: "waiting", + title: null, + startedAt: createdAt, + completedAt: null, + updatedAt: createdAt, + type: "approval_request", + requestId, + requestKind: input.requestKind, + ...(input.prompt === null || input.prompt === undefined + ? {} + : { prompt: input.prompt }), + }; + return { node, request, turnItem }; + }); + + const buildUserInputRequestArtifacts = (input: { + readonly context: ActiveCodexTurnContext; + readonly nativeItemId: string; + readonly nativeRequestId: string; + readonly questions: ReadonlyArray; + }) => + Effect.gen(function* () { + const createdAt = yield* DateTime.now; + const requestId = yield* idAllocator.allocate.runtimeRequest({ + driver: CODEX_PROVIDER, + providerTurnId: input.context.providerTurnId, + nativeRequestId: input.nativeRequestId, + }); + const providerSessionId = input.context.input.providerThread.providerSessionId; + if (providerSessionId === null) { + return yield* toProtocolError( + `Provider thread ${input.context.providerThread.id} is missing a provider session id.`, + ); + } + const questions = input.questions.map((question, index) => ({ + id: nonEmptyText(question.id, `question-${index + 1}`), + header: nonEmptyText(question.header, "Question"), + question: nonEmptyText(question.question, "Choose an answer."), + options: + question.options?.map((option, optionIndex) => ({ + label: nonEmptyText(option.label, `Option ${optionIndex + 1}`), + description: nonEmptyText(option.description, option.label), + })) ?? [], + })); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CODEX_PROVIDER, + nativeItemId: input.nativeItemId, + }); + const ordinal = yield* resolveItemOrdinal(input.context, input.nativeItemId); + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + parentNodeId: input.context.itemParentNodeId, + rootNodeId: input.context.rootNodeId, + kind: "user_input_request", + status: "waiting", + countsForRun: false, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.nativeItemId), + runtimeRequestId: requestId, + checkpointScopeId: null, + startedAt: createdAt, + completedAt: null, + }; + const request: OrchestrationV2RuntimeRequest = { + id: requestId, + nodeId, + providerTurnId: input.context.providerTurnId, + nativeRequestRef: { + driver: CODEX_PROVIDER, + nativeId: input.nativeRequestId, + strength: "strong", + }, + kind: "user_input", + status: "pending", + responseCapability: { + type: "live", + providerSessionId, + }, + createdAt, + resolvedAt: null, + }; + const turnItem: OrchestrationV2TurnItem = { + id: turnItemId, + threadId: input.context.projectionThreadId, + runId: input.context.projectionRunId, + nodeId, + providerThreadId: input.context.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: codexNativeItemRef(input.nativeItemId), + parentItemId: null, + ordinal, + status: "waiting", + title: null, + startedAt: createdAt, + completedAt: null, + updatedAt: createdAt, + type: "user_input_request", + requestId, + questions, + }; + return { node, request, turnItem }; + }); + + yield* client.handleServerNotification("item/agentMessage/delta", (payload) => + Ref.update(agentMessageDeltas, (current) => { + const updated = new Map(current); + updated.set(payload.itemId, `${updated.get(payload.itemId) ?? ""}${payload.delta}`); + return updated; + }), + ); + + yield* client.handleServerNotification("item/plan/delta", (payload) => + Effect.gen(function* () { + const context = yield* awaitActiveTurn(payload.turnId); + if (context === undefined) { + return; + } + const markdown = yield* Ref.modify(planDeltas, (current) => { + const updated = new Map(current); + const next = `${updated.get(payload.itemId) ?? ""}${payload.delta}`; + updated.set(payload.itemId, next); + return [next, updated]; + }); + const artifacts = yield* buildProposedPlanArtifacts({ + context, + nativeItemId: payload.itemId, + status: "active", + markdown, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "plan.updated", + driver: CODEX_PROVIDER, + plan: artifacts.plan, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + }).pipe(Effect.orDie), + ); + + yield* client.handleServerNotification("turn/plan/updated", (payload) => + Effect.gen(function* () { + const context = yield* awaitActiveTurn(payload.turnId); + if (context === undefined) { + return; + } + const steps = payload.plan.map((step, index) => ({ + id: `step-${index + 1}`, + text: nonEmptyText(step.step, `Step ${index + 1}`), + status: codexPlanStepStatus(step.status), + })); + const explanation = trimText(payload.explanation); + const artifacts = yield* buildTodoListArtifacts({ + context, + nativeItemId: `turn-plan:${payload.turnId}`, + status: "active", + ...(explanation === undefined ? {} : { explanation }), + steps, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "plan.updated", + driver: CODEX_PROVIDER, + plan: artifacts.plan, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + }).pipe(Effect.orDie), + ); + + yield* client.handleServerNotification("turn/started", (payload) => + Effect.gen(function* () { + const context = (yield* Ref.get(activeTurns)).get(payload.turn.id); + if (context !== undefined) { + return; + } + const pendingRootTurn = (yield* Ref.get(pendingRootTurns)).get(payload.threadId); + if (pendingRootTurn !== undefined) { + yield* registerRootTurn({ + turnInput: pendingRootTurn, + nativeTurnId: payload.turn.id, + startedAt: codexTimestamp(payload.turn.startedAt), + }); + yield* Ref.update(pendingRootTurns, (current) => { + const updated = new Map(current); + updated.delete(payload.threadId); + return updated; + }); + return; + } + yield* rememberSubagentTurnStarted({ + nativeThreadId: payload.threadId, + nativeTurnId: payload.turn.id, + startedAt: codexTimestamp(payload.turn.startedAt), + }); + }).pipe(Effect.orDie), + ); + + yield* client.handleServerNotification("item/started", (payload) => + Effect.gen(function* () { + const context = yield* awaitActiveTurn(payload.turnId); + if (context === undefined) { + return; + } + + if (payload.item.type === "userMessage") { + if (yield* emitSubagentUserMessage(context, payload.item)) { + return; + } + } + + if (payload.item.type === "commandExecution") { + const artifacts = yield* buildCommandExecutionArtifacts(context, payload.item); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + return; + } + + if (payload.item.type === "mcpToolCall" || payload.item.type === "dynamicToolCall") { + const artifacts = yield* buildDynamicToolArtifacts(context, payload.item); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + return; + } + + if (payload.item.type !== "webSearch") { + return; + } + + const artifacts = yield* buildWebSearchArtifacts({ + context, + item: payload.item, + completed: false, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + }).pipe(Effect.orDie), + ); + + yield* client.handleServerNotification("item/completed", (payload) => + Effect.gen(function* () { + const context = yield* awaitActiveTurn(payload.turnId); + if (context === undefined) { + return; + } + + if (payload.item.type === "userMessage") { + if (yield* emitSubagentUserMessage(context, payload.item)) { + return; + } + } + + if (payload.item.type === "commandExecution") { + const artifacts = yield* buildCommandExecutionArtifacts(context, payload.item); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + return; + } + + if (payload.item.type === "mcpToolCall" || payload.item.type === "dynamicToolCall") { + const artifacts = yield* buildDynamicToolArtifacts(context, payload.item); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + return; + } + + if (payload.item.type === "fileChange") { + const artifacts = yield* buildFileChangeArtifacts(context, payload.item); + if (artifacts === null) { + return; + } + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + return; + } + + if (payload.item.type === "webSearch") { + const artifacts = yield* buildWebSearchArtifacts({ + context, + item: payload.item, + completed: true, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + return; + } + + if (payload.item.type === "plan") { + const deltas = yield* Ref.get(planDeltas); + const markdown = + payload.item.text.length > 0 + ? payload.item.text + : (deltas.get(payload.item.id) ?? ""); + const artifacts = yield* buildProposedPlanArtifacts({ + context, + nativeItemId: payload.item.id, + status: "completed", + markdown, + completed: true, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "plan.updated", + driver: CODEX_PROVIDER, + plan: artifacts.plan, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + return; + } + + if (payload.item.type === "collabAgentToolCall") { + yield* registerSubagentThreads({ + context, + item: payload.item, + }); + yield* updateSubagentStates({ + item: payload.item, + }); + return; + } + + if (payload.item.type !== "agentMessage") { + return; + } + + const deltas = yield* Ref.get(agentMessageDeltas); + const text = + payload.item.text.length > 0 + ? payload.item.text + : (deltas.get(payload.item.id) ?? ""); + const artifacts = yield* buildAgentMessageArtifacts(context, { + ...payload.item, + text, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "message.updated", + driver: CODEX_PROVIDER, + message: artifacts.message, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + if ( + context.subagent !== null && + context.providerTurnOrdinal === 1 && + payload.item.phase !== "commentary" + ) { + yield* emitSubagentTaskUpdate({ + subagent: context.subagent, + status: context.subagent.task.status, + result: text, + }); + } + }).pipe(Effect.orDie), + ); + + yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) => + Effect.gen(function* () { + const context = yield* awaitActiveTurn(payload.turnId); + if (context === undefined) { + return yield* toProtocolError( + `No active Codex turn context for approval turn ${payload.turnId}.`, + payload, + ); + } + + const nativeRequestId = payload.approvalId ?? payload.itemId; + const artifacts = yield* buildApprovalRequestArtifacts({ + context, + nativeItemId: payload.itemId, + nativeRequestId, + requestKind: "command", + ...((payload.reason ?? payload.command) === undefined + ? {} + : { prompt: payload.reason ?? payload.command }), + }); + const decision = yield* Deferred.make(); + yield* Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.set(String(artifacts.request.id), { + type: "approval", + requestId: artifacts.request.id, + requestKind: "command", + decision, + }); + return updated; + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver: CODEX_PROVIDER, + threadId: artifacts.node.threadId, + runtimeRequest: artifacts.request, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + + const resolved = yield* Deferred.await(decision).pipe( + Effect.ensuring( + Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.delete(String(artifacts.request.id)); + return updated; + }), + ), + ); + return { + decision: resolved, + } satisfies CodexSchema.CommandExecutionRequestApprovalResponse; + }).pipe(Effect.orDie), + ); + + yield* client.handleServerRequest("item/fileChange/requestApproval", (payload) => + Effect.gen(function* () { + const context = yield* awaitActiveTurn(payload.turnId); + if (context === undefined) { + return yield* toProtocolError( + `No active Codex turn context for file change approval turn ${payload.turnId}.`, + payload, + ); + } + + const artifacts = yield* buildApprovalRequestArtifacts({ + context, + nativeItemId: payload.itemId, + nativeRequestId: payload.itemId, + requestKind: "file-change", + ...(payload.reason === undefined ? {} : { prompt: payload.reason }), + }); + const decision = yield* Deferred.make(); + yield* Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.set(String(artifacts.request.id), { + type: "approval", + requestId: artifacts.request.id, + requestKind: "file-change", + decision, + }); + return updated; + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver: CODEX_PROVIDER, + threadId: artifacts.node.threadId, + runtimeRequest: artifacts.request, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + + const resolved = yield* Deferred.await(decision).pipe( + Effect.ensuring( + Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.delete(String(artifacts.request.id)); + return updated; + }), + ), + ); + return { + decision: resolved, + } satisfies CodexSchema.FileChangeRequestApprovalResponse; + }).pipe(Effect.orDie), + ); + + yield* client.handleServerRequest("item/permissions/requestApproval", (payload) => + Effect.gen(function* () { + const context = yield* awaitActiveTurn(payload.turnId); + if (context === undefined) { + return yield* toProtocolError( + `No active Codex turn context for permissions approval turn ${payload.turnId}.`, + payload, + ); + } + + const requestKind = providerRequestKindFromPermissions(payload.permissions); + const artifacts = yield* buildApprovalRequestArtifacts({ + context, + nativeItemId: payload.itemId, + nativeRequestId: payload.itemId, + requestKind, + ...(payload.reason === undefined ? {} : { prompt: payload.reason }), + }); + const decision = yield* Deferred.make(); + yield* Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.set(String(artifacts.request.id), { + type: "approval", + requestId: artifacts.request.id, + requestKind, + decision, + }); + return updated; + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver: CODEX_PROVIDER, + threadId: artifacts.node.threadId, + runtimeRequest: artifacts.request, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + + const resolved = yield* Deferred.await(decision).pipe( + Effect.ensuring( + Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.delete(String(artifacts.request.id)); + return updated; + }), + ), + ); + return permissionsResponseFromDecision({ + decision: resolved, + permissions: payload.permissions, + }); + }).pipe(Effect.orDie), + ); + + yield* client.handleServerRequest("execCommandApproval", (payload) => + Effect.gen(function* () { + const context = yield* findActiveTurnByNativeThreadId(payload.conversationId); + if (context === undefined) { + return yield* toProtocolError( + `No active Codex turn context for exec approval thread ${payload.conversationId}.`, + payload, + ); + } + + const nativeRequestId = payload.approvalId ?? payload.callId; + const artifacts = yield* buildApprovalRequestArtifacts({ + context, + nativeItemId: payload.callId, + nativeRequestId, + requestKind: "command", + prompt: payload.reason ?? payload.command.join(" "), + }); + const decision = yield* Deferred.make(); + yield* Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.set(String(artifacts.request.id), { + type: "approval", + requestId: artifacts.request.id, + requestKind: "command", + decision, + }); + return updated; + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver: CODEX_PROVIDER, + threadId: artifacts.node.threadId, + runtimeRequest: artifacts.request, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + + const resolved = yield* Deferred.await(decision).pipe( + Effect.ensuring( + Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.delete(String(artifacts.request.id)); + return updated; + }), + ), + ); + return { + decision: approvalDecisionToLegacyReviewDecision(resolved), + } satisfies CodexSchema.ExecCommandApprovalResponse; + }).pipe(Effect.orDie), + ); + + yield* client.handleServerRequest("applyPatchApproval", (payload) => + Effect.gen(function* () { + const context = yield* findActiveTurnByNativeThreadId(payload.conversationId); + if (context === undefined) { + return yield* toProtocolError( + `No active Codex turn context for apply patch approval thread ${payload.conversationId}.`, + payload, + ); + } + + const artifacts = yield* buildApprovalRequestArtifacts({ + context, + nativeItemId: payload.callId, + nativeRequestId: payload.callId, + requestKind: "file-change", + prompt: payload.reason ?? Object.keys(payload.fileChanges).join(", "), + }); + const decision = yield* Deferred.make(); + yield* Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.set(String(artifacts.request.id), { + type: "approval", + requestId: artifacts.request.id, + requestKind: "file-change", + decision, + }); + return updated; + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver: CODEX_PROVIDER, + threadId: artifacts.node.threadId, + runtimeRequest: artifacts.request, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + + const resolved = yield* Deferred.await(decision).pipe( + Effect.ensuring( + Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.delete(String(artifacts.request.id)); + return updated; + }), + ), + ); + return { + decision: approvalDecisionToLegacyReviewDecision(resolved), + } satisfies CodexSchema.ApplyPatchApprovalResponse; + }).pipe(Effect.orDie), + ); + + yield* client.handleServerRequest("item/tool/requestUserInput", (payload) => + Effect.gen(function* () { + const context = yield* awaitActiveTurn(payload.turnId); + if (context === undefined) { + return yield* toProtocolError( + `No active Codex turn context for user input request turn ${payload.turnId}.`, + payload, + ); + } + + const artifacts = yield* buildUserInputRequestArtifacts({ + context, + nativeItemId: payload.itemId, + nativeRequestId: payload.itemId, + questions: payload.questions, + }); + const answers = yield* Deferred.make(); + yield* Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.set(String(artifacts.request.id), { + type: "user_input", + requestId: artifacts.request.id, + answers, + }); + return updated; + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: artifacts.node, + }); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver: CODEX_PROVIDER, + threadId: artifacts.node.threadId, + runtimeRequest: artifacts.request, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CODEX_PROVIDER, + turnItem: artifacts.turnItem, + }); + + const resolved = yield* Deferred.await(answers).pipe( + Effect.ensuring( + Ref.update(pendingRuntimeRequests, (current) => { + const updated = new Map(current); + updated.delete(String(artifacts.request.id)); + return updated; + }), + ), + ); + return { + answers: toCodexUserInputAnswers( + resolved, + new Set(payload.questions.map((question) => question.id)), + ), + } satisfies CodexSchema.ToolRequestUserInputResponse; + }).pipe(Effect.orDie), + ); + + yield* client.handleServerNotification("turn/completed", (payload) => + Effect.gen(function* () { + const context = (yield* Ref.get(activeTurns)).get(payload.turn.id); + if (context === undefined) { + return; + } + const completedAt = codexTimestamp(payload.turn.completedAt); + const status = mapCodexTurnStatus(payload.turn.status); + yield* emitProviderEvent({ + type: "provider_turn.updated", + driver: CODEX_PROVIDER, + threadId: context.projectionThreadId, + providerTurn: { + id: context.providerTurnId, + providerThreadId: context.providerThread.id, + nodeId: context.providerNodeId, + runAttemptId: context.subagent === null ? context.input.attemptId : null, + nativeTurnRef: { + driver: CODEX_PROVIDER, + nativeId: payload.turn.id, + strength: "strong", + }, + ordinal: context.providerTurnOrdinal, + status, + startedAt: context.startedAt, + completedAt, + }, + }); + if (context.subagent !== null) { + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: { + id: context.providerNodeId, + threadId: context.projectionThreadId, + runId: null, + parentNodeId: null, + rootNodeId: context.rootNodeId, + kind: "root_turn", + status, + countsForRun: false, + providerThreadId: context.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef: context.subagent.task.nativeTaskRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: context.providerNodeStartedAt, + completedAt, + }, + }); + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver: CODEX_PROVIDER, + providerThread: { + ...context.providerThread, + status: "idle", + updatedAt: completedAt, + }, + }); + if (context.providerTurnOrdinal === 1) { + yield* emitProviderEvent({ + type: "node.updated", + driver: CODEX_PROVIDER, + node: { + id: context.subagent.subagentNodeId, + threadId: context.subagent.parentContext.projectionThreadId, + runId: context.subagent.parentContext.projectionRunId, + parentNodeId: context.subagent.parentContext.rootNodeId, + rootNodeId: context.subagent.parentContext.rootNodeId, + kind: "subagent", + status, + countsForRun: false, + providerThreadId: context.providerThread.id, + providerTurnId: context.subagent.parentContext.providerTurnId, + nativeItemRef: context.subagent.task.nativeTaskRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: context.subagent.startedAt, + completedAt, + }, + }); + yield* emitSubagentTaskUpdate({ + subagent: context.subagent, + status, + completedAt, + }); + } + } + if (context.subagent === null) { + yield* emitProviderEvent({ + type: "turn.terminal", + driver: CODEX_PROVIDER, + providerTurnId: context.providerTurnId, + status: providerTurnStatusToTerminal(status), + }); + } + const waiter = (yield* Ref.get(turnWaiters)).get(payload.turn.id); + if (waiter !== undefined) { + yield* Deferred.succeed(waiter, undefined); + } + yield* Ref.update(activeTurns, (current) => { + const updated = new Map(current); + updated.delete(payload.turn.id); + return updated; + }); + }), + ); + + const runtime: ProviderAdapterV2SessionRuntime = { + instanceId: adapterOptions.instanceId, + driver: CODEX_PROVIDER, + providerSessionId: input.providerSessionId, + providerSession: session, + rawEvents: Stream.empty, + events: Stream.fromEffectRepeat(Queue.take(events)), + ensureThread: (threadInput) => + ensureInitialized.pipe( + Effect.andThen( + client.request( + "thread/start", + codexThreadRuntimeParams({ + threadId: threadInput.threadId, + modelSelection: threadInput.modelSelection, + runtimePolicy: threadInput.runtimePolicy, + }), + ), + ), + Effect.map( + (response): OrchestrationV2ProviderThread => + providerThreadFromCodexThread({ + appThreadId: threadInput.threadId, + idAllocator, + ownerNodeId: null, + providerSessionId: input.providerSessionId, + providerInstanceId: adapterOptions.instanceId, + thread: response.thread, + }), + ), + Effect.mapError( + (cause) => + new ProviderAdapterEnsureThreadError({ + driver: CODEX_PROVIDER, + threadId: threadInput.threadId, + cause: normalizeCodexCause(cause), + }), + ), + ), + resumeThread: (threadInput) => + Effect.gen(function* () { + const nativeThreadId = yield* getNativeThreadId(threadInput.providerThread); + + const response = yield* ensureInitialized.pipe( + Effect.andThen( + client.request("thread/resume", { + threadId: nativeThreadId, + ...codexThreadRuntimeParams({ + threadId: threadInput.threadId ?? threadInput.providerThread.appThreadId, + ...(threadInput.modelSelection === undefined + ? {} + : { modelSelection: threadInput.modelSelection }), + ...(threadInput.runtimePolicy === undefined + ? {} + : { runtimePolicy: threadInput.runtimePolicy }), + }), + }), + ), + ); + return { + ...threadInput.providerThread, + providerSessionId: input.providerSessionId, + providerInstanceId: adapterOptions.instanceId, + status: "idle", + nativeThreadRef: { + driver: CODEX_PROVIDER, + nativeId: response.thread.id, + strength: "strong", + }, + nativeConversationHeadRef: threadInput.providerThread.nativeConversationHeadRef, + updatedAt: codexTimestamp(response.thread.updatedAt), + } satisfies OrchestrationV2ProviderThread; + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterResumeThreadError({ + driver: CODEX_PROVIDER, + providerSessionId: input.providerSessionId, + providerThreadId: threadInput.providerThread.id, + cause: normalizeCodexCause(cause), + }), + ), + ), + startTurn: (turnInput) => + Effect.gen(function* () { + const threadId = yield* getNativeThreadId(turnInput.providerThread); + + const codexInput = yield* toCodexInput(turnInput); + const turnStartParams = yield* buildCodexTurnStartParams({ + nativeThreadId: threadId, + codexInput, + runtimePolicy: turnInput.runtimePolicy, + model: turnInput.modelSelection.model, + }); + yield* Ref.update(pendingRootTurns, (current) => { + const updated = new Map(current); + updated.set(threadId, turnInput); + return updated; + }); + const started = yield* client.request("turn/start", turnStartParams); + const nativeTurnId = started.turn.id; + const startedAt = codexTimestamp(started.turn.startedAt); + yield* registerRootTurn({ turnInput, nativeTurnId, startedAt }); + yield* Ref.update(pendingRootTurns, (current) => { + const updated = new Map(current); + updated.delete(threadId); + return updated; + }); + }).pipe( + Effect.ensuring( + Effect.flatMap(getNativeThreadId(turnInput.providerThread), (threadId) => + Ref.update(pendingRootTurns, (current) => { + const updated = new Map(current); + updated.delete(threadId); + return updated; + }), + ).pipe(Effect.ignore), + ), + Effect.mapError( + (cause) => + new ProviderAdapterTurnStartError({ + driver: CODEX_PROVIDER, + threadId: turnInput.threadId, + providerThreadId: turnInput.providerThread.id, + runId: turnInput.runId, + cause, + }), + ), + ), + steerTurn: (turnInput) => + Effect.gen(function* () { + const threadId = yield* getNativeThreadId(turnInput.providerThread); + const activeTurn = Array.from((yield* Ref.get(activeTurns)).values()).find( + (candidate) => candidate.providerTurnId === turnInput.providerTurnId, + ); + if (activeTurn === undefined) { + return yield* toProtocolError( + `Provider turn ${turnInput.providerTurnId} is not active and cannot be steered.`, + ); + } + + const codexInput = yield* toCodexInput(turnInput); + yield* client.request("turn/steer", { + expectedTurnId: activeTurn.nativeTurnId, + input: codexInput, + threadId, + }); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterSteerRunError({ + driver: CODEX_PROVIDER, + providerThreadId: turnInput.providerThread.id, + providerTurnId: turnInput.providerTurnId, + cause, + }), + ), + ), + interruptTurn: (turnInput) => + Effect.gen(function* () { + const threadId = yield* getNativeThreadId(turnInput.providerThread); + const activeTurn = Array.from((yield* Ref.get(activeTurns)).values()).find( + (candidate) => candidate.providerTurnId === turnInput.providerTurnId, + ); + if (activeTurn === undefined) { + return yield* toProtocolError( + `Provider turn ${turnInput.providerTurnId} is not active and cannot be interrupted.`, + ); + } + yield* client.request("turn/interrupt", { + threadId, + turnId: activeTurn.nativeTurnId, + }); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterInterruptError({ + driver: CODEX_PROVIDER, + providerThreadId: turnInput.providerThread.id, + providerTurnId: turnInput.providerTurnId, + cause, + }), + ), + ), + respondToRuntimeRequest: (requestInput) => + Effect.gen(function* () { + const pending = (yield* Ref.get(pendingRuntimeRequests)).get( + String(requestInput.requestId), + ); + if (pending === undefined) { + return yield* new ProviderAdapterRuntimeRequestResponseError({ + driver: CODEX_PROVIDER, + requestId: requestInput.requestId, + cause: toProtocolError( + `No pending Codex runtime request ${requestInput.requestId}.`, + ), + }); + } + if (pending.type === "user_input") { + if (requestInput.answers === undefined) { + return yield* new ProviderAdapterRuntimeRequestResponseError({ + driver: CODEX_PROVIDER, + requestId: requestInput.requestId, + cause: toProtocolError( + `Codex user input request ${requestInput.requestId} requires answers.`, + ), + }); + } + yield* Deferred.succeed(pending.answers, requestInput.answers); + return; + } + if (requestInput.decision === undefined) { + return yield* new ProviderAdapterRuntimeRequestResponseError({ + driver: CODEX_PROVIDER, + requestId: requestInput.requestId, + cause: toProtocolError( + `Codex ${pending.requestKind} request ${requestInput.requestId} requires an approval decision.`, + ), + }); + } + yield* Deferred.succeed(pending.decision, requestInput.decision); + }).pipe( + Effect.mapError((cause) => + Schema.is(ProviderAdapterRuntimeRequestResponseError)(cause) + ? cause + : new ProviderAdapterRuntimeRequestResponseError({ + driver: CODEX_PROVIDER, + requestId: requestInput.requestId, + cause, + }), + ), + ), + readThreadSnapshot: (threadInput) => + Effect.gen(function* () { + const threadId = yield* getNativeThreadId(threadInput.providerThread); + const response = yield* ensureInitialized.pipe( + Effect.andThen(client.request("thread/read", { threadId, includeTurns: true })), + ); + return { + providerThread: { + ...threadInput.providerThread, + nativeThreadRef: { + driver: CODEX_PROVIDER, + nativeId: response.thread.id, + strength: "strong" as const, + }, + nativeConversationHeadRef: threadInput.providerThread.nativeConversationHeadRef, + updatedAt: codexTimestamp(response.thread.updatedAt), + }, + providerTurns: [], + messages: [], + runtimeRequests: [], + providerPayload: response.thread, + }; + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterReadThreadSnapshotError({ + driver: CODEX_PROVIDER, + providerThreadId: threadInput.providerThread.id, + cause, + }), + ), + ), + rollbackThread: (threadInput) => + Effect.gen(function* () { + const threadId = yield* getNativeThreadId(threadInput.providerThread); + const numTurns = yield* resolveCodexRollbackTurnCount(threadInput); + const nativeConversationHeadRef = + threadInput.target.type === "provider_turn" + ? threadInput.target.providerTurn.nativeTurnRef + : null; + if (numTurns === 0) { + return { + providerThread: { + ...threadInput.providerThread, + nativeConversationHeadRef, + status: "idle" as const, + }, + providerTurns: [], + messages: [], + runtimeRequests: [], + }; + } + const response = yield* ensureInitialized.pipe( + Effect.andThen(client.request("thread/rollback", { threadId, numTurns })), + ); + return { + providerThread: { + ...threadInput.providerThread, + nativeThreadRef: { + driver: CODEX_PROVIDER, + nativeId: response.thread.id, + strength: "strong" as const, + }, + nativeConversationHeadRef, + status: "idle" as const, + updatedAt: codexTimestamp(response.thread.updatedAt), + }, + providerTurns: [], + messages: [], + runtimeRequests: [], + providerPayload: response.thread, + }; + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterRollbackThreadError({ + driver: CODEX_PROVIDER, + providerThreadId: threadInput.providerThread.id, + cause: normalizeCodexCause(cause), + }), + ), + ), + forkThread: (threadInput) => + Effect.gen(function* () { + const threadId = yield* getNativeThreadId(threadInput.sourceProviderThread); + const response = yield* ensureInitialized.pipe( + Effect.andThen( + client.request("thread/fork", { + threadId, + ...codexThreadRuntimeParams({ + threadId: threadInput.targetThreadId, + ...(threadInput.modelSelection === undefined + ? {} + : { modelSelection: threadInput.modelSelection }), + ...(threadInput.runtimePolicy === undefined + ? {} + : { runtimePolicy: threadInput.runtimePolicy }), + }), + }), + ), + Effect.mapError( + (cause) => + new ProviderAdapterForkThreadError({ + driver: CODEX_PROVIDER, + providerThreadId: threadInput.sourceProviderThread.id, + cause: normalizeCodexCause(cause), + }), + ), + ); + const rollbackTurnCount = yield* resolveCodexForkRollbackTurnCount(threadInput); + const forkedThread = + rollbackTurnCount === 0 + ? response.thread + : (yield* ensureInitialized.pipe( + Effect.andThen( + client.request("thread/rollback", { + threadId: response.thread.id, + numTurns: rollbackTurnCount, + }), + ), + Effect.mapError( + (cause) => + new ProviderAdapterForkThreadError({ + driver: CODEX_PROVIDER, + providerThreadId: threadInput.sourceProviderThread.id, + cause: normalizeCodexCause(cause), + }), + ), + )).thread; + return providerThreadFromCodexThread({ + appThreadId: threadInput.targetThreadId, + idAllocator, + ownerNodeId: threadInput.ownerNodeId ?? null, + providerSessionId: input.providerSessionId, + providerInstanceId: adapterOptions.instanceId, + thread: forkedThread, + forkedFrom: { + providerThreadId: threadInput.sourceProviderThread.id, + ...(threadInput.providerTurnId === undefined + ? {} + : { providerTurnId: threadInput.providerTurnId }), + }, + }); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterForkThreadError({ + driver: CODEX_PROVIDER, + providerThreadId: threadInput.sourceProviderThread.id, + cause: normalizeCodexCause(cause), + }), + ), + ), + }; + return runtime; + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterOpenSessionError({ + driver: CODEX_PROVIDER, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ), + }); +} diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts new file mode 100644 index 00000000000..57a25241346 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts @@ -0,0 +1,171 @@ +import { assert, describe, it } from "@effect/vitest"; +import { EnvironmentId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; + +import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; +import { + CursorProviderCapabilitiesV2, + cursorMcpServers, + cursorRuntimeAgentPolicy, + cursorSdkModelSelection, + makeCursorAgentOptions, +} from "./CursorAdapterV2.ts"; +import { isCursorCancellationError, loggedCursorAgentOptions } from "./CursorAgentSdk.ts"; + +describe("CursorAdapterV2", () => { + it("maps Cursor auto and model parameters to SDK selections", () => { + assert.deepEqual( + cursorSdkModelSelection({ + instanceId: ProviderInstanceId.make("cursor"), + model: "auto", + options: [ + { id: "thinking", value: "high" }, + { id: "contextWindow", value: "1m" }, + { id: "fastMode", value: true }, + ], + }), + { + id: "default", + params: [ + { id: "thinking", value: "high" }, + { id: "context", value: "1m" }, + { id: "fast", value: "true" }, + ], + }, + ); + }); + + it("maps runtime modes to the SDK sandbox and auto-review controls", () => { + const base = { + interactionMode: "default" as const, + cwd: "/tmp/cursor-adapter", + }; + assert.deepEqual( + cursorRuntimeAgentPolicy({ + ...base, + runtimeMode: "full-access", + }), + { + autoReview: false, + sandboxEnabled: false, + }, + ); + assert.deepEqual( + cursorRuntimeAgentPolicy({ + ...base, + runtimeMode: "auto-accept-edits", + }), + { + autoReview: false, + sandboxEnabled: true, + }, + ); + assert.deepEqual( + cursorRuntimeAgentPolicy({ + ...base, + runtimeMode: "approval-required", + }), + { + autoReview: true, + sandboxEnabled: true, + }, + ); + assert.deepEqual( + cursorRuntimeAgentPolicy({ + ...base, + runtimeMode: "full-access", + approvalPolicy: "never", + sandboxPolicy: { type: "readOnly" }, + }), + { + autoReview: false, + sandboxEnabled: true, + }, + ); + assert.deepEqual( + cursorRuntimeAgentPolicy({ + ...base, + runtimeMode: "approval-required", + approvalPolicy: "never", + sandboxPolicy: { type: "dangerFullAccess" }, + }), + { + autoReview: false, + sandboxEnabled: false, + }, + ); + }); + + it("advertises only capabilities exposed by the official SDK adapter", () => { + assert.isTrue(CursorProviderCapabilitiesV2.threads.canReadThreadSnapshot); + assert.isFalse(CursorProviderCapabilitiesV2.threads.canForkThread); + assert.isFalse(CursorProviderCapabilitiesV2.threads.canRollbackThread); + assert.isTrue(CursorProviderCapabilitiesV2.turns.supportsInterrupt); + assert.isFalse(CursorProviderCapabilitiesV2.turns.supportsActiveSteering); + assert.isTrue(CursorProviderCapabilitiesV2.turns.supportsSteeringByInterruptRestart); + assert.isTrue(CursorProviderCapabilitiesV2.tools.supportsMcpTools); + assert.isTrue(CursorProviderCapabilitiesV2.subagents.supportsSubagents); + assert.isFalse(CursorProviderCapabilitiesV2.subagents.exposesSubagentThreadIds); + assert.equal(CursorProviderCapabilitiesV2.identity.nativeItemIds, "weak"); + assert.isFalse(CursorProviderCapabilitiesV2.approvals.supportsCommandApproval); + }); + + it("injects thread-scoped MCP credentials without logging them", () => { + const threadId = ThreadId.make("thread-cursor-mcp"); + McpProviderSession.setMcpProviderSession({ + environmentId: EnvironmentId.make("environment-cursor-mcp"), + threadId, + providerSessionId: "mcp-session-cursor", + providerInstanceId: ProviderInstanceId.make("cursor"), + endpoint: "http://127.0.0.1:43123/mcp", + authorizationHeader: "Bearer secret-cursor-mcp-token", + }); + + try { + assert.deepEqual(cursorMcpServers(threadId), { + "t3-code": { + type: "http", + url: "http://127.0.0.1:43123/mcp", + headers: { + Authorization: "Bearer secret-cursor-mcp-token", + }, + }, + }); + + const options = makeCursorAgentOptions({ + apiKey: "secret-cursor-api-key", + modelSelection: { + instanceId: ProviderInstanceId.make("cursor"), + model: "composer-2.5", + }, + runtimePolicy: { + runtimeMode: "full-access", + interactionMode: "default", + cwd: "/workspace", + }, + threadId, + }); + assert.deepEqual(options.mcpServers, cursorMcpServers(threadId)); + + const logged = JSON.stringify(loggedCursorAgentOptions(options)); + assert.notInclude(logged, "secret-cursor-api-key"); + assert.notInclude(logged, "secret-cursor-mcp-token"); + } finally { + McpProviderSession.clearMcpProviderSession(threadId); + } + }); + + it("recognizes direct and SDK-wrapped abort failures as cancellation", () => { + assert.isTrue(isCursorCancellationError({ name: "AbortError" })); + assert.isTrue( + isCursorCancellationError({ + name: "ConnectError", + cause: { + name: "ConnectError", + cause: { name: "AbortError" }, + }, + }), + ); + assert.isFalse(isCursorCancellationError(new Error("request failed"))); + assert.isFalse(isCursorCancellationError(null)); + }); +}); diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts new file mode 100644 index 00000000000..0e46276c797 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts @@ -0,0 +1,985 @@ +import { Agent, type InteractionUpdate, type RunResult } from "@cursor/sdk"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { + ProviderReplayEntry, + type ModelSelection, + type ProviderReplayTranscript, + type ThreadId, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; +import * as Schema from "effect/Schema"; + +import { ServerConfig } from "../../config.ts"; +import { layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { ProviderAdapterDriverCreateError } from "../ProviderAdapterDriver.ts"; +import { makeDriverLayer as makeProviderAdapterRegistryDriverLayer } from "../ProviderAdapterRegistry.ts"; +import type { OrchestratorV2ProviderReplayHarness } from "../testkit/ProviderReplayHarness.ts"; +import { + CURSOR_AGENT_SDK_PROTOCOL, + CURSOR_PROVIDER, + CursorAgentSdkRunner, + CursorAgentSdkRunnerError, + isCursorCancellationError, + loggedCursorAgentOptions, + loggedCursorSendOptions, + type CursorAgentSdkOpenInput, + type CursorAgentSdkProtocolLogEvent, + type CursorAgentSdkRun, + type CursorAgentSdkRunnerShape, + type CursorAgentSdkSendInput, + type CursorAgentSdkSession, +} from "./CursorAgentSdk.ts"; +import { + CURSOR_DEFAULT_INSTANCE_ID, + CURSOR_DRIVER_KIND, + CursorAdapterV2Driver, + cursorSdkModelSelection, + makeCursorAgentOptions, +} from "./CursorAdapterV2.ts"; +import type { ProviderAdapterV2RuntimePolicy } from "../ProviderAdapter.ts"; + +const CursorAgentSdkReplayTranscript = Schema.Struct({ + provider: Schema.Literal(CURSOR_PROVIDER), + protocol: Schema.Literal(CURSOR_AGENT_SDK_PROTOCOL), + version: Schema.String, + scenario: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + entries: Schema.Array(ProviderReplayEntry), +}); +export type CursorAgentSdkReplayTranscript = typeof CursorAgentSdkReplayTranscript.Type; +const decodeCursorAgentSdkReplayTranscript = Schema.decodeUnknownEffect( + CursorAgentSdkReplayTranscript, +); + +export class CursorReplayTranscriptDecodeError extends Schema.TaggedErrorClass()( + "CursorReplayTranscriptDecodeError", + { + driver: Schema.optional(Schema.String), + protocol: Schema.optional(Schema.String), + scenario: Schema.optional(Schema.String), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to decode Cursor Agent SDK replay transcript for scenario ${this.scenario ?? ""}.`; + } +} + +export class CursorReplayExhaustedError extends Schema.TaggedErrorClass()( + "CursorReplayExhaustedError", + { + scenario: Schema.String, + cursor: Schema.Number, + actual: Schema.Unknown, + }, +) { + override get message(): string { + return `Cursor Agent SDK replay transcript exhausted at cursor ${this.cursor} in scenario ${this.scenario}.`; + } +} + +export class CursorReplayFrameMismatchError extends Schema.TaggedErrorClass()( + "CursorReplayFrameMismatchError", + { + scenario: Schema.String, + cursor: Schema.Number, + expected: Schema.Unknown, + actual: Schema.Unknown, + }, +) { + override get message(): string { + return `Cursor Agent SDK replay frame mismatch at cursor ${this.cursor} in scenario ${this.scenario}.`; + } +} + +export class CursorReplayIncompleteError extends Schema.TaggedErrorClass()( + "CursorReplayIncompleteError", + { + scenario: Schema.String, + cursor: Schema.Number, + remaining: Schema.Number, + }, +) { + override get message(): string { + return `Cursor Agent SDK replay ended with ${this.remaining} unconsumed entries in scenario ${this.scenario}.`; + } +} + +export class CursorReplayRuntimeError extends Schema.TaggedErrorClass()( + "CursorReplayRuntimeError", + { + scenario: Schema.String, + cursor: Schema.Number, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Cursor Agent SDK replay failed at cursor ${this.cursor} in scenario ${this.scenario}.`; + } +} + +export const CursorAgentSdkReplayError = Schema.Union([ + CursorReplayTranscriptDecodeError, + CursorReplayExhaustedError, + CursorReplayFrameMismatchError, + CursorReplayIncompleteError, + CursorReplayRuntimeError, +]); +export type CursorAgentSdkReplayError = typeof CursorAgentSdkReplayError.Type; +const isCursorAgentSdkReplayError = Schema.is(CursorAgentSdkReplayError); +const isCursorAgentSdkRunnerError = Schema.is(CursorAgentSdkRunnerError); + +export const CursorOrchestratorReplayHarnessError = Schema.Union([ + CursorAgentSdkReplayError, + ProviderAdapterDriverCreateError, +]); +export type CursorOrchestratorReplayHarnessError = typeof CursorOrchestratorReplayHarnessError.Type; + +type CursorProtocolPayload = CursorAgentSdkProtocolLogEvent["payload"]; +type CursorOutgoingFrame = Extract< + CursorAgentSdkProtocolLogEvent, + { readonly direction: "outgoing" } +>["payload"]; + +function stableStringify(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (typeof value === "object" && value !== null) { + const record = value as Record; + return `{${Object.keys(record) + .toSorted() + .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function normalizeContextHandoffText(value: string): string { + if (!value.startsWith("Context handoff (")) { + return value; + } + const marker = "\n\nUser message:\n"; + const markerIndex = value.indexOf(marker); + const headerEnd = value.indexOf(":\n"); + if (markerIndex === -1 || headerEnd === -1 || headerEnd >= markerIndex) { + return value; + } + return `${value.slice(0, headerEnd + 2)}${value.slice(markerIndex)}`; +} + +function normalizeFrame(value: unknown): unknown { + if (typeof value === "string") { + return normalizeContextHandoffText(value); + } + if (Array.isArray(value)) { + return value.map(normalizeFrame); + } + if (typeof value !== "object" || value === null) { + return value; + } + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [key, normalizeFrame(entry)]), + ); +} + +function sameFrame(left: unknown, right: unknown): boolean { + return stableStringify(normalizeFrame(left)) === stableStringify(normalizeFrame(right)); +} + +function makeSignal(): { + readonly promise: Promise; + readonly resolve: () => void; +} { + let resolve = () => {}; + const promise = new Promise((onResolve) => { + resolve = onResolve; + }); + return { promise, resolve }; +} + +function replayRunnerError( + transcript: CursorAgentSdkReplayTranscript, + cause: unknown, + method: string, +): CursorAgentSdkRunnerError { + if (isCursorAgentSdkRunnerError(cause)) { + return cause; + } + return new CursorAgentSdkRunnerError({ + method, + cause: isCursorAgentSdkReplayError(cause) + ? cause + : new CursorReplayRuntimeError({ + scenario: transcript.scenario, + cursor: 0, + cause, + }), + }); +} + +export function makeCursorAgentSdkReplayRunner( + transcript: CursorAgentSdkReplayTranscript, +): CursorAgentSdkRunnerShape { + let cursor = 0; + let failure: CursorAgentSdkReplayError | null = null; + let cursorAdvanced = makeSignal(); + + const recordFailure = (error: Error): Error => { + failure = error; + return error; + }; + + const fail = (error: CursorAgentSdkReplayError): never => { + throw recordFailure(error); + }; + + const advance = () => { + cursor += 1; + const signal = cursorAdvanced; + cursorAdvanced = makeSignal(); + signal.resolve(); + }; + + const assertOutbound = (actual: CursorOutgoingFrame) => { + if (failure !== null) { + throw failure; + } + const entry = transcript.entries[cursor]; + if (entry === undefined) { + return fail( + new CursorReplayExhaustedError({ + scenario: transcript.scenario, + cursor, + actual, + }), + ); + } + if (entry.type !== "expect_outbound" || !sameFrame(entry.frame, actual)) { + return fail( + new CursorReplayFrameMismatchError({ + scenario: transcript.scenario, + cursor, + expected: entry.type === "expect_outbound" ? entry.frame : entry, + actual, + }), + ); + } + advance(); + }; + + const consumeInbound = ( + type: Type, + ): Extract => { + const entry = transcript.entries[cursor]; + if ( + entry === undefined || + entry.type !== "emit_inbound" || + typeof entry.frame !== "object" || + entry.frame === null || + Reflect.get(entry.frame, "type") !== type + ) { + return fail( + new CursorReplayFrameMismatchError({ + scenario: transcript.scenario, + cursor, + expected: { type }, + actual: entry, + }), + ); + } + const frame = entry.frame as Extract; + advance(); + return frame; + }; + + const waitForRun = ( + runId: string, + sendInput: CursorAgentSdkSendInput, + ): Effect.Effect => + Effect.gen(function* () { + while (true) { + if (failure !== null) { + return yield* failure; + } + const entry = transcript.entries[cursor]; + if (entry === undefined) { + return yield* recordFailure( + new CursorReplayExhaustedError({ + scenario: transcript.scenario, + cursor, + actual: { type: "run.completed", runId }, + }), + ); + } + if (entry.type === "expect_outbound") { + const signal = cursorAdvanced; + yield* Effect.promise(() => signal.promise); + continue; + } + if (entry.type === "runtime_exit") { + advance(); + if (entry.status === "success") { + continue; + } + return yield* recordFailure( + new CursorReplayRuntimeError({ + scenario: transcript.scenario, + cursor: cursor - 1, + cause: entry.error ?? entry.status, + }), + ); + } + if ( + typeof entry.frame !== "object" || + entry.frame === null || + typeof Reflect.get(entry.frame, "type") !== "string" + ) { + return yield* recordFailure( + new CursorReplayFrameMismatchError({ + scenario: transcript.scenario, + cursor, + expected: { type: "interaction.update | run.completed" }, + actual: entry.frame, + }), + ); + } + const frame = entry.frame as CursorProtocolPayload; + if (frame.type === "interaction.update") { + if (frame.runId !== runId) { + return yield* recordFailure( + new CursorReplayFrameMismatchError({ + scenario: transcript.scenario, + cursor, + expected: { runId }, + actual: frame, + }), + ); + } + advance(); + yield* (sendInput.onDelta?.(frame.update) ?? Effect.void).pipe( + Effect.mapError((cause) => + recordFailure( + new CursorReplayRuntimeError({ + scenario: transcript.scenario, + cursor: cursor - 1, + cause, + }), + ), + ), + ); + yield* Effect.yieldNow; + continue; + } + if (frame.type === "run.completed") { + if (frame.result.id !== runId) { + return yield* recordFailure( + new CursorReplayFrameMismatchError({ + scenario: transcript.scenario, + cursor, + expected: { runId }, + actual: frame.result, + }), + ); + } + advance(); + return frame.result; + } + return yield* recordFailure( + new CursorReplayFrameMismatchError({ + scenario: transcript.scenario, + cursor, + expected: { type: "interaction.update | run.completed" }, + actual: frame, + }), + ); + } + }); + + return { + open: (input: CursorAgentSdkOpenInput) => + Effect.try({ + try: () => { + assertOutbound({ + type: "agent.open", + operation: input.operation, + ...(input.agentId === undefined ? {} : { agentId: input.agentId }), + options: loggedCursorAgentOptions(input.options), + }); + const opened = consumeInbound("agent.opened"); + const session: CursorAgentSdkSession = { + agentId: opened.agentId, + send: (sendInput) => + Effect.try({ + try: () => { + assertOutbound({ + type: "run.start", + message: sendInput.message, + options: loggedCursorSendOptions(sendInput.options), + }); + const started = consumeInbound("run.started"); + const run: CursorAgentSdkRun = { + runId: started.runId, + agentId: started.agentId, + wait: waitForRun(started.runId, sendInput).pipe( + Effect.mapError((cause) => + replayRunnerError(transcript, cause, "replay.run.wait"), + ), + ), + cancel: Effect.try({ + try: () => + assertOutbound({ + type: "run.cancel", + runId: started.runId, + }), + catch: (cause) => replayRunnerError(transcript, cause, "replay.run.cancel"), + }), + }; + return run; + }, + catch: (cause) => replayRunnerError(transcript, cause, "replay.session.send"), + }), + listMessages: Effect.try({ + try: () => { + assertOutbound({ + type: "agent.messages.list", + agentId: opened.agentId, + }); + return consumeInbound("agent.messages").messages; + }, + catch: (cause) => replayRunnerError(transcript, cause, "replay.agent.messages.list"), + }), + close: Effect.try({ + try: () => + assertOutbound({ + type: "agent.close", + agentId: opened.agentId, + }), + catch: (cause) => replayRunnerError(transcript, cause, "replay.agent.close"), + }), + }; + return session; + }, + catch: (cause) => replayRunnerError(transcript, cause, "replay.agent.open"), + }), + assertComplete: Effect.try({ + try: () => { + if (failure !== null) { + throw failure; + } + if (cursor !== transcript.entries.length) { + throw new CursorReplayIncompleteError({ + scenario: transcript.scenario, + cursor, + remaining: transcript.entries.length - cursor, + }); + } + }, + catch: (cause) => replayRunnerError(transcript, cause, "replay.assertComplete"), + }), + }; +} + +export function makeCursorAgentSdkReplayLayer( + transcript: CursorAgentSdkReplayTranscript, + options?: { + readonly runner?: CursorAgentSdkRunnerShape; + readonly assertCompleteOnFinalize?: boolean; + }, +): Layer.Layer { + const runner = options?.runner ?? makeCursorAgentSdkReplayRunner(transcript); + return Layer.effect( + CursorAgentSdkRunner, + Effect.gen(function* () { + yield* Effect.addFinalizer(() => + options?.assertCompleteOnFinalize === false + ? Effect.void + : runner.assertComplete.pipe(Effect.orDie), + ); + return CursorAgentSdkRunner.of(runner); + }), + ); +} + +function makeReplayServerConfig( + scenario: string, +): Effect.Effect< + ServerConfig["Service"], + PlatformError.PlatformError, + FileSystem.FileSystem | Path.Path +> { + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = yield* fs.makeTempDirectory({ + prefix: `t3-orchestration-v2-cursor-${scenario}-`, + }); + const stateDir = path.join(baseDir, "userdata"); + const logsDir = path.join(stateDir, "logs"); + const providerLogsDir = path.join(logsDir, "provider"); + const terminalLogsDir = path.join(logsDir, "terminals"); + const attachmentsDir = path.join(stateDir, "attachments"); + const worktreesDir = path.join(baseDir, "worktrees"); + const providerStatusCacheDir = path.join(baseDir, "caches"); + for (const directory of [ + stateDir, + logsDir, + providerLogsDir, + terminalLogsDir, + attachmentsDir, + worktreesDir, + providerStatusCacheDir, + ]) { + yield* fs.makeDirectory(directory, { recursive: true }); + } + return { + logLevel: "Error", + traceMinLevel: "Info", + traceTimingEnabled: true, + traceBatchWindowMs: 200, + traceMaxBytes: 10 * 1024 * 1024, + traceMaxFiles: 10, + otlpTracesUrl: undefined, + otlpMetricsUrl: undefined, + otlpExportIntervalMs: 10_000, + otlpServiceName: "t3-server", + mode: "web", + port: 0, + host: undefined, + cwd: process.cwd(), + baseDir, + staticDir: undefined, + devUrl: undefined, + noBrowser: false, + startupPresentation: "browser", + tailscaleServeEnabled: false, + tailscaleServePort: 443, + desktopBootstrapToken: undefined, + autoBootstrapProjectFromCwd: false, + logWebSocketEvents: false, + stateDir, + dbPath: path.join(stateDir, "state.sqlite"), + keybindingsConfigPath: path.join(stateDir, "keybindings.json"), + settingsPath: path.join(stateDir, "settings.json"), + providerStatusCacheDir, + worktreesDir, + attachmentsDir, + logsDir, + serverLogPath: path.join(logsDir, "server.log"), + serverTracePath: path.join(logsDir, "server.trace.ndjson"), + providerLogsDir, + providerEventLogPath: path.join(providerLogsDir, "events.log"), + terminalLogsDir, + anonymousIdPath: path.join(stateDir, "anonymous-id"), + environmentIdPath: path.join(stateDir, "environment-id"), + serverRuntimeStatePath: path.join(stateDir, "server-runtime.json"), + secretsDir: path.join(stateDir, "secrets"), + }; + }); +} + +export function makeCursorProviderAdapterRegistryReplayLayer( + transcript: CursorAgentSdkReplayTranscript, + options?: { + readonly runner?: CursorAgentSdkRunnerShape; + readonly assertCompleteOnFinalize?: boolean; + }, +) { + const serverConfigLayer = Layer.effect( + ServerConfig, + makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie), + ).pipe(Layer.provide(NodeServices.layer)); + return makeProviderAdapterRegistryDriverLayer({ + drivers: [CursorAdapterV2Driver], + configMap: { + [CURSOR_DEFAULT_INSTANCE_ID]: { + driver: CURSOR_DRIVER_KIND, + }, + }, + }).pipe( + Layer.provide( + Layer.mergeAll( + makeCursorAgentSdkReplayLayer(transcript, options), + serverConfigLayer, + NodeServices.layer, + idAllocatorLayer, + ), + ), + ); +} + +function metadataFromTranscript(transcript: ProviderReplayTranscript): { + readonly provider?: string; + readonly protocol?: string; + readonly scenario?: string; +} { + return { + provider: transcript.provider, + protocol: transcript.protocol, + scenario: transcript.scenario, + }; +} + +export const CursorOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness< + CursorAgentSdkReplayTranscript, + CursorOrchestratorReplayHarnessError +> = { + driver: CURSOR_PROVIDER, + decodeTranscript: (transcript) => + decodeCursorAgentSdkReplayTranscript(transcript).pipe( + Effect.mapError( + (cause) => + new CursorReplayTranscriptDecodeError({ + ...metadataFromTranscript(transcript), + cause, + }), + ), + ), + makeProviderAdapterRegistryLayer: (transcript) => + makeCursorProviderAdapterRegistryReplayLayer(transcript), +}; + +function sanitizeReplayValue( + value: unknown, + replacements: ReadonlyArray, +): unknown { + if (typeof value === "string") { + return replacements.reduce( + (text, [from, to]) => (from.length === 0 ? text : text.replaceAll(from, to)), + value, + ); + } + if (Array.isArray(value)) { + return value.map((entry) => sanitizeReplayValue(entry, replacements)); + } + if (typeof value !== "object" || value === null) { + return value; + } + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [key, sanitizeReplayValue(entry, replacements)]), + ); +} + +function serializeCursorRecordingError(cause: unknown): unknown { + if (typeof cause !== "object" || cause === null) { + return cause; + } + return { + name: typeof Reflect.get(cause, "name") === "string" ? Reflect.get(cause, "name") : "Error", + message: + typeof Reflect.get(cause, "message") === "string" + ? Reflect.get(cause, "message") + : String(cause), + }; +} + +function makeRecordingSignal(): { + readonly promise: Promise; + readonly resolve: () => void; +} { + let resolve = () => {}; + const promise = new Promise((onResolve) => { + resolve = onResolve; + }); + return { promise, resolve }; +} + +async function waitForRecordingSignal(signal: Promise, description: string): Promise { + const controller = new AbortController(); + const timeout = Effect.runPromise( + Effect.sleep("30 seconds").pipe( + Effect.andThen(Effect.die(`Timed out waiting for ${description}.`)), + ), + { signal: controller.signal }, + ); + try { + await Promise.race([signal, timeout]); + } finally { + controller.abort(); + } +} + +function recordingRuntimePolicy(input: { + readonly cwd: string; + readonly interactionMode: "default" | "plan"; +}): ProviderAdapterV2RuntimePolicy { + return { + runtimeMode: "full-access", + interactionMode: input.interactionMode, + cwd: input.cwd, + approvalPolicy: "never", + sandboxPolicy: { + type: "dangerFullAccess", + networkAccess: true, + }, + }; +} + +export async function recordCursorAgentSdkReplayTranscript(input: { + readonly scenario: string; + readonly prompts: ReadonlyArray; + readonly modelSelection: ModelSelection; + readonly cwd: string; + readonly interactionMode?: "default" | "plan"; + readonly apiKey?: string; + readonly interruptAfterToolStart?: boolean; + readonly interruptAfterRunStartPromptIndex?: number; + readonly restartBeforePromptIndex?: number; +}): Promise { + if (input.interruptAfterToolStart === true && input.prompts.length !== 1) { + throw new Error("Cursor interrupt recordings require exactly one prompt."); + } + if ( + input.interruptAfterToolStart === true && + input.interruptAfterRunStartPromptIndex !== undefined + ) { + throw new Error("Cursor recordings cannot use both interrupt triggers."); + } + if ( + input.interruptAfterRunStartPromptIndex !== undefined && + (input.interruptAfterRunStartPromptIndex < 0 || + input.interruptAfterRunStartPromptIndex >= input.prompts.length) + ) { + throw new Error("Cursor interrupt prompt index is outside the prompt list."); + } + const entries: Array = []; + const interactionMode = input.interactionMode ?? "default"; + const runtimePolicy = recordingRuntimePolicy({ + cwd: input.cwd, + interactionMode, + }); + const options = makeCursorAgentOptions({ + ...(input.apiKey === undefined ? {} : { apiKey: input.apiKey }), + modelSelection: input.modelSelection, + runtimePolicy, + threadId: "thread:cursor-replay" as ThreadId, + }); + entries.push({ + type: "expect_outbound", + label: "agent.open", + frame: { + type: "agent.open", + operation: "create", + options: loggedCursorAgentOptions(options), + }, + }); + + let agent = await Agent.create(options); + const nativeAgentId = agent.agentId; + entries.push({ + type: "emit_inbound", + label: "agent.opened", + frame: { + type: "agent.opened", + agentId: agent.agentId, + }, + }); + + const replacements: ReadonlyArray = [ + [input.cwd, `/tmp/cursor-replay-${input.scenario}`], + ]; + + try { + for (const [index, prompt] of input.prompts.entries()) { + if (input.restartBeforePromptIndex === index) { + entries.push({ + type: "expect_outbound", + label: `agent.close:before-prompt-${index + 1}`, + frame: { + type: "agent.close", + agentId: nativeAgentId, + }, + }); + agent.close(); + entries.push({ + type: "expect_outbound", + label: `agent.resume:before-prompt-${index + 1}`, + frame: { + type: "agent.open", + operation: "resume", + agentId: nativeAgentId, + options: loggedCursorAgentOptions(options), + }, + }); + agent = await Agent.resume(nativeAgentId, options); + entries.push({ + type: "emit_inbound", + label: `agent.resumed:before-prompt-${index + 1}`, + frame: { + type: "agent.opened", + agentId: agent.agentId, + }, + }); + } + const sendOptions = { + model: cursorSdkModelSelection(input.modelSelection), + mode: interactionMode === "plan" ? "plan" : "agent", + } as const; + entries.push({ + type: "expect_outbound", + label: `run.start:${index + 1}`, + frame: { + type: "run.start", + message: prompt, + options: loggedCursorSendOptions(sendOptions), + }, + }); + const pendingUpdates: Array = []; + let runReady = false; + let runId = ""; + let updatesPaused = false; + const toolStarted = makeRecordingSignal(); + const runActivityStarted = makeRecordingSignal(); + const resumeUpdates = makeRecordingSignal(); + let interruptTriggerObserved = false; + let callbackChain = Promise.resolve(); + const recordUpdate = async (update: InteractionUpdate) => { + if (updatesPaused) { + await resumeUpdates.promise; + } + entries.push({ + type: "emit_inbound", + label: update.type, + frame: { + type: "interaction.update", + runId, + update: sanitizeReplayValue(update, replacements) as InteractionUpdate, + }, + }); + if ( + input.interruptAfterToolStart === true && + !interruptTriggerObserved && + update.type === "tool-call-started" + ) { + interruptTriggerObserved = true; + updatesPaused = true; + toolStarted.resolve(); + } + }; + const scheduleUpdate = (update: InteractionUpdate): Promise => { + callbackChain = callbackChain.then(() => recordUpdate(update)); + return callbackChain; + }; + const run = await agent.send(prompt, { + ...sendOptions, + onDelta: async ({ update }) => { + runActivityStarted.resolve(); + if (!runReady) { + pendingUpdates.push(update); + return; + } + await scheduleUpdate(update); + }, + }); + runId = run.id; + entries.push({ + type: "emit_inbound", + label: `run.started:${index + 1}`, + frame: { + type: "run.started", + runId: run.id, + agentId: run.agentId, + }, + }); + runReady = true; + for (const update of pendingUpdates) { + void scheduleUpdate(update); + } + const resultPromise = run.wait().then( + (result) => ({ type: "success" as const, result }), + (cause: unknown) => ({ type: "failure" as const, cause }), + ); + if (input.interruptAfterRunStartPromptIndex === index) { + await waitForRecordingSignal( + runActivityStarted.promise, + "Cursor SDK run activity before interrupt", + ); + entries.push({ + type: "expect_outbound", + label: `run.cancel:${index + 1}`, + frame: { + type: "run.cancel", + runId: run.id, + }, + }); + await run.cancel().catch((cause: unknown) => { + if (!isCursorCancellationError(cause)) { + throw cause; + } + }); + } + if (input.interruptAfterToolStart === true) { + await waitForRecordingSignal( + toolStarted.promise, + "Cursor SDK tool-call-started before interrupt", + ); + entries.push({ + type: "expect_outbound", + label: `run.cancel:${index + 1}`, + frame: { + type: "run.cancel", + runId: run.id, + }, + }); + const cancelPromise = run.cancel().catch((cause: unknown) => { + if (!isCursorCancellationError(cause)) { + throw cause; + } + }); + updatesPaused = false; + resumeUpdates.resolve(); + await cancelPromise; + } + const outcome = await resultPromise; + await callbackChain; + if (outcome.type === "success") { + entries.push({ + type: "emit_inbound", + label: `run.completed:${index + 1}`, + frame: { + type: "run.completed", + result: sanitizeReplayValue(outcome.result, replacements) as RunResult, + }, + }); + } else if ( + (input.interruptAfterToolStart === true || + input.interruptAfterRunStartPromptIndex === index) && + isCursorCancellationError(outcome.cause) + ) { + entries.push({ + type: "runtime_exit", + status: "cancelled", + error: serializeCursorRecordingError(outcome.cause), + }); + } else { + throw outcome.cause; + } + } + } finally { + entries.push({ + type: "expect_outbound", + label: "agent.close", + frame: { + type: "agent.close", + agentId: nativeAgentId, + }, + }); + agent.close(); + } + + return { + provider: CURSOR_PROVIDER, + protocol: CURSOR_AGENT_SDK_PROTOCOL, + version: "1", + scenario: input.scenario, + metadata: { + generatedBy: "recordCursorAgentSdkReplayTranscript", + nativeAgentId, + }, + entries, + }; +} diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts new file mode 100644 index 00000000000..eff2a11c4d7 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts @@ -0,0 +1,2508 @@ +import type { + AgentMessage, + AgentOptions, + InteractionUpdate, + McpServerConfig, + ModelSelection as CursorSdkModelSelection, + RunResult, + SDKUserMessage, + ToolCall, +} from "@cursor/sdk"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { + CursorSettings, + defaultInstanceIdForDriver, + type ChatAttachment, + type ModelSelection, + type OrchestrationV2ConversationMessage, + type OrchestrationV2ExecutionNode, + type OrchestrationV2PlanArtifact, + type OrchestrationV2PlanStep, + type OrchestrationV2ProviderCapabilities, + type OrchestrationV2ProviderSession, + type OrchestrationV2ProviderThread, + type OrchestrationV2ProviderTurn, + type OrchestrationV2Subagent, + type OrchestrationV2TurnItem, + type ProviderInstanceId, + type ThreadId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; + +import { resolveAttachmentPath } from "../../attachmentStore.ts"; +import { ServerConfig } from "../../config.ts"; +import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; +import { cursorSdkParameterId } from "../../provider/cursorSdkModel.ts"; +import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; +import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts"; +import { + ProviderAdapterEnsureThreadError, + ProviderAdapterForkThreadError, + ProviderAdapterInterruptError, + ProviderAdapterOpenSessionError, + ProviderAdapterProtocolError, + ProviderAdapterReadThreadSnapshotError, + ProviderAdapterResumeThreadError, + ProviderAdapterRollbackThreadError, + ProviderAdapterRuntimeRequestResponseError, + ProviderAdapterSteerRunUnsupportedError, + ProviderAdapterTurnStartError, + ProviderAdapterV2, + type ProviderAdapterV2EnsureThreadInput, + type ProviderAdapterV2Event, + type ProviderAdapterV2InterruptInput, + type ProviderAdapterV2OpenSessionInput, + type ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2SessionRuntime, + type ProviderAdapterV2Shape, + type ProviderAdapterV2TurnInput, +} from "../ProviderAdapter.ts"; +import { + ProviderAdapterDriverCreateError, + type ProviderAdapterDriver, + type ProviderAdapterDriverCreateInput, +} from "../ProviderAdapterDriver.ts"; +import { + makeSubagentChildThread, + makeSubagentConversationArtifacts, + subagentThreadTitle, +} from "../SubagentProjection.ts"; +import { + CURSOR_PROVIDER, + CursorAgentSdkRunner, + type CursorAgentSdkRun, + type CursorAgentSdkRunnerShape, + type CursorAgentSdkSession, +} from "./CursorAgentSdk.ts"; + +export { CURSOR_PROVIDER } from "./CursorAgentSdk.ts"; + +export const CURSOR_DRIVER_KIND = CURSOR_PROVIDER; +export const CURSOR_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(CURSOR_DRIVER_KIND); +const DEFAULT_CURSOR_SETTINGS = Schema.decodeSync(CursorSettings)({}); + +export const CursorProviderCapabilitiesV2 = { + sessions: { + supportsMultipleProviderThreadsPerSession: false, + supportsModelSwitchInSession: true, + supportsProviderSwitchingViaHandoff: true, + supportsRuntimeModeSwitchInSession: false, + pendingRequestsSurviveRestart: false, + }, + threads: { + canCreateEmptyThread: true, + canReadThreadSnapshot: true, + canRollbackThread: false, + canForkThread: false, + canForkFromTurn: false, + canForkFromSubagentThread: false, + exposesNativeThreadId: true, + }, + turns: { + exposesNativeTurnId: true, + emitsTurnStarted: true, + emitsTurnCompleted: true, + supportsInterrupt: true, + supportsActiveSteering: false, + supportsSteeringByInterruptRestart: true, + supportsQueuedMessages: true, + terminalStatusQuality: "strong", + }, + streaming: { + streamsAssistantText: true, + streamsReasoning: true, + streamsToolOutput: true, + streamsPlanText: true, + emitsMessageCompleted: true, + }, + tools: { + exposesToolItemIds: true, + emitsToolStarted: true, + emitsToolCompleted: true, + emitsToolOutput: true, + supportsMcpTools: true, + supportsDynamicToolCallbacks: false, + }, + approvals: { + supportsCommandApproval: false, + supportsFileReadApproval: false, + supportsFileChangeApproval: false, + supportsApplyPatchApproval: false, + approvalsHaveNativeRequestIds: false, + approvalCallbacksAreLiveOnly: false, + approvalsCanOriginateFromSubagents: false, + }, + planning: { + emitsPlanUpdated: true, + emitsTodoList: true, + emitsProposedPlan: true, + supportsStructuredQuestions: false, + planDeltasHaveItemIds: true, + }, + subagents: { + supportsSubagents: true, + exposesSubagentThreadIds: false, + emitsSubagentLifecycle: true, + canWaitForSubagents: true, + canCloseSubagents: false, + canForkSubagentThread: false, + }, + context: { + acceptsSystemContext: false, + acceptsDeveloperContext: false, + acceptsSyntheticUserContext: true, + canGenerateSummaries: true, + canConsumeHandoffSummaries: true, + supportsDeltaHandoff: true, + supportsFullThreadHandoff: true, + maxRecommendedHandoffChars: null, + }, + checkpointing: { + appCanCheckpointFilesystem: true, + supportsNestedCheckpointScopes: true, + providerCanRollbackConversation: false, + providerRollbackReturnsSnapshot: false, + providerCanReadConversationSnapshot: true, + }, + identity: { + nativeThreadIds: "strong", + nativeTurnIds: "strong", + nativeItemIds: "weak", + nativeRequestIds: "none", + }, +} satisfies OrchestrationV2ProviderCapabilities; + +export interface CursorRuntimeAgentPolicy { + readonly autoReview: boolean; + readonly sandboxEnabled: boolean; +} + +export function cursorRuntimeAgentPolicy( + runtimePolicy: ProviderAdapterV2RuntimePolicy, +): CursorRuntimeAgentPolicy { + const sandboxPolicyType = + typeof runtimePolicy.sandboxPolicy === "object" && + runtimePolicy.sandboxPolicy !== null && + "type" in runtimePolicy.sandboxPolicy && + typeof runtimePolicy.sandboxPolicy.type === "string" + ? runtimePolicy.sandboxPolicy.type + : undefined; + return { + autoReview: + runtimePolicy.approvalPolicy === undefined + ? runtimePolicy.runtimeMode === "approval-required" + : runtimePolicy.approvalPolicy !== "never", + sandboxEnabled: + runtimePolicy.sandboxPolicy === undefined + ? runtimePolicy.runtimeMode !== "full-access" + : sandboxPolicyType !== "dangerFullAccess", + }; +} + +export function cursorSdkModelSelection(modelSelection: ModelSelection): CursorSdkModelSelection { + return { + id: modelSelection.model === "auto" ? "default" : modelSelection.model, + ...(modelSelection.options === undefined || modelSelection.options.length === 0 + ? {} + : { + params: modelSelection.options.map((option) => ({ + id: cursorSdkParameterId(option.id), + value: String(option.value), + })), + }), + }; +} + +export function cursorMcpServers(threadId: ThreadId): Record | undefined { + const session = McpProviderSession.readMcpProviderSession(threadId); + if (session === undefined) { + return undefined; + } + return { + "t3-code": { + type: "http", + url: session.endpoint, + headers: { + Authorization: session.authorizationHeader, + }, + }, + }; +} + +function providerSession(input: { + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; + readonly providerInstanceId: ProviderInstanceId; + readonly cwd: string | null; + readonly model: string; + readonly now: DateTime.Utc; +}): OrchestrationV2ProviderSession { + return { + id: input.providerSessionId, + driver: CURSOR_PROVIDER, + providerInstanceId: input.providerInstanceId, + status: "ready", + cwd: input.cwd ?? process.cwd(), + model: input.model, + capabilities: CursorProviderCapabilitiesV2, + createdAt: input.now, + updatedAt: input.now, + lastError: null, + }; +} + +function makeProviderThread(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly providerInstanceId: ProviderInstanceId; + readonly appThreadId: OrchestrationV2ProviderThread["appThreadId"]; + readonly providerSessionId: OrchestrationV2ProviderThread["providerSessionId"]; + readonly nativeThreadId: string; + readonly ownerNodeId?: OrchestrationV2ProviderThread["ownerNodeId"]; + readonly forkedFrom?: OrchestrationV2ProviderThread["forkedFrom"]; + readonly now: DateTime.Utc; +}): OrchestrationV2ProviderThread { + return { + id: input.idAllocator.derive.providerThread({ + driver: CURSOR_PROVIDER, + nativeThreadId: input.nativeThreadId, + }), + driver: CURSOR_PROVIDER, + providerInstanceId: input.providerInstanceId, + providerSessionId: input.providerSessionId, + appThreadId: input.appThreadId, + ownerNodeId: input.ownerNodeId ?? null, + nativeThreadRef: { + driver: CURSOR_PROVIDER, + nativeId: input.nativeThreadId, + strength: "strong", + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: input.forkedFrom ?? null, + createdAt: input.now, + updatedAt: input.now, + }; +} + +function nativeThreadId(providerThread: OrchestrationV2ProviderThread): string { + const id = providerThread.nativeThreadRef?.nativeId; + if (id === null || id === undefined) { + throw new ProviderAdapterProtocolError({ + driver: CURSOR_PROVIDER, + detail: `Provider thread ${providerThread.id} is missing its Cursor agent id.`, + }); + } + return id; +} + +export function makeCursorAgentOptions(input: { + readonly apiKey?: string; + readonly modelSelection: ModelSelection; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; + readonly threadId: ThreadId; +}): AgentOptions { + const policy = cursorRuntimeAgentPolicy(input.runtimePolicy); + const mcpServers = cursorMcpServers(input.threadId); + return { + model: cursorSdkModelSelection(input.modelSelection), + name: `T3 Code ${input.threadId}`, + mode: input.runtimePolicy.interactionMode === "plan" ? "plan" : "agent", + ...(input.apiKey === undefined ? {} : { apiKey: input.apiKey }), + local: { + ...(input.runtimePolicy.cwd === null ? {} : { cwd: input.runtimePolicy.cwd }), + autoReview: policy.autoReview, + sandboxOptions: { + enabled: policy.sandboxEnabled, + }, + enableAgentRetries: true, + }, + ...(mcpServers === undefined ? {} : { mcpServers }), + }; +} + +function stableJson(value: unknown): string { + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function unknownRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +function nestedString(value: unknown, keys: ReadonlyArray): string | undefined { + let current: unknown = value; + for (const key of keys) { + current = unknownRecord(current)?.[key]; + } + return typeof current === "string" ? current : undefined; +} + +function cursorToolFailed(toolCall: ToolCall): boolean { + if (toolCall.result?.status === "error") { + return true; + } + return toolCall.type === "mcp" && toolCall.result?.status === "success" + ? toolCall.result.value.isError + : false; +} + +function cursorToolOutput(toolCall: ToolCall): unknown { + const result = toolCall.result; + if (result === undefined) { + return undefined; + } + return result.status === "success" ? result.value : result.error; +} + +function cursorToolOutputText(toolCall: ToolCall): string { + if (toolCall.type === "shell" && toolCall.result?.status === "success") { + return [toolCall.result.value.stdout, toolCall.result.value.stderr] + .filter((part) => part.length > 0) + .join("\n"); + } + if (toolCall.type === "mcp" && toolCall.result?.status === "success") { + return toolCall.result.value.content + .flatMap((part) => (part.text === undefined ? [] : [part.text.text])) + .join("\n"); + } + const output = cursorToolOutput(toolCall); + if (output === undefined) { + return ""; + } + return typeof output === "string" ? output : stableJson(output); +} + +function cursorToolName(toolCall: ToolCall): string { + if (toolCall.type !== "mcp") { + return toolCall.type; + } + const provider = toolCall.args.providerIdentifier ?? "mcp"; + const tool = toolCall.args.toolName ?? "unknown"; + return `mcp__${provider}__${tool}`; +} + +function cursorToolFileName(toolCall: ToolCall): string { + switch (toolCall.type) { + case "write": + case "delete": + case "read": + case "edit": + case "ls": + return toolCall.args.path; + case "generateImage": + return toolCall.args.filePath ?? "generated-image"; + default: + return cursorToolName(toolCall); + } +} + +function cursorToolSearchPattern(toolCall: ToolCall): string | undefined { + switch (toolCall.type) { + case "glob": + return toolCall.args.globPattern; + case "grep": + return toolCall.args.pattern; + case "semSearch": + return toolCall.args.query; + case "read": + case "ls": + return toolCall.args.path; + case "readLints": + return toolCall.args.paths.join(", "); + default: + return undefined; + } +} + +function cursorToolSearchResults(toolCall: ToolCall): ReadonlyArray<{ + readonly fileName: string; + readonly line?: number; + readonly preview?: string; +}> { + if (toolCall.result?.status !== "success") { + return []; + } + switch (toolCall.type) { + case "read": + return [ + { + fileName: toolCall.args.path, + preview: toolCall.result.value.content, + }, + ]; + case "glob": + return toolCall.result.value.files.map((fileName) => ({ fileName })); + case "grep": + return Object.values(toolCall.result.value.workspaceResults ?? {}).flatMap((result) => { + if (result.type === "files") { + return result.output.files.map((fileName) => ({ fileName })); + } + if (result.type === "count") { + return result.output.counts.map((entry) => ({ + fileName: entry.file, + preview: `${entry.count} matches`, + })); + } + return result.output.matches.map((entry) => ({ + fileName: entry.file, + ...(entry.lineNumber === undefined ? {} : { line: entry.lineNumber }), + preview: entry.line, + })); + }); + case "semSearch": + return [ + { + fileName: "semantic-search", + preview: toolCall.result.value.results, + }, + ]; + default: + return []; + } +} + +function cursorTodoSteps( + toolCall: Extract, +): ReadonlyArray { + const todos = + toolCall.result?.status === "success" ? toolCall.result.value.todos : toolCall.args.todos; + return todos + .filter((todo) => todo.status !== "cancelled" && todo.content.trim().length > 0) + .map((todo, index) => ({ + id: `todo-${index + 1}`, + text: todo.content, + status: + todo.status === "inProgress" + ? "running" + : todo.status === "completed" + ? "completed" + : "pending", + })); +} + +function assistantTextsFromConversationSteps(steps: ReadonlyArray): ReadonlyArray { + return steps.flatMap((step) => { + const record = unknownRecord(step); + if (record?.type === "assistantMessage") { + const text = nestedString(record, ["message", "text"]); + return text === undefined || text.length === 0 ? [] : [text]; + } + const text = nestedString(record, ["assistantMessage", "text"]); + return text === undefined || text.length === 0 ? [] : [text]; + }); +} + +function nestedToolResult>( + rawResult: unknown, + mapSuccess: (success: Record) => Value = (success) => success as Value, +): + | { readonly status: "success"; readonly value: Value } + | { readonly status: "error"; readonly error: unknown } + | undefined { + const result = unknownRecord(rawResult); + if (result === undefined) { + return undefined; + } + const success = unknownRecord(result.success); + if (success !== undefined) { + return { + status: "success", + value: mapSuccess(success), + }; + } + return { + status: "error", + error: result.error ?? result.permissionDenied ?? result, + }; +} + +function nestedGrepWorkspaceResults(success: Record): Record { + const workspaces = unknownRecord(success.workspaceResults); + if (workspaces === undefined) { + return {}; + } + return Object.fromEntries( + Object.entries(workspaces).map(([workspace, rawWorkspaceResult]) => { + const workspaceResult = unknownRecord(rawWorkspaceResult); + const content = unknownRecord(workspaceResult?.content); + const rawMatches = Array.isArray(content?.matches) ? content.matches : []; + const matches = rawMatches.flatMap((rawFileMatch) => { + const fileMatch = unknownRecord(rawFileMatch); + const file = typeof fileMatch?.file === "string" ? fileMatch.file : ""; + const lineMatches = Array.isArray(fileMatch?.matches) ? fileMatch.matches : []; + return lineMatches.flatMap((rawLineMatch) => { + const lineMatch = unknownRecord(rawLineMatch); + if (lineMatch === undefined) { + return []; + } + const line = typeof lineMatch.content === "string" ? lineMatch.content : undefined; + if (file.length === 0 || line === undefined) { + return []; + } + return [ + { + file, + line, + ...(typeof lineMatch.lineNumber === "number" + ? { lineNumber: lineMatch.lineNumber } + : {}), + }, + ]; + }); + }); + return [ + workspace, + { + type: "content", + output: { + matches, + totalMatches: + typeof content?.totalMatchedLines === "number" + ? content.totalMatchedLines + : matches.length, + }, + }, + ]; + }), + ); +} + +function nestedToolCallFromEnvelope( + envelope: Record, +): { readonly callId: string; readonly toolCall: ToolCall } | undefined { + const callId = typeof envelope.toolCallId === "string" ? envelope.toolCallId : undefined; + const wrapperEntry = Object.entries(envelope).find(([key]) => key.endsWith("ToolCall")); + if (wrapperEntry === undefined) { + return undefined; + } + const [wrapperName, rawCall] = wrapperEntry; + const call = unknownRecord(rawCall); + if (call === undefined) { + return undefined; + } + const args = unknownRecord(call.args) ?? {}; + const fallbackCallId = `nested-${wrapperName}`; + const nestedCallId = callId ?? fallbackCallId; + + switch (wrapperName) { + case "readToolCall": { + const success = unknownRecord(unknownRecord(call.result)?.success); + const path = + typeof args.path === "string" + ? args.path + : typeof success?.path === "string" + ? success.path + : undefined; + if (path === undefined) { + return undefined; + } + return { + callId: nestedCallId, + toolCall: { + type: "read", + args: { path }, + result: nestedToolResult(call.result, (value) => ({ + content: typeof value.content === "string" ? value.content : "", + totalLines: typeof value.totalLines === "number" ? value.totalLines : 0, + fileSize: typeof value.fileSize === "number" ? value.fileSize : 0, + })), + } as unknown as ToolCall, + }; + } + case "globToolCall": + return { + callId: nestedCallId, + toolCall: { + type: "glob", + args: { + globPattern: typeof args.globPattern === "string" ? args.globPattern : "**/*", + ...(typeof args.targetDirectory === "string" + ? { targetDirectory: args.targetDirectory } + : {}), + }, + result: nestedToolResult(call.result, (value) => ({ + files: Array.isArray(value.files) + ? value.files.filter((file): file is string => typeof file === "string") + : [], + totalFiles: typeof value.totalFiles === "number" ? value.totalFiles : 0, + clientTruncated: false, + ripgrepTruncated: false, + })), + } as unknown as ToolCall, + }; + case "grepToolCall": + return { + callId: nestedCallId, + toolCall: { + type: "grep", + args: { + pattern: typeof args.pattern === "string" ? args.pattern : "", + ...(typeof args.path === "string" ? { path: args.path } : {}), + ...(typeof args.glob === "string" ? { glob: args.glob } : {}), + ...(typeof args.outputMode === "string" ? { outputMode: args.outputMode } : {}), + ...(typeof args.caseInsensitive === "boolean" + ? { caseInsensitive: args.caseInsensitive } + : {}), + ...(typeof args.offset === "number" ? { offset: args.offset } : {}), + ...(typeof args.multiline === "boolean" ? { multiline: args.multiline } : {}), + }, + result: nestedToolResult(call.result, (value) => ({ + workspaceResults: nestedGrepWorkspaceResults(value), + })), + } as unknown as ToolCall, + }; + case "shellToolCall": { + const rawResult = unknownRecord(call.result); + const permissionDenied = unknownRecord(rawResult?.permissionDenied); + const command = + typeof args.command === "string" + ? args.command + : typeof permissionDenied?.command === "string" + ? permissionDenied.command + : ""; + return { + callId: nestedCallId, + toolCall: { + type: "shell", + args: { command }, + result: nestedToolResult(call.result, (value) => ({ + stdout: typeof value.stdout === "string" ? value.stdout : "", + stderr: typeof value.stderr === "string" ? value.stderr : "", + exitCode: typeof value.exitCode === "number" ? value.exitCode : 0, + signal: typeof value.signal === "string" ? value.signal : "", + executionTime: typeof value.executionTime === "number" ? value.executionTime : 0, + })), + } as unknown as ToolCall, + }; + } + default: { + const type = wrapperName.slice(0, -"ToolCall".length); + return { + callId: nestedCallId, + toolCall: { + type, + args, + result: nestedToolResult(call.result), + } as unknown as ToolCall, + }; + } + } +} + +function toolCallsFromConversationSteps( + steps: ReadonlyArray, +): ReadonlyArray<{ readonly callId: string; readonly toolCall: ToolCall }> { + return steps.flatMap((step) => { + const record = unknownRecord(step); + if (record?.type === "toolCall") { + const message = unknownRecord(record.message); + return typeof message?.type === "string" + ? [ + { + callId: typeof record.callId === "string" ? record.callId : `nested-${message.type}`, + toolCall: message as ToolCall, + }, + ] + : []; + } + const envelope = unknownRecord(record?.toolCall); + const nested = envelope === undefined ? undefined : nestedToolCallFromEnvelope(envelope); + return nested === undefined ? [] : [nested]; + }); +} + +function textFromAgentMessage(message: AgentMessage): string { + const value = message.message; + if (typeof value === "string") { + return value; + } + const direct = + nestedString(value, ["text"]) ?? + nestedString(value, ["message", "text"]) ?? + nestedString(value, ["userMessage", "text"]) ?? + nestedString(value, ["assistantMessage", "text"]); + if (direct !== undefined) { + return direct; + } + const content = unknownRecord(value)?.content; + if (!Array.isArray(content)) { + return ""; + } + return content + .flatMap((part) => { + const text = unknownRecord(part)?.text; + return typeof text === "string" ? [text] : []; + }) + .join(""); +} + +interface CursorProjectionTarget { + readonly threadId: ThreadId; + readonly runId: ProviderAdapterV2TurnInput["runId"] | null; + readonly rootNodeId: OrchestrationV2ExecutionNode["rootNodeId"]; + readonly parentNodeId: OrchestrationV2ExecutionNode["id"]; + readonly providerThreadId: OrchestrationV2ProviderThread["id"] | null; + readonly providerTurnId: OrchestrationV2ProviderTurn["id"] | null; +} + +interface ActiveCursorToolCall { + readonly callId: string; + toolCall: ToolCall; + readonly target: CursorProjectionTarget; + readonly ordinal: number; + readonly startedAt: DateTime.Utc; + streamedOutput: string; +} + +interface ActiveCursorSubagent { + task: OrchestrationV2Subagent; + readonly callId: string; + readonly childThreadId: ThreadId; + readonly childRootNodeId: OrchestrationV2ExecutionNode["id"]; + readonly turnItemId: OrchestrationV2TurnItem["id"]; + readonly turnItemOrdinal: number; + nextChildOrdinal: number; + resultProjected: boolean; +} + +interface ActiveCursorTextSegment { + readonly nativeItemId: string; + readonly startedAt: DateTime.Utc; + text: string; +} + +interface ActiveCursorTextStream { + current: ActiveCursorTextSegment | null; + nextSegment: number; +} + +interface ActiveCursorTurn { + readonly input: ProviderAdapterV2TurnInput; + readonly run: CursorAgentSdkRun; + readonly providerTurnId: OrchestrationV2ProviderTurn["id"]; + readonly startedAt: DateTime.Utc; + readonly completed: Deferred.Deferred; + readonly tools: Map; + readonly subagents: Map; + readonly assistant: ActiveCursorTextStream; + readonly reasoning: ActiveCursorTextStream; + interrupted: boolean; + finalized: boolean; +} + +interface CursorLiveAgent { + readonly nativeThreadId: string; + readonly session: CursorAgentSdkSession; +} + +export interface CursorAdapterV2Options { + readonly instanceId: ProviderInstanceId; + readonly settings: CursorSettings; + readonly environment: NodeJS.ProcessEnv; + readonly fileSystem: FileSystem.FileSystem; + readonly idAllocator: IdAllocatorV2Shape; + readonly runner: CursorAgentSdkRunnerShape; + readonly serverConfig: ServerConfig["Service"]; +} + +export function makeCursorAdapterV2( + adapterOptions: CursorAdapterV2Options, +): ProviderAdapterV2Shape { + const { fileSystem, idAllocator, runner, serverConfig } = adapterOptions; + const apiKey = adapterOptions.environment.CURSOR_API_KEY?.trim() || undefined; + + return ProviderAdapterV2.of({ + instanceId: adapterOptions.instanceId, + driver: CURSOR_PROVIDER, + getCapabilities: () => Effect.succeed(CursorProviderCapabilitiesV2), + openSession: Effect.fn("CursorAdapterV2.openSession")( + function* (input: ProviderAdapterV2OpenSessionInput) { + const sessionScope = yield* Effect.scope; + const createdAt = yield* DateTime.now; + const session = providerSession({ + providerSessionId: input.providerSessionId, + providerInstanceId: adapterOptions.instanceId, + cwd: input.runtimePolicy.cwd, + model: input.modelSelection.model, + now: createdAt, + }); + const events = yield* Queue.unbounded(); + const liveAgent = yield* Ref.make(null); + const activeTurn = yield* Ref.make(null); + const itemOrdinals = yield* Ref.make(new Map()); + const nextItemOrdinalsByTurn = yield* Ref.make(new Map()); + const planIds = yield* Ref.make(new Map()); + + const emitProviderEvent = (event: ProviderAdapterV2Event) => + Queue.offer(events, event).pipe(Effect.asVoid); + + const resolveItemOrdinal = Effect.fnUntraced(function* ( + context: ActiveCursorTurn, + nativeItemId: string, + ) { + const existing = (yield* Ref.get(itemOrdinals)).get(nativeItemId); + if (existing !== undefined) { + return existing; + } + const nextWithinTurn = yield* Ref.modify(nextItemOrdinalsByTurn, (current) => { + const next = (current.get(context.run.runId) ?? 0) + 1; + const updated = new Map(current); + updated.set(context.run.runId, next); + return [next, updated]; + }); + const ordinal = context.input.runOrdinal * 100 + nextWithinTurn; + yield* Ref.update(itemOrdinals, (current) => { + const updated = new Map(current); + updated.set(nativeItemId, ordinal); + return updated; + }); + return ordinal; + }); + + const resolvePlanId = Effect.fnUntraced(function* ( + context: ActiveCursorTurn, + nativeItemId: string, + ) { + const existing = (yield* Ref.get(planIds)).get(nativeItemId); + if (existing !== undefined) { + return existing; + } + const planId = yield* idAllocator.allocate.plan({ + threadId: context.input.threadId, + runId: context.input.runId, + driver: CURSOR_PROVIDER, + }); + yield* Ref.update(planIds, (current) => { + const updated = new Map(current); + updated.set(nativeItemId, planId); + return updated; + }); + return planId; + }); + + const parentTarget = (context: ActiveCursorTurn): CursorProjectionTarget => ({ + threadId: context.input.threadId, + runId: context.input.runId, + rootNodeId: context.input.rootNodeId, + parentNodeId: context.input.rootNodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + }); + + const emitAssistant = Effect.fnUntraced(function* ( + context: ActiveCursorTurn, + completed: boolean, + ) { + const segment = context.assistant.current; + if (segment === null || segment.text.length === 0) { + return; + } + const now = yield* DateTime.now; + const ordinal = yield* resolveItemOrdinal(context, segment.nativeItemId); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: segment.nativeItemId, + }); + const messageId = idAllocator.derive.messageFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: segment.nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: segment.nativeItemId, + }); + const nativeItemRef = { + driver: CURSOR_PROVIDER, + nativeId: segment.nativeItemId, + strength: "weak" as const, + }; + yield* emitProviderEvent({ + type: "node.updated", + driver: CURSOR_PROVIDER, + node: { + id: nodeId, + threadId: context.input.threadId, + runId: context.input.runId, + parentNodeId: context.input.rootNodeId, + rootNodeId: context.input.rootNodeId, + kind: "assistant_message", + status: completed ? "completed" : "running", + countsForRun: false, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: segment.startedAt, + completedAt: completed ? now : null, + }, + }); + yield* emitProviderEvent({ + type: "message.updated", + driver: CURSOR_PROVIDER, + message: { + createdBy: "agent", + creationSource: "provider", + id: messageId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + role: "assistant", + text: segment.text, + attachments: [], + streaming: !completed, + createdAt: segment.startedAt, + updatedAt: now, + }, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CURSOR_PROVIDER, + turnItem: { + id: turnItemId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: completed ? "completed" : "running", + title: null, + startedAt: segment.startedAt, + completedAt: completed ? now : null, + updatedAt: now, + type: "assistant_message", + messageId, + text: segment.text, + streaming: !completed, + }, + }); + }); + + const emitReasoning = Effect.fnUntraced(function* ( + context: ActiveCursorTurn, + completed: boolean, + ) { + const segment = context.reasoning.current; + if (segment === null || segment.text.length === 0) { + return; + } + const now = yield* DateTime.now; + const ordinal = yield* resolveItemOrdinal(context, segment.nativeItemId); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: segment.nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: segment.nativeItemId, + }); + const nativeItemRef = { + driver: CURSOR_PROVIDER, + nativeId: segment.nativeItemId, + strength: "weak" as const, + }; + yield* emitProviderEvent({ + type: "node.updated", + driver: CURSOR_PROVIDER, + node: { + id: nodeId, + threadId: context.input.threadId, + runId: context.input.runId, + parentNodeId: context.input.rootNodeId, + rootNodeId: context.input.rootNodeId, + kind: "reasoning", + status: completed ? "completed" : "running", + countsForRun: false, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: segment.startedAt, + completedAt: completed ? now : null, + }, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CURSOR_PROVIDER, + turnItem: { + id: turnItemId, + threadId: context.input.threadId, + runId: context.input.runId, + nodeId, + providerThreadId: context.input.providerThread.id, + providerTurnId: context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: completed ? "completed" : "running", + title: null, + startedAt: segment.startedAt, + completedAt: completed ? now : null, + updatedAt: now, + type: "reasoning", + text: segment.text, + streaming: !completed, + }, + }); + }); + + const appendTextSegment = Effect.fnUntraced(function* (input: { + readonly context: ActiveCursorTurn; + readonly stream: ActiveCursorTextStream; + readonly kind: "assistant" | "reasoning"; + readonly text: string; + }) { + if (input.stream.current === null) { + input.stream.nextSegment += 1; + input.stream.current = { + nativeItemId: `${input.kind}:${input.context.run.runId}:${input.stream.nextSegment}`, + startedAt: yield* DateTime.now, + text: "", + }; + } + input.stream.current.text += input.text; + }); + + const completeAssistant = Effect.fnUntraced(function* (context: ActiveCursorTurn) { + if (context.assistant.current === null) { + return; + } + yield* emitAssistant(context, true); + context.assistant.current = null; + }); + + const completeReasoning = Effect.fnUntraced(function* (context: ActiveCursorTurn) { + if (context.reasoning.current === null) { + return; + } + yield* emitReasoning(context, true); + context.reasoning.current = null; + }); + + const emitToolArtifacts = Effect.fnUntraced(function* (input: { + readonly active: ActiveCursorToolCall; + readonly completed: boolean; + }) { + const { active } = input; + const toolCall = active.toolCall; + const now = yield* DateTime.now; + const failed = input.completed && cursorToolFailed(toolCall); + const status = input.completed ? (failed ? "failed" : "completed") : "running"; + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: active.callId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: active.callId, + }); + const nativeItemRef = { + driver: CURSOR_PROVIDER, + nativeId: active.callId, + strength: "strong" as const, + }; + const node: OrchestrationV2ExecutionNode = { + id: nodeId, + threadId: active.target.threadId, + runId: active.target.runId, + parentNodeId: active.target.parentNodeId, + rootNodeId: active.target.rootNodeId, + kind: "tool_call", + status, + countsForRun: false, + providerThreadId: active.target.providerThreadId, + providerTurnId: active.target.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: active.startedAt, + completedAt: input.completed ? now : null, + }; + const base = { + id: turnItemId, + threadId: active.target.threadId, + runId: active.target.runId, + nodeId, + providerThreadId: active.target.providerThreadId, + providerTurnId: active.target.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal: active.ordinal, + status, + title: null, + startedAt: active.startedAt, + completedAt: input.completed ? now : null, + updatedAt: now, + } satisfies Pick< + OrchestrationV2TurnItem, + | "id" + | "threadId" + | "runId" + | "nodeId" + | "providerThreadId" + | "providerTurnId" + | "nativeItemRef" + | "parentItemId" + | "ordinal" + | "status" + | "title" + | "startedAt" + | "completedAt" + | "updatedAt" + >; + const outputText = cursorToolOutputText(toolCall) || active.streamedOutput; + let turnItem: OrchestrationV2TurnItem; + switch (toolCall.type) { + case "shell": + turnItem = { + ...base, + type: "command_execution", + input: toolCall.args.command, + ...(outputText.length === 0 ? {} : { output: outputText }), + ...(toolCall.result?.status === "success" + ? { exitCode: toolCall.result.value.exitCode } + : {}), + }; + break; + case "write": + case "delete": + case "edit": + case "generateImage": + turnItem = { + ...base, + type: "file_change", + fileName: cursorToolFileName(toolCall), + ...(toolCall.type === "edit" && + toolCall.result?.status === "success" && + toolCall.result.value.linesAdded !== undefined + ? { additions: toolCall.result.value.linesAdded } + : {}), + ...(toolCall.type === "edit" && + toolCall.result?.status === "success" && + toolCall.result.value.linesRemoved !== undefined + ? { deletions: toolCall.result.value.linesRemoved } + : {}), + ...(toolCall.type === "edit" && + toolCall.result?.status === "success" && + toolCall.result.value.diffString !== undefined + ? { diffStr: toolCall.result.value.diffString } + : {}), + ...(toolCall.type === "write" ? { newStr: toolCall.args.fileText } : {}), + }; + break; + case "glob": + case "grep": + case "read": + case "ls": + case "readLints": + case "semSearch": { + const results = cursorToolSearchResults(toolCall); + turnItem = { + ...base, + type: "file_search", + ...(cursorToolSearchPattern(toolCall) === undefined + ? {} + : { pattern: cursorToolSearchPattern(toolCall) }), + ...(results.length === 0 ? {} : { results: [...results] }), + }; + break; + } + default: + turnItem = { + ...base, + type: "dynamic_tool", + toolName: cursorToolName(toolCall), + input: toolCall.args, + ...(cursorToolOutput(toolCall) === undefined + ? {} + : { output: cursorToolOutput(toolCall) }), + }; + } + yield* emitProviderEvent({ + type: "node.updated", + driver: CURSOR_PROVIDER, + node, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CURSOR_PROVIDER, + turnItem, + }); + }); + + const ensureToolStarted = Effect.fnUntraced(function* ( + context: ActiveCursorTurn, + callId: string, + toolCall: ToolCall, + ) { + const existing = context.tools.get(callId); + if (existing !== undefined) { + existing.toolCall = toolCall; + return existing; + } + const startedAt = yield* DateTime.now; + const active: ActiveCursorToolCall = { + callId, + toolCall, + target: parentTarget(context), + ordinal: yield* resolveItemOrdinal(context, callId), + startedAt, + streamedOutput: "", + }; + context.tools.set(callId, active); + yield* emitToolArtifacts({ active, completed: false }); + return active; + }); + + const emitPlanArtifacts = Effect.fnUntraced(function* (input: { + readonly context: ActiveCursorTurn; + readonly callId: string; + readonly markdown: string; + readonly completed: boolean; + readonly failed: boolean; + }) { + const nativeItemId = `plan:${input.callId}`; + const now = yield* DateTime.now; + const planId = yield* resolvePlanId(input.context, nativeItemId); + const ordinal = yield* resolveItemOrdinal(input.context, nativeItemId); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId, + }); + const status = input.failed ? "failed" : input.completed ? "completed" : "running"; + const nativeItemRef = { + driver: CURSOR_PROVIDER, + nativeId: input.callId, + strength: "strong" as const, + }; + const plan: OrchestrationV2PlanArtifact = { + id: planId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + nodeId, + kind: "proposed_plan", + status: input.completed ? "active" : "draft", + markdown: input.markdown, + }; + yield* emitProviderEvent({ + type: "node.updated", + driver: CURSOR_PROVIDER, + node: { + id: nodeId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + parentNodeId: input.context.input.rootNodeId, + rootNodeId: input.context.input.rootNodeId, + kind: "plan", + status, + countsForRun: false, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: input.context.startedAt, + completedAt: input.completed ? now : null, + }, + }); + yield* emitProviderEvent({ + type: "plan.updated", + driver: CURSOR_PROVIDER, + plan, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CURSOR_PROVIDER, + turnItem: { + id: turnItemId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + nodeId, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status, + title: null, + startedAt: input.context.startedAt, + completedAt: input.completed ? now : null, + updatedAt: now, + type: "proposed_plan", + planId, + markdown: input.markdown, + streaming: !input.completed, + }, + }); + }); + + const emitTodoArtifacts = Effect.fnUntraced(function* (input: { + readonly context: ActiveCursorTurn; + readonly callId: string; + readonly toolCall: Extract; + readonly completed: boolean; + readonly failed: boolean; + }) { + const nativeItemId = `todos:${input.callId}`; + const now = yield* DateTime.now; + const planId = yield* resolvePlanId(input.context, nativeItemId); + const ordinal = yield* resolveItemOrdinal(input.context, nativeItemId); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId, + }); + const status = input.failed ? "failed" : input.completed ? "completed" : "running"; + const steps = cursorTodoSteps(input.toolCall); + const nativeItemRef = { + driver: CURSOR_PROVIDER, + nativeId: input.callId, + strength: "strong" as const, + }; + yield* emitProviderEvent({ + type: "node.updated", + driver: CURSOR_PROVIDER, + node: { + id: nodeId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + parentNodeId: input.context.input.rootNodeId, + rootNodeId: input.context.input.rootNodeId, + kind: "todo_list", + status, + countsForRun: false, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: input.context.startedAt, + completedAt: input.completed ? now : null, + }, + }); + yield* emitProviderEvent({ + type: "plan.updated", + driver: CURSOR_PROVIDER, + plan: { + id: planId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + nodeId, + kind: "todo_list", + status: input.completed ? "active" : "draft", + steps: [...steps], + }, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CURSOR_PROVIDER, + turnItem: { + id: turnItemId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + nodeId, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status, + title: null, + startedAt: input.context.startedAt, + completedAt: input.completed ? now : null, + updatedAt: now, + type: "todo_list", + planId, + steps: [...steps], + }, + }); + }); + + const emitSubagent = Effect.fnUntraced(function* (input: { + readonly context: ActiveCursorTurn; + readonly callId: string; + readonly toolCall: Extract; + readonly completed: boolean; + }) { + const args = input.toolCall.args; + const result = + input.toolCall.result?.status === "success" ? input.toolCall.result.value : undefined; + const existing = input.context.subagents.get(input.callId); + const now = yield* DateTime.now; + const status: OrchestrationV2Subagent["status"] = input.completed + ? cursorToolFailed(input.toolCall) + ? "failed" + : "completed" + : "running"; + const resultText = [ + ...assistantTextsFromConversationSteps(result?.conversationSteps ?? []), + ...(result?.resultSuffix === undefined ? [] : [result.resultSuffix]), + ] + .filter((part) => part.trim().length > 0) + .join("\n"); + const nativeItemId = input.callId; + const nodeId = + existing?.task.id ?? + idAllocator.derive.nodeFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId, + }); + const childRootNodeId = + existing?.childRootNodeId ?? + idAllocator.derive.nodeFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: `${nativeItemId}:child-root`, + }); + const childThreadId = + existing?.childThreadId ?? + idAllocator.derive.threadFromProviderThread({ + driver: CURSOR_PROVIDER, + nativeThreadId: `${input.context.run.runId}:task:${input.callId}`, + }); + const task: OrchestrationV2Subagent = { + ...(existing?.task ?? { + id: nodeId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + parentNodeId: input.context.input.rootNodeId, + origin: "provider_native" as const, + createdBy: "agent" as const, + driver: CURSOR_PROVIDER, + providerInstanceId: input.context.input.modelSelection.instanceId, + providerThreadId: null, + childThreadId, + nativeTaskRef: { + driver: CURSOR_PROVIDER, + nativeId: input.callId, + strength: "strong" as const, + }, + prompt: args.prompt, + title: args.description, + model: args.model ?? input.context.input.modelSelection.model, + result: null, + startedAt: now, + }), + nativeTaskRef: { + driver: CURSOR_PROVIDER, + nativeId: input.callId, + strength: "strong" as const, + }, + status, + result: resultText.length === 0 ? (existing?.task.result ?? null) : resultText, + completedAt: input.completed ? now : null, + updatedAt: now, + }; + const subagent: ActiveCursorSubagent = { + task, + callId: input.callId, + childThreadId, + childRootNodeId, + turnItemId: + existing?.turnItemId ?? + idAllocator.derive.turnItemFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId, + }), + turnItemOrdinal: + existing?.turnItemOrdinal ?? (yield* resolveItemOrdinal(input.context, nativeItemId)), + nextChildOrdinal: existing?.nextChildOrdinal ?? 100, + resultProjected: existing?.resultProjected ?? false, + }; + input.context.subagents.set(input.callId, subagent); + + if (existing === undefined) { + yield* emitProviderEvent({ + type: "app_thread.created", + driver: CURSOR_PROVIDER, + appThread: makeSubagentChildThread({ + parentThread: input.context.input.appThread, + childThreadId, + parentNodeId: nodeId, + activeProviderThreadId: null, + providerInstanceId: input.context.input.modelSelection.instanceId, + modelSelection: input.context.input.modelSelection, + title: subagentThreadTitle({ + parentTitle: input.context.input.appThread.title, + title: args.description, + prompt: args.prompt, + ordinal: input.context.subagents.size, + }), + now, + createdBy: "agent", + creationSource: "provider", + }), + }); + const promptNativeId = `${nativeItemId}:prompt`; + const promptArtifacts = makeSubagentConversationArtifacts({ + messageId: idAllocator.derive.messageFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: promptNativeId, + }), + turnItemId: idAllocator.derive.turnItemFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: promptNativeId, + }), + threadId: childThreadId, + rootNodeId: childRootNodeId, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: { + driver: CURSOR_PROVIDER, + nativeId: promptNativeId, + strength: "weak", + }, + role: "user", + text: args.prompt, + ordinal: 100, + now, + }); + yield* emitProviderEvent({ + type: "message.updated", + driver: CURSOR_PROVIDER, + message: promptArtifacts.message, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CURSOR_PROVIDER, + turnItem: promptArtifacts.turnItem, + }); + } + + yield* emitProviderEvent({ + type: "node.updated", + driver: CURSOR_PROVIDER, + node: { + id: nodeId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + parentNodeId: input.context.input.rootNodeId, + rootNodeId: input.context.input.rootNodeId, + kind: "subagent", + status, + countsForRun: false, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: task.nativeTaskRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: task.startedAt, + completedAt: input.completed ? now : null, + }, + }); + yield* emitProviderEvent({ + type: "node.updated", + driver: CURSOR_PROVIDER, + node: { + id: childRootNodeId, + threadId: childThreadId, + runId: null, + parentNodeId: null, + rootNodeId: childRootNodeId, + kind: "root_turn", + status, + countsForRun: false, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: task.nativeTaskRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: task.startedAt, + completedAt: input.completed ? now : null, + }, + }); + yield* emitProviderEvent({ + type: "subagent.updated", + driver: CURSOR_PROVIDER, + subagent: task, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CURSOR_PROVIDER, + turnItem: { + id: subagent.turnItemId, + threadId: input.context.input.threadId, + runId: input.context.input.runId, + nodeId, + providerThreadId: input.context.input.providerThread.id, + providerTurnId: input.context.providerTurnId, + nativeItemRef: task.nativeTaskRef, + parentItemId: null, + ordinal: subagent.turnItemOrdinal, + status, + title: task.title, + startedAt: task.startedAt, + completedAt: task.completedAt, + updatedAt: now, + type: "subagent", + subagentId: task.id, + origin: task.origin, + driver: task.driver, + providerInstanceId: task.providerInstanceId, + childThreadId, + prompt: task.prompt, + result: task.result, + }, + }); + + if (input.completed && !subagent.resultProjected) { + for (const [index, nestedTool] of toolCallsFromConversationSteps( + result?.conversationSteps ?? [], + ).entries()) { + const callId = `${nativeItemId}:child-tool:${nestedTool.callId || index + 1}`; + const startedAt = task.startedAt ?? now; + const active: ActiveCursorToolCall = { + callId, + toolCall: nestedTool.toolCall, + target: { + threadId: childThreadId, + runId: null, + rootNodeId: childRootNodeId, + parentNodeId: childRootNodeId, + providerThreadId: null, + providerTurnId: null, + }, + ordinal: ++subagent.nextChildOrdinal, + startedAt, + streamedOutput: "", + }; + yield* emitToolArtifacts({ active, completed: true }); + } + if (task.result !== null && task.result.length > 0) { + const resultNativeId = `${nativeItemId}:result`; + const resultArtifacts = makeSubagentConversationArtifacts({ + messageId: idAllocator.derive.messageFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: resultNativeId, + }), + turnItemId: idAllocator.derive.turnItemFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: resultNativeId, + }), + threadId: childThreadId, + rootNodeId: childRootNodeId, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: { + driver: CURSOR_PROVIDER, + nativeId: resultNativeId, + strength: "weak", + }, + role: "assistant", + text: task.result, + ordinal: ++subagent.nextChildOrdinal, + now, + }); + yield* emitProviderEvent({ + type: "message.updated", + driver: CURSOR_PROVIDER, + message: resultArtifacts.message, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: CURSOR_PROVIDER, + turnItem: resultArtifacts.turnItem, + }); + } + subagent.resultProjected = true; + } + }); + + const handleToolUpdate = Effect.fnUntraced(function* ( + context: ActiveCursorTurn, + update: Extract< + InteractionUpdate, + { + readonly type: "tool-call-started" | "partial-tool-call" | "tool-call-completed"; + } + >, + ) { + const completed = update.type === "tool-call-completed"; + const toolCall = update.toolCall; + switch (toolCall.type) { + case "createPlan": + yield* emitPlanArtifacts({ + context, + callId: update.callId, + markdown: toolCall.args.plan, + completed, + failed: completed && cursorToolFailed(toolCall), + }); + return; + case "updateTodos": + yield* emitTodoArtifacts({ + context, + callId: update.callId, + toolCall, + completed, + failed: completed && cursorToolFailed(toolCall), + }); + return; + case "task": + if (update.type === "partial-tool-call") { + return; + } + yield* emitSubagent({ + context, + callId: update.callId, + toolCall, + completed, + }); + return; + default: { + const active = yield* ensureToolStarted(context, update.callId, toolCall); + active.toolCall = toolCall; + if (update.type !== "tool-call-started") { + yield* emitToolArtifacts({ active, completed }); + } + if (completed) { + context.tools.delete(update.callId); + } + } + } + }); + + const shellOutputText = (event: Record): string => { + const candidates = [ + event.text, + event.output, + event.data, + event.stdout, + event.stderr, + event.chunk, + ]; + return candidates.filter((value): value is string => typeof value === "string").join(""); + }; + + const handleInteractionUpdate = Effect.fnUntraced(function* ( + context: ActiveCursorTurn, + update: InteractionUpdate, + ) { + if (context.finalized) { + return; + } + switch (update.type) { + case "text-delta": + yield* completeReasoning(context); + yield* appendTextSegment({ + context, + stream: context.assistant, + kind: "assistant", + text: update.text, + }); + yield* emitAssistant(context, false); + return; + case "thinking-delta": + yield* completeAssistant(context); + yield* appendTextSegment({ + context, + stream: context.reasoning, + kind: "reasoning", + text: update.text, + }); + yield* emitReasoning(context, false); + return; + case "thinking-completed": + yield* completeReasoning(context); + return; + case "tool-call-started": + case "partial-tool-call": + case "tool-call-completed": + yield* completeAssistant(context); + yield* completeReasoning(context); + yield* handleToolUpdate(context, update); + return; + case "step-completed": + case "turn-ended": + yield* completeAssistant(context); + yield* completeReasoning(context); + return; + case "shell-output-delta": { + const shell = Array.from(context.tools.values()) + .toReversed() + .find((candidate) => candidate.toolCall.type === "shell"); + if (shell === undefined) { + return; + } + shell.streamedOutput += shellOutputText(update.event); + yield* emitToolArtifacts({ active: shell, completed: false }); + return; + } + default: + return; + } + }); + + const providerTurnPayload = (input: { + readonly context: ActiveCursorTurn; + readonly status: OrchestrationV2ProviderTurn["status"]; + readonly completedAt: DateTime.Utc | null; + }): OrchestrationV2ProviderTurn => ({ + id: input.context.providerTurnId, + providerThreadId: input.context.input.providerThread.id, + nodeId: input.context.input.rootNodeId, + runAttemptId: input.context.input.attemptId, + nativeTurnRef: { + driver: CURSOR_PROVIDER, + nativeId: input.context.run.runId, + strength: "strong", + }, + ordinal: input.context.input.providerTurnOrdinal, + status: input.status, + startedAt: input.context.startedAt, + completedAt: input.completedAt, + }); + + const finalizeTurn = Effect.fnUntraced(function* (input: { + readonly context: ActiveCursorTurn; + readonly status: Extract< + OrchestrationV2ProviderTurn["status"], + "completed" | "interrupted" | "failed" | "cancelled" + >; + }) { + if (input.context.finalized) { + return; + } + input.context.finalized = true; + const completedAt = yield* DateTime.now; + for (const tool of input.context.tools.values()) { + yield* emitToolArtifacts({ active: tool, completed: true }); + } + input.context.tools.clear(); + yield* completeReasoning(input.context); + yield* completeAssistant(input.context); + yield* emitProviderEvent({ + type: "provider_turn.updated", + driver: CURSOR_PROVIDER, + providerTurn: providerTurnPayload({ + context: input.context, + status: input.status, + completedAt, + }), + }); + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver: CURSOR_PROVIDER, + providerThread: { + ...input.context.input.providerThread, + providerSessionId: session.id, + status: input.status === "failed" ? "error" : "idle", + firstRunOrdinal: + input.context.input.providerThread.firstRunOrdinal ?? + input.context.input.runOrdinal, + lastRunOrdinal: input.context.input.runOrdinal, + updatedAt: completedAt, + }, + }); + yield* emitProviderEvent({ + type: "turn.terminal", + driver: CURSOR_PROVIDER, + providerTurnId: input.context.providerTurnId, + status: input.status, + }); + yield* Ref.update(activeTurn, (current) => + current?.providerTurnId === input.context.providerTurnId ? null : current, + ); + yield* Deferred.succeed(input.context.completed, undefined); + }); + + const terminalStatus = ( + context: ActiveCursorTurn, + result: RunResult, + ): Extract< + OrchestrationV2ProviderTurn["status"], + "completed" | "interrupted" | "failed" | "cancelled" + > => { + if (context.interrupted) { + return "interrupted"; + } + switch (result.status) { + case "finished": + return "completed"; + case "cancelled": + return "cancelled"; + case "error": + return "failed"; + } + }; + + const openAgent = Effect.fnUntraced(function* (openInput: { + readonly operation: "create" | "resume"; + readonly threadId: ThreadId; + readonly modelSelection: ModelSelection; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; + readonly agentId?: string; + }) { + const existing = yield* Ref.get(liveAgent); + if ( + existing !== null && + openInput.operation === "resume" && + existing.nativeThreadId === openInput.agentId + ) { + return existing; + } + if (existing !== null) { + yield* existing.session.close.pipe(Effect.ignore); + yield* Ref.set(liveAgent, null); + } + const sdkSession = yield* runner.open({ + operation: openInput.operation, + ...(openInput.agentId === undefined ? {} : { agentId: openInput.agentId }), + options: makeCursorAgentOptions({ + ...(apiKey === undefined ? {} : { apiKey }), + modelSelection: openInput.modelSelection, + runtimePolicy: openInput.runtimePolicy, + threadId: openInput.threadId, + }), + threadId: openInput.threadId, + providerSessionId: input.providerSessionId, + }); + const next = { + nativeThreadId: sdkSession.agentId, + session: sdkSession, + } satisfies CursorLiveAgent; + yield* Ref.set(liveAgent, next); + return next; + }); + + const resolveUserMessage = Effect.fnUntraced(function* ( + turnInput: ProviderAdapterV2TurnInput, + ) { + const images = yield* Effect.forEach( + turnInput.message.attachments, + (attachment: ChatAttachment) => + Effect.gen(function* () { + const path = resolveAttachmentPath({ + attachmentsDir: serverConfig.attachmentsDir, + attachment, + }); + if (path === null) { + return yield* new ProviderAdapterProtocolError({ + driver: CURSOR_PROVIDER, + detail: `Invalid attachment id '${attachment.id}'.`, + }); + } + const bytes = yield* fileSystem.readFile(path).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterProtocolError({ + driver: CURSOR_PROVIDER, + detail: `Failed to read attachment '${attachment.id}'.`, + payload: cause, + }), + ), + ); + return { + data: Buffer.from(bytes).toString("base64"), + mimeType: attachment.mimeType, + }; + }), + { concurrency: 1 }, + ); + if (turnInput.message.text.length === 0 && images.length === 0) { + return yield* new ProviderAdapterProtocolError({ + driver: CURSOR_PROVIDER, + detail: "Cursor turn requires non-empty text or attachments.", + }); + } + return images.length === 0 + ? turnInput.message.text + : ({ + text: turnInput.message.text, + images, + } satisfies SDKUserMessage); + }); + + const startTurn = Effect.fn("CursorAdapterV2.startTurn")( + function* (turnInput: ProviderAdapterV2TurnInput) { + const current = yield* Ref.get(activeTurn); + if (current !== null) { + return yield* new ProviderAdapterProtocolError({ + driver: CURSOR_PROVIDER, + detail: `Cursor provider turn ${current.providerTurnId} is still active.`, + }); + } + const agentId = nativeThreadId(turnInput.providerThread); + const agent = yield* openAgent({ + operation: "resume", + agentId, + threadId: turnInput.threadId, + modelSelection: turnInput.modelSelection, + runtimePolicy: turnInput.runtimePolicy, + }); + const message = yield* resolveUserMessage(turnInput); + const mcpServers = cursorMcpServers(turnInput.threadId); + const pendingUpdates: Array = []; + let context: ActiveCursorTurn | null = null; + const sdkRun = yield* agent.session.send({ + message, + options: { + model: cursorSdkModelSelection(turnInput.modelSelection), + mode: turnInput.runtimePolicy.interactionMode === "plan" ? "plan" : "agent", + ...(mcpServers === undefined ? {} : { mcpServers }), + }, + onDelta: (update) => { + if (context === null) { + return Effect.sync(() => { + pendingUpdates.push(update); + }); + } + return handleInteractionUpdate(context, update); + }, + }); + const startedAt = yield* DateTime.now; + const completed = yield* Deferred.make(); + const providerTurnId = idAllocator.derive.providerTurn({ + driver: CURSOR_PROVIDER, + nativeTurnId: sdkRun.runId, + }); + context = { + input: turnInput, + run: sdkRun, + providerTurnId, + startedAt, + completed, + tools: new Map(), + subagents: new Map(), + assistant: { + current: null, + nextSegment: 0, + }, + reasoning: { + current: null, + nextSegment: 0, + }, + interrupted: false, + finalized: false, + }; + yield* Ref.set(activeTurn, context); + yield* emitProviderEvent({ + type: "provider_turn.updated", + driver: CURSOR_PROVIDER, + providerTurn: providerTurnPayload({ + context, + status: "running", + completedAt: null, + }), + }); + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver: CURSOR_PROVIDER, + providerThread: { + ...turnInput.providerThread, + providerSessionId: session.id, + status: "active", + updatedAt: startedAt, + }, + }); + for (const update of pendingUpdates) { + yield* handleInteractionUpdate(context, update); + } + + yield* sdkRun.wait.pipe( + Effect.flatMap((result) => + Effect.gen(function* () { + if ( + context !== null && + context.assistant.nextSegment === 0 && + result.result !== undefined && + result.result.length > 0 + ) { + yield* appendTextSegment({ + context, + stream: context.assistant, + kind: "assistant", + text: result.result, + }); + } + if (context !== null) { + yield* finalizeTurn({ + context, + status: terminalStatus(context, result), + }); + } + }), + ), + Effect.catch((cause) => + Effect.gen(function* () { + if (context !== null) { + yield* finalizeTurn({ + context, + status: context.interrupted ? "interrupted" : "failed", + }); + } + yield* Effect.logWarning("orchestration-v2.cursor-run-failed", { + providerSessionId: input.providerSessionId, + providerThreadId: turnInput.providerThread.id, + providerTurnId, + cause, + }); + }), + ), + Effect.forkIn(sessionScope), + ); + }, + (effect, turnInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterTurnStartError({ + driver: CURSOR_PROVIDER, + threadId: turnInput.threadId, + providerThreadId: turnInput.providerThread.id, + runId: turnInput.runId, + cause, + }), + ), + ), + ); + + const closeSession = Effect.fnUntraced(function* () { + const existing = yield* Ref.get(liveAgent); + if (existing !== null) { + yield* existing.session.close.pipe(Effect.ignore); + yield* Ref.set(liveAgent, null); + } + yield* runner.assertComplete.pipe( + Effect.catchCause((cause) => + Effect.logWarning("orchestration-v2.cursor-runner-incomplete", { + providerSessionId: input.providerSessionId, + cause, + }), + ), + ); + }); + yield* Effect.addFinalizer(() => closeSession()); + + const runtime: ProviderAdapterV2SessionRuntime = { + instanceId: adapterOptions.instanceId, + driver: CURSOR_PROVIDER, + providerSessionId: input.providerSessionId, + providerSession: session, + rawEvents: Stream.empty, + events: Stream.fromEffectRepeat(Queue.take(events)), + ensureThread: Effect.fn("CursorAdapterV2.ensureThread")( + function* (threadInput: ProviderAdapterV2EnsureThreadInput) { + const opened = yield* openAgent({ + operation: "create", + threadId: threadInput.threadId, + modelSelection: threadInput.modelSelection, + runtimePolicy: threadInput.runtimePolicy, + }); + const now = yield* DateTime.now; + return makeProviderThread({ + idAllocator, + providerInstanceId: adapterOptions.instanceId, + appThreadId: threadInput.threadId, + providerSessionId: input.providerSessionId, + nativeThreadId: opened.nativeThreadId, + now, + }); + }, + (effect, threadInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterEnsureThreadError({ + driver: CURSOR_PROVIDER, + threadId: threadInput.threadId, + cause, + }), + ), + ), + ), + resumeThread: Effect.fn("CursorAdapterV2.resumeThread")( + function* (threadInput: { readonly providerThread: OrchestrationV2ProviderThread }) { + const agentId = nativeThreadId(threadInput.providerThread); + yield* openAgent({ + operation: "resume", + agentId, + threadId: threadInput.providerThread.appThreadId ?? input.threadId, + modelSelection: input.modelSelection, + runtimePolicy: input.runtimePolicy, + }); + const now = yield* DateTime.now; + return { + ...threadInput.providerThread, + providerSessionId: input.providerSessionId, + status: "idle" as const, + updatedAt: now, + }; + }, + (effect, threadInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterResumeThreadError({ + driver: CURSOR_PROVIDER, + providerSessionId: input.providerSessionId, + providerThreadId: threadInput.providerThread.id, + cause, + }), + ), + ), + ), + startTurn, + steerTurn: (turnInput) => + Effect.fail( + new ProviderAdapterSteerRunUnsupportedError({ + driver: CURSOR_PROVIDER, + providerThreadId: turnInput.providerThread.id, + }), + ), + interruptTurn: Effect.fn("CursorAdapterV2.interruptTurn")( + function* (turnInput: ProviderAdapterV2InterruptInput) { + const context = yield* Ref.get(activeTurn); + if (context?.providerTurnId !== turnInput.providerTurnId) { + return yield* new ProviderAdapterProtocolError({ + driver: CURSOR_PROVIDER, + detail: `Cursor provider turn ${turnInput.providerTurnId} is not active.`, + }); + } + context.interrupted = true; + yield* context.run.cancel; + const stopped = yield* Deferred.await(context.completed).pipe( + Effect.timeoutOption("10 seconds"), + ); + if (Option.isSome(stopped)) { + return; + } + yield* Effect.logWarning("orchestration-v2.cursor-interrupt-timeout", { + providerSessionId: input.providerSessionId, + providerThreadId: turnInput.providerThread.id, + providerTurnId: turnInput.providerTurnId, + }); + yield* finalizeTurn({ context, status: "interrupted" }); + }, + (effect, turnInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterInterruptError({ + driver: CURSOR_PROVIDER, + providerThreadId: turnInput.providerThread.id, + providerTurnId: turnInput.providerTurnId, + cause, + }), + ), + ), + ), + respondToRuntimeRequest: (requestInput) => + Effect.fail( + new ProviderAdapterRuntimeRequestResponseError({ + driver: CURSOR_PROVIDER, + requestId: requestInput.requestId, + cause: new ProviderAdapterProtocolError({ + driver: CURSOR_PROVIDER, + detail: "Cursor Agent SDK does not expose interactive approval requests.", + }), + }), + ), + readThreadSnapshot: Effect.fn("CursorAdapterV2.readThreadSnapshot")( + function* (snapshotInput) { + const requestedAgentId = nativeThreadId(snapshotInput.providerThread); + const agent = yield* openAgent({ + operation: "resume", + agentId: requestedAgentId, + threadId: snapshotInput.providerThread.appThreadId ?? input.threadId, + modelSelection: input.modelSelection, + runtimePolicy: input.runtimePolicy, + }); + const messages = yield* agent.session.listMessages; + const now = yield* DateTime.now; + const threadId = snapshotInput.providerThread.appThreadId ?? input.threadId; + const projectedMessages: Array = messages.flatMap( + (message) => { + const text = textFromAgentMessage(message); + if (text.length === 0) { + return []; + } + return [ + { + createdBy: message.type === "user" ? "user" : "agent", + creationSource: "provider", + id: idAllocator.derive.messageFromProviderItem({ + driver: CURSOR_PROVIDER, + nativeItemId: message.uuid, + }), + threadId, + runId: null, + nodeId: null, + role: message.type, + text, + attachments: [], + streaming: false, + createdAt: now, + updatedAt: now, + }, + ]; + }, + ); + return { + providerThread: { + ...snapshotInput.providerThread, + providerSessionId: input.providerSessionId, + status: "idle" as const, + updatedAt: now, + }, + providerTurns: [], + messages: projectedMessages, + runtimeRequests: [], + providerPayload: { + agentId: requestedAgentId, + messages, + }, + }; + }, + (effect, snapshotInput) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterReadThreadSnapshotError({ + driver: CURSOR_PROVIDER, + providerThreadId: snapshotInput.providerThread.id, + cause, + }), + ), + ), + ), + rollbackThread: (rollbackInput) => + Effect.fail( + new ProviderAdapterRollbackThreadError({ + driver: CURSOR_PROVIDER, + providerThreadId: rollbackInput.providerThread.id, + checkpointId: rollbackInput.target.checkpointId, + cause: "Cursor Agent SDK does not expose conversation rollback.", + }), + ), + forkThread: (forkInput) => + Effect.fail( + new ProviderAdapterForkThreadError({ + driver: CURSOR_PROVIDER, + providerThreadId: forkInput.sourceProviderThread.id, + cause: "Cursor Agent SDK does not expose native agent forks.", + }), + ), + }; + return runtime; + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterOpenSessionError({ + driver: CURSOR_PROVIDER, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ), + ), + }); +} + +export type CursorAdapterV2DriverEnv = + | CursorAgentSdkRunner + | FileSystem.FileSystem + | IdAllocatorV2 + | ServerConfig; + +export const CursorAdapterV2Driver: ProviderAdapterDriver< + CursorSettings, + CursorAdapterV2DriverEnv +> = { + driverKind: CURSOR_DRIVER_KIND, + configSchema: CursorSettings, + defaultConfig: (): CursorSettings => DEFAULT_CURSOR_SETTINGS, + create: Effect.fn("CursorAdapterV2Driver.create")( + function* (input: ProviderAdapterDriverCreateInput) { + const hostEnvironment = yield* HostProcessEnvironment; + const fileSystem = yield* FileSystem.FileSystem; + const idAllocator = yield* IdAllocatorV2; + const runner = yield* CursorAgentSdkRunner; + const serverConfig = yield* ServerConfig; + if (input.config.apiEndpoint.length > 0) { + yield* Effect.logWarning( + "Cursor V2 uses the official SDK, which does not expose an API endpoint override.", + { + instanceId: input.instanceId, + }, + ); + } + return makeCursorAdapterV2({ + instanceId: input.instanceId, + settings: { + ...input.config, + enabled: input.enabled, + }, + environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + fileSystem, + idAllocator, + runner, + serverConfig, + }); + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterDriverCreateError({ + driver: CURSOR_DRIVER_KIND, + instanceId: input.instanceId, + detail: "Failed to create Cursor Agent SDK adapter.", + cause, + }), + ), + ), + ), +}; + +export const layer: Layer.Layer< + ProviderAdapterV2, + never, + CursorAgentSdkRunner | FileSystem.FileSystem | IdAllocatorV2 | ServerConfig +> = Layer.effect( + ProviderAdapterV2, + Effect.gen(function* () { + const hostEnvironment = yield* HostProcessEnvironment; + const fileSystem = yield* FileSystem.FileSystem; + const idAllocator = yield* IdAllocatorV2; + const runner = yield* CursorAgentSdkRunner; + const serverConfig = yield* ServerConfig; + return makeCursorAdapterV2({ + instanceId: CURSOR_DEFAULT_INSTANCE_ID, + settings: DEFAULT_CURSOR_SETTINGS, + environment: hostEnvironment, + fileSystem, + idAllocator, + runner, + serverConfig, + }); + }), +); diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts b/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts new file mode 100644 index 00000000000..5c474d9207d --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/CursorAgentSdk.ts @@ -0,0 +1,517 @@ +import { + Agent, + type AgentMessage, + type AgentOptions, + type InteractionUpdate, + type RunResult, + type SDKUserMessage, + type SendOptions, +} from "@cursor/sdk"; +import { + type OrchestrationV2ProviderSession, + ProviderDriverKind, + type ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import { ServerConfig } from "../../config.ts"; +import { + type EventNdjsonLogger, + makeEventNdjsonLogger, +} from "../../provider/Layers/EventNdjsonLogger.ts"; + +export const CURSOR_AGENT_SDK_PROTOCOL = "cursor-agent-sdk.local" as const; +export const CURSOR_PROVIDER = ProviderDriverKind.make("cursor"); + +export class CursorAgentSdkRunnerError extends Schema.TaggedErrorClass()( + "CursorAgentSdkRunnerError", + { + method: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Cursor Agent SDK ${this.method} failed.`; + } +} + +const isCursorAgentSdkRunnerError = Schema.is(CursorAgentSdkRunnerError); + +export interface CursorAgentSdkOpenInput { + readonly operation: "create" | "resume"; + readonly agentId?: string; + readonly options: AgentOptions; + readonly threadId: ThreadId; + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; +} + +export interface CursorAgentSdkSendInput { + readonly message: string | SDKUserMessage; + readonly options?: Omit; + readonly onDelta?: (update: InteractionUpdate) => Effect.Effect; +} + +export interface CursorAgentSdkRun { + readonly runId: string; + readonly agentId: string; + readonly wait: Effect.Effect; + readonly cancel: Effect.Effect; +} + +export interface CursorAgentSdkSession { + readonly agentId: string; + readonly send: ( + input: CursorAgentSdkSendInput, + ) => Effect.Effect; + readonly listMessages: Effect.Effect, CursorAgentSdkRunnerError>; + readonly close: Effect.Effect; +} + +export interface CursorAgentSdkRunnerShape { + readonly open: ( + input: CursorAgentSdkOpenInput, + ) => Effect.Effect; + readonly assertComplete: Effect.Effect; +} + +export class CursorAgentSdkRunner extends Context.Service< + CursorAgentSdkRunner, + CursorAgentSdkRunnerShape +>()("t3/orchestration-v2/Adapters/CursorAgentSdk/CursorAgentSdkRunner") {} + +export interface CursorAgentSdkLoggedAgentOptions { + readonly model?: AgentOptions["model"]; + readonly hasName?: boolean; + readonly mode?: AgentOptions["mode"]; + readonly local?: { + readonly hasCwd?: boolean; + readonly autoReview?: boolean; + readonly settingSources?: AgentOptions["local"] extends infer Local + ? Local extends { readonly settingSources?: infer Sources } + ? Sources + : never + : never; + readonly sandboxEnabled?: boolean; + readonly enableAgentRetries?: boolean; + readonly hasCustomTools?: boolean; + }; + readonly agents?: ReadonlyArray; +} + +export interface CursorAgentSdkLoggedSendOptions { + readonly model?: SendOptions["model"]; + readonly mode?: SendOptions["mode"]; + readonly local?: { + readonly force?: boolean; + readonly hasCustomTools?: boolean; + }; + readonly idempotencyKey?: string; +} + +export type CursorAgentSdkProtocolLogEvent = + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "agent.open"; + readonly operation: CursorAgentSdkOpenInput["operation"]; + readonly agentId?: string; + readonly options: CursorAgentSdkLoggedAgentOptions; + }; + } + | { + readonly direction: "incoming"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "agent.opened"; + readonly agentId: string; + }; + } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "run.start"; + readonly message: string | SDKUserMessage; + readonly options: CursorAgentSdkLoggedSendOptions; + }; + } + | { + readonly direction: "incoming"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "run.started"; + readonly runId: string; + readonly agentId: string; + }; + } + | { + readonly direction: "incoming"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "interaction.update"; + readonly runId: string; + readonly update: InteractionUpdate; + }; + } + | { + readonly direction: "incoming"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "run.completed"; + readonly result: RunResult; + }; + } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "run.cancel"; + readonly runId: string; + }; + } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "agent.messages.list"; + readonly agentId: string; + }; + } + | { + readonly direction: "incoming"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "agent.messages"; + readonly agentId: string; + readonly messages: ReadonlyArray; + }; + } + | { + readonly direction: "outgoing"; + readonly stage: "decoded"; + readonly payload: { + readonly type: "agent.close"; + readonly agentId: string; + }; + }; + +export type CursorAgentSdkProtocolLogger = ( + event: CursorAgentSdkProtocolLogEvent, +) => Effect.Effect; + +function runnerError(cause: unknown, method: string): CursorAgentSdkRunnerError { + return isCursorAgentSdkRunnerError(cause) + ? cause + : new CursorAgentSdkRunnerError({ method, cause }); +} + +export function isCursorCancellationError(cause: unknown): boolean { + let current = cause; + const seen = new Set(); + + while (typeof current === "object" && current !== null && !seen.has(current)) { + if (Reflect.get(current, "name") === "AbortError") { + return true; + } + seen.add(current); + current = Reflect.get(current, "cause"); + } + + return false; +} + +export function loggedCursorAgentOptions(options: AgentOptions): CursorAgentSdkLoggedAgentOptions { + return { + ...(options.model === undefined ? {} : { model: options.model }), + ...(options.name === undefined ? {} : { hasName: true }), + ...(options.mode === undefined ? {} : { mode: options.mode }), + ...(options.local === undefined + ? {} + : { + local: { + ...(options.local.cwd === undefined ? {} : { hasCwd: true }), + ...(options.local.autoReview === undefined + ? {} + : { autoReview: options.local.autoReview }), + ...(options.local.settingSources === undefined + ? {} + : { settingSources: options.local.settingSources }), + ...(options.local.sandboxOptions === undefined + ? {} + : { sandboxEnabled: options.local.sandboxOptions.enabled }), + ...(options.local.enableAgentRetries === undefined + ? {} + : { enableAgentRetries: options.local.enableAgentRetries }), + ...(options.local.customTools === undefined ? {} : { hasCustomTools: true }), + }, + }), + ...(options.agents === undefined ? {} : { agents: Object.keys(options.agents).toSorted() }), + }; +} + +export function loggedCursorSendOptions( + options: Omit | undefined, +): CursorAgentSdkLoggedSendOptions { + if (options === undefined) { + return {}; + } + return { + ...(options.model === undefined ? {} : { model: options.model }), + ...(options.mode === undefined ? {} : { mode: options.mode }), + ...(options.local === undefined + ? {} + : { + local: { + ...(options.local.force === undefined ? {} : { force: options.local.force }), + ...(options.local.customTools === undefined ? {} : { hasCustomTools: true }), + }, + }), + ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }), + }; +} + +export function makeCursorAgentSdkProtocolLogger(input: { + readonly nativeEventLogger: EventNdjsonLogger | undefined; + readonly threadId: ThreadId; + readonly providerSessionId: OrchestrationV2ProviderSession["id"]; +}): CursorAgentSdkProtocolLogger | undefined { + if (input.nativeEventLogger === undefined) { + return undefined; + } + const nativeEventLogger = input.nativeEventLogger; + return (event) => + nativeEventLogger + .write( + { + provider: CURSOR_PROVIDER, + protocol: CURSOR_AGENT_SDK_PROTOCOL, + kind: "protocol", + providerSessionId: input.providerSessionId, + event, + }, + input.threadId, + ) + .pipe(Effect.ignore); +} + +export const cursorAgentSdkRunnerLiveLayer: Layer.Layer = + Layer.effect( + CursorAgentSdkRunner, + Effect.gen(function* () { + const { providerEventLogPath } = yield* ServerConfig; + const nativeEventLogger = yield* makeEventNdjsonLogger(providerEventLogPath, { + stream: "native", + }); + + return CursorAgentSdkRunner.of({ + open: Effect.fn("CursorAgentSdkRunner.open")(function* (input) { + const protocolLogger = makeCursorAgentSdkProtocolLogger({ + nativeEventLogger, + threadId: input.threadId, + providerSessionId: input.providerSessionId, + }); + const log = (event: CursorAgentSdkProtocolLogEvent) => + protocolLogger === undefined ? Effect.void : protocolLogger(event); + + yield* log({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "agent.open", + operation: input.operation, + ...(input.agentId === undefined ? {} : { agentId: input.agentId }), + options: loggedCursorAgentOptions(input.options), + }, + }); + + const agent = yield* Effect.tryPromise({ + try: () => + input.operation === "create" + ? Agent.create(input.options) + : Agent.resume(input.agentId!, input.options), + catch: (cause) => runnerError(cause, `agent.${input.operation}`), + }); + + yield* log({ + direction: "incoming", + stage: "decoded", + payload: { + type: "agent.opened", + agentId: agent.agentId, + }, + }); + + const cwd = + typeof input.options.local?.cwd === "string" + ? input.options.local.cwd + : input.options.local?.cwd?.[0]; + + return { + agentId: agent.agentId, + send: Effect.fn("CursorAgentSdkSession.send")(function* (sendInput) { + const context = yield* Effect.context(); + yield* log({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "run.start", + message: sendInput.message, + options: loggedCursorSendOptions(sendInput.options), + }, + }); + + let callbacksReady = false; + const pendingUpdates: Array = []; + let callbackChain = Promise.resolve(); + let runId = ""; + const dispatchUpdate = (update: InteractionUpdate): Promise => { + callbackChain = callbackChain.then(() => + Effect.runPromiseWith(context)( + log({ + direction: "incoming", + stage: "decoded", + payload: { + type: "interaction.update", + runId, + update, + }, + }).pipe(Effect.andThen(sendInput.onDelta?.(update) ?? Effect.void)), + ), + ); + return callbackChain; + }; + + const run = yield* Effect.tryPromise({ + try: () => + agent.send(sendInput.message, { + ...sendInput.options, + onDelta: async ({ update }) => { + if (!callbacksReady) { + pendingUpdates.push(update); + return; + } + await dispatchUpdate(update); + }, + }), + catch: (cause) => runnerError(cause, "run.start"), + }); + runId = run.id; + yield* log({ + direction: "incoming", + stage: "decoded", + payload: { + type: "run.started", + runId: run.id, + agentId: run.agentId, + }, + }); + callbacksReady = true; + for (const update of pendingUpdates) { + yield* Effect.tryPromise({ + try: () => dispatchUpdate(update), + catch: (cause) => runnerError(cause, "run.onDelta"), + }); + } + + return { + runId: run.id, + agentId: run.agentId, + wait: Effect.tryPromise({ + try: async () => { + const result = await run.wait(); + await callbackChain; + return result; + }, + catch: (cause) => runnerError(cause, "run.wait"), + }).pipe( + Effect.tap((result) => + log({ + direction: "incoming", + stage: "decoded", + payload: { + type: "run.completed", + result, + }, + }), + ), + ), + cancel: log({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "run.cancel", + runId: run.id, + }, + }).pipe( + Effect.andThen( + Effect.tryPromise({ + try: async () => { + try { + await run.cancel(); + } catch (cause) { + if (!isCursorCancellationError(cause)) { + throw cause; + } + } + }, + catch: (cause) => runnerError(cause, "run.cancel"), + }), + ), + ), + } satisfies CursorAgentSdkRun; + }), + listMessages: log({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "agent.messages.list", + agentId: agent.agentId, + }, + }).pipe( + Effect.andThen( + Effect.tryPromise({ + try: () => + Agent.messages.list(agent.agentId, { + runtime: "local", + ...(cwd === undefined ? {} : { cwd }), + }), + catch: (cause) => runnerError(cause, "agent.messages.list"), + }), + ), + Effect.tap((messages) => + log({ + direction: "incoming", + stage: "decoded", + payload: { + type: "agent.messages", + agentId: agent.agentId, + messages, + }, + }), + ), + ), + close: Effect.try({ + try: () => agent.close(), + catch: (cause) => runnerError(cause, "agent.close"), + }).pipe( + Effect.tap(() => + log({ + direction: "outgoing", + stage: "decoded", + payload: { + type: "agent.close", + agentId: agent.agentId, + }, + }), + ), + ), + } satisfies CursorAgentSdkSession; + }), + assertComplete: Effect.void, + }); + }), + ); diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts new file mode 100644 index 00000000000..0697ec85b48 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts @@ -0,0 +1,111 @@ +import { assert, describe, it } from "@effect/vitest"; +import type * as EffectAcpSchema from "effect-acp/schema"; + +import { ProviderAdapterV2RuntimePolicy } from "../ProviderAdapter.ts"; +import { AcpProviderCapabilitiesV2, acpPermissionDisposition } from "./AcpAdapterV2.ts"; +import { GrokProviderCapabilitiesV2 } from "./GrokAdapterV2.ts"; + +function permissionRequest( + kind: EffectAcpSchema.ToolKind, +): EffectAcpSchema.RequestPermissionRequest { + return { + sessionId: "session-1", + options: [ + { optionId: "allow-once", name: "Allow once", kind: "allow_once" }, + { optionId: "allow-always", name: "Allow always", kind: "allow_always" }, + { optionId: "reject-once", name: "Reject", kind: "reject_once" }, + ], + toolCall: { + toolCallId: "tool-1", + title: "Test tool", + kind, + }, + }; +} + +function runtimePolicy(input: { + readonly runtimeMode: "approval-required" | "auto-accept-edits" | "full-access"; + readonly approvalPolicy?: unknown; + readonly sandboxPolicy?: unknown; +}) { + return ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: input.runtimeMode, + interactionMode: "default", + cwd: "/workspace", + ...(input.approvalPolicy === undefined ? {} : { approvalPolicy: input.approvalPolicy }), + ...(input.sandboxPolicy === undefined ? {} : { sandboxPolicy: input.sandboxPolicy }), + }); +} + +describe("GrokAdapterV2 capabilities", () => { + it("keeps optional protocol features conservative until a flavor or handshake confirms them", () => { + assert.isFalse(AcpProviderCapabilitiesV2.sessions.supportsModelSwitchInSession); + assert.isFalse(AcpProviderCapabilitiesV2.sessions.supportsRuntimeModeSwitchInSession); + assert.isFalse(AcpProviderCapabilitiesV2.threads.canReadThreadSnapshot); + assert.isFalse(AcpProviderCapabilitiesV2.tools.supportsMcpTools); + }); + + it("leaves non-native features available to orchestrator fallbacks", () => { + assert.isFalse(GrokProviderCapabilitiesV2.threads.canForkThread); + assert.isFalse(GrokProviderCapabilitiesV2.subagents.supportsSubagents); + assert.isFalse(GrokProviderCapabilitiesV2.turns.supportsActiveSteering); + assert.isTrue(GrokProviderCapabilitiesV2.turns.supportsInterrupt); + assert.isTrue(GrokProviderCapabilitiesV2.turns.supportsSteeringByInterruptRestart); + assert.isTrue(GrokProviderCapabilitiesV2.context.supportsFullThreadHandoff); + }); + + it("declares the optional ACP features verified by the Grok handshake", () => { + assert.isTrue(GrokProviderCapabilitiesV2.sessions.supportsModelSwitchInSession); + assert.isTrue(GrokProviderCapabilitiesV2.threads.canReadThreadSnapshot); + assert.isTrue(GrokProviderCapabilitiesV2.tools.supportsMcpTools); + assert.isTrue(GrokProviderCapabilitiesV2.checkpointing.providerCanReadConversationSnapshot); + }); +}); + +describe("ACP permission policy", () => { + it("honors explicit on-request approval over full-access runtime mode", () => { + assert.equal( + acpPermissionDisposition( + runtimePolicy({ + runtimeMode: "full-access", + approvalPolicy: "on-request", + sandboxPolicy: { type: "readOnly" }, + }), + permissionRequest("execute"), + ), + "ask", + ); + }); + + it("rejects mutating escalation under a non-interactive read-only policy", () => { + const policy = runtimePolicy({ + runtimeMode: "full-access", + approvalPolicy: "never", + sandboxPolicy: { type: "readOnly" }, + }); + assert.equal(acpPermissionDisposition(policy, permissionRequest("execute")), "deny"); + assert.equal(acpPermissionDisposition(policy, permissionRequest("edit")), "deny"); + assert.equal(acpPermissionDisposition(policy, permissionRequest("read")), "allow"); + }); + + it("auto-approves requests only when the resolved policy permits them", () => { + assert.equal( + acpPermissionDisposition( + runtimePolicy({ + runtimeMode: "full-access", + approvalPolicy: "never", + sandboxPolicy: { type: "dangerFullAccess" }, + }), + permissionRequest("execute"), + ), + "allow", + ); + assert.equal( + acpPermissionDisposition( + runtimePolicy({ runtimeMode: "approval-required" }), + permissionRequest("read"), + ), + "ask", + ); + }); +}); diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts new file mode 100644 index 00000000000..68834a71260 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts @@ -0,0 +1,76 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { GrokSettings } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import { ChildProcessSpawner } from "effect/unstable/process"; + +import { ServerConfig } from "../../config.ts"; +import { layer as idAllocatorLayer, IdAllocatorV2 } from "../IdAllocator.ts"; +import { makeLayerEffect as makeProviderAdapterRegistryLayerEffect } from "../ProviderAdapterRegistry.ts"; +import type { OrchestratorV2ProviderReplayHarness } from "../testkit/ProviderReplayHarness.ts"; +import { makeReplayServerConfig } from "../testkit/ProviderReplayHarness.ts"; +import { + type AcpReplayTranscript, + AcpReplayTranscriptDecodeError, + decodeAcpReplayTranscript, + makeAcpReplayCompletenessAssertion, + makeAcpReplayRuntime, +} from "./AcpAdapterV2.testkit.ts"; +import { GROK_DEFAULT_INSTANCE_ID, GROK_PROVIDER, makeGrokAdapterV2 } from "./GrokAdapterV2.ts"; + +const DEFAULT_GROK_SETTINGS = Schema.decodeUnknownSync(GrokSettings)({}); + +export function makeGrokProviderAdapterRegistryReplayLayer(transcript: AcpReplayTranscript) { + const serverConfigLayer = Layer.effect( + ServerConfig, + makeReplayServerConfig(`grok-${transcript.scenario}`).pipe(Effect.orDie), + ).pipe(Layer.provide(NodeServices.layer)); + + return makeProviderAdapterRegistryLayerEffect( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const idAllocator = yield* IdAllocatorV2; + const serverConfig = yield* ServerConfig; + const replayDir = yield* fileSystem + .makeTempDirectory({ + prefix: `t3-orchestration-v2-grok-replay-${transcript.scenario}-`, + }) + .pipe(Effect.orDie); + const statusPath = path.join(replayDir, "status.json"); + const scriptPath = yield* path + .fromFileUrl(new URL("../../../scripts/acp-replay-agent.ts", import.meta.url)) + .pipe(Effect.orDie); + const adapter = makeGrokAdapterV2({ + instanceId: GROK_DEFAULT_INSTANCE_ID, + settings: DEFAULT_GROK_SETTINGS, + environment: {}, + childProcessSpawner, + fileSystem, + idAllocator, + serverConfig, + makeRuntime: makeAcpReplayRuntime({ + transcript, + statusPath, + scriptPath, + childProcessSpawner, + }), + assertComplete: makeAcpReplayCompletenessAssertion(fileSystem, statusPath, transcript), + }); + return [adapter]; + }), + ).pipe(Layer.provide(Layer.mergeAll(serverConfigLayer, NodeServices.layer, idAllocatorLayer))); +} + +export const GrokOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness< + AcpReplayTranscript, + AcpReplayTranscriptDecodeError +> = { + driver: GROK_PROVIDER, + decodeTranscript: (transcript) => decodeAcpReplayTranscript(transcript, GROK_PROVIDER), + makeProviderAdapterRegistryLayer: makeGrokProviderAdapterRegistryReplayLayer, +}; diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts new file mode 100644 index 00000000000..7757d603e30 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts @@ -0,0 +1,214 @@ +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { + defaultInstanceIdForDriver, + GrokSettings, + ProviderDriverKind, + type OrchestrationV2ProviderCapabilities, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import type * as Scope from "effect/Scope"; +import { ChildProcessSpawner } from "effect/unstable/process"; +import type * as EffectAcpErrors from "effect-acp/errors"; + +import { ServerConfig } from "../../config.ts"; +import { + makeGrokAcpRuntime, + resolveGrokAcpBaseModelId, +} from "../../provider/acp/GrokAcpSupport.ts"; +import { + extractXAiAskUserQuestionIdentity, + extractXAiAskUserQuestions, + makeXAiAskUserQuestionCancelledResponse, + makeXAiAskUserQuestionResponse, + XAiAskUserQuestionRequest, +} from "../../provider/acp/XAiAcpExtension.ts"; +import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; +import * as AcpSessionRuntime from "../../provider/acp/AcpSessionRuntime.ts"; +import { IdAllocatorV2 } from "../IdAllocator.ts"; +import { ProviderAdapterV2 } from "../ProviderAdapter.ts"; +import { + ProviderAdapterDriverCreateError, + type ProviderAdapterDriver, + type ProviderAdapterDriverCreateInput, +} from "../ProviderAdapterDriver.ts"; +import { + AcpProviderCapabilitiesV2, + makeAcpAdapterV2, + type AcpAdapterV2Flavor, + type AcpAdapterV2RuntimeInput, +} from "./AcpAdapterV2.ts"; + +export const GROK_PROVIDER = ProviderDriverKind.make("grok"); +export const GROK_DRIVER_KIND = GROK_PROVIDER; +export const GROK_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(GROK_DRIVER_KIND); +const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({}); + +export const GrokProviderCapabilitiesV2 = { + ...AcpProviderCapabilitiesV2, + sessions: { + ...AcpProviderCapabilitiesV2.sessions, + supportsModelSwitchInSession: true, + supportsRuntimeModeSwitchInSession: false, + }, + threads: { + ...AcpProviderCapabilitiesV2.threads, + canReadThreadSnapshot: true, + canForkThread: false, + canForkFromTurn: false, + }, + subagents: { + ...AcpProviderCapabilitiesV2.subagents, + supportsSubagents: false, + }, + tools: { + ...AcpProviderCapabilitiesV2.tools, + supportsMcpTools: true, + }, + checkpointing: { + ...AcpProviderCapabilitiesV2.checkpointing, + providerCanReadConversationSnapshot: true, + }, +} satisfies OrchestrationV2ProviderCapabilities; + +export interface GrokAdapterV2Options { + readonly instanceId: Parameters[0]["instanceId"]; + readonly settings: GrokSettings; + readonly environment: NodeJS.ProcessEnv; + readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; + readonly fileSystem: FileSystem.FileSystem; + readonly idAllocator: IdAllocatorV2["Service"]; + readonly serverConfig: ServerConfig["Service"]; + readonly makeRuntime?: ( + input: AcpAdapterV2RuntimeInput, + ) => Effect.Effect< + AcpSessionRuntime.AcpSessionRuntime["Service"], + EffectAcpErrors.AcpError, + Scope.Scope + >; + readonly assertComplete?: Effect.Effect; +} + +export const registerGrokAcpExtensions: NonNullable = ({ + runtime, + requestUserInput, +}) => + Effect.forEach( + ["x.ai/ask_user_question", "_x.ai/ask_user_question"] as const, + (method) => + runtime.handleExtRequest(method, XAiAskUserQuestionRequest, (params) => { + const identity = extractXAiAskUserQuestionIdentity(params); + const questions = extractXAiAskUserQuestions(params).map((question) => ({ + id: question.id, + header: question.header, + question: question.question, + options: [...question.options], + })); + return requestUserInput({ + nativeItemId: `${identity.sessionId}:xai-question:${identity.toolCallId}`, + nativeRequestId: identity.toolCallId, + questions, + }).pipe( + Effect.map((answers) => + answers === null + ? makeXAiAskUserQuestionCancelledResponse() + : makeXAiAskUserQuestionResponse(params, answers), + ), + ); + }), + { discard: true }, + ); + +export function makeGrokAdapterV2(options: GrokAdapterV2Options) { + const flavor: AcpAdapterV2Flavor = { + driver: GROK_PROVIDER, + capabilities: GrokProviderCapabilitiesV2, + resolveModelId: (selection) => resolveGrokAcpBaseModelId(selection.model), + makeRuntime: + options.makeRuntime ?? + ((input) => + makeGrokAcpRuntime({ + ...input, + grokSettings: options.settings, + environment: options.environment, + childProcessSpawner: options.childProcessSpawner, + })), + registerExtensions: registerGrokAcpExtensions, + ...(options.assertComplete === undefined ? {} : { assertComplete: options.assertComplete }), + }; + return makeAcpAdapterV2({ + instanceId: options.instanceId, + flavor, + fileSystem: options.fileSystem, + idAllocator: options.idAllocator, + serverConfig: options.serverConfig, + }); +} + +export type GrokAdapterV2DriverEnv = + | ChildProcessSpawner.ChildProcessSpawner + | FileSystem.FileSystem + | IdAllocatorV2 + | ServerConfig; + +export const GrokAdapterV2Driver: ProviderAdapterDriver = { + driverKind: GROK_DRIVER_KIND, + configSchema: GrokSettings, + defaultConfig: (): GrokSettings => DEFAULT_GROK_SETTINGS, + create: Effect.fn("GrokAdapterV2Driver.create")( + function* (input: ProviderAdapterDriverCreateInput) { + const hostEnvironment = yield* HostProcessEnvironment; + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fileSystem = yield* FileSystem.FileSystem; + const idAllocator = yield* IdAllocatorV2; + const serverConfig = yield* ServerConfig; + return makeGrokAdapterV2({ + instanceId: input.instanceId, + settings: { ...input.config, enabled: input.enabled }, + environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + childProcessSpawner, + fileSystem, + idAllocator, + serverConfig, + }); + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterDriverCreateError({ + driver: GROK_DRIVER_KIND, + instanceId: input.instanceId, + detail: "Failed to create Grok ACP adapter.", + cause, + }), + ), + ), + ), +}; + +export const layer: Layer.Layer< + ProviderAdapterV2, + never, + ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | IdAllocatorV2 | ServerConfig +> = Layer.effect( + ProviderAdapterV2, + Effect.gen(function* () { + const hostEnvironment = yield* HostProcessEnvironment; + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fileSystem = yield* FileSystem.FileSystem; + const idAllocator = yield* IdAllocatorV2; + const serverConfig = yield* ServerConfig; + return makeGrokAdapterV2({ + instanceId: GROK_DEFAULT_INSTANCE_ID, + settings: DEFAULT_GROK_SETTINGS, + environment: hostEnvironment, + childProcessSpawner, + fileSystem, + idAllocator, + serverConfig, + }); + }), +); diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts new file mode 100644 index 00000000000..7686740ee13 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts @@ -0,0 +1,185 @@ +import { assert, describe, it } from "@effect/vitest"; +import { + NodeId, + ProviderThreadId, + ProviderTurnId, + type OrchestrationV2ProviderTurn, +} from "@t3tools/contracts"; + +import { + openCodeBoundaryAfterProviderTurn, + openCodeChildPermissionRules, + openCodePermissionRules, + openCodePermissionRequestKind, + openCodeToolProjectionKind, + OPENCODE_PROVIDER, + OpenCodeProviderCapabilitiesV2, +} from "./OpenCodeAdapterV2.ts"; +import { ProviderAdapterV2RuntimePolicy } from "../ProviderAdapter.ts"; + +function runtimePolicy( + runtimeMode: ProviderAdapterV2RuntimePolicy["runtimeMode"], + override: Partial = {}, +): ProviderAdapterV2RuntimePolicy { + return ProviderAdapterV2RuntimePolicy.make({ + runtimeMode, + interactionMode: "default", + cwd: null, + ...override, + }); +} + +function permissionAction(rules: ReturnType, permission: string) { + return rules.findLast((rule) => rule.permission === "*" || rule.permission === permission) + ?.action; +} + +function providerTurn(input: { + readonly id: string; + readonly ordinal: number; + readonly nativeId: string | null; +}): OrchestrationV2ProviderTurn { + return { + id: ProviderTurnId.make(input.id), + providerThreadId: ProviderThreadId.make("provider-thread:opencode-test"), + nodeId: NodeId.make(`node:${input.id}`), + runAttemptId: null, + nativeTurnRef: + input.nativeId === null + ? null + : { driver: OPENCODE_PROVIDER, nativeId: input.nativeId, strength: "weak" }, + ordinal: input.ordinal, + status: "completed", + startedAt: null, + completedAt: null, + }; +} + +describe("OpenCodeAdapterV2", () => { + it("advertises the identity strengths exposed by the SDK boundary", () => { + assert.equal(OpenCodeProviderCapabilitiesV2.identity.nativeThreadIds, "strong"); + assert.equal(OpenCodeProviderCapabilitiesV2.identity.nativeTurnIds, "weak"); + assert.equal(OpenCodeProviderCapabilitiesV2.identity.nativeItemIds, "strong"); + assert.equal(OpenCodeProviderCapabilitiesV2.identity.nativeRequestIds, "strong"); + assert.isTrue(OpenCodeProviderCapabilitiesV2.threads.canForkFromTurn); + assert.isTrue(OpenCodeProviderCapabilitiesV2.turns.supportsActiveSteering); + assert.equal(OpenCodeProviderCapabilitiesV2.turns.terminalStatusQuality, "strong"); + assert.isFalse(OpenCodeProviderCapabilitiesV2.subagents.canCloseSubagents); + }); + + it("maps native permission families to orchestration request kinds", () => { + assert.equal(openCodePermissionRequestKind("bash"), "command"); + assert.equal(openCodePermissionRequestKind("read"), "file-read"); + assert.equal(openCodePermissionRequestKind("grep"), "file-read"); + assert.equal(openCodePermissionRequestKind("external_directory"), "file-read"); + assert.equal(openCodePermissionRequestKind("external_directory", "edit"), "file-change"); + assert.equal(openCodePermissionRequestKind("edit"), "file-change"); + assert.equal(openCodePermissionRequestKind("apply_patch"), "file-change"); + }); + + it("maps OpenCode tools to semantic turn-item families", () => { + assert.equal(openCodeToolProjectionKind("bash"), "command_execution"); + assert.equal(openCodeToolProjectionKind("edit"), "file_change"); + assert.equal(openCodeToolProjectionKind("read"), "file_search"); + assert.equal(openCodeToolProjectionKind("lsp"), "file_search"); + assert.equal(openCodeToolProjectionKind("websearch"), "web_search"); + assert.equal(openCodeToolProjectionKind("codesearch"), "web_search"); + assert.equal(openCodeToolProjectionKind("custom_tool"), "dynamic_tool"); + }); + + it("maps runtime modes to safe OpenCode permission rules", () => { + const approvalRequired = openCodePermissionRules(runtimePolicy("approval-required")); + assert.equal(permissionAction(approvalRequired, "read"), "allow"); + assert.equal(permissionAction(approvalRequired, "edit"), "ask"); + assert.equal(permissionAction(approvalRequired, "bash"), "ask"); + assert.equal(permissionAction(approvalRequired, "doom_loop"), "ask"); + assert.equal(permissionAction(approvalRequired, "unknown_plugin_tool"), "ask"); + assert.equal(permissionAction(approvalRequired, "question"), "allow"); + + const autoAcceptEdits = openCodePermissionRules(runtimePolicy("auto-accept-edits")); + assert.equal(permissionAction(autoAcceptEdits, "edit"), "allow"); + assert.equal(permissionAction(autoAcceptEdits, "bash"), "ask"); + + const fullAccess = openCodePermissionRules(runtimePolicy("full-access")); + assert.equal(permissionAction(fullAccess, "bash"), "allow"); + assert.equal(permissionAction(fullAccess, "edit"), "allow"); + + const granularApproval = openCodePermissionRules( + runtimePolicy("full-access", { + approvalPolicy: { granular: { request_permissions: true } }, + }), + ); + assert.equal(permissionAction(granularApproval, "bash"), "ask"); + assert.equal(permissionAction(granularApproval, "read"), "allow"); + }); + + it("enforces non-interactive sandbox policy through OpenCode permissions", () => { + const readOnly = openCodePermissionRules( + runtimePolicy("full-access", { + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }), + ); + assert.equal(permissionAction(readOnly, "read"), "allow"); + assert.equal(permissionAction(readOnly, "edit"), "deny"); + assert.equal(permissionAction(readOnly, "bash"), "deny"); + assert.equal(permissionAction(readOnly, "webfetch"), "deny"); + assert.equal(permissionAction(readOnly, "doom_loop"), "deny"); + assert.equal(permissionAction(readOnly, "unknown_plugin_tool"), "deny"); + assert.equal(permissionAction(readOnly, "external_directory"), "allow"); + + const workspaceWrite = openCodePermissionRules( + runtimePolicy("auto-accept-edits", { + approvalPolicy: "never", + sandboxPolicy: { + type: "workspaceWrite", + writableRoots: ["/tmp/opencode-workspace"], + networkAccess: true, + }, + }), + ); + assert.equal(permissionAction(workspaceWrite, "edit"), "allow"); + assert.equal(permissionAction(workspaceWrite, "bash"), "deny"); + assert.equal(permissionAction(workspaceWrite, "webfetch"), "allow"); + assert.deepInclude(workspaceWrite, { + permission: "external_directory", + pattern: "/tmp/opencode-workspace/*", + action: "allow", + }); + }); + + it("preserves OpenCode's recursion guard on task-created child sessions", () => { + const childRules = openCodeChildPermissionRules(runtimePolicy("full-access"), [ + { permission: "task", pattern: "*", action: "deny" }, + ]); + + assert.equal(permissionAction(childRules, "read"), "allow"); + assert.equal(permissionAction(childRules, "bash"), "allow"); + assert.equal(permissionAction(childRules, "task"), "deny"); + + const approvalRequiredPolicy = runtimePolicy("approval-required"); + const parentRules = openCodePermissionRules(approvalRequiredPolicy); + const childApprovalRules = openCodeChildPermissionRules(approvalRequiredPolicy, [ + ...parentRules.filter((rule) => rule.action === "deny"), + { permission: "task", pattern: "*", action: "deny" }, + ]); + assert.equal(permissionAction(childApprovalRules, "bash"), "ask"); + assert.equal(permissionAction(childApprovalRules, "task"), "deny"); + }); + + it("uses the next native user message as the exclusive fork and revert boundary", () => { + const first = providerTurn({ id: "turn:first", ordinal: 1, nativeId: "msg-user-1" }); + const synthetic = providerTurn({ id: "turn:synthetic", ordinal: 2, nativeId: null }); + const third = providerTurn({ id: "turn:third", ordinal: 3, nativeId: "msg-user-3" }); + + assert.equal( + openCodeBoundaryAfterProviderTurn([third, first, synthetic], first.id), + "msg-user-3", + ); + assert.isUndefined(openCodeBoundaryAfterProviderTurn([first, synthetic, third], third.id)); + }); +}); diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts new file mode 100644 index 00000000000..e5457c8daf5 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts @@ -0,0 +1,385 @@ +import type { Event as OpenCodeEvent, OpencodeClient } from "@opencode-ai/sdk/v2"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { ProviderReplayEntry, type ProviderReplayTranscript } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Duration from "effect/Duration"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import { ServerConfig } from "../../config.ts"; +import { + OpenCodeRuntime, + OpenCodeRuntimeError, + type OpenCodeRuntimeShape, +} from "../../provider/opencodeRuntime.ts"; +import { layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { ProviderAdapterDriverCreateError } from "../ProviderAdapterDriver.ts"; +import { makeDriverLayer as makeProviderAdapterRegistryDriverLayer } from "../ProviderAdapterRegistry.ts"; +import { + makeReplayServerConfig, + type OrchestratorV2ProviderReplayHarness, +} from "../testkit/ProviderReplayHarness.ts"; +import { + OPENCODE_DEFAULT_INSTANCE_ID, + OPENCODE_DRIVER_KIND, + OPENCODE_PROVIDER, + OpenCodeAdapterV2Driver, +} from "./OpenCodeAdapterV2.ts"; + +export const OPENCODE_SDK_REPLAY_PROTOCOL = "opencode-sdk.sse" as const; + +const OpenCodeSdkReplayTranscript = Schema.Struct({ + provider: Schema.Literal(OPENCODE_PROVIDER), + protocol: Schema.Literal(OPENCODE_SDK_REPLAY_PROTOCOL), + version: Schema.String, + scenario: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + entries: Schema.Array(ProviderReplayEntry), +}); +export type OpenCodeSdkReplayTranscript = typeof OpenCodeSdkReplayTranscript.Type; +const decodeOpenCodeSdkReplayTranscript = Schema.decodeUnknownEffect(OpenCodeSdkReplayTranscript); + +export class OpenCodeReplayTranscriptDecodeError extends Schema.TaggedErrorClass()( + "OpenCodeReplayTranscriptDecodeError", + { + driver: Schema.optional(Schema.String), + protocol: Schema.optional(Schema.String), + scenario: Schema.optional(Schema.String), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to decode OpenCode replay transcript for scenario ${this.scenario ?? ""}.`; + } +} + +export class OpenCodeReplayMismatchError extends Schema.TaggedErrorClass()( + "OpenCodeReplayMismatchError", + { + scenario: Schema.String, + cursor: Schema.Number, + expected: Schema.Unknown, + actual: Schema.Unknown, + }, +) { + override get message(): string { + return `OpenCode replay frame mismatch at cursor ${this.cursor} in scenario ${this.scenario}.`; + } +} + +export class OpenCodeReplayIncompleteError extends Schema.TaggedErrorClass()( + "OpenCodeReplayIncompleteError", + { + scenario: Schema.String, + cursor: Schema.Number, + remaining: Schema.Number, + }, +) { + override get message(): string { + return `OpenCode replay ended with ${this.remaining} unconsumed entries in scenario ${this.scenario}.`; + } +} + +export const OpenCodeReplayError = Schema.Union([ + OpenCodeReplayTranscriptDecodeError, + OpenCodeReplayMismatchError, + OpenCodeReplayIncompleteError, +]); +export type OpenCodeReplayError = typeof OpenCodeReplayError.Type; +export const OpenCodeOrchestratorReplayHarnessError = Schema.Union([ + OpenCodeReplayError, + ProviderAdapterDriverCreateError, +]); +export type OpenCodeOrchestratorReplayHarnessError = + typeof OpenCodeOrchestratorReplayHarnessError.Type; + +function replayValueMatches(expected: unknown, actual: unknown): boolean { + if (expected === "" || expected === "") return true; + if (Array.isArray(expected)) { + return ( + Array.isArray(actual) && + expected.length === actual.length && + expected.every((entry, index) => replayValueMatches(entry, actual[index])) + ); + } + if (typeof expected === "object" && expected !== null) { + if (typeof actual !== "object" || actual === null) return false; + return Object.entries(expected).every(([key, value]) => + replayValueMatches(value, (actual as Record)[key]), + ); + } + return Object.is(expected, actual); +} + +function frameRecord(frame: unknown): Record | null { + return typeof frame === "object" && frame !== null ? (frame as Record) : null; +} + +class OpenCodeReplayController { + private cursor = 0; + private readonly waiters = new Set<() => void>(); + private failure: unknown = null; + private readonly transcript: OpenCodeSdkReplayTranscript; + + constructor(transcript: OpenCodeSdkReplayTranscript) { + this.transcript = transcript; + } + + async expectOutbound(actual: unknown): Promise { + try { + const entry = this.transcript.entries[this.cursor]; + if (entry?.type !== "expect_outbound" || !replayValueMatches(entry.frame, actual)) { + throw new OpenCodeReplayMismatchError({ + scenario: this.transcript.scenario, + cursor: this.cursor, + expected: entry?.type === "expect_outbound" ? entry.frame : (entry ?? null), + actual, + }); + } + this.advance(); + } catch (cause) { + this.fail(cause); + throw cause; + } + } + + async response(operation: string): Promise { + while (true) { + this.throwFailure(); + const entry = this.transcript.entries[this.cursor]; + if (entry?.type === "emit_inbound") { + const frame = frameRecord(entry.frame); + if (frame?.type === "sdk.response" && frame.operation === operation) { + if (entry.afterMs !== undefined && entry.afterMs > 0) { + await Effect.runPromise(Effect.sleep(Duration.millis(entry.afterMs))); + } + const data = frame.data; + this.advance(); + return data; + } + } + if (entry?.type === "runtime_exit") { + const mismatch = new OpenCodeReplayMismatchError({ + scenario: this.transcript.scenario, + cursor: this.cursor, + expected: { type: "sdk.response", operation }, + actual: entry, + }); + this.fail(mismatch); + throw mismatch; + } + await this.changed(); + } + } + + async *events(signal?: AbortSignal): AsyncIterable { + while (true) { + if (signal?.aborted === true) return; + this.throwFailure(); + const entry = this.transcript.entries[this.cursor]; + if (entry?.type === "emit_inbound") { + const frame = frameRecord(entry.frame); + if (frame?.type === "sdk.event") { + if (entry.afterMs !== undefined && entry.afterMs > 0) { + await Effect.runPromise(Effect.sleep(Duration.millis(entry.afterMs))); + } + const event = frame.event as OpenCodeEvent; + this.advance(); + yield event; + continue; + } + } + if (entry?.type === "runtime_exit") { + this.advance(); + if (entry.status === "success") return; + const mismatch = new OpenCodeReplayMismatchError({ + scenario: this.transcript.scenario, + cursor: this.cursor - 1, + expected: { status: "success" }, + actual: entry, + }); + this.fail(mismatch); + throw mismatch; + } + await this.changed(signal); + } + } + + assertComplete(): void { + while (this.transcript.entries[this.cursor]?.type === "runtime_exit") { + const exit = this.transcript.entries[this.cursor]; + if (exit?.type !== "runtime_exit" || exit.status !== "success") break; + this.cursor += 1; + } + this.throwFailure(); + if (this.cursor !== this.transcript.entries.length) { + throw new OpenCodeReplayIncompleteError({ + scenario: this.transcript.scenario, + cursor: this.cursor, + remaining: this.transcript.entries.length - this.cursor, + }); + } + } + + private advance(): void { + this.cursor += 1; + for (const waiter of this.waiters) waiter(); + this.waiters.clear(); + } + + private fail(cause: unknown): void { + this.failure = cause; + for (const waiter of this.waiters) waiter(); + this.waiters.clear(); + } + + private throwFailure(): void { + if (this.failure !== null) throw this.failure; + } + + private changed(signal?: AbortSignal): Promise { + if (signal?.aborted === true) return Promise.resolve(); + return new Promise((resolve) => { + const done = () => { + signal?.removeEventListener("abort", done); + this.waiters.delete(done); + resolve(); + }; + this.waiters.add(done); + signal?.addEventListener("abort", done, { once: true }); + }); + } +} + +function makeReplayClient(controller: OpenCodeReplayController): OpencodeClient { + const request = async (operation: string, input: unknown) => { + await controller.expectOutbound({ type: operation, input }); + return { data: await controller.response(operation) }; + }; + return { + event: { + subscribe: async (_input?: unknown, options?: { readonly signal?: AbortSignal }) => { + await controller.expectOutbound({ type: "event.subscribe" }); + return { stream: controller.events(options?.signal) }; + }, + }, + session: { + create: (input: unknown) => request("session.create", input), + get: (input: unknown) => request("session.get", input), + update: (input: unknown) => request("session.update", input), + messages: (input: unknown) => request("session.messages", input), + promptAsync: (input: unknown) => request("session.promptAsync", input), + abort: (input: unknown) => request("session.abort", input), + revert: (input: unknown) => request("session.revert", input), + unrevert: (input: unknown) => request("session.unrevert", input), + fork: (input: unknown) => request("session.fork", input), + }, + permission: { + reply: (input: unknown) => request("permission.reply", input), + }, + question: { + reply: (input: unknown) => request("question.reply", input), + }, + mcp: { + add: (input: unknown) => request("mcp.add", input), + }, + } as unknown as OpencodeClient; +} + +function makeOpenCodeReplayRuntimeLayer(transcript: OpenCodeSdkReplayTranscript) { + return Layer.effect( + OpenCodeRuntime, + Effect.gen(function* () { + const controller = new OpenCodeReplayController(transcript); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + controller.assertComplete(); + }), + ); + const client = makeReplayClient(controller); + return OpenCodeRuntime.of({ + startOpenCodeServerProcess: () => + Effect.fail( + new OpenCodeRuntimeError({ + operation: "startOpenCodeServerProcess", + detail: "OpenCode replay uses an external in-memory SDK boundary.", + }), + ), + connectToOpenCodeServer: () => + Effect.succeed({ + url: "replay://opencode", + exitCode: null, + external: true, + }), + runOpenCodeCommand: () => + Effect.fail( + new OpenCodeRuntimeError({ + operation: "runOpenCodeCommand", + detail: "OpenCode replay does not execute commands.", + }), + ), + createOpenCodeSdkClient: () => client, + loadOpenCodeInventory: () => + Effect.fail( + new OpenCodeRuntimeError({ + operation: "loadOpenCodeInventory", + detail: "OpenCode replay does not load inventory.", + }), + ), + } satisfies OpenCodeRuntimeShape); + }), + ); +} + +export function makeOpenCodeProviderAdapterRegistryReplayLayer( + transcript: OpenCodeSdkReplayTranscript, +) { + const serverConfigLayer = Layer.effect( + ServerConfig, + makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie), + ).pipe(Layer.provide(NodeServices.layer)); + return makeProviderAdapterRegistryDriverLayer({ + drivers: [OpenCodeAdapterV2Driver], + configMap: { + [OPENCODE_DEFAULT_INSTANCE_ID]: { + driver: OPENCODE_DRIVER_KIND, + config: { serverUrl: "replay://opencode" }, + }, + }, + }).pipe( + Layer.provide( + Layer.mergeAll( + makeOpenCodeReplayRuntimeLayer(transcript), + serverConfigLayer, + NodeServices.layer, + idAllocatorLayer, + ), + ), + ); +} + +function transcriptMetadata(transcript: ProviderReplayTranscript) { + return { + provider: transcript.provider, + protocol: transcript.protocol, + scenario: transcript.scenario, + }; +} + +export const OpenCodeOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness< + OpenCodeSdkReplayTranscript, + OpenCodeOrchestratorReplayHarnessError +> = { + driver: OPENCODE_PROVIDER, + decodeTranscript: (transcript) => + decodeOpenCodeSdkReplayTranscript(transcript).pipe( + Effect.mapError( + (cause) => + new OpenCodeReplayTranscriptDecodeError({ + ...transcriptMetadata(transcript), + cause, + }), + ), + ), + makeProviderAdapterRegistryLayer: makeOpenCodeProviderAdapterRegistryReplayLayer, +}; diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts new file mode 100644 index 00000000000..1255e9330e1 --- /dev/null +++ b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts @@ -0,0 +1,2764 @@ +import type { + Event as OpenCodeEvent, + Message as OpenCodeMessage, + Part as OpenCodePart, + PermissionRequest, + PermissionRuleset, + QuestionRequest, + Session as OpenCodeSession, + Todo as OpenCodeTodo, + ToolPart, +} from "@opencode-ai/sdk/v2"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; +import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; +import { + defaultInstanceIdForDriver, + type ModelSelection, + type OpenCodeSettings, + type OrchestrationV2AppThread, + type OrchestrationV2ConversationMessage, + type OrchestrationV2ExecutionNode, + type OrchestrationV2PlanStep, + type OrchestrationV2ProviderCapabilities, + type OrchestrationV2ProviderRef, + type OrchestrationV2ProviderSession, + type OrchestrationV2ProviderThread, + type OrchestrationV2ProviderTurn, + type OrchestrationV2RawProviderEvent, + type OrchestrationV2RuntimeRequest, + type OrchestrationV2Subagent, + type OrchestrationV2TurnItem, + OpenCodeSettings as OpenCodeSettingsSchema, + type PlanId, + ProviderDriverKind, + type ProviderInstanceId, + type ProviderRequestKind, + type RuntimeRequestId, + type ThreadId, +} from "@t3tools/contracts"; +import * as Cause from "effect/Cause"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Scope from "effect/Scope"; +import * as Stream from "effect/Stream"; + +import { resolveAttachmentPath } from "../../attachmentStore.ts"; +import { ServerConfig } from "../../config.ts"; +import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; +import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; +import { + OpenCodeRuntime, + OpenCodeRuntimeError, + openCodeQuestionId, + openCodeRuntimeErrorDetail, + parseOpenCodeModelSlug, + runOpenCodeSdk, + toOpenCodeFileParts, + toOpenCodePermissionReply, + toOpenCodeQuestionAnswers, + type OpenCodeRuntimeShape, +} from "../../provider/opencodeRuntime.ts"; +import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts"; +import { + ProviderAdapterEnsureThreadError, + ProviderAdapterForkThreadError, + ProviderAdapterInterruptError, + ProviderAdapterOpenSessionError, + ProviderAdapterProtocolError, + ProviderAdapterReadThreadSnapshotError, + ProviderAdapterResumeThreadError, + ProviderAdapterRollbackThreadError, + ProviderAdapterRuntimeRequestResponseError, + ProviderAdapterSteerRunError, + ProviderAdapterTurnStartError, + ProviderAdapterV2, + type ProviderAdapterV2Event, + type ProviderAdapterV2OpenSessionInput, + type ProviderAdapterV2SessionRuntime, + type ProviderAdapterV2Shape, + type ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2TurnInput, +} from "../ProviderAdapter.ts"; +import { + ProviderAdapterDriverCreateError, + type ProviderAdapterDriver, + type ProviderAdapterDriverCreateInput, +} from "../ProviderAdapterDriver.ts"; +import { makeSubagentChildThread, subagentThreadTitle } from "../SubagentProjection.ts"; + +export const OPENCODE_PROVIDER = ProviderDriverKind.make("opencode"); +export const OPENCODE_DRIVER_KIND = OPENCODE_PROVIDER; +export const OPENCODE_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(OPENCODE_DRIVER_KIND); +const DEFAULT_OPENCODE_SETTINGS = Schema.decodeSync(OpenCodeSettingsSchema)({}); + +/** + * OpenCode's session, message, part, and interaction-request identifiers are + * durable. It does not expose a first-class turn object: the initiating user + * message is the best native turn correlation point, and session idle is the + * authoritative terminal signal. + */ +export const OpenCodeProviderCapabilitiesV2 = { + sessions: { + // The current adapter owns one directory-bound client/server per session. + // Keep it isolated until its runtime is made safe for cross-thread pooling. + supportsMultipleProviderThreadsPerSession: false, + supportsModelSwitchInSession: true, + supportsProviderSwitchingViaHandoff: true, + supportsRuntimeModeSwitchInSession: false, + pendingRequestsSurviveRestart: false, + }, + threads: { + canCreateEmptyThread: true, + canReadThreadSnapshot: true, + canRollbackThread: true, + canForkThread: true, + canForkFromTurn: true, + canForkFromSubagentThread: true, + exposesNativeThreadId: true, + }, + turns: { + exposesNativeTurnId: false, + emitsTurnStarted: true, + emitsTurnCompleted: true, + supportsInterrupt: true, + supportsActiveSteering: true, + supportsSteeringByInterruptRestart: true, + supportsQueuedMessages: true, + terminalStatusQuality: "strong", + }, + streaming: { + streamsAssistantText: true, + streamsReasoning: true, + streamsToolOutput: true, + streamsPlanText: false, + emitsMessageCompleted: true, + }, + tools: { + exposesToolItemIds: true, + emitsToolStarted: true, + emitsToolCompleted: true, + emitsToolOutput: true, + supportsMcpTools: true, + supportsDynamicToolCallbacks: false, + }, + approvals: { + supportsCommandApproval: true, + supportsFileReadApproval: true, + supportsFileChangeApproval: true, + supportsApplyPatchApproval: true, + approvalsHaveNativeRequestIds: true, + approvalCallbacksAreLiveOnly: true, + approvalsCanOriginateFromSubagents: true, + }, + planning: { + emitsPlanUpdated: true, + emitsTodoList: true, + emitsProposedPlan: false, + supportsStructuredQuestions: true, + planDeltasHaveItemIds: false, + }, + subagents: { + supportsSubagents: true, + exposesSubagentThreadIds: true, + emitsSubagentLifecycle: true, + canWaitForSubagents: true, + canCloseSubagents: false, + canForkSubagentThread: true, + }, + context: { + acceptsSystemContext: false, + acceptsDeveloperContext: false, + acceptsSyntheticUserContext: true, + canGenerateSummaries: true, + canConsumeHandoffSummaries: true, + supportsDeltaHandoff: true, + supportsFullThreadHandoff: true, + maxRecommendedHandoffChars: null, + }, + checkpointing: { + appCanCheckpointFilesystem: true, + supportsNestedCheckpointScopes: true, + providerCanRollbackConversation: true, + providerRollbackReturnsSnapshot: true, + providerCanReadConversationSnapshot: true, + }, + identity: { + nativeThreadIds: "strong", + nativeTurnIds: "weak", + nativeItemIds: "strong", + nativeRequestIds: "strong", + }, +} satisfies OrchestrationV2ProviderCapabilities; + +type TerminalTurnStatus = Extract< + OrchestrationV2ProviderTurn["status"], + "completed" | "interrupted" | "failed" | "cancelled" +>; + +interface ActiveOpenCodeTurn { + readonly isRoot: boolean; + readonly threadId: ThreadId; + readonly runId: ProviderAdapterV2TurnInput["runId"] | null; + readonly rootNodeId: ProviderAdapterV2TurnInput["rootNodeId"]; + readonly appThread: OrchestrationV2AppThread; + readonly modelSelection: ModelSelection; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; + readonly providerTurnId: OrchestrationV2ProviderTurn["id"]; + readonly providerTurnOrdinal: number; + readonly runAttemptId: OrchestrationV2ProviderTurn["runAttemptId"]; + readonly startedAt: DateTime.Utc; + readonly itemOrdinals: Map; + readonly parts: Map; + readonly partIdsByMessage: Map>; + readonly providerTurn: OrchestrationV2ProviderTurn; + nextItemOrdinal: number; + nativeUserMessageId: string | null; + interrupted: boolean; + finalized: boolean; + planId: PlanId | null; +} + +interface OpenCodeSubagentContext { + readonly nativeItemId: string; + readonly nodeId: OrchestrationV2Subagent["id"]; + readonly parentTurn: ActiveOpenCodeTurn; + readonly prompt: string; + readonly title: string | null; + readonly startedAt: DateTime.Utc; + childSessionId: string | null; + childThreadId: ThreadId | null; + childProviderThreadId: OrchestrationV2ProviderThread["id"] | null; + model: string | null; + result: string | null; +} + +interface OpenCodeThreadState { + readonly nativeSessionId: string; + providerThread: OrchestrationV2ProviderThread; + appThread: OrchestrationV2AppThread | null; + activeTurn: ActiveOpenCodeTurn | null; + readonly providerTurns: Map; + readonly messages: Map; + readonly runtimeRequests: Map; + readonly messageRoles: Map; + readonly userMessageIds: Array; + parentSubagent: OpenCodeSubagentContext | null; + nextChildTurnOrdinal: number; +} + +interface PendingOpenCodeRequest { + readonly requestId: RuntimeRequestId; + readonly nativeRequestId: string; + readonly turn: ActiveOpenCodeTurn; + readonly state: OpenCodeThreadState; + readonly nodeId: OrchestrationV2ExecutionNode["id"]; + readonly turnItemId: OrchestrationV2TurnItem["id"]; + readonly requestKind: OpenCodePermissionRequestKind | "user_input"; + readonly createdAt: DateTime.Utc; + readonly permission?: PermissionRequest; + readonly question?: QuestionRequest; +} + +export interface OpenCodeAdapterV2Options { + readonly instanceId: ProviderInstanceId; + readonly settings: OpenCodeSettings; + readonly environment: NodeJS.ProcessEnv; + readonly runtime: OpenCodeRuntimeShape; + readonly idAllocator: IdAllocatorV2Shape; + readonly serverConfig: ServerConfig["Service"]; +} + +function protocolError(detail: string, payload?: unknown): ProviderAdapterProtocolError { + return new ProviderAdapterProtocolError({ + driver: OPENCODE_PROVIDER, + detail, + ...(payload === undefined ? {} : { payload }), + }); +} + +function nativeThreadId(providerThread: OrchestrationV2ProviderThread): string { + const nativeId = providerThread.nativeThreadRef?.nativeId; + if (nativeId === null || nativeId === undefined) { + throw protocolError(`Provider thread ${providerThread.id} has no OpenCode session id`); + } + return nativeId; +} + +function dateTimeFromEpoch(value: number | undefined, fallback: DateTime.Utc): DateTime.Utc { + if (value === undefined) return fallback; + return Option.getOrElse(DateTime.make(value), () => fallback); +} + +function nonEmptyString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value : undefined; +} + +function recordValue(input: unknown, key: string): unknown { + return typeof input === "object" && input !== null && key in input + ? (input as Record)[key] + : undefined; +} + +function recordString(input: unknown, ...keys: ReadonlyArray): string | undefined { + for (const key of keys) { + const value = nonEmptyString(recordValue(input, key)); + if (value !== undefined) return value; + } + return undefined; +} + +function recordNumber(input: unknown, ...keys: ReadonlyArray): number | undefined { + for (const key of keys) { + const value = recordValue(input, key); + if (typeof value === "number" && Number.isFinite(value)) return value; + } + return undefined; +} + +function stableJson(value: unknown): string { + if (typeof value === "string") return value; + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function sdkResponseForRawLog(value: unknown): unknown { + if (typeof value !== "object" || value === null) return value; + if ("data" in value) return { data: (value as { readonly data?: unknown }).data ?? null }; + if ("stream" in value) return { subscribed: true }; + return value; +} + +type OpenCodePermissionRequestKind = Extract< + ProviderRequestKind, + "command" | "file-read" | "file-change" +>; + +export function openCodePermissionRequestKind( + permission: string, + toolName?: string, +): OpenCodePermissionRequestKind { + const normalized = permission.toLowerCase(); + const normalizedTool = toolName?.toLowerCase() ?? ""; + if ( + normalized === "edit" || + normalized.includes("write") || + normalized.includes("patch") || + normalizedTool.includes("edit") || + normalizedTool.includes("write") || + normalizedTool.includes("patch") + ) { + return "file-change"; + } + if ( + normalized === "read" || + normalized === "glob" || + normalized === "grep" || + normalized === "lsp" || + normalized === "external_directory" || + normalizedTool === "read" || + normalizedTool.includes("glob") || + normalizedTool.includes("grep") || + normalizedTool.includes("search") + ) { + return "file-read"; + } + return "command"; +} + +export function openCodeToolProjectionKind( + toolName: string, +): "command_execution" | "file_change" | "file_search" | "web_search" | "dynamic_tool" { + const normalized = toolName.toLowerCase(); + if (normalized.includes("bash") || normalized.includes("shell")) { + return "command_execution"; + } + if (normalized.includes("edit") || normalized.includes("write") || normalized.includes("patch")) { + return "file_change"; + } + if (normalized.includes("web") || normalized === "codesearch" || normalized === "code_search") { + return "web_search"; + } + if ( + normalized === "read" || + normalized.includes("glob") || + normalized.includes("grep") || + normalized.includes("search") || + normalized.includes("lsp") + ) { + return "file_search"; + } + return "dynamic_tool"; +} + +const OPENCODE_ALWAYS_ALLOWED_PERMISSIONS = [ + "question", + "read", + "glob", + "grep", + "lsp", + "todowrite", + "task", + "skill", +] as const; + +const OPENCODE_RESTRICTED_PERMISSIONS = [ + "bash", + "edit", + "webfetch", + "websearch", + "codesearch", + "external_directory", + "doom_loop", +] as const; + +/** + * OpenCode does not provide an OS sandbox, so permission rules are also the + * enforcement boundary for non-interactive policies. Read/planning tools are + * safe by default; edits are auto-approved only for workspace-write modes, + * while shell/network/external access remains gated unless policy explicitly + * allows it. + */ +export function openCodePermissionRules( + runtimePolicy: ProviderAdapterV2RuntimePolicy, +): PermissionRuleset { + const sandboxPolicy = recordValue(runtimePolicy, "sandboxPolicy"); + const sandboxType = recordString(sandboxPolicy, "type"); + const rawApprovalPolicy = runtimePolicy.approvalPolicy; + const approvalPolicy = nonEmptyString(rawApprovalPolicy); + const requiresApproval = + approvalPolicy === undefined + ? (typeof rawApprovalPolicy === "object" && rawApprovalPolicy !== null) || + runtimePolicy.runtimeMode !== "full-access" + : approvalPolicy !== "never"; + const externallySandboxed = sandboxType === "externalSandbox"; + const dangerFullAccess = sandboxType === "dangerFullAccess"; + const implicitFullAccess = + sandboxType === undefined && runtimePolicy.runtimeMode === "full-access"; + + if (!requiresApproval && (externallySandboxed || dangerFullAccess || implicitFullAccess)) { + return [{ permission: "*", pattern: "*", action: "allow" }]; + } + + // Task sessions initially inherit only parent deny rules. Seed explicit + // denies before the effective ask/allow overrides so a child is safe during + // the short interval before emitSubagent installs its complete policy. + const rules: PermissionRuleset = [ + { permission: "*", pattern: "*", action: "deny" }, + ...OPENCODE_RESTRICTED_PERMISSIONS.map((permission) => ({ + permission, + pattern: "*", + action: "deny" as const, + })), + ]; + + if (requiresApproval) { + rules.push({ permission: "*", pattern: "*", action: "ask" }); + for (const permission of OPENCODE_RESTRICTED_PERMISSIONS) { + rules.push({ permission, pattern: "*", action: "ask" }); + } + } + + rules.push( + ...OPENCODE_ALWAYS_ALLOWED_PERMISSIONS.map((permission) => ({ + permission, + pattern: "*", + action: "allow" as const, + })), + ); + + if (runtimePolicy.runtimeMode === "auto-accept-edits" || sandboxType === "workspaceWrite") { + rules.push({ permission: "edit", pattern: "*", action: "allow" }); + } + + if (!requiresApproval && recordValue(sandboxPolicy, "networkAccess") === true) { + for (const permission of ["webfetch", "websearch", "codesearch"] as const) { + rules.push({ permission, pattern: "*", action: "allow" }); + } + } + + if (!requiresApproval && sandboxType === "readOnly") { + const access = recordValue(sandboxPolicy, "access"); + if (recordString(access, "type") === "fullAccess") { + rules.push({ permission: "external_directory", pattern: "*", action: "allow" }); + } + } + + if (!requiresApproval && sandboxType === "workspaceWrite") { + const writableRoots = recordValue(sandboxPolicy, "writableRoots"); + if (Array.isArray(writableRoots)) { + for (const root of writableRoots) { + if (typeof root === "string" && root.trim().length > 0) { + rules.push({ + permission: "external_directory", + pattern: `${root.replace(/\/$/, "")}/*`, + action: "allow", + }); + } + } + } + } + + return rules; +} + +function permissionRuleEquals( + left: PermissionRuleset[number], + right: PermissionRuleset[number], +): boolean { + return ( + left.permission === right.permission && + left.pattern === right.pattern && + left.action === right.action + ); +} + +/** + * OpenCode task sessions inherit only the parent's deny/external-directory + * rules and then add agent-specific restrictions such as disabling nested + * tasks. Install the complete parent policy while retaining only rules that + * were added specifically for the selected child agent. + */ +export function openCodeChildPermissionRules( + runtimePolicy: ProviderAdapterV2RuntimePolicy, + nativeChildRules: PermissionRuleset, +): PermissionRuleset { + const parentRules = openCodePermissionRules(runtimePolicy); + const inheritedRules = parentRules.filter( + (rule) => rule.permission === "external_directory" || rule.action === "deny", + ); + const childSpecificRules = nativeChildRules.filter( + (childRule) => + !inheritedRules.some((inheritedRule) => permissionRuleEquals(childRule, inheritedRule)), + ); + return [...parentRules, ...childSpecificRules]; +} + +/** + * OpenCode's fork/revert boundary is exclusive. To retain the selected app + * turn, address the next native user message; omitting a boundary retains the + * current head when the selected turn is already last. + */ +export function openCodeBoundaryAfterProviderTurn( + providerTurns: ReadonlyArray, + selectedProviderTurnId: OrchestrationV2ProviderTurn["id"], +): string | undefined { + const selected = providerTurns.find((turn) => turn.id === selectedProviderTurnId); + if (selected === undefined) return undefined; + return providerTurns + .filter((turn) => turn.ordinal > selected.ordinal) + .toSorted((left, right) => left.ordinal - right.ordinal) + .map((turn) => turn.nativeTurnRef?.nativeId) + .find((nativeId): nativeId is string => nativeId !== null && nativeId !== undefined); +} + +function toolStatus(part: ToolPart): { + readonly node: OrchestrationV2ExecutionNode["status"]; + readonly item: OrchestrationV2TurnItem["status"]; +} { + switch (part.state.status) { + case "pending": + return { node: "pending", item: "pending" }; + case "running": + return { node: "running", item: "running" }; + case "completed": + return { node: "completed", item: "completed" }; + case "error": + return { node: "failed", item: "failed" }; + } +} + +function toolInput(part: ToolPart): Record { + return part.state.input; +} + +function toolOutput(part: ToolPart): string | undefined { + if (part.state.status === "completed") return part.state.output; + if (part.state.status === "error") return part.state.error; + return undefined; +} + +function toolStartedAt(part: ToolPart, now: DateTime.Utc): DateTime.Utc { + return dateTimeFromEpoch( + part.state.status === "pending" ? undefined : part.state.time.start, + now, + ); +} + +function toolCompletedAt(part: ToolPart, now: DateTime.Utc): DateTime.Utc | null { + return part.state.status === "completed" || part.state.status === "error" + ? dateTimeFromEpoch(part.state.time.end, now) + : null; +} + +function toolTitle(part: ToolPart): string | null { + return part.state.status === "running" || part.state.status === "completed" + ? (part.state.title ?? null) + : null; +} + +function toolModel(part: ToolPart): string | null { + const metadata = + part.state.status === "running" || + part.state.status === "completed" || + part.state.status === "error" + ? part.state.metadata + : undefined; + const model = recordValue(metadata, "model"); + const providerId = recordString(model, "providerID", "providerId"); + const modelId = recordString(model, "modelID", "modelId", "id"); + return providerId !== undefined && modelId !== undefined ? `${providerId}/${modelId}` : null; +} + +function taskSessionId(part: ToolPart): string | null { + const metadata = + part.state.status === "running" || + part.state.status === "completed" || + part.state.status === "error" + ? part.state.metadata + : undefined; + return recordString(metadata, "sessionId", "sessionID") ?? null; +} + +function makeProviderThread(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly providerInstanceId: ProviderInstanceId; + readonly providerSessionId: OrchestrationV2ProviderThread["providerSessionId"]; + readonly appThreadId: OrchestrationV2ProviderThread["appThreadId"]; + readonly ownerNodeId?: OrchestrationV2ProviderThread["ownerNodeId"]; + readonly nativeSession: OpenCodeSession; + readonly forkedFrom?: OrchestrationV2ProviderThread["forkedFrom"]; + readonly now: DateTime.Utc; +}): OrchestrationV2ProviderThread { + const createdAt = dateTimeFromEpoch(input.nativeSession.time.created, input.now); + return { + id: input.idAllocator.derive.providerThread({ + driver: OPENCODE_PROVIDER, + nativeThreadId: input.nativeSession.id, + }), + driver: OPENCODE_PROVIDER, + providerInstanceId: input.providerInstanceId, + providerSessionId: input.providerSessionId, + appThreadId: input.appThreadId, + ownerNodeId: input.ownerNodeId ?? null, + nativeThreadRef: { + driver: OPENCODE_PROVIDER, + nativeId: input.nativeSession.id, + strength: "strong", + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: input.forkedFrom ?? null, + createdAt, + updatedAt: dateTimeFromEpoch(input.nativeSession.time.updated, input.now), + }; +} + +function providerRef(nativeId: string, strength: "strong" | "weak" = "strong") { + return { + driver: OPENCODE_PROVIDER, + nativeId, + strength, + } satisfies OrchestrationV2ProviderRef; +} + +function openCodeErrorMessage(event: Extract): string { + const error = event.properties.error; + if (error === undefined) return "OpenCode session failed without an error payload."; + return recordString(error.data, "message") ?? error.name; +} + +function terminalStatusForError( + event: Extract, + turn: ActiveOpenCodeTurn, +): TerminalTurnStatus { + return turn.interrupted || isMessageAbortedError(event) ? "interrupted" : "failed"; +} + +function isMessageAbortedError(event: Extract): boolean { + return event.properties.error?.name === "MessageAbortedError"; +} + +function unwrapData(operation: string, result: { readonly data?: A }): NonNullable { + if (result.data === undefined) { + throw new OpenCodeRuntimeError({ + operation, + detail: `OpenCode ${operation} returned no response payload.`, + }); + } + return result.data as NonNullable; +} + +export function makeOpenCodeAdapterV2(options: OpenCodeAdapterV2Options): ProviderAdapterV2Shape { + const { idAllocator, runtime, serverConfig } = options; + + return ProviderAdapterV2.of({ + instanceId: options.instanceId, + driver: OPENCODE_PROVIDER, + getCapabilities: () => Effect.succeed(OpenCodeProviderCapabilitiesV2), + openSession: Effect.fn("OpenCodeAdapterV2.openSession")( + function* (input: ProviderAdapterV2OpenSessionInput) { + const scope = yield* Effect.scope; + const cwd = input.runtimePolicy.cwd ?? serverConfig.cwd; + const connection = yield* runtime.connectToOpenCodeServer({ + binaryPath: options.settings.binaryPath, + serverUrl: options.settings.serverUrl, + environment: options.environment, + }); + const client = runtime.createOpenCodeSdkClient({ + baseUrl: connection.url, + directory: cwd, + ...(connection.external && options.settings.serverPassword + ? { serverPassword: options.settings.serverPassword } + : {}), + }); + + const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); + if (mcpSession !== undefined && !connection.external) { + yield* runOpenCodeSdk("mcp.add", () => + client.mcp.add({ + name: "t3-code", + config: { + type: "remote", + url: mcpSession.endpoint, + headers: { Authorization: mcpSession.authorizationHeader }, + oauth: false, + }, + }), + ); + } + + const now = yield* DateTime.now; + let sessionEntity: OrchestrationV2ProviderSession = { + id: input.providerSessionId, + driver: OPENCODE_PROVIDER, + providerInstanceId: options.instanceId, + status: "ready", + cwd, + model: input.modelSelection.model, + capabilities: OpenCodeProviderCapabilitiesV2, + createdAt: now, + updatedAt: now, + lastError: null, + }; + const events = yield* Queue.unbounded(); + const rawEvents = yield* Queue.unbounded(); + const rawSequence = yield* Ref.make(0); + const threads = new Map(); + const pendingRequests = new Map(); + const pendingRequestsByNativeId = new Map(); + const subagentsByNativeItemId = new Map(); + const subagentsByChildSessionId = new Map(); + const abortController = new AbortController(); + + const emitProviderEvent = (event: ProviderAdapterV2Event) => + Queue.offer(events, event).pipe(Effect.asVoid); + + const emitRawEvent = (raw: { + readonly direction: "incoming" | "outgoing"; + readonly messageKind: OrchestrationV2RawProviderEvent["messageKind"]; + readonly method: string; + readonly payload: unknown; + }): Effect.Effect => + Effect.gen(function* () { + const sequence = yield* Ref.updateAndGet(rawSequence, (value) => value + 1); + const observedAt = yield* DateTime.now; + const rawEventId = yield* idAllocator.allocate.rawEvent({ + providerSessionId: input.providerSessionId, + method: raw.method, + }); + yield* Queue.offer(rawEvents, { + id: rawEventId, + driver: OPENCODE_PROVIDER, + providerInstanceId: options.instanceId, + providerSessionId: input.providerSessionId, + sequence, + direction: raw.direction, + messageKind: raw.messageKind, + method: raw.method, + jsonRpcId: null, + payload: raw.payload, + observedAt, + }); + }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("orchestration-v2.opencode-raw-event-failed", { + providerSessionId: input.providerSessionId, + cause, + }), + ), + ); + + const sdkCall = ( + method: string, + payload: unknown, + call: () => Promise, + ): Effect.Effect => + emitRawEvent({ + direction: "outgoing", + messageKind: "request", + method, + payload, + }).pipe( + Effect.andThen(runOpenCodeSdk(method, call)), + Effect.tap((response) => + emitRawEvent({ + direction: "incoming", + messageKind: "response", + method, + payload: sdkResponseForRawLog(response), + }), + ), + ); + + const updateProviderSession = ( + status: OrchestrationV2ProviderSession["status"], + lastError: string | null = sessionEntity.lastError, + ) => + Effect.gen(function* () { + const updatedAt = yield* DateTime.now; + sessionEntity = { ...sessionEntity, status, lastError, updatedAt }; + yield* emitProviderEvent({ + type: "provider_session.updated", + driver: OPENCODE_PROVIDER, + providerSession: sessionEntity, + }); + }); + + const updateProviderThread = ( + state: OpenCodeThreadState, + patch: Partial, + ) => + Effect.gen(function* () { + const updatedAt = yield* DateTime.now; + state.providerThread = { ...state.providerThread, ...patch, updatedAt }; + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver: OPENCODE_PROVIDER, + providerThread: state.providerThread, + }); + }); + + const itemOrdinal = (turn: ActiveOpenCodeTurn, nativeItemId: string): number => { + const existing = turn.itemOrdinals.get(nativeItemId); + if (existing !== undefined) return existing; + const ordinal = turn.nextItemOrdinal++; + turn.itemOrdinals.set(nativeItemId, ordinal); + return ordinal; + }; + + const emitProviderTurn = ( + state: OpenCodeThreadState, + turn: ActiveOpenCodeTurn, + status: OrchestrationV2ProviderTurn["status"], + completedAt: DateTime.Utc | null, + ) => { + const providerTurn: OrchestrationV2ProviderTurn = { + ...turn.providerTurn, + nativeTurnRef: + turn.nativeUserMessageId === null + ? turn.providerTurn.nativeTurnRef + : providerRef(turn.nativeUserMessageId, "weak"), + status, + completedAt, + }; + Object.assign(turn.providerTurn, providerTurn); + state.providerTurns.set(String(providerTurn.id), providerTurn); + return emitProviderEvent({ + type: "provider_turn.updated", + driver: OPENCODE_PROVIDER, + threadId: turn.threadId, + providerTurn, + }); + }; + + const emitTextPart = Effect.fnUntraced(function* ( + state: OpenCodeThreadState, + turn: ActiveOpenCodeTurn, + part: Extract, + forceCompleted = false, + ) { + if (part.type === "text" && (part.ignored === true || part.synthetic === true)) return; + if (part.text.length === 0) return; + const emittedAt = yield* DateTime.now; + const isCompleted = forceCompleted || part.time?.end !== undefined; + const startedAt = dateTimeFromEpoch(part.time?.start, emittedAt); + const completedAt = isCompleted ? dateTimeFromEpoch(part.time?.end, emittedAt) : null; + const nativeItemRef = providerRef(part.id); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: part.id, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: part.id, + }); + const ordinal = itemOrdinal(turn, part.id); + yield* emitProviderEvent({ + type: "node.updated", + driver: OPENCODE_PROVIDER, + node: { + id: nodeId, + threadId: turn.threadId, + runId: turn.runId, + parentNodeId: turn.rootNodeId, + rootNodeId: turn.rootNodeId, + kind: part.type === "text" ? "assistant_message" : "reasoning", + status: isCompleted ? "completed" : "running", + countsForRun: false, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt, + completedAt, + }, + }); + if (part.type === "text") { + const messageId = idAllocator.derive.messageFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: part.id, + }); + const message: OrchestrationV2ConversationMessage = { + createdBy: "agent", + creationSource: "provider", + id: messageId, + threadId: turn.threadId, + runId: turn.runId, + nodeId, + role: "assistant", + text: part.text, + attachments: [], + streaming: !isCompleted, + createdAt: startedAt, + updatedAt: emittedAt, + }; + state.messages.set(String(message.id), message); + yield* emitProviderEvent({ + type: "message.updated", + driver: OPENCODE_PROVIDER, + message, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: OPENCODE_PROVIDER, + turnItem: { + id: turnItemId, + threadId: turn.threadId, + runId: turn.runId, + nodeId, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: isCompleted ? "completed" : "running", + title: null, + startedAt, + completedAt, + updatedAt: emittedAt, + type: "assistant_message", + messageId, + text: part.text, + streaming: !isCompleted, + }, + }); + return; + } + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: OPENCODE_PROVIDER, + turnItem: { + id: turnItemId, + threadId: turn.threadId, + runId: turn.runId, + nodeId, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal, + status: isCompleted ? "completed" : "running", + title: null, + startedAt, + completedAt, + updatedAt: emittedAt, + type: "reasoning", + text: part.text, + streaming: !isCompleted, + }, + }); + }); + + const emitSubagent = Effect.fnUntraced(function* ( + state: OpenCodeThreadState, + turn: ActiveOpenCodeTurn, + part: ToolPart, + ) { + const now = yield* DateTime.now; + const nativeItemRef = providerRef(part.id); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: part.id, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: part.id, + }); + const input = toolInput(part); + const prompt = recordString(input, "prompt") ?? ""; + const title = toolTitle(part) ?? recordString(input, "description") ?? null; + let context = subagentsByNativeItemId.get(part.id); + if (context === undefined) { + context = { + nativeItemId: part.id, + nodeId, + parentTurn: turn, + prompt, + title, + startedAt: toolStartedAt(part, now), + childSessionId: null, + childThreadId: null, + childProviderThreadId: null, + model: null, + result: null, + }; + subagentsByNativeItemId.set(part.id, context); + } + context.model = toolModel(part) ?? context.model; + const childSessionId = taskSessionId(part); + if (childSessionId !== null && context.childSessionId === null) { + context.childSessionId = childSessionId; + context.childThreadId = idAllocator.derive.threadFromProviderThread({ + driver: OPENCODE_PROVIDER, + nativeThreadId: childSessionId, + }); + context.childProviderThreadId = idAllocator.derive.providerThread({ + driver: OPENCODE_PROVIDER, + nativeThreadId: childSessionId, + }); + subagentsByChildSessionId.set(childSessionId, context); + const childModelSelection: ModelSelection = { + instanceId: options.instanceId, + model: context.model ?? turn.modelSelection.model, + }; + const childThread = makeSubagentChildThread({ + parentThread: turn.appThread, + childThreadId: context.childThreadId, + parentNodeId: nodeId, + activeProviderThreadId: context.childProviderThreadId, + providerInstanceId: options.instanceId, + modelSelection: childModelSelection, + title: subagentThreadTitle({ + parentTitle: turn.appThread.title, + title, + prompt, + ordinal: itemOrdinal(turn, part.id), + }), + now, + createdBy: "agent", + creationSource: "provider", + }); + const childProviderThread: OrchestrationV2ProviderThread = { + id: context.childProviderThreadId, + driver: OPENCODE_PROVIDER, + providerInstanceId: options.instanceId, + providerSessionId: inputProviderSessionId, + appThreadId: context.childThreadId, + ownerNodeId: nodeId, + nativeThreadRef: providerRef(childSessionId), + nativeConversationHeadRef: null, + status: "active", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: null, + createdAt: now, + updatedAt: now, + }; + const childSessionResponse = yield* sdkCall( + "session.get", + { sessionID: childSessionId }, + () => client.session.get({ sessionID: childSessionId }), + ); + const nativeChildSession = unwrapData("session.get", childSessionResponse); + const childPermission = openCodeChildPermissionRules( + turn.runtimePolicy, + nativeChildSession.permission ?? [], + ); + yield* sdkCall( + "session.update", + { sessionID: childSessionId, permission: childPermission }, + () => + client.session.update({ + sessionID: childSessionId, + permission: childPermission, + }), + ); + threads.set(childSessionId, { + nativeSessionId: childSessionId, + providerThread: childProviderThread, + appThread: childThread, + activeTurn: null, + providerTurns: new Map(), + messages: new Map(), + runtimeRequests: new Map(), + messageRoles: new Map(), + userMessageIds: [], + parentSubagent: context, + nextChildTurnOrdinal: 1, + }); + yield* emitProviderEvent({ + type: "app_thread.created", + driver: OPENCODE_PROVIDER, + appThread: childThread, + }); + yield* emitProviderEvent({ + type: "provider_thread.updated", + driver: OPENCODE_PROVIDER, + providerThread: childProviderThread, + }); + } + const output = toolOutput(part); + if (part.state.status === "completed" && output !== undefined) context.result = output; + const status = toolStatus(part); + const completedAt = toolCompletedAt(part, now); + const subagentStatus: OrchestrationV2Subagent["status"] = + status.item === "failed" + ? "failed" + : status.item === "completed" + ? "completed" + : status.item === "pending" + ? "pending" + : "running"; + const subagent: OrchestrationV2Subagent = { + id: nodeId, + threadId: turn.threadId, + runId: turn.runId, + parentNodeId: turn.rootNodeId, + origin: "provider_native", + createdBy: "agent", + driver: OPENCODE_PROVIDER, + providerInstanceId: options.instanceId, + providerThreadId: context.childProviderThreadId, + childThreadId: context.childThreadId, + nativeTaskRef: nativeItemRef, + prompt, + title, + model: context.model, + status: subagentStatus, + result: context.result, + startedAt: context.startedAt, + completedAt, + updatedAt: now, + }; + yield* emitProviderEvent({ + type: "node.updated", + driver: OPENCODE_PROVIDER, + node: { + id: nodeId, + threadId: turn.threadId, + runId: turn.runId, + parentNodeId: turn.rootNodeId, + rootNodeId: turn.rootNodeId, + kind: "subagent", + status: status.node, + countsForRun: false, + providerThreadId: context.childProviderThreadId ?? state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: context.startedAt, + completedAt, + }, + }); + yield* emitProviderEvent({ + type: "subagent.updated", + driver: OPENCODE_PROVIDER, + subagent, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: OPENCODE_PROVIDER, + turnItem: { + id: turnItemId, + threadId: turn.threadId, + runId: turn.runId, + nodeId, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal: itemOrdinal(turn, part.id), + status: status.item, + title, + startedAt: context.startedAt, + completedAt, + updatedAt: now, + type: "subagent", + subagentId: nodeId, + origin: "provider_native", + driver: OPENCODE_PROVIDER, + providerInstanceId: options.instanceId, + childThreadId: context.childThreadId, + prompt, + result: context.result, + }, + }); + }); + + const emitToolPart = Effect.fnUntraced(function* ( + state: OpenCodeThreadState, + turn: ActiveOpenCodeTurn, + part: ToolPart, + ) { + const normalizedTool = part.tool.toLowerCase(); + if (normalizedTool === "task") { + yield* emitSubagent(state, turn, part); + return; + } + // question.asked carries the respondable semantic item. Projecting + // the implementation tool as well would duplicate it in the UI. + if (normalizedTool === "question") return; + const now = yield* DateTime.now; + const status = toolStatus(part); + const startedAt = toolStartedAt(part, now); + const completedAt = toolCompletedAt(part, now); + const nativeItemRef = providerRef(part.id); + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: part.id, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: part.id, + }); + const base = { + id: turnItemId, + threadId: turn.threadId, + runId: turn.runId, + nodeId, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef, + parentItemId: null, + ordinal: itemOrdinal(turn, part.id), + status: status.item, + title: toolTitle(part), + startedAt, + completedAt, + updatedAt: now, + } satisfies Pick< + OrchestrationV2TurnItem, + | "id" + | "threadId" + | "runId" + | "nodeId" + | "providerThreadId" + | "providerTurnId" + | "nativeItemRef" + | "parentItemId" + | "ordinal" + | "status" + | "title" + | "startedAt" + | "completedAt" + | "updatedAt" + >; + const input = toolInput(part); + const output = toolOutput(part); + const projectionKind = openCodeToolProjectionKind(part.tool); + let turnItem: OrchestrationV2TurnItem; + if (projectionKind === "command_execution") { + turnItem = { + ...base, + type: "command_execution", + input: recordString(input, "command", "cmd") ?? stableJson(input), + ...(output === undefined ? {} : { output }), + ...(recordNumber( + part.state.status === "completed" ? part.state.metadata : undefined, + "exit", + "exitCode", + ) === undefined + ? {} + : { + exitCode: recordNumber( + part.state.status === "completed" ? part.state.metadata : undefined, + "exit", + "exitCode", + )!, + }), + }; + } else if (projectionKind === "file_change") { + turnItem = { + ...base, + type: "file_change", + fileName: recordString(input, "filePath", "path", "file") ?? part.tool, + ...(recordString(input, "oldString", "oldText") === undefined + ? {} + : { oldStr: recordString(input, "oldString", "oldText")! }), + ...(recordString(input, "newString", "content", "newText") === undefined + ? {} + : { newStr: recordString(input, "newString", "content", "newText")! }), + ...(recordString( + part.state.status === "completed" ? part.state.metadata : undefined, + "diff", + "patch", + ) === undefined + ? {} + : { + diffStr: recordString( + part.state.status === "completed" ? part.state.metadata : undefined, + "diff", + "patch", + )!, + }), + }; + } else if (projectionKind === "file_search") { + turnItem = { + ...base, + type: "file_search", + ...(recordString(input, "pattern", "query", "path", "filePath") === undefined + ? {} + : { pattern: recordString(input, "pattern", "query", "path", "filePath")! }), + }; + } else if (projectionKind === "web_search") { + const pattern = recordString(input, "query", "url", "pattern"); + turnItem = { + ...base, + type: "web_search", + ...(pattern === undefined ? {} : { patterns: [pattern] }), + }; + } else { + turnItem = { + ...base, + type: "dynamic_tool", + toolName: part.tool, + input, + ...(output === undefined ? {} : { output }), + }; + } + yield* emitProviderEvent({ + type: "node.updated", + driver: OPENCODE_PROVIDER, + node: { + id: nodeId, + threadId: turn.threadId, + runId: turn.runId, + parentNodeId: turn.rootNodeId, + rootNodeId: turn.rootNodeId, + kind: "tool_call", + status: status.node, + countsForRun: false, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt, + completedAt, + }, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: OPENCODE_PROVIDER, + turnItem, + }); + }); + + const emitTodo = Effect.fnUntraced(function* ( + state: OpenCodeThreadState, + turn: ActiveOpenCodeTurn, + todos: ReadonlyArray, + ) { + const now = yield* DateTime.now; + if (turn.planId === null) { + turn.planId = yield* idAllocator.allocate.plan({ + threadId: turn.threadId, + ...(turn.runId === null ? {} : { runId: turn.runId }), + driver: OPENCODE_PROVIDER, + }); + } + const planId = turn.planId; + const nativeItemId = `${state.nativeSessionId}:todo:${turn.providerTurnId}`; + const nodeId = idAllocator.derive.nodeFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId, + }); + const steps: Array = todos.map((todo, index) => ({ + id: `${nativeItemId}:${index + 1}`, + text: todo.content.trim() || `Todo ${index + 1}`, + status: + todo.status === "completed" + ? "completed" + : todo.status === "in_progress" + ? "running" + : "pending", + })); + const completed = steps.length > 0 && steps.every((step) => step.status === "completed"); + yield* emitProviderEvent({ + type: "node.updated", + driver: OPENCODE_PROVIDER, + node: { + id: nodeId, + threadId: turn.threadId, + runId: turn.runId, + parentNodeId: turn.rootNodeId, + rootNodeId: turn.rootNodeId, + kind: "todo_list", + status: completed ? "completed" : "running", + countsForRun: false, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef: providerRef(nativeItemId, "weak"), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: turn.startedAt, + completedAt: completed ? now : null, + }, + }); + yield* emitProviderEvent({ + type: "plan.updated", + driver: OPENCODE_PROVIDER, + plan: { + id: planId, + threadId: turn.threadId, + runId: turn.runId, + nodeId, + status: completed ? "completed" : "active", + kind: "todo_list", + steps, + }, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: OPENCODE_PROVIDER, + turnItem: { + id: turnItemId, + threadId: turn.threadId, + runId: turn.runId, + nodeId, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef: providerRef(nativeItemId, "weak"), + parentItemId: null, + ordinal: itemOrdinal(turn, nativeItemId), + status: completed ? "completed" : "running", + title: "Todo list", + startedAt: turn.startedAt, + completedAt: completed ? now : null, + updatedAt: now, + type: "todo_list", + planId, + steps, + }, + }); + }); + + const requestQuestions = (request: QuestionRequest) => + request.questions.map((question, index) => ({ + id: openCodeQuestionId(index, question), + header: question.header.trim() || `Question ${index + 1}`, + question: question.question.trim() || question.header.trim() || `Question ${index + 1}`, + options: question.options.map((option) => ({ + label: option.label.trim() || "Option", + description: option.description.trim() || option.label.trim() || "Option", + })), + })); + + const runtimeRequestTurnItem = ( + pending: PendingOpenCodeRequest, + status: OrchestrationV2TurnItem["status"], + completedAt: DateTime.Utc | null, + updatedAt: DateTime.Utc, + ): OrchestrationV2TurnItem => { + const base = { + id: pending.turnItemId, + threadId: pending.turn.threadId, + runId: pending.turn.runId, + nodeId: pending.nodeId, + providerThreadId: pending.state.providerThread.id, + providerTurnId: pending.turn.providerTurnId, + nativeItemRef: providerRef(pending.nativeRequestId), + parentItemId: null, + ordinal: itemOrdinal(pending.turn, pending.nativeRequestId), + status, + startedAt: pending.createdAt, + completedAt, + updatedAt, + }; + if (pending.question !== undefined) { + return { + ...base, + title: "User input", + type: "user_input_request", + requestId: pending.requestId, + questions: requestQuestions(pending.question), + }; + } + const permission = pending.permission; + if (permission === undefined) { + throw protocolError(`OpenCode request ${pending.requestId} has no native payload`); + } + return { + ...base, + title: permission.permission, + type: "approval_request", + requestId: pending.requestId, + requestKind: pending.requestKind === "user_input" ? "command" : pending.requestKind, + prompt: + permission.patterns.length === 0 + ? permission.permission + : permission.patterns.join("\n"), + }; + }; + + const emitRuntimeRequest = Effect.fnUntraced(function* ( + state: OpenCodeThreadState, + turn: ActiveOpenCodeTurn, + nativeRequestId: string, + request: + | { readonly type: "permission"; readonly value: PermissionRequest } + | { readonly type: "question"; readonly value: QuestionRequest }, + ) { + if (pendingRequestsByNativeId.has(nativeRequestId)) return; + const now = yield* DateTime.now; + const requestId = yield* idAllocator.allocate.runtimeRequest({ + driver: OPENCODE_PROVIDER, + providerTurnId: turn.providerTurnId, + nativeRequestId, + }); + const nodeId = idAllocator.derive.approvalNode({ requestId }); + const turnItemId = idAllocator.derive.approvalTurnItem({ requestId }); + const permissionToolName = + request.type === "permission" && request.value.tool !== undefined + ? Array.from(turn.parts.values()).find( + (part): part is ToolPart => + part.type === "tool" && part.callID === request.value.tool?.callID, + )?.tool + : undefined; + const permissionRequestKind = + request.type === "permission" + ? openCodePermissionRequestKind(request.value.permission, permissionToolName) + : undefined; + const requestKind: OrchestrationV2RuntimeRequest["kind"] = + permissionRequestKind ?? "user_input"; + const pending: PendingOpenCodeRequest = { + requestId, + nativeRequestId, + turn, + state, + nodeId, + turnItemId, + requestKind, + createdAt: now, + ...(request.type === "permission" + ? { permission: request.value } + : { question: request.value }), + }; + pendingRequests.set(String(requestId), pending); + pendingRequestsByNativeId.set(nativeRequestId, pending); + const runtimeRequest: OrchestrationV2RuntimeRequest = { + id: requestId, + nodeId, + providerTurnId: turn.providerTurnId, + nativeRequestRef: providerRef(nativeRequestId), + kind: requestKind, + status: "pending", + responseCapability: { + type: "live", + providerSessionId: inputProviderSessionId, + }, + createdAt: now, + resolvedAt: null, + }; + state.runtimeRequests.set(String(requestId), runtimeRequest); + yield* emitProviderEvent({ + type: "node.updated", + driver: OPENCODE_PROVIDER, + node: { + id: nodeId, + threadId: turn.threadId, + runId: turn.runId, + parentNodeId: turn.rootNodeId, + rootNodeId: turn.rootNodeId, + kind: request.type === "question" ? "user_input_request" : "approval_request", + status: "waiting", + countsForRun: false, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef: providerRef(nativeRequestId), + runtimeRequestId: requestId, + checkpointScopeId: null, + startedAt: now, + completedAt: null, + }, + }); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver: OPENCODE_PROVIDER, + threadId: turn.threadId, + runtimeRequest, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: OPENCODE_PROVIDER, + turnItem: runtimeRequestTurnItem(pending, "waiting", null, now), + }); + yield* updateProviderSession("waiting", null); + }); + + const resolveRuntimeRequest = Effect.fnUntraced(function* ( + nativeRequestId: string, + status: "resolved" | "cancelled", + ) { + const pending = pendingRequestsByNativeId.get(nativeRequestId); + if (pending === undefined) return; + const now = yield* DateTime.now; + const current = pending.state.runtimeRequests.get(String(pending.requestId)); + if (current !== undefined) { + const resolved: OrchestrationV2RuntimeRequest = { + ...current, + status, + resolvedAt: now, + }; + pending.state.runtimeRequests.set(String(pending.requestId), resolved); + yield* emitProviderEvent({ + type: "runtime_request.updated", + driver: OPENCODE_PROVIDER, + threadId: pending.turn.threadId, + runtimeRequest: resolved, + }); + } + yield* emitProviderEvent({ + type: "node.updated", + driver: OPENCODE_PROVIDER, + node: { + id: pending.nodeId, + threadId: pending.turn.threadId, + runId: pending.turn.runId, + parentNodeId: pending.turn.rootNodeId, + rootNodeId: pending.turn.rootNodeId, + kind: pending.question === undefined ? "approval_request" : "user_input_request", + status: status === "resolved" ? "completed" : "cancelled", + countsForRun: false, + providerThreadId: pending.state.providerThread.id, + providerTurnId: pending.turn.providerTurnId, + nativeItemRef: providerRef(nativeRequestId), + runtimeRequestId: pending.requestId, + checkpointScopeId: null, + startedAt: pending.createdAt, + completedAt: now, + }, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: OPENCODE_PROVIDER, + turnItem: runtimeRequestTurnItem( + pending, + status === "resolved" ? "completed" : "cancelled", + now, + now, + ), + }); + pendingRequests.delete(String(pending.requestId)); + pendingRequestsByNativeId.delete(nativeRequestId); + const hasOtherPending = Array.from(pendingRequests.values()).some( + (candidate) => candidate.turn.isRoot, + ); + if (!hasOtherPending) yield* updateProviderSession("running", null); + }); + + const finalizeTurn = Effect.fnUntraced(function* ( + state: OpenCodeThreadState, + turn: ActiveOpenCodeTurn, + status: TerminalTurnStatus, + ) { + if (turn.finalized) return; + turn.finalized = true; + const completedAt = yield* DateTime.now; + for (const part of turn.parts.values()) { + if (part.type === "text" || part.type === "reasoning") { + yield* emitTextPart(state, turn, part, true); + } + } + for (const pending of Array.from(pendingRequests.values())) { + if (pending.turn.providerTurnId === turn.providerTurnId) { + yield* resolveRuntimeRequest(pending.nativeRequestId, "cancelled"); + } + } + yield* emitProviderTurn(state, turn, status, completedAt); + yield* updateProviderThread(state, { + status: status === "failed" ? "error" : "idle", + nativeConversationHeadRef: + turn.nativeUserMessageId === null + ? state.providerThread.nativeConversationHeadRef + : providerRef(turn.nativeUserMessageId, "weak"), + }); + state.activeTurn = null; + if (!turn.isRoot) { + yield* emitProviderEvent({ + type: "node.updated", + driver: OPENCODE_PROVIDER, + node: { + id: turn.rootNodeId, + threadId: turn.threadId, + runId: null, + parentNodeId: null, + rootNodeId: turn.rootNodeId, + kind: "root_turn", + status, + countsForRun: false, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef: providerRef(state.nativeSessionId), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: turn.startedAt, + completedAt, + }, + }); + return; + } + const anotherTurnIsActive = Array.from(threads.values()).some( + (candidate) => candidate.activeTurn?.isRoot === true, + ); + yield* updateProviderSession( + anotherTurnIsActive ? "running" : status === "failed" ? "error" : "ready", + status === "failed" ? sessionEntity.lastError : null, + ); + yield* emitProviderEvent({ + type: "turn.terminal", + driver: OPENCODE_PROVIDER, + providerTurnId: turn.providerTurnId, + status, + }); + }); + + const createChildTurn = Effect.fnUntraced(function* ( + state: OpenCodeThreadState, + message: Extract, + ) { + if (state.appThread === null || state.parentSubagent === null) return null; + const now = yield* DateTime.now; + const startedAt = dateTimeFromEpoch(message.time.created, now); + const rootNodeId = idAllocator.derive.nodeFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: `${state.nativeSessionId}:root:${message.id}`, + }); + const providerTurnId = idAllocator.derive.providerTurn({ + driver: OPENCODE_PROVIDER, + nativeTurnId: message.id, + }); + const providerTurn: OrchestrationV2ProviderTurn = { + id: providerTurnId, + providerThreadId: state.providerThread.id, + nodeId: rootNodeId, + runAttemptId: null, + nativeTurnRef: providerRef(message.id, "weak"), + ordinal: state.nextChildTurnOrdinal++, + status: "running", + startedAt, + completedAt: null, + }; + const turn: ActiveOpenCodeTurn = { + isRoot: false, + threadId: state.appThread.id, + runId: null, + rootNodeId, + appThread: state.appThread, + modelSelection: state.appThread.modelSelection, + runtimePolicy: state.parentSubagent.parentTurn.runtimePolicy, + providerTurnId, + providerTurnOrdinal: providerTurn.ordinal, + runAttemptId: null, + startedAt, + itemOrdinals: new Map(), + parts: new Map(), + partIdsByMessage: new Map(), + providerTurn, + nextItemOrdinal: 1, + nativeUserMessageId: message.id, + interrupted: false, + finalized: false, + planId: null, + }; + state.activeTurn = turn; + state.providerTurns.set(String(providerTurnId), providerTurn); + yield* emitProviderEvent({ + type: "node.updated", + driver: OPENCODE_PROVIDER, + node: { + id: rootNodeId, + threadId: turn.threadId, + runId: null, + parentNodeId: null, + rootNodeId, + kind: "root_turn", + status: "running", + countsForRun: false, + providerThreadId: state.providerThread.id, + providerTurnId, + nativeItemRef: providerRef(message.id, "weak"), + runtimeRequestId: null, + checkpointScopeId: null, + startedAt, + completedAt: null, + }, + }); + yield* emitProviderTurn(state, turn, "running", null); + return turn; + }); + + const projectChildUserPart = Effect.fnUntraced(function* ( + state: OpenCodeThreadState, + turn: ActiveOpenCodeTurn, + part: Extract, + ) { + const now = yield* DateTime.now; + const messageId = idAllocator.derive.messageFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: part.messageID, + }); + const turnItemId = idAllocator.derive.turnItemFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: part.messageID, + }); + const projected: OrchestrationV2ConversationMessage = { + createdBy: "agent", + creationSource: "provider", + id: messageId, + threadId: turn.threadId, + runId: null, + nodeId: turn.rootNodeId, + role: "user", + text: part.text, + attachments: [], + streaming: false, + createdAt: turn.startedAt, + updatedAt: now, + }; + state.messages.set(String(messageId), projected); + yield* emitProviderEvent({ + type: "message.updated", + driver: OPENCODE_PROVIDER, + message: projected, + }); + yield* emitProviderEvent({ + type: "turn_item.updated", + driver: OPENCODE_PROVIDER, + turnItem: { + createdBy: "agent", + creationSource: "provider", + id: turnItemId, + threadId: turn.threadId, + runId: null, + nodeId: turn.rootNodeId, + providerThreadId: state.providerThread.id, + providerTurnId: turn.providerTurnId, + nativeItemRef: providerRef(part.messageID), + parentItemId: null, + ordinal: itemOrdinal(turn, part.messageID), + status: "completed", + title: null, + startedAt: projected.createdAt, + completedAt: now, + updatedAt: now, + type: "user_message", + messageId, + inputIntent: "turn_start", + text: part.text, + attachments: [], + }, + }); + }); + + const handleMessageUpdated = Effect.fnUntraced(function* ( + event: Extract, + ) { + const state = threads.get(event.properties.sessionID); + if (state === undefined) return; + const message = event.properties.info; + state.messageRoles.set(message.id, message.role); + if (message.role !== "user") return; + const isNewUserMessage = !state.userMessageIds.includes(message.id); + if (isNewUserMessage) state.userMessageIds.push(message.id); + let turn = state.activeTurn; + if (turn === null && state.parentSubagent !== null && isNewUserMessage) { + turn = yield* createChildTurn(state, message); + } + if (turn !== null && turn.nativeUserMessageId === null) { + turn.nativeUserMessageId = message.id; + yield* emitProviderTurn(state, turn, "running", null); + } + }); + + const handlePartUpdated = Effect.fnUntraced(function* ( + event: Extract, + ) { + const part = event.properties.part; + const state = threads.get(part.sessionID); + const turn = state?.activeTurn; + if (state === undefined || turn === null || turn === undefined || turn.finalized) return; + if (part.type === "text" && state.messageRoles.get(part.messageID) === "user") { + if (!turn.isRoot) yield* projectChildUserPart(state, turn, part); + return; + } + turn.parts.set(part.id, part); + const ids = turn.partIdsByMessage.get(part.messageID) ?? new Set(); + ids.add(part.id); + turn.partIdsByMessage.set(part.messageID, ids); + switch (part.type) { + case "text": + case "reasoning": + yield* emitTextPart(state, turn, part); + return; + case "tool": + yield* emitToolPart(state, turn, part); + return; + default: + return; + } + }); + + const handlePartDelta = Effect.fnUntraced(function* ( + event: Extract, + ) { + if (event.properties.field !== "text") return; + const state = threads.get(event.properties.sessionID); + const turn = state?.activeTurn; + const current = turn?.parts.get(event.properties.partID); + if ( + state === undefined || + turn === null || + turn === undefined || + current === undefined || + (current.type !== "text" && current.type !== "reasoning") + ) { + return; + } + const updated = { ...current, text: current.text + event.properties.delta }; + turn.parts.set(updated.id, updated); + yield* emitTextPart(state, turn, updated); + }); + + const handleAssistantCompleted = Effect.fnUntraced(function* ( + event: Extract, + ) { + const message = event.properties.info; + if (message.role !== "assistant" || message.time.completed === undefined) return; + const state = threads.get(message.sessionID); + const turn = state?.activeTurn; + if (state === undefined || turn === null || turn === undefined) return; + for (const partId of turn.partIdsByMessage.get(message.id) ?? []) { + const part = turn.parts.get(partId); + if (part?.type === "text" || part?.type === "reasoning") { + yield* emitTextPart(state, turn, part, true); + } + } + }); + + const handleEvent = Effect.fnUntraced(function* (event: OpenCodeEvent) { + yield* emitRawEvent({ + direction: "incoming", + messageKind: "notification", + method: event.type, + payload: event, + }); + switch (event.type) { + case "message.updated": + yield* handleMessageUpdated(event); + yield* handleAssistantCompleted(event); + return; + case "message.part.updated": + yield* handlePartUpdated(event); + return; + case "message.part.delta": + yield* handlePartDelta(event); + return; + case "todo.updated": { + const state = threads.get(event.properties.sessionID); + if (state?.activeTurn !== null && state?.activeTurn !== undefined) { + yield* emitTodo(state, state.activeTurn, event.properties.todos); + } + return; + } + case "permission.asked": { + const state = threads.get(event.properties.sessionID); + if (state?.activeTurn !== null && state?.activeTurn !== undefined) { + yield* emitRuntimeRequest(state, state.activeTurn, event.properties.id, { + type: "permission", + value: event.properties, + }); + } + return; + } + case "question.asked": { + const state = threads.get(event.properties.sessionID); + if (state?.activeTurn !== null && state?.activeTurn !== undefined) { + yield* emitRuntimeRequest(state, state.activeTurn, event.properties.id, { + type: "question", + value: event.properties, + }); + } + return; + } + case "permission.replied": + yield* resolveRuntimeRequest(event.properties.requestID, "resolved"); + return; + case "question.replied": + yield* resolveRuntimeRequest(event.properties.requestID, "resolved"); + return; + case "question.rejected": + yield* resolveRuntimeRequest(event.properties.requestID, "cancelled"); + return; + case "session.status": { + const state = threads.get(event.properties.sessionID); + if (state === undefined) return; + if (event.properties.status.type === "busy") { + yield* updateProviderThread(state, { status: "active" }); + return; + } + if (event.properties.status.type === "idle" && state.activeTurn !== null) { + yield* finalizeTurn( + state, + state.activeTurn, + state.activeTurn.interrupted ? "interrupted" : "completed", + ); + } + return; + } + case "session.idle": { + const state = threads.get(event.properties.sessionID); + if (state?.activeTurn !== null && state?.activeTurn !== undefined) { + yield* finalizeTurn( + state, + state.activeTurn, + state.activeTurn.interrupted ? "interrupted" : "completed", + ); + } + return; + } + case "session.error": { + const states = + event.properties.sessionID === undefined + ? Array.from(threads.values()).filter((state) => state.activeTurn !== null) + : [threads.get(event.properties.sessionID)].filter( + (state): state is OpenCodeThreadState => state !== undefined, + ); + const message = openCodeErrorMessage(event); + if ( + !isMessageAbortedError(event) && + (event.properties.sessionID === undefined || + states.some((state) => state.parentSubagent === null)) + ) { + yield* updateProviderSession("error", message); + } + for (const state of states) { + if (state.activeTurn !== null) { + yield* finalizeTurn( + state, + state.activeTurn, + terminalStatusForError(event, state.activeTurn), + ); + } + } + return; + } + default: + return; + } + }); + + const inputProviderSessionId = input.providerSessionId; + + const subscription = yield* sdkCall("event.subscribe", {}, () => + client.event.subscribe(undefined, { signal: abortController.signal }), + ); + yield* Scope.addFinalizer( + scope, + Effect.sync(() => abortController.abort()), + ); + yield* Stream.fromAsyncIterable( + subscription.stream, + (cause) => + new OpenCodeRuntimeError({ + operation: "event.subscribe", + detail: openCodeRuntimeErrorDetail(cause), + cause, + }), + ).pipe( + Stream.runForEach(handleEvent), + Effect.exit, + Effect.flatMap((exit) => + Effect.gen(function* () { + if (abortController.signal.aborted || Exit.isSuccess(exit)) return; + const detail = openCodeRuntimeErrorDetail(Cause.squash(exit.cause)); + yield* updateProviderSession("error", detail); + for (const state of threads.values()) { + if (state.activeTurn !== null) + yield* finalizeTurn(state, state.activeTurn, "failed"); + } + }), + ), + Effect.forkIn(scope), + ); + + if (!connection.external && connection.exitCode !== null) { + yield* connection.exitCode.pipe( + Effect.flatMap((code) => + abortController.signal.aborted + ? Effect.void + : Effect.gen(function* () { + const detail = `OpenCode server exited unexpectedly (${code}).`; + yield* updateProviderSession("error", detail); + for (const state of threads.values()) { + if (state.activeTurn !== null) { + yield* finalizeTurn(state, state.activeTurn, "failed"); + } + } + }), + ), + Effect.forkIn(scope), + ); + } + + const registerThread = ( + nativeSession: OpenCodeSession, + providerThread: OrchestrationV2ProviderThread, + appThread: OrchestrationV2AppThread | null, + ): OpenCodeThreadState => { + const existing = threads.get(nativeSession.id); + if (existing !== undefined) { + existing.providerThread = providerThread; + if (appThread !== null) existing.appThread = appThread; + return existing; + } + const state: OpenCodeThreadState = { + nativeSessionId: nativeSession.id, + providerThread, + appThread, + activeTurn: null, + providerTurns: new Map(), + messages: new Map(), + runtimeRequests: new Map(), + messageRoles: new Map(), + userMessageIds: [], + parentSubagent: subagentsByChildSessionId.get(nativeSession.id) ?? null, + nextChildTurnOrdinal: 1, + }; + threads.set(nativeSession.id, state); + return state; + }; + + const resolvePromptParts = (turnInput: ProviderAdapterV2TurnInput) => { + const text = turnInput.message.text.trim(); + const files = toOpenCodeFileParts({ + attachments: turnInput.message.attachments, + resolveAttachmentPath: (attachment) => + resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment }), + }); + if (text.length === 0 && files.length === 0) { + throw protocolError("OpenCode turns require text or at least one valid attachment"); + } + return [...(text.length === 0 ? [] : [{ type: "text" as const, text }]), ...files]; + }; + + const readSnapshot = Effect.fnUntraced(function* ( + providerThread: OrchestrationV2ProviderThread, + ) { + const sessionId = nativeThreadId(providerThread); + const response = yield* sdkCall("session.messages", { sessionID: sessionId }, () => + client.session.messages({ sessionID: sessionId }), + ); + const nativeMessages = unwrapData("session.messages", response); + const state = threads.get(sessionId); + const snapshotNow = yield* DateTime.now; + const messages: Array = nativeMessages.flatMap( + ({ info, parts }) => { + const text = parts + .filter( + (part): part is Extract => part.type === "text", + ) + .filter((part) => part.ignored !== true && part.synthetic !== true) + .map((part) => part.text) + .join("\n"); + if (text.length === 0) return []; + const createdAt = dateTimeFromEpoch(info.time.created, snapshotNow); + return [ + { + createdBy: info.role === "user" ? "user" : "agent", + creationSource: "provider", + id: idAllocator.derive.messageFromProviderItem({ + driver: OPENCODE_PROVIDER, + nativeItemId: info.id, + }), + threadId: providerThread.appThreadId ?? input.threadId, + runId: null, + nodeId: null, + role: info.role, + text, + attachments: [], + streaming: false, + createdAt, + updatedAt: + info.role === "assistant" + ? dateTimeFromEpoch(info.time.completed, createdAt) + : createdAt, + }, + ]; + }, + ); + const lastUser = nativeMessages.findLast(({ info }) => info.role === "user")?.info.id; + return { + providerThread: { + ...providerThread, + providerSessionId: input.providerSessionId, + nativeConversationHeadRef: + lastUser === undefined ? null : providerRef(lastUser, "weak"), + status: "idle" as const, + updatedAt: snapshotNow, + }, + providerTurns: state === undefined ? [] : [...state.providerTurns.values()], + messages, + runtimeRequests: state === undefined ? [] : [...state.runtimeRequests.values()], + providerPayload: nativeMessages, + }; + }); + + const runtimeSession: ProviderAdapterV2SessionRuntime = { + instanceId: options.instanceId, + driver: OPENCODE_PROVIDER, + providerSessionId: input.providerSessionId, + providerSession: sessionEntity, + rawEvents: Stream.fromEffectRepeat(Queue.take(rawEvents)), + events: Stream.fromEffectRepeat(Queue.take(events)), + ensureThread: (threadInput) => + Effect.gen(function* () { + if (threadInput.existingProviderThread !== undefined) { + return yield* runtimeSession.resumeThread({ + providerThread: threadInput.existingProviderThread, + }); + } + const response = yield* sdkCall( + "session.create", + { + title: `T3 Code ${threadInput.threadId}`, + permission: openCodePermissionRules(threadInput.runtimePolicy), + }, + () => + client.session.create({ + title: `T3 Code ${threadInput.threadId}`, + permission: openCodePermissionRules(threadInput.runtimePolicy), + }), + ); + const nativeSession = unwrapData("session.create", response); + const createdAt = yield* DateTime.now; + const providerThread = makeProviderThread({ + idAllocator, + providerInstanceId: options.instanceId, + providerSessionId: input.providerSessionId, + appThreadId: threadInput.threadId, + nativeSession, + now: createdAt, + }); + registerThread(nativeSession, providerThread, null); + return providerThread; + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterEnsureThreadError({ + driver: OPENCODE_PROVIDER, + threadId: threadInput.threadId, + cause, + }), + ), + ), + resumeThread: (threadInput) => + Effect.gen(function* () { + const sessionId = nativeThreadId(threadInput.providerThread); + const response = yield* sdkCall("session.get", { sessionID: sessionId }, () => + client.session.get({ sessionID: sessionId }), + ); + const nativeSession = unwrapData("session.get", response); + const resumedAt = yield* DateTime.now; + const providerThread = { + ...threadInput.providerThread, + providerSessionId: input.providerSessionId, + status: "idle" as const, + updatedAt: dateTimeFromEpoch(nativeSession.time.updated, resumedAt), + }; + registerThread(nativeSession, providerThread, null); + return providerThread; + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterResumeThreadError({ + driver: OPENCODE_PROVIDER, + providerSessionId: input.providerSessionId, + providerThreadId: threadInput.providerThread.id, + cause, + }), + ), + ), + startTurn: (turnInput) => + Effect.gen(function* () { + const sessionId = nativeThreadId(turnInput.providerThread); + const state = threads.get(sessionId); + if (state === undefined) { + return yield* protocolError(`OpenCode session ${sessionId} is not registered`); + } + if (state.activeTurn !== null) { + return yield* protocolError( + `OpenCode provider thread ${turnInput.providerThread.id} already has an active turn`, + ); + } + const parsedModel = parseOpenCodeModelSlug(turnInput.modelSelection.model); + if (parsedModel === null) { + return yield* protocolError( + `OpenCode model '${turnInput.modelSelection.model}' must use provider/model format`, + ); + } + const parts = resolvePromptParts(turnInput); + const startedAt = yield* DateTime.now; + const syntheticNativeTurnId = `${sessionId}:attempt:${turnInput.attemptId}`; + const providerTurnId = idAllocator.derive.providerTurn({ + driver: OPENCODE_PROVIDER, + nativeTurnId: syntheticNativeTurnId, + }); + const providerTurn: OrchestrationV2ProviderTurn = { + id: providerTurnId, + providerThreadId: turnInput.providerThread.id, + nodeId: turnInput.rootNodeId, + runAttemptId: turnInput.attemptId, + nativeTurnRef: providerRef(syntheticNativeTurnId, "weak"), + ordinal: turnInput.providerTurnOrdinal, + status: "running", + startedAt, + completedAt: null, + }; + const turn: ActiveOpenCodeTurn = { + isRoot: true, + threadId: turnInput.threadId, + runId: turnInput.runId, + rootNodeId: turnInput.rootNodeId, + appThread: turnInput.appThread, + modelSelection: turnInput.modelSelection, + runtimePolicy: turnInput.runtimePolicy, + providerTurnId, + providerTurnOrdinal: turnInput.providerTurnOrdinal, + runAttemptId: turnInput.attemptId, + startedAt, + itemOrdinals: new Map(), + parts: new Map(), + partIdsByMessage: new Map(), + providerTurn, + nextItemOrdinal: 100, + nativeUserMessageId: null, + interrupted: false, + finalized: false, + planId: null, + }; + state.appThread = turnInput.appThread; + state.activeTurn = turn; + state.providerTurns.set(String(providerTurnId), providerTurn); + yield* emitProviderTurn(state, turn, "running", null); + yield* updateProviderThread(state, { + status: "active", + firstRunOrdinal: state.providerThread.firstRunOrdinal ?? turnInput.runOrdinal, + lastRunOrdinal: turnInput.runOrdinal, + }); + yield* updateProviderSession("running", null); + const agent = + getModelSelectionStringOptionValue(turnInput.modelSelection, "agent") ?? + (turnInput.runtimePolicy.interactionMode === "plan" ? "plan" : undefined); + const variant = getModelSelectionStringOptionValue( + turnInput.modelSelection, + "variant", + ); + yield* sdkCall( + "session.promptAsync", + { + sessionID: sessionId, + model: parsedModel, + ...(agent === undefined ? {} : { agent }), + ...(variant === undefined ? {} : { variant }), + parts, + }, + () => + client.session.promptAsync({ + sessionID: sessionId, + model: parsedModel, + ...(agent === undefined ? {} : { agent }), + ...(variant === undefined ? {} : { variant }), + parts, + }), + ).pipe(Effect.tapError(() => finalizeTurn(state, turn, "failed"))); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterTurnStartError({ + driver: OPENCODE_PROVIDER, + threadId: turnInput.threadId, + providerThreadId: turnInput.providerThread.id, + runId: turnInput.runId, + cause, + }), + ), + ), + steerTurn: (steerInput) => + Effect.gen(function* () { + const sessionId = nativeThreadId(steerInput.providerThread); + const state = threads.get(sessionId); + const turn = state?.activeTurn; + if ( + turn === undefined || + turn === null || + turn.providerTurnId !== steerInput.providerTurnId + ) { + return yield* protocolError( + `OpenCode turn ${steerInput.providerTurnId} is not active`, + ); + } + const parsedModel = parseOpenCodeModelSlug(turn.modelSelection.model); + if (parsedModel === null) { + return yield* protocolError( + `OpenCode model '${turn.modelSelection.model}' must use provider/model format`, + ); + } + const text = steerInput.message.text.trim(); + const files = toOpenCodeFileParts({ + attachments: steerInput.message.attachments, + resolveAttachmentPath: (attachment) => + resolveAttachmentPath({ + attachmentsDir: serverConfig.attachmentsDir, + attachment, + }), + }); + if (text.length === 0 && files.length === 0) { + return yield* protocolError("OpenCode steering requires text or an attachment"); + } + const parts = [ + ...(text.length === 0 ? [] : [{ type: "text" as const, text }]), + ...files, + ]; + yield* sdkCall( + "session.promptAsync", + { sessionID: sessionId, model: parsedModel, parts }, + () => + client.session.promptAsync({ + sessionID: sessionId, + model: parsedModel, + parts, + }), + ); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterSteerRunError({ + driver: OPENCODE_PROVIDER, + providerThreadId: steerInput.providerThread.id, + providerTurnId: steerInput.providerTurnId, + cause, + }), + ), + ), + interruptTurn: (interruptInput) => + Effect.gen(function* () { + const sessionId = nativeThreadId(interruptInput.providerThread); + const state = threads.get(sessionId); + const turn = state?.activeTurn; + if ( + turn === undefined || + turn === null || + turn.providerTurnId !== interruptInput.providerTurnId + ) { + return yield* protocolError( + `OpenCode turn ${interruptInput.providerTurnId} is not active`, + ); + } + turn.interrupted = true; + yield* sdkCall("session.abort", { sessionID: sessionId }, () => + client.session.abort({ sessionID: sessionId }), + ).pipe(Effect.tapError(() => Effect.sync(() => (turn.interrupted = false)))); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterInterruptError({ + driver: OPENCODE_PROVIDER, + providerThreadId: interruptInput.providerThread.id, + providerTurnId: interruptInput.providerTurnId, + cause, + }), + ), + ), + respondToRuntimeRequest: (requestInput) => + Effect.gen(function* () { + const pending = pendingRequests.get(String(requestInput.requestId)); + if (pending === undefined) { + return yield* protocolError( + `No pending OpenCode request ${requestInput.requestId}`, + ); + } + if (pending.question !== undefined) { + if (requestInput.answers === undefined) { + return yield* protocolError( + `OpenCode question request ${requestInput.requestId} requires answers`, + ); + } + const answers = toOpenCodeQuestionAnswers(pending.question, requestInput.answers); + yield* sdkCall( + "question.reply", + { requestID: pending.nativeRequestId, answers }, + () => + client.question.reply({ + requestID: pending.nativeRequestId, + answers, + }), + ); + return; + } + if (requestInput.decision === undefined) { + return yield* protocolError( + `OpenCode approval request ${requestInput.requestId} requires a decision`, + ); + } + const reply = toOpenCodePermissionReply(requestInput.decision); + yield* sdkCall( + "permission.reply", + { requestID: pending.nativeRequestId, reply }, + () => + client.permission.reply({ + requestID: pending.nativeRequestId, + reply, + }), + ); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterRuntimeRequestResponseError({ + driver: OPENCODE_PROVIDER, + requestId: requestInput.requestId, + cause, + }), + ), + ), + readThreadSnapshot: (snapshotInput) => + readSnapshot(snapshotInput.providerThread).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterReadThreadSnapshotError({ + driver: OPENCODE_PROVIDER, + providerThreadId: snapshotInput.providerThread.id, + cause, + }), + ), + ), + rollbackThread: (rollbackInput) => + Effect.gen(function* () { + const sessionId = nativeThreadId(rollbackInput.providerThread); + const state = threads.get(sessionId); + if (state?.activeTurn !== null && state?.activeTurn !== undefined) { + return yield* protocolError( + `Cannot roll back OpenCode thread ${rollbackInput.providerThread.id} while a turn is active`, + ); + } + const response = yield* sdkCall("session.messages", { sessionID: sessionId }, () => + client.session.messages({ sessionID: sessionId }), + ); + const messages = unwrapData("session.messages", response); + let boundaryMessageId: string | undefined; + if (rollbackInput.target.type === "thread_start") { + boundaryMessageId = messages.find(({ info }) => info.role === "user")?.info.id; + } else { + boundaryMessageId = openCodeBoundaryAfterProviderTurn( + rollbackInput.providerThreadTurns, + rollbackInput.target.providerTurn.id, + ); + } + if (boundaryMessageId !== undefined) { + yield* sdkCall( + "session.revert", + { sessionID: sessionId, messageID: boundaryMessageId }, + () => + client.session.revert({ sessionID: sessionId, messageID: boundaryMessageId }), + ); + } + const snapshot = yield* readSnapshot(rollbackInput.providerThread); + return { + ...snapshot, + providerThread: { + ...snapshot.providerThread, + nativeConversationHeadRef: + rollbackInput.target.type === "provider_turn" + ? rollbackInput.target.providerTurn.nativeTurnRef + : null, + }, + }; + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterRollbackThreadError({ + driver: OPENCODE_PROVIDER, + providerThreadId: rollbackInput.providerThread.id, + checkpointId: rollbackInput.target.checkpointId, + cause, + }), + ), + ), + forkThread: (forkInput) => + Effect.gen(function* () { + const sourceSessionId = nativeThreadId(forkInput.sourceProviderThread); + const sourceState = threads.get(sourceSessionId); + if (sourceState?.activeTurn !== null && sourceState?.activeTurn !== undefined) { + return yield* protocolError( + `Cannot fork OpenCode thread ${forkInput.sourceProviderThread.id} while a turn is active`, + ); + } + let boundaryMessageId: string | undefined; + if (forkInput.providerTurnId !== undefined) { + const sourceTurns = forkInput.sourceProviderTurns ?? []; + const selected = sourceTurns.find((turn) => turn.id === forkInput.providerTurnId); + if (selected === undefined) { + return yield* protocolError( + `OpenCode fork boundary turn ${forkInput.providerTurnId} was not found`, + ); + } + boundaryMessageId = openCodeBoundaryAfterProviderTurn(sourceTurns, selected.id); + } + const response = yield* sdkCall( + "session.fork", + { + sessionID: sourceSessionId, + ...(boundaryMessageId === undefined ? {} : { messageID: boundaryMessageId }), + }, + () => + client.session.fork({ + sessionID: sourceSessionId, + ...(boundaryMessageId === undefined ? {} : { messageID: boundaryMessageId }), + }), + ); + const nativeSession = unwrapData("session.fork", response); + const forkedAt = yield* DateTime.now; + const providerThread = makeProviderThread({ + idAllocator, + providerInstanceId: options.instanceId, + providerSessionId: input.providerSessionId, + appThreadId: forkInput.targetThreadId, + ...(forkInput.ownerNodeId === undefined + ? {} + : { ownerNodeId: forkInput.ownerNodeId }), + nativeSession, + forkedFrom: { + providerThreadId: forkInput.sourceProviderThread.id, + ...(forkInput.providerTurnId === undefined + ? {} + : { providerTurnId: forkInput.providerTurnId }), + }, + now: forkedAt, + }); + registerThread(nativeSession, providerThread, null); + return providerThread; + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterForkThreadError({ + driver: OPENCODE_PROVIDER, + providerThreadId: forkInput.sourceProviderThread.id, + cause, + }), + ), + ), + }; + + return runtimeSession; + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterOpenSessionError({ + driver: OPENCODE_PROVIDER, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ), + ), + }); +} + +export type OpenCodeAdapterV2DriverEnv = OpenCodeRuntime | IdAllocatorV2 | ServerConfig; + +export const OpenCodeAdapterV2Driver: ProviderAdapterDriver< + OpenCodeSettings, + OpenCodeAdapterV2DriverEnv +> = { + driverKind: OPENCODE_DRIVER_KIND, + configSchema: OpenCodeSettingsSchema, + defaultConfig: (): OpenCodeSettings => DEFAULT_OPENCODE_SETTINGS, + create: Effect.fn("OpenCodeAdapterV2Driver.create")( + function* (input: ProviderAdapterDriverCreateInput) { + const hostEnvironment = yield* HostProcessEnvironment; + const openCodeRuntime = yield* OpenCodeRuntime; + const idAllocator = yield* IdAllocatorV2; + const serverConfig = yield* ServerConfig; + return makeOpenCodeAdapterV2({ + instanceId: input.instanceId, + settings: { ...input.config, enabled: input.enabled }, + environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + runtime: openCodeRuntime, + idAllocator, + serverConfig, + }); + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterDriverCreateError({ + driver: OPENCODE_DRIVER_KIND, + instanceId: input.instanceId, + detail: "Failed to create OpenCode v2 adapter.", + cause, + }), + ), + ), + ), +}; + +export const layer: Layer.Layer = + Layer.effect( + ProviderAdapterV2, + Effect.gen(function* () { + const hostEnvironment = yield* HostProcessEnvironment; + const openCodeRuntime = yield* OpenCodeRuntime; + const idAllocator = yield* IdAllocatorV2; + const serverConfig = yield* ServerConfig; + return makeOpenCodeAdapterV2({ + instanceId: OPENCODE_DEFAULT_INSTANCE_ID, + settings: DEFAULT_OPENCODE_SETTINGS, + environment: hostEnvironment, + runtime: openCodeRuntime, + idAllocator, + serverConfig, + }); + }), + ); diff --git a/apps/server/src/orchestration-v2/CheckpointCaptureService.ts b/apps/server/src/orchestration-v2/CheckpointCaptureService.ts new file mode 100644 index 00000000000..cdc763f854e --- /dev/null +++ b/apps/server/src/orchestration-v2/CheckpointCaptureService.ts @@ -0,0 +1,217 @@ +import { + CheckpointScopeId, + CommandId, + type OrchestrationV2Checkpoint, + type OrchestrationV2ExecutionNode, + type OrchestrationV2ProviderThread, + type OrchestrationV2Run, + type OrchestrationV2TurnItem, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import { CheckpointServiceV2 } from "./CheckpointService.ts"; +import { EventSinkV2 } from "./EventSink.ts"; +import { IdAllocatorV2, type IdAllocatorV2Shape } from "./IdAllocator.ts"; +import { ProjectionStoreV2 } from "./ProjectionStore.ts"; + +export class CheckpointCaptureExecutionError extends Schema.TaggedErrorClass()( + "CheckpointCaptureExecutionError", + { + threadId: ThreadId, + runId: RunId, + scopeId: CheckpointScopeId, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface CheckpointCaptureServiceV2Shape { + readonly execute: (input: { + readonly threadId: ThreadId; + readonly runId: RunId; + readonly scopeId: CheckpointScopeId; + }) => Effect.Effect; +} + +export class CheckpointCaptureServiceV2 extends Context.Service< + CheckpointCaptureServiceV2, + CheckpointCaptureServiceV2Shape +>()("t3/orchestration-v2/CheckpointCaptureService/CheckpointCaptureServiceV2") {} + +export const layer: Layer.Layer< + CheckpointCaptureServiceV2, + never, + CheckpointServiceV2 | EventSinkV2 | IdAllocatorV2 | ProjectionStoreV2 +> = Layer.effect( + CheckpointCaptureServiceV2, + Effect.gen(function* () { + const checkpoints = yield* CheckpointServiceV2; + const eventSink = yield* EventSinkV2; + const ids = yield* IdAllocatorV2; + const projections = yield* ProjectionStoreV2; + + const execute = Effect.fn("orchestrationV2.checkpointCapture.execute")(function* (input: { + readonly threadId: ThreadId; + readonly runId: RunId; + readonly scopeId: CheckpointScopeId; + }) { + const projection = yield* projections.getThreadProjection(input.threadId); + const run = projection.runs.find((candidate) => candidate.id === input.runId); + + // The effect is at-least-once. A completed run with a checkpoint proves + // that an earlier execution committed its result. + if (run?.status === "completed" && run.checkpointId !== null) { + return; + } + + const rootNode = projection.nodes.find((candidate) => candidate.id === run?.rootNodeId); + const scope = projection.checkpointScopes.find((candidate) => candidate.id === input.scopeId); + const providerThread = projection.providerThreads.find( + (candidate) => candidate.id === run?.providerThreadId, + ); + if ( + run === undefined || + run.status !== "waiting" || + rootNode === undefined || + scope === undefined || + rootNode.checkpointScopeId !== scope.id || + providerThread === undefined + ) { + return yield* new CheckpointCaptureExecutionError({ + threadId: input.threadId, + runId: input.runId, + scopeId: input.scopeId, + cause: "The persisted checkpoint capture target is incomplete or no longer waiting.", + }); + } + + const capturedAt = yield* DateTime.now; + const checkpoint = yield* checkpoints.capture({ + scope, + runId: run.id, + nodeId: rootNode.id, + ordinalWithinScope: run.ordinal, + appRunOrdinal: run.ordinal, + capturedAt, + }); + const commandId = CommandId.make(`command:effect:checkpoint.capture:${run.id}`); + yield* eventSink.commitCommand({ + commandId, + threadId: input.threadId, + commandType: "checkpoint.capture", + acceptedAt: capturedAt, + effects: [], + events: [ + { + id: yield* ids.allocate.event({ threadId: input.threadId, commandId }), + type: "checkpoint.captured", + threadId: input.threadId, + runId: run.id, + nodeId: rootNode.id, + driver: providerThread.driver, + providerInstanceId: run.providerInstanceId, + occurredAt: capturedAt, + payload: checkpoint, + }, + { + id: yield* ids.allocate.event({ threadId: input.threadId, commandId }), + type: "turn-item.updated", + threadId: input.threadId, + runId: run.id, + nodeId: rootNode.id, + driver: providerThread.driver, + providerInstanceId: run.providerInstanceId, + occurredAt: capturedAt, + payload: makeCheckpointTurnItem({ + idAllocator: ids, + run, + rootNode, + providerThread, + checkpoint, + completedAt: capturedAt, + }), + }, + { + id: yield* ids.allocate.event({ threadId: input.threadId, commandId }), + type: "run.updated", + threadId: input.threadId, + runId: run.id, + nodeId: rootNode.id, + providerInstanceId: run.providerInstanceId, + occurredAt: capturedAt, + payload: { + ...run, + status: "completed", + completedAt: capturedAt, + checkpointId: checkpoint.id, + }, + }, + { + id: yield* ids.allocate.event({ threadId: input.threadId, commandId }), + type: "node.updated", + threadId: input.threadId, + runId: run.id, + nodeId: rootNode.id, + providerInstanceId: run.providerInstanceId, + occurredAt: capturedAt, + payload: { + ...rootNode, + status: "completed", + completedAt: capturedAt, + checkpointScopeId: scope.id, + }, + }, + ], + }); + }); + + return CheckpointCaptureServiceV2.of({ + execute: (input) => + execute(input).pipe( + Effect.mapError((cause) => + Schema.is(CheckpointCaptureExecutionError)(cause) + ? cause + : new CheckpointCaptureExecutionError({ ...input, cause }), + ), + ), + }); + }), +); + +function makeCheckpointTurnItem(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly run: OrchestrationV2Run; + readonly rootNode: OrchestrationV2ExecutionNode; + readonly providerThread: OrchestrationV2ProviderThread; + readonly checkpoint: OrchestrationV2Checkpoint; + readonly completedAt: DateTime.Utc; +}): OrchestrationV2TurnItem { + return { + id: input.idAllocator.derive.turnItemFromProviderItem({ + driver: input.providerThread.driver, + nativeItemId: `checkpoint:${input.checkpoint.id}`, + }), + threadId: input.run.threadId, + runId: input.run.id, + nodeId: input.rootNode.id, + providerThreadId: input.providerThread.id, + providerTurnId: input.rootNode.providerTurnId, + nativeItemRef: null, + parentItemId: null, + ordinal: input.run.ordinal * 100 + 99, + status: "completed", + title: null, + startedAt: input.completedAt, + completedAt: input.completedAt, + updatedAt: input.completedAt, + type: "checkpoint", + checkpointId: input.checkpoint.id, + scopeId: input.checkpoint.scopeId, + files: input.checkpoint.files, + }; +} diff --git a/apps/server/src/orchestration-v2/CheckpointPolicy.ts b/apps/server/src/orchestration-v2/CheckpointPolicy.ts new file mode 100644 index 00000000000..256f590ae55 --- /dev/null +++ b/apps/server/src/orchestration-v2/CheckpointPolicy.ts @@ -0,0 +1,84 @@ +import { + CheckpointId, + CheckpointScopeId, + NodeId, + OrchestrationV2Checkpoint, + OrchestrationV2CheckpointScope, + OrchestrationV2ExecutionNode, + OrchestrationV2Run, + OrchestrationV2ThreadProjection, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Schema from "effect/Schema"; +import type * as Effect from "effect/Effect"; + +export class CheckpointPolicyPrepareRunError extends Schema.TaggedErrorClass()( + "CheckpointPolicyPrepareRunError", + { + threadId: ThreadId, + runId: RunId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to prepare checkpoint policy for run ${this.runId}.`; + } +} + +export class CheckpointPolicyFinalizeNodeError extends Schema.TaggedErrorClass()( + "CheckpointPolicyFinalizeNodeError", + { + threadId: ThreadId, + nodeId: NodeId, + scopeId: Schema.optional(CheckpointScopeId), + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to finalize checkpoint policy for node ${this.nodeId}.`; + } +} + +export class CheckpointPolicyRollbackError extends Schema.TaggedErrorClass()( + "CheckpointPolicyRollbackError", + { + threadId: ThreadId, + scopeId: CheckpointScopeId, + checkpointId: CheckpointId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to apply checkpoint rollback ${this.checkpointId} for scope ${this.scopeId}.`; + } +} + +export const CheckpointPolicyV2Error = Schema.Union([ + CheckpointPolicyPrepareRunError, + CheckpointPolicyFinalizeNodeError, + CheckpointPolicyRollbackError, +]); +export type CheckpointPolicyV2Error = typeof CheckpointPolicyV2Error.Type; + +export interface CheckpointPolicyV2Shape { + readonly prepareRun: (input: { + readonly projection: OrchestrationV2ThreadProjection; + readonly run: OrchestrationV2Run; + }) => Effect.Effect, CheckpointPolicyV2Error>; + readonly finalizeNode: (input: { + readonly projection: OrchestrationV2ThreadProjection; + readonly node: OrchestrationV2ExecutionNode; + }) => Effect.Effect, CheckpointPolicyV2Error>; + readonly rollback: (input: { + readonly projection: OrchestrationV2ThreadProjection; + readonly scopeId: CheckpointScopeId; + readonly checkpointId: CheckpointId; + }) => Effect.Effect, CheckpointPolicyV2Error>; +} + +export class CheckpointPolicyV2 extends Context.Service< + CheckpointPolicyV2, + CheckpointPolicyV2Shape +>()("t3/orchestration-v2/CheckpointPolicy/CheckpointPolicyV2") {} diff --git a/apps/server/src/orchestration-v2/CheckpointRollbackService.ts b/apps/server/src/orchestration-v2/CheckpointRollbackService.ts new file mode 100644 index 00000000000..f8732c22ae1 --- /dev/null +++ b/apps/server/src/orchestration-v2/CheckpointRollbackService.ts @@ -0,0 +1,254 @@ +import { + CheckpointId, + CheckpointScopeId, + type OrchestrationV2DomainEvent, + ProviderThreadId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import { CheckpointServiceV2 } from "./CheckpointService.ts"; +import { EventSinkV2 } from "./EventSink.ts"; +import { IdAllocatorV2 } from "./IdAllocator.ts"; +import { ProjectionStoreV2 } from "./ProjectionStore.ts"; +import type { ProviderAdapterV2RollbackTarget } from "./ProviderAdapter.ts"; +import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts"; +import { RuntimePolicyV2 } from "./RuntimePolicy.ts"; + +export class CheckpointRollbackExecutionError extends Schema.TaggedErrorClass()( + "CheckpointRollbackExecutionError", + { + threadId: ThreadId, + providerThreadId: ProviderThreadId, + checkpointId: CheckpointId, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface CheckpointRollbackServiceV2Shape { + readonly execute: (input: { + readonly threadId: ThreadId; + readonly providerThreadId: ProviderThreadId; + readonly checkpointId: CheckpointId; + readonly scopeId: CheckpointScopeId; + }) => Effect.Effect; +} + +export class CheckpointRollbackServiceV2 extends Context.Service< + CheckpointRollbackServiceV2, + CheckpointRollbackServiceV2Shape +>()("t3/orchestration-v2/CheckpointRollbackService/CheckpointRollbackServiceV2") {} + +export const layer: Layer.Layer< + CheckpointRollbackServiceV2, + never, + | CheckpointServiceV2 + | EventSinkV2 + | IdAllocatorV2 + | ProjectionStoreV2 + | ProviderSessionManagerV2 + | RuntimePolicyV2 +> = Layer.effect( + CheckpointRollbackServiceV2, + Effect.gen(function* () { + const checkpoints = yield* CheckpointServiceV2; + const eventSink = yield* EventSinkV2; + const ids = yield* IdAllocatorV2; + const projections = yield* ProjectionStoreV2; + const sessions = yield* ProviderSessionManagerV2; + const runtimePolicy = yield* RuntimePolicyV2; + + const execute = Effect.fn("orchestrationV2.checkpointRollback.execute")(function* (input: { + readonly threadId: ThreadId; + readonly providerThreadId: ProviderThreadId; + readonly checkpointId: CheckpointId; + readonly scopeId: CheckpointScopeId; + }) { + const projection = yield* projections.getThreadProjection(input.threadId); + const providerThread = projection.providerThreads.find( + (candidate) => candidate.id === input.providerThreadId, + ); + const checkpoint = projection.checkpoints.find( + (candidate) => candidate.id === input.checkpointId, + ); + const scope = projection.checkpointScopes.find((candidate) => candidate.id === input.scopeId); + if ( + providerThread === undefined || + providerThread.providerSessionId === null || + checkpoint === undefined || + scope === undefined || + checkpoint.scopeId !== scope.id + ) { + return yield* new CheckpointRollbackExecutionError({ + threadId: input.threadId, + providerThreadId: input.providerThreadId, + checkpointId: input.checkpointId, + cause: "The persisted rollback target is incomplete or no longer valid.", + }); + } + + const modelSelection = projection.thread.modelSelection; + const resolvedRuntimePolicy = yield* runtimePolicy.resolve({ + thread: projection.thread, + modelSelection, + }); + const existingSession = projection.providerSessions.find( + (candidate) => candidate.id === providerThread.providerSessionId, + ); + const session = yield* sessions.open({ + threadId: input.threadId, + providerSessionId: providerThread.providerSessionId, + modelSelection, + runtimePolicy: resolvedRuntimePolicy, + ...(existingSession === undefined ? {} : { resumeFromSession: existingSession }), + }); + + const targetOrdinal = checkpoint.appRunOrdinal ?? 0; + const runsToRollback = projection.runs.filter( + (run) => run.ordinal > targetOrdinal && run.status === "completed", + ); + const providerThreadTurns = projection.providerTurns.filter( + (turn) => turn.providerThreadId === providerThread.id, + ); + const rollbackTarget: ProviderAdapterV2RollbackTarget = + targetOrdinal === 0 + ? { + type: "thread_start", + checkpointId: checkpoint.id, + appRunOrdinal: 0, + } + : yield* Effect.gen(function* () { + const targetRun = projection.runs.find((run) => run.ordinal === targetOrdinal); + const targetAttempt = projection.attempts.find( + (attempt) => attempt.id === targetRun?.activeAttemptId, + ); + const targetTurn = projection.providerTurns.find( + (turn) => + turn.id === targetAttempt?.providerTurnId || + turn.runAttemptId === targetAttempt?.id, + ); + if (targetTurn === undefined || targetTurn.providerThreadId !== providerThread.id) { + return yield* new CheckpointRollbackExecutionError({ + threadId: input.threadId, + providerThreadId: input.providerThreadId, + checkpointId: input.checkpointId, + cause: "The provider rollback turn is unavailable.", + }); + } + return { + type: "provider_turn" as const, + checkpointId: checkpoint.id, + appRunOrdinal: targetOrdinal, + providerTurn: targetTurn, + }; + }); + + yield* checkpoints.restore({ scope, checkpoint }); + const snapshot = + runsToRollback.length === 0 + ? { providerThread } + : yield* session.rollbackThread({ + providerThread, + target: rollbackTarget, + providerThreadTurns, + }); + const staleCheckpoints = projection.checkpoints.filter( + (candidate) => + candidate.scopeId === scope.id && + candidate.appRunOrdinal !== null && + candidate.appRunOrdinal > targetOrdinal && + candidate.status === "ready", + ); + if (staleCheckpoints.length > 0) { + yield* checkpoints.deleteStaleRefs({ scope, checkpoints: staleCheckpoints }); + } + + const now = yield* DateTime.now; + const makeEvent = (event: Omit) => + Effect.map( + ids.allocate.event({ threadId: event.threadId }), + (id) => + ({ + ...event, + id, + }) as Event, + ); + const events: Array = []; + events.push( + yield* makeEvent({ + type: "provider-thread.updated", + threadId: input.threadId, + driver: providerThread.driver, + providerInstanceId: providerThread.providerInstanceId, + occurredAt: now, + payload: { + ...snapshot.providerThread, + lastRunOrdinal: targetOrdinal === 0 ? null : targetOrdinal, + updatedAt: now, + }, + }), + ); + for (const staleCheckpoint of staleCheckpoints) { + events.push( + yield* makeEvent({ + type: "checkpoint.captured", + threadId: input.threadId, + ...(staleCheckpoint.runId === null ? {} : { runId: staleCheckpoint.runId }), + nodeId: staleCheckpoint.nodeId, + providerInstanceId: providerThread.providerInstanceId, + occurredAt: now, + payload: { ...staleCheckpoint, status: "stale" }, + }), + ); + } + for (const run of runsToRollback) { + const rootNode = projection.nodes.find((candidate) => candidate.id === run.rootNodeId); + events.push( + yield* makeEvent({ + type: "run.updated", + threadId: input.threadId, + runId: run.id, + ...(rootNode === undefined ? {} : { nodeId: rootNode.id }), + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: { ...run, status: "rolled_back", completedAt: now }, + }), + ); + if (rootNode !== undefined) { + events.push( + yield* makeEvent({ + type: "node.updated", + threadId: input.threadId, + runId: run.id, + nodeId: rootNode.id, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: { ...rootNode, status: "rolled_back", completedAt: now }, + }), + ); + } + } + yield* eventSink.write({ events }); + }); + + return CheckpointRollbackServiceV2.of({ + execute: (input) => + execute(input).pipe( + Effect.mapError((cause) => + Schema.is(CheckpointRollbackExecutionError)(cause) + ? cause + : new CheckpointRollbackExecutionError({ + threadId: input.threadId, + providerThreadId: input.providerThreadId, + checkpointId: input.checkpointId, + cause, + }), + ), + ), + }); + }), +); diff --git a/apps/server/src/orchestration-v2/CheckpointService.ts b/apps/server/src/orchestration-v2/CheckpointService.ts new file mode 100644 index 00000000000..370efeea42b --- /dev/null +++ b/apps/server/src/orchestration-v2/CheckpointService.ts @@ -0,0 +1,515 @@ +import { + CheckpointId, + CheckpointRef, + CheckpointScopeId, + NodeId, + OrchestrationV2Checkpoint, + OrchestrationV2CheckpointScope, + ProviderThreadId, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import * as NodeCrypto from "node:crypto"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Encoding from "effect/Encoding"; +import * as Layer from "effect/Layer"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Semaphore from "effect/Semaphore"; + +import { parseTurnDiffFilesFromUnifiedDiff } from "../checkpointing/Diffs.ts"; +import * as CheckpointStore from "../checkpointing/CheckpointStore.ts"; +import { IdAllocatorV2, type IdAllocatorV2Shape } from "./IdAllocator.ts"; + +const CHECKPOINT_REFS_PREFIX = "refs/t3/orchestration-v2/checkpoints"; +const ROOT_CHECKPOINT_SCOPE_NAME = "root"; + +export class CheckpointRootScopePrepareError extends Schema.TaggedErrorClass()( + "CheckpointRootScopePrepareError", + { + threadId: ThreadId, + runId: RunId, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to prepare root checkpoint scope for run ${this.runId}.`; + } +} + +export class CheckpointScopeEnsureError extends Schema.TaggedErrorClass()( + "CheckpointScopeEnsureError", + { + scopeId: CheckpointScopeId, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to ensure checkpoint scope ${this.scopeId}.`; + } +} + +export class CheckpointBaselineCaptureError extends Schema.TaggedErrorClass()( + "CheckpointBaselineCaptureError", + { + scopeId: CheckpointScopeId, + ordinalWithinScope: Schema.Number, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to capture checkpoint baseline ${this.ordinalWithinScope} for scope ${this.scopeId}.`; + } +} + +export class CheckpointCaptureError extends Schema.TaggedErrorClass()( + "CheckpointCaptureError", + { + scopeId: CheckpointScopeId, + parentCheckpointId: Schema.optional(CheckpointId), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to capture checkpoint for scope ${this.scopeId}.`; + } +} + +export class CheckpointRestoreError extends Schema.TaggedErrorClass()( + "CheckpointRestoreError", + { + scopeId: CheckpointScopeId, + checkpointId: CheckpointId, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to restore checkpoint ${this.checkpointId} for scope ${this.scopeId}.`; + } +} + +export class CheckpointDeleteStaleRefsError extends Schema.TaggedErrorClass()( + "CheckpointDeleteStaleRefsError", + { + scopeId: CheckpointScopeId, + checkpointIds: Schema.Array(CheckpointId), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to delete stale checkpoint refs for scope ${this.scopeId}.`; + } +} + +export const CheckpointServiceV2Error = Schema.Union([ + CheckpointRootScopePrepareError, + CheckpointScopeEnsureError, + CheckpointBaselineCaptureError, + CheckpointCaptureError, + CheckpointRestoreError, + CheckpointDeleteStaleRefsError, +]); +export type CheckpointServiceV2Error = typeof CheckpointServiceV2Error.Type; + +const isCheckpointRestoreError = Schema.is(CheckpointRestoreError); + +export interface CheckpointServiceV2Shape { + readonly prepareRootRunScope: (input: { + readonly threadId: ThreadId; + readonly runId: RunId; + readonly rootNodeId: NodeId; + readonly providerThreadId: ProviderThreadId; + readonly cwd: string; + readonly createdAt: DateTime.Utc; + }) => Effect.Effect; + readonly ensureScope: ( + scope: OrchestrationV2CheckpointScope, + ) => Effect.Effect; + readonly captureBaseline: (input: { + readonly scope: OrchestrationV2CheckpointScope; + readonly ordinalWithinScope: number; + }) => Effect.Effect; + readonly capture: (input: { + readonly scope: OrchestrationV2CheckpointScope; + readonly runId: RunId | null; + readonly nodeId: NodeId; + readonly ordinalWithinScope: number; + readonly appRunOrdinal: number | null; + readonly capturedAt: DateTime.Utc; + }) => Effect.Effect; + readonly restore: (input: { + readonly scope: OrchestrationV2CheckpointScope; + readonly checkpoint: OrchestrationV2Checkpoint; + }) => Effect.Effect; + readonly deleteStaleRefs: (input: { + readonly scope: OrchestrationV2CheckpointScope; + readonly checkpoints: ReadonlyArray; + }) => Effect.Effect; +} + +export class CheckpointServiceV2 extends Context.Service< + CheckpointServiceV2, + CheckpointServiceV2Shape +>()("t3/orchestration-v2/CheckpointService/CheckpointServiceV2") {} + +export function checkpointRefForScopeOrdinal(input: { + readonly scopeId: CheckpointScopeId; + readonly ordinalWithinScope: number; +}): CheckpointRef { + const scopeKey = NodeCrypto.createHash("sha256").update(input.scopeId).digest("hex").slice(0, 32); + return CheckpointRef.make( + `${CHECKPOINT_REFS_PREFIX}/${Encoding.encodeBase64Url(scopeKey)}/ordinal/${input.ordinalWithinScope}`, + ); +} + +function checkpointIdForScopeOrdinal( + idAllocator: IdAllocatorV2Shape, + input: { + readonly scopeId: CheckpointScopeId; + readonly ordinalWithinScope: number; + }, +) { + return idAllocator.allocate.checkpoint({ + checkpointScopeId: input.scopeId, + name: String(input.ordinalWithinScope), + }); +} + +function makeRootRunScope(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly threadId: ThreadId; + readonly runId: RunId; + readonly rootNodeId: NodeId; + readonly providerThreadId: ProviderThreadId; + readonly cwd: string; + readonly createdAt: DateTime.Utc; +}) { + return Effect.gen(function* () { + const scopeId = yield* input.idAllocator.allocate.checkpointScope({ + threadId: input.threadId, + name: ROOT_CHECKPOINT_SCOPE_NAME, + }); + return { + id: scopeId, + threadId: input.threadId, + runId: input.runId, + nodeId: input.rootNodeId, + parentScopeId: null, + providerThreadId: input.providerThreadId, + kind: "root_run", + ordinalWithinParent: 0, + advancesAppRunCount: true, + cwd: input.cwd, + createdAt: input.createdAt, + } satisfies OrchestrationV2CheckpointScope; + }); +} + +function makeCheckpoint(input: { + readonly id: CheckpointId; + readonly scope: OrchestrationV2CheckpointScope; + readonly runId: RunId | null; + readonly nodeId: NodeId; + readonly parentCheckpointId: CheckpointId | null; + readonly ordinalWithinScope: number; + readonly appRunOrdinal: number | null; + readonly ref: CheckpointRef; + readonly status: OrchestrationV2Checkpoint["status"]; + readonly files: OrchestrationV2Checkpoint["files"]; + readonly capturedAt: DateTime.Utc; +}): OrchestrationV2Checkpoint { + return { + id: input.id, + threadId: input.scope.threadId, + scopeId: input.scope.id, + runId: input.runId, + nodeId: input.nodeId, + parentCheckpointId: input.parentCheckpointId, + ordinalWithinScope: input.ordinalWithinScope, + appRunOrdinal: input.appRunOrdinal, + ref: input.ref, + status: input.status, + files: input.files, + capturedAt: input.capturedAt, + }; +} + +export const layer: Layer.Layer< + CheckpointServiceV2, + never, + CheckpointStore.CheckpointStore | IdAllocatorV2 +> = Layer.effect( + CheckpointServiceV2, + Effect.gen(function* () { + const checkpointStore = yield* CheckpointStore.CheckpointStore; + const idAllocator = yield* IdAllocatorV2; + const workspaceSemaphores = yield* Ref.make(new Map()); + + const getWorkspaceSemaphore = (cwd: string) => + Effect.gen(function* () { + const existing = (yield* Ref.get(workspaceSemaphores)).get(cwd); + if (existing !== undefined) { + return existing; + } + + const created = yield* Semaphore.make(1); + return yield* Ref.modify(workspaceSemaphores, (current) => { + const concurrent = current.get(cwd); + if (concurrent !== undefined) { + return [concurrent, current]; + } + const updated = new Map(current); + updated.set(cwd, created); + return [created, updated]; + }); + }); + + const withWorkspaceLock = (cwd: string, effect: Effect.Effect) => + Effect.flatMap(getWorkspaceSemaphore(cwd), (semaphore) => semaphore.withPermits(1)(effect)); + + const isGitCheckpointable = (cwd: string) => + checkpointStore.isGitRepository(cwd).pipe(Effect.orElseSucceed(() => false)); + + const ensureScope: CheckpointServiceV2Shape["ensureScope"] = (scope) => Effect.succeed(scope); + + const captureBaseline: CheckpointServiceV2Shape["captureBaseline"] = (input) => + withWorkspaceLock( + input.scope.cwd, + Effect.gen(function* () { + if (!(yield* isGitCheckpointable(input.scope.cwd))) { + return; + } + + const checkpointRef = checkpointRefForScopeOrdinal({ + scopeId: input.scope.id, + ordinalWithinScope: input.ordinalWithinScope, + }); + const exists = yield* checkpointStore.hasCheckpointRef({ + cwd: input.scope.cwd, + checkpointRef, + }); + if (exists) { + return; + } + + yield* checkpointStore.captureCheckpoint({ + cwd: input.scope.cwd, + checkpointRef, + }); + }), + ).pipe( + Effect.mapError( + (cause) => + new CheckpointBaselineCaptureError({ + scopeId: input.scope.id, + ordinalWithinScope: input.ordinalWithinScope, + cause, + }), + ), + ); + + const capture: CheckpointServiceV2Shape["capture"] = (input) => + withWorkspaceLock( + input.scope.cwd, + Effect.gen(function* () { + const checkpointId = yield* checkpointIdForScopeOrdinal(idAllocator, { + scopeId: input.scope.id, + ordinalWithinScope: input.ordinalWithinScope, + }); + const parentCheckpointId = + input.ordinalWithinScope > 1 + ? yield* checkpointIdForScopeOrdinal(idAllocator, { + scopeId: input.scope.id, + ordinalWithinScope: input.ordinalWithinScope - 1, + }) + : null; + const checkpointRef = checkpointRefForScopeOrdinal({ + scopeId: input.scope.id, + ordinalWithinScope: input.ordinalWithinScope, + }); + const previousCheckpointRef = checkpointRefForScopeOrdinal({ + scopeId: input.scope.id, + ordinalWithinScope: Math.max(0, input.ordinalWithinScope - 1), + }); + + if (!(yield* isGitCheckpointable(input.scope.cwd))) { + return makeCheckpoint({ + id: checkpointId, + scope: input.scope, + runId: input.runId, + nodeId: input.nodeId, + parentCheckpointId, + ordinalWithinScope: input.ordinalWithinScope, + appRunOrdinal: input.appRunOrdinal, + ref: checkpointRef, + status: "missing", + files: [], + capturedAt: input.capturedAt, + }); + } + + const captured = yield* checkpointStore + .captureCheckpoint({ + cwd: input.scope.cwd, + checkpointRef, + }) + .pipe( + Effect.as(true), + Effect.catch((cause) => + Effect.logWarning("orchestration V2 checkpoint capture failed", { + scopeId: input.scope.id, + checkpointRef, + cause: String(cause), + }).pipe(Effect.as(false)), + ), + ); + + if (!captured) { + return makeCheckpoint({ + id: checkpointId, + scope: input.scope, + runId: input.runId, + nodeId: input.nodeId, + parentCheckpointId, + ordinalWithinScope: input.ordinalWithinScope, + appRunOrdinal: input.appRunOrdinal, + ref: checkpointRef, + status: "error", + files: [], + capturedAt: input.capturedAt, + }); + } + + const previousExists = yield* checkpointStore.hasCheckpointRef({ + cwd: input.scope.cwd, + checkpointRef: previousCheckpointRef, + }); + const files = previousExists + ? yield* checkpointStore + .diffCheckpoints({ + cwd: input.scope.cwd, + fromCheckpointRef: previousCheckpointRef, + toCheckpointRef: checkpointRef, + fallbackFromToHead: false, + ignoreWhitespace: false, + }) + .pipe( + Effect.map((diff) => + parseTurnDiffFilesFromUnifiedDiff(diff).map((file) => ({ + path: file.path, + kind: "modified", + additions: file.additions, + deletions: file.deletions, + })), + ), + Effect.catch((cause) => + Effect.logWarning("orchestration V2 checkpoint diff summary failed", { + scopeId: input.scope.id, + checkpointRef, + cause: String(cause), + }).pipe(Effect.as([])), + ), + ) + : []; + + return makeCheckpoint({ + id: checkpointId, + scope: input.scope, + runId: input.runId, + nodeId: input.nodeId, + parentCheckpointId, + ordinalWithinScope: input.ordinalWithinScope, + appRunOrdinal: input.appRunOrdinal, + ref: checkpointRef, + status: "ready", + files, + capturedAt: input.capturedAt, + }); + }), + ).pipe( + Effect.mapError( + (cause) => + new CheckpointCaptureError({ + scopeId: input.scope.id, + cause, + }), + ), + ); + + const restore: CheckpointServiceV2Shape["restore"] = (input) => + withWorkspaceLock( + input.scope.cwd, + Effect.gen(function* () { + if (input.checkpoint.status !== "ready") { + return yield* new CheckpointRestoreError({ + scopeId: input.scope.id, + checkpointId: input.checkpoint.id, + cause: `Checkpoint status is ${input.checkpoint.status}.`, + }); + } + + const restored = yield* checkpointStore.restoreCheckpoint({ + cwd: input.scope.cwd, + checkpointRef: input.checkpoint.ref, + fallbackToHead: false, + }); + if (!restored) { + return yield* new CheckpointRestoreError({ + scopeId: input.scope.id, + checkpointId: input.checkpoint.id, + cause: "Checkpoint ref is unavailable.", + }); + } + }), + ).pipe( + Effect.mapError((cause) => + isCheckpointRestoreError(cause) + ? cause + : new CheckpointRestoreError({ + scopeId: input.scope.id, + checkpointId: input.checkpoint.id, + cause, + }), + ), + ); + + const deleteStaleRefs: CheckpointServiceV2Shape["deleteStaleRefs"] = (input) => + withWorkspaceLock( + input.scope.cwd, + checkpointStore.deleteCheckpointRefs({ + cwd: input.scope.cwd, + checkpointRefs: input.checkpoints.map((checkpoint) => checkpoint.ref), + }), + ).pipe( + Effect.mapError( + (cause) => + new CheckpointDeleteStaleRefsError({ + scopeId: input.scope.id, + checkpointIds: input.checkpoints.map((checkpoint) => checkpoint.id), + cause, + }), + ), + ); + + return CheckpointServiceV2.of({ + prepareRootRunScope: (input) => + makeRootRunScope({ ...input, idAllocator }).pipe( + Effect.mapError( + (cause) => + new CheckpointRootScopePrepareError({ + threadId: input.threadId, + runId: input.runId, + cause, + }), + ), + ), + ensureScope, + captureBaseline, + capture, + restore, + deleteStaleRefs, + } satisfies CheckpointServiceV2Shape); + }), +); diff --git a/apps/server/src/orchestration-v2/CommandPolicy.test.ts b/apps/server/src/orchestration-v2/CommandPolicy.test.ts new file mode 100644 index 00000000000..90f004f3135 --- /dev/null +++ b/apps/server/src/orchestration-v2/CommandPolicy.test.ts @@ -0,0 +1,310 @@ +import { assert, it } from "@effect/vitest"; +import { + CommandId, + type OrchestrationV2ProviderCapabilities, + ProviderInstanceId, + ThreadId, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; + +import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; +import { CursorProviderCapabilitiesV2 } from "./Adapters/CursorAdapterV2.ts"; +import { GrokProviderCapabilitiesV2 } from "./Adapters/GrokAdapterV2.ts"; +import { + CommandPolicyCapabilityUnsupportedError, + CommandPolicyV2, + layer as commandPolicyLayer, +} from "./CommandPolicy.ts"; + +const commandId = CommandId.make("command-policy-test"); +const threadId = ThreadId.make("command-policy-thread"); + +const baseCapabilities: OrchestrationV2ProviderCapabilities = CodexProviderCapabilitiesV2; + +function capabilities( + override: (current: OrchestrationV2ProviderCapabilities) => OrchestrationV2ProviderCapabilities, +): OrchestrationV2ProviderCapabilities { + return override(baseCapabilities); +} + +const layer = it.layer(commandPolicyLayer); + +layer("CommandPolicyV2", (it) => { + it.effect("prefers direct active steering when the provider supports it", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const result = yield* policy.decideSteeringExecution({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + capabilities: baseCapabilities, + }); + + assert.equal(result, "active_steering"); + }), + ); + + it.effect("uses interrupt-and-restart steering when direct steering is unavailable", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const result = yield* policy.decideSteeringExecution({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + capabilities: capabilities((current) => ({ + ...current, + turns: { + ...current.turns, + supportsActiveSteering: false, + supportsInterrupt: true, + supportsSteeringByInterruptRestart: true, + }, + })), + }); + + assert.equal(result, "interrupt_restart"); + }), + ); + + it.effect("uses interrupt-and-restart steering for Grok ACP", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const result = yield* policy.decideSteeringExecution({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("grok"), + capabilities: GrokProviderCapabilitiesV2, + }); + + assert.equal(result, "interrupt_restart"); + }), + ); + + it.effect("honors an explicit interrupt-and-restart request", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const result = yield* policy.decideSteeringExecution({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + capabilities: CodexProviderCapabilitiesV2, + forceRestart: true, + }); + + assert.equal(result, "interrupt_restart"); + }), + ); + + it.effect("returns typed capability errors for unsupported active steering", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const error = yield* policy + .decideSteeringExecution({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + capabilities: capabilities((current) => ({ + ...current, + turns: { + ...current.turns, + supportsActiveSteering: false, + supportsInterrupt: false, + supportsSteeringByInterruptRestart: false, + }, + })), + }) + .pipe(Effect.flip); + + assert.instanceOf(error, CommandPolicyCapabilityUnsupportedError); + assert.equal(error.capability, "active_steering"); + }), + ); + + it.effect("guards native fork behind fork and identity capabilities", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const error = yield* policy + .ensureNativeFork({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + fromSpecificTurn: true, + capabilities: capabilities((current) => ({ + ...current, + identity: { + ...current.identity, + nativeThreadIds: "weak", + }, + })), + }) + .pipe(Effect.flip); + + assert.instanceOf(error, CommandPolicyCapabilityUnsupportedError); + assert.equal(error.capability, "native_fork"); + }), + ); + + it.effect("uses a native fork when the provider supports the requested source point", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const result = yield* policy.decideForkExecution({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + capabilities: CodexProviderCapabilitiesV2, + sameProvider: true, + hasStrongNativeSource: true, + fromSpecificTurn: true, + }); + + assert.equal(result, "native_fork"); + }), + ); + + it.effect("falls back to portable context when Cursor cannot fork natively", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const result = yield* policy.decideForkExecution({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("cursor"), + capabilities: CursorProviderCapabilitiesV2, + sameProvider: true, + hasStrongNativeSource: true, + fromSpecificTurn: true, + }); + + assert.equal(result, "portable_context"); + }), + ); + + it.effect("falls back to portable context when Grok ACP cannot fork natively", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const result = yield* policy.decideForkExecution({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("grok"), + capabilities: GrokProviderCapabilitiesV2, + sameProvider: true, + hasStrongNativeSource: true, + fromSpecificTurn: true, + }); + + assert.equal(result, "portable_context"); + }), + ); + + it.effect("returns a typed error when neither native nor portable fork is available", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const error = yield* policy + .decideForkExecution({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("cursor"), + capabilities: capabilities((current) => ({ + ...current, + threads: { + ...current.threads, + canForkThread: false, + }, + context: { + ...current.context, + canConsumeHandoffSummaries: false, + }, + })), + sameProvider: true, + hasStrongNativeSource: true, + fromSpecificTurn: true, + }) + .pipe(Effect.flip); + + assert.instanceOf(error, CommandPolicyCapabilityUnsupportedError); + assert.equal(error.capability, "context_handoff"); + }), + ); + + it.effect("guards rollback behind provider rollback snapshot support", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const error = yield* policy + .ensureRollback({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + capabilities: capabilities((current) => ({ + ...current, + checkpointing: { + ...current.checkpointing, + providerRollbackReturnsSnapshot: false, + }, + })), + }) + .pipe(Effect.flip); + + assert.instanceOf(error, CommandPolicyCapabilityUnsupportedError); + assert.equal(error.capability, "rollback_snapshot"); + }), + ); + + it.effect("guards fork-delta handoff behind context handoff capabilities", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const error = yield* policy + .ensureContextHandoff({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + strategy: "fork_delta_context", + capabilities: capabilities((current) => ({ + ...current, + context: { + ...current.context, + supportsDeltaHandoff: false, + }, + })), + }) + .pipe(Effect.flip); + + assert.instanceOf(error, CommandPolicyCapabilityUnsupportedError); + assert.equal(error.capability, "context_handoff"); + }), + ); + + it.effect("guards queued turns behind queued-message support", () => + Effect.gen(function* () { + const policy = yield* CommandPolicyV2; + + const error = yield* policy + .ensureQueuedMessages({ + commandId, + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + capabilities: capabilities((current) => ({ + ...current, + turns: { + ...current.turns, + supportsQueuedMessages: false, + }, + })), + }) + .pipe(Effect.flip); + + assert.instanceOf(error, CommandPolicyCapabilityUnsupportedError); + assert.equal(error.capability, "queued_messages"); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/CommandPolicy.ts b/apps/server/src/orchestration-v2/CommandPolicy.ts new file mode 100644 index 00000000000..c917c14bac2 --- /dev/null +++ b/apps/server/src/orchestration-v2/CommandPolicy.ts @@ -0,0 +1,424 @@ +import { + CommandId, + ModelSelection, + OrchestrationV2ProviderCapabilities, + OrchestrationV2ThreadProjection, + ProviderInstanceId, + ProviderTurnId, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +export const MessageDispatchDecisionV2 = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("start_run"), + modelSelection: ModelSelection, + }), + Schema.Struct({ + type: Schema.Literal("steer_active"), + targetRunId: RunId, + providerTurnId: ProviderTurnId, + }), + Schema.Struct({ + type: Schema.Literal("restart_active"), + targetRunId: RunId, + interruptProviderTurnId: ProviderTurnId, + }), + Schema.Struct({ + type: Schema.Literal("queue_after_active"), + activeRunId: RunId, + }), + Schema.Struct({ + type: Schema.Literal("switch_provider"), + fromProviderInstanceId: ProviderInstanceId, + toModelSelection: ModelSelection, + }), +]); +export type MessageDispatchDecisionV2 = typeof MessageDispatchDecisionV2.Type; + +export const SteeringExecutionPolicyV2 = Schema.Literals(["active_steering", "interrupt_restart"]); +export type SteeringExecutionPolicyV2 = typeof SteeringExecutionPolicyV2.Type; + +export const ForkExecutionPolicyV2 = Schema.Literals(["native_fork", "portable_context"]); +export type ForkExecutionPolicyV2 = typeof ForkExecutionPolicyV2.Type; + +export const CommandPolicyCapability = Schema.Literals([ + "queued_messages", + "active_steering", + "interrupt", + "interrupt_restart_steering", + "native_fork", + "fork_from_turn", + "rollback", + "rollback_snapshot", + "context_handoff", + "strong_terminal_status", +]); +export type CommandPolicyCapability = typeof CommandPolicyCapability.Type; + +export class CommandPolicyMessageDispatchError extends Schema.TaggedErrorClass()( + "CommandPolicyMessageDispatchError", + { + commandId: CommandId, + threadId: ThreadId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to choose message dispatch policy for command ${this.commandId}.`; + } +} + +export class CommandPolicyUnsupportedError extends Schema.TaggedErrorClass()( + "CommandPolicyUnsupportedError", + { + commandId: CommandId, + threadId: ThreadId, + requestedMode: Schema.String, + providerInstanceId: ProviderInstanceId, + }, +) { + override get message(): string { + return `${this.providerInstanceId} cannot satisfy message dispatch mode ${this.requestedMode} for command ${this.commandId}.`; + } +} + +export class CommandPolicyCapabilityUnsupportedError extends Schema.TaggedErrorClass()( + "CommandPolicyCapabilityUnsupportedError", + { + commandId: CommandId, + threadId: ThreadId, + providerInstanceId: ProviderInstanceId, + capability: CommandPolicyCapability, + detail: Schema.String, + }, +) { + override get message(): string { + return `${this.providerInstanceId} cannot satisfy ${this.capability} for command ${this.commandId}: ${this.detail}`; + } +} + +export const CommandPolicyV2Error = Schema.Union([ + CommandPolicyMessageDispatchError, + CommandPolicyUnsupportedError, + CommandPolicyCapabilityUnsupportedError, +]); +export type CommandPolicyV2Error = typeof CommandPolicyV2Error.Type; + +interface CapabilityCheckInput { + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly providerInstanceId: ProviderInstanceId; + readonly capabilities: OrchestrationV2ProviderCapabilities; +} + +export interface CommandPolicyV2Shape { + readonly decideMessageDispatch: (input: { + readonly commandId: CommandId; + readonly projection: OrchestrationV2ThreadProjection; + readonly requestedModelSelection?: ModelSelection; + readonly requestedMode: + | { readonly type: "steer_active"; readonly targetRunId: RunId } + | { readonly type: "restart_active"; readonly targetRunId: RunId } + | { readonly type: "queue_after_active" } + | { readonly type: "start_immediately" }; + readonly capabilities: OrchestrationV2ProviderCapabilities; + }) => Effect.Effect; + readonly ensureQueuedMessages: ( + input: CapabilityCheckInput, + ) => Effect.Effect; + readonly decideSteeringExecution: ( + input: CapabilityCheckInput & { + readonly forceRestart?: boolean; + }, + ) => Effect.Effect; + readonly ensureInterrupt: ( + input: CapabilityCheckInput, + ) => Effect.Effect; + readonly ensureNativeFork: ( + input: CapabilityCheckInput & { + readonly fromSpecificTurn: boolean; + }, + ) => Effect.Effect; + readonly decideForkExecution: ( + input: CapabilityCheckInput & { + readonly sameProvider: boolean; + readonly hasStrongNativeSource: boolean; + readonly fromSpecificTurn: boolean; + }, + ) => Effect.Effect; + readonly ensureRollback: ( + input: CapabilityCheckInput, + ) => Effect.Effect; + readonly ensureContextHandoff: ( + input: CapabilityCheckInput & { + readonly strategy: "fork_delta_context" | "delta_context" | "full_thread_summary"; + }, + ) => Effect.Effect; +} + +export class CommandPolicyV2 extends Context.Service()( + "t3/orchestration-v2/CommandPolicy/CommandPolicyV2", +) {} + +function unsupported( + input: CapabilityCheckInput, + capability: CommandPolicyCapability, + detail: string, +) { + return new CommandPolicyCapabilityUnsupportedError({ + commandId: input.commandId, + threadId: input.threadId, + providerInstanceId: input.providerInstanceId, + capability, + detail, + }); +} + +const ensureQueuedMessages: CommandPolicyV2Shape["ensureQueuedMessages"] = (input) => + input.capabilities.turns.supportsQueuedMessages + ? Effect.void + : Effect.fail( + unsupported( + input, + "queued_messages", + "providerInstanceId does not support app-owned queued turns", + ), + ); + +const decideSteeringExecution: CommandPolicyV2Shape["decideSteeringExecution"] = (input) => { + if (!input.forceRestart && input.capabilities.turns.supportsActiveSteering) { + return Effect.succeed("active_steering"); + } + if ( + input.capabilities.turns.supportsInterrupt && + input.capabilities.turns.supportsSteeringByInterruptRestart + ) { + return Effect.succeed("interrupt_restart"); + } + return Effect.fail( + unsupported( + input, + input.capabilities.turns.supportsInterrupt ? "interrupt_restart_steering" : "active_steering", + "providerInstanceId cannot steer active turns directly or by interrupt-and-restart", + ), + ); +}; + +const ensureInterrupt: CommandPolicyV2Shape["ensureInterrupt"] = (input) => + input.capabilities.turns.supportsInterrupt + ? Effect.void + : Effect.fail( + unsupported(input, "interrupt", "providerInstanceId does not support turn interrupts"), + ); + +const ensureNativeFork: CommandPolicyV2Shape["ensureNativeFork"] = (input) => { + if (!input.capabilities.threads.canForkThread) { + return Effect.fail( + unsupported(input, "native_fork", "providerInstanceId does not support native thread forks"), + ); + } + if (input.fromSpecificTurn && !input.capabilities.threads.canForkFromTurn) { + return Effect.fail( + unsupported( + input, + "fork_from_turn", + "providerInstanceId cannot fork from a specific completed turn", + ), + ); + } + if (input.capabilities.identity.nativeThreadIds !== "strong") { + return Effect.fail( + unsupported( + input, + "native_fork", + "providerInstanceId does not expose strong native thread ids", + ), + ); + } + return Effect.void; +}; + +const ensureRollback: CommandPolicyV2Shape["ensureRollback"] = (input) => { + if ( + !input.capabilities.threads.canRollbackThread || + !input.capabilities.checkpointing.providerCanRollbackConversation + ) { + return Effect.fail( + unsupported(input, "rollback", "providerInstanceId conversation rollback is unavailable"), + ); + } + if (!input.capabilities.checkpointing.providerRollbackReturnsSnapshot) { + return Effect.fail( + unsupported( + input, + "rollback_snapshot", + "rollback must return a providerInstanceId thread snapshot", + ), + ); + } + return Effect.void; +}; + +const ensureContextHandoff: CommandPolicyV2Shape["ensureContextHandoff"] = (input) => { + if (!input.capabilities.context.canConsumeHandoffSummaries) { + return Effect.fail( + unsupported(input, "context_handoff", "providerInstanceId cannot consume handoff summaries"), + ); + } + if (!input.capabilities.context.acceptsSyntheticUserContext) { + return Effect.fail( + unsupported( + input, + "context_handoff", + "providerInstanceId cannot receive synthetic user context", + ), + ); + } + if ( + (input.strategy === "fork_delta_context" || input.strategy === "delta_context") && + !input.capabilities.context.supportsDeltaHandoff + ) { + return Effect.fail( + unsupported(input, "context_handoff", "providerInstanceId does not support delta handoff"), + ); + } + if ( + input.strategy === "full_thread_summary" && + !input.capabilities.context.supportsFullThreadHandoff + ) { + return Effect.fail( + unsupported( + input, + "context_handoff", + "providerInstanceId does not support full-thread handoff", + ), + ); + } + return Effect.void; +}; + +const decideForkExecution: CommandPolicyV2Shape["decideForkExecution"] = (input) => { + const canForkNatively = + input.sameProvider && + input.hasStrongNativeSource && + input.capabilities.threads.canForkThread && + (!input.fromSpecificTurn || input.capabilities.threads.canForkFromTurn) && + input.capabilities.identity.nativeThreadIds === "strong"; + + if (canForkNatively) { + return Effect.succeed("native_fork"); + } + + return ensureContextHandoff({ + commandId: input.commandId, + threadId: input.threadId, + providerInstanceId: input.providerInstanceId, + capabilities: input.capabilities, + strategy: "full_thread_summary", + }).pipe(Effect.as("portable_context")); +}; + +const decideMessageDispatch: CommandPolicyV2Shape["decideMessageDispatch"] = (input) => { + const activeRun = input.projection.runs.find( + (run) => run.status === "starting" || run.status === "running" || run.status === "waiting", + ); + const modelSelection = input.requestedModelSelection ?? input.projection.thread.modelSelection; + + switch (input.requestedMode.type) { + case "steer_active": { + if (activeRun?.id !== input.requestedMode.targetRunId) { + return Effect.fail( + new CommandPolicyUnsupportedError({ + commandId: input.commandId, + threadId: input.projection.thread.id, + requestedMode: input.requestedMode.type, + providerInstanceId: modelSelection.instanceId, + }), + ); + } + const providerTurnId = + activeRun.activeAttemptId === null + ? undefined + : input.projection.providerTurns.find( + (turn) => + turn.runAttemptId === activeRun.activeAttemptId && turn.status === "running", + )?.id; + return providerTurnId === undefined + ? Effect.fail( + new CommandPolicyMessageDispatchError({ + commandId: input.commandId, + threadId: input.projection.thread.id, + cause: `No running providerInstanceId turn found for active run ${activeRun.id}.`, + }), + ) + : Effect.succeed({ + type: "steer_active", + targetRunId: activeRun.id, + providerTurnId, + }); + } + case "restart_active": { + if (activeRun?.id !== input.requestedMode.targetRunId || activeRun.activeAttemptId === null) { + return Effect.fail( + new CommandPolicyUnsupportedError({ + commandId: input.commandId, + threadId: input.projection.thread.id, + requestedMode: input.requestedMode.type, + providerInstanceId: modelSelection.instanceId, + }), + ); + } + const providerTurnId = input.projection.providerTurns.find( + (turn) => turn.runAttemptId === activeRun.activeAttemptId && turn.status === "running", + )?.id; + return providerTurnId === undefined + ? Effect.fail( + new CommandPolicyMessageDispatchError({ + commandId: input.commandId, + threadId: input.projection.thread.id, + cause: `No running providerInstanceId turn found for active run ${activeRun.id}.`, + }), + ) + : Effect.succeed({ + type: "restart_active", + targetRunId: activeRun.id, + interruptProviderTurnId: providerTurnId, + }); + } + case "queue_after_active": + return activeRun === undefined + ? Effect.succeed({ type: "start_run", modelSelection }) + : ensureQueuedMessages({ + commandId: input.commandId, + threadId: input.projection.thread.id, + providerInstanceId: modelSelection.instanceId, + capabilities: input.capabilities, + }).pipe(Effect.as({ type: "queue_after_active", activeRunId: activeRun.id })); + case "start_immediately": + if (activeRun === undefined) { + return Effect.succeed({ type: "start_run", modelSelection }); + } + return ensureQueuedMessages({ + commandId: input.commandId, + threadId: input.projection.thread.id, + providerInstanceId: modelSelection.instanceId, + capabilities: input.capabilities, + }).pipe(Effect.as({ type: "queue_after_active", activeRunId: activeRun.id })); + } +}; + +export const layer: Layer.Layer = Layer.succeed(CommandPolicyV2, { + decideMessageDispatch, + ensureQueuedMessages, + decideSteeringExecution, + ensureInterrupt, + ensureNativeFork, + decideForkExecution, + ensureRollback, + ensureContextHandoff, +}); diff --git a/apps/server/src/orchestration-v2/CommandReceiptStore.ts b/apps/server/src/orchestration-v2/CommandReceiptStore.ts new file mode 100644 index 00000000000..5cb413027b7 --- /dev/null +++ b/apps/server/src/orchestration-v2/CommandReceiptStore.ts @@ -0,0 +1,167 @@ +import { CommandId, NonNegativeInt, ThreadId } from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +import { OrchestrationCommandReceiptRepositoryLive } from "../persistence/Layers/OrchestrationCommandReceipts.ts"; +import { + OrchestrationCommandReceiptRepository, + type OrchestrationCommandReceipt, +} from "../persistence/Services/OrchestrationCommandReceipts.ts"; + +/** + * ERRORS + */ +export class CommandReceiptStoreWriteError extends Schema.TaggedErrorClass()( + "CommandReceiptStoreWriteError", + { + commandId: CommandId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to write orchestration V2 command receipt ${this.commandId}.`; + } +} + +export class CommandReceiptStoreReadError extends Schema.TaggedErrorClass()( + "CommandReceiptStoreReadError", + { + commandId: CommandId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to read orchestration V2 command receipt ${this.commandId}.`; + } +} + +export const CommandReceiptStoreV2Error = Schema.Union([ + CommandReceiptStoreWriteError, + CommandReceiptStoreReadError, +]); +export type CommandReceiptStoreV2Error = typeof CommandReceiptStoreV2Error.Type; + +/** + * SERVICE DEFINITION + */ +export const CommandReceiptV2Status = Schema.Literals(["accepted", "rejected"]); +export type CommandReceiptV2Status = typeof CommandReceiptV2Status.Type; + +export const CommandReceiptV2 = Schema.Struct({ + commandId: CommandId, + threadId: ThreadId, + commandType: Schema.String, + acceptedAt: Schema.DateTimeUtc, + resultSequence: NonNegativeInt, + status: CommandReceiptV2Status, + error: Schema.NullOr(Schema.String), +}); +export type CommandReceiptV2 = typeof CommandReceiptV2.Type; + +export interface CommandReceiptStoreV2Shape { + readonly insertIfAbsent: ( + receipt: CommandReceiptV2, + ) => Effect.Effect; + readonly upsert: (receipt: CommandReceiptV2) => Effect.Effect; + readonly getByCommandId: ( + commandId: CommandId, + ) => Effect.Effect, CommandReceiptStoreV2Error>; +} + +export class CommandReceiptStoreV2 extends Context.Service< + CommandReceiptStoreV2, + CommandReceiptStoreV2Shape +>()("t3/orchestration-v2/CommandReceiptStore/CommandReceiptStoreV2") {} + +/** + * IMPLEMENTATIONS + */ +const decodeReceipt = Schema.decodeUnknownEffect( + CommandReceiptV2.mapFields((fields) => ({ + ...fields, + acceptedAt: Schema.DateTimeUtcFromString, + })), +); + +function fromApplicationReceipt(receipt: OrchestrationCommandReceipt) { + return decodeReceipt({ + commandId: receipt.commandId, + threadId: receipt.aggregateId, + commandType: receipt.commandType, + acceptedAt: receipt.acceptedAt, + resultSequence: receipt.resultSequence, + status: receipt.status, + error: receipt.error, + }); +} + +function toApplicationReceipt(receipt: CommandReceiptV2): OrchestrationCommandReceipt { + return { + commandId: receipt.commandId, + aggregateKind: "thread", + aggregateId: receipt.threadId, + commandType: receipt.commandType, + acceptedAt: DateTime.formatIso(receipt.acceptedAt), + resultSequence: receipt.resultSequence, + status: receipt.status, + error: receipt.error, + }; +} + +const baseLayer: Layer.Layer = + Layer.effect( + CommandReceiptStoreV2, + Effect.gen(function* () { + const receipts = yield* OrchestrationCommandReceiptRepository; + + return CommandReceiptStoreV2.of({ + insertIfAbsent: (receipt) => + receipts.insertIfAbsent(toApplicationReceipt(receipt)).pipe( + Effect.mapError( + (cause) => + new CommandReceiptStoreWriteError({ + commandId: receipt.commandId, + cause, + }), + ), + ), + upsert: (receipt) => + receipts.upsert(toApplicationReceipt(receipt)).pipe( + Effect.mapError( + (cause) => + new CommandReceiptStoreWriteError({ + commandId: receipt.commandId, + cause, + }), + ), + ), + getByCommandId: (commandId) => + receipts.getByCommandId({ commandId }).pipe( + Effect.flatMap( + Option.match({ + onNone: () => Effect.succeed(Option.none()), + onSome: (receipt) => + receipt.aggregateKind !== "thread" + ? Effect.succeed(Option.none()) + : fromApplicationReceipt(receipt).pipe(Effect.map(Option.some)), + }), + ), + Effect.mapError( + (cause) => + new CommandReceiptStoreReadError({ + commandId, + cause, + }), + ), + ), + } satisfies CommandReceiptStoreV2Shape); + }), + ); + +export const layer = baseLayer.pipe(Layer.provide(OrchestrationCommandReceiptRepositoryLive)); + +export const layerFromApplicationReceipts = baseLayer; diff --git a/apps/server/src/orchestration-v2/ContextHandoffService.ts b/apps/server/src/orchestration-v2/ContextHandoffService.ts new file mode 100644 index 00000000000..2f644fcf79d --- /dev/null +++ b/apps/server/src/orchestration-v2/ContextHandoffService.ts @@ -0,0 +1,343 @@ +import { + OrchestrationV2ContextHandoff, + type OrchestrationV2TurnItem, + ProviderInstanceId, + ProviderThreadId, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import { IdAllocatorV2 } from "./IdAllocator.ts"; + +export class ContextHandoffPrepareError extends Schema.TaggedErrorClass()( + "ContextHandoffPrepareError", + { + threadId: ThreadId, + targetRunId: RunId, + fromProviderThreadIds: Schema.Array(ProviderThreadId), + toProviderThreadId: ProviderThreadId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to prepare context handoff for run ${this.targetRunId} in thread ${this.threadId}.`; + } +} + +export const ContextHandoffServiceV2Error = Schema.Union([ContextHandoffPrepareError]); +export type ContextHandoffServiceV2Error = typeof ContextHandoffServiceV2Error.Type; + +export interface ContextHandoffServiceV2Shape { + readonly prepare: (input: { + readonly threadId: ThreadId; + readonly targetRunId: RunId; + readonly fromProviderThreadIds: ReadonlyArray; + readonly toProviderThreadId: ProviderThreadId; + }) => Effect.Effect; + readonly prepareForkDelta: (input: { + readonly sourceThreadId: ThreadId; + readonly targetThreadId: ThreadId; + readonly targetRunId: RunId; + readonly transferId: OrchestrationV2ContextHandoff["transferId"]; + readonly fromProviderThreadIds: ReadonlyArray; + readonly toProviderThreadId: ProviderThreadId; + readonly fromProviderInstanceId: ProviderInstanceId; + readonly toProviderInstanceId: ProviderInstanceId; + readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"]; + readonly deltaItems: ReadonlyArray; + readonly createdAt: DateTime.Utc; + }) => Effect.Effect; + readonly prepareProviderHandoff: (input: { + readonly threadId: ThreadId; + readonly targetRunId: RunId; + readonly transferId: NonNullable; + readonly fromProviderThreadIds: ReadonlyArray; + readonly toProviderThreadId: ProviderThreadId; + readonly fromProviderInstanceId: ProviderInstanceId; + readonly toProviderInstanceId: ProviderInstanceId; + readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"]; + readonly strategy: Extract< + OrchestrationV2ContextHandoff["strategy"], + "delta_since_target_last_seen" | "full_thread_summary" + >; + readonly items: ReadonlyArray; + readonly createdAt: DateTime.Utc; + }) => Effect.Effect; +} + +export class ContextHandoffServiceV2 extends Context.Service< + ContextHandoffServiceV2, + ContextHandoffServiceV2Shape +>()("t3/orchestration-v2/ContextHandoffService/ContextHandoffServiceV2") {} + +function compactText(text: string, maxLength = 240): string { + const compacted = text.replace(/\s+/g, " ").trim(); + if (compacted.length <= maxLength) { + return compacted; + } + return `${compacted.slice(0, maxLength - 3)}...`; +} + +function summarizeDeltaItem(item: OrchestrationV2TurnItem): string | null { + switch (item.type) { + case "user_message": + return `- User: ${compactText(item.text)}`; + case "assistant_message": + return `- Assistant: ${compactText(item.text)}`; + case "command_execution": + return `- Command: ${compactText(item.input)}`; + case "file_change": + return `- File change: ${item.fileName}`; + case "checkpoint": + return `- Checkpoint: ${item.files.length} files`; + case "handoff": + return `- Handoff: ${compactText(item.summary ?? item.strategy)}`; + default: + return null; + } +} + +function makeForkDeltaSummary(input: { + readonly sourceThreadId: ThreadId; + readonly targetThreadId: ThreadId; + readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"]; + readonly deltaItems: ReadonlyArray; +}): string { + const itemLines = input.deltaItems.flatMap((item) => { + const line = summarizeDeltaItem(item); + return line === null ? [] : [line]; + }); + return [ + "Merge-back context from forked conversation.", + `Source thread: ${input.sourceThreadId}`, + `Target thread: ${input.targetThreadId}`, + `Covered fork runs: ${input.coveredRunOrdinals.from}-${input.coveredRunOrdinals.to}`, + "", + "Fork delta:", + ...(itemLines.length === 0 ? ["- No user-visible delta items."] : itemLines), + ].join("\n"); +} + +function makeProviderHandoffSummary(input: { + readonly fromProviderInstanceId: ProviderInstanceId; + readonly toProviderInstanceId: ProviderInstanceId; + readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"]; + readonly strategy: Extract< + OrchestrationV2ContextHandoff["strategy"], + "delta_since_target_last_seen" | "full_thread_summary" + >; + readonly items: ReadonlyArray; +}): string { + const itemLines = input.items.flatMap((item) => { + if (item.type === "handoff") { + return []; + } + const line = summarizeDeltaItem(item); + return line === null ? [] : [line]; + }); + return [ + input.strategy === "full_thread_summary" + ? "Full conversation context for provider handoff." + : "Conversation delta since this provider last participated.", + `From driver: ${input.fromProviderInstanceId}`, + `To driver: ${input.toProviderInstanceId}`, + `Covered app runs: ${input.coveredRunOrdinals.from}-${input.coveredRunOrdinals.to}`, + "", + "Canonical conversation context:", + ...(itemLines.length === 0 ? ["- No user-visible context items."] : itemLines), + ].join("\n"); +} + +export function providerMessageWithContextHandoff(input: { + readonly handoff: OrchestrationV2ContextHandoff; + readonly userText: string; +}): string { + return providerMessageWithContextHandoffs({ + handoffs: [input.handoff], + userText: input.userText, + }); +} + +export function providerMessageWithContextHandoffs(input: { + readonly handoffs: ReadonlyArray; + readonly userText: string; +}): string { + const handoffSections = input.handoffs.flatMap((handoff) => { + const label = + handoff.strategy === "fork_delta_summary" + ? "merge_back / fork_delta_summary" + : handoff.strategy; + return [`Context handoff (${label}):`, handoff.summaryText, ""]; + }); + return [...handoffSections, "User message:", input.userText].join("\n"); +} + +const makeContextHandoffService = Effect.fn("orchestrationV2.ContextHandoffService.layer")( + function* () { + const idAllocator = yield* IdAllocatorV2; + + const prepare = Effect.fn("orchestrationV2.contextHandoff.prepare")(function* (input: { + readonly threadId: ThreadId; + readonly targetRunId: RunId; + readonly fromProviderThreadIds: ReadonlyArray; + readonly toProviderThreadId: ProviderThreadId; + }) { + const now = yield* DateTime.now; + const handoffId = yield* idAllocator.allocate + .contextHandoff({ + threadId: input.threadId, + fromProviderInstanceId: ProviderInstanceId.make("manual"), + toProviderInstanceId: ProviderInstanceId.make("manual"), + }) + .pipe( + Effect.mapError( + (cause) => + new ContextHandoffPrepareError({ + threadId: input.threadId, + targetRunId: input.targetRunId, + fromProviderThreadIds: Array.from(input.fromProviderThreadIds), + toProviderThreadId: input.toProviderThreadId, + cause, + }), + ), + ); + return { + id: handoffId, + transferId: null, + threadId: input.threadId, + targetRunId: input.targetRunId, + fromProviderThreadIds: Array.from(input.fromProviderThreadIds), + toProviderThreadId: input.toProviderThreadId, + coveredRunOrdinals: { from: 1, to: 1 }, + strategy: "manual_context", + status: "ready", + summaryMessageId: null, + summaryText: "Manual context handoff.", + createdByProviderInstanceId: null, + createdAt: now, + updatedAt: now, + } satisfies OrchestrationV2ContextHandoff; + }); + + const prepareForkDelta = Effect.fn("orchestrationV2.contextHandoff.prepareForkDelta")( + function* (input: { + readonly sourceThreadId: ThreadId; + readonly targetThreadId: ThreadId; + readonly targetRunId: RunId; + readonly transferId: OrchestrationV2ContextHandoff["transferId"]; + readonly fromProviderThreadIds: ReadonlyArray; + readonly toProviderThreadId: ProviderThreadId; + readonly fromProviderInstanceId: ProviderInstanceId; + readonly toProviderInstanceId: ProviderInstanceId; + readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"]; + readonly deltaItems: ReadonlyArray; + readonly createdAt: DateTime.Utc; + }) { + const handoffId = yield* idAllocator.allocate + .contextHandoff({ + threadId: input.targetThreadId, + fromProviderInstanceId: input.fromProviderInstanceId, + toProviderInstanceId: input.toProviderInstanceId, + }) + .pipe( + Effect.mapError( + (cause) => + new ContextHandoffPrepareError({ + threadId: input.targetThreadId, + targetRunId: input.targetRunId, + fromProviderThreadIds: Array.from(input.fromProviderThreadIds), + toProviderThreadId: input.toProviderThreadId, + cause, + }), + ), + ); + return { + id: handoffId, + transferId: input.transferId, + threadId: input.targetThreadId, + targetRunId: input.targetRunId, + fromProviderThreadIds: Array.from(input.fromProviderThreadIds), + toProviderThreadId: input.toProviderThreadId, + coveredRunOrdinals: input.coveredRunOrdinals, + strategy: "fork_delta_summary", + status: "ready", + summaryMessageId: null, + summaryText: makeForkDeltaSummary(input), + createdByProviderInstanceId: null, + createdAt: input.createdAt, + updatedAt: input.createdAt, + } satisfies OrchestrationV2ContextHandoff; + }, + ); + + const prepareProviderHandoff = Effect.fn( + "orchestrationV2.contextHandoff.prepareProviderHandoff", + )(function* (input: { + readonly threadId: ThreadId; + readonly targetRunId: RunId; + readonly transferId: NonNullable; + readonly fromProviderThreadIds: ReadonlyArray; + readonly toProviderThreadId: ProviderThreadId; + readonly fromProviderInstanceId: ProviderInstanceId; + readonly toProviderInstanceId: ProviderInstanceId; + readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"]; + readonly strategy: Extract< + OrchestrationV2ContextHandoff["strategy"], + "delta_since_target_last_seen" | "full_thread_summary" + >; + readonly items: ReadonlyArray; + readonly createdAt: DateTime.Utc; + }) { + const handoffId = yield* idAllocator.allocate + .contextHandoff({ + threadId: input.threadId, + fromProviderInstanceId: input.fromProviderInstanceId, + toProviderInstanceId: input.toProviderInstanceId, + }) + .pipe( + Effect.mapError( + (cause) => + new ContextHandoffPrepareError({ + threadId: input.threadId, + targetRunId: input.targetRunId, + fromProviderThreadIds: Array.from(input.fromProviderThreadIds), + toProviderThreadId: input.toProviderThreadId, + cause, + }), + ), + ); + return { + id: handoffId, + transferId: input.transferId, + threadId: input.threadId, + targetRunId: input.targetRunId, + fromProviderThreadIds: Array.from(input.fromProviderThreadIds), + toProviderThreadId: input.toProviderThreadId, + coveredRunOrdinals: input.coveredRunOrdinals, + strategy: input.strategy, + status: "ready", + summaryMessageId: null, + summaryText: makeProviderHandoffSummary(input), + createdByProviderInstanceId: null, + createdAt: input.createdAt, + updatedAt: input.createdAt, + } satisfies OrchestrationV2ContextHandoff; + }); + + return ContextHandoffServiceV2.of({ + prepare, + prepareForkDelta, + prepareProviderHandoff, + }); + }, +); + +export const layer: Layer.Layer = Layer.effect( + ContextHandoffServiceV2, + makeContextHandoffService(), +); diff --git a/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts b/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts new file mode 100644 index 00000000000..56abf680ddf --- /dev/null +++ b/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts @@ -0,0 +1,191 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import { + CommandId, + MessageId, + ProjectId, + ThreadId, + type OrchestrationV2ThreadProjection, +} from "@t3tools/contracts"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { FetchHttpClient } from "effect/unstable/http"; +import { describe } from "vite-plus/test"; + +import * as CheckpointStore from "../checkpointing/CheckpointStore.ts"; +import { ServerConfig } from "../config.ts"; +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import { ProviderInstanceRegistryHydrationLive } from "../provider/Layers/ProviderInstanceRegistryHydration.ts"; +import { + NoOpProviderEventLoggers, + ProviderEventLoggers, +} from "../provider/Layers/ProviderEventLoggers.ts"; +import { OpenCodeRuntimeLive } from "../provider/opencodeRuntime.ts"; +import { ServerSettingsService } from "../serverSettings.ts"; +import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; +import * as VcsProcess from "../vcs/VcsProcess.ts"; +import { OrchestratorV2 } from "./Orchestrator.ts"; +import { OrchestrationV2LayerLive } from "./runtimeLayer.ts"; +import { layer as mcpSessionRegistryTestLayer } from "../mcp/McpSessionRegistry.testkit.ts"; +import { CURSOR_MODEL_SELECTION } from "./testkit/fixtures/shared.ts"; + +const serverConfigLayer = ServerConfig.layerTest(process.cwd(), { + prefix: "t3-cursor-v2-live-", +}); + +const vcsDriverRegistryLayer = VcsDriverRegistry.layer.pipe( + Layer.provide(VcsProcess.layer), + Layer.provide(serverConfigLayer), + Layer.provide(NodeServices.layer), +); + +const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(vcsDriverRegistryLayer)); + +const serverSettingsLayer = ServerSettingsService.layerTest({ + providers: { + cursor: { enabled: true }, + }, +}); +const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe( + Layer.provide( + Layer.mergeAll( + serverConfigLayer.pipe(Layer.provide(NodeServices.layer)), + serverSettingsLayer, + NodeServices.layer, + FetchHttpClient.layer, + OpenCodeRuntimeLive.pipe(Layer.provide(NodeServices.layer)), + Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers), + ), + ), +); + +const liveLayer = OrchestrationV2LayerLive.pipe( + Layer.provide(mcpSessionRegistryTestLayer), + Layer.provide(SqlitePersistenceMemory), + Layer.provide(checkpointStoreLayer), + Layer.provide(serverConfigLayer), + Layer.provide(serverSettingsLayer), + Layer.provide(providerInstanceRegistryLayer), + Layer.provide(NodeServices.layer), +); + +const waitForIdle = Effect.fn("CursorOrchestratorV2Live.waitForIdle")(function* ( + threadId: ThreadId, +) { + const orchestrator = yield* OrchestratorV2; + for (let attempt = 0; attempt < 600; attempt += 1) { + const projection = yield* orchestrator.getThreadProjection(threadId); + if ( + projection.runs.length > 0 && + projection.runs.every( + (run) => !["queued", "starting", "running", "waiting"].includes(run.status), + ) + ) { + return projection; + } + yield* Effect.sleep("500 millis"); + } + return yield* Effect.die(new Error(`Timed out waiting for Cursor thread ${threadId}.`)); +}); + +describe.runIf(process.env.T3_CURSOR_LIVE_ORCHESTRATOR === "1")( + "Cursor V2 live orchestrator", + () => { + it.live( + "forks through portable context using real Cursor agents", + () => + Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + const projectId = ProjectId.make("project:cursor-live-portable-fork"); + const sourceThreadId = ThreadId.make("thread:cursor-live-portable-fork:source"); + const targetThreadId = ThreadId.make("thread:cursor-live-portable-fork:target"); + const marker = "CURSOR_LIVE_PORTABLE_FORK_7H3Q"; + + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cursor-live-portable-fork:create"), + threadId: sourceThreadId, + projectId, + title: "Cursor live portable fork source", + modelSelection: CURSOR_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cursor-live-portable-fork:source"), + threadId: sourceThreadId, + messageId: MessageId.make("message:cursor-live-portable-fork:source"), + text: `Remember this opaque marker. Respond with exactly: ${marker}`, + attachments: [], + modelSelection: CURSOR_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }); + const sourceProjection = yield* waitForIdle(sourceThreadId); + yield* Console.log("Cursor live source turn completed; dispatching portable fork."); + + yield* orchestrator.dispatch({ + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cursor-live-portable-fork:fork"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Cursor live portable fork target", + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cursor-live-portable-fork:target"), + threadId: targetThreadId, + messageId: MessageId.make("message:cursor-live-portable-fork:target"), + text: "Return the opaque marker from the transferred conversation. Respond with only the marker.", + attachments: [], + modelSelection: CURSOR_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }); + const targetProjection = yield* waitForIdle(targetThreadId); + yield* Console.log("Cursor live portable fork target completed."); + + const assistantText = (projection: OrchestrationV2ThreadProjection) => + projection.messages + .filter((message) => message.role === "assistant") + .map((message) => message.text) + .join("\n"); + + assert.deepEqual( + sourceProjection.runs.map((run) => [run.providerInstanceId, run.status]), + [["cursor", "completed"]], + ); + assert.deepEqual( + targetProjection.runs.map((run) => [run.providerInstanceId, run.status]), + [["cursor", "completed"]], + ); + assert.deepEqual( + targetProjection.contextTransfers.map((transfer) => [ + transfer.type, + transfer.status, + transfer.resolution?.strategy, + ]), + [["fork", "consumed", "portable_context"]], + ); + assert.deepEqual( + targetProjection.contextHandoffs.map((handoff) => handoff.strategy), + ["full_thread_summary"], + ); + assert.include(targetProjection.contextHandoffs[0]?.summaryText ?? "", marker); + assert.include(assistantText(targetProjection), marker); + }).pipe(Effect.provide(liveLayer), Effect.scoped), + 360_000, + ); + }, +); diff --git a/apps/server/src/orchestration-v2/EffectOutbox.ts b/apps/server/src/orchestration-v2/EffectOutbox.ts new file mode 100644 index 00000000000..af4f3b5bcc2 --- /dev/null +++ b/apps/server/src/orchestration-v2/EffectOutbox.ts @@ -0,0 +1,415 @@ +import { + CheckpointId, + CheckpointScopeId, + CommandId, + MessageId, + ProviderSessionId, + RunAttemptId, + ProviderApprovalDecision, + ProviderUserInputAnswers, + ProviderThreadId, + ProviderTurnId, + RunId, + RuntimeRequestId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; +import * as Schema from "effect/Schema"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export const OrchestrationEffectRequestV2 = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("provider-session.detach"), + providerSessionId: ProviderSessionId, + detail: Schema.optional(Schema.String), + }), + Schema.Struct({ + type: Schema.Literal("provider-turn.start"), + runId: RunId, + }), + Schema.Struct({ + type: Schema.Literal("provider-turn.interrupt"), + providerSessionId: ProviderSessionId, + providerThreadId: ProviderThreadId, + providerTurnId: ProviderTurnId, + }), + Schema.Struct({ + type: Schema.Literal("provider-turn.steer"), + providerSessionId: ProviderSessionId, + providerThreadId: ProviderThreadId, + providerTurnId: ProviderTurnId, + messageId: MessageId, + }), + Schema.Struct({ + type: Schema.Literal("provider-turn.restart"), + providerSessionId: ProviderSessionId, + providerThreadId: ProviderThreadId, + providerTurnId: ProviderTurnId, + interruptedAttemptId: RunAttemptId, + runId: RunId, + }), + Schema.Struct({ + type: Schema.Literal("runtime-request.respond"), + providerSessionId: ProviderSessionId, + requestId: RuntimeRequestId, + decision: Schema.optional(ProviderApprovalDecision), + answers: Schema.optional(ProviderUserInputAnswers), + }), + Schema.Struct({ + type: Schema.Literal("provider-thread.rollback"), + providerThreadId: ProviderThreadId, + checkpointId: CheckpointId, + scopeId: CheckpointScopeId, + }), + Schema.Struct({ + type: Schema.Literal("checkpoint.capture"), + runId: RunId, + scopeId: CheckpointScopeId, + }), + Schema.Struct({ + type: Schema.Literal("terminal.cleanup"), + }), + Schema.Struct({ + type: Schema.Literal("attachment.cleanup"), + attachmentIds: Schema.Array(Schema.String), + }), +]); +export type OrchestrationEffectRequestV2 = typeof OrchestrationEffectRequestV2.Type; + +export const OrchestrationEffectStatusV2 = Schema.Literals([ + "pending", + "running", + "succeeded", + "failed", +]); +export type OrchestrationEffectStatusV2 = typeof OrchestrationEffectStatusV2.Type; + +export interface OrchestrationEffectV2 { + readonly id: string; + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly request: OrchestrationEffectRequestV2; + readonly status: OrchestrationEffectStatusV2; + readonly attemptCount: number; + readonly availableAt: string; + readonly leaseOwner: string | null; + readonly leaseExpiresAt: string | null; + readonly createdAt: string; + readonly updatedAt: string; + readonly completedAt: string | null; + readonly lastError: string | null; +} + +export interface PendingOrchestrationEffectV2 { + readonly id: string; + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly request: OrchestrationEffectRequestV2; + readonly availableAt?: DateTime.Utc; +} + +export class EffectOutboxError extends Schema.TaggedErrorClass()( + "EffectOutboxError", + { + operation: Schema.String, + effectId: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Orchestration effect outbox ${this.operation} failed${this.effectId === undefined ? "" : ` for ${this.effectId}`}.`; + } +} + +export interface EffectOutboxV2Shape { + readonly awaitAvailable: Effect.Effect; + readonly notifyAvailable: Effect.Effect; + readonly enqueue: ( + effects: ReadonlyArray, + ) => Effect.Effect; + readonly get: ( + effectId: string, + ) => Effect.Effect, EffectOutboxError>; + readonly listByCommandId: ( + commandId: CommandId, + ) => Effect.Effect, EffectOutboxError>; + readonly reclaimRunning: Effect.Effect; + readonly claimNext: (input: { + readonly workerId: string; + readonly leaseDurationMs: number; + }) => Effect.Effect, EffectOutboxError>; + readonly succeed: (input: { + readonly effectId: string; + readonly workerId: string; + }) => Effect.Effect; + readonly retry: (input: { + readonly effectId: string; + readonly workerId: string; + readonly error: string; + readonly delayMs: number; + }) => Effect.Effect; + readonly fail: (input: { + readonly effectId: string; + readonly workerId: string; + readonly error: string; + }) => Effect.Effect; +} + +export class EffectOutboxV2 extends Context.Service()( + "t3/orchestration-v2/EffectOutbox/EffectOutboxV2", +) {} + +type EffectRow = { + readonly effect_id: string; + readonly command_id: string; + readonly thread_id: string; + readonly effect_type: string; + readonly payload_json: string; + readonly status: string; + readonly attempt_count: number; + readonly available_at: string; + readonly lease_owner: string | null; + readonly lease_expires_at: string | null; + readonly created_at: string; + readonly updated_at: string; + readonly completed_at: string | null; + readonly last_error: string | null; +}; + +const encodeRequest = Schema.encodeSync(Schema.fromJsonString(OrchestrationEffectRequestV2)); +const decodeRequest = Schema.decodeUnknownEffect( + Schema.fromJsonString(OrchestrationEffectRequestV2), +); + +const rowToEffect = (row: EffectRow) => + decodeRequest(row.payload_json).pipe( + Effect.map( + (request): OrchestrationEffectV2 => ({ + id: row.effect_id, + commandId: CommandId.make(row.command_id), + threadId: ThreadId.make(row.thread_id), + request, + status: row.status as OrchestrationEffectStatusV2, + attemptCount: row.attempt_count, + availableAt: row.available_at, + leaseOwner: row.lease_owner, + leaseExpiresAt: row.lease_expires_at, + createdAt: row.created_at, + updatedAt: row.updated_at, + completedAt: row.completed_at, + lastError: row.last_error, + }), + ), + ); + +export const layer: Layer.Layer = Layer.effect( + EffectOutboxV2, + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const available = yield* Queue.unbounded(); + + const decodeRows = (operation: string, rows: ReadonlyArray) => + Effect.forEach(rows, rowToEffect).pipe( + Effect.mapError((cause) => new EffectOutboxError({ operation, cause })), + ); + + const service: EffectOutboxV2Shape = { + enqueue: (effects) => + Effect.gen(function* () { + const now = yield* DateTime.now; + const nowIso = DateTime.formatIso(now); + yield* Effect.forEach( + effects, + (effect) => sql` + INSERT INTO orchestration_v2_effect_outbox ( + effect_id, + command_id, + thread_id, + effect_type, + payload_json, + status, + attempt_count, + available_at, + created_at, + updated_at + ) + VALUES ( + ${effect.id}, + ${effect.commandId}, + ${effect.threadId}, + ${effect.request.type}, + ${encodeRequest(effect.request)}, + 'pending', + 0, + ${DateTime.formatIso(effect.availableAt ?? now)}, + ${nowIso}, + ${nowIso} + ) + ON CONFLICT(effect_id) DO NOTHING + `, + { concurrency: 1, discard: true }, + ); + if (effects.length > 0) { + yield* Queue.offer(available, undefined); + } + }).pipe(Effect.mapError((cause) => new EffectOutboxError({ operation: "enqueue", cause }))), + get: (effectId) => + sql` + SELECT * + FROM orchestration_v2_effect_outbox + WHERE effect_id = ${effectId} + LIMIT 1 + `.pipe( + Effect.flatMap((rows) => { + const row = rows[0]; + return row === undefined + ? Effect.succeed(Option.none()) + : rowToEffect(row).pipe(Effect.map(Option.some)); + }), + Effect.mapError((cause) => new EffectOutboxError({ operation: "get", effectId, cause })), + ), + awaitAvailable: Queue.take(available), + notifyAvailable: Queue.offer(available, undefined).pipe(Effect.asVoid), + listByCommandId: (commandId) => + sql` + SELECT * + FROM orchestration_v2_effect_outbox + WHERE command_id = ${commandId} + ORDER BY created_at ASC, effect_id ASC + `.pipe( + Effect.flatMap((rows) => decodeRows("list", rows)), + Effect.mapError((cause) => + Schema.is(EffectOutboxError)(cause) + ? cause + : new EffectOutboxError({ operation: "list", cause }), + ), + ), + reclaimRunning: Effect.gen(function* () { + const now = DateTime.formatIso(yield* DateTime.now); + const rows = yield* sql<{ readonly effect_id: string }>` + UPDATE orchestration_v2_effect_outbox + SET + status = 'pending', + lease_owner = NULL, + lease_expires_at = NULL, + available_at = ${now}, + updated_at = ${now} + WHERE status = 'running' + RETURNING effect_id + `; + if (rows.length > 0) yield* Queue.offer(available, undefined); + return rows.length; + }).pipe(Effect.mapError((cause) => new EffectOutboxError({ operation: "reclaim", cause }))), + claimNext: ({ workerId, leaseDurationMs }) => + Effect.gen(function* () { + const now = yield* DateTime.now; + const nowIso = DateTime.formatIso(now); + const leaseExpiresAt = DateTime.formatIso( + DateTime.add(now, { milliseconds: Math.max(1, leaseDurationMs) }), + ); + const rows = yield* sql` + UPDATE orchestration_v2_effect_outbox + SET + status = 'running', + attempt_count = attempt_count + 1, + lease_owner = ${workerId}, + lease_expires_at = ${leaseExpiresAt}, + updated_at = ${nowIso}, + last_error = NULL + WHERE effect_id = ( + SELECT effect_id + FROM orchestration_v2_effect_outbox + WHERE available_at <= ${nowIso} + AND ( + status = 'pending' + OR (status = 'running' AND lease_expires_at <= ${nowIso}) + ) + ORDER BY available_at ASC, created_at ASC, effect_id ASC + LIMIT 1 + ) + RETURNING * + `; + const row = rows[0]; + return row === undefined ? Option.none() : Option.some(yield* rowToEffect(row)); + }).pipe(Effect.mapError((cause) => new EffectOutboxError({ operation: "claim", cause }))), + succeed: ({ effectId, workerId }) => + Effect.gen(function* () { + const now = DateTime.formatIso(yield* DateTime.now); + const rows = yield* sql<{ readonly effect_id: string }>` + UPDATE orchestration_v2_effect_outbox + SET + status = 'succeeded', + lease_owner = NULL, + lease_expires_at = NULL, + completed_at = ${now}, + updated_at = ${now}, + last_error = NULL + WHERE effect_id = ${effectId} + AND status = 'running' + AND lease_owner = ${workerId} + RETURNING effect_id + `; + return rows.length === 1; + }).pipe( + Effect.mapError( + (cause) => new EffectOutboxError({ operation: "succeed", effectId, cause }), + ), + ), + retry: ({ effectId, workerId, error, delayMs }) => + Effect.gen(function* () { + const now = yield* DateTime.now; + const nowIso = DateTime.formatIso(now); + const availableAt = DateTime.formatIso( + DateTime.add(now, { milliseconds: Math.max(0, delayMs) }), + ); + const rows = yield* sql<{ readonly effect_id: string }>` + UPDATE orchestration_v2_effect_outbox + SET + status = 'pending', + available_at = ${availableAt}, + lease_owner = NULL, + lease_expires_at = NULL, + updated_at = ${nowIso}, + last_error = ${error} + WHERE effect_id = ${effectId} + AND status = 'running' + AND lease_owner = ${workerId} + RETURNING effect_id + `; + return rows.length === 1; + }).pipe( + Effect.mapError( + (cause) => new EffectOutboxError({ operation: "retry", effectId, cause }), + ), + ), + fail: ({ effectId, workerId, error }) => + Effect.gen(function* () { + const now = DateTime.formatIso(yield* DateTime.now); + const rows = yield* sql<{ readonly effect_id: string }>` + UPDATE orchestration_v2_effect_outbox + SET + status = 'failed', + lease_owner = NULL, + lease_expires_at = NULL, + completed_at = ${now}, + updated_at = ${now}, + last_error = ${error} + WHERE effect_id = ${effectId} + AND status = 'running' + AND lease_owner = ${workerId} + RETURNING effect_id + `; + return rows.length === 1; + }).pipe( + Effect.mapError((cause) => new EffectOutboxError({ operation: "fail", effectId, cause })), + ), + }; + + return service; + }), +); diff --git a/apps/server/src/orchestration-v2/EffectWorker.ts b/apps/server/src/orchestration-v2/EffectWorker.ts new file mode 100644 index 00000000000..86d092d7fa3 --- /dev/null +++ b/apps/server/src/orchestration-v2/EffectWorker.ts @@ -0,0 +1,366 @@ +import * as Cause from "effect/Cause"; +import * as Context from "effect/Context"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +import { RunFinalizationService } from "./RunFinalizationService.ts"; +import { ResourceCleanupService } from "./ResourceCleanupService.ts"; +import { EffectOutboxV2, type OrchestrationEffectV2 } from "./EffectOutbox.ts"; +import { CheckpointRollbackServiceV2 } from "./CheckpointRollbackService.ts"; +import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts"; +import { ProviderTurnControlServiceV2 } from "./ProviderTurnControlService.ts"; +import { ProviderTurnStartServiceV2 } from "./ProviderTurnStartService.ts"; +import { RuntimeRequestServiceV2 } from "./RuntimeRequestService.ts"; + +export class OrchestrationEffectExecutionError extends Schema.TaggedErrorClass()( + "OrchestrationEffectExecutionError", + { + effectId: Schema.String, + effectType: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface OrchestrationEffectExecutorV2Shape { + readonly execute: ( + effect: OrchestrationEffectV2, + ) => Effect.Effect; +} + +export class OrchestrationEffectExecutorV2 extends Context.Service< + OrchestrationEffectExecutorV2, + OrchestrationEffectExecutorV2Shape +>()("t3/orchestration-v2/EffectWorker/OrchestrationEffectExecutorV2") {} + +export const executorLayer: Layer.Layer< + OrchestrationEffectExecutorV2, + never, + | ProviderSessionManagerV2 + | RunFinalizationService + | CheckpointRollbackServiceV2 + | ProviderTurnControlServiceV2 + | ProviderTurnStartServiceV2 + | RuntimeRequestServiceV2 +> = Layer.effect( + OrchestrationEffectExecutorV2, + Effect.gen(function* () { + const runFinalization = yield* RunFinalizationService; + const resourceCleanup = yield* ResourceCleanupService; + const checkpointRollback = yield* CheckpointRollbackServiceV2; + const providerSessions = yield* ProviderSessionManagerV2; + const providerTurnControl = yield* ProviderTurnControlServiceV2; + const providerTurnStart = yield* ProviderTurnStartServiceV2; + const runtimeRequests = yield* RuntimeRequestServiceV2; + return OrchestrationEffectExecutorV2.of({ + execute: (effect) => { + switch (effect.request.type) { + case "provider-session.detach": + return providerSessions + .detach({ + providerSessionId: effect.request.providerSessionId, + threadId: effect.threadId, + ...(effect.request.detail === undefined ? {} : { detail: effect.request.detail }), + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + case "provider-turn.start": + return providerTurnStart + .start({ threadId: effect.threadId, runId: effect.request.runId }) + .pipe( + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + case "provider-turn.interrupt": + return providerTurnControl + .interrupt({ + threadId: effect.threadId, + providerSessionId: effect.request.providerSessionId, + providerThreadId: effect.request.providerThreadId, + providerTurnId: effect.request.providerTurnId, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + case "provider-turn.steer": + return providerTurnControl + .steer({ + threadId: effect.threadId, + providerSessionId: effect.request.providerSessionId, + providerThreadId: effect.request.providerThreadId, + providerTurnId: effect.request.providerTurnId, + messageId: effect.request.messageId, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + case "provider-turn.restart": + return providerTurnControl + .interruptAndAwaitTerminal({ + threadId: effect.threadId, + providerSessionId: effect.request.providerSessionId, + providerThreadId: effect.request.providerThreadId, + providerTurnId: effect.request.providerTurnId, + interruptedAttemptId: effect.request.interruptedAttemptId, + }) + .pipe( + Effect.andThen( + providerTurnStart.start({ + threadId: effect.threadId, + runId: effect.request.runId, + }), + ), + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + case "runtime-request.respond": + return runtimeRequests + .respond({ + threadId: effect.threadId, + providerSessionId: effect.request.providerSessionId, + requestId: effect.request.requestId, + ...(effect.request.decision === undefined + ? {} + : { decision: effect.request.decision }), + ...(effect.request.answers === undefined + ? {} + : { answers: effect.request.answers }), + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + case "provider-thread.rollback": + return checkpointRollback + .execute({ + threadId: effect.threadId, + providerThreadId: effect.request.providerThreadId, + checkpointId: effect.request.checkpointId, + scopeId: effect.request.scopeId, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + case "checkpoint.capture": + return runFinalization + .finalize({ + threadId: effect.threadId, + runId: effect.request.runId, + scopeId: effect.request.scopeId, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + case "terminal.cleanup": + return resourceCleanup.cleanupTerminals(effect.threadId).pipe( + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + case "attachment.cleanup": + return resourceCleanup.cleanupAttachments(effect.request.attachmentIds).pipe( + Effect.mapError( + (cause) => + new OrchestrationEffectExecutionError({ + effectId: effect.id, + effectType: effect.request.type, + cause, + }), + ), + ); + } + }, + }); + }), +); + +export class OrchestrationEffectWorkerError extends Schema.TaggedErrorClass()( + "OrchestrationEffectWorkerError", + { + operation: Schema.String, + effectId: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface OrchestrationEffectWorkerV2Shape { + readonly awaitWork: Effect.Effect; + readonly runOnce: Effect.Effect; + readonly drain: (maxEffects?: number) => Effect.Effect; +} + +export class OrchestrationEffectWorkerV2 extends Context.Service< + OrchestrationEffectWorkerV2, + OrchestrationEffectWorkerV2Shape +>()("t3/orchestration-v2/EffectWorker/OrchestrationEffectWorkerV2") {} + +export interface OrchestrationEffectWorkerOptions { + readonly workerId?: string; + readonly leaseDurationMs?: number; + readonly maxAttempts?: number; +} + +export const layerWithOptions = ( + options: OrchestrationEffectWorkerOptions = {}, +): Layer.Layer< + OrchestrationEffectWorkerV2, + never, + EffectOutboxV2 | OrchestrationEffectExecutorV2 +> => + Layer.effect( + OrchestrationEffectWorkerV2, + Effect.gen(function* () { + const outbox = yield* EffectOutboxV2; + const executor = yield* OrchestrationEffectExecutorV2; + const workerId = options.workerId ?? `orchestration-v2:${process.pid}`; + const leaseDurationMs = Math.max(1, options.leaseDurationMs ?? 30_000); + const maxAttempts = Math.max(1, options.maxAttempts ?? 5); + + const runOnce = Effect.gen(function* () { + const claimed = yield* outbox.claimNext({ workerId, leaseDurationMs }); + if (Option.isNone(claimed)) { + return false; + } + const effect = claimed.value; + const exit = yield* Effect.exit(executor.execute(effect)); + if (Exit.isSuccess(exit)) { + const completed = yield* outbox.succeed({ effectId: effect.id, workerId }); + if (!completed) { + return yield* new OrchestrationEffectWorkerError({ + operation: "complete", + effectId: effect.id, + cause: "The worker no longer owns the effect lease.", + }); + } + return true; + } + + const error = Cause.pretty(exit.cause); + yield* Effect.logWarning("Orchestration effect execution failed", { + effectId: effect.id, + effectType: effect.request.type, + attemptCount: effect.attemptCount, + error, + }); + const updated = + effect.attemptCount >= maxAttempts + ? yield* outbox.fail({ effectId: effect.id, workerId, error }) + : yield* outbox.retry({ + effectId: effect.id, + workerId, + error, + delayMs: Math.min(30_000, 100 * 2 ** Math.max(0, effect.attemptCount - 1)), + }); + if (!updated) { + return yield* new OrchestrationEffectWorkerError({ + operation: "reschedule", + effectId: effect.id, + cause: "The worker no longer owns the effect lease.", + }); + } + return true; + }).pipe( + Effect.mapError((cause) => + Schema.is(OrchestrationEffectWorkerError)(cause) + ? cause + : new OrchestrationEffectWorkerError({ operation: "run", cause }), + ), + ); + + return OrchestrationEffectWorkerV2.of({ + awaitWork: outbox.awaitAvailable, + runOnce, + drain: (maxEffects = Number.MAX_SAFE_INTEGER) => + Effect.gen(function* () { + let completed = 0; + while (completed < maxEffects && (yield* runOnce)) { + completed += 1; + } + return completed; + }), + }); + }), + ); + +export const layer = layerWithOptions(); + +export const runDaemon = Effect.gen(function* () { + const worker = yield* OrchestrationEffectWorkerV2; + return yield* Effect.forever( + worker.runOnce.pipe( + Effect.catchCause((cause) => + Effect.logWarning("Orchestration effect worker failed", cause).pipe(Effect.as(false)), + ), + Effect.flatMap((worked) => + worked + ? Effect.yieldNow + : Effect.raceFirst(worker.awaitWork, Effect.sleep(Duration.millis(50))), + ), + ), + ); +}); + +export const daemonLayer: Layer.Layer = + Layer.effectDiscard(runDaemon.pipe(Effect.forkScoped)); diff --git a/apps/server/src/orchestration-v2/EventSink.ts b/apps/server/src/orchestration-v2/EventSink.ts new file mode 100644 index 00000000000..257b694db0f --- /dev/null +++ b/apps/server/src/orchestration-v2/EventSink.ts @@ -0,0 +1,475 @@ +import { + CommandId, + OrchestrationV2DomainEvent, + OrchestrationV2StoredEvent, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as PubSub from "effect/PubSub"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { + CommandReceiptStoreV2, + type CommandReceiptV2, + layer as commandReceiptStoreLayer, +} from "./CommandReceiptStore.ts"; +import { + EffectOutboxV2, + type PendingOrchestrationEffectV2, + layer as effectOutboxLayer, +} from "./EffectOutbox.ts"; +import { EventStoreV2 } from "./EventStore.ts"; +import { + ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION, + ProjectionStoreV2, +} from "./ProjectionStore.ts"; +import { + TurnItemPositionStoreV2, + layer as turnItemPositionStoreLayer, +} from "./TurnItemPositionStore.ts"; + +/** + * ERRORS + */ +export class EventSinkWriteError extends Schema.TaggedErrorClass()( + "EventSinkWriteError", + { + eventCount: Schema.Number, + commandId: Schema.optional(CommandId), + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to write ${this.eventCount} orchestration V2 event(s).`; + } +} + +export class EventSinkStreamError extends Schema.TaggedErrorClass()( + "EventSinkStreamError", + { + threadId: Schema.optional(ThreadId), + afterSequence: Schema.optional(Schema.Number), + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return this.threadId === undefined + ? "Failed to stream orchestration V2 events." + : `Failed to stream orchestration V2 events for thread ${this.threadId}.`; + } +} + +export const EventSinkV2Error = Schema.Union([EventSinkWriteError, EventSinkStreamError]); +export type EventSinkV2Error = typeof EventSinkV2Error.Type; + +/** + * SERVICE DEFINITION + */ +export interface EventSinkV2Shape { + readonly write: (input: { + readonly commandId?: CommandId; + readonly events: ReadonlyArray; + }) => Effect.Effect, EventSinkV2Error>; + readonly writeWithEffects: (input: { + readonly commandId?: CommandId; + readonly events: ReadonlyArray; + readonly effects: ReadonlyArray; + }) => Effect.Effect, EventSinkV2Error>; + readonly commitCommand: (input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly commandType: string; + readonly acceptedAt: DateTime.Utc; + readonly events: ReadonlyArray; + readonly effects: ReadonlyArray; + }) => Effect.Effect< + { + readonly receipt: CommandReceiptV2; + readonly storedEvents: ReadonlyArray; + readonly committed: boolean; + }, + EventSinkV2Error + >; + readonly commitRejectedCommand: (input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly commandType: string; + readonly rejectedAt: DateTime.Utc; + readonly error: string; + }) => Effect.Effect; + readonly stream: (input?: { + readonly threadId?: ThreadId; + readonly afterSequence?: number; + }) => Stream.Stream; + readonly latestSequence: (input?: { + readonly threadId?: ThreadId; + }) => Effect.Effect; + readonly readByCommandId: (input: { + readonly commandId: CommandId; + }) => Stream.Stream; +} + +export class EventSinkV2 extends Context.Service()( + "t3/orchestration-v2/EventSink/EventSinkV2", +) {} + +/** + * IMPLEMENTATIONS + */ +const baseLayer: Layer.Layer< + EventSinkV2, + never, + | CommandReceiptStoreV2 + | EffectOutboxV2 + | EventStoreV2 + | ProjectionStoreV2 + | SqlClient.SqlClient + | TurnItemPositionStoreV2 +> = Layer.effect( + EventSinkV2, + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const commandReceipts = yield* CommandReceiptStoreV2; + const effectOutbox = yield* EffectOutboxV2; + const eventStore = yield* EventStoreV2; + const projectionStore = yield* ProjectionStoreV2; + const turnItemPositions = yield* TurnItemPositionStoreV2; + const liveEvents = yield* PubSub.unbounded(); + + const normalizeEvents = (events: ReadonlyArray) => { + const runOrdinals = new Map( + events.flatMap((event) => + event.type === "run.created" || event.type === "run.updated" + ? [[event.payload.id, event.payload.ordinal] as const] + : [], + ), + ); + return Effect.forEach( + events, + (event): Effect.Effect => + event.type === "turn-item.updated" + ? turnItemPositions + .normalize( + event.payload, + event.payload.runId === null ? undefined : runOrdinals.get(event.payload.runId), + ) + .pipe(Effect.map((payload) => ({ ...event, payload }))) + : Effect.succeed(event), + { concurrency: 1 }, + ); + }; + + const applyStoredEvents = (storedEvents: ReadonlyArray) => + Effect.gen(function* () { + yield* Effect.forEach(storedEvents, (stored) => projectionStore.apply(stored.event), { + concurrency: 1, + }); + const sequence = storedEvents.at(-1)?.sequence; + if (sequence !== undefined) { + const now = DateTime.formatIso(yield* DateTime.now); + yield* sql` + INSERT INTO orchestration_v2_projection_metadata ( + projection_name, + schema_version, + last_sequence, + updated_at + ) + VALUES ( + 'thread-projections', + ${ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION}, + ${sequence}, + ${now} + ) + ON CONFLICT(projection_name) + DO UPDATE SET + schema_version = excluded.schema_version, + last_sequence = excluded.last_sequence, + updated_at = excluded.updated_at + `; + } + }); + + const writeEffect = Effect.fn("orchestrationV2.EventSink.write")(function* ( + input: Parameters[0], + ) { + yield* Effect.annotateCurrentSpan({ + "orchestration_v2.command_id": input.commandId ?? null, + "orchestration_v2.event_count": input.events.length, + "orchestration_v2.thread_id": input.events[0]?.threadId ?? null, + }); + + const storedEvents = yield* sql.withTransaction( + Effect.gen(function* () { + const normalized = yield* normalizeEvents(input.events); + const committed = yield* eventStore.append({ + ...(input.commandId === undefined ? {} : { commandId: input.commandId }), + events: normalized, + }); + yield* applyStoredEvents(committed); + yield* effectOutbox.enqueue(input.effects); + return committed; + }), + ); + if (input.effects.length > 0) { + yield* effectOutbox.notifyAvailable; + } + yield* eventStore.publishCommitted(storedEvents); + yield* PubSub.publishAll(liveEvents, storedEvents); + return storedEvents; + }); + + const existingCommandResult = (commandId: CommandId) => + Effect.gen(function* () { + const existing = yield* commandReceipts.getByCommandId(commandId); + if (Option.isNone(existing)) { + return yield* Effect.die( + new Error(`Command receipt ${commandId} disappeared during its transaction.`), + ); + } + const storedEvents = yield* eventStore.readByCommandId({ commandId }).pipe( + Stream.runCollect, + Effect.map((events): ReadonlyArray => Array.from(events)), + ); + return { receipt: existing.value, storedEvents }; + }); + + const commitCommandEffect = Effect.fn("orchestrationV2.EventSink.commitCommand")(function* ( + input: Parameters[0], + ) { + const result = yield* sql.withTransaction( + Effect.gen(function* () { + const reserved = yield* commandReceipts.insertIfAbsent({ + commandId: input.commandId, + threadId: input.threadId, + commandType: input.commandType, + acceptedAt: input.acceptedAt, + resultSequence: 0, + status: "accepted", + error: null, + }); + if (!reserved) { + const existing = yield* existingCommandResult(input.commandId); + return { ...existing, committed: false as const }; + } + + const normalized = yield* normalizeEvents(input.events); + const storedEvents = yield* eventStore.append({ + commandId: input.commandId, + events: normalized, + }); + const sequence = storedEvents.at(-1)?.sequence; + if (sequence === undefined) { + return yield* Effect.die( + new Error(`Command ${input.commandId} produced no orchestration events.`), + ); + } + yield* applyStoredEvents(storedEvents); + yield* effectOutbox.enqueue(input.effects); + const receipt: CommandReceiptV2 = { + commandId: input.commandId, + threadId: input.threadId, + commandType: input.commandType, + acceptedAt: input.acceptedAt, + resultSequence: sequence, + status: "accepted", + error: null, + }; + yield* commandReceipts.upsert(receipt); + return { receipt, storedEvents, committed: true as const }; + }), + ); + if (input.effects.length > 0) { + yield* effectOutbox.notifyAvailable; + } + if (result.committed) { + yield* eventStore.publishCommitted(result.storedEvents); + yield* PubSub.publishAll(liveEvents, result.storedEvents); + } + return result; + }); + + const commitRejectedCommandEffect = Effect.fn( + "orchestrationV2.EventSink.commitRejectedCommand", + )(function* (input: Parameters[0]) { + return yield* sql.withTransaction( + Effect.gen(function* () { + const sequence = yield* eventStore.latestSequence({ threadId: input.threadId }); + const receipt: CommandReceiptV2 = { + commandId: input.commandId, + threadId: input.threadId, + commandType: input.commandType, + acceptedAt: input.rejectedAt, + resultSequence: sequence, + status: "rejected", + error: input.error, + }; + const inserted = yield* commandReceipts.insertIfAbsent(receipt); + if (inserted) { + return receipt; + } + const existing = yield* commandReceipts.getByCommandId(input.commandId); + return Option.getOrElse(existing, () => receipt); + }), + ); + }); + + const catchUp = (input: { + readonly afterSequence: number; + readonly throughSequence: number; + readonly threadId?: ThreadId; + }): Stream.Stream => { + const pageSize = 256; + const loop = (afterSequence: number): Stream.Stream => + Stream.unwrap( + eventStore + .read({ + afterSequence, + throughSequence: input.throughSequence, + ...(input.threadId === undefined ? {} : { threadId: input.threadId }), + limit: pageSize, + }) + .pipe( + Stream.runCollect, + Effect.map((chunk) => Array.from(chunk)), + Effect.map((events) => { + if (events.length === 0) { + return Stream.empty; + } + const current = Stream.fromIterable(events); + const last = events.at(-1)?.sequence ?? input.throughSequence; + return events.length < pageSize || last >= input.throughSequence + ? current + : Stream.concat(current, loop(last)); + }), + ), + ); + return loop(input.afterSequence); + }; + + const stream = (input?: { readonly threadId?: ThreadId; readonly afterSequence?: number }) => + Stream.unwrap( + Effect.gen(function* () { + // Subscribe first, then capture the database high-water mark. Events + // committed between those operations are buffered by the subscription. + const subscription = yield* PubSub.subscribe(liveEvents); + const highWater = yield* eventStore.latestSequence(); + const afterSequence = input?.afterSequence ?? 0; + const replay = catchUp({ + afterSequence, + throughSequence: highWater, + ...(input?.threadId === undefined ? {} : { threadId: input.threadId }), + }); + const live = Stream.fromSubscription(subscription).pipe( + Stream.filter((stored) => stored.sequence > Math.max(highWater, afterSequence)), + Stream.filter( + (stored) => input?.threadId === undefined || stored.event.threadId === input.threadId, + ), + ); + return Stream.concat(replay, live); + }), + ); + + return EventSinkV2.of({ + write: (input) => + writeEffect({ ...input, effects: [] }).pipe( + Effect.mapError( + (cause) => + new EventSinkWriteError({ + eventCount: input.events.length, + ...(input.commandId === undefined ? {} : { commandId: input.commandId }), + cause, + }), + ), + ), + writeWithEffects: (input) => + writeEffect(input).pipe( + Effect.mapError( + (cause) => + new EventSinkWriteError({ + eventCount: input.events.length, + ...(input.commandId === undefined ? {} : { commandId: input.commandId }), + cause, + }), + ), + ), + commitCommand: (input) => + commitCommandEffect(input).pipe( + Effect.mapError( + (cause) => + new EventSinkWriteError({ + commandId: input.commandId, + eventCount: input.events.length, + cause, + }), + ), + ), + commitRejectedCommand: (input) => + commitRejectedCommandEffect(input).pipe( + Effect.mapError( + (cause) => + new EventSinkWriteError({ + commandId: input.commandId, + eventCount: 0, + cause, + }), + ), + ), + stream: (input) => + stream(input).pipe( + Stream.mapError( + (cause) => + new EventSinkStreamError({ + ...(input?.threadId === undefined ? {} : { threadId: input.threadId }), + ...(input?.afterSequence === undefined + ? {} + : { afterSequence: input.afterSequence }), + cause, + }), + ), + ), + latestSequence: (input) => + eventStore.latestSequence(input).pipe( + Effect.mapError( + (cause) => + new EventSinkStreamError({ + ...(input?.threadId === undefined ? {} : { threadId: input.threadId }), + cause, + }), + ), + ), + readByCommandId: (input) => + eventStore.readByCommandId(input).pipe( + Stream.mapError( + (cause) => + new EventSinkStreamError({ + cause, + }), + ), + ), + } satisfies EventSinkV2Shape); + }), +); + +/** + * Event sink layer for application compositions that already own the + * persistence services. Keeping the outbox instance shared with the worker is + * important because enqueue notifications are in-memory wakeups backed by the + * durable SQL queue. + */ +export const layerFromStores = baseLayer; + +export const layer: Layer.Layer< + EventSinkV2, + never, + EventStoreV2 | ProjectionStoreV2 | SqlClient.SqlClient +> = baseLayer.pipe( + Layer.provide( + Layer.mergeAll(commandReceiptStoreLayer, effectOutboxLayer, turnItemPositionStoreLayer), + ), +); diff --git a/apps/server/src/orchestration-v2/EventStore.ts b/apps/server/src/orchestration-v2/EventStore.ts new file mode 100644 index 00000000000..495fa4cbaec --- /dev/null +++ b/apps/server/src/orchestration-v2/EventStore.ts @@ -0,0 +1,139 @@ +import { + CommandId, + OrchestrationV2DomainEvent, + OrchestrationV2StoredEvent, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import type * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { OrchestrationEventStoreLive } from "../persistence/Layers/OrchestrationEventStore.ts"; +import { OrchestrationEventStore } from "../persistence/Services/OrchestrationEventStore.ts"; + +export class EventStoreAppendEventsError extends Schema.TaggedErrorClass()( + "EventStoreAppendEventsError", + { + eventCount: Schema.Number, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to append ${this.eventCount} agent orchestration event(s).`; + } +} + +export class EventStoreReadEventsError extends Schema.TaggedErrorClass()( + "EventStoreReadEventsError", + { + afterSequence: Schema.optional(Schema.Number), + threadId: Schema.optional(ThreadId), + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return this.threadId === undefined + ? "Failed to read agent orchestration events." + : `Failed to read agent orchestration events for thread ${this.threadId}.`; + } +} + +export const EventStoreV2Error = Schema.Union([ + EventStoreAppendEventsError, + EventStoreReadEventsError, +]); +export type EventStoreV2Error = typeof EventStoreV2Error.Type; + +export interface EventStoreV2Shape { + readonly append: (input: { + readonly commandId?: CommandId; + readonly events: ReadonlyArray; + }) => Effect.Effect, EventStoreV2Error>; + readonly read: (input?: { + readonly afterSequence?: number; + readonly throughSequence?: number; + readonly threadId?: ThreadId; + readonly limit?: number; + }) => Stream.Stream; + readonly readByCommandId: (input: { + readonly commandId: CommandId; + }) => Stream.Stream; + readonly latestSequence: (input?: { + readonly threadId?: ThreadId; + }) => Effect.Effect; + readonly publishCommitted: ( + events: ReadonlyArray, + ) => Effect.Effect; +} + +export class EventStoreV2 extends Context.Service()( + "t3/orchestration-v2/EventStore/EventStoreV2", +) {} + +const baseLayer: Layer.Layer = Layer.effect( + EventStoreV2, + Effect.gen(function* () { + const applicationEvents = yield* OrchestrationEventStore; + + const read: EventStoreV2Shape["read"] = (input) => + applicationEvents + .readAgentEvents({ + ...(input?.afterSequence === undefined ? {} : { afterSequence: input.afterSequence }), + ...(input?.throughSequence === undefined + ? {} + : { throughSequence: input.throughSequence }), + ...(input?.threadId === undefined ? {} : { threadId: input.threadId }), + ...(input?.limit === undefined ? {} : { limit: input.limit }), + }) + .pipe( + Stream.mapError( + (cause) => + new EventStoreReadEventsError({ + ...(input?.afterSequence === undefined + ? {} + : { afterSequence: input.afterSequence }), + ...(input?.threadId === undefined ? {} : { threadId: input.threadId }), + cause, + }), + ), + ); + + return EventStoreV2.of({ + append: (input) => + applicationEvents.appendAgentEvents(input).pipe( + Effect.mapError( + (cause) => + new EventStoreAppendEventsError({ + eventCount: input.events.length, + cause, + }), + ), + ), + read, + readByCommandId: ({ commandId }) => + applicationEvents + .readAgentEvents({ commandId }) + .pipe(Stream.mapError((cause) => new EventStoreReadEventsError({ cause }))), + latestSequence: (input) => + applicationEvents.latestAgentSequence(input?.threadId).pipe( + Effect.mapError( + (cause) => + new EventStoreReadEventsError({ + ...(input?.threadId === undefined ? {} : { threadId: input.threadId }), + cause, + }), + ), + ), + publishCommitted: applicationEvents.publishCommitted, + }); + }), +); + +export const layer: Layer.Layer = baseLayer.pipe( + Layer.provide(OrchestrationEventStoreLive), +); + +export const layerFromOrchestrationEventStore = baseLayer; diff --git a/apps/server/src/orchestration-v2/FoundationPersistence.test.ts b/apps/server/src/orchestration-v2/FoundationPersistence.test.ts new file mode 100644 index 00000000000..c4ad8ed9860 --- /dev/null +++ b/apps/server/src/orchestration-v2/FoundationPersistence.test.ts @@ -0,0 +1,543 @@ +import { assert, it } from "@effect/vitest"; +import { + CheckpointId, + CheckpointRef, + CheckpointScopeId, + CommandId, + EventId, + MessageId, + type ModelSelection, + NodeId, + type OrchestrationV2AppThread, + type OrchestrationV2DomainEvent, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + ProviderThreadId, + RunId, + ThreadId, + TurnItemId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Ref from "effect/Ref"; +import * as Stream from "effect/Stream"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import { CommandReceiptStoreV2, layer as commandReceiptStoreLayer } from "./CommandReceiptStore.ts"; +import { EffectOutboxV2, layer as effectOutboxLayer } from "./EffectOutbox.ts"; +import { + layerWithOptions as effectWorkerLayerWithOptions, + OrchestrationEffectExecutorV2, + OrchestrationEffectWorkerV2, +} from "./EffectWorker.ts"; +import { EventSinkV2, layer as eventSinkLayer } from "./EventSink.ts"; +import { EventStoreV2, layer as eventStoreLayer } from "./EventStore.ts"; +import { + ProjectionMaintenanceV2, + layer as projectionMaintenanceLayer, +} from "./ProjectionMaintenance.ts"; +import { ProjectionStoreV2, layer as projectionStoreLayer } from "./ProjectionStore.ts"; + +const databaseLayer = SqlitePersistenceMemory; +const eventStoreProvided = eventStoreLayer.pipe(Layer.provideMerge(databaseLayer)); +const projectionStoreProvided = projectionStoreLayer.pipe(Layer.provideMerge(databaseLayer)); +const storesProvided = Layer.mergeAll(databaseLayer, eventStoreProvided, projectionStoreProvided); +const eventSinkProvided = eventSinkLayer.pipe(Layer.provide(storesProvided)); +const effectOutboxProvided = effectOutboxLayer.pipe(Layer.provide(databaseLayer)); +const commandReceiptStoreProvided = commandReceiptStoreLayer.pipe(Layer.provide(databaseLayer)); +const projectionMaintenanceProvided = projectionMaintenanceLayer.pipe( + Layer.provide(storesProvided), +); +const TestLayer = Layer.mergeAll( + storesProvided, + eventSinkProvided, + effectOutboxProvided, + commandReceiptStoreProvided, + projectionMaintenanceProvided, +); + +const providerInstanceId = ProviderInstanceId.make("codex"); +const providerDriver = ProviderDriverKind.make("codex"); +const modelSelection = { + instanceId: providerInstanceId, + model: "gpt-5.4", +} satisfies ModelSelection; + +function makeThread(threadId: ThreadId, now: DateTime.Utc): OrchestrationV2AppThread { + return { + createdBy: "user", + creationSource: "web", + id: threadId, + projectId: ProjectId.make(`project:${threadId}`), + title: `Thread ${threadId}`, + providerInstanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: null, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: threadId, + }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }; +} + +function threadCreatedEvent(input: { + readonly id: string; + readonly thread: OrchestrationV2AppThread; + readonly now: DateTime.Utc; +}): OrchestrationV2DomainEvent { + return { + id: EventId.make(input.id), + type: "thread.created", + threadId: input.thread.id, + providerInstanceId, + occurredAt: input.now, + payload: input.thread, + }; +} + +it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => { + it.effect("paginates catch-up beyond the event-store read limit", () => + Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const now = yield* DateTime.now; + const threadId = ThreadId.make("thread:foundation-large-catch-up"); + const thread = makeThread(threadId, now); + const eventCount = 1_005; + const events: Array = [ + threadCreatedEvent({ id: "event:foundation-catch-up:0", thread, now }), + ...Array.from({ length: eventCount - 1 }, (_, index) => ({ + id: EventId.make(`event:foundation-catch-up:${index + 1}`), + type: "thread.metadata-updated" as const, + threadId, + providerInstanceId, + occurredAt: now, + payload: { + ...thread, + title: `Catch-up update ${index + 1}`, + }, + })), + ]; + + yield* eventSink.write({ events }); + const replayed = yield* eventSink.stream({ afterSequence: 0 }).pipe( + Stream.take(eventCount), + Stream.runCollect, + Effect.map((events) => Array.from(events)), + ); + + assert.lengthOf(replayed, eventCount); + assert.deepEqual( + replayed.map((stored) => stored.sequence), + Array.from({ length: eventCount }, (_, index) => index + 1), + ); + }), + ); + + it.effect("does not lose or duplicate events while transitioning from catch-up to live", () => + Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const now = yield* DateTime.now; + const threadId = ThreadId.make("thread:foundation-stream-race"); + const thread = makeThread(threadId, now); + const created = yield* eventSink.write({ + events: [threadCreatedEvent({ id: "event:foundation-stream-race:0", thread, now })], + }); + + let afterSequence = created[0]!.sequence; + for (let index = 1; index <= 32; index += 1) { + const nextEvent = { + id: EventId.make(`event:foundation-stream-race:${index}`), + type: "thread.metadata-updated" as const, + threadId, + providerInstanceId, + occurredAt: now, + payload: { ...thread, title: `Race update ${index}` }, + } satisfies OrchestrationV2DomainEvent; + const reader = yield* eventSink + .stream({ threadId, afterSequence }) + .pipe(Stream.runHead, Effect.forkChild); + yield* Effect.yieldNow; + const written = yield* eventSink.write({ events: [nextEvent] }); + const received = yield* Fiber.join(reader); + if (Option.isNone(received)) { + return yield* Effect.die("The event stream ended before delivering the live event."); + } + assert.equal(received.value.sequence, written[0]?.sequence); + assert.equal(received.value.event.id, nextEvent.id); + afterSequence = received.value.sequence; + } + }), + ); + + it.effect( + "rolls back events, projections, receipts, and effects after a projection failure", + () => + Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const eventStore = yield* EventStoreV2; + const receipts = yield* CommandReceiptStoreV2; + const outbox = yield* EffectOutboxV2; + const sql = yield* SqlClient.SqlClient; + const now = yield* DateTime.now; + const commandId = CommandId.make("command:foundation-atomic-failure"); + const threadId = ThreadId.make("thread:foundation-atomic-failure"); + const scopeId = CheckpointScopeId.make("scope:foundation-atomic-failure"); + const checkpoint = (index: number) => ({ + id: CheckpointId.make(`checkpoint:foundation-atomic-failure:${index}`), + threadId, + scopeId, + runId: null, + nodeId: NodeId.make("node:foundation-atomic-failure"), + parentCheckpointId: null, + ordinalWithinScope: 1, + appRunOrdinal: null, + ref: CheckpointRef.make(`checkpoint-ref:foundation-atomic-failure:${index}`), + status: "ready" as const, + files: [], + capturedAt: now, + }); + const events = [1, 2].map( + (index) => + ({ + id: EventId.make(`event:foundation-atomic-failure:${index}`), + type: "checkpoint.captured", + threadId, + occurredAt: now, + payload: checkpoint(index), + }) satisfies OrchestrationV2DomainEvent, + ); + + const exit = yield* Effect.exit( + eventSink.commitCommand({ + commandId, + threadId, + commandType: "checkpoint.atomicity-test", + acceptedAt: now, + events, + effects: [ + { + id: "effect:foundation-atomic-failure", + commandId, + threadId, + request: { + type: "provider-turn.start", + runId: RunId.make("run:foundation-atomic-failure"), + }, + }, + ], + }), + ); + assert.equal(exit._tag, "Failure"); + assert.isTrue(Option.isNone(yield* receipts.getByCommandId(commandId))); + assert.deepEqual(yield* outbox.listByCommandId(commandId), []); + assert.deepEqual( + yield* eventStore.readByCommandId({ commandId }).pipe( + Stream.runCollect, + Effect.map((events) => Array.from(events)), + ), + [], + ); + const checkpointRows = yield* sql<{ readonly count: number }>` + SELECT COUNT(*) AS count + FROM orchestration_v2_projection_checkpoints + WHERE thread_id = ${threadId} + `; + assert.equal(checkpointRows[0]?.count, 0); + }), + ); + + it.effect("keeps one durable effect across command retries and executes it after recovery", () => + Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const outbox = yield* EffectOutboxV2; + const now = yield* DateTime.now; + const commandId = CommandId.make("command:foundation-effect-recovery"); + const threadId = ThreadId.make("thread:foundation-effect-recovery"); + const thread = makeThread(threadId, now); + const event = threadCreatedEvent({ + id: "event:foundation-effect-recovery", + thread, + now, + }); + const effect = { + id: "effect:foundation-effect-recovery", + commandId, + threadId, + request: { + type: "provider-turn.start" as const, + runId: RunId.make("run:foundation-effect-recovery"), + }, + }; + + const first = yield* eventSink.commitCommand({ + commandId, + threadId, + commandType: "foundation.effect-recovery", + acceptedAt: now, + events: [event], + effects: [effect], + }); + const retry = yield* eventSink.commitCommand({ + commandId, + threadId, + commandType: "foundation.effect-recovery", + acceptedAt: now, + events: [event], + effects: [effect], + }); + + assert.isTrue(first.committed); + assert.isFalse(retry.committed); + assert.equal(retry.receipt.resultSequence, first.receipt.resultSequence); + assert.lengthOf(retry.storedEvents, 1); + assert.lengthOf(yield* outbox.listByCommandId(commandId), 1); + + const executionCount = yield* Ref.make(0); + const executorLayer = Layer.succeed( + OrchestrationEffectExecutorV2, + OrchestrationEffectExecutorV2.of({ + execute: () => Ref.update(executionCount, (count) => count + 1), + }), + ); + const workerLayer = effectWorkerLayerWithOptions({ workerId: "recovery-worker" }).pipe( + Layer.provide(Layer.merge(Layer.succeed(EffectOutboxV2, outbox), executorLayer)), + ); + yield* Effect.gen(function* () { + const worker = yield* OrchestrationEffectWorkerV2; + assert.isTrue(yield* worker.runOnce); + assert.isFalse(yield* worker.runOnce); + }).pipe(Effect.provide(workerLayer)); + + assert.equal(yield* Ref.get(executionCount), 1); + const storedEffect = yield* outbox.get(effect.id); + assert.isTrue(Option.isSome(storedEffect)); + if (Option.isSome(storedEffect)) { + assert.equal(storedEffect.value.status, "succeeded"); + } + }), + ); + + it.effect("allows only one worker to claim an available effect", () => + Effect.gen(function* () { + const outbox = yield* EffectOutboxV2; + const commandId = CommandId.make("command:foundation-exclusive-claim"); + yield* outbox.enqueue([ + { + id: "effect:foundation-exclusive-claim", + commandId, + threadId: ThreadId.make("thread:foundation-exclusive-claim"), + request: { + type: "provider-turn.start", + runId: RunId.make("run:foundation-exclusive-claim"), + }, + }, + ]); + + const claims = yield* Effect.all( + [ + outbox.claimNext({ workerId: "worker-a", leaseDurationMs: 30_000 }), + outbox.claimNext({ workerId: "worker-b", leaseDurationMs: 30_000 }), + ], + { concurrency: "unbounded" }, + ); + assert.equal(claims.filter(Option.isSome).length, 1); + assert.equal(claims.filter(Option.isNone).length, 1); + const claimedByA = claims[0]; + const claimedByB = claims[1]; + if (Option.isSome(claimedByA)) { + yield* outbox.succeed({ effectId: claimedByA.value.id, workerId: "worker-a" }); + } + if (Option.isSome(claimedByB)) { + yield* outbox.succeed({ effectId: claimedByB.value.id, workerId: "worker-b" }); + } + }), + ); + + it.effect("reclaims running effects immediately during startup recovery", () => + Effect.gen(function* () { + const outbox = yield* EffectOutboxV2; + const commandId = CommandId.make("command:foundation-reclaim-running"); + yield* outbox.enqueue([ + { + id: "effect:foundation-reclaim-running", + commandId, + threadId: ThreadId.make("thread:foundation-reclaim-running"), + request: { + type: "provider-turn.start", + runId: RunId.make("run:foundation-reclaim-running"), + }, + }, + ]); + assert.isTrue( + Option.isSome( + yield* outbox.claimNext({ workerId: "crashed-worker", leaseDurationMs: 30_000 }), + ), + ); + assert.equal(yield* outbox.reclaimRunning, 1); + const reclaimed = yield* outbox.claimNext({ + workerId: "recovery-worker", + leaseDurationMs: 30_000, + }); + assert.isTrue(Option.isSome(reclaimed)); + if (Option.isSome(reclaimed)) assert.equal(reclaimed.value.attemptCount, 2); + }), + ); + + it.effect("allocates collision-free positions beyond 100 items and rebuilds equivalently", () => + Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const projectionStore = yield* ProjectionStoreV2; + const maintenance = yield* ProjectionMaintenanceV2; + const sql = yield* SqlClient.SqlClient; + const now = yield* DateTime.now; + const threadId = ThreadId.make("thread:foundation-many-items"); + const runId = RunId.make("run:foundation-many-items"); + const providerThreadId = ProviderThreadId.make("provider-thread:foundation-many-items"); + const thread = makeThread(threadId, now); + const providerThreadEvent = { + id: EventId.make("event:foundation-many-items:provider-thread"), + type: "provider-thread.updated" as const, + threadId, + providerInstanceId, + occurredAt: now, + payload: { + id: providerThreadId, + driver: providerDriver, + providerInstanceId, + providerSessionId: null, + appThreadId: threadId, + ownerNodeId: null, + nativeThreadRef: null, + nativeConversationHeadRef: null, + status: "active" as const, + firstRunOrdinal: 1, + lastRunOrdinal: 1, + handoffIds: [], + forkedFrom: null, + createdAt: now, + updatedAt: now, + }, + } satisfies OrchestrationV2DomainEvent; + const runEvent = { + id: EventId.make("event:foundation-many-items:run"), + type: "run.created" as const, + threadId, + runId, + providerInstanceId, + occurredAt: now, + payload: { + id: runId, + threadId, + ordinal: 1, + providerInstanceId, + modelSelection, + providerThreadId: null, + userMessageId: MessageId.make("message:foundation-many-items"), + rootNodeId: null, + activeAttemptId: null, + status: "completed" as const, + queuePosition: null, + requestedAt: now, + startedAt: now, + completedAt: now, + checkpointId: null, + contextHandoffId: null, + }, + } satisfies OrchestrationV2DomainEvent; + const items = Array.from({ length: 151 }, (_, index) => ({ + id: TurnItemId.make(`turn-item:foundation-many-items:${index}`), + threadId, + runId, + nodeId: null, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: index % 3, + status: "completed" as const, + title: null, + startedAt: now, + completedAt: now, + updatedAt: now, + type: "dynamic_tool" as const, + toolName: `tool-${index}`, + input: { index }, + output: { completed: true }, + })); + const itemEvents = items.map( + (item, index) => + ({ + id: EventId.make(`event:foundation-many-items:item:${index}`), + type: "turn-item.updated", + threadId, + runId, + providerInstanceId, + occurredAt: now, + payload: item, + }) satisfies OrchestrationV2DomainEvent, + ); + + yield* eventSink.write({ + events: [ + threadCreatedEvent({ id: "event:foundation-many-items:thread", thread, now }), + providerThreadEvent, + runEvent, + ...itemEvents, + ], + }); + const beforeUpdate = yield* projectionStore.getThreadProjection(threadId); + assert.equal(beforeUpdate.thread.activeProviderThreadId, providerThreadId); + const ordinals = beforeUpdate.turnItems.map((item) => item.ordinal); + assert.lengthOf(ordinals, 151); + assert.equal(new Set(ordinals).size, 151); + assert.isTrue(ordinals.every((ordinal) => ordinal > 1_000_000)); + assert.isTrue( + ordinals.every((ordinal, index) => index === 0 || ordinal > ordinals[index - 1]!), + ); + + yield* eventSink.write({ + events: [ + { + id: EventId.make("event:foundation-many-items:update"), + type: "turn-item.updated", + threadId, + runId, + providerInstanceId, + occurredAt: now, + payload: { ...items[0]!, ordinal: 99_999_999, title: "Updated" }, + }, + ], + }); + const afterUpdate = yield* projectionStore.getThreadProjection(threadId); + assert.equal(afterUpdate.turnItems[0]?.ordinal, ordinals[0]); + assert.equal((yield* maintenance.verify).valid, true); + + yield* sql` + DELETE FROM orchestration_v2_projection_turn_items + WHERE turn_item_id = ${items[75]!.id} + `; + const broken = yield* maintenance.verify; + assert.isFalse(broken.valid); + assert.deepEqual(broken.differingThreadIds, [threadId]); + + const rebuilt = yield* maintenance.rebuild; + assert.isTrue(rebuilt.valid); + assert.lengthOf((yield* projectionStore.getThreadProjection(threadId)).turnItems, 151); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/GrokOrchestratorV2.live.test.ts b/apps/server/src/orchestration-v2/GrokOrchestratorV2.live.test.ts new file mode 100644 index 00000000000..6c8ac937774 --- /dev/null +++ b/apps/server/src/orchestration-v2/GrokOrchestratorV2.live.test.ts @@ -0,0 +1,188 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import { + CommandId, + MessageId, + type OrchestrationV2ThreadProjection, + ProjectId, + ThreadId, +} from "@t3tools/contracts"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { FetchHttpClient } from "effect/unstable/http"; +import { describe } from "vite-plus/test"; + +import * as CheckpointStore from "../checkpointing/CheckpointStore.ts"; +import { ServerConfig } from "../config.ts"; +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import { ProviderInstanceRegistryHydrationLive } from "../provider/Layers/ProviderInstanceRegistryHydration.ts"; +import { + NoOpProviderEventLoggers, + ProviderEventLoggers, +} from "../provider/Layers/ProviderEventLoggers.ts"; +import { OpenCodeRuntimeLive } from "../provider/opencodeRuntime.ts"; +import { ServerSettingsService } from "../serverSettings.ts"; +import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; +import * as VcsProcess from "../vcs/VcsProcess.ts"; +import { OrchestratorV2 } from "./Orchestrator.ts"; +import { OrchestrationV2LayerLive } from "./runtimeLayer.ts"; +import { layer as mcpSessionRegistryTestLayer } from "../mcp/McpSessionRegistry.testkit.ts"; +import { GROK_MODEL_SELECTION } from "./testkit/fixtures/shared.ts"; + +const serverConfigLayer = ServerConfig.layerTest(process.cwd(), { + prefix: "t3-grok-v2-live-", +}); + +const vcsDriverRegistryLayer = VcsDriverRegistry.layer.pipe( + Layer.provide(VcsProcess.layer), + Layer.provide(serverConfigLayer), + Layer.provide(NodeServices.layer), +); + +const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(vcsDriverRegistryLayer)); + +const serverSettingsLayer = ServerSettingsService.layerTest({ + providers: { + grok: { enabled: true }, + }, +}); +const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe( + Layer.provide( + Layer.mergeAll( + serverConfigLayer.pipe(Layer.provide(NodeServices.layer)), + serverSettingsLayer, + NodeServices.layer, + FetchHttpClient.layer, + OpenCodeRuntimeLive.pipe(Layer.provide(NodeServices.layer)), + Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers), + ), + ), +); + +const liveLayer = OrchestrationV2LayerLive.pipe( + Layer.provide(mcpSessionRegistryTestLayer), + Layer.provide(SqlitePersistenceMemory), + Layer.provide(checkpointStoreLayer), + Layer.provide(serverConfigLayer), + Layer.provide(serverSettingsLayer), + Layer.provide(providerInstanceRegistryLayer), + Layer.provide(NodeServices.layer), +); + +const waitForIdle = Effect.fn("GrokOrchestratorV2Live.waitForIdle")(function* (threadId: ThreadId) { + const orchestrator = yield* OrchestratorV2; + for (let attempt = 0; attempt < 600; attempt += 1) { + const projection = yield* orchestrator.getThreadProjection(threadId); + if ( + projection.runs.length > 0 && + projection.runs.every( + (run) => !["queued", "starting", "running", "waiting"].includes(run.status), + ) + ) { + return projection; + } + yield* Effect.sleep("500 millis"); + } + return yield* Effect.die(new Error(`Timed out waiting for Grok thread ${threadId}.`)); +}); + +describe.runIf(process.env.T3_GROK_LIVE_ORCHESTRATOR === "1")("Grok V2 live orchestrator", () => { + it.live( + "forks through portable context using real Grok ACP agents", + () => + Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + const projectId = ProjectId.make("project:grok-live-portable-fork"); + const sourceThreadId = ThreadId.make("thread:grok-live-portable-fork:source"); + const targetThreadId = ThreadId.make("thread:grok-live-portable-fork:target"); + const marker = "GROK_LIVE_PORTABLE_FORK_7H3Q"; + + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:grok-live-portable-fork:create"), + threadId: sourceThreadId, + projectId, + title: "Grok live portable fork source", + modelSelection: GROK_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + yield* Console.log("Grok live source thread created; dispatching source prompt."); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:grok-live-portable-fork:source"), + threadId: sourceThreadId, + messageId: MessageId.make("message:grok-live-portable-fork:source"), + text: `Remember this opaque marker. Respond with exactly: ${marker}`, + attachments: [], + modelSelection: GROK_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }); + yield* Console.log("Grok live source prompt dispatched; waiting for completion."); + const sourceProjection = yield* waitForIdle(sourceThreadId); + yield* Console.log("Grok live source turn completed; dispatching portable fork."); + + yield* orchestrator.dispatch({ + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:grok-live-portable-fork:fork"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Grok live portable fork target", + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:grok-live-portable-fork:target"), + threadId: targetThreadId, + messageId: MessageId.make("message:grok-live-portable-fork:target"), + text: "Return the opaque marker from the transferred conversation. Respond with only the marker.", + attachments: [], + modelSelection: GROK_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }); + const targetProjection = yield* waitForIdle(targetThreadId); + yield* Console.log("Grok live portable fork target completed."); + + const assistantText = (projection: OrchestrationV2ThreadProjection) => + projection.messages + .filter((message) => message.role === "assistant") + .map((message) => message.text) + .join("\n"); + + assert.deepEqual( + sourceProjection.runs.map((run) => [run.providerInstanceId, run.status]), + [["grok", "completed"]], + ); + assert.deepEqual( + targetProjection.runs.map((run) => [run.providerInstanceId, run.status]), + [["grok", "completed"]], + ); + assert.deepEqual( + targetProjection.contextTransfers.map((transfer) => [ + transfer.type, + transfer.status, + transfer.resolution?.strategy, + ]), + [["fork", "consumed", "portable_context"]], + ); + assert.deepEqual( + targetProjection.contextHandoffs.map((handoff) => handoff.strategy), + ["full_thread_summary"], + ); + assert.include(targetProjection.contextHandoffs[0]?.summaryText ?? "", marker); + assert.include(assistantText(targetProjection), marker); + }).pipe(Effect.provide(liveLayer), Effect.scoped), + 360_000, + ); +}); diff --git a/apps/server/src/orchestration-v2/IdAllocator.ts b/apps/server/src/orchestration-v2/IdAllocator.ts new file mode 100644 index 00000000000..7e07c76590b --- /dev/null +++ b/apps/server/src/orchestration-v2/IdAllocator.ts @@ -0,0 +1,404 @@ +import { + CheckpointId, + CheckpointScopeId, + CommandId, + ContextHandoffId, + ContextTransferId, + EventId, + MessageId, + NodeId, + PlanId, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + ProviderSessionId, + ProviderThreadId, + ProviderTurnId, + RawEventId, + RunAttemptId, + RunId, + RuntimeRequestId, + ThreadId, + TurnItemId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import { randomUuidV4 } from "./RandomUuid.ts"; + +export const IdAllocatorV2Kind = Schema.Literals([ + "command", + "event", + "raw_event", + "project", + "thread", + "message", + "run", + "run_attempt", + "node", + "provider_session", + "provider_thread", + "provider_turn", + "runtime_request", + "turn_item", + "checkpoint_scope", + "checkpoint", + "context_handoff", + "context_transfer", + "plan", +]); +export type IdAllocatorV2Kind = typeof IdAllocatorV2Kind.Type; + +export class IdAllocatorV2AllocationError extends Schema.TaggedErrorClass()( + "IdAllocatorV2AllocationError", + { + kind: IdAllocatorV2Kind, + input: Schema.optional(Schema.Unknown), + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to allocate orchestration ${this.kind} id.`; + } +} + +export const IdAllocatorV2Error = Schema.Union([IdAllocatorV2AllocationError]); +export type IdAllocatorV2Error = typeof IdAllocatorV2Error.Type; + +export interface IdAllocatorV2AllocateShape { + readonly command: (input: { + readonly fixtureName: string; + readonly commandName: string; + }) => Effect.Effect; + readonly event: (input: { + readonly threadId?: ThreadId; + readonly commandId?: CommandId; + readonly providerSessionId?: ProviderSessionId; + }) => Effect.Effect; + readonly rawEvent: (input: { + readonly providerSessionId: ProviderSessionId; + readonly method: string | null; + }) => Effect.Effect; + readonly project: (input: { + readonly fixtureName: string; + }) => Effect.Effect; + readonly thread: (input: { + readonly fixtureName?: string; + readonly projectId?: ProjectId; + }) => Effect.Effect; + readonly message: (input: { + readonly threadId: ThreadId; + readonly ordinal: number; + }) => Effect.Effect; + readonly providerSession: (input: { + readonly providerInstanceId: ProviderInstanceId; + readonly threadId: ThreadId; + }) => Effect.Effect; + readonly runtimeRequest: (input: { + readonly driver: ProviderDriverKind; + readonly providerTurnId?: ProviderTurnId; + readonly nativeRequestId?: string; + }) => Effect.Effect; + readonly checkpointScope: (input: { + readonly threadId: ThreadId; + readonly name: string; + }) => Effect.Effect; + readonly checkpoint: (input: { + readonly checkpointScopeId: CheckpointScopeId; + readonly name: string; + }) => Effect.Effect; + readonly contextHandoff: (input: { + readonly threadId: ThreadId; + readonly fromProviderInstanceId: ProviderInstanceId; + readonly toProviderInstanceId: ProviderInstanceId; + }) => Effect.Effect; + readonly contextTransfer: (input: { + readonly sourceThreadId: ThreadId; + readonly targetThreadId: ThreadId; + readonly type: string; + }) => Effect.Effect; + readonly plan: (input: { + readonly threadId: ThreadId; + readonly runId?: RunId; + readonly driver: ProviderDriverKind; + }) => Effect.Effect; +} + +export interface IdAllocatorV2DeriveShape { + readonly providerSession: (input: { + readonly providerInstanceId: ProviderInstanceId; + }) => ProviderSessionId; + readonly delegatedTaskNode: (input: { readonly commandId: CommandId }) => NodeId; + readonly delegatedTaskThread: (input: { readonly commandId: CommandId }) => ThreadId; + readonly delegatedTaskMessage: (input: { readonly commandId: CommandId }) => MessageId; + readonly delegatedTaskTurnItem: (input: { readonly commandId: CommandId }) => TurnItemId; + readonly threadFromProviderThread: (input: { + readonly driver: ProviderDriverKind; + readonly nativeThreadId: string; + }) => ThreadId; + readonly run: (input: { readonly threadId: ThreadId; readonly ordinal: number }) => RunId; + readonly runAttempt: (input: { + readonly runId: RunId; + readonly attemptOrdinal: number; + }) => RunAttemptId; + readonly rootNode: (input: { readonly runId: RunId }) => NodeId; + readonly rootNodeAttempt: (input: { + readonly runId: RunId; + readonly attemptOrdinal: number; + }) => NodeId; + readonly userTurnItem: (input: { readonly messageId: MessageId }) => TurnItemId; + readonly runSignalTurnItem: (input: { + readonly runId: RunId; + readonly signal: string; + }) => TurnItemId; + readonly providerThread: (input: { + readonly driver: ProviderDriverKind; + readonly nativeThreadId: string; + }) => ProviderThreadId; + readonly providerTurn: (input: { + readonly driver: ProviderDriverKind; + readonly nativeTurnId: string; + }) => ProviderTurnId; + readonly nodeFromProviderItem: (input: { + readonly driver: ProviderDriverKind; + readonly nativeItemId: string; + }) => NodeId; + readonly messageFromProviderItem: (input: { + readonly driver: ProviderDriverKind; + readonly nativeItemId: string; + }) => MessageId; + readonly turnItemFromProviderItem: (input: { + readonly driver: ProviderDriverKind; + readonly nativeItemId: string; + }) => TurnItemId; + readonly approvalNode: (input: { readonly requestId: RuntimeRequestId }) => NodeId; + readonly approvalTurnItem: (input: { readonly requestId: RuntimeRequestId }) => TurnItemId; +} + +export interface IdAllocatorV2Shape { + readonly allocate: IdAllocatorV2AllocateShape; + readonly derive: IdAllocatorV2DeriveShape; +} + +export class IdAllocatorV2 extends Context.Service()( + "t3/orchestration-v2/IdAllocator/IdAllocatorV2", +) {} + +const encodePart = (part: string | number): string => encodeURIComponent(String(part)); + +const joinId = (prefix: string, ...parts: ReadonlyArray): string => + [prefix, ...parts.map(encodePart)].join(":"); + +const randomId = + (input: { + readonly kind: IdAllocatorV2Kind; + readonly prefix: string; + readonly parts: ReadonlyArray; + readonly make: (value: string) => Id; + }) => + (allocationInput: Input): Effect.Effect => + randomUuidV4.pipe( + Effect.map((uuid) => input.make(joinId(input.prefix, ...input.parts, uuid))), + Effect.mapError( + (cause) => + new IdAllocatorV2AllocationError({ + kind: input.kind, + input: allocationInput, + cause, + }), + ), + ); + +export const layer: Layer.Layer = Layer.succeed( + IdAllocatorV2, + IdAllocatorV2.of({ + allocate: { + command: (input) => + randomId({ + kind: "command", + prefix: "command", + parts: ["fixture", input.fixtureName, input.commandName], + make: CommandId.make, + })(input), + event: (input) => + randomId({ + kind: "event", + prefix: "event", + parts: [ + ...(input.threadId === undefined ? [] : ["thread", input.threadId]), + ...(input.commandId === undefined ? [] : ["command", input.commandId]), + ...(input.providerSessionId === undefined + ? [] + : ["provider-session", input.providerSessionId]), + ], + make: EventId.make, + })(input), + rawEvent: (input) => + randomId({ + kind: "raw_event", + prefix: "raw-event", + parts: [ + "provider-session", + input.providerSessionId, + ...(input.method === null ? [] : ["method", input.method]), + ], + make: RawEventId.make, + })(input), + project: (input) => + randomId({ + kind: "project", + prefix: "project", + parts: ["fixture", input.fixtureName], + make: ProjectId.make, + })(input), + thread: (input) => + randomId({ + kind: "thread", + prefix: "thread", + parts: [ + ...(input.fixtureName === undefined ? [] : ["fixture", input.fixtureName]), + ...(input.projectId === undefined ? [] : ["project", input.projectId]), + ], + make: ThreadId.make, + })(input), + message: (input) => + randomId({ + kind: "message", + prefix: "message", + parts: ["thread", input.threadId, "ordinal", input.ordinal], + make: MessageId.make, + })(input), + providerSession: (input) => + randomId({ + kind: "provider_session", + prefix: "provider-session", + parts: ["provider-instance", input.providerInstanceId, "thread", input.threadId], + make: ProviderSessionId.make, + })(input), + runtimeRequest: (input) => + randomId({ + kind: "runtime_request", + prefix: "runtime-request", + parts: [ + "provider", + input.driver, + ...(input.providerTurnId === undefined ? [] : ["provider-turn", input.providerTurnId]), + ...(input.nativeRequestId === undefined + ? [] + : ["native-request", input.nativeRequestId]), + ], + make: RuntimeRequestId.make, + })(input), + checkpointScope: (input) => + Effect.succeed( + CheckpointScopeId.make( + joinId("checkpoint-scope", "thread", input.threadId, "name", input.name), + ), + ), + checkpoint: (input) => + Effect.succeed( + CheckpointId.make( + joinId("checkpoint", "scope", input.checkpointScopeId, "name", input.name), + ), + ), + contextHandoff: (input) => + randomId({ + kind: "context_handoff", + prefix: "context-handoff", + parts: [ + "thread", + input.threadId, + "from-provider-instance", + input.fromProviderInstanceId, + "to-provider-instance", + input.toProviderInstanceId, + ], + make: ContextHandoffId.make, + })(input), + contextTransfer: (input) => + randomId({ + kind: "context_transfer", + prefix: "context-transfer", + parts: [ + "type", + input.type, + "source-thread", + input.sourceThreadId, + "target-thread", + input.targetThreadId, + ], + make: ContextTransferId.make, + })(input), + plan: (input) => + randomId({ + kind: "plan", + prefix: "plan", + parts: [ + "thread", + input.threadId, + "provider", + input.driver, + ...(input.runId === undefined ? [] : ["run", input.runId]), + ], + make: PlanId.make, + })(input), + }, + derive: { + providerSession: (input) => + ProviderSessionId.make( + joinId("provider-session", "provider-instance", input.providerInstanceId, "shared"), + ), + delegatedTaskNode: (input) => NodeId.make(joinId("node", "delegated-task", input.commandId)), + delegatedTaskThread: (input) => + ThreadId.make(joinId("thread", "delegated-task", input.commandId)), + delegatedTaskMessage: (input) => + MessageId.make(joinId("message", "delegated-task", input.commandId)), + delegatedTaskTurnItem: (input) => + TurnItemId.make(joinId("turn-item", "delegated-task", input.commandId)), + threadFromProviderThread: (input) => + ThreadId.make( + joinId("thread", "provider", input.driver, "native-thread", input.nativeThreadId), + ), + run: (input) => RunId.make(joinId("run", "thread", input.threadId, "ordinal", input.ordinal)), + runAttempt: (input) => + RunAttemptId.make( + joinId("run-attempt", "run", input.runId, "attempt", input.attemptOrdinal), + ), + rootNode: (input) => NodeId.make(joinId("node", "run", input.runId, "root")), + rootNodeAttempt: (input) => + NodeId.make(joinId("node", "run", input.runId, "attempt", input.attemptOrdinal, "root")), + userTurnItem: (input) => TurnItemId.make(joinId("turn-item", "message", input.messageId)), + runSignalTurnItem: (input) => + TurnItemId.make(joinId("turn-item", "run", input.runId, "signal", input.signal)), + providerThread: (input) => + ProviderThreadId.make( + joinId( + "provider-thread", + "provider", + input.driver, + "native-thread", + input.nativeThreadId, + ), + ), + providerTurn: (input) => + ProviderTurnId.make( + joinId("provider-turn", "provider", input.driver, "native-turn", input.nativeTurnId), + ), + nodeFromProviderItem: (input) => + NodeId.make(joinId("node", "provider", input.driver, "native-item", input.nativeItemId)), + messageFromProviderItem: (input) => + MessageId.make( + joinId("message", "provider", input.driver, "native-item", input.nativeItemId), + ), + turnItemFromProviderItem: (input) => + TurnItemId.make( + joinId("turn-item", "provider", input.driver, "native-item", input.nativeItemId), + ), + approvalNode: (input) => NodeId.make(joinId("node", "runtime-request", input.requestId)), + approvalTurnItem: (input) => + TurnItemId.make(joinId("turn-item", "runtime-request", input.requestId)), + }, + }), +); diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts new file mode 100644 index 00000000000..545c3849a4e --- /dev/null +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -0,0 +1,4488 @@ +import { + type ChatAttachment, + CommandId, + type ModelSelection, + OrchestrationV2Command, + type OrchestrationV2AppThread, + type OrchestrationV2ContextHandoff, + type OrchestrationV2ContextSourcePoint, + type OrchestrationV2ContextTransfer, + type OrchestrationV2ContextTransferResolution, + type OrchestrationV2ConversationMessage, + type OrchestrationV2DomainEvent, + type OrchestrationV2ExecutionNode, + type OrchestrationV2ProviderThread, + type OrchestrationV2ProviderTurn, + type OrchestrationV2Run, + type OrchestrationV2RunAttempt, + type OrchestrationV2ThreadShellSnapshot, + type OrchestrationV2StoredEvent, + type OrchestrationV2Subagent, + type OrchestrationV2ThreadProjection, + type OrchestrationV2TurnItem, + ProviderInstanceId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import * as Semaphore from "effect/Semaphore"; + +import { CheckpointServiceV2 } from "./CheckpointService.ts"; +import { CommandPolicyV2 } from "./CommandPolicy.ts"; +import { CommandReceiptStoreV2 } from "./CommandReceiptStore.ts"; +import { ContextHandoffServiceV2 } from "./ContextHandoffService.ts"; +import { EventSinkV2 } from "./EventSink.ts"; +import type { PendingOrchestrationEffectV2 } from "./EffectOutbox.ts"; +import { OrchestrationEffectWorkerV2 } from "./EffectWorker.ts"; +import { IdAllocatorV2 } from "./IdAllocator.ts"; +import { applyToProjection, emptyProjection, ProjectionStoreV2 } from "./ProjectionStore.ts"; +import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts"; +import { ProviderAdapterRegistryV2 } from "./ProviderAdapterRegistry.ts"; +import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts"; +import { ProviderSwitchServiceV2 } from "./ProviderSwitchService.ts"; +import { RuntimePolicyV2 } from "./RuntimePolicy.ts"; +import { + makeSubagentChildThread, + subagentResultForRun, + subagentThreadTitle, +} from "./SubagentProjection.ts"; +import { ThreadForkServiceV2 } from "./ThreadForkService.ts"; + +export class OrchestratorDispatchError extends Schema.TaggedErrorClass()( + "OrchestratorDispatchError", + { + commandId: CommandId, + commandType: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to dispatch orchestration command ${this.commandType} (${this.commandId}).`; + } +} + +export class OrchestratorProjectionError extends Schema.TaggedErrorClass()( + "OrchestratorProjectionError", + { + threadId: ThreadId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to load orchestration projection for thread ${this.threadId}.`; + } +} + +export class OrchestratorDomainEventStreamError extends Schema.TaggedErrorClass()( + "OrchestratorDomainEventStreamError", + { + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return "Failed while streaming orchestration domain events."; + } +} + +export class OrchestratorProviderAdapterError extends Schema.TaggedErrorClass()( + "OrchestratorProviderAdapterError", + { + commandId: CommandId, + providerInstanceId: ProviderInstanceId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Provider adapter failed while dispatching orchestration command ${this.commandId}.`; + } +} + +export class OrchestratorCommandPreviouslyRejectedError extends Schema.TaggedErrorClass()( + "OrchestratorCommandPreviouslyRejectedError", + { + commandId: CommandId, + commandType: Schema.String, + detail: Schema.String, + }, +) { + override get message(): string { + return `Command ${this.commandId} was previously rejected: ${this.detail}`; + } +} + +export const OrchestratorV2Error = Schema.Union([ + OrchestratorDispatchError, + OrchestratorProjectionError, + OrchestratorDomainEventStreamError, + OrchestratorProviderAdapterError, + OrchestratorCommandPreviouslyRejectedError, +]); +export type OrchestratorV2Error = typeof OrchestratorV2Error.Type; + +export interface OrchestratorV2DispatchResult { + readonly sequence: number; + readonly storedEvents: ReadonlyArray; +} + +export interface OrchestratorV2Shape { + readonly dispatch: ( + command: OrchestrationV2Command, + ) => Effect.Effect; + readonly getThreadProjection: ( + threadId: ThreadId, + ) => Effect.Effect; + readonly getThreadSnapshot: (threadId: ThreadId) => Effect.Effect< + { + readonly schemaVersion: number; + readonly snapshotSequence: number; + readonly projection: OrchestrationV2ThreadProjection; + }, + OrchestratorV2Error + >; + readonly getShellSnapshot: () => Effect.Effect< + OrchestrationV2ThreadShellSnapshot, + OrchestratorV2Error + >; + readonly getThreadEventSequence: ( + threadId: ThreadId, + ) => Effect.Effect; + readonly streamStoredEvents: Stream.Stream; + readonly streamStoredEventsFrom: (input?: { + readonly threadId?: ThreadId; + readonly afterSequence?: number; + }) => Stream.Stream; + readonly streamDomainEvents: Stream.Stream; +} + +export class OrchestratorV2 extends Context.Service()( + "t3/orchestration-v2/Orchestrator/OrchestratorV2", +) {} + +function nextRunOrdinal(projection: OrchestrationV2ThreadProjection): number { + return projection.runs.length + 1; +} + +function commandThreadId(command: OrchestrationV2Command): ThreadId { + switch (command.type) { + case "thread.create": + case "thread.archive": + case "thread.unarchive": + case "thread.delete": + case "thread.metadata.update": + case "thread.runtime-mode.set": + case "thread.interaction-mode.set": + case "thread.model-selection.set": + case "provider-session.detach": + case "message.dispatch": + case "run.interrupt": + case "queued-message.promote-to-steer": + case "queued-run.reorder": + case "runtime-request.respond": + case "checkpoint.rollback": + case "provider.switch": + return command.threadId; + case "delegated_task.request": + return command.parentThreadId; + case "thread.fork": + case "thread.merge_back": + return command.targetThreadId; + } +} + +function nextTurnItemOrdinal(projection: OrchestrationV2ThreadProjection): number { + return Math.max(0, ...projection.turnItems.map((item) => item.ordinal)) + 1; +} + +function isBlockingRun(run: OrchestrationV2Run): boolean { + return run.status === "starting" || run.status === "running" || run.status === "waiting"; +} + +function delegatedTaskTerminalStatus( + status: OrchestrationV2Run["status"], +): OrchestrationV2Subagent["status"] | null { + switch (status) { + case "completed": + case "failed": + case "cancelled": + case "interrupted": + return status; + case "rolled_back": + return "cancelled"; + case "queued": + case "starting": + case "running": + case "waiting": + return null; + } +} + +function nextQueuedRun( + projection: OrchestrationV2ThreadProjection, +): OrchestrationV2Run | undefined { + return projection.runs + .filter((run) => run.status === "queued") + .toSorted( + (left, right) => + (left.queuePosition ?? left.ordinal) - (right.queuePosition ?? right.ordinal) || + left.ordinal - right.ordinal, + )[0]; +} + +function latestStableRun(projection: OrchestrationV2ThreadProjection): OrchestrationV2Run | null { + return ( + projection.runs + .filter((run) => run.status === "completed" && run.checkpointId !== null) + .toSorted((left, right) => right.ordinal - left.ordinal)[0] ?? null + ); +} + +function runForSourcePoint( + projection: OrchestrationV2ThreadProjection, + sourcePoint: Extract< + OrchestrationV2Command, + { readonly type: "thread.fork" | "thread.merge_back" } + >["sourcePoint"], +): OrchestrationV2Run | null { + switch (sourcePoint.type) { + case "latest_stable": + return latestStableRun(projection); + case "run": + return projection.runs.find((run) => run.id === sourcePoint.runId) ?? null; + case "checkpoint": { + const checkpoint = projection.checkpoints.find( + (candidate) => candidate.id === sourcePoint.checkpointId, + ); + return checkpoint?.runId === null || checkpoint === undefined + ? null + : (projection.runs.find((run) => run.id === checkpoint.runId) ?? null); + } + } +} + +function providerThreadForRun( + projection: OrchestrationV2ThreadProjection, + run: OrchestrationV2Run, +): OrchestrationV2ProviderThread | undefined { + return run.providerThreadId === null + ? undefined + : projection.providerThreads.find((candidate) => candidate.id === run.providerThreadId); +} + +function providerTurnForRun( + projection: OrchestrationV2ThreadProjection, + run: OrchestrationV2Run, +): OrchestrationV2ProviderTurn | undefined { + if (run.activeAttemptId === null) { + return undefined; + } + + return ( + projection.providerTurns.find((turn) => turn.runAttemptId === run.activeAttemptId) ?? + projection.providerTurns.find((turn) => { + const attempt = projection.attempts.find((candidate) => candidate.id === run.activeAttemptId); + return attempt?.providerTurnId === turn.id; + }) + ); +} + +function contextSourcePointForRun( + projection: OrchestrationV2ThreadProjection, + run: OrchestrationV2Run, +): OrchestrationV2ContextSourcePoint { + const providerThread = providerThreadForRun(projection, run); + const providerTurn = providerTurnForRun(projection, run); + return { + threadId: projection.thread.id, + runId: run.id, + ...(run.checkpointId === null ? {} : { checkpointId: run.checkpointId }), + ...(providerThread?.nativeThreadRef === null || providerThread?.nativeThreadRef === undefined + ? {} + : { providerThreadRef: providerThread.nativeThreadRef }), + ...(providerTurn?.nativeTurnRef === null || providerTurn?.nativeTurnRef === undefined + ? {} + : { providerTurnRef: providerTurn.nativeTurnRef }), + }; +} + +function pendingForkTransferForThread( + projection: OrchestrationV2ThreadProjection, +): OrchestrationV2ContextTransfer | undefined { + return projection.contextTransfers.find( + (transfer) => + transfer.type === "fork" && + transfer.targetThreadId === projection.thread.id && + transfer.status === "pending", + ); +} + +function pendingMergeBackTransfersForThread( + projection: OrchestrationV2ThreadProjection, +): ReadonlyArray { + return projection.contextTransfers.filter( + (transfer) => + transfer.type === "merge_back" && + transfer.targetThreadId === projection.thread.id && + transfer.status === "pending", + ); +} + +function latestContextTransfer( + transfers: ReadonlyArray, +): OrchestrationV2ContextTransfer | undefined { + return transfers.reduce((latest, transfer) => { + if (latest === undefined) { + return transfer; + } + return DateTime.toEpochMillis(transfer.updatedAt) >= DateTime.toEpochMillis(latest.updatedAt) + ? transfer + : latest; + }, undefined); +} + +function visibleDeltaRunOrdinals( + projection: OrchestrationV2ThreadProjection, + items: ReadonlyArray, +): OrchestrationV2ContextHandoff["coveredRunOrdinals"] { + const ordinals = items.flatMap((item) => { + if (item.runId === null) { + return []; + } + const run = projection.runs.find((candidate) => candidate.id === item.runId); + return run === undefined ? [] : [run.ordinal]; + }); + if (ordinals.length === 0) { + return { from: 1, to: 1 }; + } + return { + from: Math.min(...ordinals), + to: Math.max(...ordinals), + }; +} + +function rootProviderThreadsForProvider( + projection: OrchestrationV2ThreadProjection, + providerInstanceId: ModelSelection["instanceId"], +): ReadonlyArray { + return projection.providerThreads + .filter( + (providerThread) => + providerThread.providerInstanceId === providerInstanceId && + providerThread.appThreadId === projection.thread.id && + providerThread.ownerNodeId === null, + ) + .toSorted( + (left, right) => + (right.lastRunOrdinal ?? 0) - (left.lastRunOrdinal ?? 0) || + DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt), + ); +} + +const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(function* () { + const checkpointService = yield* CheckpointServiceV2; + const commandPolicy = yield* CommandPolicyV2; + const contextHandoffService = yield* ContextHandoffServiceV2; + const eventSink = yield* EventSinkV2; + const effectWorker = yield* OrchestrationEffectWorkerV2; + const commandReceipts = yield* CommandReceiptStoreV2; + const idAllocator = yield* IdAllocatorV2; + const projectionStore = yield* ProjectionStoreV2; + const providerAdapters = yield* ProviderAdapterRegistryV2; + const providerSessions = yield* ProviderSessionManagerV2; + const providerSwitchService = yield* ProviderSwitchServiceV2; + const runtimePolicy = yield* RuntimePolicyV2; + const threadForkService = yield* ThreadForkServiceV2; + const dispatchSemaphore = yield* Semaphore.make(1); + + const mapDispatchError = + (command: OrchestrationV2Command) => + (effect: Effect.Effect): Effect.Effect => + effect.pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + + const providerSessionIdFor = (input: { + readonly adapter: ProviderAdapterV2Shape; + readonly providerInstanceId: ProviderInstanceId; + readonly threadId: ThreadId; + }) => + input.adapter.getCapabilities().pipe( + Effect.flatMap((capabilities) => + capabilities.sessions.supportsMultipleProviderThreadsPerSession + ? Effect.succeed( + idAllocator.derive.providerSession({ + providerInstanceId: input.providerInstanceId, + }), + ) + : idAllocator.allocate.providerSession({ + providerInstanceId: input.providerInstanceId, + threadId: input.threadId, + }), + ), + ); + + const enforceCommandPolicy = + (command: OrchestrationV2Command) => + (effect: Effect.Effect): Effect.Effect => + effect.pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + + const makeEvent = ( + command: OrchestrationV2Command, + event: Omit, + ) => + Effect.gen(function* () { + const eventId = yield* mapDispatchError(command)( + idAllocator.allocate.event({ + threadId: event.threadId, + commandId: command.commandId, + }), + ); + return { + ...event, + id: eventId, + } as Event; + }); + + const emit = + (events: Ref.Ref>, command: OrchestrationV2Command) => + (event: Omit) => + Effect.gen(function* () { + const withId = yield* makeEvent(command, event); + yield* Ref.update(events, (existing) => [...existing, withId]); + return withId; + }); + + const getProjectionWithPendingEvents = ( + threadId: ThreadId, + events: Ref.Ref>, + ) => + Effect.gen(function* () { + const pending = (yield* Ref.get(events)).filter((event) => event.threadId === threadId); + const stored = yield* Effect.option(projectionStore.getThreadProjection(threadId)); + let projection: OrchestrationV2ThreadProjection; + if (Option.isSome(stored)) { + projection = stored.value; + } else { + const created = pending.find( + ( + event, + ): event is Extract => + event.type === "thread.created", + ); + if (created === undefined) { + return yield* new OrchestratorProjectionError({ threadId }); + } + projection = emptyProjection(created); + } + + for (const event of pending) { + if (event.type === "thread.created" && projection.thread.id === event.payload.id) { + projection = { ...projection, thread: event.payload, updatedAt: event.occurredAt }; + continue; + } + projection = applyToProjection(projection, event); + } + return projection; + }); + + const makeSystemEvent = (event: Omit) => + Effect.gen(function* () { + const eventId = yield* idAllocator.allocate.event({ + threadId: event.threadId, + }); + return { + ...event, + id: eventId, + } as Event; + }); + + const writeSystemEvents = ( + events: ReadonlyArray>, + effects: ReadonlyArray = [], + ) => + Effect.gen(function* () { + const withIds = yield* Effect.forEach(events, (event) => + makeSystemEvent(event as Omit), + ); + yield* eventSink.writeWithEffects({ events: withIds, effects }); + }); + + const startNextQueuedRun = (threadId: ThreadId) => + Effect.gen(function* () { + const projection = yield* projectionStore.getThreadProjection(threadId); + if (projection.runs.some(isBlockingRun)) { + return; + } + + const queuedRun = nextQueuedRun(projection); + if (queuedRun === undefined) { + return; + } + const rootNodeId = queuedRun.rootNodeId; + const attemptId = queuedRun.activeAttemptId; + const providerThreadId = queuedRun.providerThreadId; + if (rootNodeId === null || attemptId === null || providerThreadId === null) { + return yield* new OrchestratorDispatchError({ + commandId: CommandId.make(`command:system:start-queued:${queuedRun.id}`), + commandType: "message.dispatch", + cause: `Queued run ${queuedRun.id} is missing execution identity.`, + }); + } + + const rootNode = projection.nodes.find((candidate) => candidate.id === rootNodeId); + const attempt = projection.attempts.find((candidate) => candidate.id === attemptId); + const queuedProviderThread = projection.providerThreads.find( + (candidate) => candidate.id === providerThreadId, + ); + const checkpointScope = projection.checkpointScopes.find( + (scope) => scope.id === rootNode?.checkpointScopeId, + ); + if ( + rootNode === undefined || + attempt === undefined || + queuedProviderThread === undefined || + checkpointScope === undefined + ) { + return yield* new OrchestratorDispatchError({ + commandId: CommandId.make(`command:system:start-queued:${queuedRun.id}`), + commandType: "message.dispatch", + cause: `Queued run ${queuedRun.id} is missing projection state.`, + }); + } + + const commandId = CommandId.make(`command:system:start-queued:${queuedRun.id}`); + const providerSessionId = + queuedProviderThread.providerSessionId ?? + (yield* providerAdapters.get(queuedRun.providerInstanceId).pipe( + Effect.flatMap((adapter) => + providerSessionIdFor({ + adapter, + providerInstanceId: queuedRun.providerInstanceId, + threadId, + }), + ), + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId, + commandType: "message.dispatch", + cause, + }), + ), + )); + const now = yield* DateTime.now; + const providerThread: OrchestrationV2ProviderThread = { + ...queuedProviderThread, + providerSessionId, + status: "not_loaded", + firstRunOrdinal: queuedProviderThread.firstRunOrdinal ?? queuedRun.ordinal, + lastRunOrdinal: queuedRun.ordinal, + updatedAt: now, + }; + const startingRun: OrchestrationV2Run = { + ...queuedRun, + status: "starting", + queuePosition: null, + startedAt: null, + }; + yield* writeSystemEvents( + [ + { + type: "provider-thread.updated", + threadId, + providerInstanceId: queuedRun.providerInstanceId, + occurredAt: now, + payload: providerThread, + }, + { + type: "run.updated", + threadId, + runId: queuedRun.id, + nodeId: rootNodeId, + providerInstanceId: queuedRun.providerInstanceId, + occurredAt: now, + payload: startingRun, + }, + ], + [ + { + id: `effect:${commandId}:provider-turn.start:${queuedRun.id}`, + commandId, + threadId, + request: { type: "provider-turn.start", runId: queuedRun.id }, + }, + ], + ); + yield* effectWorker.drain(1); + }); + + const dispatchThreadCreate = Effect.fn("orchestrationV2.dispatch.threadCreate")(function* ( + command: Extract, + events: Ref.Ref>, + ) { + yield* Effect.annotateCurrentSpan({ + "orchestration_v2.command_id": command.commandId, + "orchestration_v2.command_type": command.type, + "orchestration_v2.thread_id": command.threadId, + "orchestration_v2.driver": command.modelSelection.instanceId, + }); + + const now = yield* DateTime.now; + const emitEvent = emit(events, command); + const thread: OrchestrationV2AppThread = { + createdBy: command.createdBy, + creationSource: command.creationSource, + id: command.threadId, + projectId: command.projectId, + title: command.title, + providerInstanceId: command.modelSelection.instanceId, + modelSelection: command.modelSelection, + runtimeMode: command.runtimeMode, + interactionMode: command.interactionMode, + branch: command.branch, + worktreePath: command.worktreePath, + activeProviderThreadId: null, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: command.threadId, + }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }; + + yield* emitEvent({ + type: "thread.created", + threadId: command.threadId, + providerInstanceId: command.modelSelection.instanceId, + occurredAt: now, + payload: thread, + }); + }); + + const dispatchThreadMutation = Effect.fn("orchestrationV2.dispatch.threadMutation")(function* ( + command: Extract< + OrchestrationV2Command, + { + readonly type: + | "thread.archive" + | "thread.unarchive" + | "thread.delete" + | "thread.metadata.update" + | "thread.runtime-mode.set" + | "thread.interaction-mode.set" + | "thread.model-selection.set" + | "provider.switch"; + } + >, + events: Ref.Ref>, + effects: Ref.Ref>, + ) { + const projection = yield* projectionStore.getThreadProjection(command.threadId).pipe( + Effect.mapError( + (cause) => + new OrchestratorProjectionError({ + threadId: command.threadId, + cause, + }), + ), + ); + const thread = projection.thread; + if (thread.deletedAt !== null && command.type !== "thread.delete") { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Thread ${command.threadId} is deleted.`, + }); + } + if (command.type === "thread.archive" && thread.archivedAt !== null) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Thread ${command.threadId} is already archived.`, + }); + } + if (command.type === "thread.unarchive" && thread.archivedAt === null) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Thread ${command.threadId} is not archived.`, + }); + } + + const providerSwitchPlan = + command.type === "thread.model-selection.set" || command.type === "provider.switch" + ? yield* Effect.gen(function* () { + yield* providerAdapters.get(command.modelSelection.instanceId).pipe( + Effect.mapError( + (cause) => + new OrchestratorProviderAdapterError({ + commandId: command.commandId, + providerInstanceId: command.modelSelection.instanceId, + cause, + }), + ), + ); + return yield* providerSwitchService + .plan({ + projection, + targetModelSelection: command.modelSelection, + }) + .pipe(mapDispatchError(command)); + }) + : null; + + const now = yield* DateTime.now; + const updatedThread: OrchestrationV2AppThread = (() => { + switch (command.type) { + case "thread.archive": + return { ...thread, archivedAt: now, updatedAt: now }; + case "thread.unarchive": + return { ...thread, archivedAt: null, updatedAt: now }; + case "thread.delete": + return { ...thread, deletedAt: thread.deletedAt ?? now, updatedAt: now }; + case "thread.metadata.update": + return { + ...thread, + ...(command.title === undefined ? {} : { title: command.title }), + ...(command.branch === undefined ? {} : { branch: command.branch }), + ...(command.worktreePath === undefined ? {} : { worktreePath: command.worktreePath }), + updatedAt: now, + }; + case "thread.runtime-mode.set": + return { ...thread, runtimeMode: command.runtimeMode, updatedAt: now }; + case "thread.interaction-mode.set": + return { ...thread, interactionMode: command.interactionMode, updatedAt: now }; + case "thread.model-selection.set": + case "provider.switch": + return { + ...thread, + providerInstanceId: command.modelSelection.instanceId, + modelSelection: command.modelSelection, + updatedAt: now, + }; + } + })(); + const eventType = (() => { + switch (command.type) { + case "thread.archive": + return "thread.archived" as const; + case "thread.unarchive": + return "thread.unarchived" as const; + case "thread.delete": + return "thread.deleted" as const; + case "thread.metadata.update": + return "thread.metadata-updated" as const; + case "thread.runtime-mode.set": + return "thread.runtime-mode-updated" as const; + case "thread.interaction-mode.set": + return "thread.interaction-mode-updated" as const; + case "thread.model-selection.set": + return "thread.model-selection-updated" as const; + case "provider.switch": + return "thread.provider-switched" as const; + } + })(); + yield* emit( + events, + command, + )({ + type: eventType, + threadId: command.threadId, + providerInstanceId: updatedThread.providerInstanceId, + occurredAt: now, + payload: updatedThread, + }); + + if (command.type === "thread.delete") { + const emitEvent = emit(events, command); + const activeRunIds = new Set( + projection.runs + .filter((run) => ["queued", "starting", "running", "waiting"].includes(run.status)) + .map((run) => run.id), + ); + for (const run of projection.runs.filter((candidate) => activeRunIds.has(candidate.id))) { + yield* emitEvent({ + type: "run.updated", + threadId: command.threadId, + runId: run.id, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: { ...run, status: "cancelled", queuePosition: null, completedAt: now }, + }); + } + for (const attempt of projection.attempts.filter( + (candidate) => + activeRunIds.has(candidate.runId) && + (candidate.status === "pending" || candidate.status === "running"), + )) { + const run = projection.runs.find((candidate) => candidate.id === attempt.runId)!; + yield* emitEvent({ + type: "run-attempt.updated", + threadId: command.threadId, + runId: attempt.runId, + nodeId: attempt.rootNodeId, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: { ...attempt, status: "cancelled", completedAt: now }, + }); + } + for (const node of projection.nodes.filter( + (candidate) => + candidate.runId !== null && + activeRunIds.has(candidate.runId) && + ["pending", "running", "waiting"].includes(candidate.status), + )) { + const run = projection.runs.find((candidate) => candidate.id === node.runId)!; + yield* emitEvent({ + type: "node.updated", + threadId: command.threadId, + runId: run.id, + nodeId: node.id, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: { ...node, status: "cancelled", completedAt: now }, + }); + } + for (const request of projection.runtimeRequests.filter( + (candidate) => candidate.status === "pending", + )) { + yield* emitEvent({ + type: "runtime-request.updated", + threadId: command.threadId, + nodeId: request.nodeId, + occurredAt: now, + payload: { + ...request, + status: "cancelled", + responseCapability: { + type: "not_resumable", + reason: "The thread was deleted.", + }, + resolvedAt: now, + }, + }); + } + } + + const detachSessionIds = new Set( + command.type === "thread.archive" || command.type === "thread.delete" + ? projection.providerSessions.map((session) => session.id) + : command.type === "thread.metadata.update" && + command.worktreePath !== undefined && + command.worktreePath !== thread.worktreePath + ? projection.providerSessions.map((session) => session.id) + : command.type === "thread.runtime-mode.set" + ? projection.providerSessions + .filter( + (session) => !session.capabilities.sessions.supportsRuntimeModeSwitchInSession, + ) + .map((session) => session.id) + : (providerSwitchPlan?.releaseProviderSessionIds ?? []), + ); + if (detachSessionIds.size > 0) { + const liveSessions = projection.providerSessions.filter( + (session) => + detachSessionIds.has(session.id) && + session.status !== "stopped" && + session.status !== "error", + ); + yield* Effect.forEach( + liveSessions, + (session) => + Effect.gen(function* () { + yield* emit( + events, + command, + )({ + type: "provider-session.detached", + threadId: command.threadId, + driver: session.driver, + providerInstanceId: session.providerInstanceId, + occurredAt: now, + payload: { + providerSessionId: session.id, + detachedAt: now, + reason: + command.type === "thread.archive" + ? "Thread archived." + : command.type === "thread.delete" + ? "Thread deleted." + : command.type === "thread.metadata.update" + ? "Workspace changed." + : command.type === "thread.runtime-mode.set" + ? "Runtime mode changed." + : "Provider or model selection changed.", + }, + }); + const pendingEffect = { + id: `effect:${command.commandId}:provider-session.detach:${session.id}`, + commandId: command.commandId, + threadId: command.threadId, + request: { + type: "provider-session.detach", + providerSessionId: session.id, + detail: + command.type === "thread.archive" + ? "Thread archived." + : command.type === "thread.delete" + ? "Thread deleted." + : command.type === "thread.metadata.update" + ? "Workspace changed." + : command.type === "thread.runtime-mode.set" + ? "Runtime mode changed." + : "Provider or model selection changed.", + }, + } satisfies PendingOrchestrationEffectV2; + yield* Ref.update(effects, (existing) => [...existing, pendingEffect]); + }), + { concurrency: 1, discard: true }, + ); + } + + if (command.type === "thread.archive" || command.type === "thread.delete") { + yield* Ref.update(effects, (existing) => [ + ...existing, + { + id: `effect:${command.commandId}:terminal.cleanup`, + commandId: command.commandId, + threadId: command.threadId, + request: { type: "terminal.cleanup" }, + } satisfies PendingOrchestrationEffectV2, + ]); + } + + if (command.type === "thread.delete") { + const attachmentIds = Array.from( + new Set( + projection.messages.flatMap((message) => message.attachments.map((item) => item.id)), + ), + ); + if (attachmentIds.length > 0) { + yield* Ref.update(effects, (existing) => [ + ...existing, + { + id: `effect:${command.commandId}:attachment.cleanup`, + commandId: command.commandId, + threadId: command.threadId, + request: { type: "attachment.cleanup", attachmentIds }, + } satisfies PendingOrchestrationEffectV2, + ]); + } + } + }); + + const dispatchProviderSessionDetach = Effect.fn("orchestrationV2.dispatch.providerSessionDetach")( + function* ( + command: Extract, + events: Ref.Ref>, + effects: Ref.Ref>, + ) { + const projection = yield* projectionStore + .getThreadProjection(command.threadId) + .pipe( + Effect.mapError( + (cause) => new OrchestratorProjectionError({ threadId: command.threadId, cause }), + ), + ); + const session = projection.providerSessions.find( + (candidate) => candidate.id === command.providerSessionId, + ); + if (session === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Provider session ${command.providerSessionId} does not belong to thread ${command.threadId}.`, + }); + } + const now = yield* DateTime.now; + yield* emit( + events, + command, + )({ + type: "provider-session.detached", + threadId: command.threadId, + driver: session.driver, + providerInstanceId: session.providerInstanceId, + occurredAt: now, + payload: { + providerSessionId: session.id, + detachedAt: now, + ...(command.reason === undefined ? {} : { reason: command.reason }), + }, + }); + const pendingEffect = { + id: `effect:${command.commandId}:provider-session.detach:${command.providerSessionId}`, + commandId: command.commandId, + threadId: command.threadId, + request: { + type: "provider-session.detach", + providerSessionId: command.providerSessionId, + ...(command.reason === undefined ? {} : { detail: command.reason }), + }, + } satisfies PendingOrchestrationEffectV2; + yield* Ref.update(effects, (existing) => [...existing, pendingEffect]); + }, + ); + + const dispatchThreadFork = Effect.fn("orchestrationV2.dispatch.threadFork")(function* ( + command: Extract, + events: Ref.Ref>, + ) { + yield* Effect.annotateCurrentSpan({ + "orchestration_v2.command_id": command.commandId, + "orchestration_v2.command_type": command.type, + "orchestration_v2.source_thread_id": command.sourceThreadId, + "orchestration_v2.target_thread_id": command.targetThreadId, + "orchestration_v2.source_point_type": command.sourcePoint.type, + }); + + const sourceProjection = yield* projectionStore + .getThreadProjection(command.sourceThreadId) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorProjectionError({ + threadId: command.sourceThreadId, + cause, + }), + ), + ); + + const sourceRun = runForSourcePoint(sourceProjection, command.sourcePoint); + + if (sourceRun === null) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `No stable source run was found for fork source ${command.sourcePoint.type}.`, + }); + } + if (sourceRun.status !== "completed") { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Fork source run ${sourceRun.id} is ${sourceRun.status}; only completed runs are supported.`, + }); + } + const sourceProviderThread = providerThreadForRun(sourceProjection, sourceRun); + const now = command.createdAt ?? (yield* DateTime.now); + const emitEvent = emit(events, command); + const transferId = yield* mapDispatchError(command)( + idAllocator.allocate.contextTransfer({ + sourceThreadId: sourceProjection.thread.id, + targetThreadId: command.targetThreadId, + type: "fork", + }), + ); + const { targetThread, transfer } = yield* threadForkService + .plan({ + sourceProjection, + sourceRun, + sourceProviderThread, + canonicalSourcePoint: contextSourcePointForRun(sourceProjection, sourceRun), + transferId, + targetThreadId: command.targetThreadId, + ...(command.title === undefined ? {} : { title: command.title }), + createdBy: command.createdBy, + creationSource: command.creationSource, + createdAt: now, + }) + .pipe(mapDispatchError(command)); + + yield* emitEvent({ + type: "thread.created", + threadId: command.targetThreadId, + providerInstanceId: targetThread.providerInstanceId, + occurredAt: now, + payload: targetThread, + }); + yield* emitEvent({ + type: "context-transfer.created", + threadId: command.targetThreadId, + providerInstanceId: sourceRun.providerInstanceId, + occurredAt: now, + payload: transfer, + }); + }); + + const dispatchThreadMergeBack = Effect.fn("orchestrationV2.dispatch.threadMergeBack")(function* ( + command: Extract, + events: Ref.Ref>, + ) { + yield* Effect.annotateCurrentSpan({ + "orchestration_v2.command_id": command.commandId, + "orchestration_v2.command_type": command.type, + "orchestration_v2.source_thread_id": command.sourceThreadId, + "orchestration_v2.target_thread_id": command.targetThreadId, + "orchestration_v2.source_point_type": command.sourcePoint.type, + }); + + const sourceProjection = yield* projectionStore + .getThreadProjection(command.sourceThreadId) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorProjectionError({ + threadId: command.sourceThreadId, + cause, + }), + ), + ); + const targetProjection = yield* projectionStore + .getThreadProjection(command.targetThreadId) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorProjectionError({ + threadId: command.targetThreadId, + cause, + }), + ), + ); + + if ( + sourceProjection.thread.lineage.relationshipToParent !== "fork" || + sourceProjection.thread.lineage.parentThreadId !== command.targetThreadId + ) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Thread ${command.sourceThreadId} is not a fork of ${command.targetThreadId}.`, + }); + } + + const sourceRun = runForSourcePoint(sourceProjection, command.sourcePoint); + if (sourceRun === null) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `No stable source run was found for merge-back source ${command.sourcePoint.type}.`, + }); + } + if (sourceRun.status !== "completed") { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Merge-back source run ${sourceRun.id} is ${sourceRun.status}; only completed runs are supported.`, + }); + } + + const forkTransfer = sourceProjection.contextTransfers.findLast( + (transfer) => + transfer.type === "fork" && + transfer.sourceThreadId === command.targetThreadId && + transfer.targetThreadId === command.sourceThreadId, + ); + if (forkTransfer === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `No fork transfer exists between ${command.targetThreadId} and ${command.sourceThreadId}.`, + }); + } + + const sourceProviderThread = providerThreadForRun(sourceProjection, sourceRun); + const now = command.createdAt ?? (yield* DateTime.now); + const emitEvent = emit(events, command); + const transferId = yield* mapDispatchError(command)( + idAllocator.allocate.contextTransfer({ + sourceThreadId: command.sourceThreadId, + targetThreadId: command.targetThreadId, + type: "merge_back", + }), + ); + const pendingMergeBackTransfersForPair = targetProjection.contextTransfers.filter( + (transfer) => + transfer.type === "merge_back" && + transfer.status === "pending" && + transfer.sourceThreadId === command.sourceThreadId && + transfer.targetThreadId === command.targetThreadId, + ); + const transfer: OrchestrationV2ContextTransfer = { + id: transferId, + type: "merge_back", + sourceThreadId: command.sourceThreadId, + targetThreadId: command.targetThreadId, + sourcePoint: contextSourcePointForRun(sourceProjection, sourceRun), + basePoint: forkTransfer.sourcePoint, + sourceProviderInstanceId: sourceRun.providerInstanceId, + targetProviderInstanceId: targetProjection.thread.modelSelection.instanceId, + targetRunId: null, + status: "pending", + resolution: null, + createdBy: command.createdBy, + error: + sourceProviderThread === undefined ? "Source merge-back run has no provider thread." : null, + createdAt: now, + updatedAt: now, + consumedAt: null, + }; + + for (const pendingTransfer of pendingMergeBackTransfersForPair) { + yield* emitEvent({ + type: "context-transfer.updated", + threadId: command.targetThreadId, + providerInstanceId: sourceRun.providerInstanceId, + occurredAt: now, + payload: { + ...pendingTransfer, + status: "superseded", + error: `Superseded by merge-back transfer ${transferId}.`, + updatedAt: now, + }, + }); + } + yield* emitEvent({ + type: "context-transfer.created", + threadId: command.targetThreadId, + providerInstanceId: sourceRun.providerInstanceId, + occurredAt: now, + payload: transfer, + }); + }); + + const dispatchSteerIntoRun = (input: { + readonly command: Extract< + OrchestrationV2Command, + { readonly type: "message.dispatch" | "queued-message.promote-to-steer" } + >; + readonly events: Ref.Ref>; + readonly effects: Ref.Ref>; + readonly projection: OrchestrationV2ThreadProjection; + readonly modelSelection: ModelSelection; + readonly targetRunId: OrchestrationV2Run["id"]; + readonly messageId: OrchestrationV2ConversationMessage["id"]; + readonly text: string; + readonly attachments: ReadonlyArray; + readonly createdBy: OrchestrationV2ConversationMessage["createdBy"]; + readonly creationSource: OrchestrationV2ConversationMessage["creationSource"]; + readonly forceRestart: boolean; + }) => + Effect.gen(function* () { + const targetRun = input.projection.runs.find( + (candidate) => candidate.id === input.targetRunId, + ); + if (targetRun === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause: `Target run ${input.targetRunId} was not found.`, + }); + } + const rootNodeId = targetRun.rootNodeId; + if (rootNodeId === null) { + return yield* new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause: `Target run ${targetRun.id} has no root node.`, + }); + } + if (targetRun.status !== "running") { + return yield* new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause: `Target run ${targetRun.id} is ${targetRun.status} and cannot be steered.`, + }); + } + const providerThread = input.projection.providerThreads.find( + (candidate) => candidate.id === targetRun.providerThreadId, + ); + if (providerThread === undefined || providerThread.providerSessionId === null) { + return yield* new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause: `Provider thread ${targetRun.providerThreadId} has no active provider session for steering.`, + }); + } + const providerSessionId = providerThread.providerSessionId; + const providerTurn = input.projection.providerTurns.find( + (candidate) => + candidate.runAttemptId === targetRun.activeAttemptId && candidate.status === "running", + ); + if (providerTurn === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause: `No running provider turn found for active run ${targetRun.id}.`, + }); + } + const sessionOption = yield* providerSessions.get(providerSessionId).pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause, + }), + ), + ); + if (Option.isNone(sessionOption)) { + return yield* new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause: `Provider session ${providerThread.providerSessionId} is not active.`, + }); + } + + const session = sessionOption.value; + const now = yield* DateTime.now; + const emitEvent = emit(input.events, input.command); + const appendSteeringMessage = (messageInput: { + readonly runId: OrchestrationV2Run["id"]; + readonly nodeId: OrchestrationV2ExecutionNode["id"]; + readonly providerTurnId: typeof providerTurn.id | null; + }) => + Effect.gen(function* () { + const message: OrchestrationV2ConversationMessage = { + createdBy: input.createdBy, + creationSource: input.creationSource, + id: input.messageId, + threadId: input.command.threadId, + runId: messageInput.runId, + nodeId: messageInput.nodeId, + role: "user", + text: input.text, + attachments: input.attachments, + streaming: false, + createdAt: now, + updatedAt: now, + }; + const turnItem: OrchestrationV2TurnItem = { + createdBy: input.createdBy, + creationSource: input.creationSource, + id: idAllocator.derive.userTurnItem({ messageId: input.messageId }), + threadId: input.command.threadId, + runId: messageInput.runId, + nodeId: messageInput.nodeId, + providerThreadId: providerThread.id, + providerTurnId: messageInput.providerTurnId, + nativeItemRef: null, + parentItemId: null, + ordinal: nextTurnItemOrdinal(input.projection), + status: "completed", + title: null, + startedAt: now, + completedAt: now, + updatedAt: now, + type: "user_message", + messageId: input.messageId, + inputIntent: + input.command.type === "queued-message.promote-to-steer" + ? "promoted_queued_to_steer" + : "steer", + text: input.text, + attachments: input.attachments, + }; + yield* emitEvent({ + type: "message.updated", + threadId: input.command.threadId, + runId: messageInput.runId, + nodeId: messageInput.nodeId, + providerInstanceId: targetRun.providerInstanceId, + occurredAt: now, + payload: message, + }); + yield* emitEvent({ + type: "turn-item.updated", + threadId: input.command.threadId, + runId: messageInput.runId, + nodeId: messageInput.nodeId, + providerInstanceId: targetRun.providerInstanceId, + occurredAt: now, + payload: turnItem, + }); + }); + + const steeringPolicy = yield* enforceCommandPolicy(input.command)( + commandPolicy.decideSteeringExecution({ + commandId: input.command.commandId, + threadId: input.command.threadId, + providerInstanceId: targetRun.providerInstanceId, + capabilities: session.providerSession.capabilities, + forceRestart: input.forceRestart, + }), + ); + + if (steeringPolicy === "active_steering") { + yield* appendSteeringMessage({ + runId: targetRun.id, + nodeId: rootNodeId, + providerTurnId: providerTurn.id, + }); + yield* Ref.update(input.effects, (existing) => [ + ...existing, + { + id: `effect:${input.command.commandId}:provider-turn.steer:${providerTurn.id}`, + commandId: input.command.commandId, + threadId: input.command.threadId, + request: { + type: "provider-turn.steer", + providerSessionId, + providerThreadId: providerThread.id, + providerTurnId: providerTurn.id, + messageId: input.messageId, + }, + } satisfies PendingOrchestrationEffectV2, + ]); + return; + } + + const currentAttempt = input.projection.attempts.find( + (candidate) => candidate.id === targetRun.activeAttemptId, + ); + const currentRootNode = input.projection.nodes.find( + (candidate) => candidate.id === rootNodeId, + ); + const attemptOrdinal = + Math.max( + 0, + ...input.projection.attempts + .filter((candidate) => candidate.runId === targetRun.id) + .map((candidate) => candidate.attemptOrdinal), + ) + 1; + const nextAttemptId = idAllocator.derive.runAttempt({ + runId: targetRun.id, + attemptOrdinal, + }); + const nextRootNodeId = idAllocator.derive.rootNodeAttempt({ + runId: targetRun.id, + attemptOrdinal, + }); + const resolvedRuntimePolicy = yield* runtimePolicy + .resolve({ thread: input.projection.thread, modelSelection: input.modelSelection }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause, + }), + ), + ); + const checkpointScope = yield* checkpointService + .prepareRootRunScope({ + threadId: input.command.threadId, + runId: targetRun.id, + rootNodeId: nextRootNodeId, + providerThreadId: providerThread.id, + cwd: resolvedRuntimePolicy.cwd ?? session.providerSession.cwd, + createdAt: now, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause, + }), + ), + ); + const ensuredCheckpointScope = yield* checkpointService.ensureScope(checkpointScope).pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause, + }), + ), + ); + const restartedRun: OrchestrationV2Run = { + ...targetRun, + rootNodeId: nextRootNodeId, + activeAttemptId: nextAttemptId, + userMessageId: input.messageId, + status: "starting", + }; + const nextAttempt: OrchestrationV2RunAttempt = { + id: nextAttemptId, + runId: targetRun.id, + attemptOrdinal, + rootNodeId: nextRootNodeId, + providerInstanceId: targetRun.providerInstanceId, + providerThreadId: providerThread.id, + providerTurnId: null, + reason: "steering_restart", + status: "pending", + startedAt: null, + completedAt: null, + }; + const nextRootNode: OrchestrationV2ExecutionNode = { + id: nextRootNodeId, + threadId: input.command.threadId, + runId: targetRun.id, + parentNodeId: null, + rootNodeId: nextRootNodeId, + kind: "root_turn", + status: "pending", + countsForRun: true, + providerThreadId: providerThread.id, + providerTurnId: null, + nativeItemRef: null, + runtimeRequestId: null, + checkpointScopeId: ensuredCheckpointScope.id, + startedAt: null, + completedAt: null, + }; + if (currentAttempt !== undefined) { + yield* emitEvent({ + type: "run-attempt.updated", + threadId: input.command.threadId, + runId: targetRun.id, + nodeId: rootNodeId, + providerInstanceId: targetRun.providerInstanceId, + occurredAt: now, + payload: { ...currentAttempt, status: "superseded", completedAt: now }, + }); + } + if (currentRootNode !== undefined) { + yield* emitEvent({ + type: "node.updated", + threadId: input.command.threadId, + runId: targetRun.id, + nodeId: rootNodeId, + providerInstanceId: targetRun.providerInstanceId, + occurredAt: now, + payload: { ...currentRootNode, status: "interrupted", completedAt: now }, + }); + } + yield* emitEvent({ + type: "run.updated", + threadId: input.command.threadId, + runId: targetRun.id, + nodeId: nextRootNodeId, + providerInstanceId: targetRun.providerInstanceId, + occurredAt: now, + payload: restartedRun, + }); + yield* emitEvent({ + type: "run-attempt.created", + threadId: input.command.threadId, + runId: targetRun.id, + nodeId: nextRootNodeId, + providerInstanceId: targetRun.providerInstanceId, + occurredAt: now, + payload: nextAttempt, + }); + yield* emitEvent({ + type: "node.updated", + threadId: input.command.threadId, + runId: targetRun.id, + nodeId: nextRootNodeId, + providerInstanceId: targetRun.providerInstanceId, + occurredAt: now, + payload: nextRootNode, + }); + yield* emitEvent({ + type: "checkpoint-scope.created", + threadId: input.command.threadId, + runId: targetRun.id, + nodeId: nextRootNodeId, + providerInstanceId: targetRun.providerInstanceId, + occurredAt: now, + payload: ensuredCheckpointScope, + }); + yield* appendSteeringMessage({ + runId: targetRun.id, + nodeId: nextRootNodeId, + providerTurnId: null, + }); + const interruptedAttemptId = targetRun.activeAttemptId; + if (interruptedAttemptId === null) { + return yield* new OrchestratorDispatchError({ + commandId: input.command.commandId, + commandType: input.command.type, + cause: `Active run ${targetRun.id} has no attempt to interrupt.`, + }); + } + yield* Ref.update(input.effects, (existing) => [ + ...existing, + { + id: `effect:${input.command.commandId}:provider-turn.restart:${providerTurn.id}`, + commandId: input.command.commandId, + threadId: input.command.threadId, + request: { + type: "provider-turn.restart", + providerSessionId, + providerThreadId: providerThread.id, + providerTurnId: providerTurn.id, + interruptedAttemptId, + runId: targetRun.id, + }, + } satisfies PendingOrchestrationEffectV2, + ]); + }); + + const dispatchMessage = ( + command: Extract, + events: Ref.Ref>, + effects: Ref.Ref>, + ) => + Effect.gen(function* () { + const projection = yield* getProjectionWithPendingEvents(command.threadId, events); + const modelSelection = command.modelSelection ?? projection.thread.modelSelection; + const dispatchMode = command.dispatchMode; + const sourcePlanProjection = + command.sourcePlanRef === undefined + ? null + : yield* getProjectionWithPendingEvents(command.sourcePlanRef.threadId, events); + const sourcePlan = + command.sourcePlanRef === undefined + ? null + : (sourcePlanProjection?.plans.find( + (plan) => plan.id === command.sourcePlanRef?.planId && plan.kind === "proposed_plan", + ) ?? null); + if (command.sourcePlanRef !== undefined && sourcePlan === null) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Proposed plan ${command.sourcePlanRef.planId} does not exist on thread ${command.sourcePlanRef.threadId}.`, + }); + } + if ( + sourcePlanProjection !== null && + sourcePlanProjection.thread.projectId !== projection.thread.projectId + ) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Proposed plan ${command.sourcePlanRef?.planId} belongs to a different project.`, + }); + } + if (sourcePlan !== null && sourcePlan.status !== "active") { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Proposed plan ${sourcePlan.id} is not active.`, + }); + } + const completeSourcePlan = (occurredAt: DateTime.Utc) => + sourcePlan === null + ? Effect.void + : emit( + events, + command, + )({ + type: "plan.updated", + threadId: sourcePlan.threadId, + ...(sourcePlan.runId === null ? {} : { runId: sourcePlan.runId }), + nodeId: sourcePlan.nodeId, + occurredAt, + payload: { ...sourcePlan, status: "completed" }, + }); + + if (dispatchMode.type === "steer_active" || dispatchMode.type === "restart_active") { + yield* dispatchSteerIntoRun({ + command, + events, + effects, + projection, + modelSelection, + targetRunId: dispatchMode.targetRunId, + messageId: command.messageId, + text: command.text, + attachments: command.attachments, + createdBy: command.createdBy, + creationSource: command.creationSource, + forceRestart: dispatchMode.type === "restart_active", + }); + return; + } + + const activeProviderThread = projection.providerThreads.find( + (candidate) => candidate.id === projection.thread.activeProviderThreadId, + ); + const activeRun = projection.runs.find(isBlockingRun); + const pendingMergeBackTransfers = pendingMergeBackTransfersForThread(projection); + const shouldQueue = + activeRun !== undefined && + (dispatchMode.type === "start_immediately" || dispatchMode.type === "queue_after_active"); + if (shouldQueue) { + if (pendingMergeBackTransfers.length > 0) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Thread ${command.threadId} has a pending merge-back transfer; queued merge-back consumption is not implemented yet.`, + }); + } + const queueProviderThread = + activeProviderThread ?? + projection.providerThreads.find( + (candidate) => candidate.id === activeRun.providerThreadId, + ); + if (queueProviderThread === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Active run ${activeRun.id} has no provider thread for queued dispatch.`, + }); + } + if (modelSelection.instanceId !== queueProviderThread.providerInstanceId) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Queued dispatch for provider instance ${modelSelection.instanceId} cannot run behind active provider instance ${queueProviderThread.providerInstanceId}.`, + }); + } + const existingProviderSession = + queueProviderThread.providerSessionId === null + ? undefined + : projection.providerSessions.find( + (candidate) => candidate.id === queueProviderThread.providerSessionId, + ); + if (existingProviderSession !== undefined) { + yield* enforceCommandPolicy(command)( + commandPolicy.ensureQueuedMessages({ + commandId: command.commandId, + threadId: command.threadId, + providerInstanceId: modelSelection.instanceId, + capabilities: existingProviderSession.capabilities, + }), + ); + } + + const resolvedRuntimePolicy = yield* runtimePolicy + .resolve({ thread: projection.thread, modelSelection }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + const now = yield* DateTime.now; + const ordinal = nextRunOrdinal(projection); + const runId = idAllocator.derive.run({ threadId: command.threadId, ordinal }); + const attemptId = idAllocator.derive.runAttempt({ runId, attemptOrdinal: 1 }); + const rootNodeId = idAllocator.derive.rootNode({ runId }); + const checkpointScope = yield* checkpointService + .prepareRootRunScope({ + threadId: command.threadId, + runId, + rootNodeId, + providerThreadId: queueProviderThread.id, + cwd: + resolvedRuntimePolicy.cwd ?? + existingProviderSession?.cwd ?? + projection.thread.worktreePath ?? + process.cwd(), + createdAt: now, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + const run: OrchestrationV2Run = { + id: runId, + threadId: command.threadId, + ordinal, + providerInstanceId: modelSelection.instanceId, + modelSelection, + providerThreadId: queueProviderThread.id, + userMessageId: command.messageId, + rootNodeId, + activeAttemptId: attemptId, + status: "queued", + queuePosition: + Math.max( + 0, + ...projection.runs + .filter((candidate) => candidate.status === "queued") + .map((candidate) => candidate.queuePosition ?? candidate.ordinal), + ) + 1, + requestedAt: now, + startedAt: null, + completedAt: null, + checkpointId: null, + contextHandoffId: null, + ...(command.sourcePlanRef === undefined ? {} : { sourcePlanRef: command.sourcePlanRef }), + }; + const attempt: OrchestrationV2RunAttempt = { + id: attemptId, + runId, + attemptOrdinal: 1, + rootNodeId, + providerInstanceId: modelSelection.instanceId, + providerThreadId: queueProviderThread.id, + providerTurnId: null, + reason: "initial", + status: "pending", + startedAt: null, + completedAt: null, + }; + const rootNode: OrchestrationV2ExecutionNode = { + id: rootNodeId, + threadId: command.threadId, + runId, + parentNodeId: null, + rootNodeId, + kind: "root_turn", + status: "pending", + countsForRun: true, + providerThreadId: queueProviderThread.id, + providerTurnId: null, + nativeItemRef: null, + runtimeRequestId: null, + checkpointScopeId: checkpointScope.id, + startedAt: null, + completedAt: null, + }; + const message: OrchestrationV2ConversationMessage = { + createdBy: command.createdBy, + creationSource: command.creationSource, + id: command.messageId, + threadId: command.threadId, + runId, + nodeId: rootNodeId, + role: "user", + text: command.text, + attachments: command.attachments, + streaming: false, + createdAt: now, + updatedAt: now, + }; + const turnItem: OrchestrationV2TurnItem = { + createdBy: command.createdBy, + creationSource: command.creationSource, + id: idAllocator.derive.userTurnItem({ messageId: command.messageId }), + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerThreadId: queueProviderThread.id, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: ordinal * 100, + status: "completed", + title: null, + startedAt: now, + completedAt: now, + updatedAt: now, + type: "user_message", + messageId: command.messageId, + inputIntent: "queued_turn", + text: command.text, + attachments: command.attachments, + }; + const emitEvent = emit(events, command); + yield* emitEvent({ + type: "run.created", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: run, + }); + yield* completeSourcePlan(now); + yield* emitEvent({ + type: "run-attempt.created", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: attempt, + }); + yield* emitEvent({ + type: "node.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: rootNode, + }); + yield* emitEvent({ + type: "checkpoint-scope.created", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: yield* checkpointService.ensureScope(checkpointScope).pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ), + }); + yield* emitEvent({ + type: "message.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: message, + }); + yield* emitEvent({ + type: "turn-item.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: turnItem, + }); + return; + } + const pendingForkTransfer = pendingForkTransferForThread(projection); + const pendingMergeBackSourceThreadIds = new Set( + pendingMergeBackTransfers.map((transfer) => transfer.sourceThreadId), + ); + if (pendingMergeBackSourceThreadIds.size > 1) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Thread ${command.threadId} has pending merge-back transfers from multiple forks.`, + }); + } + const pendingMergeBackTransfer = latestContextTransfer(pendingMergeBackTransfers); + const supersededMergeBackTransfers = pendingMergeBackTransfers.filter( + (transfer) => transfer.id !== pendingMergeBackTransfer?.id, + ); + const now = yield* DateTime.now; + const ordinal = nextRunOrdinal(projection); + const runId = idAllocator.derive.run({ threadId: command.threadId, ordinal }); + const latestCompletedRun = projection.runs.findLast((run) => run.status === "completed"); + const isProviderSwitch = + activeProviderThread !== undefined && + activeProviderThread.providerInstanceId !== modelSelection.instanceId; + + if ( + pendingForkTransfer === undefined && + pendingMergeBackTransfer === undefined && + !isProviderSwitch + ) { + const adapter = yield* providerAdapters.get(modelSelection.instanceId).pipe( + Effect.mapError( + (cause) => + new OrchestratorProviderAdapterError({ + commandId: command.commandId, + providerInstanceId: modelSelection.instanceId, + cause, + }), + ), + ); + const providerSessionId = + activeProviderThread?.providerSessionId ?? + (yield* mapDispatchError(command)( + providerSessionIdFor({ + adapter, + providerInstanceId: modelSelection.instanceId, + threadId: command.threadId, + }), + )); + const providerThreadId = + activeProviderThread?.id ?? + idAllocator.derive.providerThread({ + driver: adapter.driver, + nativeThreadId: `pending:${runId}`, + }); + const providerThread: OrchestrationV2ProviderThread = + activeProviderThread === undefined + ? { + id: providerThreadId, + driver: adapter.driver, + providerInstanceId: modelSelection.instanceId, + providerSessionId, + appThreadId: command.threadId, + ownerNodeId: null, + nativeThreadRef: null, + nativeConversationHeadRef: null, + status: "not_loaded", + firstRunOrdinal: ordinal, + lastRunOrdinal: ordinal, + handoffIds: [], + forkedFrom: null, + createdAt: now, + updatedAt: now, + } + : { + ...activeProviderThread, + providerSessionId, + lastRunOrdinal: ordinal, + updatedAt: now, + }; + const attemptId = idAllocator.derive.runAttempt({ runId, attemptOrdinal: 1 }); + const rootNodeId = idAllocator.derive.rootNode({ runId }); + const resolvedRuntimePolicy = yield* runtimePolicy + .resolve({ + thread: projection.thread, + modelSelection, + }) + .pipe(mapDispatchError(command)); + const checkpointScope = yield* checkpointService + .prepareRootRunScope({ + threadId: command.threadId, + runId, + rootNodeId, + providerThreadId, + cwd: resolvedRuntimePolicy.cwd ?? projection.thread.worktreePath ?? process.cwd(), + createdAt: now, + }) + .pipe(mapDispatchError(command)); + const run: OrchestrationV2Run = { + id: runId, + threadId: command.threadId, + ordinal, + providerInstanceId: modelSelection.instanceId, + modelSelection, + providerThreadId, + userMessageId: command.messageId, + rootNodeId, + activeAttemptId: attemptId, + status: "starting", + queuePosition: null, + requestedAt: now, + startedAt: null, + completedAt: null, + checkpointId: null, + contextHandoffId: null, + ...(command.sourcePlanRef === undefined ? {} : { sourcePlanRef: command.sourcePlanRef }), + }; + const attempt: OrchestrationV2RunAttempt = { + id: attemptId, + runId, + attemptOrdinal: 1, + rootNodeId, + providerInstanceId: modelSelection.instanceId, + providerThreadId, + providerTurnId: null, + reason: "initial", + status: "pending", + startedAt: null, + completedAt: null, + }; + const rootNode: OrchestrationV2ExecutionNode = { + id: rootNodeId, + threadId: command.threadId, + runId, + parentNodeId: null, + rootNodeId, + kind: "root_turn", + status: "pending", + countsForRun: true, + providerThreadId, + providerTurnId: null, + nativeItemRef: null, + runtimeRequestId: null, + checkpointScopeId: checkpointScope.id, + startedAt: null, + completedAt: null, + }; + const message: OrchestrationV2ConversationMessage = { + createdBy: command.createdBy, + creationSource: command.creationSource, + id: command.messageId, + threadId: command.threadId, + runId, + nodeId: rootNodeId, + role: "user", + text: command.text, + attachments: command.attachments, + streaming: false, + createdAt: now, + updatedAt: now, + }; + const turnItem: OrchestrationV2TurnItem = { + createdBy: command.createdBy, + creationSource: command.creationSource, + id: idAllocator.derive.userTurnItem({ messageId: command.messageId }), + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerThreadId, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: nextTurnItemOrdinal(projection), + status: "completed", + title: null, + startedAt: now, + completedAt: now, + updatedAt: now, + type: "user_message", + messageId: command.messageId, + inputIntent: "turn_start", + text: command.text, + attachments: command.attachments, + }; + const emitEvent = emit(events, command); + yield* emitEvent({ + type: "provider-thread.updated", + threadId: command.threadId, + driver: adapter.driver, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: providerThread, + }); + yield* emitEvent({ + type: "run.created", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: run, + }); + yield* completeSourcePlan(now); + yield* emitEvent({ + type: "run-attempt.created", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: attempt, + }); + yield* emitEvent({ + type: "node.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: rootNode, + }); + yield* emitEvent({ + type: "checkpoint-scope.created", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: checkpointScope, + }); + yield* emitEvent({ + type: "message.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: message, + }); + yield* emitEvent({ + type: "turn-item.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: turnItem, + }); + const pendingEffect = { + id: `effect:${command.commandId}:provider-turn.start:${runId}`, + commandId: command.commandId, + threadId: command.threadId, + request: { type: "provider-turn.start", runId }, + } satisfies PendingOrchestrationEffectV2; + yield* Ref.update(effects, (existing) => [...existing, pendingEffect]); + return; + } + const sourceProjection = + pendingForkTransfer === undefined + ? null + : yield* projectionStore.getThreadProjection(pendingForkTransfer.sourceThreadId).pipe( + Effect.mapError( + (cause) => + new OrchestratorProjectionError({ + threadId: pendingForkTransfer.sourceThreadId, + cause, + }), + ), + ); + const sourceRun = + pendingForkTransfer?.sourcePoint.runId === undefined || sourceProjection === null + ? null + : (sourceProjection.runs.find( + (candidate) => candidate.id === pendingForkTransfer.sourcePoint.runId, + ) ?? null); + const sourceProviderThread = + sourceProjection === null || sourceRun === null + ? undefined + : providerThreadForRun(sourceProjection, sourceRun); + const sourceProviderTurnId = + sourceProjection === null || sourceRun === null || sourceRun.activeAttemptId === null + ? undefined + : (sourceProjection.providerTurns.find( + (candidate) => candidate.runAttemptId === sourceRun.activeAttemptId, + )?.id ?? + sourceProjection.attempts.find( + (candidate) => candidate.id === sourceRun.activeAttemptId, + )?.providerTurnId ?? + undefined); + if (pendingForkTransfer !== undefined) { + if (sourceRun === null || sourceProviderThread === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Pending fork transfer ${pendingForkTransfer.id} has no resolvable source provider thread.`, + }); + } + if (pendingForkTransfer.sourceProviderInstanceId === null) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Pending fork transfer ${pendingForkTransfer.id} has no source provider.`, + }); + } + } + + const adapter = yield* providerAdapters.get(modelSelection.instanceId).pipe( + Effect.mapError( + (cause) => + new OrchestratorProviderAdapterError({ + commandId: command.commandId, + providerInstanceId: modelSelection.instanceId, + cause, + }), + ), + ); + const targetProviderThread = isProviderSwitch + ? rootProviderThreadsForProvider(projection, modelSelection.instanceId)[0] + : activeProviderThread; + const providerSessionId = + targetProviderThread?.providerSessionId ?? + (yield* mapDispatchError(command)( + providerSessionIdFor({ + adapter, + providerInstanceId: modelSelection.instanceId, + threadId: command.threadId, + }), + )); + const existingProviderSession = projection.providerSessions.find( + (candidate) => candidate.id === providerSessionId, + ); + const resolvedRuntimePolicy = yield* runtimePolicy + .resolve({ thread: projection.thread, modelSelection }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + + const capabilities = yield* adapter.getCapabilities().pipe( + Effect.mapError( + (cause) => + new OrchestratorProviderAdapterError({ + commandId: command.commandId, + providerInstanceId: modelSelection.instanceId, + cause, + }), + ), + ); + const forkExecution = + pendingForkTransfer === undefined + ? null + : yield* enforceCommandPolicy(command)( + commandPolicy.decideForkExecution({ + commandId: command.commandId, + threadId: command.threadId, + providerInstanceId: modelSelection.instanceId, + capabilities, + sameProvider: + pendingForkTransfer.sourceProviderInstanceId === modelSelection.instanceId, + hasStrongNativeSource: sourceProviderThread?.nativeThreadRef?.strength === "strong", + fromSpecificTurn: sourceRun !== null, + }), + ); + const canResolveForkNatively = forkExecution === "native_fork"; + const requiresPortableFork = forkExecution === "portable_context"; + + if (canResolveForkNatively) { + yield* enforceCommandPolicy(command)( + commandPolicy.ensureNativeFork({ + commandId: command.commandId, + threadId: command.threadId, + providerInstanceId: modelSelection.instanceId, + capabilities, + fromSpecificTurn: sourceRun !== null, + }), + ); + } + + const ensuredProviderThread: OrchestrationV2ProviderThread = + targetProviderThread === undefined + ? { + id: idAllocator.derive.providerThread({ + driver: adapter.driver, + nativeThreadId: `pending:${runId}`, + }), + driver: adapter.driver, + providerInstanceId: modelSelection.instanceId, + providerSessionId, + appThreadId: command.threadId, + ownerNodeId: null, + nativeThreadRef: null, + nativeConversationHeadRef: null, + status: "not_loaded", + firstRunOrdinal: ordinal, + lastRunOrdinal: ordinal, + handoffIds: [], + forkedFrom: + canResolveForkNatively && sourceProviderThread !== undefined + ? { + providerThreadId: sourceProviderThread.id, + ...(sourceProviderTurnId === undefined + ? {} + : { providerTurnId: sourceProviderTurnId }), + } + : null, + createdAt: now, + updatedAt: now, + } + : { + ...targetProviderThread, + providerSessionId, + updatedAt: now, + }; + const portableForkItems = + !requiresPortableFork || sourceProjection === null || sourceRun === null + ? [] + : sourceProjection.turnItems.filter((item) => { + if (item.runId === null) { + return false; + } + const itemRun = sourceProjection.runs.find( + (candidate) => candidate.id === item.runId, + ); + return itemRun !== undefined && itemRun.ordinal <= sourceRun.ordinal; + }); + const portableForkHandoff = + !requiresPortableFork || + pendingForkTransfer === undefined || + sourceProjection === null || + sourceRun === null + ? null + : yield* contextHandoffService + .prepareProviderHandoff({ + threadId: command.threadId, + targetRunId: runId, + transferId: pendingForkTransfer.id, + fromProviderThreadIds: + sourceProviderThread === undefined ? [] : [sourceProviderThread.id], + toProviderThreadId: ensuredProviderThread.id, + fromProviderInstanceId: sourceRun.providerInstanceId, + toProviderInstanceId: modelSelection.instanceId, + coveredRunOrdinals: visibleDeltaRunOrdinals(sourceProjection, portableForkItems), + strategy: "full_thread_summary", + items: portableForkItems, + createdAt: now, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + const requiresFullProviderSwitchContext = + isProviderSwitch && pendingMergeBackTransfer !== undefined; + const providerSwitchCoveredRuns = + !isProviderSwitch || latestCompletedRun === undefined + ? [] + : projection.runs.filter( + (run) => + run.status === "completed" && + run.ordinal > + (requiresFullProviderSwitchContext + ? 0 + : (targetProviderThread?.lastRunOrdinal ?? 0)) && + run.ordinal <= latestCompletedRun.ordinal, + ); + const providerSwitchItems = + providerSwitchCoveredRuns.length === 0 + ? [] + : projection.turnItems.filter( + (item) => + item.runId !== null && + providerSwitchCoveredRuns.some((run) => run.id === item.runId), + ); + const providerSwitchTransferId = + providerSwitchCoveredRuns.length === 0 || latestCompletedRun === undefined + ? null + : yield* mapDispatchError(command)( + idAllocator.allocate.contextTransfer({ + sourceThreadId: command.threadId, + targetThreadId: command.threadId, + type: "provider_handoff", + }), + ); + if (providerSwitchTransferId !== null) { + yield* enforceCommandPolicy(command)( + commandPolicy.ensureContextHandoff({ + commandId: command.commandId, + threadId: command.threadId, + providerInstanceId: modelSelection.instanceId, + capabilities, + strategy: + targetProviderThread === undefined || requiresFullProviderSwitchContext + ? "full_thread_summary" + : "delta_context", + }), + ); + } + const providerSwitchHandoff = + providerSwitchTransferId === null || latestCompletedRun === undefined + ? null + : yield* contextHandoffService + .prepareProviderHandoff({ + threadId: command.threadId, + targetRunId: runId, + transferId: providerSwitchTransferId, + fromProviderThreadIds: Array.from( + new Set( + providerSwitchCoveredRuns.flatMap((run) => + run.providerThreadId === null ? [] : [run.providerThreadId], + ), + ), + ), + toProviderThreadId: ensuredProviderThread.id, + fromProviderInstanceId: latestCompletedRun.providerInstanceId, + toProviderInstanceId: modelSelection.instanceId, + coveredRunOrdinals: { + from: providerSwitchCoveredRuns[0]!.ordinal, + to: providerSwitchCoveredRuns.at(-1)!.ordinal, + }, + strategy: + targetProviderThread === undefined || requiresFullProviderSwitchContext + ? "full_thread_summary" + : "delta_since_target_last_seen", + items: providerSwitchItems, + createdAt: now, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + const providerThread: OrchestrationV2ProviderThread = { + ...ensuredProviderThread, + status: "active", + firstRunOrdinal: ensuredProviderThread.firstRunOrdinal ?? ordinal, + lastRunOrdinal: ordinal, + handoffIds: [ + ...ensuredProviderThread.handoffIds, + ...[portableForkHandoff, providerSwitchHandoff].flatMap((handoff) => + handoff === null ? [] : [handoff.id], + ), + ], + updatedAt: now, + }; + + const attemptId = idAllocator.derive.runAttempt({ runId, attemptOrdinal: 1 }); + const rootNodeId = idAllocator.derive.rootNode({ runId }); + const emitEvent = emit(events, command); + const mergeBackSourceProjection = + pendingMergeBackTransfer === undefined + ? null + : yield* projectionStore + .getThreadProjection(pendingMergeBackTransfer.sourceThreadId) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorProjectionError({ + threadId: pendingMergeBackTransfer.sourceThreadId, + cause, + }), + ), + ); + const mergeBackSourceRun = + pendingMergeBackTransfer?.sourcePoint.runId === undefined || + mergeBackSourceProjection === null + ? null + : (mergeBackSourceProjection.runs.find( + (candidate) => candidate.id === pendingMergeBackTransfer.sourcePoint.runId, + ) ?? null); + if (pendingMergeBackTransfer !== undefined && mergeBackSourceRun === null) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Pending merge-back transfer ${pendingMergeBackTransfer.id} has no resolvable source run.`, + }); + } + const mergeBackSourceProviderThread = + mergeBackSourceProjection === null || mergeBackSourceRun === null + ? undefined + : providerThreadForRun(mergeBackSourceProjection, mergeBackSourceRun); + if (pendingMergeBackTransfer !== undefined && mergeBackSourceProviderThread === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Pending merge-back transfer ${pendingMergeBackTransfer.id} has no resolvable source provider thread.`, + }); + } + if (pendingMergeBackTransfer !== undefined) { + yield* enforceCommandPolicy(command)( + commandPolicy.ensureContextHandoff({ + commandId: command.commandId, + threadId: command.threadId, + providerInstanceId: modelSelection.instanceId, + capabilities, + strategy: "fork_delta_context", + }), + ); + } + const mergeBackDeltaItems = + mergeBackSourceProjection === null || mergeBackSourceRun === null + ? [] + : mergeBackSourceProjection.turnItems.filter((item) => { + if (item.runId === null) { + return false; + } + const itemRun = mergeBackSourceProjection.runs.find( + (candidate) => candidate.id === item.runId, + ); + return itemRun !== undefined && itemRun.ordinal <= mergeBackSourceRun.ordinal; + }); + const mergeBackHandoff = + pendingMergeBackTransfer === undefined || + mergeBackSourceProjection === null || + mergeBackSourceRun === null || + mergeBackSourceProviderThread === undefined + ? null + : yield* contextHandoffService + .prepareForkDelta({ + sourceThreadId: pendingMergeBackTransfer.sourceThreadId, + targetThreadId: command.threadId, + targetRunId: runId, + transferId: pendingMergeBackTransfer.id, + fromProviderThreadIds: [mergeBackSourceProviderThread.id], + toProviderThreadId: providerThread.id, + fromProviderInstanceId: mergeBackSourceRun.providerInstanceId, + toProviderInstanceId: modelSelection.instanceId, + coveredRunOrdinals: visibleDeltaRunOrdinals( + mergeBackSourceProjection, + mergeBackDeltaItems, + ), + deltaItems: mergeBackDeltaItems, + createdAt: now, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + const checkpointScope = yield* checkpointService + .prepareRootRunScope({ + threadId: command.threadId, + runId, + rootNodeId, + providerThreadId: providerThread.id, + cwd: + resolvedRuntimePolicy.cwd ?? + existingProviderSession?.cwd ?? + projection.thread.worktreePath ?? + process.cwd(), + createdAt: now, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + const run: OrchestrationV2Run = { + id: runId, + threadId: command.threadId, + ordinal, + providerInstanceId: modelSelection.instanceId, + modelSelection, + providerThreadId: providerThread.id, + userMessageId: command.messageId, + rootNodeId, + activeAttemptId: attemptId, + status: "starting", + queuePosition: null, + requestedAt: now, + startedAt: null, + completedAt: null, + checkpointId: null, + contextHandoffId: + portableForkHandoff?.id ?? providerSwitchHandoff?.id ?? mergeBackHandoff?.id ?? null, + ...(command.sourcePlanRef === undefined ? {} : { sourcePlanRef: command.sourcePlanRef }), + }; + const attempt: OrchestrationV2RunAttempt = { + id: attemptId, + runId, + attemptOrdinal: 1, + rootNodeId, + providerInstanceId: modelSelection.instanceId, + providerThreadId: providerThread.id, + providerTurnId: null, + reason: "initial", + status: "pending", + startedAt: null, + completedAt: null, + }; + const rootNode: OrchestrationV2ExecutionNode = { + id: rootNodeId, + threadId: command.threadId, + runId, + parentNodeId: null, + rootNodeId, + kind: "root_turn", + status: "pending", + countsForRun: true, + providerThreadId: providerThread.id, + providerTurnId: null, + nativeItemRef: null, + runtimeRequestId: null, + checkpointScopeId: checkpointScope.id, + startedAt: null, + completedAt: null, + }; + const message: OrchestrationV2ConversationMessage = { + createdBy: command.createdBy, + creationSource: command.creationSource, + id: command.messageId, + threadId: command.threadId, + runId, + nodeId: rootNodeId, + role: "user", + text: command.text, + attachments: command.attachments, + streaming: false, + createdAt: now, + updatedAt: now, + }; + const turnItem: OrchestrationV2TurnItem = { + createdBy: command.createdBy, + creationSource: command.creationSource, + id: idAllocator.derive.userTurnItem({ messageId: command.messageId }), + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerThreadId: providerThread.id, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: ordinal * 100, + status: "completed", + title: null, + startedAt: now, + completedAt: now, + updatedAt: now, + type: "user_message", + messageId: command.messageId, + inputIntent: "turn_start", + text: command.text, + attachments: command.attachments, + }; + const activeHandoff = portableForkHandoff ?? mergeBackHandoff ?? providerSwitchHandoff; + const handoffTurnItem: OrchestrationV2TurnItem | null = + activeHandoff === null + ? null + : { + id: idAllocator.derive.runSignalTurnItem({ + runId, + signal: `context-handoff:${activeHandoff.id}`, + }), + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerThreadId: providerThread.id, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: ordinal * 100 - 1, + status: "completed", + title: + portableForkHandoff !== null + ? "Fork context" + : providerSwitchHandoff !== null + ? "Provider handoff" + : "Merge-back context", + startedAt: now, + completedAt: now, + updatedAt: now, + type: "handoff", + contextHandoffId: activeHandoff.id, + fromProviderThreadIds: activeHandoff.fromProviderThreadIds, + toProviderThreadId: activeHandoff.toProviderThreadId, + fromProviderInstanceIds: + portableForkHandoff !== null + ? sourceRun === null + ? [] + : [sourceRun.providerInstanceId] + : providerSwitchHandoff === null + ? mergeBackSourceRun === null + ? [] + : [mergeBackSourceRun.providerInstanceId] + : Array.from( + new Set(providerSwitchCoveredRuns.map((run) => run.providerInstanceId)), + ), + toProviderInstanceId: modelSelection.instanceId, + strategy: activeHandoff.strategy, + summary: activeHandoff.summaryText, + }; + const nativeForkResolution: OrchestrationV2ContextTransferResolution | null = + !canResolveForkNatively || providerThread.nativeThreadRef === null + ? null + : { + strategy: "native_fork", + providerThreadRef: providerThread.nativeThreadRef, + }; + const portableForkResolution: OrchestrationV2ContextTransferResolution | null = + pendingForkTransfer === undefined || portableForkHandoff === null + ? null + : { + strategy: "portable_context", + contextHandoffId: portableForkHandoff.id, + }; + const mergeBackResolution: OrchestrationV2ContextTransferResolution | null = + pendingMergeBackTransfer === undefined || mergeBackHandoff === null + ? null + : { + strategy: "fork_delta_context", + contextHandoffId: mergeBackHandoff.id, + }; + + if (pendingForkTransfer !== undefined && canResolveForkNatively) { + yield* emitEvent({ + type: "context-transfer.updated", + threadId: command.threadId, + runId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: { + ...pendingForkTransfer, + targetProviderInstanceId: modelSelection.instanceId, + targetRunId: runId, + status: "pending", + resolution: null, + error: null, + updatedAt: now, + }, + }); + } + if (pendingForkTransfer !== undefined && portableForkResolution !== null) { + yield* emitEvent({ + type: "context-transfer.updated", + threadId: command.threadId, + runId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: { + ...pendingForkTransfer, + targetProviderInstanceId: modelSelection.instanceId, + targetRunId: runId, + status: "resolved_portable", + resolution: portableForkResolution, + error: null, + updatedAt: now, + }, + }); + } + yield* emitEvent({ + type: "provider-thread.updated", + threadId: command.threadId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: providerThread, + }); + if (portableForkHandoff !== null) { + yield* emitEvent({ + type: "context-handoff.updated", + threadId: command.threadId, + runId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: portableForkHandoff, + }); + } + if ( + providerSwitchTransferId !== null && + providerSwitchHandoff !== null && + latestCompletedRun !== undefined + ) { + const transfer: OrchestrationV2ContextTransfer = { + id: providerSwitchTransferId, + type: "provider_handoff", + sourceThreadId: command.threadId, + targetThreadId: command.threadId, + sourcePoint: contextSourcePointForRun(projection, latestCompletedRun), + basePoint: + requiresFullProviderSwitchContext || + targetProviderThread?.lastRunOrdinal === null || + targetProviderThread?.lastRunOrdinal === undefined + ? null + : (() => { + const baseRun = projection.runs.find( + (run) => run.ordinal === targetProviderThread.lastRunOrdinal, + ); + return baseRun === undefined + ? null + : contextSourcePointForRun(projection, baseRun); + })(), + sourceProviderInstanceId: latestCompletedRun.providerInstanceId, + targetProviderInstanceId: modelSelection.instanceId, + targetRunId: runId, + status: "consumed", + resolution: { + strategy: + providerSwitchHandoff.strategy === "full_thread_summary" + ? "portable_context" + : "delta_context", + contextHandoffId: providerSwitchHandoff.id, + }, + createdBy: command.createdBy, + error: null, + createdAt: now, + updatedAt: now, + consumedAt: now, + }; + yield* emitEvent({ + type: "context-transfer.created", + threadId: command.threadId, + runId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: transfer, + }); + yield* emitEvent({ + type: "context-handoff.updated", + threadId: command.threadId, + runId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: providerSwitchHandoff, + }); + } + if (mergeBackHandoff !== null) { + yield* emitEvent({ + type: "context-handoff.updated", + threadId: command.threadId, + runId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: mergeBackHandoff, + }); + } + for (const supersededTransfer of supersededMergeBackTransfers) { + yield* emitEvent({ + type: "context-transfer.updated", + threadId: command.threadId, + runId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: { + ...supersededTransfer, + status: "superseded", + error: + pendingMergeBackTransfer === undefined + ? "Superseded while consuming merge-back transfer." + : `Superseded by merge-back transfer ${pendingMergeBackTransfer.id}.`, + updatedAt: now, + }, + }); + } + if (pendingMergeBackTransfer !== undefined && mergeBackResolution !== null) { + yield* emitEvent({ + type: "context-transfer.updated", + threadId: command.threadId, + runId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: { + ...pendingMergeBackTransfer, + targetProviderInstanceId: modelSelection.instanceId, + targetRunId: runId, + status: "consumed", + resolution: mergeBackResolution, + error: null, + updatedAt: now, + consumedAt: now, + }, + }); + } + yield* emitEvent({ + type: "run.created", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: run, + }); + yield* completeSourcePlan(now); + yield* emitEvent({ + type: "run-attempt.created", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: attempt, + }); + yield* emitEvent({ + type: "node.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: rootNode, + }); + yield* emitEvent({ + type: "checkpoint-scope.created", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: yield* checkpointService.ensureScope(checkpointScope).pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ), + }); + if (handoffTurnItem !== null) { + yield* emitEvent({ + type: "turn-item.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: handoffTurnItem, + }); + } + yield* emitEvent({ + type: "message.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: message, + }); + yield* emitEvent({ + type: "turn-item.updated", + threadId: command.threadId, + runId, + nodeId: rootNodeId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: turnItem, + }); + const forkResolution = nativeForkResolution ?? portableForkResolution; + if (pendingForkTransfer !== undefined && forkResolution !== null) { + yield* emitEvent({ + type: "context-transfer.updated", + threadId: command.threadId, + runId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: { + ...pendingForkTransfer, + targetProviderInstanceId: modelSelection.instanceId, + targetRunId: runId, + status: "consumed", + resolution: forkResolution, + error: null, + updatedAt: now, + consumedAt: now, + }, + }); + } + + const pendingEffect = { + id: `effect:${command.commandId}:provider-turn.start:${runId}`, + commandId: command.commandId, + threadId: command.threadId, + request: { type: "provider-turn.start", runId }, + } satisfies PendingOrchestrationEffectV2; + yield* Ref.update(effects, (existing) => [...existing, pendingEffect]); + }); + + const dispatchDelegatedTaskRequest = Effect.fn("orchestrationV2.dispatch.delegatedTaskRequest")( + function* ( + command: Extract, + events: Ref.Ref>, + effects: Ref.Ref>, + ) { + const parentProjection = yield* projectionStore + .getThreadProjection(command.parentThreadId) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorProjectionError({ + threadId: command.parentThreadId, + cause, + }), + ), + ); + const parentRun = parentProjection.runs.find( + (candidate) => candidate.id === command.parentRunId, + ); + if (parentRun === undefined || !isBlockingRun(parentRun)) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Parent run ${command.parentRunId} is not active.`, + }); + } + const parentNode = parentProjection.nodes.find( + (candidate) => candidate.id === command.parentNodeId, + ); + if ( + parentNode === undefined || + parentNode.runId !== parentRun.id || + parentRun.rootNodeId === null + ) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Parent node ${command.parentNodeId} is not part of active run ${parentRun.id}.`, + }); + } + + const targetAdapter = yield* providerAdapters.get(command.modelSelection.instanceId).pipe( + Effect.mapError( + (cause) => + new OrchestratorProviderAdapterError({ + commandId: command.commandId, + providerInstanceId: command.modelSelection.instanceId, + cause, + }), + ), + ); + + const now = command.createdAt ?? (yield* DateTime.now); + const taskNodeId = idAllocator.derive.delegatedTaskNode({ + commandId: command.commandId, + }); + const childThreadId = idAllocator.derive.delegatedTaskThread({ + commandId: command.commandId, + }); + const childMessageId = idAllocator.derive.delegatedTaskMessage({ + commandId: command.commandId, + }); + const taskTurnItemId = idAllocator.derive.delegatedTaskTurnItem({ + commandId: command.commandId, + }); + const taskTitle = subagentThreadTitle({ + parentTitle: parentProjection.thread.title, + prompt: command.task, + ...(command.title === undefined ? {} : { title: command.title }), + ordinal: parentProjection.subagents.length + 1, + }); + const childThread: OrchestrationV2AppThread = { + ...makeSubagentChildThread({ + parentThread: parentProjection.thread, + childThreadId, + parentNodeId: taskNodeId, + activeProviderThreadId: null, + providerInstanceId: command.modelSelection.instanceId, + modelSelection: command.modelSelection, + title: taskTitle, + now, + createdBy: command.createdBy, + creationSource: command.creationSource, + }), + runtimeMode: command.runtimeMode, + interactionMode: command.interactionMode, + }; + const task: OrchestrationV2Subagent = { + id: taskNodeId, + threadId: command.parentThreadId, + runId: parentRun.id, + parentNodeId: command.parentNodeId, + origin: "app_owned", + createdBy: command.createdBy, + driver: targetAdapter.driver, + providerInstanceId: command.modelSelection.instanceId, + providerThreadId: null, + childThreadId, + nativeTaskRef: null, + prompt: command.task, + title: command.title ?? null, + model: command.modelSelection.model, + status: "running", + result: null, + startedAt: now, + completedAt: null, + updatedAt: now, + }; + const taskNode: OrchestrationV2ExecutionNode = { + id: taskNodeId, + threadId: command.parentThreadId, + runId: parentRun.id, + parentNodeId: command.parentNodeId, + rootNodeId: parentRun.rootNodeId, + kind: "subagent", + status: "running", + countsForRun: false, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: null, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: now, + completedAt: null, + }; + const parentProviderTurn = providerTurnForRun(parentProjection, parentRun); + const taskTurnItem: OrchestrationV2TurnItem = { + id: taskTurnItemId, + threadId: command.parentThreadId, + runId: parentRun.id, + nodeId: taskNodeId, + providerThreadId: parentRun.providerThreadId, + providerTurnId: parentProviderTurn?.id ?? null, + nativeItemRef: null, + parentItemId: null, + ordinal: nextTurnItemOrdinal(parentProjection), + status: "running", + title: command.title ?? taskTitle, + startedAt: now, + completedAt: null, + updatedAt: now, + type: "subagent", + subagentId: taskNodeId, + origin: "app_owned", + driver: targetAdapter.driver, + providerInstanceId: command.modelSelection.instanceId, + childThreadId, + prompt: command.task, + result: null, + }; + const emitEvent = emit(events, command); + + yield* emitEvent({ + type: "thread.created", + threadId: childThreadId, + driver: targetAdapter.driver, + providerInstanceId: command.modelSelection.instanceId, + occurredAt: now, + payload: childThread, + }); + yield* emitEvent({ + type: "node.updated", + threadId: command.parentThreadId, + runId: parentRun.id, + nodeId: taskNodeId, + driver: targetAdapter.driver, + providerInstanceId: command.modelSelection.instanceId, + occurredAt: now, + payload: taskNode, + }); + yield* emitEvent({ + type: "subagent.updated", + threadId: command.parentThreadId, + runId: parentRun.id, + nodeId: taskNodeId, + driver: targetAdapter.driver, + providerInstanceId: command.modelSelection.instanceId, + occurredAt: now, + payload: task, + }); + yield* emitEvent({ + type: "turn-item.updated", + threadId: command.parentThreadId, + runId: parentRun.id, + nodeId: taskNodeId, + driver: targetAdapter.driver, + providerInstanceId: command.modelSelection.instanceId, + occurredAt: now, + payload: taskTurnItem, + }); + + const childMessageCommand = { + type: "message.dispatch", + createdBy: command.createdBy, + creationSource: command.creationSource, + commandId: command.commandId, + threadId: childThreadId, + messageId: childMessageId, + text: command.task, + attachments: [], + modelSelection: command.modelSelection, + dispatchMode: { type: "start_immediately" }, + } satisfies Extract; + yield* dispatchMessage(childMessageCommand, events, effects); + + const childProjection = yield* getProjectionWithPendingEvents(childThreadId, events); + const childRun = childProjection.runs[0]; + if (childRun === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Delegated child thread ${childThreadId} did not create a run.`, + }); + } + const spawnTransferId = yield* mapDispatchError(command)( + idAllocator.allocate.contextTransfer({ + sourceThreadId: command.parentThreadId, + targetThreadId: childThreadId, + type: "subagent_spawn", + }), + ); + const spawnTransfer: OrchestrationV2ContextTransfer = { + id: spawnTransferId, + type: "subagent_spawn", + sourceThreadId: command.parentThreadId, + targetThreadId: childThreadId, + sourcePoint: { + ...contextSourcePointForRun(parentProjection, parentRun), + turnItemId: taskTurnItemId, + }, + basePoint: null, + sourceProviderInstanceId: parentRun.providerInstanceId, + targetProviderInstanceId: command.modelSelection.instanceId, + targetRunId: childRun.id, + status: "consumed", + resolution: null, + createdBy: command.createdBy, + error: null, + createdAt: now, + updatedAt: now, + consumedAt: now, + }; + yield* emitEvent({ + type: "context-transfer.created", + threadId: childThreadId, + runId: childRun.id, + providerInstanceId: command.modelSelection.instanceId, + occurredAt: now, + payload: spawnTransfer, + }); + }, + ); + + const dispatchRuntimeRequestRespond = ( + command: Extract, + events: Ref.Ref>, + effects: Ref.Ref>, + ) => + Effect.gen(function* () { + const projection = yield* projectionStore + .getThreadProjection(command.threadId) + .pipe( + Effect.mapError(() => new OrchestratorProjectionError({ threadId: command.threadId })), + ); + const runtimeRequest = projection.runtimeRequests.find( + (candidate) => candidate.id === command.requestId, + ); + if (runtimeRequest === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Runtime request ${command.requestId} was not found.`, + }); + } + if (runtimeRequest.status !== "pending") { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Runtime request ${command.requestId} is ${runtimeRequest.status}.`, + }); + } + if (runtimeRequest.responseCapability.type !== "live") { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: runtimeRequest.responseCapability.reason, + }); + } + const providerSessionId = runtimeRequest.responseCapability.providerSessionId; + + const providerSession = projection.providerSessions.find( + (candidate) => candidate.id === providerSessionId, + ); + if (providerSession === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Provider session ${providerSessionId} was not found.`, + }); + } + + const now = yield* DateTime.now; + const resolvedRequest = { + ...runtimeRequest, + status: "resolved" as const, + resolvedAt: now, + }; + const emitEvent = emit(events, command); + const requestNode = projection.nodes.find((node) => node.id === runtimeRequest.nodeId); + const resolvedNodeStatus = + command.decision === "decline" || command.decision === "cancel" + ? ("cancelled" as const) + : ("completed" as const); + yield* emitEvent({ + type: "runtime-request.updated", + threadId: command.threadId, + ...(requestNode?.runId == null ? {} : { runId: requestNode.runId }), + nodeId: runtimeRequest.nodeId, + driver: providerSession.driver, + providerInstanceId: providerSession.providerInstanceId, + occurredAt: now, + payload: resolvedRequest, + }); + if (requestNode !== undefined) { + yield* emitEvent({ + type: "node.updated", + threadId: command.threadId, + ...(requestNode.runId === null ? {} : { runId: requestNode.runId }), + nodeId: requestNode.id, + driver: providerSession.driver, + providerInstanceId: providerSession.providerInstanceId, + occurredAt: now, + payload: { + ...requestNode, + status: resolvedNodeStatus, + completedAt: now, + }, + }); + } + + const approvalTurnItem = projection.turnItems.find( + (item) => + (item.type === "approval_request" || item.type === "user_input_request") && + item.requestId === command.requestId, + ); + if (approvalTurnItem !== undefined) { + yield* emitEvent({ + type: "turn-item.updated", + threadId: command.threadId, + ...(approvalTurnItem.runId === null ? {} : { runId: approvalTurnItem.runId }), + ...(approvalTurnItem.nodeId === null ? {} : { nodeId: approvalTurnItem.nodeId }), + driver: providerSession.driver, + providerInstanceId: providerSession.providerInstanceId, + occurredAt: now, + payload: { + ...approvalTurnItem, + status: resolvedNodeStatus, + completedAt: now, + updatedAt: now, + }, + }); + } + yield* Ref.update(effects, (existing) => [ + ...existing, + { + id: `effect:${command.commandId}:runtime-request.respond:${command.requestId}`, + commandId: command.commandId, + threadId: command.threadId, + request: { + type: "runtime-request.respond", + providerSessionId, + requestId: command.requestId, + ...(command.decision === undefined ? {} : { decision: command.decision }), + ...(command.answers === undefined ? {} : { answers: command.answers }), + }, + } satisfies PendingOrchestrationEffectV2, + ]); + }); + + const dispatchQueuedMessagePromoteToSteer = ( + command: Extract, + events: Ref.Ref>, + effects: Ref.Ref>, + ) => + Effect.gen(function* () { + const projection = yield* projectionStore + .getThreadProjection(command.threadId) + .pipe( + Effect.mapError(() => new OrchestratorProjectionError({ threadId: command.threadId })), + ); + const queuedRun = projection.runs.find((candidate) => candidate.id === command.queuedRunId); + if (queuedRun === undefined || queuedRun.status !== "queued") { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Queued run ${command.queuedRunId} is not queued.`, + }); + } + const queuedRootNode = + queuedRun.rootNodeId === null + ? undefined + : projection.nodes.find((candidate) => candidate.id === queuedRun.rootNodeId); + const queuedAttempt = + queuedRun.activeAttemptId === null + ? undefined + : projection.attempts.find((candidate) => candidate.id === queuedRun.activeAttemptId); + const queuedMessage = projection.messages.find( + (candidate) => candidate.id === queuedRun.userMessageId, + ); + if ( + queuedRootNode === undefined || + queuedAttempt === undefined || + queuedMessage === undefined + ) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Queued run ${queuedRun.id} is missing message or execution state.`, + }); + } + + const now = yield* DateTime.now; + const emitEvent = emit(events, command); + yield* emitEvent({ + type: "run.updated", + threadId: command.threadId, + runId: queuedRun.id, + nodeId: queuedRootNode.id, + providerInstanceId: queuedRun.providerInstanceId, + occurredAt: now, + payload: { + ...queuedRun, + status: "cancelled", + queuePosition: null, + completedAt: now, + }, + }); + yield* emitEvent({ + type: "run-attempt.updated", + threadId: command.threadId, + runId: queuedRun.id, + nodeId: queuedRootNode.id, + providerInstanceId: queuedRun.providerInstanceId, + occurredAt: now, + payload: { + ...queuedAttempt, + status: "cancelled", + completedAt: now, + }, + }); + yield* emitEvent({ + type: "node.updated", + threadId: command.threadId, + runId: queuedRun.id, + nodeId: queuedRootNode.id, + providerInstanceId: queuedRun.providerInstanceId, + occurredAt: now, + payload: { + ...queuedRootNode, + status: "cancelled", + completedAt: now, + }, + }); + + yield* dispatchSteerIntoRun({ + command, + events, + effects, + projection, + modelSelection: projection.thread.modelSelection, + targetRunId: command.targetRunId, + messageId: queuedMessage.id, + text: queuedMessage.text, + attachments: queuedMessage.attachments, + createdBy: queuedMessage.createdBy, + creationSource: queuedMessage.creationSource, + forceRestart: false, + }); + }); + + const dispatchQueuedRunReorder = ( + command: Extract, + events: Ref.Ref>, + ) => + Effect.gen(function* () { + const projection = yield* projectionStore + .getThreadProjection(command.threadId) + .pipe( + Effect.mapError(() => new OrchestratorProjectionError({ threadId: command.threadId })), + ); + const queuedRuns = projection.runs + .filter((run) => run.status === "queued") + .toSorted( + (left, right) => + (left.queuePosition ?? left.ordinal) - (right.queuePosition ?? right.ordinal) || + left.ordinal - right.ordinal, + ); + const moving = queuedRuns.find((run) => run.id === command.runId); + if (moving === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Run ${command.runId} is not queued.`, + }); + } + const withoutMoving = queuedRuns.filter((run) => run.id !== command.runId); + const beforeIndex = + command.beforeRunId === null + ? withoutMoving.length + : withoutMoving.findIndex((run) => run.id === command.beforeRunId); + if (beforeIndex === -1) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Queue target ${command.beforeRunId} is not queued.`, + }); + } + const reordered = [ + ...withoutMoving.slice(0, beforeIndex), + moving, + ...withoutMoving.slice(beforeIndex), + ]; + const now = yield* DateTime.now; + const emitEvent = emit(events, command); + yield* Effect.forEach( + reordered, + (run, index) => + Effect.gen(function* () { + const queuePosition = index + 1; + if (run.queuePosition === queuePosition) { + return; + } + yield* emitEvent({ + type: "run.updated", + threadId: command.threadId, + runId: run.id, + ...(run.rootNodeId === null ? {} : { nodeId: run.rootNodeId }), + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: { + ...run, + queuePosition, + }, + }); + }), + { concurrency: 1 }, + ); + }); + + const loadProjectionForCommand = (command: OrchestrationV2Command) => + projectionStore + .getThreadProjection(commandThreadId(command)) + .pipe( + Effect.mapError( + () => new OrchestratorProjectionError({ threadId: commandThreadId(command) }), + ), + ); + + const dispatchRunInterrupt = ( + command: Extract, + events: Ref.Ref>, + effects: Ref.Ref>, + ) => + Effect.gen(function* () { + const findInterruptTarget = ( + attemptsRemaining = 100, + ): Effect.Effect< + { + readonly projection: OrchestrationV2ThreadProjection; + readonly run: OrchestrationV2Run; + readonly rootNode: OrchestrationV2ExecutionNode; + readonly providerThread: OrchestrationV2ProviderThread; + readonly providerTurn: NonNullable< + OrchestrationV2ThreadProjection["providerTurns"][number] + >; + }, + OrchestratorV2Error + > => + Effect.gen(function* () { + const projection = yield* loadProjectionForCommand(command); + const run = projection.runs.find((candidate) => candidate.id === command.runId); + const rootNode = + run?.rootNodeId === null + ? undefined + : projection.nodes.find((candidate) => candidate.id === run?.rootNodeId); + const providerThread = + run?.providerThreadId === null + ? undefined + : projection.providerThreads.find( + (candidate) => candidate.id === run?.providerThreadId, + ); + const providerTurn = projection.providerTurns.find( + (candidate) => + candidate.runAttemptId === run?.activeAttemptId && candidate.status === "running", + ); + + if ( + run !== undefined && + rootNode !== undefined && + providerThread !== undefined && + providerTurn !== undefined + ) { + return { projection, run, rootNode, providerThread, providerTurn }; + } + + if (attemptsRemaining <= 0) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Run ${command.runId} is not interruptible.`, + }); + } + yield* Effect.yieldNow; + return yield* findInterruptTarget(attemptsRemaining - 1); + }); + + const { projection, run, rootNode, providerThread, providerTurn } = + yield* findInterruptTarget(); + if (providerThread.providerSessionId === null) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Provider thread ${providerThread.id} has no active provider session.`, + }); + } + const providerSessionId = providerThread.providerSessionId; + const sessionOption = yield* providerSessions.get(providerSessionId).pipe( + Effect.mapError( + (cause) => + new OrchestratorProviderAdapterError({ + commandId: command.commandId, + providerInstanceId: run.providerInstanceId, + cause, + }), + ), + ); + if (Option.isNone(sessionOption)) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Provider session ${providerThread.providerSessionId} is not active.`, + }); + } + yield* enforceCommandPolicy(command)( + commandPolicy.ensureInterrupt({ + commandId: command.commandId, + threadId: command.threadId, + providerInstanceId: run.providerInstanceId, + capabilities: sessionOption.value.providerSession.capabilities, + }), + ); + + const now = yield* DateTime.now; + const emitEvent = emit(events, command); + /* + * TODO(interrupt-hardening): before shipping, make these interrupt + * semantics explicit in tests and policy. + * + * Current behavior: + * - emit a `run_interrupt_request` item as user intent; + * - call the provider interrupt RPC; + * - keep the run active and continue ingesting provider chunks; + * - let RunExecutionService emit `run_interrupt_result` only if the + * provider later reports terminal status `interrupted`. + * + * Known scenarios we do not fully harden yet: + * - provider accepts interrupt, then emits more chunks before terminal; + * - provider accepts interrupt, then completes normally instead; + * - provider accepts interrupt but never terminalizes; + * - user queues, steers, or starts another message while the interrupted + * provider turn is still active. + * + * Likely policy: + * - queue should wait behind the still-active provider turn; + * - explicit steer may target the active turn if provider steering is + * supported; + * - starting a new root turn before provider terminalization should be + * an explicit policy decision because it can weaken native-item + * correlation. + */ + const interruptRequestItem: OrchestrationV2TurnItem = { + id: idAllocator.derive.runSignalTurnItem({ + runId: run.id, + signal: "interrupt-request", + }), + threadId: command.threadId, + runId: run.id, + nodeId: rootNode.id, + providerThreadId: providerThread.id, + providerTurnId: providerTurn.id, + nativeItemRef: null, + parentItemId: null, + ordinal: nextTurnItemOrdinal(projection), + status: "completed", + title: "Interrupt requested", + startedAt: now, + completedAt: now, + updatedAt: now, + type: "run_interrupt_request", + message: command.reason ?? "Interrupt requested", + }; + yield* emitEvent({ + type: "turn-item.updated", + threadId: command.threadId, + runId: run.id, + nodeId: rootNode.id, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: interruptRequestItem, + }); + yield* Ref.update(effects, (existing) => [ + ...existing, + { + id: `effect:${command.commandId}:provider-turn.interrupt:${providerTurn.id}`, + commandId: command.commandId, + threadId: command.threadId, + request: { + type: "provider-turn.interrupt", + providerSessionId, + providerThreadId: providerThread.id, + providerTurnId: providerTurn.id, + }, + } satisfies PendingOrchestrationEffectV2, + ]); + }); + + const dispatchCheckpointRollback = ( + command: Extract, + events: Ref.Ref>, + effects: Ref.Ref>, + ) => + Effect.gen(function* () { + const projection = yield* loadProjectionForCommand(command); + const providerThread = projection.providerThreads.find( + (candidate) => candidate.id === projection.thread.activeProviderThreadId, + ); + if (providerThread === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: "No active provider thread exists for rollback.", + }); + } + if (providerThread.providerSessionId === null) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Provider thread ${providerThread.id} has no provider session.`, + }); + } + + const modelSelection = projection.thread.modelSelection; + const capabilities = yield* providerAdapters.get(modelSelection.instanceId).pipe( + Effect.flatMap((adapter) => adapter.getCapabilities()), + Effect.mapError( + (cause) => + new OrchestratorProviderAdapterError({ + commandId: command.commandId, + providerInstanceId: modelSelection.instanceId, + cause, + }), + ), + ); + yield* enforceCommandPolicy(command)( + commandPolicy.ensureRollback({ + commandId: command.commandId, + threadId: command.threadId, + providerInstanceId: modelSelection.instanceId, + capabilities, + }), + ); + + const targetCheckpoint = projection.checkpoints.find( + (candidate) => candidate.id === command.checkpointId, + ); + if (targetCheckpoint === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Checkpoint ${command.checkpointId} was not found.`, + }); + } + const targetScope = projection.checkpointScopes.find( + (candidate) => candidate.id === targetCheckpoint.scopeId, + ); + if (targetScope === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Checkpoint scope ${targetCheckpoint.scopeId} was not found.`, + }); + } + if (targetScope.id !== command.scopeId) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Checkpoint ${command.checkpointId} belongs to scope ${targetScope.id}, not ${command.scopeId}.`, + }); + } + const targetOrdinal = targetCheckpoint.appRunOrdinal ?? 0; + if (targetOrdinal > 0) { + const targetRun = projection.runs.find((run) => run.ordinal === targetOrdinal); + const targetProviderTurn = + targetRun === undefined ? undefined : providerTurnForRun(projection, targetRun); + if (targetRun === undefined || targetProviderTurn === undefined) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Cannot roll back to checkpoint ${targetCheckpoint.id}: its provider turn is unavailable.`, + }); + } + if (targetProviderTurn.providerThreadId !== providerThread.id) { + return yield* new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Cannot roll back provider thread ${providerThread.id} to checkpoint ${targetCheckpoint.id}: target provider turn ${targetProviderTurn.id} belongs to provider thread ${targetProviderTurn.providerThreadId}.`, + }); + } + } + + const now = yield* DateTime.now; + yield* emit( + events, + command, + )({ + type: "checkpoint.rollback-requested", + threadId: command.threadId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: { + scopeId: targetScope.id, + checkpointId: targetCheckpoint.id, + requestedAt: now, + }, + }); + yield* Ref.update(effects, (existing) => [ + ...existing, + { + id: `effect:${command.commandId}:provider-thread.rollback:${providerThread.id}:${targetCheckpoint.id}`, + commandId: command.commandId, + threadId: command.threadId, + request: { + type: "provider-thread.rollback", + providerThreadId: providerThread.id, + checkpointId: targetCheckpoint.id, + scopeId: targetScope.id, + }, + } satisfies PendingOrchestrationEffectV2, + ]); + }); + + const finalizeAppOwnedSubagent = (childThreadId: ThreadId) => + Effect.gen(function* () { + const childProjection = yield* projectionStore.getThreadProjection(childThreadId); + const forkedFrom = childProjection.thread.forkedFrom; + if ( + childProjection.thread.lineage.relationshipToParent !== "subagent" || + childProjection.thread.lineage.parentThreadId === null || + forkedFrom?.type !== "node" + ) { + return; + } + const childRun = childProjection.runs[0]; + if (childRun === undefined) { + return; + } + const terminalStatus = delegatedTaskTerminalStatus(childRun.status); + if (terminalStatus === null) { + return; + } + + const parentThreadId = childProjection.thread.lineage.parentThreadId; + const parentProjection = yield* projectionStore.getThreadProjection(parentThreadId); + const task = parentProjection.subagents.find( + (candidate) => + candidate.id === forkedFrom.nodeId && + candidate.origin === "app_owned" && + candidate.childThreadId === childThreadId, + ); + if (task === undefined) { + return; + } + const existingResultTransfer = parentProjection.contextTransfers.find( + (transfer) => + transfer.type === "subagent_result" && + transfer.sourceThreadId === childThreadId && + transfer.targetThreadId === parentThreadId, + ); + if (existingResultTransfer !== undefined) { + return; + } + + const now = yield* DateTime.now; + const result = subagentResultForRun(childProjection, childRun); + const parentRun = + task.runId === null + ? undefined + : parentProjection.runs.find((candidate) => candidate.id === task.runId); + const parentNode = parentProjection.nodes.find((candidate) => candidate.id === task.id); + const parentTurnItem = parentProjection.turnItems.find( + (candidate) => candidate.type === "subagent" && candidate.subagentId === task.id, + ); + const updatedTask: OrchestrationV2Subagent = { + ...task, + providerThreadId: childRun.providerThreadId, + status: terminalStatus, + result: result.text, + completedAt: now, + updatedAt: now, + }; + const resultTransferId = yield* idAllocator.allocate.contextTransfer({ + sourceThreadId: childThreadId, + targetThreadId: parentThreadId, + type: "subagent_result", + }); + const childProviderThread = + childRun.providerThreadId === null + ? undefined + : childProjection.providerThreads.find( + (candidate) => candidate.id === childRun.providerThreadId, + ); + const parentProviderThread = + parentRun?.providerThreadId === null || parentRun?.providerThreadId === undefined + ? undefined + : parentProjection.providerThreads.find( + (candidate) => candidate.id === parentRun.providerThreadId, + ); + const resultHandoff: OrchestrationV2ContextHandoff | null = + parentRun === undefined || + childProviderThread === undefined || + parentProviderThread === undefined + ? null + : { + id: yield* idAllocator.allocate.contextHandoff({ + threadId: parentThreadId, + fromProviderInstanceId: childRun.providerInstanceId, + toProviderInstanceId: parentRun.providerInstanceId, + }), + transferId: resultTransferId, + threadId: parentThreadId, + targetRunId: parentRun.id, + fromProviderThreadIds: [childProviderThread.id], + toProviderThreadId: parentProviderThread.id, + coveredRunOrdinals: { + from: childRun.ordinal, + to: childRun.ordinal, + }, + strategy: "manual_context", + status: "ready", + summaryMessageId: result.messageId, + summaryText: result.text, + createdByProviderInstanceId: childRun.providerInstanceId, + createdAt: now, + updatedAt: now, + }; + const resultTransfer: OrchestrationV2ContextTransfer = { + id: resultTransferId, + type: "subagent_result", + sourceThreadId: childThreadId, + targetThreadId: parentThreadId, + sourcePoint: { + ...contextSourcePointForRun(childProjection, childRun), + ...(result.turnItemId === null ? {} : { turnItemId: result.turnItemId }), + }, + basePoint: null, + sourceProviderInstanceId: childRun.providerInstanceId, + targetProviderInstanceId: + parentRun?.providerInstanceId ?? parentProjection.thread.providerInstanceId, + targetRunId: parentRun?.id ?? null, + status: "consumed", + resolution: + resultHandoff === null + ? null + : { + strategy: "portable_context", + contextHandoffId: resultHandoff.id, + }, + createdBy: "system", + error: null, + createdAt: now, + updatedAt: now, + consumedAt: now, + }; + + yield* writeSystemEvents([ + { + type: "subagent.updated", + threadId: parentThreadId, + ...(task.runId === null ? {} : { runId: task.runId }), + nodeId: task.id, + driver: task.driver, + occurredAt: now, + payload: updatedTask, + }, + ...(parentNode === undefined + ? [] + : [ + { + type: "node.updated" as const, + threadId: parentThreadId, + ...(parentNode.runId === null ? {} : { runId: parentNode.runId }), + nodeId: parentNode.id, + driver: task.driver, + occurredAt: now, + payload: { + ...parentNode, + status: terminalStatus, + providerThreadId: childRun.providerThreadId, + completedAt: now, + }, + }, + ]), + ...(parentTurnItem === undefined + ? [] + : [ + { + type: "turn-item.updated" as const, + threadId: parentThreadId, + ...(parentTurnItem.runId === null ? {} : { runId: parentTurnItem.runId }), + ...(parentTurnItem.nodeId === null ? {} : { nodeId: parentTurnItem.nodeId }), + driver: task.driver, + occurredAt: now, + payload: { + ...parentTurnItem, + status: terminalStatus, + result: result.text, + completedAt: now, + updatedAt: now, + }, + }, + ]), + ...(resultHandoff === null + ? [] + : [ + { + type: "context-handoff.updated" as const, + threadId: parentThreadId, + ...(parentRun === undefined ? {} : { runId: parentRun.id }), + providerInstanceId: childRun.providerInstanceId, + occurredAt: now, + payload: resultHandoff, + }, + ]), + { + type: "context-transfer.created", + threadId: parentThreadId, + ...(parentRun === undefined ? {} : { runId: parentRun.id }), + providerInstanceId: childRun.providerInstanceId, + occurredAt: now, + payload: resultTransfer, + }, + ]); + }); + + const dispatchUnsupported = (command: OrchestrationV2Command) => + Effect.fail( + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + }), + ); + + const dispatchOnce = Effect.fn("orchestrationV2.dispatch.once")(function* ( + command: OrchestrationV2Command, + ): Effect.fn.Return< + { + readonly events: ReadonlyArray; + readonly effects: ReadonlyArray; + }, + OrchestratorV2Error + > { + yield* Effect.annotateCurrentSpan({ + "orchestration_v2.command_id": command.commandId, + "orchestration_v2.command_type": command.type, + "orchestration_v2.thread_id": commandThreadId(command), + }); + + const events = yield* Ref.make>([]); + const effects = yield* Ref.make>([]); + switch (command.type) { + case "thread.create": + yield* dispatchThreadCreate(command, events); + break; + case "thread.archive": + case "thread.unarchive": + case "thread.delete": + case "thread.metadata.update": + case "thread.runtime-mode.set": + case "thread.interaction-mode.set": + case "thread.model-selection.set": + case "provider.switch": + yield* dispatchThreadMutation(command, events, effects); + break; + case "provider-session.detach": + yield* dispatchProviderSessionDetach(command, events, effects); + break; + case "message.dispatch": + yield* dispatchMessage(command, events, effects); + break; + case "runtime-request.respond": + yield* dispatchRuntimeRequestRespond(command, events, effects); + break; + case "run.interrupt": + yield* dispatchRunInterrupt(command, events, effects); + break; + case "queued-message.promote-to-steer": + yield* dispatchQueuedMessagePromoteToSteer(command, events, effects); + break; + case "queued-run.reorder": + yield* dispatchQueuedRunReorder(command, events); + break; + case "checkpoint.rollback": + yield* dispatchCheckpointRollback(command, events, effects); + break; + case "thread.fork": + yield* dispatchThreadFork(command, events); + break; + case "thread.merge_back": + yield* dispatchThreadMergeBack(command, events); + break; + case "delegated_task.request": + yield* dispatchDelegatedTaskRequest(command, events, effects); + break; + default: + return yield* dispatchUnsupported(command); + } + return { + events: yield* Ref.get(events), + effects: yield* Ref.get(effects), + }; + }); + + const dispatchWithReceiptEffect = Effect.fn("orchestrationV2.dispatch.withReceipt")(function* ( + command: OrchestrationV2Command, + ): Effect.fn.Return { + yield* Effect.annotateCurrentSpan({ + "orchestration_v2.command_id": command.commandId, + "orchestration_v2.command_type": command.type, + "orchestration_v2.thread_id": commandThreadId(command), + }); + + const existingReceipt = yield* commandReceipts.getByCommandId(command.commandId).pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + + if (Option.isSome(existingReceipt)) { + const receipt = existingReceipt.value; + if (receipt.status === "rejected") { + return yield* new OrchestratorCommandPreviouslyRejectedError({ + commandId: command.commandId, + commandType: command.type, + detail: receipt.error ?? "Previously rejected.", + }); + } + const storedEvents = yield* eventSink.readByCommandId({ commandId: command.commandId }).pipe( + Stream.runCollect, + Effect.map((events): ReadonlyArray => Array.from(events)), + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + return { + sequence: receipt.resultSequence, + storedEvents, + } satisfies OrchestratorV2DispatchResult; + } + + const plan = yield* dispatchOnce(command).pipe( + Effect.flatMap((planned) => + planned.events.length > 0 + ? Effect.succeed(planned) + : Effect.fail( + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: "Command produced no domain events.", + }), + ), + ), + Effect.catch((cause) => + Effect.gen(function* () { + const rejectedAt = yield* DateTime.now; + yield* eventSink + .commitRejectedCommand({ + commandId: command.commandId, + threadId: commandThreadId(command), + commandType: command.type, + rejectedAt, + error: cause instanceof Error ? cause.message : String(cause), + }) + .pipe( + Effect.mapError( + (receiptCause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: receiptCause, + }), + ), + ); + return yield* cause; + }), + ), + ); + + const acceptedAt = plan.events.at(-1)?.occurredAt ?? (yield* DateTime.now); + const committed = yield* eventSink + .commitCommand({ + commandId: command.commandId, + threadId: commandThreadId(command), + commandType: command.type, + acceptedAt, + events: plan.events, + effects: plan.effects, + }) + .pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + + if (committed.receipt.status === "rejected") { + return yield* new OrchestratorCommandPreviouslyRejectedError({ + commandId: command.commandId, + commandType: command.type, + detail: committed.receipt.error ?? "Previously rejected.", + }); + } + + // The receipt and outbox rows are already committed here. Draining makes + // direct callers deterministic; the daemon remains responsible for work + // left behind by a crash between commit and this wake-up. + yield* effectWorker.drain(100).pipe( + Effect.mapError( + (cause) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause, + }), + ), + ); + + return { + sequence: committed.receipt.resultSequence, + storedEvents: committed.storedEvents, + } satisfies OrchestratorV2DispatchResult; + }); + + const dispatchWithReceipt = (command: OrchestrationV2Command) => + dispatchSemaphore.withPermit(dispatchWithReceiptEffect(command)); + + yield* eventSink.stream().pipe( + Stream.filter( + (stored) => + stored.event.type === "run.updated" && + (stored.event.payload.status === "completed" || + stored.event.payload.status === "interrupted" || + stored.event.payload.status === "failed" || + stored.event.payload.status === "cancelled" || + stored.event.payload.status === "rolled_back"), + ), + Stream.runForEach((stored) => + dispatchSemaphore + .withPermit( + finalizeAppOwnedSubagent(stored.event.threadId).pipe( + Effect.andThen(startNextQueuedRun(stored.event.threadId)), + ), + ) + .pipe(Effect.catchCause(() => Effect.void)), + ), + Effect.forkDetach, + ); + + return OrchestratorV2.of({ + dispatch: dispatchWithReceipt, + getThreadProjection: (threadId) => + projectionStore + .getThreadProjection(threadId) + .pipe(Effect.mapError((cause) => new OrchestratorProjectionError({ threadId, cause }))), + getThreadSnapshot: (threadId) => + projectionStore + .getThreadSnapshot(threadId) + .pipe(Effect.mapError((cause) => new OrchestratorProjectionError({ threadId, cause }))), + getShellSnapshot: () => + projectionStore.getShellSnapshot().pipe( + Effect.mapError( + (cause) => + new OrchestratorProjectionError({ + threadId: ThreadId.make("thread:shell"), + cause, + }), + ), + ), + getThreadEventSequence: (threadId) => + eventSink + .latestSequence({ threadId }) + .pipe(Effect.mapError((cause) => new OrchestratorProjectionError({ threadId, cause }))), + streamStoredEvents: eventSink.stream().pipe( + Stream.mapError( + (cause) => + new OrchestratorDomainEventStreamError({ + cause, + }), + ), + ), + streamStoredEventsFrom: (input) => + eventSink.stream(input).pipe( + Stream.mapError( + (cause) => + new OrchestratorDomainEventStreamError({ + cause, + }), + ), + ), + streamDomainEvents: eventSink.stream().pipe( + Stream.map((stored) => stored.event), + Stream.mapError( + (cause) => + new OrchestratorDomainEventStreamError({ + cause, + }), + ), + ), + }); +}); + +export const layer: Layer.Layer< + OrchestratorV2, + never, + | CheckpointServiceV2 + | CommandPolicyV2 + | CommandReceiptStoreV2 + | ContextHandoffServiceV2 + | EventSinkV2 + | OrchestrationEffectWorkerV2 + | IdAllocatorV2 + | ProviderAdapterRegistryV2 + | ProviderSessionManagerV2 + | ProviderSwitchServiceV2 + | ProjectionStoreV2 + | RuntimePolicyV2 + | ThreadForkServiceV2 +> = Layer.effect(OrchestratorV2, makeOrchestrator()); + +export const layerUnavailable: Layer.Layer = Layer.succeed( + OrchestratorV2, + OrchestratorV2.of({ + dispatch: (command) => + Effect.fail( + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: "Orchestration V2 live runtime is not configured.", + }), + ), + getThreadProjection: (threadId) => + Effect.fail( + new OrchestratorProjectionError({ + threadId, + cause: "Orchestration V2 live runtime is not configured.", + }), + ), + getThreadSnapshot: (threadId) => + Effect.fail( + new OrchestratorProjectionError({ + threadId, + cause: "Orchestration V2 live runtime is not configured.", + }), + ), + getShellSnapshot: () => + Effect.fail( + new OrchestratorProjectionError({ + threadId: ThreadId.make("thread:shell"), + cause: "Orchestration V2 live runtime is not configured.", + }), + ), + getThreadEventSequence: (threadId) => + Effect.fail( + new OrchestratorProjectionError({ + threadId, + cause: "Orchestration V2 live runtime is not configured.", + }), + ), + streamStoredEvents: Stream.fail( + new OrchestratorDomainEventStreamError({ + cause: "Orchestration V2 live runtime is not configured.", + }), + ), + streamStoredEventsFrom: () => + Stream.fail( + new OrchestratorDomainEventStreamError({ + cause: "Orchestration V2 live runtime is not configured.", + }), + ), + streamDomainEvents: Stream.fail( + new OrchestratorDomainEventStreamError({ + cause: "Orchestration V2 live runtime is not configured.", + }), + ), + } satisfies OrchestratorV2Shape), +); diff --git a/apps/server/src/orchestration-v2/ProjectionMaintenance.ts b/apps/server/src/orchestration-v2/ProjectionMaintenance.ts new file mode 100644 index 00000000000..5ea252d58e5 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProjectionMaintenance.ts @@ -0,0 +1,272 @@ +import { + type OrchestrationV2StoredEvent, + type OrchestrationV2ThreadProjection, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { EventStoreV2 } from "./EventStore.ts"; +import { + applyToProjection, + emptyProjection, + ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION, + ProjectionStoreV2, +} from "./ProjectionStore.ts"; + +export interface ProjectionVerificationV2 { + readonly valid: boolean; + readonly schemaVersion: number; + readonly expectedSequence: number; + readonly projectionSequence: number; + readonly differingThreadIds: ReadonlyArray; + readonly missingThreadIds: ReadonlyArray; + readonly unexpectedThreadIds: ReadonlyArray; +} + +export class ProjectionMaintenanceError extends Schema.TaggedErrorClass()( + "ProjectionMaintenanceError", + { + operation: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface ProjectionMaintenanceV2Shape { + readonly verify: Effect.Effect; + readonly rebuild: Effect.Effect; +} + +export class ProjectionMaintenanceV2 extends Context.Service< + ProjectionMaintenanceV2, + ProjectionMaintenanceV2Shape +>()("t3/orchestration-v2/ProjectionMaintenance/ProjectionMaintenanceV2") {} + +type ProjectionMetadataRow = { + readonly schema_version: number; + readonly last_sequence: number; +}; + +function sortedProjection(projection: OrchestrationV2ThreadProjection): unknown { + const sortById = (values: ReadonlyArray) => + values.toSorted((left, right) => left.id.localeCompare(right.id)); + return { + ...projection, + runs: sortById(projection.runs), + attempts: sortById(projection.attempts), + nodes: sortById(projection.nodes), + subagents: sortById(projection.subagents), + providerSessions: sortById(projection.providerSessions), + providerThreads: sortById(projection.providerThreads), + providerTurns: sortById(projection.providerTurns), + runtimeRequests: sortById(projection.runtimeRequests), + messages: sortById(projection.messages), + plans: sortById(projection.plans), + turnItems: sortById(projection.turnItems), + checkpointScopes: sortById(projection.checkpointScopes), + checkpoints: sortById(projection.checkpoints), + contextHandoffs: sortById(projection.contextHandoffs), + contextTransfers: sortById(projection.contextTransfers), + // This is a derived lineage view. Its source entities are compared above. + visibleTurnItems: [], + }; +} + +function stableStringify(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (typeof value === "object" && value !== null) { + const record = value as Record; + return `{${Object.keys(record) + .toSorted() + .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +export const layer: Layer.Layer< + ProjectionMaintenanceV2, + never, + EventStoreV2 | ProjectionStoreV2 | SqlClient.SqlClient +> = Layer.effect( + ProjectionMaintenanceV2, + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const eventStore = yield* EventStoreV2; + const projectionStore = yield* ProjectionStoreV2; + + const readAllEvents = Effect.gen(function* () { + const events: Array = []; + const pageSize = 500; + let afterSequence = 0; + while (true) { + const page = yield* eventStore.read({ afterSequence, limit: pageSize }).pipe( + Stream.runCollect, + Effect.map((chunk) => Array.from(chunk)), + ); + events.push(...page); + if (page.length < pageSize) { + break; + } + afterSequence = page.at(-1)?.sequence ?? afterSequence; + } + return events; + }); + + const expectedProjections = (events: ReadonlyArray) => { + const projections = new Map(); + for (const stored of events) { + const event = stored.event; + if (event.type === "thread.created") { + projections.set(event.threadId, emptyProjection(event)); + continue; + } + const current = projections.get(event.threadId); + if (current !== undefined) { + projections.set(event.threadId, applyToProjection(current, event)); + } + } + return projections; + }; + + const verify = Effect.gen(function* () { + const events = yield* readAllEvents; + const expected = expectedProjections(events); + const projectionRows = yield* sql<{ readonly thread_id: string }>` + SELECT thread_id + FROM orchestration_v2_projection_threads + ORDER BY thread_id ASC + `; + const actualIds = projectionRows.map((row) => ThreadId.make(row.thread_id)); + const expectedIds = [...expected.keys()].toSorted((left, right) => left.localeCompare(right)); + const actualSet = new Set(actualIds); + const expectedSet = new Set(expectedIds); + const missingThreadIds = expectedIds.filter((threadId) => !actualSet.has(threadId)); + const unexpectedThreadIds = actualIds.filter((threadId) => !expectedSet.has(threadId)); + const differingThreadIds: Array = []; + for (const threadId of expectedIds) { + if (!actualSet.has(threadId)) { + continue; + } + const actual = yield* projectionStore.getThreadProjection(threadId); + const expectedProjection = expected.get(threadId); + if ( + expectedProjection === undefined || + stableStringify(sortedProjection(actual)) !== + stableStringify(sortedProjection(expectedProjection)) + ) { + differingThreadIds.push(threadId); + } + } + const metadata = yield* sql` + SELECT schema_version, last_sequence + FROM orchestration_v2_projection_metadata + WHERE projection_name = 'thread-projections' + LIMIT 1 + `; + const expectedSequence = events.at(-1)?.sequence ?? 0; + const schemaVersion = metadata[0]?.schema_version ?? 0; + const projectionSequence = metadata[0]?.last_sequence ?? 0; + return { + valid: + schemaVersion === ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION && + projectionSequence === expectedSequence && + missingThreadIds.length === 0 && + unexpectedThreadIds.length === 0 && + differingThreadIds.length === 0, + schemaVersion, + expectedSequence, + projectionSequence, + differingThreadIds, + missingThreadIds, + unexpectedThreadIds, + } satisfies ProjectionVerificationV2; + }); + + const rebuild = Effect.gen(function* () { + const events = yield* readAllEvents; + yield* sql.withTransaction( + Effect.gen(function* () { + yield* sql`DELETE FROM orchestration_v2_projection_context_transfers`; + yield* sql`DELETE FROM orchestration_v2_projection_context_handoffs`; + yield* sql`DELETE FROM orchestration_v2_projection_checkpoints`; + yield* sql`DELETE FROM orchestration_v2_projection_checkpoint_scopes`; + yield* sql`DELETE FROM orchestration_v2_projection_turn_items`; + yield* sql`DELETE FROM orchestration_v2_projection_plans`; + yield* sql`DELETE FROM orchestration_v2_projection_messages`; + yield* sql`DELETE FROM orchestration_v2_projection_runtime_requests`; + yield* sql`DELETE FROM orchestration_v2_projection_provider_turns`; + yield* sql`DELETE FROM orchestration_v2_projection_provider_threads`; + yield* sql`DELETE FROM orchestration_v2_projection_provider_session_bindings`; + yield* sql`DELETE FROM orchestration_v2_projection_provider_sessions`; + yield* sql`DELETE FROM orchestration_v2_projection_subagents`; + yield* sql`DELETE FROM orchestration_v2_projection_nodes`; + yield* sql`DELETE FROM orchestration_v2_projection_run_attempts`; + yield* sql`DELETE FROM orchestration_v2_projection_runs`; + yield* sql`DELETE FROM orchestration_v2_projection_threads`; + yield* sql`DELETE FROM orchestration_v2_turn_item_positions`; + + for (const stored of events) { + yield* projectionStore.apply(stored.event); + if (stored.event.type === "turn-item.updated") { + yield* sql` + INSERT INTO orchestration_v2_turn_item_positions ( + thread_id, + turn_item_id, + ordinal + ) + VALUES ( + ${stored.event.threadId}, + ${stored.event.payload.id}, + ${stored.event.payload.ordinal} + ) + ON CONFLICT(thread_id, turn_item_id) DO UPDATE SET + ordinal = excluded.ordinal + `; + } + } + const now = DateTime.formatIso(yield* DateTime.now); + const lastSequence = events.at(-1)?.sequence ?? 0; + yield* sql` + INSERT INTO orchestration_v2_projection_metadata ( + projection_name, + schema_version, + last_sequence, + updated_at + ) + VALUES ( + 'thread-projections', + ${ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION}, + ${lastSequence}, + ${now} + ) + ON CONFLICT(projection_name) DO UPDATE SET + schema_version = excluded.schema_version, + last_sequence = excluded.last_sequence, + updated_at = excluded.updated_at + `; + }), + ); + return yield* verify; + }); + + const mapError = + (operation: string) => + (effect: Effect.Effect) => + effect.pipe( + Effect.mapError((cause) => new ProjectionMaintenanceError({ operation, cause })), + ); + + return ProjectionMaintenanceV2.of({ + verify: mapError("verify")(verify), + rebuild: mapError("rebuild")(rebuild), + }); + }), +); diff --git a/apps/server/src/orchestration-v2/ProjectionStore.test.ts b/apps/server/src/orchestration-v2/ProjectionStore.test.ts new file mode 100644 index 00000000000..a9a33fa9435 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProjectionStore.test.ts @@ -0,0 +1,869 @@ +import { assert, it } from "@effect/vitest"; +import { + EventId, + MessageId, + type ModelSelection, + NodeId, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + ProviderSessionId, + ProviderThreadId, + ProviderTurnId, + RunAttemptId, + RunId, + ThreadId, + TurnItemId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; +import { ProjectionStoreV2, layer as projectionStoreLayer } from "./ProjectionStore.ts"; + +const TestLayer = Layer.mergeAll( + projectionStoreLayer.pipe(Layer.provideMerge(SqlitePersistenceMemory)), + SqlitePersistenceMemory, +); +const modelSelection = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", +} satisfies ModelSelection; +const driver = ProviderDriverKind.make("codex"); +const providerInstanceId = modelSelection.instanceId; +const encodeUnknownJsonString = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); + +it.layer(TestLayer)("ProjectionStoreV2", (it) => { + it.effect("projects one shared provider session into multiple thread bindings", () => + Effect.gen(function* () { + const projectionStore = yield* ProjectionStoreV2; + const now = yield* DateTime.now; + const projectId = ProjectId.make("project:projection-shared-provider-session"); + const firstThreadId = ThreadId.make("thread:projection-shared-provider-session:first"); + const secondThreadId = ThreadId.make("thread:projection-shared-provider-session:second"); + const providerSessionId = ProviderSessionId.make( + "provider-session:projection-shared-provider-session", + ); + const makeThread = (threadId: ThreadId) => ({ + createdBy: "user" as const, + creationSource: "web" as const, + id: threadId, + projectId, + title: "Shared provider session", + providerInstanceId, + modelSelection, + runtimeMode: "full-access" as const, + interactionMode: "default" as const, + branch: null, + worktreePath: null, + activeProviderThreadId: null, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: threadId, + }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }); + const session = { + id: providerSessionId, + driver, + providerInstanceId, + status: "ready" as const, + cwd: "/workspace", + model: modelSelection.model, + capabilities: CodexProviderCapabilitiesV2, + createdAt: now, + updatedAt: now, + lastError: null, + }; + + yield* projectionStore.apply({ + id: EventId.make("event:projection-shared-provider-session:first-thread"), + type: "thread.created", + threadId: firstThreadId, + occurredAt: now, + payload: makeThread(firstThreadId), + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-shared-provider-session:second-thread"), + type: "thread.created", + threadId: secondThreadId, + occurredAt: now, + payload: makeThread(secondThreadId), + }); + for (const [threadId, suffix] of [ + [firstThreadId, "first"], + [secondThreadId, "second"], + ] as const) { + yield* projectionStore.apply({ + id: EventId.make(`event:projection-shared-provider-session:${suffix}-binding`), + type: "provider-session.attached", + threadId, + driver, + providerInstanceId, + occurredAt: now, + payload: session, + }); + } + + assert.deepEqual( + (yield* projectionStore.getThreadProjection(firstThreadId)).providerSessions.map( + (value) => value.id, + ), + [providerSessionId], + ); + assert.deepEqual( + (yield* projectionStore.getThreadProjection(secondThreadId)).providerSessions.map( + (value) => value.id, + ), + [providerSessionId], + ); + + yield* projectionStore.apply({ + id: EventId.make("event:projection-shared-provider-session:first-detached"), + type: "provider-session.detached", + threadId: firstThreadId, + driver, + providerInstanceId, + occurredAt: now, + payload: { providerSessionId, detachedAt: now }, + }); + + assert.lengthOf( + (yield* projectionStore.getThreadProjection(firstThreadId)).providerSessions, + 0, + ); + assert.lengthOf( + (yield* projectionStore.getThreadProjection(secondThreadId)).providerSessions, + 1, + ); + }), + ); + + it.effect("builds shell snapshots without decoding full turn item payloads", () => + Effect.gen(function* () { + const projectionStore = yield* ProjectionStoreV2; + const sql = yield* SqlClient.SqlClient; + const now = yield* DateTime.now; + const nowIso = DateTime.formatIso(now); + const threadId = ThreadId.make("thread:projection-shell-stale-item"); + const projectId = ProjectId.make("project:projection-shell"); + + yield* projectionStore.apply({ + id: EventId.make("event:projection-shell-thread-created"), + type: "thread.created", + threadId, + occurredAt: now, + payload: { + createdBy: "user", + creationSource: "web", + id: threadId, + projectId, + title: "Projection shell", + providerInstanceId, + modelSelection: modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: null, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: threadId, + }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }, + }); + + yield* sql` + INSERT INTO orchestration_v2_projection_turn_items ( + turn_item_id, + thread_id, + run_id, + node_id, + provider_thread_id, + provider_turn_id, + parent_item_id, + ordinal, + type, + status, + updated_at, + payload_json + ) + VALUES ( + ${"turn-item:stale-user-message"}, + ${threadId}, + ${null}, + ${null}, + ${null}, + ${null}, + ${null}, + ${0}, + ${"user_message"}, + ${"completed"}, + ${nowIso}, + ${encodeUnknownJsonString({ + id: "turn-item:stale-user-message", + threadId, + runId: null, + nodeId: null, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: 0, + status: "completed", + title: null, + startedAt: nowIso, + completedAt: nowIso, + updatedAt: nowIso, + type: "user_message", + messageId: "message:stale-user-message", + text: "stale user message", + attachments: [], + })} + ) + `; + + const shell = yield* projectionStore.getShellSnapshot(); + const fullProjectionExit = yield* Effect.exit(projectionStore.getThreadProjection(threadId)); + + assert.deepEqual( + shell.threads + .filter((thread) => thread.id === threadId) + .map((thread) => ({ + id: thread.id, + itemCount: thread.itemCount, + visibleItemCount: thread.visibleItemCount, + status: thread.status, + })), + [ + { + id: threadId, + itemCount: 1, + visibleItemCount: 1, + status: "idle", + }, + ], + ); + assert.equal(fullProjectionExit._tag, "Failure"); + }), + ); + + it.effect("removes rolled back runs from the active visible projection", () => + Effect.gen(function* () { + const projectionStore = yield* ProjectionStoreV2; + const now = yield* DateTime.now; + const threadId = ThreadId.make("thread:projection-rollback-prune"); + const projectId = ProjectId.make("project:projection-rollback-prune"); + const runId = RunId.make("run:projection-rollback-prune"); + const attemptId = RunAttemptId.make("attempt:projection-rollback-prune"); + const rootNodeId = NodeId.make("node:projection-rollback-prune:root"); + const assistantNodeId = NodeId.make("node:projection-rollback-prune:assistant"); + const providerThreadId = ProviderThreadId.make("provider-thread:projection-rollback-prune"); + const providerTurnId = ProviderTurnId.make("provider-turn:projection-rollback-prune"); + const userMessageId = MessageId.make("message:projection-rollback-prune:user"); + const assistantMessageId = MessageId.make("message:projection-rollback-prune:assistant"); + const userTurnItemId = TurnItemId.make("turn-item:projection-rollback-prune:user"); + const assistantTurnItemId = TurnItemId.make("turn-item:projection-rollback-prune:assistant"); + + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:thread-created"), + type: "thread.created", + threadId, + occurredAt: now, + payload: { + createdBy: "user", + creationSource: "web", + id: threadId, + projectId, + title: "Projection rollback prune", + providerInstanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: providerThreadId, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: threadId, + }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:provider-thread"), + type: "provider-thread.updated", + threadId, + driver, + occurredAt: now, + payload: { + id: providerThreadId, + driver, + providerInstanceId, + providerSessionId: null, + appThreadId: threadId, + ownerNodeId: null, + nativeThreadRef: null, + nativeConversationHeadRef: null, + status: "active", + firstRunOrdinal: 1, + lastRunOrdinal: 1, + handoffIds: [], + forkedFrom: null, + createdAt: now, + updatedAt: now, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:run-created"), + type: "run.created", + threadId, + runId, + nodeId: rootNodeId, + driver, + occurredAt: now, + payload: { + id: runId, + threadId, + ordinal: 1, + providerInstanceId, + modelSelection, + providerThreadId, + userMessageId, + rootNodeId, + activeAttemptId: attemptId, + status: "completed", + requestedAt: now, + startedAt: now, + completedAt: now, + checkpointId: null, + contextHandoffId: null, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:attempt-created"), + type: "run-attempt.created", + threadId, + runId, + nodeId: rootNodeId, + driver, + occurredAt: now, + payload: { + id: attemptId, + runId, + attemptOrdinal: 1, + rootNodeId, + providerInstanceId, + providerThreadId, + providerTurnId, + reason: "initial", + status: "completed", + startedAt: now, + completedAt: now, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:root-node"), + type: "node.updated", + threadId, + runId, + nodeId: rootNodeId, + driver, + occurredAt: now, + payload: { + id: rootNodeId, + threadId, + runId, + parentNodeId: null, + rootNodeId, + kind: "root_turn", + status: "completed", + countsForRun: true, + providerThreadId, + providerTurnId: null, + nativeItemRef: null, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: now, + completedAt: now, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:assistant-node"), + type: "node.updated", + threadId, + runId, + nodeId: assistantNodeId, + driver, + occurredAt: now, + payload: { + id: assistantNodeId, + threadId, + runId, + parentNodeId: rootNodeId, + rootNodeId, + kind: "assistant_message", + status: "completed", + countsForRun: false, + providerThreadId, + providerTurnId, + nativeItemRef: null, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: now, + completedAt: now, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:provider-turn"), + type: "provider-turn.updated", + threadId, + runId, + nodeId: rootNodeId, + driver, + occurredAt: now, + payload: { + id: providerTurnId, + providerThreadId, + nodeId: rootNodeId, + runAttemptId: attemptId, + nativeTurnRef: null, + ordinal: 1, + status: "completed", + startedAt: now, + completedAt: now, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:user-message"), + type: "message.updated", + threadId, + runId, + nodeId: rootNodeId, + driver, + occurredAt: now, + payload: { + createdBy: "user", + creationSource: "web", + id: userMessageId, + threadId, + runId, + nodeId: rootNodeId, + role: "user", + text: "rolled back user", + attachments: [], + streaming: false, + createdAt: now, + updatedAt: now, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:assistant-message"), + type: "message.updated", + threadId, + runId, + nodeId: assistantNodeId, + driver, + occurredAt: now, + payload: { + createdBy: "agent", + creationSource: "provider", + id: assistantMessageId, + threadId, + runId, + nodeId: assistantNodeId, + role: "assistant", + text: "rolled back assistant", + attachments: [], + streaming: false, + createdAt: now, + updatedAt: now, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:user-item"), + type: "turn-item.updated", + threadId, + runId, + nodeId: rootNodeId, + driver, + occurredAt: now, + payload: { + createdBy: "user", + creationSource: "web", + id: userTurnItemId, + threadId, + runId, + nodeId: rootNodeId, + providerThreadId, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: 100, + status: "completed", + title: null, + startedAt: now, + completedAt: now, + updatedAt: now, + type: "user_message", + messageId: userMessageId, + inputIntent: "turn_start", + text: "rolled back user", + attachments: [], + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:assistant-item"), + type: "turn-item.updated", + threadId, + runId, + nodeId: assistantNodeId, + driver, + occurredAt: now, + payload: { + id: assistantTurnItemId, + threadId, + runId, + nodeId: assistantNodeId, + providerThreadId, + providerTurnId, + nativeItemRef: null, + parentItemId: null, + ordinal: 101, + status: "completed", + title: null, + startedAt: now, + completedAt: now, + updatedAt: now, + type: "assistant_message", + messageId: assistantMessageId, + text: "rolled back assistant", + streaming: false, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:run-rolled-back"), + type: "run.updated", + threadId, + runId, + nodeId: rootNodeId, + driver, + occurredAt: now, + payload: { + id: runId, + threadId, + ordinal: 1, + providerInstanceId, + modelSelection, + providerThreadId, + userMessageId, + rootNodeId, + activeAttemptId: attemptId, + status: "rolled_back", + requestedAt: now, + startedAt: now, + completedAt: now, + checkpointId: null, + contextHandoffId: null, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-rollback-prune:root-rolled-back"), + type: "node.updated", + threadId, + runId, + nodeId: rootNodeId, + driver, + occurredAt: now, + payload: { + id: rootNodeId, + threadId, + runId, + parentNodeId: null, + rootNodeId, + kind: "root_turn", + status: "rolled_back", + countsForRun: true, + providerThreadId, + providerTurnId: null, + nativeItemRef: null, + runtimeRequestId: null, + checkpointScopeId: null, + startedAt: now, + completedAt: now, + }, + }); + + const projection = yield* projectionStore.getThreadProjection(threadId); + + assert.deepEqual( + projection.runs.map((run) => run.status), + ["rolled_back"], + ); + assert.deepEqual( + projection.nodes.map((node) => [node.id, node.status]), + [ + [assistantNodeId, "completed"], + [rootNodeId, "rolled_back"], + ], + ); + assert.lengthOf(projection.providerTurns, 1); + assert.lengthOf(projection.messages, 2); + assert.lengthOf(projection.turnItems, 2); + assert.lengthOf(projection.visibleTurnItems, 0); + }), + ); + + it.effect("keeps fork visible items stable after a source run is rolled back", () => + Effect.gen(function* () { + const projectionStore = yield* ProjectionStoreV2; + const now = yield* DateTime.now; + const projectId = ProjectId.make("project:projection-fork-source-rollback"); + const sourceThreadId = ThreadId.make("thread:projection-fork-source-rollback:source"); + const targetThreadId = ThreadId.make("thread:projection-fork-source-rollback:target"); + const sourceProviderThreadId = ProviderThreadId.make( + "provider-thread:projection-fork-source-rollback:source", + ); + const targetProviderThreadId = ProviderThreadId.make( + "provider-thread:projection-fork-source-rollback:target", + ); + const sourceRun1Id = RunId.make("run:projection-fork-source-rollback:source:1"); + const sourceRun2Id = RunId.make("run:projection-fork-source-rollback:source:2"); + const sourceRun1NodeId = NodeId.make("node:projection-fork-source-rollback:source:1"); + const sourceRun2NodeId = NodeId.make("node:projection-fork-source-rollback:source:2"); + + yield* projectionStore.apply({ + id: EventId.make("event:projection-fork-source-rollback:source-thread"), + type: "thread.created", + threadId: sourceThreadId, + occurredAt: now, + payload: { + createdBy: "user", + creationSource: "web", + id: sourceThreadId, + projectId, + title: "Projection fork source rollback source", + providerInstanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: sourceProviderThreadId, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: sourceThreadId, + }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-fork-source-rollback:target-thread"), + type: "thread.created", + threadId: targetThreadId, + occurredAt: now, + payload: { + createdBy: "user", + creationSource: "web", + id: targetThreadId, + projectId, + title: "Projection fork source rollback target", + providerInstanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: targetProviderThreadId, + lineage: { + parentThreadId: sourceThreadId, + relationshipToParent: "fork", + rootThreadId: sourceThreadId, + }, + forkedFrom: { + type: "run", + threadId: sourceThreadId, + runId: sourceRun2Id, + }, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }, + }); + + for (const [ordinal, runId, nodeId, promptText, responseText] of [ + [1, sourceRun1Id, sourceRun1NodeId, "source one", "one"], + [2, sourceRun2Id, sourceRun2NodeId, "source two", "two"], + ] as const) { + yield* projectionStore.apply({ + id: EventId.make(`event:projection-fork-source-rollback:run-${ordinal}`), + type: "run.created", + threadId: sourceThreadId, + runId, + nodeId, + driver, + occurredAt: now, + payload: { + id: runId, + threadId: sourceThreadId, + ordinal, + providerInstanceId, + modelSelection, + providerThreadId: sourceProviderThreadId, + userMessageId: MessageId.make( + `message:projection-fork-source-rollback:user:${ordinal}`, + ), + rootNodeId: nodeId, + activeAttemptId: null, + status: "completed", + requestedAt: now, + startedAt: now, + completedAt: now, + checkpointId: null, + contextHandoffId: null, + }, + }); + yield* projectionStore.apply({ + id: EventId.make(`event:projection-fork-source-rollback:user-item-${ordinal}`), + type: "turn-item.updated", + threadId: sourceThreadId, + runId, + nodeId, + driver, + occurredAt: now, + payload: { + createdBy: "user", + creationSource: "web", + id: TurnItemId.make(`turn-item:projection-fork-source-rollback:user:${ordinal}`), + threadId: sourceThreadId, + runId, + nodeId, + providerThreadId: sourceProviderThreadId, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: ordinal * 100, + status: "completed", + title: null, + startedAt: now, + completedAt: now, + updatedAt: now, + type: "user_message", + messageId: MessageId.make(`message:projection-fork-source-rollback:user:${ordinal}`), + inputIntent: "turn_start", + text: promptText, + attachments: [], + }, + }); + yield* projectionStore.apply({ + id: EventId.make(`event:projection-fork-source-rollback:assistant-item-${ordinal}`), + type: "turn-item.updated", + threadId: sourceThreadId, + runId, + nodeId, + driver, + occurredAt: now, + payload: { + id: TurnItemId.make(`turn-item:projection-fork-source-rollback:assistant:${ordinal}`), + threadId: sourceThreadId, + runId, + nodeId, + providerThreadId: sourceProviderThreadId, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: ordinal * 100 + 1, + status: "completed", + title: null, + startedAt: now, + completedAt: now, + updatedAt: now, + type: "assistant_message", + messageId: MessageId.make( + `message:projection-fork-source-rollback:assistant:${ordinal}`, + ), + text: responseText, + streaming: false, + }, + }); + } + + const targetBeforeRollback = yield* projectionStore.getThreadProjection(targetThreadId); + assert.deepEqual( + targetBeforeRollback.visibleTurnItems.map((row) => row.item.type), + ["user_message", "assistant_message", "user_message", "assistant_message", "fork"], + ); + + yield* projectionStore.apply({ + id: EventId.make("event:projection-fork-source-rollback:run-2-rolled-back"), + type: "run.updated", + threadId: sourceThreadId, + runId: sourceRun2Id, + nodeId: sourceRun2NodeId, + driver, + occurredAt: now, + payload: { + id: sourceRun2Id, + threadId: sourceThreadId, + ordinal: 2, + providerInstanceId, + modelSelection, + providerThreadId: sourceProviderThreadId, + userMessageId: MessageId.make("message:projection-fork-source-rollback:user:2"), + rootNodeId: sourceRun2NodeId, + activeAttemptId: null, + status: "rolled_back", + requestedAt: now, + startedAt: now, + completedAt: now, + checkpointId: null, + contextHandoffId: null, + }, + }); + + const targetAfterRollback = yield* projectionStore.getThreadProjection(targetThreadId); + assert.deepEqual( + targetAfterRollback.visibleTurnItems.map((row) => [ + row.visibility, + row.item.type, + row.item.type === "user_message" || row.item.type === "assistant_message" + ? row.item.text + : row.item.title, + ]), + [ + ["inherited", "user_message", "source one"], + ["inherited", "assistant_message", "one"], + ["inherited", "user_message", "source two"], + ["inherited", "assistant_message", "two"], + ["synthetic", "fork", "Forked from conversation"], + ], + ); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/ProjectionStore.ts b/apps/server/src/orchestration-v2/ProjectionStore.ts new file mode 100644 index 00000000000..0d4fcdce2e6 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProjectionStore.ts @@ -0,0 +1,2211 @@ +import type { + OrchestrationV2ConversationMessage, + OrchestrationV2DomainEvent, + OrchestrationV2ProjectedTurnItem, + OrchestrationV2ThreadShellSnapshot, + OrchestrationV2ShellThreadStatus, + OrchestrationV2ThreadShell, + OrchestrationV2ThreadProjection, + OrchestrationV2TurnItem, +} from "@t3tools/contracts"; +import { + OrchestrationV2AppThreadJson as OrchestrationV2AppThreadJsonSchema, + OrchestrationV2CheckpointJson as OrchestrationV2CheckpointJsonSchema, + OrchestrationV2CheckpointScopeJson as OrchestrationV2CheckpointScopeJsonSchema, + OrchestrationV2ContextHandoffJson as OrchestrationV2ContextHandoffJsonSchema, + OrchestrationV2ContextTransferJson as OrchestrationV2ContextTransferJsonSchema, + OrchestrationV2ConversationMessageJson as OrchestrationV2ConversationMessageJsonSchema, + OrchestrationV2ExecutionNodeJson as OrchestrationV2ExecutionNodeJsonSchema, + OrchestrationV2PlanArtifact as OrchestrationV2PlanArtifactSchema, + OrchestrationV2ProviderSessionJson as OrchestrationV2ProviderSessionJsonSchema, + OrchestrationV2ProviderThreadJson as OrchestrationV2ProviderThreadJsonSchema, + OrchestrationV2ProviderTurnJson as OrchestrationV2ProviderTurnJsonSchema, + OrchestrationV2RunAttemptJson as OrchestrationV2RunAttemptJsonSchema, + OrchestrationV2RunJson as OrchestrationV2RunJsonSchema, + OrchestrationV2RuntimeRequestJson as OrchestrationV2RuntimeRequestJsonSchema, + OrchestrationV2SubagentJson as OrchestrationV2SubagentJsonSchema, + OrchestrationV2TurnItemJson as OrchestrationV2TurnItemJsonSchema, + RunId, + ThreadId, + TurnItemId, +} from "@t3tools/contracts"; +import { + isOrchestrationV2SupersededInterrupt, + isOrchestrationV2TurnItemVisible, +} from "@t3tools/shared/orchestrationV2Timeline"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export class ProjectionStoreApplyEventError extends Schema.TaggedErrorClass()( + "ProjectionStoreApplyEventError", + { + eventType: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to apply orchestration projection event ${this.eventType}.`; + } +} + +export class ProjectionStoreSetupError extends Schema.TaggedErrorClass()( + "ProjectionStoreSetupError", + { + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return "Failed to initialize orchestration projection store."; + } +} + +export class ProjectionStoreThreadNotFoundError extends Schema.TaggedErrorClass()( + "ProjectionStoreThreadNotFoundError", + { + threadId: ThreadId, + }, +) { + override get message(): string { + return `No orchestration projection exists for thread ${this.threadId}.`; + } +} + +export class ProjectionStoreReadError extends Schema.TaggedErrorClass()( + "ProjectionStoreReadError", + { + threadId: ThreadId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to read orchestration projection for thread ${this.threadId}.`; + } +} + +export const ProjectionStoreV2Error = Schema.Union([ + ProjectionStoreSetupError, + ProjectionStoreApplyEventError, + ProjectionStoreThreadNotFoundError, + ProjectionStoreReadError, +]); +export type ProjectionStoreV2Error = typeof ProjectionStoreV2Error.Type; + +export interface ProjectionStoreV2Shape { + readonly apply: ( + event: OrchestrationV2DomainEvent, + ) => Effect.Effect; + readonly getShellSnapshot: () => Effect.Effect< + OrchestrationV2ThreadShellSnapshot, + ProjectionStoreV2Error + >; + readonly getThreadProjection: ( + threadId: ThreadId, + ) => Effect.Effect; + readonly getThreadSnapshot: (threadId: ThreadId) => Effect.Effect< + { + readonly schemaVersion: number; + readonly snapshotSequence: number; + readonly projection: OrchestrationV2ThreadProjection; + }, + ProjectionStoreV2Error + >; +} + +export class ProjectionStoreV2 extends Context.Service()( + "t3/orchestration-v2/ProjectionStore/ProjectionStoreV2", +) {} + +export const ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION = 2; + +function upsertById(items: ReadonlyArray, next: T): Array { + const index = items.findIndex((item) => item.id === next.id); + if (index === -1) { + return [...items, next]; + } + + const updated = [...items]; + updated[index] = next; + return updated; +} + +export function emptyProjection( + event: Extract, +): OrchestrationV2ThreadProjection { + return { + thread: event.payload, + runs: [], + attempts: [], + nodes: [], + subagents: [], + providerSessions: [], + providerThreads: [], + providerTurns: [], + runtimeRequests: [], + messages: [], + plans: [], + turnItems: [], + checkpointScopes: [], + checkpoints: [], + contextHandoffs: [], + contextTransfers: [], + visibleTurnItems: [], + updatedAt: event.occurredAt, + }; +} + +export function applyToProjection( + projection: OrchestrationV2ThreadProjection, + event: OrchestrationV2DomainEvent, +): OrchestrationV2ThreadProjection { + const base = { + ...projection, + thread: { + ...projection.thread, + updatedAt: event.occurredAt, + }, + updatedAt: event.occurredAt, + }; + + switch (event.type) { + case "thread.created": + case "thread.archived": + case "thread.unarchived": + case "thread.deleted": + case "thread.metadata-updated": + case "thread.runtime-mode-updated": + case "thread.interaction-mode-updated": + case "thread.model-selection-updated": + case "thread.provider-switched": + return { + ...base, + thread: event.payload, + }; + case "run.created": + case "run.updated": + return withLocalVisibleTurnItems({ + ...base, + runs: upsertById(base.runs, event.payload), + }); + case "run-attempt.created": + case "run-attempt.updated": + return withLocalVisibleTurnItems({ + ...base, + attempts: upsertById(base.attempts, event.payload), + }); + case "node.updated": + return { + ...base, + nodes: upsertById(base.nodes, event.payload), + }; + case "subagent.updated": + return { + ...base, + subagents: upsertById(base.subagents, event.payload), + }; + case "provider-session.attached": + case "provider-session.updated": + return { + ...base, + providerSessions: upsertById(base.providerSessions, event.payload), + }; + case "provider-session.detached": + return { + ...base, + providerSessions: base.providerSessions.filter( + (session) => session.id !== event.payload.providerSessionId, + ), + }; + case "provider-thread.updated": + return { + ...base, + thread: + event.payload.appThreadId === base.thread.id + ? { + ...base.thread, + activeProviderThreadId: event.payload.id, + } + : base.thread, + providerThreads: upsertById(base.providerThreads, event.payload), + }; + case "provider-turn.updated": + return { + ...base, + providerTurns: upsertById(base.providerTurns, event.payload), + }; + case "runtime-request.updated": + return { + ...base, + runtimeRequests: upsertById(base.runtimeRequests, event.payload), + }; + case "message.updated": + return { + ...base, + messages: upsertById(base.messages, event.payload), + }; + case "turn-item.updated": + return withLocalVisibleTurnItems({ + ...base, + turnItems: upsertById(base.turnItems, event.payload), + }); + case "plan.updated": + return { + ...base, + plans: upsertById(base.plans, event.payload), + }; + case "checkpoint-scope.created": + return { + ...base, + checkpointScopes: upsertById(base.checkpointScopes, event.payload), + }; + case "checkpoint.captured": + return { + ...base, + checkpoints: upsertById(base.checkpoints, event.payload), + }; + case "checkpoint.rollback-requested": + return base; + case "context-handoff.updated": + return { + ...base, + contextHandoffs: upsertById(base.contextHandoffs, event.payload), + }; + case "context-transfer.created": + case "context-transfer.updated": + return { + ...base, + contextTransfers: upsertById(base.contextTransfers, event.payload), + }; + } +} + +type PayloadRow = { + readonly payload_json: string; +}; + +type ShellThreadRow = { + readonly thread_id: string; + readonly payload_json: string; + readonly latest_run_id: string | null; + readonly latest_run_status: string | null; + readonly active_run_id: string | null; + readonly pending_request_payload_json: string | null; + readonly latest_message_payload_json: string | null; + readonly latest_user_message_at: string | null; + readonly has_actionable_proposed_plan: number; + readonly item_count: number; +}; + +type ShellRunRow = { + readonly thread_id: string; + readonly run_id: string; + readonly ordinal: number; +}; + +type ShellRunItemCountRow = { + readonly thread_id: string; + readonly run_id: string; + readonly item_count: number; +}; + +const encodeThreadPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2AppThreadJsonSchema), +); +const encodeRunPayload = Schema.encodeEffect(Schema.fromJsonString(OrchestrationV2RunJsonSchema)); +const encodeRunAttemptPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2RunAttemptJsonSchema), +); +const encodeNodePayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2ExecutionNodeJsonSchema), +); +const encodeSubagentPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2SubagentJsonSchema), +); +const encodeProviderSessionPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2ProviderSessionJsonSchema), +); +const encodeProviderThreadPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2ProviderThreadJsonSchema), +); +const encodeProviderTurnPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2ProviderTurnJsonSchema), +); +const encodeRuntimeRequestPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2RuntimeRequestJsonSchema), +); +const encodeMessagePayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2ConversationMessageJsonSchema), +); +const encodePlanPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2PlanArtifactSchema), +); +const encodeTurnItemPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2TurnItemJsonSchema), +); +const encodeCheckpointScopePayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2CheckpointScopeJsonSchema), +); +const encodeCheckpointPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2CheckpointJsonSchema), +); +const encodeContextHandoffPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2ContextHandoffJsonSchema), +); +const encodeContextTransferPayload = Schema.encodeEffect( + Schema.fromJsonString(OrchestrationV2ContextTransferJsonSchema), +); + +const decodeThreadPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2AppThreadJsonSchema))(json); +const decodeRunPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2RunJsonSchema))(json); +const decodeRunAttemptPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2RunAttemptJsonSchema))(json); +const decodeNodePayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2ExecutionNodeJsonSchema))(json); +const decodeSubagentPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2SubagentJsonSchema))(json); +const decodeProviderSessionPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2ProviderSessionJsonSchema))(json); +const decodeProviderThreadPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2ProviderThreadJsonSchema))(json); +const decodeProviderTurnPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2ProviderTurnJsonSchema))(json); +const decodeRuntimeRequestPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2RuntimeRequestJsonSchema))(json); +const decodeMessagePayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2ConversationMessageJsonSchema))( + json, + ); +const decodePlanPayload = (json: string) => + Schema.decodeUnknownEffect(OrchestrationV2PlanArtifactSchema)(parseEncodedPayload(json)); +const decodeTurnItemPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2TurnItemJsonSchema))(json); +const decodeCheckpointScopePayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2CheckpointScopeJsonSchema))(json); +const decodeCheckpointPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2CheckpointJsonSchema))(json); +const decodeContextHandoffPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2ContextHandoffJsonSchema))(json); +const decodeContextTransferPayload = (json: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(OrchestrationV2ContextTransferJsonSchema))(json); + +function parseEncodedPayload(json: string): Record { + return JSON.parse(json) as Record; +} + +function stringField(payload: Record, field: string): string { + const value = payload[field]; + return typeof value === "string" ? value : String(value); +} + +function nullableStringField(payload: Record, field: string): string | null { + const value = payload[field]; + if (value === null || value === undefined) { + return null; + } + return typeof value === "string" ? value : String(value); +} + +function booleanInt(value: boolean): 0 | 1 { + return value ? 1 : 0; +} + +const decodeRows = + (decode: (json: string) => Effect.Effect, threadId: ThreadId) => + (rows: ReadonlyArray): Effect.Effect, ProjectionStoreReadError> => + Effect.forEach(rows, (row) => decode(row.payload_json)).pipe( + Effect.mapError( + (cause) => + new ProjectionStoreReadError({ + threadId, + cause, + }), + ), + ); + +function messageIdForTurnItem(item: OrchestrationV2TurnItem): string | null { + switch (item.type) { + case "user_message": + case "assistant_message": + return item.messageId; + default: + return null; + } +} + +function sortMessagesByTurnItemOrder( + messages: ReadonlyArray, + turnItems: ReadonlyArray, +): Array { + const messageOrdinals = new Map(); + for (const turnItem of turnItems) { + const messageId = messageIdForTurnItem(turnItem); + if (messageId === null) { + continue; + } + const existing = messageOrdinals.get(messageId); + if (existing === undefined || turnItem.ordinal < existing) { + messageOrdinals.set(messageId, turnItem.ordinal); + } + } + + return messages.toSorted((left, right) => { + const leftOrdinal = messageOrdinals.get(left.id) ?? Number.MAX_SAFE_INTEGER; + const rightOrdinal = messageOrdinals.get(right.id) ?? Number.MAX_SAFE_INTEGER; + if (leftOrdinal !== rightOrdinal) { + return leftOrdinal - rightOrdinal; + } + + const leftCreatedAt = DateTime.toEpochMillis(left.createdAt); + const rightCreatedAt = DateTime.toEpochMillis(right.createdAt); + if (leftCreatedAt !== rightCreatedAt) { + return leftCreatedAt - rightCreatedAt; + } + + return left.id.localeCompare(right.id); + }); +} + +function activeLocalTurnItems( + projection: OrchestrationV2ThreadProjection, +): Array { + return projection.turnItems + .filter((item) => + isOrchestrationV2TurnItemVisible({ + item, + runs: projection.runs, + attempts: projection.attempts, + }), + ) + .map((item, position) => ({ + position, + visibility: "local" as const, + sourceThreadId: item.threadId, + sourceItemId: item.id, + item, + })); +} + +function localVisibleTurnItems( + projection: OrchestrationV2ThreadProjection, +): Array { + return activeLocalTurnItems(projection); +} + +function inheritedVisibleTurnItemsFromLocalItems( + items: ReadonlyArray, +): Array> { + return items.map((item) => ({ + visibility: "inherited" as const, + sourceThreadId: item.threadId, + sourceItemId: item.id, + item, + })); +} + +function withLocalVisibleTurnItems( + projection: OrchestrationV2ThreadProjection, +): OrchestrationV2ThreadProjection { + return { + ...projection, + visibleTurnItems: localVisibleTurnItems(projection), + }; +} + +function renumberVisibleTurnItems( + rows: ReadonlyArray>, +): Array { + return rows.map((row, position) => ({ ...row, position })); +} + +function makeForkMarkerTurnItem(input: { + readonly targetProjection: OrchestrationV2ThreadProjection; + readonly sourceThreadId: ThreadId; + readonly sourceRunId: NonNullable; +}): OrchestrationV2TurnItem { + const createdAt = input.targetProjection.thread.createdAt; + return { + id: TurnItemId.make(`turn-item:fork:${input.targetProjection.thread.id}`), + threadId: input.targetProjection.thread.id, + runId: null, + nodeId: null, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: 0, + status: "completed", + title: "Forked from conversation", + startedAt: null, + completedAt: createdAt, + updatedAt: createdAt, + type: "fork", + source: { type: "run", threadId: input.sourceThreadId, runId: input.sourceRunId }, + targetThreadId: input.targetProjection.thread.id, + }; +} + +function visibleTurnItemsThroughRun(input: { + readonly sourceProjection: OrchestrationV2ThreadProjection; + readonly sourceRunId: NonNullable; +}): Array> { + const sourceRun = input.sourceProjection.runs.find((run) => run.id === input.sourceRunId); + if (sourceRun === undefined) { + return []; + } + + const runOrdinalById = new Map(input.sourceProjection.runs.map((run) => [run.id, run.ordinal])); + const inheritedPrefix = input.sourceProjection.visibleTurnItems + .filter( + (row) => row.item.threadId !== input.sourceProjection.thread.id || row.item.type === "fork", + ) + .map((row) => ({ + visibility: "inherited" as const, + sourceThreadId: row.sourceThreadId, + sourceItemId: row.sourceItemId, + item: row.item, + })); + const localPrefix = inheritedVisibleTurnItemsFromLocalItems( + input.sourceProjection.turnItems.filter((item) => { + if ( + isOrchestrationV2SupersededInterrupt({ + item, + attempts: input.sourceProjection.attempts, + }) + ) { + return false; + } + if (item.runId === null) { + return false; + } + const ordinal = runOrdinalById.get(item.runId); + return ordinal !== undefined && ordinal <= sourceRun.ordinal; + }), + ); + + return [...inheritedPrefix, ...localPrefix]; +} + +function buildVisibleTurnItems(input: { + readonly projection: OrchestrationV2ThreadProjection; + readonly sourceProjection: OrchestrationV2ThreadProjection | null; +}): Array { + const forkedFrom = input.projection.thread.forkedFrom; + if (forkedFrom?.type !== "run" || input.sourceProjection === null) { + return localVisibleTurnItems(input.projection); + } + + const inherited = visibleTurnItemsThroughRun({ + sourceProjection: input.sourceProjection, + sourceRunId: forkedFrom.runId, + }); + const markerItem = makeForkMarkerTurnItem({ + targetProjection: input.projection, + sourceThreadId: forkedFrom.threadId, + sourceRunId: forkedFrom.runId, + }); + const local = activeLocalTurnItems(input.projection).map((row) => ({ + visibility: "local" as const, + sourceThreadId: row.sourceThreadId, + sourceItemId: row.sourceItemId, + item: row.item, + })); + + return renumberVisibleTurnItems([ + ...inherited, + { + visibility: "synthetic", + sourceThreadId: forkedFrom.threadId, + sourceItemId: markerItem.id, + item: markerItem, + }, + ...local, + ]); +} + +export function threadShellFromProjection( + projection: OrchestrationV2ThreadProjection, +): OrchestrationV2ThreadShell { + const latestRun = projection.runs.at(-1) ?? null; + const activeRun = + projection.runs + .filter(isBlockingRunForShell) + .toSorted((left, right) => right.ordinal - left.ordinal)[0] ?? null; + const pendingRuntimeRequest = + projection.runtimeRequests + .filter((request) => request.status === "pending") + .toSorted( + (left, right) => + DateTime.toEpochMillis(right.createdAt) - DateTime.toEpochMillis(left.createdAt), + )[0] ?? null; + const latestVisibleMessage = + projection.messages.toSorted( + (left, right) => + DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt), + )[0] ?? null; + const latestUserMessage = + projection.messages + .filter((message) => message.role === "user") + .toSorted( + (left, right) => + DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt), + )[0] ?? null; + return { + createdBy: projection.thread.createdBy, + creationSource: projection.thread.creationSource, + id: projection.thread.id, + projectId: projection.thread.projectId, + title: projection.thread.title, + providerInstanceId: projection.thread.providerInstanceId, + modelSelection: projection.thread.modelSelection, + runtimeMode: projection.thread.runtimeMode, + interactionMode: projection.thread.interactionMode, + branch: projection.thread.branch, + worktreePath: projection.thread.worktreePath, + lineage: projection.thread.lineage, + forkedFrom: projection.thread.forkedFrom, + activeProviderThreadId: projection.thread.activeProviderThreadId, + latestRunId: latestRun?.id ?? null, + activeRunId: activeRun?.id ?? null, + status: latestRun?.status ?? "idle", + pendingRuntimeRequest: + pendingRuntimeRequest === null + ? null + : { + id: pendingRuntimeRequest.id, + kind: pendingRuntimeRequest.kind, + createdAt: pendingRuntimeRequest.createdAt, + }, + latestVisibleMessage: + latestVisibleMessage === null + ? null + : { + id: latestVisibleMessage.id, + role: latestVisibleMessage.role, + text: latestVisibleMessage.text, + updatedAt: latestVisibleMessage.updatedAt, + }, + latestUserMessageAt: latestUserMessage?.updatedAt ?? null, + hasActionableProposedPlan: projection.plans.some( + (plan) => plan.kind === "proposed_plan" && plan.status === "active", + ), + itemCount: activeLocalTurnItems(projection).length, + visibleItemCount: projection.visibleTurnItems.length, + createdAt: projection.thread.createdAt, + updatedAt: projection.updatedAt, + archivedAt: projection.thread.archivedAt, + deletedAt: projection.thread.deletedAt, + }; +} + +function isBlockingRunForShell(run: OrchestrationV2ThreadProjection["runs"][number]): boolean { + return run.status === "starting" || run.status === "running" || run.status === "waiting"; +} + +type ShellThreadState = { + readonly thread: OrchestrationV2ThreadProjection["thread"]; + readonly latestRunId: RunId | null; + readonly latestRunStatus: OrchestrationV2ShellThreadStatus; + readonly activeRunId: RunId | null; + readonly pendingRuntimeRequest: OrchestrationV2ThreadProjection["runtimeRequests"][number] | null; + readonly latestVisibleMessage: OrchestrationV2ConversationMessage | null; + readonly latestUserMessageAt: DateTime.Utc | null; + readonly hasActionableProposedPlan: boolean; + readonly itemCount: number; + readonly updatedAt: OrchestrationV2ThreadProjection["updatedAt"]; + readonly runOrdinalById: ReadonlyMap; + readonly itemCountByRunId: ReadonlyMap; +}; + +function shellStatusFromStoredRunStatus(status: string | null): OrchestrationV2ShellThreadStatus { + switch (status) { + case null: + return "idle"; + case "queued": + case "starting": + case "running": + case "waiting": + case "completed": + case "interrupted": + case "failed": + case "cancelled": + case "rolled_back": + return status; + default: + return "failed"; + } +} + +function itemCountThroughRun(input: { + readonly state: ShellThreadState; + readonly runId: RunId; +}): number { + const runOrdinal = input.state.runOrdinalById.get(input.runId); + if (runOrdinal === undefined) { + return 0; + } + + let count = 0; + for (const [runId, itemCount] of input.state.itemCountByRunId) { + const itemRunOrdinal = input.state.runOrdinalById.get(runId); + if (itemRunOrdinal !== undefined && itemRunOrdinal <= runOrdinal) { + count += itemCount; + } + } + return count; +} + +function visibleItemCountForShell(input: { + readonly threadId: ThreadId; + readonly statesByThreadId: ReadonlyMap; + readonly seenThreadIds?: ReadonlySet; +}): number { + const state = input.statesByThreadId.get(input.threadId); + if (state === undefined) { + return 0; + } + + const forkedFrom = state.thread.forkedFrom; + if (forkedFrom?.type !== "run") { + return state.itemCount; + } + + const seenThreadIds = input.seenThreadIds ?? new Set(); + if (seenThreadIds.has(state.thread.id)) { + return state.itemCount; + } + + const sourceState = input.statesByThreadId.get(forkedFrom.threadId); + if (sourceState === undefined) { + return state.itemCount; + } + + const sourceForkedFrom = sourceState.thread.forkedFrom; + const inheritedPrefixCount = + sourceForkedFrom?.type === "run" + ? visibleItemCountForShell({ + threadId: sourceState.thread.id, + statesByThreadId: input.statesByThreadId, + seenThreadIds: new Set([...seenThreadIds, state.thread.id]), + }) - sourceState.itemCount + : 0; + + return ( + inheritedPrefixCount + + itemCountThroughRun({ state: sourceState, runId: forkedFrom.runId }) + + 1 + + state.itemCount + ); +} + +function shellFromState(input: { + readonly state: ShellThreadState; + readonly visibleItemCount: number; +}): OrchestrationV2ThreadShell { + return { + createdBy: input.state.thread.createdBy, + creationSource: input.state.thread.creationSource, + id: input.state.thread.id, + projectId: input.state.thread.projectId, + title: input.state.thread.title, + providerInstanceId: input.state.thread.providerInstanceId, + modelSelection: input.state.thread.modelSelection, + runtimeMode: input.state.thread.runtimeMode, + interactionMode: input.state.thread.interactionMode, + branch: input.state.thread.branch, + worktreePath: input.state.thread.worktreePath, + lineage: input.state.thread.lineage, + forkedFrom: input.state.thread.forkedFrom, + activeProviderThreadId: input.state.thread.activeProviderThreadId, + latestRunId: input.state.latestRunId, + activeRunId: input.state.activeRunId, + status: input.state.latestRunStatus, + pendingRuntimeRequest: + input.state.pendingRuntimeRequest === null + ? null + : { + id: input.state.pendingRuntimeRequest.id, + kind: input.state.pendingRuntimeRequest.kind, + createdAt: input.state.pendingRuntimeRequest.createdAt, + }, + latestVisibleMessage: + input.state.latestVisibleMessage === null + ? null + : { + id: input.state.latestVisibleMessage.id, + role: input.state.latestVisibleMessage.role, + text: input.state.latestVisibleMessage.text, + updatedAt: input.state.latestVisibleMessage.updatedAt, + }, + latestUserMessageAt: input.state.latestUserMessageAt, + hasActionableProposedPlan: input.state.hasActionableProposedPlan, + itemCount: input.state.itemCount, + visibleItemCount: input.visibleItemCount, + createdAt: input.state.thread.createdAt, + updatedAt: input.state.updatedAt, + archivedAt: input.state.thread.archivedAt, + deletedAt: input.state.thread.deletedAt, + }; +} + +export const layer: Layer.Layer = Layer.effect( + ProjectionStoreV2, + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + const apply: ProjectionStoreV2Shape["apply"] = (event) => + Effect.gen(function* () { + switch (event.type) { + case "thread.created": + case "thread.archived": + case "thread.unarchived": + case "thread.deleted": + case "thread.metadata-updated": + case "thread.runtime-mode-updated": + case "thread.interaction-mode-updated": + case "thread.model-selection-updated": + case "thread.provider-switched": { + const payloadJson = yield* encodeThreadPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_threads ( + thread_id, + project_id, + title, + default_provider, + provider_instance_id, + runtime_mode, + interaction_mode, + active_provider_thread_id, + created_at, + updated_at, + archived_at, + deleted_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.projectId}, + ${event.payload.title}, + ${event.payload.providerInstanceId}, + ${event.payload.providerInstanceId}, + ${event.payload.runtimeMode}, + ${event.payload.interactionMode}, + ${event.payload.activeProviderThreadId}, + ${stringField(payload, "createdAt")}, + ${stringField(payload, "updatedAt")}, + ${nullableStringField(payload, "archivedAt")}, + ${nullableStringField(payload, "deletedAt")}, + ${payloadJson} + ) + ON CONFLICT(thread_id) + DO UPDATE SET + project_id = excluded.project_id, + title = excluded.title, + default_provider = excluded.default_provider, + provider_instance_id = excluded.provider_instance_id, + runtime_mode = excluded.runtime_mode, + interaction_mode = excluded.interaction_mode, + active_provider_thread_id = excluded.active_provider_thread_id, + created_at = excluded.created_at, + updated_at = excluded.updated_at, + archived_at = excluded.archived_at, + deleted_at = excluded.deleted_at, + payload_json = excluded.payload_json + `; + break; + } + case "run.created": + case "run.updated": { + const payloadJson = yield* encodeRunPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_runs ( + run_id, + thread_id, + ordinal, + provider, + provider_instance_id, + provider_thread_id, + status, + requested_at, + completed_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.threadId}, + ${event.payload.ordinal}, + ${event.payload.providerInstanceId}, + ${event.payload.providerInstanceId}, + ${event.payload.providerThreadId}, + ${event.payload.status}, + ${stringField(payload, "requestedAt")}, + ${nullableStringField(payload, "completedAt")}, + ${payloadJson} + ) + ON CONFLICT(run_id) + DO UPDATE SET + thread_id = excluded.thread_id, + ordinal = excluded.ordinal, + provider = excluded.provider, + provider_instance_id = excluded.provider_instance_id, + provider_thread_id = excluded.provider_thread_id, + status = excluded.status, + requested_at = excluded.requested_at, + completed_at = excluded.completed_at, + payload_json = excluded.payload_json + `; + break; + } + case "run-attempt.created": + case "run-attempt.updated": { + const payloadJson = yield* encodeRunAttemptPayload(event.payload); + yield* sql` + INSERT INTO orchestration_v2_projection_run_attempts ( + attempt_id, + thread_id, + run_id, + attempt_ordinal, + root_node_id, + provider, + provider_instance_id, + provider_thread_id, + provider_turn_id, + status, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.threadId}, + ${event.payload.runId}, + ${event.payload.attemptOrdinal}, + ${event.payload.rootNodeId}, + ${event.payload.providerInstanceId}, + ${event.payload.providerInstanceId}, + ${event.payload.providerThreadId}, + ${event.payload.providerTurnId}, + ${event.payload.status}, + ${payloadJson} + ) + ON CONFLICT(attempt_id) + DO UPDATE SET + thread_id = excluded.thread_id, + run_id = excluded.run_id, + attempt_ordinal = excluded.attempt_ordinal, + root_node_id = excluded.root_node_id, + provider = excluded.provider, + provider_instance_id = excluded.provider_instance_id, + provider_thread_id = excluded.provider_thread_id, + provider_turn_id = excluded.provider_turn_id, + status = excluded.status, + payload_json = excluded.payload_json + `; + break; + } + case "node.updated": { + const payloadJson = yield* encodeNodePayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_nodes ( + node_id, + thread_id, + run_id, + parent_node_id, + root_node_id, + kind, + status, + provider_thread_id, + provider_turn_id, + runtime_request_id, + checkpoint_scope_id, + started_at, + completed_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.threadId}, + ${event.payload.runId}, + ${event.payload.parentNodeId}, + ${event.payload.rootNodeId}, + ${event.payload.kind}, + ${event.payload.status}, + ${event.payload.providerThreadId}, + ${event.payload.providerTurnId}, + ${event.payload.runtimeRequestId}, + ${event.payload.checkpointScopeId}, + ${nullableStringField(payload, "startedAt")}, + ${nullableStringField(payload, "completedAt")}, + ${payloadJson} + ) + ON CONFLICT(node_id) + DO UPDATE SET + thread_id = excluded.thread_id, + run_id = excluded.run_id, + parent_node_id = excluded.parent_node_id, + root_node_id = excluded.root_node_id, + kind = excluded.kind, + status = excluded.status, + provider_thread_id = excluded.provider_thread_id, + provider_turn_id = excluded.provider_turn_id, + runtime_request_id = excluded.runtime_request_id, + checkpoint_scope_id = excluded.checkpoint_scope_id, + started_at = excluded.started_at, + completed_at = excluded.completed_at, + payload_json = excluded.payload_json + `; + break; + } + case "subagent.updated": { + const payloadJson = yield* encodeSubagentPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_subagents ( + subagent_id, + thread_id, + run_id, + parent_node_id, + provider, + driver, + provider_instance_id, + provider_thread_id, + child_thread_id, + origin, + status, + started_at, + completed_at, + updated_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.threadId}, + ${event.payload.runId}, + ${event.payload.parentNodeId}, + ${event.payload.providerInstanceId}, + ${event.payload.driver}, + ${event.payload.providerInstanceId}, + ${event.payload.providerThreadId}, + ${event.payload.childThreadId}, + ${event.payload.origin}, + ${event.payload.status}, + ${nullableStringField(payload, "startedAt")}, + ${nullableStringField(payload, "completedAt")}, + ${stringField(payload, "updatedAt")}, + ${payloadJson} + ) + ON CONFLICT(subagent_id) + DO UPDATE SET + thread_id = excluded.thread_id, + run_id = excluded.run_id, + parent_node_id = excluded.parent_node_id, + provider = excluded.provider, + driver = excluded.driver, + provider_instance_id = excluded.provider_instance_id, + provider_thread_id = excluded.provider_thread_id, + child_thread_id = excluded.child_thread_id, + origin = excluded.origin, + status = excluded.status, + started_at = excluded.started_at, + completed_at = excluded.completed_at, + updated_at = excluded.updated_at, + payload_json = excluded.payload_json + `; + break; + } + case "provider-session.attached": + case "provider-session.updated": { + const payloadJson = yield* encodeProviderSessionPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_provider_sessions ( + provider_session_id, + thread_id, + provider, + driver, + provider_instance_id, + status, + model, + updated_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.threadId}, + ${event.payload.providerInstanceId}, + ${event.payload.driver}, + ${event.payload.providerInstanceId}, + ${event.payload.status}, + ${event.payload.model}, + ${stringField(payload, "updatedAt")}, + ${payloadJson} + ) + ON CONFLICT(provider_session_id) + DO UPDATE SET + thread_id = excluded.thread_id, + provider = excluded.provider, + driver = excluded.driver, + provider_instance_id = excluded.provider_instance_id, + status = excluded.status, + model = excluded.model, + updated_at = excluded.updated_at, + payload_json = excluded.payload_json + `; + if (event.type === "provider-session.attached") { + yield* sql` + INSERT OR IGNORE INTO orchestration_v2_projection_provider_session_bindings ( + provider_session_id, + thread_id + ) + VALUES (${event.payload.id}, ${event.threadId}) + `; + } + break; + } + case "provider-session.detached": { + yield* sql` + DELETE FROM orchestration_v2_projection_provider_session_bindings + WHERE provider_session_id = ${event.payload.providerSessionId} + AND thread_id = ${event.threadId} + `; + break; + } + case "provider-thread.updated": { + const payloadJson = yield* encodeProviderThreadPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_provider_threads ( + provider_thread_id, + thread_id, + owner_node_id, + provider, + driver, + provider_instance_id, + provider_session_id, + status, + first_run_ordinal, + last_run_ordinal, + updated_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.appThreadId}, + ${event.payload.ownerNodeId}, + ${event.payload.providerInstanceId}, + ${event.payload.driver}, + ${event.payload.providerInstanceId}, + ${event.payload.providerSessionId}, + ${event.payload.status}, + ${event.payload.firstRunOrdinal}, + ${event.payload.lastRunOrdinal}, + ${stringField(payload, "updatedAt")}, + ${payloadJson} + ) + ON CONFLICT(provider_thread_id) + DO UPDATE SET + thread_id = excluded.thread_id, + owner_node_id = excluded.owner_node_id, + provider = excluded.provider, + driver = excluded.driver, + provider_instance_id = excluded.provider_instance_id, + provider_session_id = excluded.provider_session_id, + status = excluded.status, + first_run_ordinal = excluded.first_run_ordinal, + last_run_ordinal = excluded.last_run_ordinal, + updated_at = excluded.updated_at, + payload_json = excluded.payload_json + `; + if (event.payload.appThreadId !== null) { + const threadRows = yield* sql` + SELECT payload_json + FROM orchestration_v2_projection_threads + WHERE thread_id = ${event.payload.appThreadId} + LIMIT 1 + `; + const threadRow = threadRows[0]; + if (threadRow !== undefined) { + const thread = yield* decodeThreadPayload(threadRow.payload_json); + const updatedThread = { + ...thread, + activeProviderThreadId: event.payload.id, + updatedAt: event.payload.updatedAt, + }; + const updatedThreadPayloadJson = yield* encodeThreadPayload(updatedThread); + yield* sql` + UPDATE orchestration_v2_projection_threads + SET + active_provider_thread_id = ${event.payload.id}, + updated_at = ${stringField(parseEncodedPayload(updatedThreadPayloadJson), "updatedAt")}, + payload_json = ${updatedThreadPayloadJson} + WHERE thread_id = ${event.payload.appThreadId} + `; + } + } + break; + } + case "provider-turn.updated": { + const payloadJson = yield* encodeProviderTurnPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_provider_turns ( + provider_turn_id, + thread_id, + provider_thread_id, + node_id, + run_attempt_id, + ordinal, + status, + started_at, + completed_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.threadId}, + ${event.payload.providerThreadId}, + ${event.payload.nodeId}, + ${event.payload.runAttemptId}, + ${event.payload.ordinal}, + ${event.payload.status}, + ${nullableStringField(payload, "startedAt")}, + ${nullableStringField(payload, "completedAt")}, + ${payloadJson} + ) + ON CONFLICT(provider_turn_id) + DO UPDATE SET + thread_id = excluded.thread_id, + provider_thread_id = excluded.provider_thread_id, + node_id = excluded.node_id, + run_attempt_id = excluded.run_attempt_id, + ordinal = excluded.ordinal, + status = excluded.status, + started_at = excluded.started_at, + completed_at = excluded.completed_at, + payload_json = excluded.payload_json + `; + break; + } + case "runtime-request.updated": { + const payloadJson = yield* encodeRuntimeRequestPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_runtime_requests ( + runtime_request_id, + thread_id, + node_id, + provider_turn_id, + kind, + status, + created_at, + resolved_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.threadId}, + ${event.payload.nodeId}, + ${event.payload.providerTurnId}, + ${event.payload.kind}, + ${event.payload.status}, + ${stringField(payload, "createdAt")}, + ${nullableStringField(payload, "resolvedAt")}, + ${payloadJson} + ) + ON CONFLICT(runtime_request_id) + DO UPDATE SET + thread_id = excluded.thread_id, + node_id = excluded.node_id, + provider_turn_id = excluded.provider_turn_id, + kind = excluded.kind, + status = excluded.status, + created_at = excluded.created_at, + resolved_at = excluded.resolved_at, + payload_json = excluded.payload_json + `; + break; + } + case "message.updated": { + const payloadJson = yield* encodeMessagePayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_messages ( + message_id, + thread_id, + run_id, + node_id, + role, + streaming, + created_at, + updated_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.threadId}, + ${event.payload.runId}, + ${event.payload.nodeId}, + ${event.payload.role}, + ${booleanInt(event.payload.streaming)}, + ${stringField(payload, "createdAt")}, + ${stringField(payload, "updatedAt")}, + ${payloadJson} + ) + ON CONFLICT(message_id) + DO UPDATE SET + thread_id = excluded.thread_id, + run_id = excluded.run_id, + node_id = excluded.node_id, + role = excluded.role, + streaming = excluded.streaming, + created_at = excluded.created_at, + updated_at = excluded.updated_at, + payload_json = excluded.payload_json + `; + break; + } + case "plan.updated": { + const payloadJson = yield* encodePlanPayload(event.payload); + yield* sql` + INSERT INTO orchestration_v2_projection_plans ( + plan_id, + thread_id, + run_id, + node_id, + kind, + status, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.threadId}, + ${event.payload.runId}, + ${event.payload.nodeId}, + ${event.payload.kind}, + ${event.payload.status}, + ${payloadJson} + ) + ON CONFLICT(plan_id) + DO UPDATE SET + thread_id = excluded.thread_id, + run_id = excluded.run_id, + node_id = excluded.node_id, + kind = excluded.kind, + status = excluded.status, + payload_json = excluded.payload_json + `; + break; + } + case "turn-item.updated": { + const payloadJson = yield* encodeTurnItemPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_turn_items ( + turn_item_id, + thread_id, + run_id, + node_id, + provider_thread_id, + provider_turn_id, + parent_item_id, + ordinal, + type, + status, + updated_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.threadId}, + ${event.payload.runId}, + ${event.payload.nodeId}, + ${event.payload.providerThreadId}, + ${event.payload.providerTurnId}, + ${event.payload.parentItemId}, + ${event.payload.ordinal}, + ${event.payload.type}, + ${event.payload.status}, + ${stringField(payload, "updatedAt")}, + ${payloadJson} + ) + ON CONFLICT(turn_item_id) + DO UPDATE SET + thread_id = excluded.thread_id, + run_id = excluded.run_id, + node_id = excluded.node_id, + provider_thread_id = excluded.provider_thread_id, + provider_turn_id = excluded.provider_turn_id, + parent_item_id = excluded.parent_item_id, + ordinal = excluded.ordinal, + type = excluded.type, + status = excluded.status, + updated_at = excluded.updated_at, + payload_json = excluded.payload_json + `; + break; + } + case "checkpoint-scope.created": { + const payloadJson = yield* encodeCheckpointScopePayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_checkpoint_scopes ( + scope_id, + thread_id, + run_id, + node_id, + parent_scope_id, + provider_thread_id, + kind, + ordinal_within_parent, + advances_app_run_count, + created_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.threadId}, + ${event.payload.runId}, + ${event.payload.nodeId}, + ${event.payload.parentScopeId}, + ${event.payload.providerThreadId}, + ${event.payload.kind}, + ${event.payload.ordinalWithinParent}, + ${booleanInt(event.payload.advancesAppRunCount)}, + ${stringField(payload, "createdAt")}, + ${payloadJson} + ) + ON CONFLICT(scope_id) + DO UPDATE SET + thread_id = excluded.thread_id, + run_id = excluded.run_id, + node_id = excluded.node_id, + parent_scope_id = excluded.parent_scope_id, + provider_thread_id = excluded.provider_thread_id, + kind = excluded.kind, + ordinal_within_parent = excluded.ordinal_within_parent, + advances_app_run_count = excluded.advances_app_run_count, + created_at = excluded.created_at, + payload_json = excluded.payload_json + `; + break; + } + case "checkpoint.captured": { + const payloadJson = yield* encodeCheckpointPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_checkpoints ( + checkpoint_id, + thread_id, + scope_id, + run_id, + node_id, + parent_checkpoint_id, + ordinal_within_scope, + app_run_ordinal, + status, + captured_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.threadId}, + ${event.payload.scopeId}, + ${event.payload.runId}, + ${event.payload.nodeId}, + ${event.payload.parentCheckpointId}, + ${event.payload.ordinalWithinScope}, + ${event.payload.appRunOrdinal}, + ${event.payload.status}, + ${stringField(payload, "capturedAt")}, + ${payloadJson} + ) + ON CONFLICT(checkpoint_id) + DO UPDATE SET + thread_id = excluded.thread_id, + scope_id = excluded.scope_id, + run_id = excluded.run_id, + node_id = excluded.node_id, + parent_checkpoint_id = excluded.parent_checkpoint_id, + ordinal_within_scope = excluded.ordinal_within_scope, + app_run_ordinal = excluded.app_run_ordinal, + status = excluded.status, + captured_at = excluded.captured_at, + payload_json = excluded.payload_json + `; + break; + } + case "checkpoint.rollback-requested": + break; + case "context-handoff.updated": { + const payloadJson = yield* encodeContextHandoffPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_context_handoffs ( + context_handoff_id, + thread_id, + target_run_id, + to_provider_thread_id, + strategy, + status, + updated_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.threadId}, + ${event.payload.targetRunId}, + ${event.payload.toProviderThreadId}, + ${event.payload.strategy}, + ${event.payload.status}, + ${stringField(payload, "updatedAt")}, + ${payloadJson} + ) + ON CONFLICT(context_handoff_id) + DO UPDATE SET + thread_id = excluded.thread_id, + target_run_id = excluded.target_run_id, + to_provider_thread_id = excluded.to_provider_thread_id, + strategy = excluded.strategy, + status = excluded.status, + updated_at = excluded.updated_at, + payload_json = excluded.payload_json + `; + break; + } + case "context-transfer.created": + case "context-transfer.updated": { + const payloadJson = yield* encodeContextTransferPayload(event.payload); + const payload = parseEncodedPayload(payloadJson); + yield* sql` + INSERT INTO orchestration_v2_projection_context_transfers ( + context_transfer_id, + source_thread_id, + target_thread_id, + target_run_id, + type, + status, + source_provider, + target_provider, + source_provider_instance_id, + target_provider_instance_id, + updated_at, + payload_json + ) + VALUES ( + ${event.payload.id}, + ${event.payload.sourceThreadId}, + ${event.payload.targetThreadId}, + ${event.payload.targetRunId}, + ${event.payload.type}, + ${event.payload.status}, + ${event.payload.sourceProviderInstanceId}, + ${event.payload.targetProviderInstanceId}, + ${event.payload.sourceProviderInstanceId}, + ${event.payload.targetProviderInstanceId}, + ${stringField(payload, "updatedAt")}, + ${payloadJson} + ) + ON CONFLICT(context_transfer_id) + DO UPDATE SET + source_thread_id = excluded.source_thread_id, + target_thread_id = excluded.target_thread_id, + target_run_id = excluded.target_run_id, + type = excluded.type, + status = excluded.status, + source_provider = excluded.source_provider, + target_provider = excluded.target_provider, + source_provider_instance_id = excluded.source_provider_instance_id, + target_provider_instance_id = excluded.target_provider_instance_id, + updated_at = excluded.updated_at, + payload_json = excluded.payload_json + `; + break; + } + } + + if ( + event.type !== "thread.created" && + event.type !== "thread.archived" && + event.type !== "thread.unarchived" && + event.type !== "thread.deleted" && + event.type !== "thread.metadata-updated" && + event.type !== "thread.runtime-mode-updated" && + event.type !== "thread.interaction-mode-updated" && + event.type !== "thread.model-selection-updated" && + event.type !== "thread.provider-switched" + ) { + const rows = yield* sql` + SELECT payload_json + FROM orchestration_v2_projection_threads + WHERE thread_id = ${event.threadId} + LIMIT 1 + `; + const row = rows[0]; + if (row !== undefined) { + const thread = yield* decodeThreadPayload(row.payload_json); + const updatedThread = { ...thread, updatedAt: event.occurredAt }; + const payloadJson = yield* encodeThreadPayload(updatedThread); + yield* sql` + UPDATE orchestration_v2_projection_threads + SET + updated_at = ${stringField(parseEncodedPayload(payloadJson), "updatedAt")}, + payload_json = ${payloadJson} + WHERE thread_id = ${event.threadId} + `; + } + } + }).pipe( + Effect.mapError( + (cause) => + new ProjectionStoreApplyEventError({ + eventType: event.type, + cause, + }), + ), + ); + + const readCanonicalProjection: ProjectionStoreV2Shape["getThreadProjection"] = (threadId) => + Effect.gen(function* () { + const threadRows = yield* sql` + SELECT payload_json + FROM orchestration_v2_projection_threads + WHERE thread_id = ${threadId} + LIMIT 1 + `; + const threadRow = threadRows[0]; + if (!threadRow) { + return yield* new ProjectionStoreThreadNotFoundError({ threadId }); + } + + const [ + thread, + runRows, + attemptRows, + nodeRows, + subagentRows, + providerSessionRows, + providerThreadRows, + providerTurnRows, + runtimeRequestRows, + messageRows, + planRows, + turnItemRows, + checkpointScopeRows, + checkpointRows, + contextHandoffRows, + contextTransferRows, + ] = yield* Effect.all([ + decodeThreadPayload(threadRow.payload_json), + sql` + SELECT payload_json + FROM orchestration_v2_projection_runs + WHERE thread_id = ${threadId} + ORDER BY ordinal ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_run_attempts + WHERE thread_id = ${threadId} + ORDER BY run_id ASC, attempt_ordinal ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_nodes + WHERE thread_id = ${threadId} + ORDER BY COALESCE(started_at, ''), node_id ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_subagents + WHERE thread_id = ${threadId} + ORDER BY COALESCE(started_at, ''), subagent_id ASC + `, + sql` + SELECT sessions.payload_json + FROM orchestration_v2_projection_provider_sessions AS sessions + INNER JOIN orchestration_v2_projection_provider_session_bindings AS bindings + ON bindings.provider_session_id = sessions.provider_session_id + WHERE bindings.thread_id = ${threadId} + ORDER BY sessions.updated_at ASC, sessions.provider_session_id ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_provider_threads + WHERE thread_id = ${threadId} + OR owner_node_id IN ( + SELECT node_id + FROM orchestration_v2_projection_nodes + WHERE thread_id = ${threadId} + ) + OR provider_thread_id IN ( + SELECT provider_thread_id + FROM orchestration_v2_projection_subagents + WHERE thread_id = ${threadId} + AND provider_thread_id IS NOT NULL + ) + ORDER BY COALESCE(first_run_ordinal, 0), provider_thread_id ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_provider_turns + WHERE thread_id = ${threadId} + ORDER BY provider_thread_id ASC, ordinal ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_runtime_requests + WHERE thread_id = ${threadId} + ORDER BY created_at ASC, runtime_request_id ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_messages + WHERE thread_id = ${threadId} + ORDER BY created_at ASC, message_id ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_plans + WHERE thread_id = ${threadId} + ORDER BY plan_id ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_turn_items + WHERE thread_id = ${threadId} + ORDER BY ordinal ASC, turn_item_id ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_checkpoint_scopes + WHERE thread_id = ${threadId} + ORDER BY ordinal_within_parent ASC, scope_id ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_checkpoints + WHERE thread_id = ${threadId} + ORDER BY scope_id ASC, ordinal_within_scope ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_context_handoffs + WHERE thread_id = ${threadId} + ORDER BY rowid ASC + `, + sql` + SELECT payload_json + FROM orchestration_v2_projection_context_transfers + WHERE source_thread_id = ${threadId} OR target_thread_id = ${threadId} + ORDER BY rowid ASC + `, + ]); + + const [ + runs, + attempts, + nodes, + subagents, + providerSessions, + providerThreads, + providerTurns, + runtimeRequests, + messages, + plans, + turnItems, + checkpointScopes, + checkpoints, + contextHandoffs, + contextTransfers, + ] = yield* Effect.all([ + decodeRows(decodeRunPayload, threadId)(runRows), + decodeRows(decodeRunAttemptPayload, threadId)(attemptRows), + decodeRows(decodeNodePayload, threadId)(nodeRows), + decodeRows(decodeSubagentPayload, threadId)(subagentRows), + decodeRows(decodeProviderSessionPayload, threadId)(providerSessionRows), + decodeRows(decodeProviderThreadPayload, threadId)(providerThreadRows), + decodeRows(decodeProviderTurnPayload, threadId)(providerTurnRows), + decodeRows(decodeRuntimeRequestPayload, threadId)(runtimeRequestRows), + decodeRows(decodeMessagePayload, threadId)(messageRows), + decodeRows(decodePlanPayload, threadId)(planRows), + decodeRows(decodeTurnItemPayload, threadId)(turnItemRows), + decodeRows(decodeCheckpointScopePayload, threadId)(checkpointScopeRows), + decodeRows(decodeCheckpointPayload, threadId)(checkpointRows), + decodeRows(decodeContextHandoffPayload, threadId)(contextHandoffRows), + decodeRows(decodeContextTransferPayload, threadId)(contextTransferRows), + ]); + const orderedMessages = sortMessagesByTurnItemOrder(messages, turnItems); + const projection = { + thread, + runs, + attempts, + nodes, + subagents, + providerSessions, + providerThreads, + providerTurns, + runtimeRequests, + messages: orderedMessages, + plans, + turnItems, + checkpointScopes, + checkpoints, + contextHandoffs, + contextTransfers, + visibleTurnItems: [], + updatedAt: thread.updatedAt, + } satisfies OrchestrationV2ThreadProjection; + return withLocalVisibleTurnItems(projection); + }).pipe( + Effect.mapError((cause) => + Schema.is(ProjectionStoreThreadNotFoundError)(cause) + ? cause + : new ProjectionStoreReadError({ + threadId, + cause, + }), + ), + ); + + const readProjection = ( + threadId: ThreadId, + seenThreadIds: ReadonlySet, + ): Effect.Effect => + Effect.gen(function* () { + const projection = yield* readCanonicalProjection(threadId); + const forkedFrom = projection.thread.forkedFrom; + if (forkedFrom?.type !== "run" || seenThreadIds.has(forkedFrom.threadId)) { + return withLocalVisibleTurnItems(projection); + } + + const sourceProjection = yield* readProjection( + forkedFrom.threadId, + new Set([...seenThreadIds, threadId]), + ); + return { + ...projection, + visibleTurnItems: buildVisibleTurnItems({ + projection, + sourceProjection, + }), + }; + }); + + const getThreadProjection: ProjectionStoreV2Shape["getThreadProjection"] = (threadId) => + readProjection(threadId, new Set()); + + const getThreadSnapshot: ProjectionStoreV2Shape["getThreadSnapshot"] = (threadId) => + sql + .withTransaction( + Effect.gen(function* () { + const projection = yield* getThreadProjection(threadId); + const rows = yield* sql<{ readonly snapshot_sequence: number | null }>` + SELECT MAX(sequence) AS snapshot_sequence + FROM orchestration_events + WHERE application_event_version = 2 + AND aggregate_kind = 'thread' + AND stream_id = ${threadId} + `; + return { + schemaVersion: ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION, + snapshotSequence: rows[0]?.snapshot_sequence ?? 0, + projection, + }; + }), + ) + .pipe( + Effect.mapError((cause) => + Schema.is(ProjectionStoreThreadNotFoundError)(cause) || + Schema.is(ProjectionStoreReadError)(cause) + ? cause + : new ProjectionStoreReadError({ threadId, cause }), + ), + ); + + const getShellSnapshot: ProjectionStoreV2Shape["getShellSnapshot"] = () => + sql + .withTransaction( + Effect.gen(function* () { + const [threadRows, runRows, itemCountRows, sequenceRows] = yield* Effect.all([ + sql` + SELECT + t.thread_id, + t.payload_json, + ( + SELECT r.run_id + FROM orchestration_v2_projection_runs r + WHERE r.thread_id = t.thread_id + ORDER BY r.ordinal DESC, r.run_id DESC + LIMIT 1 + ) AS latest_run_id, + ( + SELECT r.status + FROM orchestration_v2_projection_runs r + WHERE r.thread_id = t.thread_id + ORDER BY r.ordinal DESC, r.run_id DESC + LIMIT 1 + ) AS latest_run_status, + ( + SELECT r.run_id + FROM orchestration_v2_projection_runs r + WHERE r.thread_id = t.thread_id + AND r.status IN ('starting', 'running', 'waiting') + ORDER BY r.ordinal DESC, r.run_id DESC + LIMIT 1 + ) AS active_run_id, + ( + SELECT request.payload_json + FROM orchestration_v2_projection_runtime_requests request + WHERE request.thread_id = t.thread_id + AND request.status = 'pending' + ORDER BY request.created_at DESC, request.runtime_request_id DESC + LIMIT 1 + ) AS pending_request_payload_json, + ( + SELECT message.payload_json + FROM orchestration_v2_projection_messages message + WHERE message.thread_id = t.thread_id + ORDER BY message.updated_at DESC, message.message_id DESC + LIMIT 1 + ) AS latest_message_payload_json, + ( + SELECT message.updated_at + FROM orchestration_v2_projection_messages message + WHERE message.thread_id = t.thread_id + AND message.role = 'user' + ORDER BY message.updated_at DESC, message.message_id DESC + LIMIT 1 + ) AS latest_user_message_at, + EXISTS ( + SELECT 1 + FROM orchestration_v2_projection_plans plan + WHERE plan.thread_id = t.thread_id + AND plan.kind = 'proposed_plan' + AND plan.status = 'active' + ) AS has_actionable_proposed_plan, + ( + SELECT COUNT(*) + FROM orchestration_v2_projection_turn_items i + LEFT JOIN orchestration_v2_projection_runs r + ON r.run_id = i.run_id + WHERE i.thread_id = t.thread_id + AND (i.run_id IS NULL OR r.status <> 'rolled_back') + ) AS item_count + FROM orchestration_v2_projection_threads t + WHERE t.deleted_at IS NULL + ORDER BY t.updated_at ASC, t.thread_id ASC + `, + sql` + SELECT thread_id, run_id, ordinal + FROM orchestration_v2_projection_runs + `, + sql` + SELECT thread_id, run_id, COUNT(*) AS item_count + FROM orchestration_v2_projection_turn_items + WHERE run_id IS NOT NULL + GROUP BY thread_id, run_id + `, + sql<{ readonly snapshot_sequence: number | null }>` + SELECT MAX(sequence) AS snapshot_sequence + FROM orchestration_events + WHERE application_event_version = 2 + AND aggregate_kind = 'thread' + `, + ]); + + const runOrdinalsByThreadId = new Map>(); + for (const row of runRows) { + const threadId = ThreadId.make(row.thread_id); + const runId = RunId.make(row.run_id); + const existing = runOrdinalsByThreadId.get(threadId) ?? new Map(); + existing.set(runId, row.ordinal); + runOrdinalsByThreadId.set(threadId, existing); + } + + const itemCountsByThreadId = new Map>(); + for (const row of itemCountRows) { + const threadId = ThreadId.make(row.thread_id); + const runId = RunId.make(row.run_id); + const existing = itemCountsByThreadId.get(threadId) ?? new Map(); + existing.set(runId, row.item_count); + itemCountsByThreadId.set(threadId, existing); + } + + const states = yield* Effect.forEach(threadRows, (row) => + Effect.gen(function* () { + const thread = yield* decodeThreadPayload(row.payload_json); + const pendingRuntimeRequest = + row.pending_request_payload_json === null + ? null + : yield* decodeRuntimeRequestPayload(row.pending_request_payload_json); + const latestVisibleMessage = + row.latest_message_payload_json === null + ? null + : yield* decodeMessagePayload(row.latest_message_payload_json); + return { + thread, + latestRunId: row.latest_run_id === null ? null : RunId.make(row.latest_run_id), + latestRunStatus: shellStatusFromStoredRunStatus(row.latest_run_status), + activeRunId: row.active_run_id === null ? null : RunId.make(row.active_run_id), + pendingRuntimeRequest, + latestVisibleMessage, + latestUserMessageAt: + row.latest_user_message_at === null + ? null + : DateTime.makeUnsafe(row.latest_user_message_at), + hasActionableProposedPlan: row.has_actionable_proposed_plan === 1, + itemCount: row.item_count, + updatedAt: thread.updatedAt, + runOrdinalById: + runOrdinalsByThreadId.get(ThreadId.make(row.thread_id)) ?? new Map(), + itemCountByRunId: + itemCountsByThreadId.get(ThreadId.make(row.thread_id)) ?? new Map(), + } satisfies ShellThreadState; + }), + ); + const statesByThreadId = new Map(states.map((state) => [state.thread.id, state])); + + const shells = states.map((state) => + shellFromState({ + state, + visibleItemCount: visibleItemCountForShell({ + threadId: state.thread.id, + statesByThreadId, + }), + }), + ); + + return { + schemaVersion: ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION, + snapshotSequence: sequenceRows[0]?.snapshot_sequence ?? 0, + threads: shells.filter((thread) => thread.archivedAt === null), + archivedThreads: shells.filter((thread) => thread.archivedAt !== null), + }; + }), + ) + .pipe( + Effect.mapError( + (cause) => + new ProjectionStoreReadError({ + threadId: ThreadId.make("thread:shell"), + cause, + }), + ), + ); + + return { + apply, + getShellSnapshot, + getThreadProjection, + getThreadSnapshot, + } satisfies ProjectionStoreV2Shape; + }), +); + +export const layerMemory: Layer.Layer = Layer.effect( + ProjectionStoreV2, + Effect.gen(function* () { + const projections = yield* Ref.make(new Map()); + const sequence = yield* Ref.make(0); + + const service: ProjectionStoreV2Shape = { + apply: (event) => + Effect.gen(function* () { + const result = yield* Ref.modify(projections, (existing) => { + const next = new Map(existing); + + if (event.type === "thread.created" && !next.has(event.threadId)) { + next.set(event.threadId, emptyProjection(event)); + return [undefined, next] as const; + } + + const projection = next.get(event.threadId); + if (!projection) { + return [ + new ProjectionStoreThreadNotFoundError({ threadId: event.threadId }), + existing, + ] as const; + } + + next.set(event.threadId, applyToProjection(projection, event)); + return [undefined, next] as const; + }); + + if (result) { + return yield* result; + } + yield* Ref.update(sequence, (current) => current + 1); + }), + getShellSnapshot: () => + Effect.gen(function* () { + const existing = yield* Ref.get(projections); + const shells = yield* Effect.forEach( + [...existing.keys()].toSorted((left, right) => + String(left).localeCompare(String(right)), + ), + (threadId) => + service.getThreadProjection(threadId).pipe(Effect.map(threadShellFromProjection)), + ); + const visible = shells.filter((thread) => thread.deletedAt === null); + return { + schemaVersion: ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION, + snapshotSequence: yield* Ref.get(sequence), + threads: visible.filter((thread) => thread.archivedAt === null), + archivedThreads: visible.filter((thread) => thread.archivedAt !== null), + }; + }), + getThreadProjection: (threadId) => + Effect.gen(function* () { + const existing = yield* Ref.get(projections); + const readProjection = ( + targetThreadId: ThreadId, + seenThreadIds: ReadonlySet, + ): OrchestrationV2ThreadProjection | null => { + const projection = existing.get(targetThreadId); + if (!projection) { + return null; + } + const forkedFrom = projection.thread.forkedFrom; + if (forkedFrom?.type !== "run" || seenThreadIds.has(forkedFrom.threadId)) { + return withLocalVisibleTurnItems(projection); + } + const sourceProjection = readProjection( + forkedFrom.threadId, + new Set([...seenThreadIds, targetThreadId]), + ); + return { + ...projection, + visibleTurnItems: buildVisibleTurnItems({ + projection, + sourceProjection, + }), + }; + }; + const projection = readProjection(threadId, new Set()); + if (!projection) { + return yield* new ProjectionStoreThreadNotFoundError({ threadId }); + } + return projection; + }), + getThreadSnapshot: (threadId) => + service.getThreadProjection(threadId).pipe( + Effect.flatMap((projection) => + Ref.get(sequence).pipe( + Effect.map((snapshotSequence) => ({ + schemaVersion: ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION, + snapshotSequence, + projection, + })), + ), + ), + ), + }; + + return service; + }), +); diff --git a/apps/server/src/orchestration-v2/ProviderAdapter.ts b/apps/server/src/orchestration-v2/ProviderAdapter.ts new file mode 100644 index 00000000000..62b521cf454 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderAdapter.ts @@ -0,0 +1,503 @@ +import { + ChatAttachment, + CheckpointId, + MessageId, + ModelSelection, + NodeId, + OrchestrationV2AppThread, + OrchestrationV2ConversationMessage, + OrchestrationV2ExecutionNode, + OrchestrationV2ProviderSession, + OrchestrationV2PlanArtifact, + OrchestrationV2ProviderCapabilities, + OrchestrationV2ProviderThread, + OrchestrationV2ProviderTurn, + OrchestrationV2RawProviderEvent, + OrchestrationV2RuntimeRequest, + OrchestrationV2Subagent, + OrchestrationV2TurnItem, + ProviderApprovalDecision, + ProviderInteractionMode, + ProviderDriverKind, + ProviderInstanceId, + ProviderUserInputAnswers, + ProviderSessionId, + ProviderThreadId, + ProviderTurnId, + RuntimeMode, + RuntimeRequestId, + RunAttemptId, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Schema from "effect/Schema"; +import type * as Effect from "effect/Effect"; +import type * as Scope from "effect/Scope"; +import type * as Stream from "effect/Stream"; + +export const ProviderAdapterV2RuntimePolicy = Schema.Struct({ + runtimeMode: RuntimeMode, + interactionMode: ProviderInteractionMode, + cwd: Schema.NullOr(Schema.String), + approvalPolicy: Schema.optional(Schema.Unknown), + sandboxPolicy: Schema.optional(Schema.Unknown), + reasoningEffort: Schema.optional(Schema.String), +}); +export type ProviderAdapterV2RuntimePolicy = typeof ProviderAdapterV2RuntimePolicy.Type; + +export const ProviderAdapterV2TurnMessage = Schema.Struct({ + messageId: MessageId, + text: Schema.String, + attachments: Schema.Array(ChatAttachment), + createdBy: OrchestrationV2ConversationMessage.fields.createdBy, + creationSource: OrchestrationV2ConversationMessage.fields.creationSource, +}); +export type ProviderAdapterV2TurnMessage = typeof ProviderAdapterV2TurnMessage.Type; + +export const ProviderAdapterV2SessionStatus = Schema.Literals([ + "starting", + "ready", + "running", + "waiting", + "stopped", + "error", +]); +export type ProviderAdapterV2SessionStatus = typeof ProviderAdapterV2SessionStatus.Type; + +export const ProviderAdapterV2Event = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("app_thread.created"), + driver: ProviderDriverKind, + appThread: OrchestrationV2AppThread, + }), + Schema.Struct({ + type: Schema.Literal("provider_session.updated"), + driver: ProviderDriverKind, + providerSession: OrchestrationV2ProviderSession, + }), + Schema.Struct({ + type: Schema.Literal("provider_thread.updated"), + driver: ProviderDriverKind, + providerThread: OrchestrationV2ProviderThread, + }), + Schema.Struct({ + type: Schema.Literal("provider_turn.updated"), + driver: ProviderDriverKind, + threadId: Schema.optional(ThreadId), + providerTurn: OrchestrationV2ProviderTurn, + }), + Schema.Struct({ + type: Schema.Literal("node.updated"), + driver: ProviderDriverKind, + node: OrchestrationV2ExecutionNode, + }), + Schema.Struct({ + type: Schema.Literal("subagent.updated"), + driver: ProviderDriverKind, + subagent: OrchestrationV2Subagent, + }), + Schema.Struct({ + type: Schema.Literal("message.updated"), + driver: ProviderDriverKind, + message: OrchestrationV2ConversationMessage, + }), + Schema.Struct({ + type: Schema.Literal("turn_item.updated"), + driver: ProviderDriverKind, + turnItem: OrchestrationV2TurnItem, + }), + Schema.Struct({ + type: Schema.Literal("runtime_request.updated"), + driver: ProviderDriverKind, + threadId: Schema.optional(ThreadId), + runtimeRequest: OrchestrationV2RuntimeRequest, + }), + Schema.Struct({ + type: Schema.Literal("plan.updated"), + driver: ProviderDriverKind, + plan: OrchestrationV2PlanArtifact, + }), + Schema.Struct({ + type: Schema.Literal("turn.terminal"), + driver: ProviderDriverKind, + providerTurnId: ProviderTurnId, + status: Schema.Literals(["completed", "interrupted", "failed", "cancelled"]), + }), +]); +export type ProviderAdapterV2Event = typeof ProviderAdapterV2Event.Type; + +export class ProviderAdapterCapabilitiesError extends Schema.TaggedErrorClass()( + "ProviderAdapterCapabilitiesError", + { + driver: ProviderDriverKind, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to read ${this.driver} provider capabilities.`; + } +} + +export class ProviderAdapterOpenSessionError extends Schema.TaggedErrorClass()( + "ProviderAdapterOpenSessionError", + { + driver: ProviderDriverKind, + providerSessionId: ProviderSessionId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to open ${this.driver} provider session ${this.providerSessionId}.`; + } +} + +export class ProviderAdapterCloseSessionError extends Schema.TaggedErrorClass()( + "ProviderAdapterCloseSessionError", + { + driver: ProviderDriverKind, + providerSessionId: ProviderSessionId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to close ${this.driver} provider session ${this.providerSessionId}.`; + } +} + +export class ProviderAdapterResumeThreadError extends Schema.TaggedErrorClass()( + "ProviderAdapterResumeThreadError", + { + driver: ProviderDriverKind, + providerSessionId: ProviderSessionId, + providerThreadId: ProviderThreadId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to resume ${this.driver} provider thread ${this.providerThreadId}.`; + } +} + +export class ProviderAdapterEnsureThreadError extends Schema.TaggedErrorClass()( + "ProviderAdapterEnsureThreadError", + { + driver: ProviderDriverKind, + threadId: ThreadId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to ensure ${this.driver} provider thread for app thread ${this.threadId}.`; + } +} + +export class ProviderAdapterReadThreadSnapshotError extends Schema.TaggedErrorClass()( + "ProviderAdapterReadThreadSnapshotError", + { + driver: ProviderDriverKind, + providerThreadId: ProviderThreadId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to read ${this.driver} provider thread snapshot ${this.providerThreadId}.`; + } +} + +export class ProviderAdapterRollbackThreadError extends Schema.TaggedErrorClass()( + "ProviderAdapterRollbackThreadError", + { + driver: ProviderDriverKind, + providerThreadId: ProviderThreadId, + checkpointId: Schema.optional(CheckpointId), + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to roll back ${this.driver} provider thread ${this.providerThreadId}.`; + } +} + +export class ProviderAdapterForkThreadError extends Schema.TaggedErrorClass()( + "ProviderAdapterForkThreadError", + { + driver: ProviderDriverKind, + providerThreadId: ProviderThreadId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to fork ${this.driver} provider thread ${this.providerThreadId}.`; + } +} + +export class ProviderAdapterTurnStartError extends Schema.TaggedErrorClass()( + "ProviderAdapterTurnStartError", + { + driver: ProviderDriverKind, + threadId: ThreadId, + providerThreadId: ProviderThreadId, + runId: RunId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to start run ${this.runId} on ${this.driver} provider thread ${this.providerThreadId}.`; + } +} + +export class ProviderAdapterSteerRunUnsupportedError extends Schema.TaggedErrorClass()( + "ProviderAdapterSteerRunUnsupportedError", + { + driver: ProviderDriverKind, + providerThreadId: ProviderThreadId, + }, +) { + override get message(): string { + return `${this.driver} provider thread ${this.providerThreadId} does not support active-run steering.`; + } +} + +export class ProviderAdapterSteerRunError extends Schema.TaggedErrorClass()( + "ProviderAdapterSteerRunError", + { + driver: ProviderDriverKind, + providerThreadId: ProviderThreadId, + providerTurnId: ProviderTurnId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to steer active run ${this.providerTurnId} on ${this.driver} provider thread ${this.providerThreadId}.`; + } +} + +export class ProviderAdapterInterruptError extends Schema.TaggedErrorClass()( + "ProviderAdapterInterruptError", + { + driver: ProviderDriverKind, + providerThreadId: ProviderThreadId, + providerTurnId: ProviderTurnId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to interrupt ${this.driver} provider turn ${this.providerTurnId}.`; + } +} + +export class ProviderAdapterRuntimeRequestResponseError extends Schema.TaggedErrorClass()( + "ProviderAdapterRuntimeRequestResponseError", + { + driver: ProviderDriverKind, + requestId: RuntimeRequestId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to respond to ${this.driver} runtime request ${this.requestId}.`; + } +} + +export class ProviderAdapterEventStreamError extends Schema.TaggedErrorClass()( + "ProviderAdapterEventStreamError", + { + driver: ProviderDriverKind, + providerSessionId: ProviderSessionId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed while streaming ${this.driver} provider session ${this.providerSessionId} events.`; + } +} + +export class ProviderAdapterProtocolError extends Schema.TaggedErrorClass()( + "ProviderAdapterProtocolError", + { + driver: ProviderDriverKind, + detail: Schema.String, + payload: Schema.optional(Schema.Unknown), + }, +) { + override get message(): string { + return `${this.driver} provider protocol error: ${this.detail}.`; + } +} + +export const ProviderAdapterV2Error = Schema.Union([ + ProviderAdapterCapabilitiesError, + ProviderAdapterOpenSessionError, + ProviderAdapterCloseSessionError, + ProviderAdapterResumeThreadError, + ProviderAdapterEnsureThreadError, + ProviderAdapterReadThreadSnapshotError, + ProviderAdapterRollbackThreadError, + ProviderAdapterForkThreadError, + ProviderAdapterTurnStartError, + ProviderAdapterSteerRunUnsupportedError, + ProviderAdapterSteerRunError, + ProviderAdapterInterruptError, + ProviderAdapterRuntimeRequestResponseError, + ProviderAdapterEventStreamError, + ProviderAdapterProtocolError, +]); +export type ProviderAdapterV2Error = typeof ProviderAdapterV2Error.Type; + +export interface ProviderAdapterV2OpenSessionInput { + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + readonly modelSelection: ModelSelection; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; + readonly resumeFromSession?: OrchestrationV2ProviderSession; +} + +export interface ProviderAdapterV2EnsureThreadInput { + readonly threadId: ThreadId; + readonly modelSelection: ModelSelection; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; + readonly providerSessionId?: ProviderSessionId; + readonly existingProviderThread?: OrchestrationV2ProviderThread; +} + +export interface ProviderAdapterV2TurnInput { + readonly appThread: OrchestrationV2AppThread; + readonly threadId: ThreadId; + readonly runId: RunId; + readonly runOrdinal: number; + readonly providerTurnOrdinal: number; + readonly attemptId: RunAttemptId; + readonly rootNodeId: NodeId; + readonly providerThread: OrchestrationV2ProviderThread; + readonly message: ProviderAdapterV2TurnMessage; + readonly modelSelection: ModelSelection; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; +} + +export interface ProviderAdapterV2SteerInput { + readonly threadId: ThreadId; + readonly runId: RunId; + readonly providerThread: OrchestrationV2ProviderThread; + readonly providerTurnId: ProviderTurnId; + readonly message: ProviderAdapterV2TurnMessage; +} + +export interface ProviderAdapterV2InterruptInput { + readonly providerThread: OrchestrationV2ProviderThread; + readonly providerTurnId: ProviderTurnId; +} + +export interface ProviderAdapterV2RuntimeRequestResponseInput { + readonly requestId: RuntimeRequestId; + readonly decision?: ProviderApprovalDecision; + readonly answers?: ProviderUserInputAnswers; + readonly response?: unknown; +} + +export interface ProviderAdapterV2ThreadSnapshot { + readonly providerThread: OrchestrationV2ProviderThread; + readonly providerTurns: ReadonlyArray; + readonly messages: ReadonlyArray; + readonly runtimeRequests: ReadonlyArray; + readonly providerPayload?: unknown; +} + +export interface ProviderAdapterV2ReadThreadSnapshotInput { + readonly providerThread: OrchestrationV2ProviderThread; +} + +export type ProviderAdapterV2RollbackTarget = + | { + readonly type: "thread_start"; + readonly checkpointId: CheckpointId; + readonly appRunOrdinal: 0; + } + | { + readonly type: "provider_turn"; + readonly checkpointId: CheckpointId; + readonly appRunOrdinal: number; + readonly providerTurn: OrchestrationV2ProviderTurn; + }; + +export interface ProviderAdapterV2RollbackThreadInput { + readonly providerThread: OrchestrationV2ProviderThread; + readonly target: ProviderAdapterV2RollbackTarget; + readonly providerThreadTurns: ReadonlyArray; +} + +export interface ProviderAdapterV2ForkThreadInput { + readonly sourceProviderThread: OrchestrationV2ProviderThread; + readonly sourceProviderTurns?: ReadonlyArray; + readonly providerTurnId?: ProviderTurnId; + readonly targetThreadId: ThreadId; + readonly ownerNodeId?: NodeId; + readonly modelSelection?: ModelSelection; + readonly runtimePolicy?: ProviderAdapterV2RuntimePolicy; +} + +export interface ProviderAdapterV2EventSubscription { + readonly events: Stream.Stream; + readonly close: Effect.Effect; +} + +export interface ProviderAdapterV2SessionRuntime { + readonly instanceId: ProviderInstanceId; + readonly driver: ProviderDriverKind; + readonly providerSessionId: ProviderSessionId; + readonly providerSession: OrchestrationV2ProviderSession; + readonly rawEvents: Stream.Stream; + readonly events: Stream.Stream; + /** + * Manager-owned runtimes expose a synchronous subscription so concurrent + * provider threads receive independent copies of the process event stream. + * Adapter runtimes may omit this and expose only their raw single-consumer stream. + */ + readonly subscribeEvents?: Effect.Effect; + readonly ensureThread: ( + input: ProviderAdapterV2EnsureThreadInput, + ) => Effect.Effect; + readonly resumeThread: (input: { + readonly providerThread: OrchestrationV2ProviderThread; + readonly threadId?: ThreadId; + readonly modelSelection?: ModelSelection; + readonly runtimePolicy?: ProviderAdapterV2RuntimePolicy; + }) => Effect.Effect; + readonly startTurn: ( + input: ProviderAdapterV2TurnInput, + ) => Effect.Effect; + readonly steerTurn: ( + input: ProviderAdapterV2SteerInput, + ) => Effect.Effect; + readonly interruptTurn: ( + input: ProviderAdapterV2InterruptInput, + ) => Effect.Effect; + readonly respondToRuntimeRequest: ( + input: ProviderAdapterV2RuntimeRequestResponseInput, + ) => Effect.Effect; + readonly readThreadSnapshot: ( + input: ProviderAdapterV2ReadThreadSnapshotInput, + ) => Effect.Effect; + readonly rollbackThread: ( + input: ProviderAdapterV2RollbackThreadInput, + ) => Effect.Effect; + readonly forkThread: ( + input: ProviderAdapterV2ForkThreadInput, + ) => Effect.Effect; +} + +export interface ProviderAdapterV2Shape { + readonly instanceId: ProviderInstanceId; + readonly driver: ProviderDriverKind; + readonly getCapabilities: () => Effect.Effect< + OrchestrationV2ProviderCapabilities, + ProviderAdapterV2Error + >; + readonly openSession: ( + input: ProviderAdapterV2OpenSessionInput, + ) => Effect.Effect; +} + +export class ProviderAdapterV2 extends Context.Service()( + "t3/orchestration-v2/ProviderAdapter/ProviderAdapterV2", +) {} diff --git a/apps/server/src/orchestration-v2/ProviderAdapterDriver.ts b/apps/server/src/orchestration-v2/ProviderAdapterDriver.ts new file mode 100644 index 00000000000..6534386c274 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderAdapterDriver.ts @@ -0,0 +1,44 @@ +import { + ProviderDriverKind, + ProviderInstanceId, + type ProviderInstanceEnvironment, +} from "@t3tools/contracts"; +import * as Schema from "effect/Schema"; +import type * as Effect from "effect/Effect"; +import type * as Scope from "effect/Scope"; + +import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts"; + +export class ProviderAdapterDriverCreateError extends Schema.TaggedErrorClass()( + "ProviderAdapterDriverCreateError", + { + driver: ProviderDriverKind, + instanceId: ProviderInstanceId, + detail: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to create orchestration-v2 provider adapter ${this.instanceId} (${this.driver}): ${this.detail}`; + } +} + +export interface ProviderAdapterDriverCreateInput { + readonly instanceId: ProviderInstanceId; + readonly displayName: string | undefined; + readonly accentColor?: string | undefined; + readonly environment: ProviderInstanceEnvironment; + readonly enabled: boolean; + readonly config: Config; +} + +export interface ProviderAdapterDriver { + readonly driverKind: ProviderDriverKind; + readonly configSchema: Schema.Codec; + readonly defaultConfig: () => Config; + readonly create: ( + input: ProviderAdapterDriverCreateInput, + ) => Effect.Effect; +} + +export type AnyProviderAdapterDriver = ProviderAdapterDriver; diff --git a/apps/server/src/orchestration-v2/ProviderAdapterRegistry.test.ts b/apps/server/src/orchestration-v2/ProviderAdapterRegistry.test.ts new file mode 100644 index 00000000000..6a600c9b8a9 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderAdapterRegistry.test.ts @@ -0,0 +1,68 @@ +import { assert, it } from "@effect/vitest"; +import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; + +import type { ProviderInstance } from "../provider/ProviderDriver.ts"; +import { ProviderInstanceRegistry } from "../provider/Services/ProviderInstanceRegistry.ts"; +import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts"; +import { + layerFromProviderInstanceRegistry, + ProviderAdapterRegistryV2, +} from "./ProviderAdapterRegistry.ts"; + +const driver = ProviderDriverKind.make("codex"); +const personalId = ProviderInstanceId.make("codex_personal"); +const workId = ProviderInstanceId.make("codex_work"); + +const makeAdapter = (instanceId: ProviderInstanceId): ProviderAdapterV2Shape => + ({ + instanceId, + driver, + getCapabilities: () => Effect.die("capabilities are not used by this registry test"), + openSession: () => Effect.die("sessions are not used by this registry test"), + }) as ProviderAdapterV2Shape; + +const makeInstance = ( + instanceId: ProviderInstanceId, + orchestrationAdapter: ProviderAdapterV2Shape, +): ProviderInstance => ({ + instanceId, + driverKind: driver, + continuationIdentity: { + driverKind: driver, + continuationKey: `codex:test:${instanceId}`, + }, + displayName: String(instanceId), + enabled: true, + snapshot: {} as ProviderInstance["snapshot"], + orchestrationAdapter, + textGeneration: {} as ProviderInstance["textGeneration"], +}); + +const personalAdapter = makeAdapter(personalId); +const workAdapter = makeAdapter(workId); +const instances = [ + makeInstance(personalId, personalAdapter), + makeInstance(workId, workAdapter), +] as const; +const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, { + getInstance: (instanceId) => + Effect.succeed(instances.find((instance) => instance.instanceId === instanceId)), + listInstances: Effect.succeed(instances), + listUnavailable: Effect.succeed([]), + streamChanges: Stream.empty, + subscribeChanges: Effect.never, +}); +const TestLayer = layerFromProviderInstanceRegistry.pipe(Layer.provide(instanceRegistryLayer)); + +it.effect("routes two configured instances of the same driver independently", () => + Effect.gen(function* () { + const registry = yield* ProviderAdapterRegistryV2; + + assert.strictEqual(yield* registry.get(personalId), personalAdapter); + assert.strictEqual(yield* registry.get(workId), workAdapter); + assert.deepEqual(yield* registry.list(), [personalId, workId]); + }).pipe(Effect.provide(TestLayer)), +); diff --git a/apps/server/src/orchestration-v2/ProviderAdapterRegistry.ts b/apps/server/src/orchestration-v2/ProviderAdapterRegistry.ts new file mode 100644 index 00000000000..f927cacdfa3 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderAdapterRegistry.ts @@ -0,0 +1,319 @@ +import { + ProviderInstanceId, + type OrchestrationV2ProviderCapabilities, + type ProviderDriverKind, + type ProviderInstanceConfig, + type ProviderInstanceConfigMap, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as Scope from "effect/Scope"; + +import { ProviderInstanceRegistry } from "../provider/Services/ProviderInstanceRegistry.ts"; +import { + ProviderAdapterDriverCreateError, + type AnyProviderAdapterDriver, +} from "./ProviderAdapterDriver.ts"; +import { ProviderAdapterV2, type ProviderAdapterV2Shape } from "./ProviderAdapter.ts"; + +export class ProviderAdapterRegistryLookupError extends Schema.TaggedErrorClass()( + "ProviderAdapterRegistryLookupError", + { + instanceId: ProviderInstanceId, + }, +) { + override get message(): string { + return `No orchestration provider adapter is registered for ${this.instanceId}.`; + } +} + +export class ProviderAdapterRegistryMetadataError extends Schema.TaggedErrorClass()( + "ProviderAdapterRegistryMetadataError", + { instanceId: ProviderInstanceId, cause: Schema.Defect() }, +) {} + +export const ProviderAdapterRegistryV2Error = Schema.Union([ + ProviderAdapterRegistryLookupError, + ProviderAdapterRegistryMetadataError, + ProviderAdapterDriverCreateError, +]); +export type ProviderAdapterRegistryV2Error = typeof ProviderAdapterRegistryV2Error.Type; + +export interface ProviderAdapterRegistryV2Shape { + readonly get: ( + instanceId: ProviderInstanceId, + ) => Effect.Effect; + readonly list: () => Effect.Effect>; + readonly getMetadata?: (instanceId: ProviderInstanceId) => Effect.Effect< + { + readonly driver: ProviderAdapterV2Shape["driver"]; + readonly continuationKey: string; + readonly enabled: boolean; + readonly capabilities: OrchestrationV2ProviderCapabilities; + }, + ProviderAdapterRegistryV2Error + >; +} + +export class ProviderAdapterRegistryV2 extends Context.Service< + ProviderAdapterRegistryV2, + ProviderAdapterRegistryV2Shape +>()("t3/orchestration-v2/ProviderAdapterRegistry/ProviderAdapterRegistryV2") {} + +/** + * Production facade over the canonical provider-instance registry. Adapter + * lookup stays dynamic so instance hot reloads and removals are visible + * without maintaining a second settings watcher or instance map. + */ +export const layerFromProviderInstanceRegistry: Layer.Layer< + ProviderAdapterRegistryV2, + never, + ProviderInstanceRegistry +> = Layer.effect( + ProviderAdapterRegistryV2, + Effect.gen(function* () { + const instances = yield* ProviderInstanceRegistry; + return ProviderAdapterRegistryV2.of({ + get: (instanceId) => + instances + .getInstance(instanceId) + .pipe( + Effect.flatMap((instance) => + instance === undefined + ? new ProviderAdapterRegistryLookupError({ instanceId }) + : Effect.succeed(instance.orchestrationAdapter), + ), + ), + list: () => + instances.listInstances.pipe( + Effect.map((available) => available.map((instance) => instance.instanceId)), + ), + getMetadata: (instanceId) => + Effect.gen(function* () { + const instance = yield* instances.getInstance(instanceId); + if (instance === undefined) { + return yield* new ProviderAdapterRegistryLookupError({ instanceId }); + } + const capabilities = yield* instance.orchestrationAdapter + .getCapabilities() + .pipe( + Effect.mapError( + (cause) => new ProviderAdapterRegistryMetadataError({ instanceId, cause }), + ), + ); + return { + driver: instance.driverKind, + continuationKey: instance.continuationIdentity.continuationKey, + enabled: instance.enabled, + capabilities, + }; + }), + }); + }), +); + +export const ProviderAdapterRegistryBuildError = Schema.Union([ProviderAdapterDriverCreateError]); +export type ProviderAdapterRegistryBuildError = typeof ProviderAdapterRegistryBuildError.Type; + +function makeRegistry( + adapters: ReadonlyArray, +): ProviderAdapterRegistryV2Shape { + return { + get: (instanceId) => + Effect.gen(function* () { + const adapter = adapters.find((candidate) => candidate.instanceId === instanceId); + if (!adapter) { + return yield* new ProviderAdapterRegistryLookupError({ instanceId }); + } + return adapter; + }), + list: () => Effect.succeed(adapters.map((adapter) => adapter.instanceId)), + }; +} + +export function makeLayer( + adapters: ReadonlyArray, +): Layer.Layer { + return Layer.succeed( + ProviderAdapterRegistryV2, + ProviderAdapterRegistryV2.of(makeRegistry(adapters)), + ); +} + +export function makeLayerEffect( + adapters: Effect.Effect, E, R>, +): Layer.Layer { + return Layer.effect( + ProviderAdapterRegistryV2, + adapters.pipe(Effect.map((entries) => ProviderAdapterRegistryV2.of(makeRegistry(entries)))), + ); +} + +export function makeSingleLayer( + adapter: ProviderAdapterV2Shape, +): Layer.Layer { + return makeLayer([adapter]); +} + +const decodedConfigEnabled = (config: unknown): boolean | undefined => { + if (!config || typeof config !== "object" || globalThis.Array.isArray(config)) { + return undefined; + } + const enabled = (config as { readonly enabled?: unknown }).enabled; + return typeof enabled === "boolean" ? enabled : undefined; +}; + +interface LiveAdapterEntry { + readonly adapter: ProviderAdapterV2Shape; + readonly scope: Scope.Closeable; + readonly entry: ProviderInstanceConfig; +} + +function makeDriversById( + drivers: ReadonlyArray>, +): ReadonlyMap> { + return new Map>( + drivers.map((driver) => [driver.driverKind, driver]), + ); +} + +const createAdapterEntryFromConfigEntry = Effect.fn( + "ProviderAdapterRegistry.createAdapterEntryFromConfigEntry", +)(function* (input: { + readonly driversById: ReadonlyMap>; + readonly parentScope: Scope.Scope; + readonly instanceId: ProviderInstanceId; + readonly entry: ProviderInstanceConfig; +}): Effect.fn.Return { + const driver = input.driversById.get(input.entry.driver); + if (driver === undefined) { + return yield* new ProviderAdapterDriverCreateError({ + driver: input.entry.driver, + instanceId: input.instanceId, + detail: "Unknown provider driver.", + }); + } + + const decodeConfig = Schema.decodeUnknownEffect(driver.configSchema); + const typedConfig = yield* decodeConfig(input.entry.config ?? driver.defaultConfig()).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterDriverCreateError({ + driver: input.entry.driver, + instanceId: input.instanceId, + detail: "Invalid provider instance config.", + cause, + }), + ), + ); + + const childScope = yield* Scope.make(); + yield* Scope.addFinalizer( + input.parentScope, + Scope.close(childScope, Exit.void).pipe(Effect.ignore), + ); + + const adapter = yield* driver + .create({ + instanceId: input.instanceId, + displayName: input.entry.displayName, + accentColor: input.entry.accentColor, + environment: input.entry.environment ?? [], + enabled: input.entry.enabled ?? decodedConfigEnabled(typedConfig) ?? true, + config: typedConfig, + }) + .pipe( + Effect.provideService(Scope.Scope, childScope), + Effect.tapError(() => Scope.close(childScope, Exit.void).pipe(Effect.ignore)), + ); + + return { + adapter, + scope: childScope, + entry: input.entry, + }; +}); + +const buildAdaptersFromConfigMap = Effect.fn("ProviderAdapterRegistry.buildAdaptersFromConfigMap")( + function* (input: { + readonly drivers: ReadonlyArray>; + readonly configMap: ProviderInstanceConfigMap; + readonly parentScope: Scope.Scope; + }): Effect.fn.Return< + ReadonlyMap, + ProviderAdapterRegistryBuildError, + R + > { + const driversById = makeDriversById(input.drivers); + const adapters = new Map(); + + for (const [rawInstanceId, entry] of Object.entries(input.configMap)) { + const instanceId = ProviderInstanceId.make(rawInstanceId); + if (!driversById.has(entry.driver)) { + yield* Effect.logWarning("Skipping orchestration-v2 provider adapter with unknown driver", { + instanceId, + driver: entry.driver, + }); + continue; + } + + const adapter = yield* createAdapterEntryFromConfigEntry({ + driversById, + parentScope: input.parentScope, + instanceId, + entry, + }); + adapters.set(instanceId, adapter); + } + + return adapters; + }, +); + +export function makeRegistryFromConfigMap(input: { + readonly drivers: ReadonlyArray>; + readonly configMap: ProviderInstanceConfigMap; +}): Effect.Effect< + ProviderAdapterRegistryV2Shape, + ProviderAdapterRegistryBuildError, + R | Scope.Scope +> { + return Effect.gen(function* () { + const parentScope = yield* Effect.scope; + const entries = yield* buildAdaptersFromConfigMap({ ...input, parentScope }); + return makeRegistry(Array.from(entries.values()).map((entry) => entry.adapter)); + }); +} + +export function makeDriverLayer(input: { + readonly drivers: ReadonlyArray>; + readonly configMap: ProviderInstanceConfigMap; +}): Layer.Layer { + return Layer.effect( + ProviderAdapterRegistryV2, + makeRegistryFromConfigMap(input).pipe( + Effect.map((registry) => ProviderAdapterRegistryV2.of(registry)), + ), + ) as Layer.Layer; +} + +export const layerFromProviderAdapter: Layer.Layer< + ProviderAdapterRegistryV2, + never, + ProviderAdapterV2 +> = Layer.effect( + ProviderAdapterRegistryV2, + Effect.gen(function* () { + const adapter = yield* ProviderAdapterV2; + return ProviderAdapterRegistryV2.of({ + get: (instanceId) => + adapter.instanceId === instanceId + ? Effect.succeed(adapter) + : Effect.fail(new ProviderAdapterRegistryLookupError({ instanceId })), + list: () => Effect.succeed([adapter.instanceId]), + } satisfies ProviderAdapterRegistryV2Shape); + }), +); diff --git a/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts b/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts new file mode 100644 index 00000000000..49555e28c81 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts @@ -0,0 +1,299 @@ +import { assert, it } from "@effect/vitest"; +import { + MessageId, + type ModelSelection, + NodeId, + type OrchestrationV2AppThread, + type OrchestrationV2DomainEvent, + type OrchestrationV2ProviderThread, + ProviderDriverKind, + ProviderInstanceId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; + +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import { EventSinkV2, layer as eventSinkLayer } from "./EventSink.ts"; +import { EventStoreV2, layer as eventStoreLayer } from "./EventStore.ts"; +import { + IdAllocatorV2, + type IdAllocatorV2Error, + layer as idAllocatorLayer, +} from "./IdAllocator.ts"; +import { ProjectionStoreV2, layer as projectionStoreLayer } from "./ProjectionStore.ts"; +import { + ProviderEventIngestorV2, + layer as providerEventIngestorLayer, +} from "./ProviderEventIngestor.ts"; + +const TestDatabaseLayer = SqlitePersistenceMemory; +const TestStoresLayer = Layer.merge(eventStoreLayer, projectionStoreLayer).pipe( + Layer.provide(TestDatabaseLayer), +); + +const TestEventSinkLayer = eventSinkLayer.pipe( + Layer.provide(Layer.mergeAll(TestStoresLayer, TestDatabaseLayer)), +); + +const TestLayer = Layer.mergeAll( + TestStoresLayer, + TestEventSinkLayer, + idAllocatorLayer, + providerEventIngestorLayer.pipe( + Layer.provide(Layer.mergeAll(TestStoresLayer, TestEventSinkLayer, idAllocatorLayer)), + ), +); +const modelSelection = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", +} satisfies ModelSelection; +const CODEX_DRIVER = ProviderDriverKind.make("codex"); + +function threadCreatedEvent( + now: DateTime.Utc, +): Effect.Effect { + return Effect.gen(function* () { + const idAllocator = yield* IdAllocatorV2; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-event-ingestor", + }); + const threadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-event-ingestor", + projectId, + }); + const providerThreadId = idAllocator.derive.providerThread({ + driver: CODEX_DRIVER, + nativeThreadId: "native-thread", + }); + const thread: OrchestrationV2AppThread = { + createdBy: "user", + creationSource: "web", + id: threadId, + projectId, + title: "Provider event ingestor", + providerInstanceId: modelSelection.instanceId, + modelSelection: modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: providerThreadId, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: threadId, + }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }; + + return { + id: yield* idAllocator.allocate.event({ threadId }), + type: "thread.created", + threadId, + occurredAt: now, + payload: thread, + }; + }); +} + +const layer = it.layer(TestLayer); + +layer("ProviderEventIngestorV2", (it) => { + it.effect("normalizes provider events through the real event log and projection store", () => + Effect.gen(function* () { + const now = yield* DateTime.now; + const eventSink = yield* EventSinkV2; + const eventStore = yield* EventStoreV2; + const projectionStore = yield* ProjectionStoreV2; + const ingestor = yield* ProviderEventIngestorV2; + const idAllocator = yield* IdAllocatorV2; + const threadEvent = yield* threadCreatedEvent(now); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId: threadEvent.threadId, + }); + const providerThread: OrchestrationV2ProviderThread = { + id: idAllocator.derive.providerThread({ + driver: CODEX_DRIVER, + nativeThreadId: "native-thread", + }), + driver: CODEX_DRIVER, + providerInstanceId: modelSelection.instanceId, + providerSessionId, + appThreadId: threadEvent.threadId, + ownerNodeId: null, + nativeThreadRef: { + driver: CODEX_DRIVER, + nativeId: "native-thread", + strength: "strong", + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: null, + createdAt: now, + updatedAt: now, + }; + + yield* eventSink.write({ events: [threadEvent] }); + const storedEvents = yield* ingestor.ingestNormalized({ + providerSessionId, + providerInstanceId: modelSelection.instanceId, + threadId: threadEvent.threadId, + event: { + type: "provider_thread.updated", + driver: CODEX_DRIVER, + providerThread, + }, + }); + + const projection = yield* projectionStore.getThreadProjection(threadEvent.threadId); + const storedDomainEvents = yield* eventStore.read({}).pipe(Stream.runCollect); + const afterFirstEvent = yield* eventStore + .read({ afterSequence: 1, threadId: threadEvent.threadId }) + .pipe(Stream.runCollect); + const latestThreadSequence = yield* eventStore.latestSequence({ + threadId: threadEvent.threadId, + }); + + assert.equal(storedEvents.length, 1); + assert.equal(storedEvents[0]?.event.type, "provider-thread.updated"); + assert.deepEqual( + projection.providerThreads.map((thread) => thread.id), + [providerThread.id], + ); + assert.deepEqual( + Array.from(storedDomainEvents).map((stored) => stored.event.type), + ["thread.created", "provider-thread.updated"], + ); + assert.deepEqual( + Array.from(storedDomainEvents).map((stored) => stored.sequence), + [1, 2], + ); + assert.deepEqual( + Array.from(afterFirstEvent).map((stored) => stored.event.type), + ["provider-thread.updated"], + ); + assert.equal(latestThreadSequence, 2); + }), + ); + + it.effect( + "treats provider terminal markers as orchestration control signals, not persisted domain events", + () => + Effect.gen(function* () { + const ingestor = yield* ProviderEventIngestorV2; + const idAllocator = yield* IdAllocatorV2; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-event-terminal", + }); + const threadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-event-terminal", + projectId, + }); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId, + }); + const normalized = yield* ingestor.normalize({ + providerSessionId, + providerInstanceId: modelSelection.instanceId, + threadId, + event: { + type: "turn.terminal", + driver: CODEX_DRIVER, + providerTurnId: idAllocator.derive.providerTurn({ + driver: CODEX_DRIVER, + nativeTurnId: "native-turn", + }), + status: "completed", + }, + }); + + assert.deepEqual(normalized, []); + }), + ); + + it.effect("routes provider-owned child artifacts to their child app thread", () => + Effect.gen(function* () { + const now = yield* DateTime.now; + const ingestor = yield* ProviderEventIngestorV2; + const idAllocator = yield* IdAllocatorV2; + const rootEvent = yield* threadCreatedEvent(now); + if (rootEvent.type !== "thread.created") { + throw new Error("Expected a thread.created fixture event"); + } + const childThreadId = idAllocator.derive.threadFromProviderThread({ + driver: CODEX_DRIVER, + nativeThreadId: "native-subagent-thread", + }); + const childRootNodeId = NodeId.make("node:subagent-root"); + const childThread: OrchestrationV2AppThread = { + ...rootEvent.payload, + id: childThreadId, + title: "Subagent: inspect package", + activeProviderThreadId: null, + lineage: { + parentThreadId: rootEvent.threadId, + relationshipToParent: "subagent", + rootThreadId: rootEvent.threadId, + }, + forkedFrom: { + type: "node", + nodeId: NodeId.make("node:parent-subagent"), + }, + }; + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId: rootEvent.threadId, + }); + + const threadEvents = yield* ingestor.normalize({ + providerSessionId, + providerInstanceId: modelSelection.instanceId, + threadId: rootEvent.threadId, + event: { + type: "app_thread.created", + driver: CODEX_DRIVER, + appThread: childThread, + }, + }); + const messageEvents = yield* ingestor.normalize({ + providerSessionId, + providerInstanceId: modelSelection.instanceId, + threadId: rootEvent.threadId, + event: { + type: "message.updated", + driver: CODEX_DRIVER, + message: { + createdBy: "agent", + creationSource: "provider", + id: MessageId.make("message:subagent-response"), + threadId: childThreadId, + runId: null, + nodeId: childRootNodeId, + role: "assistant", + text: "Subagent result", + attachments: [], + streaming: false, + createdAt: now, + updatedAt: now, + }, + }, + }); + + assert.equal(threadEvents[0]?.type, "thread.created"); + assert.equal(threadEvents[0]?.threadId, childThreadId); + assert.equal(messageEvents[0]?.type, "message.updated"); + assert.equal(messageEvents[0]?.threadId, childThreadId); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/ProviderEventIngestor.ts b/apps/server/src/orchestration-v2/ProviderEventIngestor.ts new file mode 100644 index 00000000000..7f40457c906 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderEventIngestor.ts @@ -0,0 +1,271 @@ +import { + NodeId, + CommandId, + OrchestrationV2DomainEvent, + OrchestrationV2StoredEvent, + ProviderInstanceId, + ProviderSessionId, + RawEventId, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import { EventSinkV2 } from "./EventSink.ts"; +import { IdAllocatorV2 } from "./IdAllocator.ts"; +import { ProviderAdapterV2Event } from "./ProviderAdapter.ts"; + +export class ProviderEventNormalizeError extends Schema.TaggedErrorClass()( + "ProviderEventNormalizeError", + { + providerSessionId: ProviderSessionId, + threadId: ThreadId, + providerEvent: ProviderAdapterV2Event, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to normalize provider event ${this.providerEvent.type} for thread ${this.threadId}.`; + } +} + +export class ProviderEventPublishError extends Schema.TaggedErrorClass()( + "ProviderEventPublishError", + { + providerSessionId: ProviderSessionId, + eventCount: Schema.Number, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to publish ${this.eventCount} normalized provider event(s).`; + } +} + +export const ProviderEventIngestorV2Error = Schema.Union([ + ProviderEventNormalizeError, + ProviderEventPublishError, +]); +export type ProviderEventIngestorV2Error = typeof ProviderEventIngestorV2Error.Type; + +export interface ProviderEventIngestorV2Shape { + readonly normalize: (input: { + readonly providerSessionId: ProviderSessionId; + readonly providerInstanceId: ProviderInstanceId; + readonly commandId?: CommandId; + readonly threadId: ThreadId; + readonly runId?: RunId; + readonly nodeId?: NodeId; + readonly rawEventId?: RawEventId; + readonly event: ProviderAdapterV2Event; + }) => Effect.Effect, ProviderEventIngestorV2Error>; + readonly ingestNormalized: (input: { + readonly providerSessionId: ProviderSessionId; + readonly providerInstanceId: ProviderInstanceId; + readonly commandId?: CommandId; + readonly threadId: ThreadId; + readonly runId?: RunId; + readonly nodeId?: NodeId; + readonly rawEventId?: RawEventId; + readonly event: ProviderAdapterV2Event; + }) => Effect.Effect, ProviderEventIngestorV2Error>; +} + +export class ProviderEventIngestorV2 extends Context.Service< + ProviderEventIngestorV2, + ProviderEventIngestorV2Shape +>()("t3/orchestration-v2/ProviderEventIngestor/ProviderEventIngestorV2") {} + +function compactUndefined>(record: T): T { + return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as T; +} + +export const layer: Layer.Layer = + Layer.effect( + ProviderEventIngestorV2, + Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + + const makeDomainEvent = ( + input: { + readonly providerSessionId: ProviderSessionId; + readonly providerInstanceId: ProviderInstanceId; + readonly commandId?: CommandId; + readonly threadId: ThreadId; + readonly runId?: RunId; + readonly nodeId?: NodeId; + readonly rawEventId?: RawEventId; + readonly event: ProviderAdapterV2Event; + }, + payloadInput: { + readonly type: OrchestrationV2DomainEvent["type"]; + readonly payload: OrchestrationV2DomainEvent["payload"]; + readonly threadId?: ThreadId; + readonly runId?: RunId | null; + readonly nodeId?: NodeId | null; + }, + ) => + Effect.gen(function* () { + const threadId = payloadInput.threadId ?? input.threadId; + const eventId = yield* idAllocator.allocate.event({ + threadId, + providerSessionId: input.providerSessionId, + }); + const occurredAt = yield* DateTime.now; + return yield* Schema.decodeUnknownEffect(OrchestrationV2DomainEvent)( + compactUndefined({ + id: eventId, + type: payloadInput.type, + threadId, + runId: payloadInput.runId ?? input.runId, + nodeId: payloadInput.nodeId ?? input.nodeId, + driver: input.event.driver, + providerInstanceId: input.providerInstanceId, + rawEventId: input.rawEventId, + occurredAt, + payload: payloadInput.payload, + }), + ); + }); + + const normalize: ProviderEventIngestorV2Shape["normalize"] = (input) => + Effect.gen(function* () { + switch (input.event.type) { + case "app_thread.created": + return [ + yield* makeDomainEvent(input, { + type: "thread.created", + threadId: input.event.appThread.id, + payload: input.event.appThread, + }), + ]; + case "provider_session.updated": + return [ + yield* makeDomainEvent(input, { + type: "provider-session.updated", + payload: input.event.providerSession, + }), + ]; + case "provider_thread.updated": + return [ + yield* makeDomainEvent(input, { + type: "provider-thread.updated", + threadId: input.event.providerThread.appThreadId ?? input.threadId, + payload: input.event.providerThread, + }), + ]; + case "provider_turn.updated": + return [ + yield* makeDomainEvent(input, { + type: "provider-turn.updated", + ...(input.event.threadId === undefined ? {} : { threadId: input.event.threadId }), + payload: input.event.providerTurn, + nodeId: input.event.providerTurn.nodeId, + }), + ]; + case "node.updated": + return [ + yield* makeDomainEvent(input, { + type: "node.updated", + threadId: input.event.node.threadId, + payload: input.event.node, + runId: input.event.node.runId, + nodeId: input.event.node.id, + }), + ]; + case "subagent.updated": + return [ + yield* makeDomainEvent(input, { + type: "subagent.updated", + threadId: input.event.subagent.threadId, + payload: input.event.subagent, + runId: input.event.subagent.runId, + nodeId: input.event.subagent.id, + }), + ]; + case "message.updated": + return [ + yield* makeDomainEvent(input, { + type: "message.updated", + threadId: input.event.message.threadId, + payload: input.event.message, + runId: input.event.message.runId, + nodeId: input.event.message.nodeId, + }), + ]; + case "turn_item.updated": + return [ + yield* makeDomainEvent(input, { + type: "turn-item.updated", + threadId: input.event.turnItem.threadId, + payload: input.event.turnItem, + runId: input.event.turnItem.runId, + nodeId: input.event.turnItem.nodeId, + }), + ]; + case "runtime_request.updated": + return [ + yield* makeDomainEvent(input, { + type: "runtime-request.updated", + ...(input.event.threadId === undefined ? {} : { threadId: input.event.threadId }), + payload: input.event.runtimeRequest, + nodeId: input.event.runtimeRequest.nodeId, + }), + ]; + case "plan.updated": + return [ + yield* makeDomainEvent(input, { + type: "plan.updated", + threadId: input.event.plan.threadId, + payload: input.event.plan, + runId: input.event.plan.runId, + nodeId: input.event.plan.nodeId, + }), + ]; + case "turn.terminal": + return []; + } + }).pipe( + Effect.mapError( + (cause) => + new ProviderEventNormalizeError({ + providerSessionId: input.providerSessionId, + threadId: input.threadId, + providerEvent: input.event, + cause, + }), + ), + ); + + return ProviderEventIngestorV2.of({ + normalize, + ingestNormalized: (input) => + Effect.gen(function* () { + const events = yield* normalize(input); + if (events.length === 0) { + return []; + } + return yield* eventSink + .write({ + ...(input.commandId === undefined ? {} : { commandId: input.commandId }), + events, + }) + .pipe( + Effect.mapError( + (cause) => + new ProviderEventPublishError({ + providerSessionId: input.providerSessionId, + eventCount: events.length, + cause, + }), + ), + ); + }), + }); + }), + ); diff --git a/apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.test.ts b/apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.test.ts new file mode 100644 index 00000000000..e878fe2d878 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.test.ts @@ -0,0 +1,260 @@ +import { assert, it, vi } from "@effect/vitest"; +import { + NodeId, + RuntimeRequestId, + ThreadId, + type OrchestrationV2ThreadProjection, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Ref from "effect/Ref"; + +import * as EffectWorker from "./EffectWorker.ts"; +import * as EffectOutbox from "./EffectOutbox.ts"; +import * as EventSink from "./EventSink.ts"; +import * as IdAllocator from "./IdAllocator.ts"; +import * as ProjectionStore from "./ProjectionStore.ts"; +import * as ProviderRuntimeRecovery from "./ProviderRuntimeRecoveryService.ts"; +import * as ProviderSessionManager from "./ProviderSessionManager.ts"; + +const { decideProviderRuntimeRecovery } = ProviderRuntimeRecovery; + +it("retries process and transport failures only within the idempotent retry budget", () => { + assert.deepEqual( + decideProviderRuntimeRecovery({ + kind: "process_exited", + attempt: 1, + maxAttempts: 3, + idempotent: true, + online: true, + }), + { type: "retry_now" }, + ); + assert.equal( + decideProviderRuntimeRecovery({ + kind: "transport_unavailable", + attempt: 3, + maxAttempts: 3, + idempotent: true, + online: true, + }).type, + "terminalize", + ); +}); + +it("waits for connectivity and requires retry-after for rate limits", () => { + assert.deepEqual( + decideProviderRuntimeRecovery({ + kind: "network_unavailable", + attempt: 0, + maxAttempts: 3, + idempotent: true, + online: false, + }), + { type: "wait_for_connectivity" }, + ); + assert.deepEqual( + decideProviderRuntimeRecovery({ + kind: "provider_rate_limited", + attempt: 0, + maxAttempts: 3, + idempotent: true, + retryAfterMs: 250, + online: true, + }), + { type: "retry_after", delayMs: 250 }, + ); +}); + +it("terminalizes non-recoverable provider failures", () => { + for (const kind of [ + "provider_quota_exceeded", + "auth_invalid", + "permission_denied", + "invalid_request", + "unsupported_model", + ] as const) { + assert.equal( + decideProviderRuntimeRecovery({ + kind, + attempt: 0, + maxAttempts: 3, + idempotent: true, + online: true, + }).type, + "terminalize", + ); + } +}); + +it("classifies wrapped provider failures without provider-name checks", () => { + assert.deepEqual( + ProviderRuntimeRecovery.classifyProviderRuntimeFailure({ + _tag: "ProviderSessionOpenError", + cause: { code: "rate_limit_exceeded", retryAfterMs: 125 }, + }), + { kind: "provider_rate_limited", retryAfterMs: 125 }, + ); + assert.deepEqual( + ProviderRuntimeRecovery.classifyProviderRuntimeFailure({ + cause: { status: 401, message: "authentication failed" }, + }), + { kind: "auth_invalid" }, + ); +}); + +it.effect("executes bounded transport retries and returns the resumed value", () => + Effect.gen(function* () { + const attempts = yield* Ref.make(0); + const value = yield* ProviderRuntimeRecovery.recoverWithPolicy({ + operation: Ref.getAndUpdate(attempts, (count) => count + 1).pipe( + Effect.flatMap((count) => + count < 2 ? Effect.fail("transport down") : Effect.succeed("resumed"), + ), + ), + classify: () => ({ kind: "transport_unavailable" }), + connectivity: { isOnline: Effect.succeed(true), awaitOnline: Effect.void }, + maxAttempts: 3, + idempotent: true, + }); + assert.equal(value, "resumed"); + assert.equal(yield* Ref.get(attempts), 3); + }), +); + +it.effect("waits for connectivity before retrying a network failure", () => + Effect.gen(function* () { + const attempts = yield* Ref.make(0); + const online = yield* Ref.make(false); + const waits = yield* Ref.make(0); + const value = yield* ProviderRuntimeRecovery.recoverWithPolicy({ + operation: Ref.getAndUpdate(attempts, (count) => count + 1).pipe( + Effect.flatMap((count) => + count === 0 ? Effect.fail("offline") : Effect.succeed("resumed"), + ), + ), + classify: () => ({ kind: "network_unavailable" }), + connectivity: { + isOnline: Ref.get(online), + awaitOnline: Ref.set(online, true).pipe( + Effect.andThen(Ref.update(waits, (count) => count + 1)), + ), + }, + maxAttempts: 3, + idempotent: true, + }); + assert.equal(value, "resumed"); + assert.equal(yield* Ref.get(waits), 1); + }), +); + +it.effect("does not retry unrecoverable failures", () => + Effect.gen(function* () { + const attempts = yield* Ref.make(0); + const result = yield* Effect.result( + ProviderRuntimeRecovery.recoverWithPolicy({ + operation: Ref.update(attempts, (count) => count + 1).pipe( + Effect.andThen(Effect.fail("invalid credentials")), + ), + classify: () => ({ kind: "auth_invalid" }), + connectivity: { isOnline: Effect.succeed(true), awaitOnline: Effect.void }, + maxAttempts: 3, + idempotent: true, + }), + ); + assert.equal(result._tag, "Failure"); + assert.equal(yield* Ref.get(attempts), 1); + }), +); + +it.effect("drains durable effects before reporting recovery complete", () => + Effect.gen(function* () { + const runs = yield* Ref.make(0); + const layer = ProviderRuntimeRecovery.layer.pipe( + Layer.provide( + Layer.mergeAll( + Layer.mock(ProjectionStore.ProjectionStoreV2)({ + getShellSnapshot: () => + Effect.succeed({ + schemaVersion: 2, + snapshotSequence: 0, + threads: [], + archivedThreads: [], + }), + }), + Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({}), + Layer.mock(EventSink.EventSinkV2)({}), + IdAllocator.layer, + Layer.mock(EffectWorker.OrchestrationEffectWorkerV2)({ + runOnce: Ref.getAndUpdate(runs, (count) => count + 1).pipe( + Effect.map((count) => count < 2), + ), + }), + Layer.mock(EffectOutbox.EffectOutboxV2)({ reclaimRunning: Effect.succeed(0) }), + ), + ), + ); + const summary = yield* Effect.gen(function* () { + return yield* (yield* ProviderRuntimeRecovery.ProviderRuntimeRecoveryService).recover; + }).pipe(Effect.provide(layer)); + assert.deepEqual(summary, { resumedSessions: 0, terminalizedRuns: 0, executedEffects: 2 }); + }), +); + +it.effect("expires orphaned runtime requests before command readiness", () => { + const threadId = ThreadId.make("thread_recovery_requests"); + let committedInput: Parameters[0] | null = + null; + const committed = vi.fn( + (input: Parameters[0]) => { + committedInput = input; + return Effect.succeed({ committed: true } as never); + }, + ); + const projection = { + thread: { id: threadId }, + runtimeRequests: [ + { + id: RuntimeRequestId.make("request_orphaned"), + nodeId: NodeId.make("node_orphaned"), + status: "pending", + responseCapability: { type: "not_resumable", reason: "old process" }, + }, + ], + providerSessions: [], + runs: [], + nodes: [], + } as unknown as OrchestrationV2ThreadProjection; + const layer = ProviderRuntimeRecovery.layer.pipe( + Layer.provide( + Layer.mergeAll( + Layer.mock(ProjectionStore.ProjectionStoreV2)({ + getShellSnapshot: () => + Effect.succeed({ + schemaVersion: 2, + snapshotSequence: 0, + threads: [{ id: threadId }], + archivedThreads: [], + } as never), + getThreadProjection: () => Effect.succeed(projection), + }), + Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({}), + Layer.mock(EventSink.EventSinkV2)({ commitCommand: committed }), + IdAllocator.layer, + Layer.mock(EffectWorker.OrchestrationEffectWorkerV2)({ runOnce: Effect.succeed(false) }), + Layer.mock(EffectOutbox.EffectOutboxV2)({ reclaimRunning: Effect.succeed(0) }), + ), + ), + ); + return Effect.gen(function* () { + yield* (yield* ProviderRuntimeRecovery.ProviderRuntimeRecoveryService).recover; + const command = committedInput; + assert.isNotNull(command); + if (command === null) return; + assert.equal(command?.events[0]?.type, "runtime-request.updated"); + if (command?.events[0]?.type === "runtime-request.updated") { + assert.equal(command.events[0].payload.status, "expired"); + assert.equal(command.events[0].payload.responseCapability.type, "not_resumable"); + } + }).pipe(Effect.provide(layer)); +}); diff --git a/apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.ts b/apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.ts new file mode 100644 index 00000000000..3915802f2c7 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.ts @@ -0,0 +1,508 @@ +import { + CommandId, + type OrchestrationV2DomainEvent, + type OrchestrationV2ThreadProjection, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import * as EffectWorker from "./EffectWorker.ts"; +import * as EffectOutbox from "./EffectOutbox.ts"; +import * as EventSink from "./EventSink.ts"; +import * as IdAllocator from "./IdAllocator.ts"; +import * as ProjectionStore from "./ProjectionStore.ts"; +import * as ProviderSessionManager from "./ProviderSessionManager.ts"; + +export class ProviderRuntimeRecoveryError extends Schema.TaggedErrorClass()( + "ProviderRuntimeRecoveryError", + { + operation: Schema.Literals([ + "read-projections", + "resume-session", + "terminalize", + "drain-outbox", + ]), + threadId: Schema.optional(ThreadId), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Provider runtime recovery failed during ${this.operation}.`; + } +} + +export interface ProviderRuntimeRecoverySummary { + readonly resumedSessions: number; + readonly terminalizedRuns: number; + readonly executedEffects: number; +} + +export const ProviderRuntimeFailureKind = Schema.Literals([ + "process_exited", + "transport_unavailable", + "network_unavailable", + "provider_rate_limited", + "provider_quota_exceeded", + "auth_invalid", + "permission_denied", + "invalid_request", + "unsupported_model", +]); +export type ProviderRuntimeFailureKind = typeof ProviderRuntimeFailureKind.Type; + +export type ProviderRuntimeRecoveryDecision = + | { readonly type: "retry_now" } + | { readonly type: "retry_after"; readonly delayMs: number } + | { readonly type: "wait_for_connectivity" } + | { readonly type: "terminalize"; readonly reason: string }; + +export function decideProviderRuntimeRecovery(input: { + readonly kind: ProviderRuntimeFailureKind; + readonly attempt: number; + readonly maxAttempts: number; + readonly idempotent: boolean; + readonly retryAfterMs?: number; + readonly online: boolean; +}): ProviderRuntimeRecoveryDecision { + if (input.attempt >= input.maxAttempts) { + return { type: "terminalize", reason: "Provider recovery retry budget was exhausted." }; + } + switch (input.kind) { + case "process_exited": + case "transport_unavailable": + return input.idempotent + ? { type: "retry_now" } + : { type: "terminalize", reason: "The interrupted operation is not idempotent." }; + case "network_unavailable": + return input.online ? { type: "retry_now" } : { type: "wait_for_connectivity" }; + case "provider_rate_limited": + return input.idempotent && input.retryAfterMs !== undefined + ? { type: "retry_after", delayMs: Math.max(0, input.retryAfterMs) } + : { + type: "terminalize", + reason: "Rate-limit recovery requires retry-after and an idempotent operation.", + }; + case "provider_quota_exceeded": + case "auth_invalid": + case "permission_denied": + case "invalid_request": + case "unsupported_model": + return { type: "terminalize", reason: `Provider failure ${input.kind} is not recoverable.` }; + } +} + +export interface ConnectivityServiceShape { + readonly isOnline: Effect.Effect; + readonly awaitOnline: Effect.Effect; +} + +export class ConnectivityService extends Context.Reference( + "t3/orchestration-v2/ConnectivityService", + { + defaultValue: () => ({ isOnline: Effect.succeed(true), awaitOnline: Effect.void }), + }, +) {} + +export interface ClassifiedProviderRuntimeFailure { + readonly kind: ProviderRuntimeFailureKind; + readonly retryAfterMs?: number; +} + +export function classifyProviderRuntimeFailure(cause: unknown): ClassifiedProviderRuntimeFailure { + const record = + cause !== null && typeof cause === "object" + ? (cause as Record) + : ({} as Record); + const serialized = (() => { + try { + return JSON.stringify(cause); + } catch { + return ""; + } + })(); + const description = [ + record._tag, + record.code, + record.status, + record.message, + record.detail, + serialized, + ] + .filter((value) => typeof value === "string" || typeof value === "number") + .join(" ") + .toLowerCase(); + const nestedCause = + record.cause !== null && typeof record.cause === "object" + ? (record.cause as Record) + : undefined; + const retryAfter = + record.retryAfterMs ?? + record.retry_after_ms ?? + nestedCause?.retryAfterMs ?? + nestedCause?.retry_after_ms; + const retryAfterMs = typeof retryAfter === "number" ? retryAfter : undefined; + if (description.includes("rate") && description.includes("limit")) { + return { + kind: "provider_rate_limited", + ...(retryAfterMs === undefined ? {} : { retryAfterMs }), + }; + } + if (description.includes("quota")) return { kind: "provider_quota_exceeded" }; + if (description.includes("auth") || description.includes("unauthorized")) { + return { kind: "auth_invalid" }; + } + if (description.includes("permission") || description.includes("forbidden")) { + return { kind: "permission_denied" }; + } + if (description.includes("unsupported") && description.includes("model")) { + return { kind: "unsupported_model" }; + } + if (description.includes("invalid") && description.includes("request")) { + return { kind: "invalid_request" }; + } + if ( + description.includes("network") || + description.includes("offline") || + description.includes("econnreset") || + description.includes("enotfound") + ) { + return { kind: "network_unavailable" }; + } + if (description.includes("exit") || description.includes("terminated")) { + return { kind: "process_exited" }; + } + return { kind: "transport_unavailable" }; +} + +export class ProviderRuntimeFailureClassifier extends Context.Reference<{ + readonly classify: (cause: unknown) => ClassifiedProviderRuntimeFailure; +}>("t3/orchestration-v2/ProviderRuntimeFailureClassifier", { + defaultValue: () => ({ classify: classifyProviderRuntimeFailure }), +}) {} + +export class ProviderRuntimeRecoveryPolicy extends Context.Reference<{ + readonly maxAttempts: number; +}>("t3/orchestration-v2/ProviderRuntimeRecoveryPolicy", { + defaultValue: () => ({ maxAttempts: 3 }), +}) {} + +export function recoverWithPolicy(input: { + readonly operation: Effect.Effect; + readonly classify: (cause: E) => ClassifiedProviderRuntimeFailure; + readonly connectivity: ConnectivityServiceShape; + readonly maxAttempts: number; + readonly idempotent: boolean; +}): Effect.Effect { + const attempt = (attemptNumber: number): Effect.Effect => + input.operation.pipe( + Effect.catch((cause) => + Effect.gen(function* () { + const classified = input.classify(cause); + const online = yield* input.connectivity.isOnline; + const decision = decideProviderRuntimeRecovery({ + kind: classified.kind, + attempt: attemptNumber, + maxAttempts: input.maxAttempts, + idempotent: input.idempotent, + online, + ...(classified.retryAfterMs === undefined + ? {} + : { retryAfterMs: classified.retryAfterMs }), + }); + switch (decision.type) { + case "retry_now": + return yield* attempt(attemptNumber + 1); + case "retry_after": + yield* Effect.sleep(Duration.millis(decision.delayMs)); + return yield* attempt(attemptNumber + 1); + case "wait_for_connectivity": + yield* input.connectivity.awaitOnline; + return yield* attempt(attemptNumber + 1); + case "terminalize": + return yield* Effect.fail(cause); + } + }), + ), + ); + return attempt(1); +} + +export class ProviderRuntimeRecoveryService extends Context.Service< + ProviderRuntimeRecoveryService, + { + readonly recover: Effect.Effect; + } +>()("t3/orchestration-v2/ProviderRuntimeRecoveryService") {} + +function activeRuns(projection: OrchestrationV2ThreadProjection) { + return projection.runs.filter( + (run) => run.status === "starting" || run.status === "running" || run.status === "waiting", + ); +} + +export const make = Effect.gen(function* () { + const projections = yield* ProjectionStore.ProjectionStoreV2; + const sessions = yield* ProviderSessionManager.ProviderSessionManagerV2; + const eventSink = yield* EventSink.EventSinkV2; + const ids = yield* IdAllocator.IdAllocatorV2; + const worker = yield* EffectWorker.OrchestrationEffectWorkerV2; + const outbox = yield* EffectOutbox.EffectOutboxV2; + const connectivity = yield* ConnectivityService; + const classifier = yield* ProviderRuntimeFailureClassifier; + const recoveryPolicy = yield* ProviderRuntimeRecoveryPolicy; + + const terminalize = Effect.fn("ProviderRuntimeRecoveryService.terminalize")(function* ( + projection: OrchestrationV2ThreadProjection, + detail: string, + ) { + const now = yield* DateTime.now; + const runs = activeRuns(projection); + if (runs.length === 0) return 0; + const commandId = CommandId.make(`command:recovery:terminalize:${projection.thread.id}`); + const allocateEventId = () => + ids.allocate.event({ threadId: projection.thread.id, commandId }).pipe( + Effect.mapError( + (cause) => + new ProviderRuntimeRecoveryError({ + operation: "terminalize", + threadId: projection.thread.id, + cause, + }), + ), + ); + const events: Array = []; + for (const run of runs) { + events.push({ + id: yield* allocateEventId(), + type: "run.updated", + threadId: projection.thread.id, + runId: run.id, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: { ...run, status: "failed", completedAt: now }, + }); + for (const node of projection.nodes.filter( + (candidate) => + candidate.runId === run.id && + (candidate.status === "pending" || + candidate.status === "running" || + candidate.status === "waiting"), + )) { + events.push({ + id: yield* allocateEventId(), + type: "node.updated", + threadId: projection.thread.id, + runId: run.id, + nodeId: node.id, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: { ...node, status: "failed", completedAt: now }, + }); + } + } + yield* eventSink + .commitCommand({ + commandId, + threadId: projection.thread.id, + commandType: "provider-runtime.recovery-terminalize", + acceptedAt: now, + events, + effects: [], + }) + .pipe( + Effect.mapError( + (cause) => + new ProviderRuntimeRecoveryError({ + operation: "terminalize", + threadId: projection.thread.id, + cause: { detail, cause }, + }), + ), + ); + return runs.length; + }); + + const expireOrphanedRequests = Effect.fn("ProviderRuntimeRecoveryService.expireOrphanedRequests")( + function* (projection: OrchestrationV2ThreadProjection) { + const requests = projection.runtimeRequests.filter((request) => request.status === "pending"); + if (requests.length === 0) return; + const now = yield* DateTime.now; + const commandId = CommandId.make(`command:recovery:expire-requests:${projection.thread.id}`); + const events: Array = []; + for (const request of requests) { + events.push({ + id: yield* ids.allocate.event({ threadId: projection.thread.id, commandId }).pipe( + Effect.mapError( + (cause) => + new ProviderRuntimeRecoveryError({ + operation: "terminalize", + threadId: projection.thread.id, + cause, + }), + ), + ), + type: "runtime-request.updated", + threadId: projection.thread.id, + nodeId: request.nodeId, + occurredAt: now, + payload: { + ...request, + status: "expired", + responseCapability: { + type: "not_resumable", + reason: "The server restarted before this runtime request was resolved.", + }, + resolvedAt: now, + }, + }); + } + yield* eventSink + .commitCommand({ + commandId, + threadId: projection.thread.id, + commandType: "provider-runtime.expire-orphaned-requests", + acceptedAt: now, + events, + effects: [], + }) + .pipe( + Effect.mapError( + (cause) => + new ProviderRuntimeRecoveryError({ + operation: "terminalize", + threadId: projection.thread.id, + cause, + }), + ), + ); + }, + ); + + const recoverProjection = Effect.fn("ProviderRuntimeRecoveryService.recoverProjection")( + function* (projection: OrchestrationV2ThreadProjection) { + yield* expireOrphanedRequests(projection); + let resumedSessions = 0; + for (const session of projection.providerSessions.filter( + (candidate) => candidate.status !== "stopped" && candidate.status !== "error", + )) { + const resume = sessions + .open({ + threadId: projection.thread.id, + providerSessionId: session.id, + modelSelection: projection.thread.modelSelection, + runtimePolicy: { + runtimeMode: projection.thread.runtimeMode, + interactionMode: projection.thread.interactionMode, + cwd: session.cwd, + }, + resumeFromSession: session, + }) + .pipe( + Effect.flatMap((runtime) => + Effect.forEach( + projection.providerThreads.filter( + (providerThread) => + providerThread.providerSessionId === session.id && + providerThread.appThreadId === projection.thread.id && + providerThread.status !== "closed" && + providerThread.status !== "archived", + ), + (providerThread) => + runtime.resumeThread({ + providerThread, + threadId: projection.thread.id, + modelSelection: projection.thread.modelSelection, + runtimePolicy: { + runtimeMode: projection.thread.runtimeMode, + interactionMode: projection.thread.interactionMode, + cwd: session.cwd, + }, + }), + { discard: true }, + ), + ), + ); + const recovered = yield* Effect.result( + recoverWithPolicy({ + operation: resume, + classify: classifier.classify, + connectivity, + maxAttempts: recoveryPolicy.maxAttempts, + idempotent: true, + }), + ); + if (recovered._tag === "Failure") { + const terminalizedRuns = yield* terminalize( + projection, + `Unable to resume provider session ${session.id}.`, + ); + return { resumedSessions, terminalizedRuns }; + } + resumedSessions += 1; + } + if ( + resumedSessions === 0 && + projection.runs.some((run) => run.status === "running" || run.status === "waiting") + ) { + const terminalizedRuns = yield* terminalize( + projection, + "No resumable provider session remained for the active run.", + ); + return { resumedSessions, terminalizedRuns }; + } + return { resumedSessions, terminalizedRuns: 0 }; + }, + ); + + const recover = Effect.gen(function* () { + const shell = yield* projections + .getShellSnapshot() + .pipe( + Effect.mapError( + (cause) => new ProviderRuntimeRecoveryError({ operation: "read-projections", cause }), + ), + ); + let resumedSessions = 0; + let terminalizedRuns = 0; + for (const thread of shell.threads) { + const projection = yield* projections.getThreadProjection(thread.id).pipe( + Effect.mapError( + (cause) => + new ProviderRuntimeRecoveryError({ + operation: "read-projections", + threadId: thread.id, + cause, + }), + ), + ); + const result = yield* recoverProjection(projection); + resumedSessions += result.resumedSessions; + terminalizedRuns += result.terminalizedRuns; + } + let executedEffects = 0; + yield* outbox.reclaimRunning.pipe( + Effect.mapError( + (cause) => new ProviderRuntimeRecoveryError({ operation: "drain-outbox", cause }), + ), + ); + while ( + yield* worker.runOnce.pipe( + Effect.mapError( + (cause) => new ProviderRuntimeRecoveryError({ operation: "drain-outbox", cause }), + ), + ) + ) { + executedEffects += 1; + } + return { resumedSessions, terminalizedRuns, executedEffects }; + }); + + return ProviderRuntimeRecoveryService.of({ recover }); +}); + +export const layer = Layer.effect(ProviderRuntimeRecoveryService, make); diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts new file mode 100644 index 00000000000..d4f14d75560 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts @@ -0,0 +1,1290 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import { + EnvironmentId, + type ModelSelection, + type OrchestrationV2AppThread, + type OrchestrationV2DomainEvent, + type OrchestrationV2ProviderCapabilities, + type OrchestrationV2ProviderSession, + type OrchestrationV2ProviderThread, + ProviderDriverKind, + ProviderInstanceId, + type ProviderSessionId, + ThreadId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; +import * as Stream from "effect/Stream"; +import { TestClock } from "effect/testing"; +import { HttpServer } from "effect/unstable/http"; + +import { ServerEnvironment } from "../environment/ServerEnvironment.ts"; +import * as McpProviderSession from "../mcp/McpProviderSession.ts"; +import * as McpSessionRegistry from "../mcp/McpSessionRegistry.ts"; +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; +import { EventSinkV2, EventSinkWriteError, layer as eventSinkLayer } from "./EventSink.ts"; +import { layer as eventStoreLayer } from "./EventStore.ts"; +import { + IdAllocatorV2, + type IdAllocatorV2Shape, + layer as idAllocatorLayer, +} from "./IdAllocator.ts"; +import { ProjectionStoreV2, layer as projectionStoreLayer } from "./ProjectionStore.ts"; +import { + ProviderAdapterEventStreamError, + type ProviderAdapterV2Event, + ProviderAdapterProtocolError, + type ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2SessionRuntime, + type ProviderAdapterV2Shape, +} from "./ProviderAdapter.ts"; +import { makeSingleLayer as makeProviderAdapterRegistryLayer } from "./ProviderAdapterRegistry.ts"; +import { + ProviderSessionManagerV2, + layerWithOptions as providerSessionManagerLayerWithOptions, +} from "./ProviderSessionManager.ts"; + +const TestDatabaseLayer = SqlitePersistenceMemory; +const TestStoresLayer = Layer.merge(eventStoreLayer, projectionStoreLayer).pipe( + Layer.provide(TestDatabaseLayer), +); +const TestEventSinkLayer = eventSinkLayer.pipe( + Layer.provide(Layer.mergeAll(TestStoresLayer, TestDatabaseLayer)), +); +const FailingReleaseEventSinkLayer = Layer.effect( + EventSinkV2, + Effect.gen(function* () { + const delegate = yield* EventSinkV2; + return EventSinkV2.of({ + ...delegate, + write: (input) => + input.events.some( + (event) => + event.type === "provider-session.updated" && + (event.payload.status === "stopped" || event.payload.status === "error"), + ) + ? Effect.fail(new EventSinkWriteError({ eventCount: input.events.length })) + : delegate.write(input), + }); + }), +).pipe(Layer.provide(TestEventSinkLayer)); + +const CodexCapabilities: OrchestrationV2ProviderCapabilities = CodexProviderCapabilitiesV2; +const ExclusiveCapabilities: OrchestrationV2ProviderCapabilities = { + ...CodexCapabilities, + sessions: { + ...CodexCapabilities.sessions, + supportsMultipleProviderThreadsPerSession: false, + }, +}; + +interface TestProviderRuntimeState { + readonly openCount: number; + readonly closeCount: number; + readonly interruptCount: number; + readonly resumeCount: number; + readonly eventQueues: ReadonlyMap>; +} + +const emptyState: TestProviderRuntimeState = { + openCount: 0, + closeCount: 0, + interruptCount: 0, + resumeCount: 0, + eventQueues: new Map(), +}; + +const modelSelection = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", +} satisfies ModelSelection; +const CODEX_DRIVER = ProviderDriverKind.make("codex"); + +const runtimePolicy = { + runtimeMode: "full-access", + interactionMode: "default", + cwd: process.cwd(), +} satisfies ProviderAdapterV2RuntimePolicy; + +function makeProviderSession(input: { + readonly providerSessionId: ProviderSessionId; + readonly now: DateTime.Utc; + readonly capabilities?: OrchestrationV2ProviderCapabilities; +}): OrchestrationV2ProviderSession { + return { + id: input.providerSessionId, + driver: CODEX_DRIVER, + providerInstanceId: modelSelection.instanceId, + status: "ready", + cwd: process.cwd(), + model: "gpt-5.4", + capabilities: input.capabilities ?? CodexCapabilities, + createdAt: input.now, + updatedAt: input.now, + lastError: null, + }; +} + +function makeThreadCreatedEvent(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly threadId: ThreadId; + readonly now: DateTime.Utc; +}) { + return Effect.gen(function* () { + const projectId = yield* input.idAllocator.allocate.project({ + fixtureName: "provider-session-manager", + }); + const providerThreadId = input.idAllocator.derive.providerThread({ + driver: CODEX_DRIVER, + nativeThreadId: "native-thread", + }); + const thread: OrchestrationV2AppThread = { + createdBy: "user", + creationSource: "web", + id: input.threadId, + projectId, + title: "Provider session manager", + providerInstanceId: modelSelection.instanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: providerThreadId, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: input.threadId, + }, + forkedFrom: null, + createdAt: input.now, + updatedAt: input.now, + archivedAt: null, + deletedAt: null, + }; + return { + id: yield* input.idAllocator.allocate.event({ threadId: input.threadId }), + type: "thread.created" as const, + threadId: input.threadId, + occurredAt: input.now, + payload: thread, + }; + }); +} + +function makeProviderThread(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + readonly now: DateTime.Utc; +}): OrchestrationV2ProviderThread { + return { + id: input.idAllocator.derive.providerThread({ + driver: CODEX_DRIVER, + nativeThreadId: "native-thread", + }), + driver: CODEX_DRIVER, + providerInstanceId: modelSelection.instanceId, + providerSessionId: input.providerSessionId, + appThreadId: input.threadId, + ownerNodeId: null, + nativeThreadRef: { + driver: CODEX_DRIVER, + nativeId: "native-thread", + strength: "strong", + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: null, + createdAt: input.now, + updatedAt: input.now, + }; +} + +function unimplemented(detail: string) { + return Effect.fail( + new ProviderAdapterProtocolError({ + driver: CODEX_DRIVER, + detail, + }), + ); +} + +function makeProviderAdapter( + state: Ref.Ref, + options: { + readonly failEventStream?: boolean; + readonly capabilities?: OrchestrationV2ProviderCapabilities; + readonly mcpConfigs?: Ref.Ref< + ReadonlyArray + >; + } = {}, +): ProviderAdapterV2Shape { + return { + instanceId: ProviderInstanceId.make("codex"), + driver: CODEX_DRIVER, + getCapabilities: () => Effect.succeed(options.capabilities ?? CodexCapabilities), + openSession: (input) => + Effect.gen(function* () { + if (options.mcpConfigs !== undefined) { + yield* Ref.update(options.mcpConfigs, (configs) => [ + ...configs, + McpProviderSession.readMcpProviderSession(input.threadId), + ]); + } + const now = yield* DateTime.now; + const events = yield* Queue.unbounded(); + const session = makeProviderSession({ + providerSessionId: input.providerSessionId, + now, + ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }), + }); + yield* Ref.update(state, (current) => { + const eventQueues = new Map(current.eventQueues); + eventQueues.set(String(input.providerSessionId), events); + return { + ...current, + openCount: current.openCount + 1, + eventQueues, + }; + }); + yield* Effect.addFinalizer(() => + Ref.update(state, (current) => ({ + ...current, + closeCount: current.closeCount + 1, + })), + ); + + return { + instanceId: ProviderInstanceId.make("codex"), + driver: CODEX_DRIVER, + providerSessionId: input.providerSessionId, + providerSession: session, + rawEvents: Stream.empty, + events: options.failEventStream + ? Stream.fail( + new ProviderAdapterEventStreamError({ + driver: CODEX_DRIVER, + providerSessionId: input.providerSessionId, + cause: "process exited", + }), + ) + : Stream.fromQueue(events), + ensureThread: () => unimplemented("ensureThread unused in test"), + resumeThread: (threadInput) => + Ref.update(state, (current) => ({ + ...current, + resumeCount: current.resumeCount + 1, + })).pipe(Effect.as(threadInput.providerThread)), + startTurn: () => Effect.void, + steerTurn: () => Effect.void, + interruptTurn: () => + Ref.update(state, (current) => ({ + ...current, + interruptCount: current.interruptCount + 1, + })), + respondToRuntimeRequest: () => Effect.void, + readThreadSnapshot: () => unimplemented("readThreadSnapshot unused in test"), + rollbackThread: () => unimplemented("rollbackThread unused in test"), + forkThread: () => unimplemented("forkThread unused in test"), + } satisfies ProviderAdapterV2SessionRuntime; + }), + }; +} + +function makeTestLayer(input: { + readonly state: Ref.Ref; + readonly idleTimeoutMs: number; + readonly failEventStream?: boolean; + readonly capabilities?: OrchestrationV2ProviderCapabilities; + readonly mcpConfigs?: Ref.Ref< + ReadonlyArray + >; + readonly failReleaseEventWrites?: boolean; +}) { + const configuredEventSinkLayer = input.failReleaseEventWrites + ? FailingReleaseEventSinkLayer + : TestEventSinkLayer; + const registryLayer = makeProviderAdapterRegistryLayer( + makeProviderAdapter(input.state, { + failEventStream: input.failEventStream ?? false, + ...(input.capabilities === undefined ? {} : { capabilities: input.capabilities }), + ...(input.mcpConfigs === undefined ? {} : { mcpConfigs: input.mcpConfigs }), + }), + ); + return Layer.mergeAll( + TestStoresLayer, + configuredEventSinkLayer, + idAllocatorLayer, + TestMcpRegistryLayer, + providerSessionManagerLayerWithOptions({ idleTimeoutMs: input.idleTimeoutMs }).pipe( + Layer.provide( + Layer.mergeAll( + registryLayer, + configuredEventSinkLayer, + idAllocatorLayer, + TestMcpRegistryLayer, + TestStoresLayer, + ), + ), + ), + ); +} + +const fakeHttpServer = HttpServer.HttpServer.of({ + address: { _tag: "TcpAddress", hostname: "127.0.0.1", port: 43123 }, + serve: (() => Effect.void) as HttpServer.HttpServer["Service"]["serve"], +}); + +const fakeEnvironment = ServerEnvironment.of({ + getEnvironmentId: Effect.succeed(EnvironmentId.make("environment-provider-session-manager")), + getDescriptor: Effect.die("unused"), +}); + +const TestMcpRegistryLayer = Layer.effect( + McpSessionRegistry.McpSessionRegistry, + McpSessionRegistry.__testing.make(), +).pipe( + Layer.provide(Layer.succeed(HttpServer.HttpServer, fakeHttpServer)), + Layer.provide(Layer.succeed(ServerEnvironment, fakeEnvironment)), + Layer.provide(NodeServices.layer), +); + +function makePendingRuntimeRequestEvents(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + readonly providerThread: OrchestrationV2ProviderThread; + readonly now: DateTime.Utc; +}) { + return Effect.gen(function* () { + const requestId = yield* input.idAllocator.allocate.runtimeRequest({ + driver: CODEX_DRIVER, + nativeRequestId: "pending-approval", + }); + const nodeId = input.idAllocator.derive.approvalNode({ requestId }); + const node = { + id: nodeId, + threadId: input.threadId, + runId: null, + parentNodeId: null, + rootNodeId: nodeId, + kind: "approval_request" as const, + status: "waiting" as const, + countsForRun: false, + providerThreadId: input.providerThread.id, + providerTurnId: null, + nativeItemRef: null, + runtimeRequestId: requestId, + checkpointScopeId: null, + startedAt: input.now, + completedAt: null, + }; + const request = { + id: requestId, + nodeId, + providerTurnId: null, + nativeRequestRef: { + driver: CODEX_DRIVER, + nativeId: "pending-approval", + strength: "strong" as const, + }, + kind: "command" as const, + status: "pending" as const, + responseCapability: { + type: "live" as const, + providerSessionId: input.providerSessionId, + }, + createdAt: input.now, + resolvedAt: null, + }; + const turnItem = { + id: input.idAllocator.derive.approvalTurnItem({ requestId }), + threadId: input.threadId, + runId: null, + nodeId, + providerThreadId: input.providerThread.id, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: 1, + status: "waiting" as const, + title: null, + startedAt: input.now, + completedAt: null, + updatedAt: input.now, + type: "approval_request" as const, + requestId, + requestKind: "command" as const, + }; + return [ + { + id: yield* input.idAllocator.allocate.event({ + threadId: input.threadId, + providerSessionId: input.providerSessionId, + }), + type: "node.updated" as const, + threadId: input.threadId, + nodeId, + driver: CODEX_DRIVER, + occurredAt: input.now, + payload: node, + }, + { + id: yield* input.idAllocator.allocate.event({ + threadId: input.threadId, + providerSessionId: input.providerSessionId, + }), + type: "runtime-request.updated" as const, + threadId: input.threadId, + nodeId, + driver: CODEX_DRIVER, + occurredAt: input.now, + payload: request, + }, + { + id: yield* input.idAllocator.allocate.event({ + threadId: input.threadId, + providerSessionId: input.providerSessionId, + }), + type: "turn-item.updated" as const, + threadId: input.threadId, + nodeId, + driver: CODEX_DRIVER, + occurredAt: input.now, + payload: turnItem, + }, + ] satisfies ReadonlyArray; + }); +} + +it.effect("ProviderSessionManagerV2 releases live sessions when its layer shuts down", () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const now = yield* DateTime.now; + const threadId = ThreadId.make("thread-provider-session-manager-shutdown"); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId, + }); + + yield* eventSink.write({ + events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })], + }); + yield* manager.open({ + threadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + + const liveState = yield* Ref.get(state); + assert.equal(liveState.openCount, 1); + assert.equal(liveState.closeCount, 0); + }); + + yield* effect.pipe( + Effect.provide( + makeTestLayer({ + state, + idleTimeoutMs: 60_000, + }), + ), + ); + + assert.equal((yield* Ref.get(state)).closeCount, 1); + }), +); + +it.effect( + "ProviderSessionManagerV2 issues MCP credentials before opening and revokes them on close", + () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const mcpConfigs = yield* Ref.make< + ReadonlyArray + >([]); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const registry = yield* McpSessionRegistry.McpSessionRegistry; + const now = yield* DateTime.now; + const threadId = ThreadId.make("thread-provider-session-manager-mcp"); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId, + }); + + yield* eventSink.write({ + events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })], + }); + yield* manager.open({ + threadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + + const captured = (yield* Ref.get(mcpConfigs))[0]; + assert.isDefined(captured); + assert.equal(captured?.threadId, threadId); + assert.equal(captured?.providerInstanceId, modelSelection.instanceId); + assert.equal(captured?.endpoint, "http://127.0.0.1:43123/mcp"); + const token = captured?.authorizationHeader.replace(/^Bearer\s+/, ""); + assert.isDefined(token); + const resolved = yield* registry.resolve(token!); + assert.equal(resolved?.threadId, threadId); + assert.deepEqual(resolved?.capabilities, new Set(["preview", "orchestration"])); + + yield* manager.close(providerSessionId); + assert.isUndefined(McpProviderSession.readMcpProviderSession(threadId)); + assert.isUndefined(yield* registry.resolve(token!)); + }); + + yield* effect.pipe( + Effect.provide( + makeTestLayer({ + state, + idleTimeoutMs: 1_000, + mcpConfigs, + }), + ), + ); + }), +); + +it.effect("ProviderSessionManagerV2 revokes MCP credentials when release persistence fails", () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const mcpConfigs = yield* Ref.make< + ReadonlyArray + >([]); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const registry = yield* McpSessionRegistry.McpSessionRegistry; + const now = yield* DateTime.now; + const threadId = ThreadId.make("thread-provider-session-manager-mcp-release-failure"); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId, + }); + + yield* eventSink.write({ + events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })], + }); + yield* manager.open({ + threadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + + const captured = (yield* Ref.get(mcpConfigs))[0]; + const token = captured?.authorizationHeader.replace(/^Bearer\s+/, ""); + assert.isDefined(token); + assert.isDefined(yield* registry.resolve(token!)); + + const closeError = yield* manager.close(providerSessionId).pipe(Effect.flip); + assert.equal(closeError._tag, "ProviderSessionCloseError"); + assert.isUndefined(McpProviderSession.readMcpProviderSession(threadId)); + assert.isUndefined(yield* registry.resolve(token!)); + }); + + yield* effect.pipe( + Effect.provide( + makeTestLayer({ + state, + idleTimeoutMs: 1_000, + mcpConfigs, + failReleaseEventWrites: true, + }), + ), + ); + }), +); + +it.effect("ProviderSessionManagerV2 releases idle sessions without sweeping all sessions", () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const projectionStore = yield* ProjectionStoreV2; + const now = yield* DateTime.now; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-session-manager-idle", + }); + const threadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-idle", + projectId, + }); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId, + }); + + yield* eventSink.write({ + events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })], + }); + yield* manager.open({ + threadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + + yield* TestClock.adjust("1 second"); + yield* Effect.yieldNow; + + const liveSession = yield* manager.get(providerSessionId); + const runtimeState = yield* Ref.get(state); + const projection = yield* projectionStore.getThreadProjection(threadId); + + assert.isTrue(Option.isNone(liveSession)); + assert.equal(runtimeState.openCount, 1); + assert.equal(runtimeState.closeCount, 1); + assert.equal(projection.providerSessions.at(-1)?.status, "stopped"); + }); + + yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 }))); + }), +); + +it.effect( + "ProviderSessionManagerV2 keeps active sessions alive until the provider turn terminates", + () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const projectionStore = yield* ProjectionStoreV2; + const now = yield* DateTime.now; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-session-manager-active", + }); + const threadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-active", + projectId, + }); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId, + }); + const providerThread = makeProviderThread({ + idAllocator, + threadId, + providerSessionId, + now, + }); + const runId = idAllocator.derive.run({ threadId, ordinal: 1 }); + const attemptId = idAllocator.derive.runAttempt({ runId, attemptOrdinal: 1 }); + const rootNodeId = idAllocator.derive.rootNode({ runId }); + const providerTurnId = idAllocator.derive.providerTurn({ + driver: CODEX_DRIVER, + nativeTurnId: "native-turn", + }); + + yield* eventSink.write({ + events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })], + }); + const runtime = yield* manager.open({ + threadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + yield* runtime.events.pipe(Stream.runDrain, Effect.forkScoped); + const appThread = (yield* projectionStore.getThreadProjection(threadId)).thread; + yield* runtime.startTurn({ + appThread, + threadId, + runId, + runOrdinal: 1, + providerTurnOrdinal: 1, + attemptId, + rootNodeId, + providerThread, + message: { + createdBy: "user", + creationSource: "web", + messageId: yield* idAllocator.allocate.message({ threadId, ordinal: 1 }), + text: "hello", + attachments: [], + }, + modelSelection, + runtimePolicy, + }); + + yield* TestClock.adjust("2 seconds"); + yield* Effect.yieldNow; + assert.equal((yield* Ref.get(state)).closeCount, 0); + + const queue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId)); + assert.isDefined(queue); + yield* Queue.offer(queue!, { + type: "turn.terminal", + driver: CODEX_DRIVER, + providerTurnId, + status: "completed", + }); + yield* TestClock.adjust("1 second"); + yield* Effect.yieldNow; + + const liveSession = yield* manager.get(providerSessionId); + const projection = yield* projectionStore.getThreadProjection(threadId); + assert.isTrue(Option.isNone(liveSession)); + assert.equal((yield* Ref.get(state)).closeCount, 1); + assert.equal(projection.providerSessions.at(-1)?.status, "stopped"); + }); + + yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 }))); + }), +); + +it.effect("ProviderSessionManagerV2 uses the same release path for runtime failures", () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const projectionStore = yield* ProjectionStoreV2; + const now = yield* DateTime.now; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-session-manager-runtime-error", + }); + const threadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-runtime-error", + projectId, + }); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId, + }); + + yield* eventSink.write({ + events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })], + }); + yield* manager.open({ + threadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + yield* manager.release({ + providerSessionId, + reason: "runtime_error", + detail: "process exited", + }); + + const liveSession = yield* manager.get(providerSessionId); + const runtimeState = yield* Ref.get(state); + const projection = yield* projectionStore.getThreadProjection(threadId); + + assert.isTrue(Option.isNone(liveSession)); + assert.equal(runtimeState.closeCount, 1); + assert.equal(projection.providerSessions.at(-1)?.status, "error"); + assert.equal(projection.providerSessions.at(-1)?.lastError, "process exited"); + }); + + yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 }))); + }), +); + +it.effect("ProviderSessionManagerV2 releases sessions when provider event streams fail", () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const projectionStore = yield* ProjectionStoreV2; + const now = yield* DateTime.now; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-session-manager-stream-error", + }); + const threadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-stream-error", + projectId, + }); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId, + }); + + yield* eventSink.write({ + events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })], + }); + const runtime = yield* manager.open({ + threadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + yield* runtime.events.pipe(Stream.runDrain, Effect.ignore, Effect.forkScoped); + yield* Effect.yieldNow; + + const liveSession = yield* manager.get(providerSessionId); + const runtimeState = yield* Ref.get(state); + const projection = yield* projectionStore.getThreadProjection(threadId); + + assert.isTrue(Option.isNone(liveSession)); + assert.equal(runtimeState.closeCount, 1); + assert.equal(projection.providerSessions.at(-1)?.status, "error"); + }); + + yield* effect.pipe( + Effect.provide( + makeTestLayer({ + state, + idleTimeoutMs: 1000, + failEventStream: true, + }), + ), + ); + }), +); + +it.effect("ProviderSessionManagerV2 marks pending runtime requests non-live on release", () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const projectionStore = yield* ProjectionStoreV2; + const now = yield* DateTime.now; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-session-manager-request-expire", + }); + const threadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-request-expire", + projectId, + }); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId, + }); + const providerThread = makeProviderThread({ + idAllocator, + threadId, + providerSessionId, + now, + }); + + yield* eventSink.write({ + events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })], + }); + yield* eventSink.write({ + events: yield* makePendingRuntimeRequestEvents({ + idAllocator, + threadId, + providerSessionId, + providerThread, + now, + }), + }); + yield* manager.open({ + threadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + yield* manager.release({ + providerSessionId, + reason: "runtime_error", + detail: "process exited", + }); + + const projection = yield* projectionStore.getThreadProjection(threadId); + const request = projection.runtimeRequests.at(-1); + const requestNode = projection.nodes.find((node) => node.id === request?.nodeId); + const requestTurnItem = projection.turnItems.find( + (item) => item.type === "approval_request" && item.requestId === request?.id, + ); + + assert.equal(request?.status, "expired"); + assert.equal(request?.responseCapability.type, "not_resumable"); + assert.equal(requestNode?.status, "failed"); + assert.equal(requestTurnItem?.status, "failed"); + }); + + yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 }))); + }), +); + +it.effect( + "ProviderSessionManagerV2 keeps a multi-thread session alive until all turns finish", + () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const projectionStore = yield* ProjectionStoreV2; + const now = yield* DateTime.now; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-session-manager-multi-thread-active", + }); + const firstThreadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-multi-thread-active-a", + projectId, + }); + const secondThreadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-multi-thread-active-b", + projectId, + }); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId: firstThreadId, + }); + const firstProviderThread = makeProviderThread({ + idAllocator, + threadId: firstThreadId, + providerSessionId, + now, + }); + const secondProviderThread = makeProviderThread({ + idAllocator, + threadId: secondThreadId, + providerSessionId, + now, + }); + const firstRunId = idAllocator.derive.run({ threadId: firstThreadId, ordinal: 1 }); + const secondRunId = idAllocator.derive.run({ threadId: secondThreadId, ordinal: 1 }); + const firstProviderTurnId = idAllocator.derive.providerTurn({ + driver: CODEX_DRIVER, + nativeTurnId: "native-turn-a", + }); + const secondProviderTurnId = idAllocator.derive.providerTurn({ + driver: CODEX_DRIVER, + nativeTurnId: "native-turn-b", + }); + + yield* eventSink.write({ + events: [ + yield* makeThreadCreatedEvent({ idAllocator, threadId: firstThreadId, now }), + yield* makeThreadCreatedEvent({ idAllocator, threadId: secondThreadId, now }), + ], + }); + const runtime = yield* manager.open({ + threadId: firstThreadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + yield* manager.open({ + threadId: secondThreadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + yield* runtime.events.pipe(Stream.runDrain, Effect.forkScoped); + const firstAppThread = (yield* projectionStore.getThreadProjection(firstThreadId)).thread; + const secondAppThread = (yield* projectionStore.getThreadProjection(secondThreadId)).thread; + yield* runtime.startTurn({ + appThread: firstAppThread, + threadId: firstThreadId, + runId: firstRunId, + runOrdinal: 1, + providerTurnOrdinal: 1, + attemptId: idAllocator.derive.runAttempt({ runId: firstRunId, attemptOrdinal: 1 }), + rootNodeId: idAllocator.derive.rootNode({ runId: firstRunId }), + providerThread: firstProviderThread, + message: { + createdBy: "user", + creationSource: "web", + messageId: yield* idAllocator.allocate.message({ threadId: firstThreadId, ordinal: 1 }), + text: "first", + attachments: [], + }, + modelSelection, + runtimePolicy, + }); + yield* runtime.startTurn({ + appThread: secondAppThread, + threadId: secondThreadId, + runId: secondRunId, + runOrdinal: 1, + providerTurnOrdinal: 1, + attemptId: idAllocator.derive.runAttempt({ runId: secondRunId, attemptOrdinal: 1 }), + rootNodeId: idAllocator.derive.rootNode({ runId: secondRunId }), + providerThread: secondProviderThread, + message: { + createdBy: "user", + creationSource: "web", + messageId: yield* idAllocator.allocate.message({ + threadId: secondThreadId, + ordinal: 1, + }), + text: "second", + attachments: [], + }, + modelSelection, + runtimePolicy, + }); + + const queue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId)); + assert.isDefined(queue); + yield* Queue.offer(queue!, { + type: "turn.terminal", + driver: CODEX_DRIVER, + providerTurnId: firstProviderTurnId, + status: "completed", + }); + yield* TestClock.adjust("2 seconds"); + yield* Effect.yieldNow; + assert.equal((yield* Ref.get(state)).closeCount, 0); + + yield* Queue.offer(queue!, { + type: "turn.terminal", + driver: CODEX_DRIVER, + providerTurnId: secondProviderTurnId, + status: "completed", + }); + yield* TestClock.adjust("1 second"); + yield* Effect.yieldNow; + assert.equal((yield* Ref.get(state)).closeCount, 1); + }); + + yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 }))); + }), +); + +it.effect( + "ProviderSessionManagerV2 opens one shared runtime, broadcasts events, and detaches threads independently", + () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const now = yield* DateTime.now; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-session-manager-shared-runtime", + }); + const firstThreadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-shared-runtime-a", + projectId, + }); + const secondThreadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-shared-runtime-b", + projectId, + }); + const providerSessionId = idAllocator.derive.providerSession({ + providerInstanceId: modelSelection.instanceId, + }); + + yield* eventSink.write({ + events: [ + yield* makeThreadCreatedEvent({ idAllocator, threadId: firstThreadId, now }), + yield* makeThreadCreatedEvent({ idAllocator, threadId: secondThreadId, now }), + ], + }); + const firstProviderThread = makeProviderThread({ + idAllocator, + threadId: firstThreadId, + providerSessionId, + now, + }); + const secondProviderThread = makeProviderThread({ + idAllocator, + threadId: secondThreadId, + providerSessionId, + now, + }); + const firstRunId = idAllocator.derive.run({ threadId: firstThreadId, ordinal: 1 }); + yield* eventSink.write({ + events: [ + { + id: yield* idAllocator.allocate.event({ threadId: firstThreadId }), + type: "provider-thread.updated", + threadId: firstThreadId, + driver: CODEX_DRIVER, + occurredAt: now, + payload: firstProviderThread, + }, + { + id: yield* idAllocator.allocate.event({ threadId: firstThreadId }), + type: "provider-turn.updated", + threadId: firstThreadId, + runId: firstRunId, + driver: CODEX_DRIVER, + occurredAt: now, + payload: { + id: idAllocator.derive.providerTurn({ + driver: CODEX_DRIVER, + nativeTurnId: "native-turn-shared-runtime-a", + }), + providerThreadId: firstProviderThread.id, + nodeId: idAllocator.derive.rootNode({ runId: firstRunId }), + runAttemptId: null, + nativeTurnRef: null, + ordinal: 1, + status: "running", + startedAt: now, + completedAt: null, + }, + }, + ], + }); + const firstRuntime = yield* manager.open({ + threadId: firstThreadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + const secondRuntime = yield* manager.open({ + threadId: secondThreadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + + assert.strictEqual(firstRuntime, secondRuntime); + assert.equal((yield* Ref.get(state)).openCount, 1); + const resumeSecondThread = secondRuntime.resumeThread({ + providerThread: secondProviderThread, + threadId: secondThreadId, + modelSelection, + runtimePolicy, + }); + yield* resumeSecondThread; + yield* resumeSecondThread; + assert.equal((yield* Ref.get(state)).resumeCount, 1); + yield* secondRuntime.resumeThread({ + providerThread: secondProviderThread, + threadId: secondThreadId, + modelSelection: { ...modelSelection, model: "gpt-5.4-mini" }, + runtimePolicy, + }); + assert.equal((yield* Ref.get(state)).resumeCount, 2); + yield* resumeSecondThread; + assert.equal((yield* Ref.get(state)).resumeCount, 3); + const subscribe = firstRuntime.subscribeEvents; + assert.isDefined(subscribe); + if (subscribe === undefined) return; + const firstSubscription = yield* subscribe; + const secondSubscription = yield* subscribe; + const queue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId)); + assert.isDefined(queue); + yield* Queue.offer(queue!, { + type: "provider_session.updated", + driver: CODEX_DRIVER, + providerSession: firstRuntime.providerSession, + }); + const received = yield* Effect.all([ + firstSubscription.events.pipe(Stream.runHead), + secondSubscription.events.pipe(Stream.runHead), + ]); + assert.isTrue(received.every(Option.isSome)); + assert.isTrue( + received.every( + (event) => Option.isSome(event) && event.value.type === "provider_session.updated", + ), + ); + + yield* manager.detach({ providerSessionId, threadId: secondThreadId }); + yield* manager.open({ + threadId: secondThreadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + yield* resumeSecondThread; + assert.equal((yield* Ref.get(state)).resumeCount, 4); + + yield* manager.detach({ providerSessionId, threadId: firstThreadId }); + assert.isTrue(Option.isSome(yield* manager.get(providerSessionId))); + assert.equal((yield* Ref.get(state)).closeCount, 0); + assert.equal((yield* Ref.get(state)).interruptCount, 1); + + yield* manager.detach({ providerSessionId, threadId: secondThreadId }); + yield* TestClock.adjust("1 second"); + yield* Effect.yieldNow; + assert.equal((yield* Ref.get(state)).closeCount, 1); + }); + + yield* effect.pipe(Effect.provide(makeTestLayer({ state, idleTimeoutMs: 1000 }))); + }), +); + +it.effect( + "ProviderSessionManagerV2 rejects a second thread when the provider runtime is exclusive", + () => + Effect.gen(function* () { + const state = yield* Ref.make(emptyState); + const effect = Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const manager = yield* ProviderSessionManagerV2; + const now = yield* DateTime.now; + const projectId = yield* idAllocator.allocate.project({ + fixtureName: "provider-session-manager-exclusive-runtime", + }); + const firstThreadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-exclusive-runtime-a", + projectId, + }); + const secondThreadId = yield* idAllocator.allocate.thread({ + fixtureName: "provider-session-manager-exclusive-runtime-b", + projectId, + }); + const providerSessionId = yield* idAllocator.allocate.providerSession({ + providerInstanceId: modelSelection.instanceId, + threadId: firstThreadId, + }); + yield* eventSink.write({ + events: [ + yield* makeThreadCreatedEvent({ idAllocator, threadId: firstThreadId, now }), + yield* makeThreadCreatedEvent({ idAllocator, threadId: secondThreadId, now }), + ], + }); + + yield* manager.open({ + threadId: firstThreadId, + providerSessionId, + modelSelection, + runtimePolicy, + }); + const error = yield* manager + .open({ + threadId: secondThreadId, + providerSessionId, + modelSelection, + runtimePolicy, + }) + .pipe(Effect.flip); + + assert.equal(error._tag, "ProviderSessionOpenError"); + assert.equal((yield* Ref.get(state)).openCount, 1); + }); + + yield* effect.pipe( + Effect.provide( + makeTestLayer({ state, idleTimeoutMs: 1000, capabilities: ExclusiveCapabilities }), + ), + ); + }), +); diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.ts new file mode 100644 index 00000000000..2b43c54fa43 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.ts @@ -0,0 +1,1279 @@ +import { + ModelSelection, + OrchestrationV2DomainEvent, + OrchestrationV2ProviderSession, + OrchestrationV2RuntimeRequest, + ProviderInstanceId, + ProviderSessionId, + ThreadId, +} from "@t3tools/contracts"; +import * as Cause from "effect/Cause"; +import * as Clock from "effect/Clock"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Fiber from "effect/Fiber"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Scope from "effect/Scope"; +import * as Semaphore from "effect/Semaphore"; +import * as Stream from "effect/Stream"; + +import * as McpProviderSession from "../mcp/McpProviderSession.ts"; +import * as McpSessionRegistry from "../mcp/McpSessionRegistry.ts"; +import { EventSinkV2 } from "./EventSink.ts"; +import { IdAllocatorV2 } from "./IdAllocator.ts"; +import { + ProviderAdapterEventStreamError, + ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2Error, + type ProviderAdapterV2Event, + type ProviderAdapterV2EventSubscription, + type ProviderAdapterV2SessionRuntime, +} from "./ProviderAdapter.ts"; +import { ProviderAdapterRegistryV2 } from "./ProviderAdapterRegistry.ts"; +import { ProjectionStoreV2 } from "./ProjectionStore.ts"; + +const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000; + +export const ProviderSessionReleaseReason = Schema.Literals([ + "idle_timeout", + "runtime_error", + "manual_shutdown", + "server_shutdown", +]); +export type ProviderSessionReleaseReason = typeof ProviderSessionReleaseReason.Type; + +/** + * ProviderSessionManager owns live session residency: open sessions, idle release, + * explicit shutdown, and release-on-runtime-failure. + * + * It intentionally does not decide whether a provider failure should be retried. + * The next recovery layer should classify adapter failures into canonical runtime + * failure kinds before attempting recovery: + * + * - process_exited / transport_unavailable: bounded restart + native thread resume. + * - network_unavailable: wait for ConnectivityService to report online, then resume. + * - provider_rate_limited: retry only when retry-after/idempotency/retry budget allow it. + * - provider_quota_exceeded / auth_invalid / permission_denied / invalid_request: + * terminal until user or configuration changes. + * + * This keeps lifecycle cleanup separate from policy-driven recovery. + */ +export class ProviderSessionOpenError extends Schema.TaggedErrorClass()( + "ProviderSessionOpenError", + { + instanceId: ProviderInstanceId, + providerSessionId: ProviderSessionId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to open provider instance ${this.instanceId} session ${this.providerSessionId}.`; + } +} + +export class ProviderSessionLookupError extends Schema.TaggedErrorClass()( + "ProviderSessionLookupError", + { + providerSessionId: ProviderSessionId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to look up provider session ${this.providerSessionId}.`; + } +} + +export class ProviderSessionCloseError extends Schema.TaggedErrorClass()( + "ProviderSessionCloseError", + { + providerSessionId: ProviderSessionId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to close provider session ${this.providerSessionId}.`; + } +} + +export class ProviderSessionReleaseError extends Schema.TaggedErrorClass()( + "ProviderSessionReleaseError", + { + providerSessionId: ProviderSessionId, + reason: ProviderSessionReleaseReason, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to release provider session ${this.providerSessionId}.`; + } +} + +export class ProviderSessionActivityError extends Schema.TaggedErrorClass()( + "ProviderSessionActivityError", + { + providerSessionId: ProviderSessionId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to update provider session activity for ${this.providerSessionId}.`; + } +} + +export const ProviderSessionManagerV2Error = Schema.Union([ + ProviderSessionOpenError, + ProviderSessionLookupError, + ProviderSessionCloseError, + ProviderSessionReleaseError, + ProviderSessionActivityError, +]); +export type ProviderSessionManagerV2Error = typeof ProviderSessionManagerV2Error.Type; + +export interface ProviderSessionManagerV2Shape { + readonly open: (input: { + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + readonly modelSelection: ModelSelection; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; + readonly resumeFromSession?: OrchestrationV2ProviderSession; + }) => Effect.Effect; + readonly get: ( + providerSessionId: ProviderSessionId, + ) => Effect.Effect, ProviderSessionManagerV2Error>; + readonly close: ( + providerSessionId: ProviderSessionId, + ) => Effect.Effect; + readonly release: (input: { + readonly providerSessionId: ProviderSessionId; + readonly reason: ProviderSessionReleaseReason; + readonly detail?: string; + }) => Effect.Effect; + readonly detach: (input: { + readonly providerSessionId: ProviderSessionId; + readonly threadId: ThreadId; + readonly detail?: string; + }) => Effect.Effect; +} + +export class ProviderSessionManagerV2 extends Context.Service< + ProviderSessionManagerV2, + ProviderSessionManagerV2Shape +>()("t3/orchestration-v2/ProviderSessionManager/ProviderSessionManagerV2") {} + +interface LiveSessionEntry { + readonly attachedThreadIds: ReadonlySet; + readonly loadedProviderThreadKeyByThread: ReadonlyMap; + readonly supportsMultipleProviderThreads: boolean; + readonly runtime: ProviderAdapterV2SessionRuntime; + readonly exposedRuntime: ProviderAdapterV2SessionRuntime; + readonly eventSubscribers: Ref.Ref>>; + readonly scope: Scope.Closeable; + readonly idleGeneration: number; + readonly busyCount: number; + readonly lastActivityAtMs: number; + readonly idleFiber: Fiber.Fiber | null; +} + +type ProviderSessionEventSignal = + | { readonly type: "event"; readonly event: ProviderAdapterV2Event } + | { + readonly type: "failure"; + readonly cause: Cause.Cause; + }; + +export interface ProviderSessionManagerV2LayerOptions { + readonly idleTimeoutMs?: number; + /** Test replay harnesses can omit T3's MCP server from provider protocol fixtures. */ + readonly configureMcp?: boolean; +} + +function releaseStatusFor( + reason: ProviderSessionReleaseReason, +): OrchestrationV2ProviderSession["status"] { + return reason === "runtime_error" ? "error" : "stopped"; +} + +function releasedRuntimeRequestStatusFor( + reason: ProviderSessionReleaseReason, +): OrchestrationV2RuntimeRequest["status"] { + return reason === "manual_shutdown" || reason === "server_shutdown" ? "cancelled" : "expired"; +} + +function sessionKey(providerSessionId: ProviderSessionId): string { + return String(providerSessionId); +} + +function providerThreadRuntimeKey( + providerThread: Parameters[0]["providerThread"], +): string { + const nativeThreadRef = providerThread.nativeThreadRef; + return nativeThreadRef === null + ? String(providerThread.id) + : `${nativeThreadRef.driver}:${nativeThreadRef.nativeId}`; +} + +function providerThreadLoadKey(input: { + readonly providerThread: Parameters< + ProviderAdapterV2SessionRuntime["resumeThread"] + >[0]["providerThread"]; + readonly modelSelection?: ModelSelection; + readonly runtimePolicy?: ProviderAdapterV2RuntimePolicy; +}): string { + return JSON.stringify({ + providerThread: providerThreadRuntimeKey(input.providerThread), + modelSelection: input.modelSelection ?? null, + runtimePolicy: input.runtimePolicy ?? null, + }); +} + +export const layerWithOptions = ( + options: ProviderSessionManagerV2LayerOptions = {}, +): Layer.Layer< + ProviderSessionManagerV2, + never, + | EventSinkV2 + | IdAllocatorV2 + | McpSessionRegistry.McpSessionRegistry + | ProjectionStoreV2 + | ProviderAdapterRegistryV2 +> => + Layer.effect( + ProviderSessionManagerV2, + Effect.gen(function* () { + const registry = yield* ProviderAdapterRegistryV2; + const mcpSessionRegistry = yield* McpSessionRegistry.McpSessionRegistry; + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const projectionStore = yield* ProjectionStoreV2; + const layerScope = yield* Effect.scope; + const sessions = yield* Ref.make(new Map()); + const nextSubscriberId = yield* Ref.make(0); + const openSemaphore = yield* Semaphore.make(1); + const idleTimeoutMs = Math.max(1, options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS); + const prepareMcpSession = (threadId: ThreadId, providerInstanceId: ProviderInstanceId) => + options.configureMcp === false + ? Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId)) + : mcpSessionRegistry.revokeThread(threadId).pipe( + Effect.andThen(mcpSessionRegistry.issue({ threadId, providerInstanceId })), + Effect.tap((credential) => + Effect.sync(() => McpProviderSession.setMcpProviderSession(credential.config)), + ), + ); + const clearMcpSession = (threadId: ThreadId) => + mcpSessionRegistry + .revokeThread(threadId) + .pipe( + Effect.tap(() => + Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId)), + ), + ); + + const publishToSubscribers = ( + subscribers: Ref.Ref>>, + signal: ProviderSessionEventSignal, + ) => + Ref.get(subscribers).pipe( + Effect.flatMap((current) => + Effect.forEach(current.values(), (queue) => Queue.offer(queue, signal), { + discard: true, + }), + ), + ); + + const failSubscribers = (entry: LiveSessionEntry, detail: string) => + Effect.gen(function* () { + const error = new ProviderAdapterEventStreamError({ + driver: entry.runtime.driver, + providerSessionId: entry.runtime.providerSessionId, + cause: detail, + }); + yield* publishToSubscribers(entry.eventSubscribers, { + type: "failure", + cause: Cause.fail(error), + }); + yield* Ref.set(entry.eventSubscribers, new Map()); + }); + + const cancelIdleFiber = (fiber: Fiber.Fiber | null) => + fiber === null ? Effect.void : Fiber.interrupt(fiber).pipe(Effect.ignore); + + const writeProviderSessionEvents = (input: { + readonly runtime: ProviderAdapterV2SessionRuntime; + readonly threadIds: Iterable; + readonly type: "provider-session.attached" | "provider-session.updated"; + readonly payload: OrchestrationV2ProviderSession; + }) => + Effect.gen(function* () { + const now = yield* DateTime.now; + const events = yield* Effect.forEach(input.threadIds, (threadId) => + Effect.gen(function* () { + return { + id: yield* idAllocator.allocate.event({ + threadId, + providerSessionId: input.runtime.providerSessionId, + }), + type: input.type, + threadId, + driver: input.runtime.driver, + providerInstanceId: input.runtime.instanceId, + occurredAt: now, + payload: input.payload, + } satisfies OrchestrationV2DomainEvent; + }), + ); + if (events.length > 0) { + yield* eventSink.write({ events }); + } + }); + + const writeReleasedSessionEvents = (input: { + readonly entry: LiveSessionEntry; + readonly reason: ProviderSessionReleaseReason; + readonly detail?: string; + }) => + Effect.gen(function* () { + const now = yield* DateTime.now; + const payload: OrchestrationV2ProviderSession = { + ...input.entry.runtime.providerSession, + status: releaseStatusFor(input.reason), + updatedAt: now, + lastError: + input.reason === "runtime_error" + ? (input.detail ?? "Provider runtime failed.") + : null, + }; + yield* writeProviderSessionEvents({ + runtime: input.entry.runtime, + threadIds: input.entry.attachedThreadIds, + type: "provider-session.updated", + payload, + }); + }); + + const writeReleasedRuntimeRequestEvents = (input: { + readonly entry: LiveSessionEntry; + readonly reason: ProviderSessionReleaseReason; + }) => + Effect.gen(function* () { + const providerSessionId = input.entry.runtime.providerSessionId; + const now = yield* DateTime.now; + const status = releasedRuntimeRequestStatusFor(input.reason); + const reason = + input.reason === "runtime_error" + ? "Provider session failed before this runtime request was resolved." + : "Provider session was closed before this runtime request was resolved."; + + const events: Array = []; + for (const threadId of input.entry.attachedThreadIds) { + const projection = yield* projectionStore.getThreadProjection(threadId); + const releasedRequests = projection.runtimeRequests.filter( + (request) => + request.status === "pending" && + request.responseCapability.type === "live" && + request.responseCapability.providerSessionId === providerSessionId, + ); + + for (const request of releasedRequests) { + events.push({ + id: yield* idAllocator.allocate.event({ + threadId, + providerSessionId, + }), + type: "runtime-request.updated", + threadId, + nodeId: request.nodeId, + driver: input.entry.runtime.driver, + occurredAt: now, + payload: { + ...request, + status, + responseCapability: { + type: "not_resumable", + reason, + }, + resolvedAt: now, + }, + }); + + const requestNode = projection.nodes.find((node) => node.id === request.nodeId); + if (requestNode !== undefined) { + events.push({ + id: yield* idAllocator.allocate.event({ + threadId, + providerSessionId, + }), + type: "node.updated", + threadId, + ...(requestNode.runId === null ? {} : { runId: requestNode.runId }), + nodeId: requestNode.id, + driver: input.entry.runtime.driver, + occurredAt: now, + payload: { + ...requestNode, + status: input.reason === "runtime_error" ? "failed" : "cancelled", + completedAt: now, + }, + }); + } + + const turnItem = projection.turnItems.find( + (item) => item.type === "approval_request" && item.requestId === request.id, + ); + if (turnItem !== undefined) { + events.push({ + id: yield* idAllocator.allocate.event({ + threadId, + providerSessionId, + }), + type: "turn-item.updated", + threadId, + ...(turnItem.runId === null ? {} : { runId: turnItem.runId }), + ...(turnItem.nodeId === null ? {} : { nodeId: turnItem.nodeId }), + driver: input.entry.runtime.driver, + occurredAt: now, + payload: { + ...turnItem, + status: input.reason === "runtime_error" ? "failed" : "cancelled", + completedAt: now, + updatedAt: now, + }, + }); + } + } + } + + if (events.length > 0) { + yield* eventSink.write({ events }); + } + }); + + const releaseEntry = (input: { + readonly providerSessionId: ProviderSessionId; + readonly reason: ProviderSessionReleaseReason; + readonly detail?: string; + readonly cancelIdleFiber?: boolean; + }) => + Effect.acquireUseRelease( + Ref.modify(sessions, (current) => { + const key = sessionKey(input.providerSessionId); + const existing = current.get(key); + if (existing === undefined) { + return [Option.none(), current] as const; + } + const updated = new Map(current); + updated.delete(key); + return [Option.some(existing), updated] as const; + }), + (entry) => + Option.match(entry, { + onNone: () => Effect.void, + onSome: (entry) => + Effect.gen(function* () { + if (input.cancelIdleFiber !== false) { + yield* cancelIdleFiber(entry.idleFiber); + } + yield* failSubscribers( + entry, + input.detail ?? `Provider session released: ${input.reason}.`, + ); + const closeExit = yield* Effect.exit(Scope.close(entry.scope, Exit.void)); + yield* writeReleasedSessionEvents({ + entry, + reason: input.reason, + ...(input.detail === undefined ? {} : { detail: input.detail }), + }); + yield* writeReleasedRuntimeRequestEvents({ + entry, + reason: input.reason, + }); + if (Exit.isFailure(closeExit)) { + return yield* Effect.failCause(closeExit.cause); + } + }), + }), + (entry) => + Option.match(entry, { + onNone: () => Effect.void, + onSome: (entry) => + Effect.forEach(entry.attachedThreadIds, clearMcpSession, { discard: true }), + }), + ).pipe( + Effect.catchCause((cause) => + Effect.fail( + new ProviderSessionReleaseError({ + providerSessionId: input.providerSessionId, + reason: input.reason, + cause, + }), + ), + ), + ); + + const releaseIfStillIdle = (input: { + readonly providerSessionId: ProviderSessionId; + readonly generation: number; + }) => + Effect.gen(function* () { + const current = yield* Ref.get(sessions); + const entry = current.get(sessionKey(input.providerSessionId)); + if ( + entry === undefined || + entry.busyCount > 0 || + entry.idleGeneration !== input.generation + ) { + return; + } + yield* releaseEntry({ + providerSessionId: input.providerSessionId, + reason: "idle_timeout", + cancelIdleFiber: false, + }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("orchestration-v2.driver-session.idle-release-failed", { + providerSessionId: input.providerSessionId, + cause, + }), + ), + ); + }); + + const withActivityError = ( + providerSessionId: ProviderSessionId, + effect: Effect.Effect, + ): Effect.Effect => + effect.pipe( + Effect.catchCause((cause) => + Effect.fail( + new ProviderSessionActivityError({ + providerSessionId, + cause, + }), + ), + ), + ); + + const scheduleIdleReleaseInternal = (providerSessionId: ProviderSessionId) => + Effect.gen(function* () { + const key = sessionKey(providerSessionId); + const current = yield* Ref.get(sessions); + const entry = current.get(key); + if (entry === undefined || entry.busyCount > 0) { + return; + } + + yield* cancelIdleFiber(entry.idleFiber); + const generation = entry.idleGeneration + 1; + const idleFiber = yield* Effect.sleep(Duration.millis(idleTimeoutMs)).pipe( + Effect.andThen(releaseIfStillIdle({ providerSessionId, generation })), + Effect.forkIn(layerScope), + ); + const lastActivityAtMs = yield* Clock.currentTimeMillis; + yield* Ref.update(sessions, (latest) => { + const latestEntry = latest.get(key); + if (latestEntry === undefined || latestEntry.busyCount > 0) { + return latest; + } + const updated = new Map(latest); + updated.set(key, { + ...latestEntry, + idleGeneration: generation, + idleFiber, + lastActivityAtMs, + }); + return updated; + }); + }); + + const scheduleIdleRelease = (providerSessionId: ProviderSessionId) => + withActivityError(providerSessionId, scheduleIdleReleaseInternal(providerSessionId)); + + const touchActivity = (providerSessionId: ProviderSessionId) => + withActivityError( + providerSessionId, + Effect.gen(function* () { + const lastActivityAtMs = yield* Clock.currentTimeMillis; + yield* Ref.update(sessions, (current) => { + const entry = current.get(sessionKey(providerSessionId)); + if (entry === undefined) { + return current; + } + const updated = new Map(current); + updated.set(sessionKey(providerSessionId), { + ...entry, + lastActivityAtMs, + }); + return updated; + }); + yield* scheduleIdleReleaseInternal(providerSessionId); + }), + ); + + const attachThread = (input: { + readonly providerSessionId: ProviderSessionId; + readonly threadId: ThreadId; + }) => + withActivityError( + input.providerSessionId, + Ref.modify(sessions, (current) => { + const entry = current.get(sessionKey(input.providerSessionId)); + if (entry === undefined || entry.attachedThreadIds.has(input.threadId)) { + return [false, current] as const; + } + const updated = new Map(current); + updated.set(sessionKey(input.providerSessionId), { + ...entry, + attachedThreadIds: new Set([...entry.attachedThreadIds, input.threadId]), + }); + return [true, updated] as const; + }), + ); + + const removeThreadAttachment = (input: { + readonly providerSessionId: ProviderSessionId; + readonly threadId: ThreadId; + }) => + Ref.update(sessions, (current) => { + const key = sessionKey(input.providerSessionId); + const entry = current.get(key); + if (entry === undefined || !entry.attachedThreadIds.has(input.threadId)) { + return current; + } + const attachedThreadIds = new Set(entry.attachedThreadIds); + attachedThreadIds.delete(input.threadId); + const loadedProviderThreadKeyByThread = new Map(entry.loadedProviderThreadKeyByThread); + loadedProviderThreadKeyByThread.delete(input.threadId); + const updated = new Map(current); + updated.set(key, { + ...entry, + attachedThreadIds, + loadedProviderThreadKeyByThread, + }); + return updated; + }); + + const isProviderThreadLoaded = (input: { + readonly providerSessionId: ProviderSessionId; + readonly threadId: ThreadId; + readonly providerThreadKey: string; + }) => + Ref.get(sessions).pipe( + Effect.map( + (current) => + current + .get(sessionKey(input.providerSessionId)) + ?.loadedProviderThreadKeyByThread.get(input.threadId) === input.providerThreadKey, + ), + ); + + const markProviderThreadLoaded = (input: { + readonly providerSessionId: ProviderSessionId; + readonly threadId: ThreadId; + readonly providerThreadKey: string; + }) => + Ref.update(sessions, (current) => { + const key = sessionKey(input.providerSessionId); + const entry = current.get(key); + if (entry === undefined) { + return current; + } + const loadedProviderThreadKeyByThread = new Map(entry.loadedProviderThreadKeyByThread); + loadedProviderThreadKeyByThread.set(input.threadId, input.providerThreadKey); + const updated = new Map(current); + updated.set(key, { ...entry, loadedProviderThreadKeyByThread }); + return updated; + }); + + const ensureThreadAttached = (input: { + readonly providerSessionId: ProviderSessionId; + readonly threadId: ThreadId; + readonly providerInstanceId: ProviderInstanceId; + }) => + Effect.gen(function* () { + const attached = yield* attachThread(input); + if (attached) { + yield* prepareMcpSession(input.threadId, input.providerInstanceId); + const entry = (yield* Ref.get(sessions)).get(sessionKey(input.providerSessionId)); + if (entry !== undefined) { + yield* withActivityError( + input.providerSessionId, + writeProviderSessionEvents({ + runtime: entry.runtime, + threadIds: [input.threadId], + type: "provider-session.attached", + payload: entry.runtime.providerSession, + }), + ); + } + } + }).pipe( + Effect.tapError(() => + removeThreadAttachment(input).pipe(Effect.andThen(clearMcpSession(input.threadId))), + ), + ); + + const markBusy = (providerSessionId: ProviderSessionId) => + withActivityError( + providerSessionId, + Effect.gen(function* () { + const key = sessionKey(providerSessionId); + const now = yield* Clock.currentTimeMillis; + const idleFiber = yield* Ref.modify(sessions, (current) => { + const entry = current.get(key); + if (entry === undefined) { + return [null, current] as const; + } + const updated = new Map(current); + updated.set(key, { + ...entry, + busyCount: entry.busyCount + 1, + idleFiber: null, + lastActivityAtMs: now, + }); + return [entry.idleFiber, updated] as const; + }); + yield* cancelIdleFiber(idleFiber); + }), + ); + + const markIdle = (providerSessionId: ProviderSessionId) => + withActivityError( + providerSessionId, + Effect.gen(function* () { + const key = sessionKey(providerSessionId); + const now = yield* Clock.currentTimeMillis; + yield* Ref.update(sessions, (current) => { + const entry = current.get(key); + if (entry === undefined) { + return current; + } + const updated = new Map(current); + updated.set(key, { + ...entry, + busyCount: Math.max(0, entry.busyCount - 1), + lastActivityAtMs: now, + }); + return updated; + }); + yield* scheduleIdleReleaseInternal(providerSessionId); + }), + ); + + const observeActivity = ( + providerSessionId: ProviderSessionId, + activity: Effect.Effect, + ) => + activity.pipe( + Effect.catchCause((cause) => + Effect.logWarning("orchestration-v2.driver-session.activity-failed", { + providerSessionId, + cause, + }), + ), + ); + + const makeEventSubscription = ( + subscribers: Ref.Ref>>, + ): Effect.Effect => + Effect.gen(function* () { + const queue = yield* Queue.unbounded(); + const subscriberId = yield* Ref.getAndUpdate(nextSubscriberId, (value) => value + 1); + yield* Ref.update(subscribers, (current) => { + const updated = new Map(current); + updated.set(subscriberId, queue); + return updated; + }); + const close = Ref.modify(subscribers, (current) => { + if (!current.has(subscriberId)) { + return [false, current] as const; + } + const updated = new Map(current); + updated.delete(subscriberId); + return [true, updated] as const; + }).pipe(Effect.flatMap((removed) => (removed ? Queue.shutdown(queue) : Effect.void))); + const events = Stream.fromQueue(queue).pipe( + Stream.mapEffect((signal) => + signal.type === "event" + ? Effect.succeed(signal.event) + : Effect.failCause(signal.cause), + ), + Stream.ensuring(close), + ); + return { events, close } satisfies ProviderAdapterV2EventSubscription; + }); + + const decorateRuntime = ( + runtime: ProviderAdapterV2SessionRuntime, + eventSubscribers: Ref.Ref>>, + ): ProviderAdapterV2SessionRuntime => { + const providerSessionId = runtime.providerSessionId; + const subscribeEvents = makeEventSubscription(eventSubscribers); + return { + ...runtime, + subscribeEvents, + events: Stream.unwrap( + subscribeEvents.pipe(Effect.map((subscription) => subscription.events)), + ), + ensureThread: (input) => + observeActivity( + providerSessionId, + ensureThreadAttached({ + providerSessionId, + threadId: input.threadId, + providerInstanceId: runtime.instanceId, + }), + ).pipe( + Effect.andThen(runtime.ensureThread(input)), + Effect.tap((providerThread) => + markProviderThreadLoaded({ + providerSessionId, + threadId: input.threadId, + providerThreadKey: providerThreadLoadKey({ + providerThread, + modelSelection: input.modelSelection, + runtimePolicy: input.runtimePolicy, + }), + }), + ), + ), + resumeThread: (input) => { + const threadId = input.threadId ?? input.providerThread.appThreadId; + if (threadId === null || threadId === undefined) { + return runtime.resumeThread(input); + } + const providerThreadKey = providerThreadLoadKey({ + providerThread: input.providerThread, + ...(input.modelSelection === undefined + ? {} + : { modelSelection: input.modelSelection }), + ...(input.runtimePolicy === undefined ? {} : { runtimePolicy: input.runtimePolicy }), + }); + return observeActivity( + providerSessionId, + ensureThreadAttached({ + providerSessionId, + threadId, + providerInstanceId: runtime.instanceId, + }), + ).pipe( + Effect.andThen( + isProviderThreadLoaded({ providerSessionId, threadId, providerThreadKey }), + ), + Effect.flatMap((loaded) => + loaded ? Effect.succeed(input.providerThread) : runtime.resumeThread(input), + ), + Effect.tap((providerThread) => + markProviderThreadLoaded({ + providerSessionId, + threadId, + providerThreadKey: providerThreadLoadKey({ + providerThread, + ...(input.modelSelection === undefined + ? {} + : { modelSelection: input.modelSelection }), + ...(input.runtimePolicy === undefined + ? {} + : { runtimePolicy: input.runtimePolicy }), + }), + }), + ), + ); + }, + forkThread: (input) => + observeActivity( + providerSessionId, + ensureThreadAttached({ + providerSessionId, + threadId: input.targetThreadId, + providerInstanceId: runtime.instanceId, + }), + ).pipe( + Effect.andThen(runtime.forkThread(input)), + Effect.tap((providerThread) => + markProviderThreadLoaded({ + providerSessionId, + threadId: input.targetThreadId, + providerThreadKey: providerThreadLoadKey({ + providerThread, + ...(input.modelSelection === undefined + ? {} + : { modelSelection: input.modelSelection }), + ...(input.runtimePolicy === undefined + ? {} + : { runtimePolicy: input.runtimePolicy }), + }), + }), + ), + ), + startTurn: (input) => + observeActivity( + providerSessionId, + ensureThreadAttached({ + providerSessionId, + threadId: input.threadId, + providerInstanceId: runtime.instanceId, + }), + ).pipe( + Effect.andThen(observeActivity(providerSessionId, markBusy(providerSessionId))), + Effect.andThen(runtime.startTurn(input)), + Effect.catch((error) => + observeActivity(providerSessionId, markIdle(providerSessionId)).pipe( + Effect.andThen(Effect.fail(error)), + ), + ), + ), + steerTurn: (input) => + observeActivity(providerSessionId, touchActivity(providerSessionId)).pipe( + Effect.andThen(runtime.steerTurn(input)), + ), + interruptTurn: (input) => + observeActivity(providerSessionId, touchActivity(providerSessionId)).pipe( + Effect.andThen(runtime.interruptTurn(input)), + ), + respondToRuntimeRequest: (input) => + observeActivity(providerSessionId, touchActivity(providerSessionId)).pipe( + Effect.andThen(runtime.respondToRuntimeRequest(input)), + ), + }; + }; + + const persistProviderSessionUpdate = ( + entry: LiveSessionEntry, + event: Extract, + ) => + Effect.gen(function* () { + const current = (yield* Ref.get(sessions)).get( + sessionKey(entry.runtime.providerSessionId), + ); + if (current?.runtime !== entry.runtime) { + return; + } + yield* writeProviderSessionEvents({ + runtime: entry.runtime, + threadIds: current.attachedThreadIds, + type: "provider-session.updated", + payload: event.providerSession, + }); + }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("orchestration-v2.driver-session.status-persist-failed", { + providerSessionId: entry.runtime.providerSessionId, + cause, + }), + ), + ); + + const startEventPump = (entry: LiveSessionEntry) => + entry.runtime.events.pipe( + Stream.runForEach((event) => + observeActivity( + entry.runtime.providerSessionId, + event.type === "turn.terminal" + ? markIdle(entry.runtime.providerSessionId) + : touchActivity(entry.runtime.providerSessionId), + ).pipe( + Effect.andThen( + event.type === "provider_session.updated" + ? persistProviderSessionUpdate(entry, event) + : Effect.void, + ), + Effect.andThen( + publishToSubscribers(entry.eventSubscribers, { type: "event", event }), + ), + ), + ), + Effect.exit, + Effect.flatMap((exit) => + Effect.gen(function* () { + const current = (yield* Ref.get(sessions)).get( + sessionKey(entry.runtime.providerSessionId), + ); + if (current?.runtime !== entry.runtime) { + return; + } + const cause = Exit.isFailure(exit) + ? exit.cause + : Cause.fail( + new ProviderAdapterEventStreamError({ + driver: entry.runtime.driver, + providerSessionId: entry.runtime.providerSessionId, + cause: "Provider event stream ended unexpectedly.", + }), + ); + yield* publishToSubscribers(entry.eventSubscribers, { + type: "failure", + cause, + }); + yield* Ref.set(entry.eventSubscribers, new Map()); + yield* releaseEntry({ + providerSessionId: entry.runtime.providerSessionId, + reason: "runtime_error", + detail: Cause.pretty(cause), + }).pipe(Effect.ignore); + }), + ), + Effect.forkIn(layerScope), + ); + + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + const activeSessions = [...(yield* Ref.get(sessions)).values()]; + yield* Effect.forEach( + activeSessions, + (entry) => + releaseEntry({ + providerSessionId: entry.runtime.providerSessionId, + reason: "server_shutdown", + }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("orchestration-v2.driver-session.shutdown-release-failed", { + providerSessionId: entry.runtime.providerSessionId, + cause, + }), + ), + ), + { discard: true }, + ); + }), + ); + + return ProviderSessionManagerV2.of({ + open: (input) => + openSemaphore.withPermit( + Effect.gen(function* () { + const key = sessionKey(input.providerSessionId); + const existing = (yield* Ref.get(sessions)).get(key); + if (existing !== undefined) { + if ( + !existing.attachedThreadIds.has(input.threadId) && + !existing.supportsMultipleProviderThreads + ) { + return yield* new ProviderSessionOpenError({ + instanceId: input.modelSelection.instanceId, + providerSessionId: input.providerSessionId, + cause: `Provider ${existing.runtime.driver} does not support attaching multiple app threads to one session.`, + }); + } + yield* ensureThreadAttached({ + providerSessionId: input.providerSessionId, + threadId: input.threadId, + providerInstanceId: existing.runtime.instanceId, + }); + yield* touchActivity(input.providerSessionId); + return existing.exposedRuntime; + } + + const adapter = yield* registry.get(input.modelSelection.instanceId).pipe( + Effect.mapError( + (cause) => + new ProviderSessionOpenError({ + instanceId: input.modelSelection.instanceId, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ); + yield* prepareMcpSession(input.threadId, input.modelSelection.instanceId); + const sessionScope = yield* Scope.make(); + const runtime = yield* adapter + .openSession({ + threadId: input.threadId, + providerSessionId: input.providerSessionId, + modelSelection: input.modelSelection, + runtimePolicy: input.runtimePolicy, + ...(input.resumeFromSession === undefined + ? {} + : { resumeFromSession: input.resumeFromSession }), + }) + .pipe( + Effect.provideService(Scope.Scope, sessionScope), + Effect.tapError(() => + Scope.close(sessionScope, Exit.void).pipe( + Effect.ignore, + Effect.andThen(clearMcpSession(input.threadId)), + ), + ), + Effect.mapError( + (cause) => + new ProviderSessionOpenError({ + instanceId: input.modelSelection.instanceId, + providerSessionId: input.providerSessionId, + cause, + }), + ), + ); + const eventSubscribers = yield* Ref.make< + ReadonlyMap> + >(new Map()); + const exposedRuntime = decorateRuntime(runtime, eventSubscribers); + const now = yield* Clock.currentTimeMillis; + const entry: LiveSessionEntry = { + attachedThreadIds: new Set([input.threadId]), + loadedProviderThreadKeyByThread: new Map(), + supportsMultipleProviderThreads: + runtime.providerSession.capabilities.sessions + .supportsMultipleProviderThreadsPerSession, + runtime, + exposedRuntime, + eventSubscribers, + scope: sessionScope, + idleGeneration: 0, + busyCount: 0, + lastActivityAtMs: now, + idleFiber: null, + }; + yield* Ref.update(sessions, (current) => { + const updated = new Map(current); + updated.set(key, entry); + return updated; + }); + yield* withActivityError( + input.providerSessionId, + writeProviderSessionEvents({ + runtime, + threadIds: [input.threadId], + type: "provider-session.attached", + payload: runtime.providerSession, + }), + ).pipe( + Effect.tapError(() => + releaseEntry({ + providerSessionId: input.providerSessionId, + reason: "runtime_error", + detail: "Failed to persist the provider-session attachment.", + }).pipe(Effect.ignore), + ), + ); + yield* startEventPump(entry); + yield* scheduleIdleRelease(input.providerSessionId); + return exposedRuntime; + }), + ), + get: (providerSessionId) => + Effect.gen(function* () { + const entry = (yield* Ref.get(sessions)).get(sessionKey(providerSessionId)); + if (entry === undefined) { + return Option.none(); + } + yield* touchActivity(providerSessionId); + return Option.some(entry.exposedRuntime); + }).pipe( + Effect.mapError( + (cause) => + new ProviderSessionLookupError({ + providerSessionId, + cause, + }), + ), + ), + close: (providerSessionId) => + releaseEntry({ providerSessionId, reason: "manual_shutdown" }).pipe( + Effect.mapError( + (cause) => + new ProviderSessionCloseError({ + providerSessionId, + cause, + }), + ), + ), + release: releaseEntry, + detach: (input) => + Effect.gen(function* () { + const key = sessionKey(input.providerSessionId); + const currentEntry = (yield* Ref.get(sessions)).get(key); + if (currentEntry?.supportsMultipleProviderThreads === true) { + const projection = yield* Effect.option( + projectionStore.getThreadProjection(input.threadId), + ); + if (Option.isSome(projection)) { + const providerThreads = new Map( + projection.value.providerThreads + .filter((thread) => thread.providerSessionId === input.providerSessionId) + .map((thread) => [thread.id, thread] as const), + ); + const activeTurns = projection.value.providerTurns.filter( + (turn) => turn.status === "running" && providerThreads.has(turn.providerThreadId), + ); + yield* Effect.forEach( + activeTurns, + (turn) => + currentEntry.exposedRuntime + .interruptTurn({ + providerThread: providerThreads.get(turn.providerThreadId)!, + providerTurnId: turn.id, + }) + .pipe( + Effect.catchCause((cause) => + Effect.logWarning( + "orchestration-v2.driver-session.detach-interrupt-failed", + { + providerSessionId: input.providerSessionId, + threadId: input.threadId, + providerTurnId: turn.id, + cause, + }, + ), + ), + ), + { concurrency: 1, discard: true }, + ); + } + } + const detached = yield* Ref.modify(sessions, (current) => { + const entry = current.get(key); + if (entry === undefined || !entry.attachedThreadIds.has(input.threadId)) { + return [Option.none(), current] as const; + } + const attachedThreadIds = new Set(entry.attachedThreadIds); + attachedThreadIds.delete(input.threadId); + const loadedProviderThreadKeyByThread = new Map( + entry.loadedProviderThreadKeyByThread, + ); + loadedProviderThreadKeyByThread.delete(input.threadId); + const updatedEntry = { + ...entry, + attachedThreadIds, + loadedProviderThreadKeyByThread, + }; + const updated = new Map(current); + updated.set(key, updatedEntry); + return [Option.some(updatedEntry), updated] as const; + }); + yield* clearMcpSession(input.threadId); + if (Option.isNone(detached)) { + return; + } + if ( + detached.value.attachedThreadIds.size === 0 && + !detached.value.supportsMultipleProviderThreads + ) { + yield* releaseEntry({ + providerSessionId: input.providerSessionId, + reason: "manual_shutdown", + ...(input.detail === undefined ? {} : { detail: input.detail }), + }); + return; + } + yield* scheduleIdleRelease(input.providerSessionId); + }).pipe( + Effect.catchCause((cause) => + Effect.fail( + new ProviderSessionReleaseError({ + providerSessionId: input.providerSessionId, + reason: "manual_shutdown", + cause, + }), + ), + ), + ), + } satisfies ProviderSessionManagerV2Shape); + }), + ); + +export const layer = layerWithOptions(); diff --git a/apps/server/src/orchestration-v2/ProviderSessionTransitionPolicy.test.ts b/apps/server/src/orchestration-v2/ProviderSessionTransitionPolicy.test.ts new file mode 100644 index 00000000000..85da958523d --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderSessionTransitionPolicy.test.ts @@ -0,0 +1,110 @@ +import { assert, it } from "@effect/vitest"; +import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts"; + +import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; +import { decideProviderSessionTransition } from "./ProviderSessionTransitionPolicy.ts"; + +const driver = ProviderDriverKind.make("codex"); +const instanceId = ProviderInstanceId.make("codex"); +const base = { + driver, + continuationIdentity: { driverKind: driver, continuationKey: "codex:account:one" }, + modelSelection: { instanceId, model: "gpt-5.1-codex" }, + runtimeMode: "full-access" as const, + interactionMode: "default" as const, + workspace: "/repo", + capabilities: CodexProviderCapabilitiesV2, +}; + +it("reuses compatible sessions and treats interaction mode as turn-scoped", () => { + assert.deepEqual( + decideProviderSessionTransition({ + current: base, + target: { ...base, interactionMode: "plan", available: true }, + }), + { type: "reuse" }, + ); +}); + +it("switches models in-session only when supported", () => { + assert.deepEqual( + decideProviderSessionTransition({ + current: base, + target: { + ...base, + modelSelection: { ...base.modelSelection, model: "gpt-5.2-codex" }, + available: true, + }, + }), + { type: "switch_model_in_session" }, + ); + assert.deepEqual( + decideProviderSessionTransition({ + current: { + ...base, + capabilities: { + ...base.capabilities, + sessions: { ...base.capabilities.sessions, supportsModelSwitchInSession: false }, + }, + }, + target: { + ...base, + capabilities: { + ...base.capabilities, + sessions: { ...base.capabilities.sessions, supportsModelSwitchInSession: false }, + }, + modelSelection: { ...base.modelSelection, model: "gpt-5.2-codex" }, + available: true, + }, + }), + { type: "restart_and_resume" }, + ); +}); + +it("restarts compatible instances for workspace or runtime changes", () => { + assert.deepEqual( + decideProviderSessionTransition({ + current: base, + target: { ...base, workspace: "/other", available: true }, + }), + { type: "restart_and_resume" }, + ); +}); + +it("uses portable handoff for incompatible continuation identities", () => { + assert.deepEqual( + decideProviderSessionTransition({ + current: base, + target: { + ...base, + modelSelection: { + ...base.modelSelection, + instanceId: ProviderInstanceId.make("codex_other"), + }, + continuationIdentity: { driverKind: driver, continuationKey: "codex:account:other" }, + available: true, + }, + }), + { type: "create_with_handoff" }, + ); +}); + +it("uses portable handoff for cross-driver transitions", () => { + const claudeDriver = ProviderDriverKind.make("claude"); + assert.deepEqual( + decideProviderSessionTransition({ + current: base, + target: { + ...base, + driver: claudeDriver, + continuationIdentity: { driverKind: claudeDriver, continuationKey: "claude:account:one" }, + modelSelection: { + instanceId: ProviderInstanceId.make("claude"), + model: "claude-opus-4-1", + }, + available: true, + }, + }), + { type: "create_with_handoff" }, + ); +}); diff --git a/apps/server/src/orchestration-v2/ProviderSessionTransitionPolicy.ts b/apps/server/src/orchestration-v2/ProviderSessionTransitionPolicy.ts new file mode 100644 index 00000000000..2415f2de8e4 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderSessionTransitionPolicy.ts @@ -0,0 +1,68 @@ +import type { + ModelSelection, + OrchestrationV2ProviderCapabilities, + ProviderDriverKind, + ProviderInteractionMode, + RuntimeMode, +} from "@t3tools/contracts"; + +import type { ProviderContinuationIdentity } from "../provider/ProviderDriver.ts"; + +export type ProviderSessionTransition = + | { readonly type: "reuse" } + | { readonly type: "switch_model_in_session" } + | { readonly type: "restart_and_resume" } + | { readonly type: "create_with_handoff" } + | { readonly type: "reject"; readonly reason: string }; + +export interface ProviderSessionTransitionState { + readonly driver: ProviderDriverKind; + readonly continuationIdentity: ProviderContinuationIdentity; + readonly modelSelection: ModelSelection; + readonly runtimeMode: RuntimeMode; + readonly interactionMode: ProviderInteractionMode; + readonly workspace: string; + readonly capabilities: OrchestrationV2ProviderCapabilities; +} + +export interface ProviderSessionTransitionTarget extends ProviderSessionTransitionState { + readonly available: boolean; +} + +export function decideProviderSessionTransition(input: { + readonly current: ProviderSessionTransitionState | null; + readonly target: ProviderSessionTransitionTarget; +}): ProviderSessionTransition { + if (!input.target.available) { + return { type: "reject", reason: "The target provider instance is unavailable." }; + } + if (input.current === null) { + return { type: "create_with_handoff" }; + } + + const current = input.current; + const target = input.target; + const continuationCompatible = + current.continuationIdentity.driverKind === target.continuationIdentity.driverKind && + current.continuationIdentity.continuationKey === target.continuationIdentity.continuationKey; + if (!continuationCompatible || current.driver !== target.driver) { + return { type: "create_with_handoff" }; + } + + const instanceChanged = current.modelSelection.instanceId !== target.modelSelection.instanceId; + const runtimeChanged = current.runtimeMode !== target.runtimeMode; + const workspaceChanged = current.workspace !== target.workspace; + if (instanceChanged || runtimeChanged || workspaceChanged) { + return { type: "restart_and_resume" }; + } + + const modelChanged = current.modelSelection.model !== target.modelSelection.model; + if (modelChanged) { + return target.capabilities.sessions.supportsModelSwitchInSession + ? { type: "switch_model_in_session" } + : { type: "restart_and_resume" }; + } + + // Interaction mode is turn-scoped and is applied when the next turn starts. + return { type: "reuse" }; +} diff --git a/apps/server/src/orchestration-v2/ProviderSwitchService.test.ts b/apps/server/src/orchestration-v2/ProviderSwitchService.test.ts new file mode 100644 index 00000000000..1e361ecc886 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderSwitchService.test.ts @@ -0,0 +1,113 @@ +import { assert, it } from "@effect/vitest"; +import { + ProviderDriverKind, + ProviderInstanceId, + ProviderSessionId, + ThreadId, + type OrchestrationV2ThreadProjection, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; +import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts"; +import * as ProviderSwitch from "./ProviderSwitchService.ts"; + +const driver = ProviderDriverKind.make("codex"); +const currentInstanceId = ProviderInstanceId.make("codex_primary"); +const currentSessionId = ProviderSessionId.make("session_primary"); +const now = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z"); +const capabilitiesWithoutModelSwitch = { + ...CodexProviderCapabilitiesV2, + sessions: { + ...CodexProviderCapabilitiesV2.sessions, + supportsModelSwitchInSession: false, + }, +}; + +function projection(): OrchestrationV2ThreadProjection { + return { + thread: { + id: ThreadId.make("thread_switch_service"), + modelSelection: { instanceId: currentInstanceId, model: "gpt-5.1-codex" }, + runtimeMode: "full-access", + interactionMode: "default", + worktreePath: "/repo", + }, + providerSessions: [ + { + id: currentSessionId, + providerInstanceId: currentInstanceId, + status: "ready", + cwd: "/repo", + capabilities: capabilitiesWithoutModelSwitch, + updatedAt: now, + }, + ], + providerThreads: [], + } as unknown as OrchestrationV2ThreadProjection; +} + +function testLayer(metadata: Readonly>) { + const registry = Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({ + getMetadata: (instanceId) => { + const value = metadata[instanceId]; + return value === undefined + ? Effect.fail( + new ProviderAdapterRegistry.ProviderAdapterRegistryLookupError({ instanceId }), + ) + : Effect.succeed({ + driver, + continuationKey: value.continuationKey, + enabled: true, + capabilities: capabilitiesWithoutModelSwitch, + }); + }, + }); + return ProviderSwitch.layer.pipe(Layer.provide(registry)); +} + +it.effect( + "restarts and releases the current session for unsupported in-session model changes", + () => + Effect.gen(function* () { + const service = yield* ProviderSwitch.ProviderSwitchServiceV2; + const result = yield* service.plan({ + projection: projection(), + targetModelSelection: { instanceId: currentInstanceId, model: "gpt-5.2-codex" }, + }); + assert.equal(result.transition.type, "restart_and_resume"); + assert.deepEqual(result.releaseProviderSessionIds, [currentSessionId]); + }).pipe( + Effect.provide( + testLayer({ [currentInstanceId]: { continuationKey: "codex:account:primary" } }), + ), + ), +); + +it.effect("distinguishes compatible and incompatible instances of the same driver", () => + Effect.gen(function* () { + const service = yield* ProviderSwitch.ProviderSwitchServiceV2; + const compatibleId = ProviderInstanceId.make("codex_compatible"); + const incompatibleId = ProviderInstanceId.make("codex_incompatible"); + const compatible = yield* service.plan({ + projection: projection(), + targetModelSelection: { instanceId: compatibleId, model: "gpt-5.1-codex" }, + }); + const incompatible = yield* service.plan({ + projection: projection(), + targetModelSelection: { instanceId: incompatibleId, model: "gpt-5.1-codex" }, + }); + assert.equal(compatible.transition.type, "restart_and_resume"); + assert.equal(incompatible.transition.type, "create_with_handoff"); + }).pipe( + Effect.provide( + testLayer({ + [currentInstanceId]: { continuationKey: "codex:account:primary" }, + codex_compatible: { continuationKey: "codex:account:primary" }, + codex_incompatible: { continuationKey: "codex:account:other" }, + }), + ), + ), +); diff --git a/apps/server/src/orchestration-v2/ProviderSwitchService.ts b/apps/server/src/orchestration-v2/ProviderSwitchService.ts new file mode 100644 index 00000000000..87d7eb6a3c2 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderSwitchService.ts @@ -0,0 +1,174 @@ +import { + ModelSelection, + OrchestrationV2ThreadProjection, + ProviderSessionId, + ProviderThreadId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts"; +import { + decideProviderSessionTransition, + type ProviderSessionTransition, +} from "./ProviderSessionTransitionPolicy.ts"; + +export interface ProviderSwitchPlanV2 { + readonly instanceChanged: boolean; + readonly modelChanged: boolean; + readonly targetProviderThreadId: ProviderThreadId | null; + readonly releaseProviderSessionIds: ReadonlyArray; + readonly transition: ProviderSessionTransition; +} + +export class ProviderSwitchPlanError extends Schema.TaggedErrorClass()( + "ProviderSwitchPlanError", + { + threadId: ThreadId, + targetProviderInstanceId: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface ProviderSwitchServiceV2Shape { + readonly plan: (input: { + readonly projection: OrchestrationV2ThreadProjection; + readonly targetModelSelection: ModelSelection; + }) => Effect.Effect; +} + +export class ProviderSwitchServiceV2 extends Context.Service< + ProviderSwitchServiceV2, + ProviderSwitchServiceV2Shape +>()("t3/orchestration-v2/ProviderSwitchService/ProviderSwitchServiceV2") {} + +export const layer: Layer.Layer< + ProviderSwitchServiceV2, + never, + ProviderAdapterRegistry.ProviderAdapterRegistryV2 +> = Layer.effect( + ProviderSwitchServiceV2, + Effect.gen(function* () { + const adapters = yield* ProviderAdapterRegistry.ProviderAdapterRegistryV2; + return ProviderSwitchServiceV2.of({ + plan: ({ projection, targetModelSelection }) => + Effect.gen(function* () { + const current = projection.thread.modelSelection; + const instanceChanged = current.instanceId !== targetModelSelection.instanceId; + const modelChanged = current.model !== targetModelSelection.model; + const getMetadata = (instanceId: typeof current.instanceId) => + adapters.getMetadata !== undefined + ? adapters.getMetadata(instanceId) + : adapters.get(instanceId).pipe( + Effect.flatMap((adapter) => + adapter.getCapabilities().pipe( + Effect.map((capabilities) => ({ + driver: adapter.driver, + continuationKey: `${adapter.driver}:instance:${instanceId}`, + enabled: true, + capabilities, + })), + ), + ), + ); + const currentInstance = yield* Effect.option(getMetadata(current.instanceId)); + const targetInstance = yield* Effect.option(getMetadata(targetModelSelection.instanceId)); + const currentSession = projection.providerSessions + .filter((session) => session.providerInstanceId === current.instanceId) + .toSorted( + (left, right) => + DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt), + )[0]; + const transition = Option.isNone(targetInstance) + ? ({ + type: "reject", + reason: "The target provider instance is unavailable.", + } as const) + : decideProviderSessionTransition({ + current: + Option.isNone(currentInstance) || currentSession === undefined + ? null + : { + driver: currentInstance.value.driver, + continuationIdentity: { + driverKind: currentInstance.value.driver, + continuationKey: currentInstance.value.continuationKey, + }, + modelSelection: current, + runtimeMode: projection.thread.runtimeMode, + interactionMode: projection.thread.interactionMode, + workspace: currentSession.cwd, + capabilities: currentSession.capabilities, + }, + target: { + driver: targetInstance.value.driver, + continuationIdentity: { + driverKind: targetInstance.value.driver, + continuationKey: targetInstance.value.continuationKey, + }, + modelSelection: targetModelSelection, + runtimeMode: projection.thread.runtimeMode, + interactionMode: projection.thread.interactionMode, + workspace: + projection.thread.worktreePath ?? + currentSession?.cwd ?? + "", + capabilities: targetInstance.value.capabilities, + available: targetInstance.value.enabled, + }, + }); + if (transition.type === "reject") { + return yield* new ProviderSwitchPlanError({ + threadId: projection.thread.id, + targetProviderInstanceId: targetModelSelection.instanceId, + cause: transition.reason, + }); + } + const targetProviderThread = projection.providerThreads + .filter( + (thread) => + thread.appThreadId === projection.thread.id && + thread.ownerNodeId === null && + thread.providerInstanceId === targetModelSelection.instanceId, + ) + .toSorted( + (left, right) => + DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt), + )[0]; + const releaseProviderSessionIds = projection.providerSessions + .filter((session) => { + if (session.status === "stopped" || session.status === "error") return false; + if (transition.type === "restart_and_resume") { + return session.id === currentSession?.id; + } + if (transition.type === "create_with_handoff") { + return session.providerInstanceId !== targetModelSelection.instanceId; + } + return false; + }) + .map((session) => session.id); + return { + instanceChanged, + modelChanged, + targetProviderThreadId: targetProviderThread?.id ?? null, + releaseProviderSessionIds, + transition, + }; + }).pipe( + Effect.mapError( + (cause) => + new ProviderSwitchPlanError({ + threadId: projection.thread.id, + targetProviderInstanceId: targetModelSelection.instanceId, + cause, + }), + ), + ), + }); + }), +); diff --git a/apps/server/src/orchestration-v2/ProviderTurnControlService.ts b/apps/server/src/orchestration-v2/ProviderTurnControlService.ts new file mode 100644 index 00000000000..87b7df54383 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderTurnControlService.ts @@ -0,0 +1,236 @@ +import { + MessageId, + ProviderSessionId, + ProviderThreadId, + ProviderTurnId, + RunAttemptId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +import { ProjectionStoreV2 } from "./ProjectionStore.ts"; +import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts"; + +const yieldToRuntime = Effect.yieldNow.pipe( + Effect.andThen( + Effect.promise( + () => + new Promise((resolve) => { + setImmediate(resolve); + }), + ), + ), +); + +export class ProviderTurnControlError extends Schema.TaggedErrorClass()( + "ProviderTurnControlError", + { + threadId: ThreadId, + operation: Schema.Literals(["interrupt", "restart", "steer"]), + providerTurnId: ProviderTurnId, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface ProviderTurnControlServiceV2Shape { + readonly interrupt: (input: { + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + readonly providerThreadId: ProviderThreadId; + readonly providerTurnId: ProviderTurnId; + }) => Effect.Effect; + readonly steer: (input: { + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + readonly providerThreadId: ProviderThreadId; + readonly providerTurnId: ProviderTurnId; + readonly messageId: MessageId; + }) => Effect.Effect; + readonly interruptAndAwaitTerminal: (input: { + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + readonly providerThreadId: ProviderThreadId; + readonly providerTurnId: ProviderTurnId; + readonly interruptedAttemptId: RunAttemptId; + }) => Effect.Effect; +} + +export class ProviderTurnControlServiceV2 extends Context.Service< + ProviderTurnControlServiceV2, + ProviderTurnControlServiceV2Shape +>()("t3/orchestration-v2/ProviderTurnControlService/ProviderTurnControlServiceV2") {} + +export const layer: Layer.Layer< + ProviderTurnControlServiceV2, + never, + ProjectionStoreV2 | ProviderSessionManagerV2 +> = Layer.effect( + ProviderTurnControlServiceV2, + Effect.gen(function* () { + const projections = yield* ProjectionStoreV2; + const sessions = yield* ProviderSessionManagerV2; + + const load = (input: { + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + readonly providerThreadId: ProviderThreadId; + readonly providerTurnId: ProviderTurnId; + readonly operation: "interrupt" | "restart" | "steer"; + }) => + Effect.gen(function* () { + const projection = yield* projections.getThreadProjection(input.threadId); + const providerThread = projection.providerThreads.find( + (candidate) => candidate.id === input.providerThreadId, + ); + const providerTurn = projection.providerTurns.find( + (candidate) => candidate.id === input.providerTurnId, + ); + if ( + providerThread === undefined || + providerTurn === undefined || + providerThread.providerSessionId !== input.providerSessionId || + providerTurn.providerThreadId !== providerThread.id + ) { + return yield* new ProviderTurnControlError({ + threadId: input.threadId, + operation: input.operation, + providerTurnId: input.providerTurnId, + cause: "The recorded provider execution target is no longer valid.", + }); + } + if (providerTurn.status !== "running") { + return { projection, providerThread, providerTurn, session: Option.none() }; + } + const session = yield* sessions.get(input.providerSessionId); + if (Option.isNone(session)) { + return yield* new ProviderTurnControlError({ + threadId: input.threadId, + operation: input.operation, + providerTurnId: input.providerTurnId, + cause: `Provider session ${input.providerSessionId} is not active.`, + }); + } + return { projection, providerThread, providerTurn, session }; + }); + + return ProviderTurnControlServiceV2.of({ + interrupt: (input) => + Effect.gen(function* () { + const loaded = yield* load({ ...input, operation: "interrupt" }); + if (Option.isNone(loaded.session)) return; + yield* loaded.session.value.interruptTurn({ + providerThread: loaded.providerThread, + providerTurnId: loaded.providerTurn.id, + }); + }).pipe( + Effect.mapError((cause) => + Schema.is(ProviderTurnControlError)(cause) + ? cause + : new ProviderTurnControlError({ + threadId: input.threadId, + operation: "interrupt", + providerTurnId: input.providerTurnId, + cause, + }), + ), + ), + interruptAndAwaitTerminal: (input) => + Effect.gen(function* () { + const loaded = yield* load({ ...input, operation: "restart" }); + if (Option.isSome(loaded.session)) { + yield* loaded.session.value.interruptTurn({ + providerThread: loaded.providerThread, + providerTurnId: loaded.providerTurn.id, + }); + } + + for (let remaining = 1_000; remaining > 0; remaining -= 1) { + const projection = yield* projections.getThreadProjection(input.threadId); + const providerTurn = projection.providerTurns.find( + (candidate) => candidate.id === input.providerTurnId, + ); + const attempt = projection.attempts.find( + (candidate) => candidate.id === input.interruptedAttemptId, + ); + if ( + providerTurn !== undefined && + providerTurn.status !== "running" && + attempt !== undefined && + attempt.status !== "running" + ) { + return; + } + // Provider terminal events are projected on a detached ingestion + // fiber. Yield through the Node event loop instead of sleeping on + // Effect's clock so deterministic runtimes cannot deadlock a + // command that is waiting for that projection. + yield* yieldToRuntime; + } + return yield* new ProviderTurnControlError({ + threadId: input.threadId, + operation: "restart", + providerTurnId: input.providerTurnId, + cause: `Provider turn ${input.providerTurnId} did not terminalize before restart.`, + }); + }).pipe( + Effect.mapError((cause) => + Schema.is(ProviderTurnControlError)(cause) + ? cause + : new ProviderTurnControlError({ + threadId: input.threadId, + operation: "restart", + providerTurnId: input.providerTurnId, + cause, + }), + ), + ), + steer: (input) => + Effect.gen(function* () { + const loaded = yield* load({ ...input, operation: "steer" }); + if (Option.isNone(loaded.session)) return; + const message = loaded.projection.messages.find( + (candidate) => candidate.id === input.messageId, + ); + const run = loaded.projection.runs.find( + (candidate) => candidate.activeAttemptId === loaded.providerTurn.runAttemptId, + ); + if (message === undefined || run === undefined) { + return yield* new ProviderTurnControlError({ + threadId: input.threadId, + operation: "steer", + providerTurnId: input.providerTurnId, + cause: "The persisted steering message or target run is missing.", + }); + } + yield* loaded.session.value.steerTurn({ + threadId: input.threadId, + runId: run.id, + providerThread: loaded.providerThread, + providerTurnId: loaded.providerTurn.id, + message: { + messageId: message.id, + text: message.text, + attachments: message.attachments, + createdBy: message.createdBy, + creationSource: message.creationSource, + }, + }); + }).pipe( + Effect.mapError((cause) => + Schema.is(ProviderTurnControlError)(cause) + ? cause + : new ProviderTurnControlError({ + threadId: input.threadId, + operation: "steer", + providerTurnId: input.providerTurnId, + cause, + }), + ), + ), + }); + }), +); diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts new file mode 100644 index 00000000000..0e66987b646 --- /dev/null +++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts @@ -0,0 +1,442 @@ +import { + CommandId, + type OrchestrationV2DomainEvent, + type OrchestrationV2ExecutionNode, + type OrchestrationV2ProviderThread, + type OrchestrationV2Run, + type OrchestrationV2RunAttempt, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import { EventSinkV2 } from "./EventSink.ts"; +import { + ContextHandoffServiceV2, + providerMessageWithContextHandoffs, +} from "./ContextHandoffService.ts"; +import { IdAllocatorV2 } from "./IdAllocator.ts"; +import { ProjectionStoreV2 } from "./ProjectionStore.ts"; +import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts"; +import { RunExecutionServiceV2 } from "./RunExecutionService.ts"; +import { RuntimePolicyV2 } from "./RuntimePolicy.ts"; + +export class ProviderTurnStartError extends Schema.TaggedErrorClass()( + "ProviderTurnStartError", + { + runId: RunId, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface ProviderTurnStartServiceV2Shape { + readonly start: (input: { + readonly threadId: ThreadId; + readonly runId: RunId; + }) => Effect.Effect; +} + +export class ProviderTurnStartServiceV2 extends Context.Service< + ProviderTurnStartServiceV2, + ProviderTurnStartServiceV2Shape +>()("t3/orchestration-v2/ProviderTurnStartService/ProviderTurnStartServiceV2") {} + +export const layer: Layer.Layer< + ProviderTurnStartServiceV2, + never, + | EventSinkV2 + | ContextHandoffServiceV2 + | IdAllocatorV2 + | ProjectionStoreV2 + | ProviderSessionManagerV2 + | RunExecutionServiceV2 + | RuntimePolicyV2 +> = Layer.effect( + ProviderTurnStartServiceV2, + Effect.gen(function* () { + const eventSink = yield* EventSinkV2; + const contextHandoffService = yield* ContextHandoffServiceV2; + const idAllocator = yield* IdAllocatorV2; + const projectionStore = yield* ProjectionStoreV2; + const providerSessions = yield* ProviderSessionManagerV2; + const runExecution = yield* RunExecutionServiceV2; + const runtimePolicy = yield* RuntimePolicyV2; + + const start = Effect.fn("orchestrationV2.providerTurnStart.start")(function* (input: { + readonly threadId: ThreadId; + readonly runId: RunId; + }) { + const { runId } = input; + const projection = yield* projectionStore.getThreadProjection(input.threadId); + const run = projection.runs.find((candidate) => candidate.id === runId); + if (run === undefined) { + return yield* new ProviderTurnStartError({ runId, cause: `Run ${runId} was not found.` }); + } + if (run.status !== "starting") { + // The effect is idempotent once the run has advanced or terminalized. + return; + } + const rootNode = projection.nodes.find((candidate) => candidate.id === run.rootNodeId); + const attempt = projection.attempts.find((candidate) => candidate.id === run.activeAttemptId); + const providerThread = projection.providerThreads.find( + (candidate) => candidate.id === run.providerThreadId, + ); + const message = projection.messages.find((candidate) => candidate.id === run.userMessageId); + const checkpointScope = projection.checkpointScopes.find( + (candidate) => candidate.id === rootNode?.checkpointScopeId, + ); + const handoffs = projection.contextHandoffs.filter( + (handoff) => handoff.targetRunId === run.id && handoff.status === "ready", + ); + const nativeForkTransfer = projection.contextTransfers.find( + (transfer) => + transfer.type === "fork" && + transfer.targetThreadId === input.threadId && + transfer.targetRunId === run.id && + transfer.status === "pending" && + transfer.resolution === null, + ); + const existingResumeFallback = projection.contextTransfers.find( + (transfer) => + transfer.type === "provider_handoff" && + transfer.sourceThreadId === projection.thread.id && + transfer.targetThreadId === projection.thread.id && + transfer.targetRunId === run.id && + transfer.status === "resolved_portable" && + transfer.resolution?.strategy === "portable_context", + ); + if ( + rootNode === undefined || + attempt === undefined || + providerThread === undefined || + providerThread.providerSessionId === null || + message === undefined || + checkpointScope === undefined + ) { + return yield* new ProviderTurnStartError({ + runId, + cause: `Run ${runId} is missing its execution projection state.`, + }); + } + const providerSessionId = providerThread.providerSessionId; + + const resolvedRuntimePolicy = yield* runtimePolicy.resolve({ + thread: projection.thread, + modelSelection: run.modelSelection, + }); + const existingSessionProjection = projection.providerSessions.find( + (candidate) => candidate.id === providerSessionId, + ); + const session = yield* providerSessions.open({ + threadId: projection.thread.id, + providerSessionId, + modelSelection: run.modelSelection, + runtimePolicy: resolvedRuntimePolicy, + ...(existingSessionProjection === undefined + ? {} + : { resumeFromSession: existingSessionProjection }), + }); + let effectiveHandoffs = handoffs; + const loadedProviderThread = yield* Effect.gen(function* () { + if (nativeForkTransfer !== undefined) { + const sourceProjection = yield* projectionStore.getThreadProjection( + nativeForkTransfer.sourceThreadId, + ); + const sourceRun = sourceProjection.runs.find( + (candidate) => candidate.id === nativeForkTransfer.sourcePoint.runId, + ); + const sourceProviderThread = sourceProjection.providerThreads.find( + (candidate) => candidate.id === sourceRun?.providerThreadId, + ); + const sourceAttempt = sourceProjection.attempts.find( + (candidate) => candidate.id === sourceRun?.activeAttemptId, + ); + const sourceProviderTurn = sourceProjection.providerTurns.find( + (candidate) => + candidate.id === sourceAttempt?.providerTurnId || + candidate.runAttemptId === sourceAttempt?.id, + ); + if (sourceRun === undefined || sourceProviderThread === undefined) { + return yield* new ProviderTurnStartError({ + runId, + cause: `Native fork transfer ${nativeForkTransfer.id} has no source provider execution.`, + }); + } + return yield* session.forkThread({ + sourceProviderThread, + sourceProviderTurns: sourceProjection.providerTurns, + targetThreadId: projection.thread.id, + modelSelection: run.modelSelection, + runtimePolicy: resolvedRuntimePolicy, + ...(sourceProviderTurn === undefined ? {} : { providerTurnId: sourceProviderTurn.id }), + }); + } + if (providerThread.nativeThreadRef === null) { + return yield* session.ensureThread({ + threadId: projection.thread.id, + modelSelection: run.modelSelection, + runtimePolicy: resolvedRuntimePolicy, + providerSessionId, + }); + } + const resumed = yield* Effect.result( + session.resumeThread({ + providerThread, + threadId: projection.thread.id, + modelSelection: run.modelSelection, + runtimePolicy: resolvedRuntimePolicy, + }), + ); + if (resumed._tag === "Success") { + return resumed.success; + } + + const replacement = yield* session.ensureThread({ + threadId: projection.thread.id, + modelSelection: run.modelSelection, + runtimePolicy: resolvedRuntimePolicy, + providerSessionId, + }); + if (existingResumeFallback !== undefined) { + return replacement; + } + const transferId = yield* idAllocator.allocate.contextTransfer({ + sourceThreadId: projection.thread.id, + targetThreadId: projection.thread.id, + type: "provider_resume_fallback", + }); + const createdAt = yield* DateTime.now; + const handoff = yield* contextHandoffService.prepareProviderHandoff({ + threadId: projection.thread.id, + targetRunId: run.id, + transferId, + fromProviderThreadIds: [providerThread.id], + toProviderThreadId: providerThread.id, + fromProviderInstanceId: providerThread.providerInstanceId, + toProviderInstanceId: run.providerInstanceId, + coveredRunOrdinals: { from: 1, to: Math.max(1, run.ordinal - 1) }, + strategy: "full_thread_summary", + items: projection.turnItems, + createdAt, + }); + effectiveHandoffs = [...handoffs, handoff]; + yield* eventSink.write({ + events: [ + { + id: yield* idAllocator.allocate.event({ threadId: projection.thread.id }), + type: "context-handoff.updated", + threadId: projection.thread.id, + runId: run.id, + providerInstanceId: run.providerInstanceId, + occurredAt: createdAt, + payload: handoff, + }, + { + id: yield* idAllocator.allocate.event({ threadId: projection.thread.id }), + type: "context-transfer.updated", + threadId: projection.thread.id, + runId: run.id, + providerInstanceId: run.providerInstanceId, + occurredAt: createdAt, + payload: { + id: transferId, + type: "provider_handoff", + sourceThreadId: projection.thread.id, + targetThreadId: projection.thread.id, + sourcePoint: { threadId: projection.thread.id }, + basePoint: null, + sourceProviderInstanceId: providerThread.providerInstanceId, + targetProviderInstanceId: run.providerInstanceId, + targetRunId: run.id, + status: "resolved_portable", + resolution: { strategy: "portable_context", contextHandoffId: handoff.id }, + createdBy: "system", + error: null, + createdAt, + updatedAt: createdAt, + consumedAt: null, + }, + }, + ], + }); + return replacement; + }); + const now = yield* DateTime.now; + const runningProviderThread: OrchestrationV2ProviderThread = { + ...loadedProviderThread, + id: providerThread.id, + driver: session.driver, + providerInstanceId: run.providerInstanceId, + providerSessionId, + appThreadId: projection.thread.id, + ownerNodeId: providerThread.ownerNodeId, + firstRunOrdinal: providerThread.firstRunOrdinal ?? run.ordinal, + lastRunOrdinal: run.ordinal, + handoffIds: providerThread.handoffIds, + forkedFrom: providerThread.forkedFrom, + status: "active", + createdAt: providerThread.createdAt, + updatedAt: now, + }; + const runningRun: OrchestrationV2Run = { + ...run, + status: "running", + startedAt: now, + }; + const runningAttempt: OrchestrationV2RunAttempt = { + ...attempt, + status: "running", + startedAt: now, + }; + const runningRootNode: OrchestrationV2ExecutionNode = { + ...rootNode, + status: "running", + startedAt: now, + }; + const events: Array = [ + { + id: yield* idAllocator.allocate.event({ + threadId: projection.thread.id, + providerSessionId, + }), + type: "provider-session.updated", + threadId: projection.thread.id, + driver: session.driver, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: session.providerSession, + }, + { + id: yield* idAllocator.allocate.event({ threadId: projection.thread.id }), + type: "provider-thread.updated", + threadId: projection.thread.id, + driver: session.driver, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: runningProviderThread, + }, + ...(nativeForkTransfer === undefined || runningProviderThread.nativeThreadRef === null + ? [] + : [ + { + id: yield* idAllocator.allocate.event({ threadId: projection.thread.id }), + type: "context-transfer.updated" as const, + threadId: projection.thread.id, + runId: run.id, + driver: session.driver, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: { + ...nativeForkTransfer, + targetProviderInstanceId: run.providerInstanceId, + targetRunId: run.id, + status: "consumed" as const, + resolution: { + strategy: "native_fork" as const, + providerThreadRef: runningProviderThread.nativeThreadRef, + }, + error: null, + updatedAt: now, + consumedAt: now, + }, + }, + ]), + { + id: yield* idAllocator.allocate.event({ threadId: projection.thread.id }), + type: "run.updated", + threadId: projection.thread.id, + runId: run.id, + nodeId: rootNode.id, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: runningRun, + }, + { + id: yield* idAllocator.allocate.event({ threadId: projection.thread.id }), + type: "run-attempt.updated", + threadId: projection.thread.id, + runId: run.id, + nodeId: rootNode.id, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: runningAttempt, + }, + { + id: yield* idAllocator.allocate.event({ threadId: projection.thread.id }), + type: "node.updated", + threadId: projection.thread.id, + runId: run.id, + nodeId: rootNode.id, + providerInstanceId: run.providerInstanceId, + occurredAt: now, + payload: runningRootNode, + }, + ]; + yield* eventSink.write({ events }); + yield* runExecution.startRootRun({ + commandId: CommandId.make(`command:effect:provider-turn.start:${run.id}`), + appThread: projection.thread, + providerSessionId, + session, + run: runningRun, + rootNode: runningRootNode, + checkpointScope, + providerThread: runningProviderThread, + attempt: runningAttempt, + attemptId: attempt.id, + relatedThreadIds: projection.subagents.flatMap((subagent) => + subagent.childThreadId === null ? [] : [subagent.childThreadId], + ), + relatedProviderThreadIds: projection.subagents.flatMap((subagent) => + subagent.providerThreadId === null ? [] : [subagent.providerThreadId], + ), + providerTurnOrdinal: + Math.max( + 0, + ...projection.providerTurns + .filter((turn) => turn.providerThreadId === providerThread.id) + .map((turn) => turn.ordinal), + ) + 1, + shouldFinalizeRun: () => + projectionStore.getThreadProjection(projection.thread.id).pipe( + Effect.map( + (current) => + current.runs.find((candidate) => candidate.id === run.id)?.activeAttemptId === + attempt.id, + ), + Effect.catchCause(() => Effect.succeed(false)), + ), + message: { + messageId: message.id, + text: + effectiveHandoffs.length === 0 + ? message.text + : providerMessageWithContextHandoffs({ + handoffs: effectiveHandoffs, + userText: message.text, + }), + attachments: message.attachments, + createdBy: message.createdBy, + creationSource: message.creationSource, + }, + modelSelection: run.modelSelection, + runtimePolicy: resolvedRuntimePolicy, + }); + }); + + return ProviderTurnStartServiceV2.of({ + start: (input) => + start(input).pipe( + Effect.mapError((cause) => + Schema.is(ProviderTurnStartError)(cause) + ? cause + : new ProviderTurnStartError({ runId: input.runId, cause }), + ), + ), + }); + }), +); diff --git a/apps/server/src/orchestration-v2/RandomUuid.ts b/apps/server/src/orchestration-v2/RandomUuid.ts new file mode 100644 index 00000000000..2f29fc5e399 --- /dev/null +++ b/apps/server/src/orchestration-v2/RandomUuid.ts @@ -0,0 +1,13 @@ +import * as Effect from "effect/Effect"; +import * as Random from "effect/Random"; + +export const randomUuidV4 = Effect.all( + Array.from({ length: 16 }, () => Random.nextIntBetween(0, 256, { halfOpen: true })), +).pipe( + Effect.map((bytes) => { + bytes[6] = (bytes[6]! & 0x0f) | 0x40; + bytes[8] = (bytes[8]! & 0x3f) | 0x80; + const hex = bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; + }), +); diff --git a/apps/server/src/orchestration-v2/ResourceCleanupService.ts b/apps/server/src/orchestration-v2/ResourceCleanupService.ts new file mode 100644 index 00000000000..45c47498ae6 --- /dev/null +++ b/apps/server/src/orchestration-v2/ResourceCleanupService.ts @@ -0,0 +1,71 @@ +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import { resolveAttachmentPathById } from "../attachmentStore.ts"; +import * as ServerConfig from "../config.ts"; +import * as TerminalManager from "../terminal/Manager.ts"; + +export class ResourceCleanupError extends Schema.TaggedErrorClass()( + "ResourceCleanupError", + { + operation: Schema.Literals(["terminal", "attachment"]), + threadId: Schema.optional(Schema.String), + attachmentId: Schema.optional(Schema.String), + cause: Schema.Defect(), + }, +) {} + +export class ResourceCleanupService extends Context.Reference<{ + readonly cleanupTerminals: (threadId: string) => Effect.Effect; + readonly cleanupAttachments: ( + attachmentIds: ReadonlyArray, + ) => Effect.Effect; +}>("t3/orchestration-v2/ResourceCleanupService", { + defaultValue: () => ({ + cleanupTerminals: () => Effect.void, + cleanupAttachments: () => Effect.void, + }), +}) {} + +export const live = Layer.effect( + ResourceCleanupService, + Effect.gen(function* () { + const terminals = yield* TerminalManager.TerminalManager; + const fileSystem = yield* FileSystem.FileSystem; + const config = yield* ServerConfig.ServerConfig; + return { + cleanupTerminals: (threadId: string) => + terminals + .close({ threadId, deleteHistory: true }) + .pipe( + Effect.mapError( + (cause) => new ResourceCleanupError({ operation: "terminal", threadId, cause }), + ), + ), + cleanupAttachments: (attachmentIds: ReadonlyArray) => + Effect.forEach( + attachmentIds, + (attachmentId) => { + const path = resolveAttachmentPathById({ + attachmentsDir: config.attachmentsDir, + attachmentId, + }); + return path === null + ? Effect.void + : fileSystem + .remove(path, { force: true }) + .pipe( + Effect.mapError( + (cause) => + new ResourceCleanupError({ operation: "attachment", attachmentId, cause }), + ), + ); + }, + { discard: true, concurrency: 4 }, + ), + }; + }), +); diff --git a/apps/server/src/orchestration-v2/RunExecutionService.test.ts b/apps/server/src/orchestration-v2/RunExecutionService.test.ts new file mode 100644 index 00000000000..c888fda2307 --- /dev/null +++ b/apps/server/src/orchestration-v2/RunExecutionService.test.ts @@ -0,0 +1,137 @@ +import { assert, it } from "@effect/vitest"; +import { + MessageId, + NodeId, + ProviderDriverKind, + ProviderThreadId, + ProviderTurnId, + RunAttemptId, + RunId, + ThreadId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; + +import type { ProviderAdapterV2Event } from "./ProviderAdapter.ts"; +import { + makeProviderEventRoutingState, + type ProviderEventRouteIdentity, + routeProviderEvent, +} from "./RunExecutionService.ts"; + +const driver = ProviderDriverKind.make("codex"); + +it.effect("routes shared-runtime events only to their owning root run", () => + Effect.gen(function* () { + const now = yield* DateTime.now; + const first: ProviderEventRouteIdentity = { + threadId: ThreadId.make("thread:shared-runtime:first"), + runId: RunId.make("run:shared-runtime:first"), + attemptId: RunAttemptId.make("attempt:shared-runtime:first"), + providerThreadId: ProviderThreadId.make("provider-thread:shared-runtime:first"), + }; + const second: ProviderEventRouteIdentity = { + threadId: ThreadId.make("thread:shared-runtime:second"), + runId: RunId.make("run:shared-runtime:second"), + attemptId: RunAttemptId.make("attempt:shared-runtime:second"), + providerThreadId: ProviderThreadId.make("provider-thread:shared-runtime:second"), + }; + const firstTurnId = ProviderTurnId.make("provider-turn:shared-runtime:first"); + const turnEvent: ProviderAdapterV2Event = { + type: "provider_turn.updated", + driver, + threadId: first.threadId, + providerTurn: { + id: firstTurnId, + providerThreadId: first.providerThreadId, + nodeId: NodeId.make("node:shared-runtime:first"), + runAttemptId: first.attemptId, + nativeTurnRef: null, + ordinal: 1, + status: "running", + startedAt: now, + completedAt: null, + }, + }; + const messageEvent: ProviderAdapterV2Event = { + type: "message.updated", + driver, + message: { + createdBy: "agent", + creationSource: "provider", + id: MessageId.make("message:shared-runtime:first"), + threadId: first.threadId, + runId: first.runId, + nodeId: NodeId.make("node:shared-runtime:first"), + role: "assistant", + text: "first only", + attachments: [], + streaming: false, + createdAt: now, + updatedAt: now, + }, + }; + const terminalEvent: ProviderAdapterV2Event = { + type: "turn.terminal", + driver, + providerTurnId: firstTurnId, + status: "completed", + }; + + const firstInitial = makeProviderEventRoutingState({ + identity: first, + providerTurnId: null, + }); + const secondInitial = makeProviderEventRoutingState({ + identity: second, + providerTurnId: null, + }); + const [firstTurnAccepted, firstAfterTurn] = routeProviderEvent(turnEvent, first, firstInitial); + const [secondTurnAccepted, secondAfterTurn] = routeProviderEvent( + turnEvent, + second, + secondInitial, + ); + + assert.isTrue(firstTurnAccepted); + assert.isFalse(secondTurnAccepted); + assert.isTrue(routeProviderEvent(messageEvent, first, firstAfterTurn)[0]); + assert.isFalse(routeProviderEvent(messageEvent, second, secondAfterTurn)[0]); + assert.isTrue(routeProviderEvent(terminalEvent, first, firstAfterTurn)[0]); + assert.isFalse(routeProviderEvent(terminalEvent, second, secondAfterTurn)[0]); + }), +); + +it("does not route a superseded attempt through a reused provider thread", () => { + const threadId = ThreadId.make("thread:shared-runtime:restart"); + const providerThreadId = ProviderThreadId.make("provider-thread:shared-runtime:restart"); + const oldAttempt: ProviderEventRouteIdentity = { + threadId, + runId: RunId.make("run:shared-runtime:restart"), + attemptId: RunAttemptId.make("attempt:shared-runtime:restart:old"), + providerThreadId, + }; + const newAttempt: ProviderEventRouteIdentity = { + ...oldAttempt, + attemptId: RunAttemptId.make("attempt:shared-runtime:restart:new"), + }; + const oldTurnEvent: ProviderAdapterV2Event = { + type: "provider_turn.updated", + driver, + threadId, + providerTurn: { + id: ProviderTurnId.make("provider-turn:shared-runtime:restart:old"), + providerThreadId, + nodeId: NodeId.make("node:shared-runtime:restart:old"), + runAttemptId: oldAttempt.attemptId, + nativeTurnRef: null, + ordinal: 1, + status: "interrupted", + startedAt: null, + completedAt: null, + }, + }; + + const newState = makeProviderEventRoutingState({ identity: newAttempt, providerTurnId: null }); + assert.isFalse(routeProviderEvent(oldTurnEvent, newAttempt, newState)[0]); +}); diff --git a/apps/server/src/orchestration-v2/RunExecutionService.ts b/apps/server/src/orchestration-v2/RunExecutionService.ts new file mode 100644 index 00000000000..a2e00a2fb10 --- /dev/null +++ b/apps/server/src/orchestration-v2/RunExecutionService.ts @@ -0,0 +1,646 @@ +import { + CommandId, + type ModelSelection, + type OrchestrationV2AppThread, + type OrchestrationV2CheckpointScope, + type OrchestrationV2ExecutionNode, + type OrchestrationV2ProviderThread, + type OrchestrationV2Run, + type OrchestrationV2RunAttempt, + type OrchestrationV2TurnItem, + type ProviderSessionId, + type ProviderThreadId, + type ProviderTurnId, + type RunAttemptId, + type ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; +import * as Layer from "effect/Layer"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; + +import { ServerSettingsService } from "../serverSettings.ts"; +import { CheckpointServiceV2 } from "./CheckpointService.ts"; +import { EventSinkV2 } from "./EventSink.ts"; +import { IdAllocatorV2, type IdAllocatorV2Shape } from "./IdAllocator.ts"; +import type { + ProviderAdapterV2Event, + ProviderAdapterV2RuntimePolicy, + ProviderAdapterV2SessionRuntime, + ProviderAdapterV2TurnMessage, +} from "./ProviderAdapter.ts"; +import { ProviderEventIngestorV2 } from "./ProviderEventIngestor.ts"; + +export interface ProviderEventRoutingState { + readonly ownedThreadIds: ReadonlySet; + readonly ownedProviderThreadIds: ReadonlySet; + readonly ownedProviderTurnIds: ReadonlySet; + readonly rootProviderTurnId: ProviderTurnId | null; +} + +export interface ProviderEventRouteIdentity { + readonly threadId: ThreadId; + readonly runId: OrchestrationV2Run["id"]; + readonly attemptId: RunAttemptId; + readonly providerThreadId: ProviderThreadId; +} + +export function makeProviderEventRoutingState(input: { + readonly identity: ProviderEventRouteIdentity; + readonly providerTurnId: ProviderTurnId | null; + readonly relatedThreadIds?: ReadonlyArray; + readonly relatedProviderThreadIds?: ReadonlyArray; +}): ProviderEventRoutingState { + return { + ownedThreadIds: new Set([input.identity.threadId, ...(input.relatedThreadIds ?? [])]), + ownedProviderThreadIds: new Set([ + input.identity.providerThreadId, + ...(input.relatedProviderThreadIds ?? []), + ]), + ownedProviderTurnIds: + input.providerTurnId === null ? new Set() : new Set([input.providerTurnId]), + rootProviderTurnId: input.providerTurnId, + }; +} + +export function routeProviderEvent( + event: ProviderAdapterV2Event, + input: ProviderEventRouteIdentity, + state: ProviderEventRoutingState, +): readonly [boolean, ProviderEventRoutingState] { + const ownsThread = (threadId: ThreadId): boolean => state.ownedThreadIds.has(threadId); + const ownsChildThread = (threadId: ThreadId): boolean => + threadId !== input.threadId && ownsThread(threadId); + const ownsRun = (runId: string | null): boolean => runId === input.runId; + const addProviderThread = (providerThreadId: ProviderThreadId): ProviderEventRoutingState => ({ + ...state, + ownedProviderThreadIds: new Set([...state.ownedProviderThreadIds, providerThreadId]), + }); + const addProviderTurn = ( + providerTurnId: ProviderTurnId, + root: boolean, + ): ProviderEventRoutingState => ({ + ...state, + ownedProviderTurnIds: new Set([...state.ownedProviderTurnIds, providerTurnId]), + rootProviderTurnId: root ? providerTurnId : state.rootProviderTurnId, + }); + + switch (event.type) { + case "provider_session.updated": + // The session manager persists process-wide status once for every + // attached app thread before broadcasting the adapter event. + return [false, state]; + case "app_thread.created": { + if (event.appThread.id === input.threadId) { + return [true, state]; + } + const isOwnedSubagent = + event.appThread.lineage.relationshipToParent === "subagent" && + event.appThread.lineage.parentThreadId !== null && + ownsThread(event.appThread.lineage.parentThreadId); + if (!isOwnedSubagent) { + return [false, state]; + } + return [ + true, + { + ...state, + ownedThreadIds: new Set([...state.ownedThreadIds, event.appThread.id]), + }, + ]; + } + case "provider_thread.updated": { + const belongs = + state.ownedProviderThreadIds.has(event.providerThread.id) || + (event.providerThread.appThreadId !== null && ownsThread(event.providerThread.appThreadId)); + return belongs ? [true, addProviderThread(event.providerThread.id)] : [false, state]; + } + case "provider_turn.updated": { + const isRoot = event.providerTurn.runAttemptId === input.attemptId; + const belongs = + isRoot || + (event.providerTurn.providerThreadId !== input.providerThreadId && + state.ownedProviderThreadIds.has(event.providerTurn.providerThreadId)) || + state.ownedProviderTurnIds.has(event.providerTurn.id) || + (event.threadId !== undefined && ownsChildThread(event.threadId)); + return belongs ? [true, addProviderTurn(event.providerTurn.id, isRoot)] : [false, state]; + } + case "node.updated": { + const belongs = ownsRun(event.node.runId) || ownsChildThread(event.node.threadId); + if (!belongs || event.node.providerThreadId === null) { + return [belongs, state]; + } + return [true, addProviderThread(event.node.providerThreadId)]; + } + case "subagent.updated": + return [ownsRun(event.subagent.runId) || ownsChildThread(event.subagent.threadId), state]; + case "message.updated": + return [ownsRun(event.message.runId) || ownsChildThread(event.message.threadId), state]; + case "turn_item.updated": + return [ownsRun(event.turnItem.runId) || ownsChildThread(event.turnItem.threadId), state]; + case "plan.updated": + return [ownsRun(event.plan.runId) || ownsChildThread(event.plan.threadId), state]; + case "runtime_request.updated": + return [ + (event.threadId !== undefined && ownsChildThread(event.threadId)) || + (event.runtimeRequest.providerTurnId !== null && + state.ownedProviderTurnIds.has(event.runtimeRequest.providerTurnId)), + state, + ]; + case "turn.terminal": + return [event.providerTurnId === state.rootProviderTurnId, state]; + } +} + +/** + * ERRORS + */ +export class RunExecutionStartError extends Schema.TaggedErrorClass()( + "RunExecutionStartError", + { + commandId: CommandId, + runId: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to start orchestration V2 run execution ${this.runId}.`; + } +} + +export class RunExecutionIngestError extends Schema.TaggedErrorClass()( + "RunExecutionIngestError", + { + runId: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed while ingesting orchestration V2 run execution ${this.runId}.`; + } +} + +export const RunExecutionServiceV2Error = Schema.Union([ + RunExecutionStartError, + RunExecutionIngestError, +]); +export type RunExecutionServiceV2Error = typeof RunExecutionServiceV2Error.Type; + +/** + * SERVICE DEFINITION + */ +export interface RunExecutionServiceV2StartRootRunInput { + readonly commandId: CommandId; + readonly appThread: OrchestrationV2AppThread; + readonly providerSessionId: ProviderSessionId; + readonly session: ProviderAdapterV2SessionRuntime; + readonly run: OrchestrationV2Run; + readonly rootNode: OrchestrationV2ExecutionNode; + readonly checkpointScope: OrchestrationV2CheckpointScope; + readonly providerThread: OrchestrationV2ProviderThread; + readonly attempt: OrchestrationV2RunAttempt; + readonly attemptId: RunAttemptId; + readonly providerTurnOrdinal: number; + readonly relatedThreadIds?: ReadonlyArray; + readonly relatedProviderThreadIds?: ReadonlyArray; + readonly shouldFinalizeRun?: () => Effect.Effect; + readonly message: ProviderAdapterV2TurnMessage; + readonly modelSelection: ModelSelection; + readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; +} + +export interface RunExecutionServiceV2Shape { + readonly startRootRun: ( + input: RunExecutionServiceV2StartRootRunInput, + ) => Effect.Effect; +} + +export class RunExecutionServiceV2 extends Context.Service< + RunExecutionServiceV2, + RunExecutionServiceV2Shape +>()("t3/orchestration-v2/RunExecutionService/RunExecutionServiceV2") {} + +export function shouldDeliverProviderEvent( + event: ProviderAdapterV2Event, + assistantStreamingEnabled: boolean, +): boolean { + if (assistantStreamingEnabled) { + return true; + } + + switch (event.type) { + case "node.updated": + return event.node.kind !== "assistant_message" || event.node.status !== "running"; + case "message.updated": + return event.message.role !== "assistant" || !event.message.streaming; + case "turn_item.updated": + return event.turnItem.type !== "assistant_message" || !event.turnItem.streaming; + default: + return true; + } +} + +/** + * IMPLEMENTATIONS + */ +export const layer: Layer.Layer< + RunExecutionServiceV2, + never, + | CheckpointServiceV2 + | EventSinkV2 + | IdAllocatorV2 + | ProviderEventIngestorV2 + | ServerSettingsService +> = Layer.effect( + RunExecutionServiceV2, + Effect.gen(function* () { + const checkpointService = yield* CheckpointServiceV2; + const eventSink = yield* EventSinkV2; + const idAllocator = yield* IdAllocatorV2; + const providerEventIngestor = yield* ProviderEventIngestorV2; + const serverSettings = yield* ServerSettingsService; + + const writeFinalRunEvents = (input: { + readonly run: OrchestrationV2Run; + readonly rootNode: OrchestrationV2ExecutionNode; + readonly checkpointScope: OrchestrationV2CheckpointScope; + readonly providerThread: OrchestrationV2ProviderThread; + readonly attempt: OrchestrationV2RunAttempt; + readonly shouldFinalizeRun?: () => Effect.Effect; + readonly status: Extract< + OrchestrationV2Run["status"], + "completed" | "interrupted" | "failed" | "cancelled" + >; + }) => + Effect.gen(function* () { + const completedAt = yield* DateTime.now; + const finalizedAttempt: OrchestrationV2RunAttempt | null = { + ...input.attempt, + status: input.status, + completedAt, + }; + const shouldFinalizeRun = + input.shouldFinalizeRun === undefined ? true : yield* input.shouldFinalizeRun(); + if (!shouldFinalizeRun) { + // A newer attempt already owns the run and the command that created + // it terminalized this attempt as superseded. Preserve that domain + // status while retaining the provider's interruption artifact. + if (input.status === "interrupted") { + yield* eventSink.write({ + events: [ + { + id: yield* idAllocator.allocate.event({ threadId: input.run.threadId }), + type: "turn-item.updated" as const, + threadId: input.run.threadId, + runId: input.run.id, + nodeId: input.rootNode.id, + providerInstanceId: input.run.providerInstanceId, + occurredAt: completedAt, + payload: makeInterruptResultTurnItem({ + idAllocator, + run: input.run, + rootNode: input.rootNode, + providerThread: input.providerThread, + completedAt, + }), + }, + ], + }); + } + return; + } + const persistedStatus = input.status === "completed" ? "waiting" : input.status; + const finalizedRun: OrchestrationV2Run = { + ...input.run, + status: persistedStatus, + completedAt: input.status === "completed" ? null : completedAt, + }; + const finalizedRootNode: OrchestrationV2ExecutionNode = { + ...input.rootNode, + status: persistedStatus, + completedAt: input.status === "completed" ? null : completedAt, + checkpointScopeId: input.checkpointScope.id, + }; + const finalizedProviderThread: OrchestrationV2ProviderThread = { + ...input.providerThread, + status: "idle", + updatedAt: completedAt, + }; + const runEventId = yield* idAllocator.allocate.event({ threadId: input.run.threadId }); + const nodeEventId = yield* idAllocator.allocate.event({ threadId: input.run.threadId }); + const providerThreadEventId = yield* idAllocator.allocate.event({ + threadId: input.run.threadId, + }); + const checkpointCaptureCommandId = CommandId.make( + `command:effect:checkpoint.capture:${input.run.id}`, + ); + yield* eventSink.writeWithEffects({ + effects: + input.status === "completed" + ? [ + { + id: `effect:checkpoint.capture:${input.run.id}`, + commandId: checkpointCaptureCommandId, + threadId: input.run.threadId, + request: { + type: "checkpoint.capture" as const, + runId: input.run.id, + scopeId: input.checkpointScope.id, + }, + }, + ] + : [], + events: [ + ...(finalizedAttempt === null + ? [] + : [ + { + id: yield* idAllocator.allocate.event({ threadId: input.run.threadId }), + type: "run-attempt.updated" as const, + threadId: input.run.threadId, + runId: input.run.id, + nodeId: input.rootNode.id, + providerInstanceId: input.run.providerInstanceId, + occurredAt: completedAt, + payload: finalizedAttempt, + }, + ]), + ...(input.status === "interrupted" + ? [ + { + id: yield* idAllocator.allocate.event({ threadId: input.run.threadId }), + type: "turn-item.updated" as const, + threadId: input.run.threadId, + runId: input.run.id, + nodeId: input.rootNode.id, + providerInstanceId: input.run.providerInstanceId, + occurredAt: completedAt, + payload: makeInterruptResultTurnItem({ + idAllocator, + run: input.run, + rootNode: input.rootNode, + providerThread: input.providerThread, + completedAt, + }), + }, + ] + : []), + { + id: runEventId, + type: "run.updated", + threadId: input.run.threadId, + runId: input.run.id, + nodeId: input.rootNode.id, + providerInstanceId: input.run.providerInstanceId, + occurredAt: completedAt, + payload: finalizedRun, + }, + { + id: nodeEventId, + type: "node.updated", + threadId: input.run.threadId, + runId: input.run.id, + nodeId: input.rootNode.id, + providerInstanceId: input.run.providerInstanceId, + occurredAt: completedAt, + payload: finalizedRootNode, + }, + { + id: providerThreadEventId, + type: "provider-thread.updated", + threadId: input.run.threadId, + providerInstanceId: input.run.providerInstanceId, + occurredAt: completedAt, + payload: finalizedProviderThread, + }, + ], + }); + }); + + return RunExecutionServiceV2.of({ + startRootRun: (input) => + Effect.gen(function* () { + const assistantStreamingEnabled = yield* serverSettings.getSettings.pipe( + Effect.map((settings) => settings.enableAssistantStreaming), + Effect.mapError( + (cause) => + new RunExecutionStartError({ + commandId: input.commandId, + runId: input.run.id, + cause, + }), + ), + ); + yield* checkpointService + .captureBaseline({ + scope: input.checkpointScope, + ordinalWithinScope: Math.max(0, input.run.ordinal - 1), + }) + .pipe( + Effect.mapError( + (cause) => + new RunExecutionStartError({ + commandId: input.commandId, + runId: input.run.id, + cause, + }), + ), + ); + const terminalStatus = yield* Ref.make | null>(null); + const latestProviderThread = yield* Ref.make(input.providerThread); + const routeIdentity: ProviderEventRouteIdentity = { + threadId: input.run.threadId, + runId: input.run.id, + attemptId: input.attempt.id, + providerThreadId: input.providerThread.id, + }; + const eventRouting = yield* Ref.make( + makeProviderEventRoutingState({ + identity: routeIdentity, + providerTurnId: input.attempt.providerTurnId, + ...(input.relatedThreadIds === undefined + ? {} + : { relatedThreadIds: input.relatedThreadIds }), + ...(input.relatedProviderThreadIds === undefined + ? {} + : { relatedProviderThreadIds: input.relatedProviderThreadIds }), + }), + ); + const eventSubscription = + input.session.subscribeEvents === undefined + ? { events: input.session.events, close: Effect.void } + : yield* input.session.subscribeEvents; + const providerEventFiber = yield* eventSubscription.events.pipe( + Stream.filterEffect((event) => + Ref.modify(eventRouting, (state) => routeProviderEvent(event, routeIdentity, state)), + ), + Stream.takeUntil((event) => event.type === "turn.terminal"), + Stream.runForEach((event) => + Effect.gen(function* () { + if (shouldDeliverProviderEvent(event, assistantStreamingEnabled)) { + yield* providerEventIngestor.ingestNormalized({ + providerSessionId: input.providerSessionId, + providerInstanceId: input.run.providerInstanceId, + threadId: input.run.threadId, + runId: input.run.id, + nodeId: input.rootNode.id, + event, + }); + } + if (event.type === "provider_thread.updated") { + if (event.providerThread.id === input.providerThread.id) { + yield* Ref.set(latestProviderThread, event.providerThread); + } + } + if (event.type === "turn.terminal") { + yield* Ref.set(terminalStatus, event.status); + } + }), + ), + Effect.mapError((cause) => new RunExecutionIngestError({ runId: input.run.id, cause })), + Effect.flatMap(() => + Effect.gen(function* () { + const status = yield* Ref.get(terminalStatus); + if (status === null) { + return; + } + const providerThread = yield* Ref.get(latestProviderThread); + yield* writeFinalRunEvents({ + run: input.run, + rootNode: input.rootNode, + checkpointScope: input.checkpointScope, + providerThread, + attempt: input.attempt, + ...(input.shouldFinalizeRun === undefined + ? {} + : { shouldFinalizeRun: input.shouldFinalizeRun }), + status, + }).pipe( + Effect.mapError( + (cause) => new RunExecutionIngestError({ runId: input.run.id, cause }), + ), + ); + }), + ), + Effect.catchCause((cause) => + Effect.logWarning("orchestration V2 provider event ingestion failed", { + runId: input.run.id, + cause, + }).pipe( + Effect.andThen(Ref.get(latestProviderThread)), + Effect.flatMap((providerThread) => + writeFinalRunEvents({ + run: input.run, + rootNode: input.rootNode, + checkpointScope: input.checkpointScope, + providerThread, + attempt: input.attempt, + ...(input.shouldFinalizeRun === undefined + ? {} + : { shouldFinalizeRun: input.shouldFinalizeRun }), + status: "failed", + }), + ), + Effect.mapError( + (writeCause) => + new RunExecutionIngestError({ + runId: input.run.id, + cause: { ingest: cause, write: writeCause }, + }), + ), + ), + ), + Effect.ensuring(eventSubscription.close), + Effect.forkDetach, + ); + + yield* input.session + .startTurn({ + appThread: input.appThread, + threadId: input.run.threadId, + runId: input.run.id, + runOrdinal: input.run.ordinal, + providerTurnOrdinal: input.providerTurnOrdinal, + attemptId: input.attemptId, + rootNodeId: input.rootNode.id, + providerThread: input.providerThread, + message: input.message, + modelSelection: input.modelSelection, + runtimePolicy: input.runtimePolicy, + }) + .pipe( + Effect.catchCause((cause) => + Effect.logError("orchestration V2 provider turn start failed", { + runId: input.run.id, + cause, + }).pipe( + Effect.andThen(Fiber.interrupt(providerEventFiber)), + Effect.andThen(Ref.get(latestProviderThread)), + Effect.flatMap((providerThread) => + writeFinalRunEvents({ + run: input.run, + rootNode: input.rootNode, + checkpointScope: input.checkpointScope, + providerThread, + attempt: input.attempt, + ...(input.shouldFinalizeRun === undefined + ? {} + : { shouldFinalizeRun: input.shouldFinalizeRun }), + status: "failed", + }), + ), + Effect.mapError( + (writeCause) => + new RunExecutionStartError({ + commandId: input.commandId, + runId: input.run.id, + cause: { start: cause, write: writeCause }, + }), + ), + ), + ), + ); + }), + } satisfies RunExecutionServiceV2Shape); + }), +); + +function makeInterruptResultTurnItem(input: { + readonly idAllocator: IdAllocatorV2Shape; + readonly run: OrchestrationV2Run; + readonly rootNode: OrchestrationV2ExecutionNode; + readonly providerThread: OrchestrationV2ProviderThread; + readonly completedAt: DateTime.Utc; +}): OrchestrationV2TurnItem { + return { + id: input.idAllocator.derive.runSignalTurnItem({ + runId: input.run.id, + signal: "interrupt-result", + }), + threadId: input.run.threadId, + runId: input.run.id, + nodeId: input.rootNode.id, + providerThreadId: input.providerThread.id, + providerTurnId: input.rootNode.providerTurnId, + nativeItemRef: null, + parentItemId: input.idAllocator.derive.runSignalTurnItem({ + runId: input.run.id, + signal: "interrupt-request", + }), + ordinal: input.run.ordinal * 100 + 98, + status: "interrupted", + title: "Interrupted", + startedAt: input.completedAt, + completedAt: input.completedAt, + updatedAt: input.completedAt, + type: "run_interrupt_result", + message: "Run interrupted by user", + }; +} diff --git a/apps/server/src/orchestration-v2/RunFinalizationService.test.ts b/apps/server/src/orchestration-v2/RunFinalizationService.test.ts new file mode 100644 index 00000000000..48d2b81c9ed --- /dev/null +++ b/apps/server/src/orchestration-v2/RunFinalizationService.test.ts @@ -0,0 +1,41 @@ +import { assert, it, vi } from "@effect/vitest"; +import { + CheckpointScopeId, + RunId, + ThreadId, + type OrchestrationV2ThreadProjection, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import * as CheckpointCapture from "./CheckpointCaptureService.ts"; +import * as ProjectionStore from "./ProjectionStore.ts"; +import * as RunFinalization from "./RunFinalizationService.ts"; + +it.effect("captures the root checkpoint and refreshes workspace state", () => { + const threadId = ThreadId.make("thread_finalize"); + const runId = RunId.make("run_finalize"); + const scopeId = CheckpointScopeId.make("scope_finalize"); + const capture = vi.fn(() => Effect.void); + const refresh = vi.fn(() => Effect.void); + const projection = { + checkpointScopes: [{ id: scopeId, cwd: "/repo" }], + } as unknown as OrchestrationV2ThreadProjection; + const layer = RunFinalization.layer.pipe( + Layer.provide( + Layer.mergeAll( + Layer.mock(CheckpointCapture.CheckpointCaptureServiceV2)({ execute: capture }), + Layer.mock(ProjectionStore.ProjectionStoreV2)({ + getThreadProjection: () => Effect.succeed(projection), + }), + Layer.succeed(RunFinalization.RunFinalizationObserver, { refresh }), + ), + ), + ); + return Effect.gen(function* () { + const service = yield* RunFinalization.RunFinalizationService; + yield* service.finalize({ threadId, runId, scopeId }); + assert.equal(capture.mock.calls.length, 1); + assert.deepEqual(refresh.mock.calls[0], ["/repo"]); + }).pipe(Effect.provide(layer)); +}); diff --git a/apps/server/src/orchestration-v2/RunFinalizationService.ts b/apps/server/src/orchestration-v2/RunFinalizationService.ts new file mode 100644 index 00000000000..1d0b8261876 --- /dev/null +++ b/apps/server/src/orchestration-v2/RunFinalizationService.ts @@ -0,0 +1,97 @@ +import { CheckpointScopeId, RunId, ThreadId } from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts"; +import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts"; +import * as CheckpointCapture from "./CheckpointCaptureService.ts"; +import * as ProjectionStore from "./ProjectionStore.ts"; + +export class RunFinalizationError extends Schema.TaggedErrorClass()( + "RunFinalizationError", + { + threadId: ThreadId, + runId: RunId, + scopeId: CheckpointScopeId, + operation: Schema.Literals(["capture-checkpoint", "refresh-workspace"]), + cause: Schema.Defect(), + }, +) {} + +export class RunFinalizationRefreshError extends Schema.TaggedErrorClass()( + "RunFinalizationRefreshError", + { cwd: Schema.String, cause: Schema.Defect() }, +) {} + +export class RunFinalizationObserver extends Context.Reference<{ + readonly refresh: (cwd: string) => Effect.Effect; +}>("t3/orchestration-v2/RunFinalizationObserver", { + defaultValue: () => ({ refresh: () => Effect.void }), +}) {} + +export class RunFinalizationService extends Context.Service< + RunFinalizationService, + { + readonly finalize: (input: { + readonly threadId: ThreadId; + readonly runId: RunId; + readonly scopeId: CheckpointScopeId; + }) => Effect.Effect; + } +>()("t3/orchestration-v2/RunFinalizationService") {} + +export const make = Effect.gen(function* () { + const checkpointCapture = yield* CheckpointCapture.CheckpointCaptureServiceV2; + const projections = yield* ProjectionStore.ProjectionStoreV2; + const observer = yield* RunFinalizationObserver; + + const finalize: RunFinalizationService["Service"]["finalize"] = Effect.fn( + "RunFinalizationService.finalize", + )(function* (input) { + yield* checkpointCapture + .execute(input) + .pipe( + Effect.mapError( + (cause) => new RunFinalizationError({ ...input, operation: "capture-checkpoint", cause }), + ), + ); + const projection = yield* projections + .getThreadProjection(input.threadId) + .pipe( + Effect.mapError( + (cause) => new RunFinalizationError({ ...input, operation: "refresh-workspace", cause }), + ), + ); + const cwd = projection.checkpointScopes.find((scope) => scope.id === input.scopeId)?.cwd; + if (cwd !== undefined) { + yield* observer + .refresh(cwd) + .pipe( + Effect.mapError( + (cause) => + new RunFinalizationError({ ...input, operation: "refresh-workspace", cause }), + ), + ); + } + }); + return RunFinalizationService.of({ finalize }); +}); + +export const layer = Layer.effect(RunFinalizationService, make); + +export const observerLive = Layer.effect( + RunFinalizationObserver, + Effect.gen(function* () { + const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; + const vcsStatus = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; + return { + refresh: (cwd: string) => + Effect.all([workspaceEntries.refresh(cwd), vcsStatus.refreshStatus(cwd)], { + discard: true, + concurrency: "unbounded", + }).pipe(Effect.mapError((cause) => new RunFinalizationRefreshError({ cwd, cause }))), + }; + }), +); diff --git a/apps/server/src/orchestration-v2/RuntimePolicy.test.ts b/apps/server/src/orchestration-v2/RuntimePolicy.test.ts new file mode 100644 index 00000000000..76a8c2a63d1 --- /dev/null +++ b/apps/server/src/orchestration-v2/RuntimePolicy.test.ts @@ -0,0 +1,99 @@ +import { assert, it } from "@effect/vitest"; +import { + type ModelSelection, + type OrchestrationV2AppThread, + ProjectId, + ProviderInstanceId, + ThreadId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; + +import * as ProjectionProjects from "../persistence/Services/ProjectionProjects.ts"; +import { layerFromProjectRepository, RuntimePolicyV2 } from "./RuntimePolicy.ts"; + +const projectId = ProjectId.make("project:runtime-policy"); +const providerInstanceId = ProviderInstanceId.make("codex"); +const modelSelection = { + instanceId: providerInstanceId, + model: "gpt-5.5", +} satisfies ModelSelection; + +function makeThread(input: { + readonly now: DateTime.Utc; + readonly worktreePath: string | null; +}): OrchestrationV2AppThread { + const threadId = ThreadId.make("thread:runtime-policy"); + return { + createdBy: "user", + creationSource: "web", + id: threadId, + projectId, + title: "Runtime policy", + providerInstanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: input.worktreePath, + activeProviderThreadId: null, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: threadId, + }, + forkedFrom: null, + createdAt: input.now, + updatedAt: input.now, + archivedAt: null, + deletedAt: null, + }; +} + +const TestLayer = layerFromProjectRepository.pipe( + Layer.provide( + Layer.mock(ProjectionProjects.ProjectionProjectRepository)({ + getById: () => + Effect.succeed( + Option.some({ + projectId, + title: "Project", + workspaceRoot: "/project-root", + defaultModelSelection: null, + scripts: [], + createdAt: "2026-06-21T00:00:00.000Z", + updatedAt: "2026-06-21T00:00:00.000Z", + deletedAt: null, + }), + ), + }), + ), +); + +it.layer(TestLayer)("RuntimePolicyV2", (it) => { + it.effect("uses the project root for local-checkout threads", () => + Effect.gen(function* () { + const policy = yield* RuntimePolicyV2; + const now = yield* DateTime.now; + const resolved = yield* policy.resolve({ + thread: makeThread({ now, worktreePath: null }), + modelSelection, + }); + assert.equal(resolved.cwd, "/project-root"); + }), + ); + + it.effect("prefers a provisioned worktree over the project root", () => + Effect.gen(function* () { + const policy = yield* RuntimePolicyV2; + const now = yield* DateTime.now; + const resolved = yield* policy.resolve({ + thread: makeThread({ now, worktreePath: "/project-worktree" }), + modelSelection, + }); + assert.equal(resolved.cwd, "/project-worktree"); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/RuntimePolicy.ts b/apps/server/src/orchestration-v2/RuntimePolicy.ts new file mode 100644 index 00000000000..06bbeae6d7a --- /dev/null +++ b/apps/server/src/orchestration-v2/RuntimePolicy.ts @@ -0,0 +1,146 @@ +import { + ModelSelection, + OrchestrationV2AppThread, + ProjectId, + ProviderInstanceId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +import * as ProjectionProjects from "../persistence/Services/ProjectionProjects.ts"; +import { + ProviderAdapterV2RuntimePolicy, + type ProviderAdapterV2RuntimePolicy as ProviderAdapterV2RuntimePolicyType, +} from "./ProviderAdapter.ts"; + +/** + * ERRORS + */ +export class RuntimePolicyResolveError extends Schema.TaggedErrorClass()( + "RuntimePolicyResolveError", + { + projectId: ProjectId, + providerInstanceId: ProviderInstanceId, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to resolve runtime policy for provider instance ${this.providerInstanceId} in project ${this.projectId}.`; + } +} + +export const RuntimePolicyV2Error = Schema.Union([RuntimePolicyResolveError]); +export type RuntimePolicyV2Error = typeof RuntimePolicyV2Error.Type; + +export const RuntimePolicyV2Override = Schema.Struct({ + cwd: Schema.optional(Schema.String), + approvalPolicy: Schema.optional(Schema.Unknown), + sandboxPolicy: Schema.optional(Schema.Unknown), + reasoningEffort: Schema.optional(Schema.String), +}); +export type RuntimePolicyV2Override = typeof RuntimePolicyV2Override.Type; + +/** + * SERVICE DEFINITION + */ +export interface RuntimePolicyV2Shape { + readonly resolve: (input: { + readonly thread: OrchestrationV2AppThread; + readonly modelSelection: ModelSelection; + }) => Effect.Effect; +} + +export class RuntimePolicyV2 extends Context.Service()( + "t3/orchestration-v2/RuntimePolicy/RuntimePolicyV2", +) {} + +/** + * IMPLEMENTATIONS + */ +export const layer: Layer.Layer = Layer.succeed(RuntimePolicyV2, { + resolve: (input) => + Effect.succeed({ + runtimeMode: input.thread.runtimeMode, + interactionMode: input.thread.interactionMode, + cwd: input.thread.worktreePath, + }), +}); + +export const layerFromProjectRepository: Layer.Layer< + RuntimePolicyV2, + never, + ProjectionProjects.ProjectionProjectRepository +> = Layer.effect( + RuntimePolicyV2, + Effect.gen(function* () { + const projects = yield* ProjectionProjects.ProjectionProjectRepository; + return RuntimePolicyV2.of({ + resolve: Effect.fn("RuntimePolicyV2.resolve")(function* (input) { + const cwd = + input.thread.worktreePath ?? + (yield* projects.getById({ projectId: input.thread.projectId }).pipe( + Effect.mapError( + (cause) => + new RuntimePolicyResolveError({ + projectId: input.thread.projectId, + providerInstanceId: input.modelSelection.instanceId, + cause, + }), + ), + Effect.flatMap( + Option.match({ + onNone: () => + Effect.fail( + new RuntimePolicyResolveError({ + projectId: input.thread.projectId, + providerInstanceId: input.modelSelection.instanceId, + cause: "Project not found.", + }), + ), + onSome: (project) => Effect.succeed(project.workspaceRoot), + }), + ), + )); + return ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: input.thread.runtimeMode, + interactionMode: input.thread.interactionMode, + cwd, + }); + }), + }); + }), +); + +export function layerWithOverride( + override: RuntimePolicyV2Override, +): Layer.Layer { + return Layer.effect( + RuntimePolicyV2, + Effect.gen(function* () { + const base = yield* RuntimePolicyV2; + return { + resolve: (input) => + base.resolve(input).pipe( + Effect.map((policy) => + ProviderAdapterV2RuntimePolicy.make({ + ...policy, + ...(override.cwd === undefined ? {} : { cwd: override.cwd }), + ...(override.approvalPolicy === undefined + ? {} + : { approvalPolicy: override.approvalPolicy }), + ...(override.sandboxPolicy === undefined + ? {} + : { sandboxPolicy: override.sandboxPolicy }), + ...(override.reasoningEffort === undefined + ? {} + : { reasoningEffort: override.reasoningEffort }), + }), + ), + ), + } satisfies RuntimePolicyV2Shape; + }), + ); +} diff --git a/apps/server/src/orchestration-v2/RuntimeRequestService.ts b/apps/server/src/orchestration-v2/RuntimeRequestService.ts new file mode 100644 index 00000000000..00bcb47d3da --- /dev/null +++ b/apps/server/src/orchestration-v2/RuntimeRequestService.ts @@ -0,0 +1,101 @@ +import { + ProviderApprovalDecision, + ProviderSessionId, + ProviderUserInputAnswers, + RuntimeRequestId, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +import { ProjectionStoreV2 } from "./ProjectionStore.ts"; +import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts"; + +export class RuntimeRequestResponseExecutionError extends Schema.TaggedErrorClass()( + "RuntimeRequestResponseExecutionError", + { + threadId: ThreadId, + requestId: RuntimeRequestId, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface RuntimeRequestServiceV2Shape { + readonly respond: (input: { + readonly threadId: ThreadId; + readonly providerSessionId: ProviderSessionId; + readonly requestId: RuntimeRequestId; + readonly decision?: ProviderApprovalDecision; + readonly answers?: ProviderUserInputAnswers; + }) => Effect.Effect; +} + +export class RuntimeRequestServiceV2 extends Context.Service< + RuntimeRequestServiceV2, + RuntimeRequestServiceV2Shape +>()("t3/orchestration-v2/RuntimeRequestService/RuntimeRequestServiceV2") {} + +export const layer: Layer.Layer< + RuntimeRequestServiceV2, + never, + ProjectionStoreV2 | ProviderSessionManagerV2 +> = Layer.effect( + RuntimeRequestServiceV2, + Effect.gen(function* () { + const projections = yield* ProjectionStoreV2; + const sessions = yield* ProviderSessionManagerV2; + + return RuntimeRequestServiceV2.of({ + respond: (input) => + Effect.gen(function* () { + const projection = yield* projections.getThreadProjection(input.threadId); + const request = projection.runtimeRequests.find( + (candidate) => candidate.id === input.requestId, + ); + if (request === undefined) { + return yield* new RuntimeRequestResponseExecutionError({ + threadId: input.threadId, + requestId: input.requestId, + cause: "The runtime request no longer exists.", + }); + } + if ( + request.responseCapability.type !== "live" || + request.responseCapability.providerSessionId !== input.providerSessionId + ) { + return yield* new RuntimeRequestResponseExecutionError({ + threadId: input.threadId, + requestId: input.requestId, + cause: "The runtime request is not resumable on the recorded provider session.", + }); + } + const session = yield* sessions.get(input.providerSessionId); + if (Option.isNone(session)) { + return yield* new RuntimeRequestResponseExecutionError({ + threadId: input.threadId, + requestId: input.requestId, + cause: `Provider session ${input.providerSessionId} is not active.`, + }); + } + yield* session.value.respondToRuntimeRequest({ + requestId: input.requestId, + ...(input.decision === undefined ? {} : { decision: input.decision }), + ...(input.answers === undefined ? {} : { answers: input.answers }), + }); + }).pipe( + Effect.mapError((cause) => + Schema.is(RuntimeRequestResponseExecutionError)(cause) + ? cause + : new RuntimeRequestResponseExecutionError({ + threadId: input.threadId, + requestId: input.requestId, + cause, + }), + ), + ), + }); + }), +); diff --git a/apps/server/src/orchestration-v2/ShellStream.ts b/apps/server/src/orchestration-v2/ShellStream.ts new file mode 100644 index 00000000000..fc63bf738fa --- /dev/null +++ b/apps/server/src/orchestration-v2/ShellStream.ts @@ -0,0 +1,72 @@ +import type { + OrchestrationV2ArchivedShellStreamItem, + OrchestrationV2ThreadShellSnapshot, + OrchestrationV2ShellStreamItem, + OrchestrationV2StoredEvent, +} from "@t3tools/contracts"; + +/** Converts a committed event and its resulting shell snapshot into one delta. */ +export function shellStreamItemFromSnapshot(input: { + readonly stored: OrchestrationV2StoredEvent; + readonly snapshot: OrchestrationV2ThreadShellSnapshot; +}): Exclude { + const active = input.snapshot.threads.find((thread) => thread.id === input.stored.event.threadId); + if (active !== undefined) { + return { + kind: "thread.updated", + sequence: input.stored.sequence, + location: "active", + thread: active, + }; + } + + const archived = input.snapshot.archivedThreads.find( + (thread) => thread.id === input.stored.event.threadId, + ); + if (archived !== undefined) { + return { + kind: "thread.updated", + sequence: input.stored.sequence, + location: "archive", + thread: archived, + }; + } + + return { + kind: "thread.removed", + sequence: input.stored.sequence, + location: + input.stored.event.type === "thread.deleted" && input.stored.event.payload.archivedAt !== null + ? "archive" + : "active", + threadId: input.stored.event.threadId, + }; +} + +/** Converts a committed event into an archive-only delta when it changes archive membership. */ +export function archivedShellStreamItemFromSnapshot(input: { + readonly stored: OrchestrationV2StoredEvent; + readonly snapshot: OrchestrationV2ThreadShellSnapshot; +}): Exclude | null { + const archived = input.snapshot.archivedThreads.find( + (thread) => thread.id === input.stored.event.threadId, + ); + if (archived !== undefined) { + return { + kind: "thread.updated", + sequence: input.stored.sequence, + thread: archived, + }; + } + if ( + input.stored.event.type === "thread.unarchived" || + (input.stored.event.type === "thread.deleted" && input.stored.event.payload.archivedAt !== null) + ) { + return { + kind: "thread.removed", + sequence: input.stored.sequence, + threadId: input.stored.event.threadId, + }; + } + return null; +} diff --git a/apps/server/src/orchestration-v2/SubagentProjection.ts b/apps/server/src/orchestration-v2/SubagentProjection.ts new file mode 100644 index 00000000000..d2bec48536b --- /dev/null +++ b/apps/server/src/orchestration-v2/SubagentProjection.ts @@ -0,0 +1,185 @@ +import type { + MessageId, + ModelSelection, + NodeId, + OrchestrationV2AppThread, + OrchestrationV2Actor, + OrchestrationV2ConversationMessage, + OrchestrationV2CreationSource, + OrchestrationV2ProviderRef, + OrchestrationV2Run, + OrchestrationV2ThreadProjection, + OrchestrationV2TurnItem, + ProviderInstanceId, + ProviderThreadId, + ProviderTurnId, + ThreadId, + TurnItemId, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; + +function trimmed(value: string | null | undefined): string | undefined { + const result = value?.trim(); + return result && result.length > 0 ? result : undefined; +} + +export function subagentThreadTitle(input: { + readonly parentTitle: string; + readonly title?: string | null; + readonly prompt: string; + readonly ordinal: number; +}): string { + const detail = trimmed(input.title) ?? trimmed(input.prompt); + if (detail === undefined) { + return `${input.parentTitle} subagent ${input.ordinal}`; + } + const clipped = detail.length > 72 ? `${detail.slice(0, 69)}...` : detail; + return `Subagent: ${clipped}`; +} + +export function makeSubagentChildThread(input: { + readonly parentThread: OrchestrationV2AppThread; + readonly childThreadId: ThreadId; + readonly parentNodeId: NodeId; + readonly activeProviderThreadId: ProviderThreadId | null; + readonly providerInstanceId: ProviderInstanceId; + readonly modelSelection: ModelSelection; + readonly title: string; + readonly now: DateTime.Utc; + readonly createdBy: OrchestrationV2Actor; + readonly creationSource: OrchestrationV2CreationSource; +}): OrchestrationV2AppThread { + return { + ...input.parentThread, + createdBy: input.createdBy, + creationSource: input.creationSource, + id: input.childThreadId, + title: input.title, + providerInstanceId: input.providerInstanceId, + modelSelection: input.modelSelection, + activeProviderThreadId: input.activeProviderThreadId, + lineage: { + parentThreadId: input.parentThread.id, + relationshipToParent: "subagent", + rootThreadId: input.parentThread.lineage.rootThreadId, + }, + forkedFrom: { + type: "node", + nodeId: input.parentNodeId, + }, + createdAt: input.now, + updatedAt: input.now, + archivedAt: null, + deletedAt: null, + }; +} + +export function makeSubagentConversationArtifacts(input: { + readonly messageId: MessageId; + readonly turnItemId: TurnItemId; + readonly threadId: ThreadId; + readonly rootNodeId: NodeId; + readonly providerThreadId: ProviderThreadId | null; + readonly providerTurnId: ProviderTurnId | null; + readonly nativeItemRef: OrchestrationV2ProviderRef | null; + readonly role: "user" | "assistant"; + readonly text: string; + readonly ordinal: number; + readonly now: DateTime.Utc; +}): { + readonly message: OrchestrationV2ConversationMessage; + readonly turnItem: OrchestrationV2TurnItem; +} { + const message: OrchestrationV2ConversationMessage = { + createdBy: "agent", + creationSource: "provider", + id: input.messageId, + threadId: input.threadId, + runId: null, + nodeId: input.rootNodeId, + role: input.role, + text: input.text, + attachments: [], + streaming: false, + createdAt: input.now, + updatedAt: input.now, + }; + const base = { + id: input.turnItemId, + threadId: input.threadId, + runId: null, + nodeId: input.rootNodeId, + providerThreadId: input.providerThreadId, + providerTurnId: input.providerTurnId, + nativeItemRef: input.nativeItemRef, + parentItemId: null, + ordinal: input.ordinal, + status: "completed" as const, + title: null, + startedAt: input.now, + completedAt: input.now, + updatedAt: input.now, + messageId: input.messageId, + text: input.text, + }; + const turnItem: OrchestrationV2TurnItem = + input.role === "user" + ? { + ...base, + createdBy: "agent", + creationSource: "provider", + type: "user_message", + inputIntent: "turn_start", + attachments: [], + } + : { + ...base, + type: "assistant_message", + streaming: false, + }; + return { message, turnItem }; +} + +export function subagentResultForRun( + projection: OrchestrationV2ThreadProjection, + run: OrchestrationV2Run, +): { + readonly text: string; + readonly messageId: OrchestrationV2ConversationMessage["id"] | null; + readonly turnItemId: OrchestrationV2TurnItem["id"] | null; +} { + const message = + projection.messages + .filter( + (candidate) => + candidate.runId === run.id && + candidate.role === "assistant" && + candidate.text.trim().length > 0, + ) + .toSorted( + (left, right) => + DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt), + )[0] ?? null; + const turnItem = + projection.turnItems + .filter( + ( + candidate, + ): candidate is Extract => + candidate.runId === run.id && + candidate.type === "assistant_message" && + candidate.text.trim().length > 0, + ) + .toSorted((left, right) => right.ordinal - left.ordinal)[0] ?? null; + const text = + message?.text ?? + turnItem?.text ?? + (run.status === "completed" + ? "Child task completed without an assistant result." + : `Child task ended with status ${run.status}.`); + return { + text, + messageId: message?.id ?? turnItem?.messageId ?? null, + turnItemId: turnItem?.id ?? null, + }; +} diff --git a/apps/server/src/orchestration-v2/TODO.md b/apps/server/src/orchestration-v2/TODO.md new file mode 100644 index 00000000000..8b2d93c0bf7 --- /dev/null +++ b/apps/server/src/orchestration-v2/TODO.md @@ -0,0 +1,168 @@ +# Orchestration V2 TODO + +This file tracks remaining backend-oriented V2 work. Architecture-level intent lives in +[`docs/orchestration-v2`](../../../../docs/orchestration-v2); this file is the local +implementation checklist for `apps/server/src/orchestration-v2`. + +## Current Baseline + +- V2 commands/events/projections are server-owned and replayable. +- Message dispatch supports start, steer, queue, queue reorder, promote queued message to steer, + interrupt, and provider switch command shapes. +- Checkpoint rollback is currently a full revert: filesystem checkpoint restore, provider thread + rollback, stale checkpoint marking, and later run/node `rolled_back` projection state. +- Codex same-provider fork is lazy: `thread.fork` records lineage and pending transfer, and first + dispatch resolves native Codex fork. Earlier source-point forks use native `thread/fork` followed + by fork-local `thread/rollback`. +- Native Codex fork-from-earlier-run has a real replay-backed test fixture: + `testkit/fixtures/thread_fork_native_prior_turn`. +- Merge-back from a fork into its source thread records a `merge_back` context transfer, materializes + a `fork_delta_summary` context handoff, and injects that handoff into the next source-thread run. + +## Projection Hardening + +Target docs: + +- [`core-graph-and-data-model.md`](../../../../docs/orchestration-v2/core-graph-and-data-model.md) +- [`thread-lineage-and-context-transfer.md`](../../../../docs/orchestration-v2/thread-lineage-and-context-transfer.md) +- [`testing-strategy.md`](../../../../docs/orchestration-v2/testing-strategy.md) + +TODO: + +- [x] Add end-to-end projection assertions for forked threads, not only provider-context behavior. + The fork projection should show inherited user-visible items through the source point plus an + explicit fork marker. +- [x] Decide and implement the projection representation for inherited fork history: + referenced lineage overlay vs physically duplicated projection items. Prefer referenced overlay + unless product requirements need independent editable history. +- [x] Add local visible projection assertions to non-fork replay fixtures: + `visibleTurnItems` mirrors canonical local `turnItems` for simple, multi-turn, queue, + steering, interrupt, rollback, planning, tool, and web-search fixtures. +- [ ] Ensure projections render rollback state consistently: + rolled-back runs/nodes/items, stale checkpoints, and active provider thread cursor. +- [ ] Add projection tests for interrupt edge cases: + provider emits chunks after interrupt requested, provider ignores/delays interrupt, interrupt is + immediately followed by queue/steer/start. +- [ ] Add projection tests for queue/steer flows: + queued message visibility, queue reorder, promote queued message to steer, and post-interrupt + dispatch visibility. + +## Context Transfer And Merge-Back + +Target docs: + +- [`thread-lineage-and-context-transfer.md`](../../../../docs/orchestration-v2/thread-lineage-and-context-transfer.md) +- [`provider-switching-and-context.md`](../../../../docs/orchestration-v2/provider-switching-and-context.md) + +TODO: + +- [x] Implement merge-back from a fork into its source thread: + source fork point as `basePoint`, fork latest stable point as `sourcePoint`, and source-thread + next user message as the consuming run. +- [x] Materialize delta context artifacts for merge-back and persist them as auditable + `ContextHandoff` records. +- [x] Add replay-backed integration coverage for merge-back: + fork, explore in fork, merge back, then assert source provider receives only the fork delta plus + the new user message. +- [ ] Implement portable context handoff for cross-provider forks and same-thread provider switches. +- [ ] Define explicit failure states for unresolved context transfers: + missing source point, unsupported provider capability, context too large, source projection not + stable, and adapter resolution failure. + +## Capability And Policy Model + +Target docs: + +- [`provider-capability-system.md`](../../../../docs/orchestration-v2/provider-capability-system.md) +- [`feature-lifecycles.md`](../../../../docs/orchestration-v2/feature-lifecycles.md) + +TODO: + +- [ ] Audit capability use against the documented nested `OrchestrationV2ProviderCapabilities` + shape and fill any behavior gaps. +- [ ] Keep orchestration decisions capability/policy driven. Shared runtime code should not branch on + provider name except at adapter registration/resolution boundaries. +- [ ] Make optional adapter methods impossible to call without going through a policy wrapper or a + capability-checked branch. +- [ ] Add typed capability/policy errors for: + native fork unavailable, rollback unavailable, steering unavailable, interrupt unavailable, + context handoff unavailable, and weak terminal status. +- [ ] Add capability-aware tests using real adapter/test layers at provider boundaries only. Do not mock + core orchestration policy. + +## Checkpoint And Rollback + +Target docs: + +- [`feature-lifecycles.md`](../../../../docs/orchestration-v2/feature-lifecycles.md) +- [`core-graph-and-data-model.md`](../../../../docs/orchestration-v2/core-graph-and-data-model.md) + +TODO: + +- [ ] Document current `checkpoint.rollback` command semantics in contracts/docs as full revert: + filesystem restore plus provider conversation rollback. +- [ ] Decide whether we need separate commands for conversation-only rollback and filesystem-only + restore. Do not add them until a real product flow needs them. +- [ ] Add tests for rollback after fork and rollback inside fork: + source rollback should not corrupt child lineage, and fork rollback should not mutate source + provider state. +- [ ] Validate rollback behavior when no active provider thread exists. Current behavior fails; decide + whether a filesystem-only rollback fallback is useful or too surprising. + +## Provider Switching And Second Adapter + +Target docs: + +- [`provider-switching-and-context.md`](../../../../docs/orchestration-v2/provider-switching-and-context.md) +- [`provider-capability-system.md`](../../../../docs/orchestration-v2/provider-capability-system.md) +- [`testing-strategy.md`](../../../../docs/orchestration-v2/testing-strategy.md) + +TODO: + +- [x] Add Claude replay fixture definitions for recorded fixtures, with TODO fixture slots that + reference the corresponding Codex transcripts and V2 docs. +- [ ] Promote Claude `simple` from the replay adapter to a real `ClaudeAdapterV2` replay test: + live and replay both consume an injected Agent SDK `query()` async iterable. +- [ ] Record Claude `multi_turn` from real usage and prove native session/thread continuation. +- [ ] Record Claude `tool_call_read_only` from real usage and prove read-only tool projection without + approvals. +- [ ] Keep unrecorded Claude fixtures out of `testkit/fixtures/index.ts` until each has a real + transcript and real adapter assertions. +- [ ] Add the second adapter once these vv0 Claude slices are stable enough to validate + cross-provider behavior. +- [ ] Use the second adapter to test: + cross-provider fork, same-thread provider switch, returning to a previous provider thread with + delta handoff, and unsupported capability fallback paths. + +## Subagents + +Target docs: + +- [`thread-lineage-and-context-transfer.md`](../../../../docs/orchestration-v2/thread-lineage-and-context-transfer.md) +- [`provider-capability-system.md`](../../../../docs/orchestration-v2/provider-capability-system.md) + +TODO: + +- [x] Add provider-native subagent observation for Codex and Claude replay-backed fixtures. +- [ ] Model native subagents and app-owned cross-provider subagents as related thread/subthread graph + entries with different creator/lifecycle policy. +- [ ] Preserve native provider subagent refs where available, but do not make the app graph depend on + provider-native ids as primary ids. +- [ ] Add deeper tests for subagent wait, close, result transfer, pending approvals, failed/stopped + tasks, and fork-from-subagent behavior. + +FOOD FOR THOUGHT: + +- Custom t3code tools/mcp_server that lets agents spawn subagents of other providers powered by the T3 Orchestrator + +## Debugger-Only Work + +- Keep debugger UI useful but temporary. Backend semantics and projections should be the source of + truth. +- [x] Wire the debugger thread tree to persisted V2 shell state through websocket RPC instead of + debugger-local thread discovery state. +- Continue exposing lightweight controls for new backend surfaces: + fork from response, new thread, full revert from user message checkpoint, merge-back, and provider + switch. +- Avoid adding mock backend behavior for debugger convenience. In-memory debugger state is fine for + layout affordances, but backend behavior must route through V2 commands/projections. diff --git a/apps/server/src/orchestration-v2/ThreadForkService.ts b/apps/server/src/orchestration-v2/ThreadForkService.ts new file mode 100644 index 00000000000..9527c5040b1 --- /dev/null +++ b/apps/server/src/orchestration-v2/ThreadForkService.ts @@ -0,0 +1,111 @@ +import { + ContextTransferId, + OrchestrationV2Actor, + OrchestrationV2AppThread, + OrchestrationV2ContextSourcePoint, + OrchestrationV2ContextTransfer, + OrchestrationV2CreationSource, + OrchestrationV2ProviderThread, + OrchestrationV2Run, + OrchestrationV2ThreadProjection, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +export interface ThreadForkPlanV2 { + readonly targetThread: OrchestrationV2AppThread; + readonly transfer: OrchestrationV2ContextTransfer; +} + +export class ThreadForkPlanError extends Schema.TaggedErrorClass()( + "ThreadForkPlanError", + { + sourceThreadId: ThreadId, + targetThreadId: ThreadId, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface ThreadForkServiceV2Shape { + readonly plan: (input: { + readonly sourceProjection: OrchestrationV2ThreadProjection; + readonly sourceRun: OrchestrationV2Run; + readonly sourceProviderThread: OrchestrationV2ProviderThread | undefined; + readonly canonicalSourcePoint: OrchestrationV2ContextSourcePoint; + readonly transferId: ContextTransferId; + readonly targetThreadId: ThreadId; + readonly title?: string; + readonly createdBy: OrchestrationV2Actor; + readonly creationSource: OrchestrationV2CreationSource; + readonly createdAt: DateTime.Utc; + }) => Effect.Effect; +} + +export class ThreadForkServiceV2 extends Context.Service< + ThreadForkServiceV2, + ThreadForkServiceV2Shape +>()("t3/orchestration-v2/ThreadForkService/ThreadForkServiceV2") {} + +export const layer: Layer.Layer = Layer.succeed( + ThreadForkServiceV2, + ThreadForkServiceV2.of({ + plan: (input) => + Effect.gen(function* () { + if (input.sourceRun.status !== "completed") { + return yield* new ThreadForkPlanError({ + sourceThreadId: input.sourceProjection.thread.id, + targetThreadId: input.targetThreadId, + cause: `Fork source run ${input.sourceRun.id} is ${input.sourceRun.status}.`, + }); + } + const targetThread: OrchestrationV2AppThread = { + ...input.sourceProjection.thread, + createdBy: input.createdBy, + creationSource: input.creationSource, + id: input.targetThreadId, + title: input.title ?? `${input.sourceProjection.thread.title} fork`, + activeProviderThreadId: null, + lineage: { + parentThreadId: input.sourceProjection.thread.id, + relationshipToParent: "fork", + rootThreadId: input.sourceProjection.thread.lineage.rootThreadId, + }, + forkedFrom: { + type: "run", + threadId: input.sourceProjection.thread.id, + runId: input.sourceRun.id, + }, + createdAt: input.createdAt, + updatedAt: input.createdAt, + archivedAt: null, + deletedAt: null, + }; + const transfer: OrchestrationV2ContextTransfer = { + id: input.transferId, + type: "fork", + sourceThreadId: input.sourceProjection.thread.id, + targetThreadId: input.targetThreadId, + sourcePoint: input.canonicalSourcePoint, + basePoint: null, + sourceProviderInstanceId: input.sourceRun.providerInstanceId, + targetProviderInstanceId: null, + targetRunId: null, + status: "pending", + resolution: null, + createdBy: input.createdBy, + error: + input.sourceProviderThread?.nativeThreadRef?.strength === "strong" + ? null + : "Source provider thread does not expose a strong native thread ref.", + createdAt: input.createdAt, + updatedAt: input.createdAt, + consumedAt: null, + }; + return { targetThread, transfer }; + }), + }), +); diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts new file mode 100644 index 00000000000..ea151f9d06f --- /dev/null +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts @@ -0,0 +1,399 @@ +import { assert, it, vi } from "@effect/vitest"; +import { + EventId, + CommandId, + ProjectId, + ProviderInstanceId, + ThreadId, + type OrchestrationV2DomainEvent, + type OrchestrationV2ThreadProjection, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; + +import * as GitWorkflow from "../git/GitWorkflowService.ts"; +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import * as ProjectService from "../project/ProjectService.ts"; +import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts"; +import * as TextGeneration from "../textGeneration/TextGeneration.ts"; +import * as IdAllocator from "./IdAllocator.ts"; +import { OrchestratorDispatchError, type OrchestratorV2DispatchResult } from "./Orchestrator.ts"; +import { emptyProjection } from "./ProjectionStore.ts"; +import * as ThreadLaunch from "./ThreadLaunchService.ts"; +import * as ThreadManagement from "./ThreadManagementService.ts"; + +const projectId = ProjectId.make("project_launch_test"); +const modelSelection = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.1-codex", +} as const; +const project = { + id: projectId, + title: "Project", + workspaceRoot: "/repo", + repositoryIdentity: null, + faviconPath: null, + defaultModelSelection: modelSelection, + scripts: [], + createdAt: "2026-06-20T00:00:00.000Z", + updatedAt: "2026-06-20T00:00:00.000Z", + deletedAt: null, +} as const; + +function projectionFor(threadId: ThreadId): OrchestrationV2ThreadProjection { + const now = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z"); + const event = { + id: EventId.make(`event:${threadId}`), + type: "thread.created", + threadId, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: { + id: threadId, + projectId, + title: "Thread", + providerInstanceId: modelSelection.instanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: null, + lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId }, + forkedFrom: null, + createdBy: "user", + creationSource: "web", + createdAt: now, + updatedAt: now, + archivedAt: null, + deletedAt: null, + }, + } satisfies Extract; + return emptyProjection(event); +} + +const makeLayer = (options?: { readonly failCreate?: boolean; readonly failSetup?: boolean }) => { + const projections = new Map(); + const dispatch = vi.fn( + ( + command: Parameters[0], + ): Effect.Effect => { + if (command.type === "thread.metadata.update") { + const existing = projections.get(command.threadId); + if (existing === undefined) return Effect.die("missing existing projection"); + projections.set(command.threadId, { + ...existing, + thread: { + ...existing.thread, + ...(command.title === undefined ? {} : { title: command.title }), + ...(command.branch === undefined ? {} : { branch: command.branch }), + ...(command.worktreePath === undefined ? {} : { worktreePath: command.worktreePath }), + }, + }); + return Effect.succeed({ sequence: 1, storedEvents: [] }); + } + if (command.type !== "thread.create") return Effect.die("unexpected command"); + if (options?.failCreate) { + return Effect.fail( + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: "create failed", + }), + ); + } + const projection = projectionFor(command.threadId); + projections.set(command.threadId, { + ...projection, + thread: { + ...projection.thread, + title: command.title, + branch: command.branch, + worktreePath: command.worktreePath, + }, + }); + return Effect.succeed({ sequence: 1, storedEvents: [] }); + }, + ); + const createWorktree = vi.fn( + (_input: Parameters[0]) => + Effect.succeed({ + worktree: { path: "/repo-worktrees/feature", refName: "feature", headSha: "abc" }, + } as never), + ); + const removeWorktree = vi.fn(() => Effect.void); + const fetchRemote = vi.fn( + (_input: Parameters[0]) => + Effect.void, + ); + const resolveRemoteTrackingCommit = vi.fn( + ( + _input: Parameters< + GitWorkflow.GitWorkflowService["Service"]["resolveRemoteTrackingCommit"] + >[0], + ) => Effect.succeed({ commitSha: "remote-main-sha", remoteRefName: "origin/main" }), + ); + const runForThread = vi.fn(() => + options?.failSetup + ? Effect.fail( + new ProjectSetupScriptRunner.ProjectSetupScriptOperationError({ + threadId: "thread", + worktreePath: "/repo-worktrees/feature", + operation: "openTerminal", + cause: "setup failed", + }), + ) + : Effect.succeed({ status: "no-script" as const }), + ); + const sendToThread = vi.fn(() => + Effect.succeed({} as ThreadManagement.ThreadManagementSendResult), + ); + + const dependencies = Layer.mergeAll( + Layer.succeed(ProjectService.ProjectService, { + create: () => Effect.die("unused"), + bootstrap: () => Effect.die("unused"), + update: () => Effect.die("unused"), + delete: () => Effect.die("unused"), + getById: (id) => Effect.succeed(id === projectId ? Option.some(project) : Option.none()), + getByWorkspaceRoot: () => Effect.succeed(Option.some(project)), + snapshot: Effect.die("unused"), + }), + Layer.mock(GitWorkflow.GitWorkflowService)({ + createWorktree, + fetchRemote, + removeWorktree, + resolveRemoteTrackingCommit, + }), + Layer.succeed(ProjectSetupScriptRunner.ProjectSetupScriptRunner, { + runForThread, + }), + Layer.mock(TextGeneration.TextGeneration)({ + generateThreadTitle: () => Effect.succeed({ title: "Generated title" }), + generateBranchName: () => Effect.succeed({ branch: "generated-branch" }), + }), + Layer.mock(ThreadManagement.ThreadManagementService)({ + dispatch, + getThreadProjection: (threadId) => + projections.has(threadId) + ? Effect.succeed(projections.get(threadId)!) + : Effect.die("missing projection"), + sendToThread, + }), + IdAllocator.layer, + ); + return { + layer: ThreadLaunch.layer.pipe( + Layer.provideMerge(dependencies), + Layer.provideMerge(SqlitePersistenceMemory), + ), + dispatch, + fetchRemote, + createWorktree, + projections, + removeWorktree, + resolveRemoteTrackingCommit, + runForThread, + sendToThread, + }; +}; + +it.effect("persists root-workspace launches and returns the committed workflow on retry", () => { + const test = makeLayer(); + return Effect.gen(function* () { + const service = yield* ThreadLaunch.ThreadLaunchService; + const input = { + commandId: CommandId.make("command_launch_root"), + projectId, + title: "Thread", + modelSelection, + runtimeMode: "full-access" as const, + interactionMode: "default" as const, + workspaceStrategy: { type: "root" as const }, + createdBy: "user" as const, + creationSource: "web" as const, + }; + const first = yield* service.launch(input); + const retry = yield* service.launch(input); + assert.equal(first.threadId, retry.threadId); + assert.isFalse(first.resumed); + assert.isTrue(retry.resumed); + assert.equal(test.dispatch.mock.calls.length, 1); + }).pipe(Effect.provide(test.layer)); +}); + +it.effect( + "generates first-run title and branch, runs setup once, and sends the initial message", + () => { + const test = makeLayer(); + return Effect.gen(function* () { + const service = yield* ThreadLaunch.ThreadLaunchService; + const input = { + commandId: CommandId.make("command_launch_generated"), + projectId, + title: "New thread", + modelSelection, + runtimeMode: "full-access" as const, + interactionMode: "default" as const, + workspaceStrategy: { type: "worktree" as const, baseRef: "main" }, + initialMessage: { text: "Build the feature", attachments: [] }, + createdBy: "user" as const, + creationSource: "web" as const, + }; + yield* service.launch(input); + yield* service.launch(input); + const createCommand = test.dispatch.mock.calls[0]?.[0]; + assert.equal(createCommand?.type, "thread.create"); + if (createCommand?.type === "thread.create") { + assert.equal(createCommand.title, "Generated title"); + assert.equal(createCommand.branch, "feature"); + } + assert.equal(test.runForThread.mock.calls.length, 1); + assert.equal(test.sendToThread.mock.calls.length, 1); + }).pipe(Effect.provide(test.layer)); + }, +); + +it.effect("preserves an existing worktree when launching a new thread", () => { + const test = makeLayer(); + return Effect.gen(function* () { + const service = yield* ThreadLaunch.ThreadLaunchService; + const result = yield* service.launch({ + commandId: CommandId.make("command_launch_existing_worktree"), + threadId: ThreadId.make("thread_existing_worktree"), + projectId, + title: "Thread", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + workspaceStrategy: { + type: "existing_worktree", + worktreePath: "/repo-worktrees/existing", + branch: "existing", + }, + createdBy: "user", + creationSource: "web", + }); + assert.equal(result.projection.thread.worktreePath, "/repo-worktrees/existing"); + assert.equal(result.projection.thread.branch, "existing"); + assert.equal(test.createWorktree.mock.calls.length, 0); + assert.equal(test.removeWorktree.mock.calls.length, 0); + }).pipe(Effect.provide(test.layer)); +}); + +it.effect("fetches and resolves origin before provisioning an origin-based worktree", () => { + const test = makeLayer(); + return Effect.gen(function* () { + const service = yield* ThreadLaunch.ThreadLaunchService; + yield* service.launch({ + commandId: CommandId.make("command_launch_origin_worktree"), + projectId, + title: "Thread", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + workspaceStrategy: { + type: "worktree", + baseRef: "main", + branch: "feature", + startFromOrigin: true, + }, + createdBy: "user", + creationSource: "web", + }); + assert.deepEqual(test.fetchRemote.mock.calls[0]?.[0], { + cwd: "/repo", + remoteName: "origin", + }); + assert.deepEqual(test.resolveRemoteTrackingCommit.mock.calls[0]?.[0], { + cwd: "/repo", + refName: "main", + fallbackRemoteName: "origin", + }); + assert.deepEqual(test.createWorktree.mock.calls[0]?.[0], { + cwd: "/repo", + refName: "remote-main-sha", + newRefName: "feature", + baseRefName: "main", + path: null, + }); + }).pipe(Effect.provide(test.layer)); +}); + +it.effect("provisions a worktree for an existing empty thread before sending", () => { + const test = makeLayer(); + const threadId = ThreadId.make("thread_existing_empty"); + test.projections.set(threadId, projectionFor(threadId)); + return Effect.gen(function* () { + const service = yield* ThreadLaunch.ThreadLaunchService; + yield* service.launch({ + commandId: CommandId.make("command_launch_existing_empty"), + threadId, + reuseExistingThread: true, + projectId, + title: "Thread", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + workspaceStrategy: { type: "worktree", baseRef: "main", branch: "feature" }, + initialMessage: { text: "Implement it", attachments: [] }, + createdBy: "user", + creationSource: "web", + }); + const updateCommand = test.dispatch.mock.calls.find( + ([command]) => command.type === "thread.metadata.update", + )?.[0]; + assert.equal(updateCommand?.type, "thread.metadata.update"); + if (updateCommand?.type === "thread.metadata.update") { + assert.equal(updateCommand.worktreePath, "/repo-worktrees/feature"); + assert.equal(updateCommand.branch, "feature"); + } + assert.equal(test.sendToThread.mock.calls.length, 1); + }).pipe(Effect.provide(test.layer)); +}); + +it.effect("removes a new worktree and does not create a thread when setup fails", () => { + const test = makeLayer({ failSetup: true }); + return Effect.gen(function* () { + const service = yield* ThreadLaunch.ThreadLaunchService; + yield* service + .launch({ + commandId: CommandId.make("command_launch_setup_failure"), + projectId, + title: "Thread", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + workspaceStrategy: { type: "worktree", baseRef: "main", branch: "feature" }, + createdBy: "user", + creationSource: "web", + }) + .pipe(Effect.flip); + assert.equal(test.removeWorktree.mock.calls.length, 1); + assert.equal(test.dispatch.mock.calls.length, 0); + }).pipe(Effect.provide(test.layer)); +}); + +it.effect("compensates a newly-created worktree when thread creation fails", () => { + const test = makeLayer({ failCreate: true }); + return Effect.gen(function* () { + const service = yield* ThreadLaunch.ThreadLaunchService; + yield* service + .launch({ + commandId: CommandId.make("command_launch_worktree"), + projectId, + title: "Thread", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + workspaceStrategy: { type: "worktree", baseRef: "main", branch: "feature" }, + createdBy: "user", + creationSource: "web", + }) + .pipe(Effect.flip); + assert.equal(test.createWorktree.mock.calls.length, 1); + assert.equal(test.removeWorktree.mock.calls.length, 1); + }).pipe(Effect.provide(test.layer)); +}); diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.ts new file mode 100644 index 00000000000..cef8b049d0a --- /dev/null +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.ts @@ -0,0 +1,504 @@ +import { + CommandId, + type ChatAttachment, + type MessageId, + type ModelSelection, + type OrchestrationV2Actor, + type OrchestrationV2CreationSource, + type OrchestrationV2ThreadProjection, + type ProviderInteractionMode, + ProjectId, + type RuntimeMode, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; +import * as Semaphore from "effect/Semaphore"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import * as GitWorkflow from "../git/GitWorkflowService.ts"; +import * as ProjectService from "../project/ProjectService.ts"; +import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts"; +import * as TextGeneration from "../textGeneration/TextGeneration.ts"; +import * as IdAllocator from "./IdAllocator.ts"; +import * as ThreadManagement from "./ThreadManagementService.ts"; + +export type ThreadLaunchWorkspaceStrategy = + | { readonly type: "root"; readonly branch?: string | undefined } + | { + readonly type: "existing_worktree"; + readonly worktreePath: string; + readonly branch?: string | undefined; + } + | { + readonly type: "worktree"; + readonly baseRef: string; + readonly branch?: string | undefined; + readonly startFromOrigin?: boolean | undefined; + }; + +export interface ThreadLaunchInitialMessage { + readonly messageId?: MessageId; + readonly text: string; + readonly attachments: ReadonlyArray; +} + +export interface ThreadLaunchInput { + readonly commandId: CommandId; + readonly threadId?: ThreadId; + readonly reuseExistingThread?: boolean; + readonly projectId: ProjectId; + readonly title: string; + readonly modelSelection: ModelSelection; + readonly runtimeMode: RuntimeMode; + readonly interactionMode: ProviderInteractionMode; + readonly workspaceStrategy: ThreadLaunchWorkspaceStrategy; + readonly initialMessage?: ThreadLaunchInitialMessage; + readonly createdBy: OrchestrationV2Actor; + readonly creationSource: OrchestrationV2CreationSource; +} + +export interface ThreadLaunchResult { + readonly threadId: ThreadId; + readonly projection: OrchestrationV2ThreadProjection; + readonly resumed: boolean; +} + +export class ThreadLaunchError extends Schema.TaggedErrorClass()( + "ThreadLaunchError", + { + operation: Schema.Literals([ + "resolve-project", + "load-workflow", + "persist-workflow", + "generate-metadata", + "provision-worktree", + "run-setup-script", + "create-thread", + "update-thread", + "dispatch-message", + "compensate-worktree", + ]), + commandId: CommandId, + projectId: ProjectId, + threadId: Schema.optional(ThreadId), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Thread launch ${this.commandId} failed during ${this.operation}.`; + } +} + +type WorkflowRow = { + readonly command_id: string; + readonly thread_id: string; + readonly project_id: string; + readonly status: string; + readonly title: string; + readonly worktree_path: string | null; + readonly branch: string | null; + readonly setup_committed: number; + readonly thread_committed: number; + readonly message_committed: number; +}; + +export class ThreadLaunchService extends Context.Service< + ThreadLaunchService, + { + readonly launch: ( + input: ThreadLaunchInput, + ) => Effect.Effect; + } +>()("t3/orchestration-v2/ThreadLaunchService") {} + +export const make = Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const projects = yield* ProjectService.ProjectService; + const git = yield* GitWorkflow.GitWorkflowService; + const setupScripts = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner; + const textGeneration = yield* TextGeneration.TextGeneration; + const ids = yield* IdAllocator.IdAllocatorV2; + const threads = yield* ThreadManagement.ThreadManagementService; + const launchSemaphore = yield* Semaphore.make(1); + + const mapError = + (input: ThreadLaunchInput, operation: ThreadLaunchError["operation"], threadId?: ThreadId) => + (cause: unknown) => + new ThreadLaunchError({ + operation, + commandId: input.commandId, + projectId: input.projectId, + ...(threadId === undefined ? {} : { threadId }), + cause, + }); + + const loadWorkflow = Effect.fn("ThreadLaunchService.loadWorkflow")(function* ( + input: ThreadLaunchInput, + ) { + const rows = yield* sql` + SELECT * FROM orchestration_v2_thread_launch_workflows + WHERE command_id = ${input.commandId} + `.pipe(Effect.mapError(mapError(input, "load-workflow"))); + return rows[0] ?? null; + }); + + const saveWorkflow = Effect.fn("ThreadLaunchService.saveWorkflow")(function* ( + input: ThreadLaunchInput, + row: { + readonly threadId: ThreadId; + readonly status: string; + readonly title: string; + readonly worktreePath: string | null; + readonly branch: string | null; + readonly setupCommitted: boolean; + readonly threadCommitted: boolean; + readonly messageCommitted: boolean; + readonly lastError?: string | null; + }, + ) { + const now = DateTime.formatIso(yield* DateTime.now); + yield* sql` + INSERT INTO orchestration_v2_thread_launch_workflows ( + command_id, thread_id, project_id, status, title, worktree_path, branch, setup_committed, + thread_committed, message_committed, last_error, created_at, updated_at + ) VALUES ( + ${input.commandId}, ${row.threadId}, ${input.projectId}, ${row.status}, ${row.title}, + ${row.worktreePath}, ${row.branch}, ${row.setupCommitted ? 1 : 0}, ${row.threadCommitted ? 1 : 0}, + ${row.messageCommitted ? 1 : 0}, ${row.lastError ?? null}, ${now}, ${now} + ) ON CONFLICT(command_id) DO UPDATE SET + status = excluded.status, + title = excluded.title, + worktree_path = excluded.worktree_path, + branch = excluded.branch, + setup_committed = excluded.setup_committed, + thread_committed = excluded.thread_committed, + message_committed = excluded.message_committed, + last_error = excluded.last_error, + updated_at = excluded.updated_at + `.pipe(Effect.mapError(mapError(input, "persist-workflow", row.threadId))); + }); + + const launchWorkflow: ThreadLaunchService["Service"]["launch"] = Effect.fn( + "ThreadLaunchService.launch", + )(function* (input) { + const project = yield* projects.getById(input.projectId).pipe( + Effect.mapError(mapError(input, "resolve-project")), + Effect.flatMap( + Option.match({ + onNone: () => Effect.fail(mapError(input, "resolve-project")("Project not found.")), + onSome: Effect.succeed, + }), + ), + ); + const stored = yield* loadWorkflow(input); + if (input.reuseExistingThread === true && input.threadId === undefined) { + return yield* mapError( + input, + "update-thread", + )("Reusing an existing thread requires a thread id."); + } + const threadId = + stored === null + ? (input.threadId ?? + (yield* ids.allocate + .thread({ projectId: input.projectId }) + .pipe(Effect.mapError(mapError(input, "persist-workflow"))))) + : ThreadId.make(stored.thread_id); + if (stored?.status === "completed") { + return { + threadId, + projection: yield* threads + .getThreadProjection(threadId) + .pipe(Effect.mapError(mapError(input, "create-thread", threadId))), + resumed: true, + }; + } + + const validateReusableThread = Effect.fn("ThreadLaunchService.validateReusableThread")( + function* () { + const projection = yield* threads + .getThreadProjection(threadId) + .pipe(Effect.mapError(mapError(input, "update-thread", threadId))); + if ( + projection.thread.projectId !== input.projectId || + projection.thread.archivedAt !== null || + projection.thread.deletedAt !== null || + projection.messages.length > 0 || + projection.runs.length > 0 + ) { + return yield* mapError( + input, + "update-thread", + threadId, + )( + "Only an empty active thread in the target project can change workspace during launch.", + ); + } + return projection; + }, + ); + if (input.reuseExistingThread === true && stored?.thread_committed !== 1) { + yield* validateReusableThread(); + } + + let branch = + stored?.branch ?? + (input.workspaceStrategy.type === "worktree" + ? null + : (input.workspaceStrategy.branch ?? null)); + let title = stored?.title ?? input.title; + if (input.initialMessage !== undefined) { + if (title === "New thread") { + title = yield* textGeneration + .generateThreadTitle({ + cwd: project.workspaceRoot, + message: input.initialMessage.text, + attachments: input.initialMessage.attachments, + modelSelection: input.modelSelection, + }) + .pipe( + Effect.map((result) => result.title), + Effect.mapError(mapError(input, "generate-metadata", threadId)), + ); + } + if (input.workspaceStrategy.type === "worktree" && branch === null) { + branch = yield* textGeneration + .generateBranchName({ + cwd: project.workspaceRoot, + message: input.initialMessage.text, + attachments: input.initialMessage.attachments, + modelSelection: input.modelSelection, + }) + .pipe( + Effect.map((result) => result.branch), + Effect.mapError(mapError(input, "generate-metadata", threadId)), + ); + } + } + if (input.workspaceStrategy.type === "worktree") { + branch = branch ?? input.workspaceStrategy.branch ?? `thread-${threadId}`; + } + + let worktreePath = + stored?.worktree_path ?? + (input.workspaceStrategy.type === "existing_worktree" + ? input.workspaceStrategy.worktreePath + : null); + const ownsProvisionedWorktree = input.workspaceStrategy.type === "worktree"; + const threadCommitted = stored?.thread_committed === 1; + let setupCommitted = stored?.setup_committed === 1; + let messageCommitted = stored?.message_committed === 1; + if (input.workspaceStrategy.type === "worktree" && worktreePath === null) { + let startRef = input.workspaceStrategy.baseRef; + if (input.workspaceStrategy.startFromOrigin === true) { + yield* git + .fetchRemote({ cwd: project.workspaceRoot, remoteName: "origin" }) + .pipe(Effect.mapError(mapError(input, "provision-worktree", threadId))); + startRef = yield* git + .resolveRemoteTrackingCommit({ + cwd: project.workspaceRoot, + refName: input.workspaceStrategy.baseRef, + fallbackRemoteName: "origin", + }) + .pipe( + Effect.map((resolved) => resolved.commitSha), + Effect.mapError(mapError(input, "provision-worktree", threadId)), + ); + } + const worktree = yield* git + .createWorktree({ + cwd: project.workspaceRoot, + refName: startRef, + newRefName: branch!, + baseRefName: input.workspaceStrategy.baseRef, + path: null, + }) + .pipe(Effect.mapError(mapError(input, "provision-worktree", threadId))); + worktreePath = worktree.worktree.path; + branch = worktree.worktree.refName; + yield* saveWorkflow(input, { + threadId, + status: "workspace_ready", + title, + worktreePath, + branch, + setupCommitted, + threadCommitted, + messageCommitted, + }); + } + const cwd = worktreePath ?? project.workspaceRoot; + + if (!setupCommitted) { + const setupExit = yield* Effect.exit( + setupScripts.runForThread({ + threadId, + projectId: input.projectId, + projectCwd: project.workspaceRoot, + worktreePath: cwd, + project: { + workspaceRoot: project.workspaceRoot, + scripts: project.scripts, + }, + }), + ); + if (setupExit._tag === "Failure") { + if (ownsProvisionedWorktree && worktreePath !== null && !threadCommitted) { + yield* git + .removeWorktree({ cwd: project.workspaceRoot, path: worktreePath }) + .pipe(Effect.mapError(mapError(input, "compensate-worktree", threadId))); + worktreePath = null; + } + yield* saveWorkflow(input, { + threadId, + status: "setup_failed", + title, + worktreePath, + branch, + setupCommitted: false, + threadCommitted, + messageCommitted, + lastError: "Project setup script failed.", + }); + return yield* Effect.failCause(setupExit.cause).pipe( + Effect.mapError(mapError(input, "run-setup-script", threadId)), + ); + } + setupCommitted = true; + yield* saveWorkflow(input, { + threadId, + status: "setup_ready", + title, + worktreePath, + branch, + setupCommitted, + threadCommitted, + messageCommitted, + }); + } + + if (!threadCommitted) { + const commitThread = + input.reuseExistingThread === true + ? Effect.gen(function* () { + yield* validateReusableThread(); + return yield* threads.dispatch({ + type: "thread.metadata.update", + commandId: input.commandId, + threadId, + title, + branch, + worktreePath, + }); + }) + : threads.dispatch({ + type: "thread.create", + commandId: input.commandId, + threadId, + projectId: input.projectId, + title, + modelSelection: input.modelSelection, + runtimeMode: input.runtimeMode, + interactionMode: input.interactionMode, + branch, + worktreePath, + createdBy: input.createdBy, + creationSource: input.creationSource, + }); + const createExit = yield* Effect.exit(commitThread); + if (createExit._tag === "Failure") { + if (ownsProvisionedWorktree && worktreePath !== null) { + yield* git + .removeWorktree({ cwd: project.workspaceRoot, path: worktreePath }) + .pipe(Effect.mapError(mapError(input, "compensate-worktree", threadId))); + worktreePath = null; + setupCommitted = false; + } + yield* saveWorkflow(input, { + threadId, + status: + input.reuseExistingThread === true ? "thread_update_failed" : "thread_create_failed", + title, + worktreePath, + branch, + setupCommitted, + threadCommitted: false, + messageCommitted, + lastError: + input.reuseExistingThread === true + ? "Thread workspace update failed." + : "Thread creation failed.", + }); + return yield* Effect.failCause(createExit.cause).pipe( + Effect.mapError( + mapError( + input, + input.reuseExistingThread === true ? "update-thread" : "create-thread", + threadId, + ), + ), + ); + } + yield* saveWorkflow(input, { + threadId, + status: "thread_created", + title, + worktreePath, + branch, + setupCommitted, + threadCommitted: true, + messageCommitted, + }); + } + + if (input.initialMessage !== undefined && !messageCommitted) { + const messageId = + input.initialMessage.messageId ?? + (yield* ids.allocate + .message({ threadId, ordinal: 1 }) + .pipe(Effect.mapError(mapError(input, "dispatch-message", threadId)))); + yield* threads + .sendToThread({ + projectId: input.projectId, + commandId: CommandId.make(`${input.commandId}:initial-message`), + threadId, + messageId, + text: input.initialMessage.text, + attachments: input.initialMessage.attachments, + mode: "auto", + createdBy: input.createdBy, + creationSource: input.creationSource, + }) + .pipe(Effect.mapError(mapError(input, "dispatch-message", threadId))); + messageCommitted = true; + } + yield* saveWorkflow(input, { + threadId, + status: "completed", + title, + worktreePath, + branch, + setupCommitted, + threadCommitted: true, + messageCommitted, + }); + return { + threadId, + projection: yield* threads + .getThreadProjection(threadId) + .pipe(Effect.mapError(mapError(input, "create-thread", threadId))), + resumed: stored !== null, + }; + }); + + return ThreadLaunchService.of({ + launch: (input) => launchSemaphore.withPermit(launchWorkflow(input)), + }); +}); + +export const layer = Layer.effect(ThreadLaunchService, make); diff --git a/apps/server/src/orchestration-v2/ThreadLifecycleService.test.ts b/apps/server/src/orchestration-v2/ThreadLifecycleService.test.ts new file mode 100644 index 00000000000..c55a8c9cceb --- /dev/null +++ b/apps/server/src/orchestration-v2/ThreadLifecycleService.test.ts @@ -0,0 +1,60 @@ +import { assert, it } from "@effect/vitest"; +import { + CommandId, + ProviderInstanceId, + ThreadId, + type OrchestrationV2ThreadProjection, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import * as ThreadLifecycle from "./ThreadLifecycleService.ts"; +import * as ThreadManagement from "./ThreadManagementService.ts"; + +it.effect("maps application lifecycle operations to V2-native commands", () => { + const threadId = ThreadId.make("thread_lifecycle_service"); + const commands: Array = []; + const projection = { thread: { id: threadId } } as OrchestrationV2ThreadProjection; + const layer = ThreadLifecycle.layer.pipe( + Layer.provide( + Layer.mock(ThreadManagement.ThreadManagementService)({ + dispatch: (command) => { + commands.push(command.type); + return Effect.succeed({ sequence: commands.length, storedEvents: [] }); + }, + getThreadProjection: () => Effect.succeed(projection), + }), + ), + ); + return Effect.gen(function* () { + const service = yield* ThreadLifecycle.ThreadLifecycleService; + yield* service.archive({ commandId: CommandId.make("archive"), threadId }); + yield* service.unarchive({ commandId: CommandId.make("unarchive"), threadId }); + yield* service.updateMetadata({ commandId: CommandId.make("metadata"), threadId, title: "T" }); + yield* service.setRuntimeMode({ + commandId: CommandId.make("runtime"), + threadId, + runtimeMode: "approval-required", + }); + yield* service.setInteractionMode({ + commandId: CommandId.make("interaction"), + threadId, + interactionMode: "plan", + }); + yield* service.setModelSelection({ + commandId: CommandId.make("model"), + threadId, + modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.2" }, + }); + yield* service.delete({ commandId: CommandId.make("delete"), threadId }); + assert.deepEqual(commands, [ + "thread.archive", + "thread.unarchive", + "thread.metadata.update", + "thread.runtime-mode.set", + "thread.interaction-mode.set", + "thread.model-selection.set", + "thread.delete", + ]); + }).pipe(Effect.provide(layer)); +}); diff --git a/apps/server/src/orchestration-v2/ThreadLifecycleService.ts b/apps/server/src/orchestration-v2/ThreadLifecycleService.ts new file mode 100644 index 00000000000..6fbb5661355 --- /dev/null +++ b/apps/server/src/orchestration-v2/ThreadLifecycleService.ts @@ -0,0 +1,142 @@ +import { + CommandId, + type ModelSelection, + type OrchestrationV2ThreadProjection, + type ProviderInteractionMode, + type RuntimeMode, + ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +import * as ThreadManagement from "./ThreadManagementService.ts"; + +export class ThreadLifecycleError extends Schema.TaggedErrorClass()( + "ThreadLifecycleError", + { + operation: Schema.Literals([ + "archive", + "unarchive", + "delete", + "update-metadata", + "set-runtime-mode", + "set-interaction-mode", + "set-model-selection", + ]), + threadId: ThreadId, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Thread lifecycle operation '${this.operation}' failed for ${this.threadId}.`; + } +} + +export class ThreadLifecycleService extends Context.Service< + ThreadLifecycleService, + { + readonly archive: (input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + }) => Effect.Effect; + readonly unarchive: (input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + }) => Effect.Effect; + readonly delete: (input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + }) => Effect.Effect; + readonly updateMetadata: (input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly title?: string; + readonly branch?: string | null; + readonly worktreePath?: string | null; + }) => Effect.Effect; + readonly setRuntimeMode: (input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly runtimeMode: RuntimeMode; + }) => Effect.Effect; + readonly setInteractionMode: (input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly interactionMode: ProviderInteractionMode; + }) => Effect.Effect; + readonly setModelSelection: (input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly modelSelection: ModelSelection; + }) => Effect.Effect; + } +>()("t3/orchestration-v2/ThreadLifecycleService") {} + +export const make = Effect.gen(function* () { + const threads = yield* ThreadManagement.ThreadManagementService; + + const dispatch = ( + operation: Operation, + threadId: ThreadId, + command: Parameters[0], + ) => + threads.dispatch(command).pipe( + Effect.andThen(threads.getThreadProjection(threadId)), + Effect.mapError((cause) => new ThreadLifecycleError({ operation, threadId, cause })), + ); + + return ThreadLifecycleService.of({ + archive: (input) => + dispatch("archive", input.threadId, { + type: "thread.archive", + commandId: input.commandId, + threadId: input.threadId, + }), + unarchive: (input) => + dispatch("unarchive", input.threadId, { + type: "thread.unarchive", + commandId: input.commandId, + threadId: input.threadId, + }), + delete: (input) => + dispatch("delete", input.threadId, { + type: "thread.delete", + commandId: input.commandId, + threadId: input.threadId, + }), + updateMetadata: (input) => + dispatch("update-metadata", input.threadId, { + type: "thread.metadata.update", + commandId: input.commandId, + threadId: input.threadId, + ...(input.title === undefined ? {} : { title: input.title }), + ...(input.branch === undefined ? {} : { branch: input.branch }), + ...(input.worktreePath === undefined ? {} : { worktreePath: input.worktreePath }), + }), + setRuntimeMode: (input) => + dispatch("set-runtime-mode", input.threadId, { + type: "thread.runtime-mode.set", + commandId: input.commandId, + threadId: input.threadId, + runtimeMode: input.runtimeMode, + }), + setInteractionMode: (input) => + dispatch("set-interaction-mode", input.threadId, { + type: "thread.interaction-mode.set", + commandId: input.commandId, + threadId: input.threadId, + interactionMode: input.interactionMode, + }), + setModelSelection: (input) => + dispatch("set-model-selection", input.threadId, { + type: "thread.model-selection.set", + commandId: input.commandId, + threadId: input.threadId, + modelSelection: input.modelSelection, + }), + }); +}); + +export const layer = Layer.effect(ThreadLifecycleService, make); diff --git a/apps/server/src/orchestration-v2/ThreadManagementService.test.ts b/apps/server/src/orchestration-v2/ThreadManagementService.test.ts new file mode 100644 index 00000000000..9965044ec8e --- /dev/null +++ b/apps/server/src/orchestration-v2/ThreadManagementService.test.ts @@ -0,0 +1,57 @@ +import { expect, it } from "@effect/vitest"; +import { + CommandId, + type OrchestrationV2Command, + ProjectId, + ProviderInstanceId, + RunId, + ThreadId, +} from "@t3tools/contracts"; + +import { withCreationProvenance } from "./ThreadManagementService.ts"; + +it("stamps authoritative provenance on commands that create threads or messages", () => { + const command: OrchestrationV2Command = { + type: "thread.create", + createdBy: "agent", + creationSource: "mcp", + commandId: CommandId.make("command:thread-management:create"), + threadId: ThreadId.make("thread:thread-management:create"), + projectId: ProjectId.make("project:thread-management"), + title: "Thread management", + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5-codex", + }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }; + + expect( + withCreationProvenance(command, { + createdBy: "user", + creationSource: "web", + }), + ).toMatchObject({ + createdBy: "user", + creationSource: "web", + }); +}); + +it("leaves commands that do not create durable authored content unchanged", () => { + const command: OrchestrationV2Command = { + type: "run.interrupt", + commandId: CommandId.make("command:thread-management:interrupt"), + threadId: ThreadId.make("thread:thread-management:interrupt"), + runId: RunId.make("run:thread-management:interrupt"), + }; + + expect( + withCreationProvenance(command, { + createdBy: "user", + creationSource: "web", + }), + ).toBe(command); +}); diff --git a/apps/server/src/orchestration-v2/ThreadManagementService.ts b/apps/server/src/orchestration-v2/ThreadManagementService.ts new file mode 100644 index 00000000000..821e324c5ec --- /dev/null +++ b/apps/server/src/orchestration-v2/ThreadManagementService.ts @@ -0,0 +1,467 @@ +import { + type ChatAttachment, + type CommandId, + type MessageId, + type ModelSelection, + type OrchestrationV2Actor, + type OrchestrationV2Command, + type OrchestrationV2ConversationMessage, + type OrchestrationV2CreationSource, + type OrchestrationV2Run, + type OrchestrationV2ThreadShellSnapshot, + type OrchestrationV2ThreadProjection, + type OrchestrationV2ThreadShell, + type OrchestrationV2TurnItem, + type ProjectId, + type RunId, + type ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +import { + OrchestratorV2, + type OrchestratorV2DispatchResult, + type OrchestratorV2Error, +} from "./Orchestrator.ts"; + +export type ThreadManagementSendMode = "auto" | "queue" | "steer" | "restart"; + +export interface ThreadManagementProvenance { + readonly createdBy: OrchestrationV2Actor; + readonly creationSource: OrchestrationV2CreationSource; +} + +export function withCreationProvenance( + command: OrchestrationV2Command, + provenance: ThreadManagementProvenance, +): OrchestrationV2Command { + switch (command.type) { + case "thread.create": + case "message.dispatch": + case "thread.fork": + case "thread.merge_back": + case "delegated_task.request": + return { ...command, ...provenance }; + default: + return command; + } +} + +export type ThreadManagementTerminalRunStatus = Extract< + OrchestrationV2Run["status"], + "completed" | "failed" | "cancelled" | "interrupted" | "rolled_back" +>; + +export interface ThreadManagementSendInput { + readonly projectId: ProjectId; + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly messageId: MessageId; + readonly text: string; + readonly attachments: ReadonlyArray; + readonly modelSelection?: ModelSelection; + readonly mode: ThreadManagementSendMode; + readonly createdBy: OrchestrationV2Actor; + readonly creationSource: OrchestrationV2CreationSource; +} + +export interface ThreadManagementSendResult { + readonly dispatch: OrchestratorV2DispatchResult; + readonly projection: OrchestrationV2ThreadProjection; + readonly message: OrchestrationV2ConversationMessage; + readonly run: OrchestrationV2Run; + readonly turnItem: Extract; + readonly delivery: "started" | "queued" | "steered" | "restarted"; +} + +export interface ThreadManagementWaitInput { + readonly projectId: ProjectId; + readonly threadId: ThreadId; + readonly runId?: RunId; + readonly timeoutMs: number; + readonly pollIntervalMs?: number; +} + +export interface ThreadManagementWaitResult { + readonly threadId: ThreadId; + readonly run: OrchestrationV2Run | null; + readonly timedOut: boolean; +} + +export interface ThreadManagementInterruptInput { + readonly projectId: ProjectId; + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly runId?: RunId; + readonly reason?: string; +} + +export type ThreadManagementInterruptResult = + | { + readonly type: "interrupt_requested"; + readonly run: OrchestrationV2Run; + readonly dispatch: OrchestratorV2DispatchResult; + } + | { readonly type: "no_active_run" } + | { + readonly type: "already_terminal"; + readonly run: OrchestrationV2Run & { readonly status: ThreadManagementTerminalRunStatus }; + }; + +export class ThreadManagementError extends Schema.TaggedErrorClass()( + "ThreadManagementError", + { + code: Schema.Literals([ + "thread_not_found", + "run_not_found", + "thread_not_sendable", + "thread_not_interruptible", + "orchestration_error", + ]), + message: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +type ThreadManagementFailure = ThreadManagementError | OrchestratorV2Error; + +export interface ThreadManagementServiceShape { + readonly dispatch: ( + command: OrchestrationV2Command, + ) => Effect.Effect; + readonly getThreadProjection: ( + threadId: ThreadId, + ) => Effect.Effect; + readonly getThreadSnapshot: OrchestratorV2["Service"]["getThreadSnapshot"]; + readonly getProjectThread: (input: { + readonly projectId: ProjectId; + readonly threadId: ThreadId; + }) => Effect.Effect; + readonly getShellSnapshot: () => Effect.Effect< + OrchestrationV2ThreadShellSnapshot, + OrchestratorV2Error + >; + readonly listProjectThreads: (input: { + readonly projectId: ProjectId; + readonly includeSubagents: boolean; + }) => Effect.Effect, ThreadManagementError>; + readonly sendToThread: ( + input: ThreadManagementSendInput, + ) => Effect.Effect; + readonly waitForThread: ( + input: ThreadManagementWaitInput, + ) => Effect.Effect; + readonly interruptThread: ( + input: ThreadManagementInterruptInput, + ) => Effect.Effect; + readonly getThreadEventSequence: OrchestratorV2["Service"]["getThreadEventSequence"]; + readonly streamStoredEvents: OrchestratorV2["Service"]["streamStoredEvents"]; + readonly streamStoredEventsFrom: OrchestratorV2["Service"]["streamStoredEventsFrom"]; + readonly streamDomainEvents: OrchestratorV2["Service"]["streamDomainEvents"]; +} + +export class ThreadManagementService extends Context.Service< + ThreadManagementService, + ThreadManagementServiceShape +>()("t3/orchestration-v2/ThreadManagementService") {} + +export function isActiveRun(run: OrchestrationV2Run): boolean { + return run.status === "starting" || run.status === "running" || run.status === "waiting"; +} + +export function isTerminalRunStatus( + status: OrchestrationV2Run["status"], +): status is ThreadManagementTerminalRunStatus { + return ( + status === "completed" || + status === "failed" || + status === "cancelled" || + status === "interrupted" || + status === "rolled_back" + ); +} + +export function latestRun( + projection: OrchestrationV2ThreadProjection, +): OrchestrationV2Run | undefined { + return projection.runs.toSorted((left, right) => right.ordinal - left.ordinal)[0]; +} + +export function latestActiveRun( + projection: OrchestrationV2ThreadProjection, +): OrchestrationV2Run | undefined { + return projection.runs + .filter(isActiveRun) + .toSorted((left, right) => right.ordinal - left.ordinal)[0]; +} + +export function latestSteerableRun( + projection: OrchestrationV2ThreadProjection, +): OrchestrationV2Run | undefined { + return projection.runs + .filter( + (run) => + run.status === "running" && + run.activeAttemptId !== null && + projection.providerTurns.some( + (turn) => turn.runAttemptId === run.activeAttemptId && turn.status === "running", + ), + ) + .toSorted((left, right) => right.ordinal - left.ordinal)[0]; +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function managementError( + code: ThreadManagementError["code"], + message: string, + cause?: unknown, +): ThreadManagementError { + return new ThreadManagementError({ code, message, ...(cause === undefined ? {} : { cause }) }); +} + +const make = Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + + const getProjectThread: ThreadManagementServiceShape["getProjectThread"] = (input) => + orchestrator.getThreadProjection(input.threadId).pipe( + Effect.mapError((cause) => + managementError( + "thread_not_found", + `Thread ${input.threadId} was not found in project ${input.projectId}.`, + cause, + ), + ), + Effect.flatMap((projection) => + projection.thread.projectId === input.projectId && projection.thread.deletedAt === null + ? Effect.succeed(projection) + : Effect.fail( + managementError( + "thread_not_found", + `Thread ${input.threadId} was not found in project ${input.projectId}.`, + ), + ), + ), + ); + + const listProjectThreads: ThreadManagementServiceShape["listProjectThreads"] = (input) => + orchestrator.getShellSnapshot().pipe( + Effect.mapError((cause) => + managementError( + "orchestration_error", + `Unable to list threads in project ${input.projectId}: ${errorMessage(cause)}`, + cause, + ), + ), + Effect.map((snapshot) => + snapshot.threads + .filter((thread) => thread.projectId === input.projectId) + .filter( + (thread) => + input.includeSubagents || thread.lineage.relationshipToParent !== "subagent", + ) + .toSorted( + (left, right) => + DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt) || + right.id.localeCompare(left.id), + ), + ), + ); + + const sendToThread: ThreadManagementServiceShape["sendToThread"] = (input) => + Effect.gen(function* () { + const target = yield* getProjectThread(input); + if (target.thread.archivedAt !== null) { + return yield* managementError( + "thread_not_sendable", + `Thread ${input.threadId} is archived and cannot receive messages.`, + ); + } + + const steerableRun = latestSteerableRun(target); + let dispatchMode: Extract< + OrchestrationV2Command, + { readonly type: "message.dispatch" } + >["dispatchMode"]; + if (input.mode === "steer" || input.mode === "restart") { + if (steerableRun === undefined) { + return yield* managementError( + "thread_not_sendable", + `Thread ${input.threadId} has no running turn that can be ${input.mode === "steer" ? "steered" : "restarted"}.`, + ); + } + dispatchMode = { + type: input.mode === "steer" ? "steer_active" : "restart_active", + targetRunId: steerableRun.id, + }; + } else if (input.mode === "auto" && steerableRun !== undefined) { + dispatchMode = { type: "steer_active", targetRunId: steerableRun.id }; + } else { + dispatchMode = { + type: input.mode === "queue" ? "queue_after_active" : "start_immediately", + }; + } + + const dispatch = yield* orchestrator.dispatch({ + type: "message.dispatch", + commandId: input.commandId, + threadId: input.threadId, + messageId: input.messageId, + text: input.text, + attachments: input.attachments, + ...(input.modelSelection === undefined ? {} : { modelSelection: input.modelSelection }), + dispatchMode, + createdBy: input.createdBy, + creationSource: input.creationSource, + }); + const projection = yield* getProjectThread(input); + const message = projection.messages.find((candidate) => candidate.id === input.messageId); + const run = + message?.runId === null || message?.runId === undefined + ? undefined + : projection.runs.find((candidate) => candidate.id === message.runId); + const turnItem = projection.turnItems.find( + ( + candidate, + ): candidate is Extract => + candidate.type === "user_message" && candidate.messageId === input.messageId, + ); + if (message === undefined || run === undefined || turnItem === undefined) { + return yield* managementError( + "orchestration_error", + `Message ${input.messageId} was accepted without a durable run projection.`, + ); + } + const delivery: ThreadManagementSendResult["delivery"] = + turnItem.inputIntent === "turn_start" + ? "started" + : turnItem.inputIntent === "queued_turn" + ? "queued" + : input.mode === "restart" + ? "restarted" + : "steered"; + return { dispatch, projection, message, run, turnItem, delivery }; + }); + + const waitForThread: ThreadManagementServiceShape["waitForThread"] = (input) => + Effect.gen(function* () { + const target = yield* getProjectThread(input); + const selectedRun = + input.runId === undefined + ? latestRun(target) + : target.runs.find((candidate) => candidate.id === input.runId); + if (input.runId !== undefined && selectedRun === undefined) { + return yield* managementError( + "run_not_found", + `Run ${input.runId} does not belong to thread ${input.threadId}.`, + ); + } + if (selectedRun === undefined) { + return { threadId: input.threadId, run: null, timedOut: false }; + } + if (isTerminalRunStatus(selectedRun.status)) { + return { threadId: input.threadId, run: selectedRun, timedOut: false }; + } + + const wait = Effect.gen(function* () { + while (true) { + const current = yield* getProjectThread(input); + const run = current.runs.find((candidate) => candidate.id === selectedRun.id); + if (run === undefined) { + return yield* managementError( + "run_not_found", + `Run ${selectedRun.id} no longer belongs to thread ${input.threadId}.`, + ); + } + if (isTerminalRunStatus(run.status)) return run; + yield* Effect.sleep(Duration.millis(Math.max(1, input.pollIntervalMs ?? 250))); + } + }).pipe(Effect.timeoutOption(Duration.millis(Math.max(1, input.timeoutMs)))); + const waited = yield* wait; + if (Option.isSome(waited)) { + return { threadId: input.threadId, run: waited.value, timedOut: false }; + } + const current = yield* getProjectThread(input); + const run = current.runs.find((candidate) => candidate.id === selectedRun.id); + if (run === undefined) { + return yield* managementError( + "run_not_found", + `Run ${selectedRun.id} no longer belongs to thread ${input.threadId}.`, + ); + } + return { threadId: input.threadId, run, timedOut: true }; + }); + + const interruptThread: ThreadManagementServiceShape["interruptThread"] = (input) => + Effect.gen(function* () { + const target = yield* getProjectThread(input); + const explicitRun = + input.runId === undefined + ? undefined + : target.runs.find((candidate) => candidate.id === input.runId); + if (input.runId !== undefined && explicitRun === undefined) { + return yield* managementError( + "run_not_found", + `Run ${input.runId} does not belong to thread ${input.threadId}.`, + ); + } + if (explicitRun !== undefined && isTerminalRunStatus(explicitRun.status)) { + return { + type: "already_terminal", + run: explicitRun as OrchestrationV2Run & { + readonly status: ThreadManagementTerminalRunStatus; + }, + } as const; + } + const interruptibleRun = latestSteerableRun(target); + if (input.runId === undefined && interruptibleRun === undefined) { + return { type: "no_active_run" } as const; + } + if ( + interruptibleRun === undefined || + (explicitRun !== undefined && interruptibleRun.id !== explicitRun.id) + ) { + return yield* managementError( + "thread_not_interruptible", + `Run ${explicitRun?.id ?? input.runId} is not currently interruptible.`, + ); + } + const dispatch = yield* orchestrator.dispatch({ + type: "run.interrupt", + commandId: input.commandId, + threadId: input.threadId, + runId: interruptibleRun.id, + ...(input.reason === undefined ? {} : { reason: input.reason }), + }); + return { type: "interrupt_requested", run: interruptibleRun, dispatch } as const; + }); + + return ThreadManagementService.of({ + dispatch: orchestrator.dispatch, + getThreadProjection: orchestrator.getThreadProjection, + getThreadSnapshot: orchestrator.getThreadSnapshot, + getProjectThread, + getShellSnapshot: orchestrator.getShellSnapshot, + listProjectThreads, + sendToThread, + waitForThread, + interruptThread, + getThreadEventSequence: orchestrator.getThreadEventSequence, + streamStoredEvents: orchestrator.streamStoredEvents, + streamStoredEventsFrom: orchestrator.streamStoredEventsFrom, + streamDomainEvents: orchestrator.streamDomainEvents, + }); +}); + +export const layer: Layer.Layer = Layer.effect( + ThreadManagementService, + make, +); diff --git a/apps/server/src/orchestration-v2/TurnItemPositionStore.ts b/apps/server/src/orchestration-v2/TurnItemPositionStore.ts new file mode 100644 index 00000000000..6f8993ae25e --- /dev/null +++ b/apps/server/src/orchestration-v2/TurnItemPositionStore.ts @@ -0,0 +1,105 @@ +import { OrchestrationV2TurnItem, RunId, ThreadId, TurnItemId } from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export class TurnItemPositionStoreError extends Schema.TaggedErrorClass()( + "TurnItemPositionStoreError", + { + threadId: ThreadId, + turnItemId: TurnItemId, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export interface TurnItemPositionStoreV2Shape { + readonly allocate: (input: { + readonly threadId: ThreadId; + readonly turnItemId: TurnItemId; + readonly runId: RunId | null; + readonly runOrdinal?: number; + }) => Effect.Effect; + readonly normalize: ( + item: OrchestrationV2TurnItem, + runOrdinal?: number, + ) => Effect.Effect; +} + +export class TurnItemPositionStoreV2 extends Context.Service< + TurnItemPositionStoreV2, + TurnItemPositionStoreV2Shape +>()("t3/orchestration-v2/TurnItemPositionStore/TurnItemPositionStoreV2") {} + +export const layer: Layer.Layer = Layer.effect( + TurnItemPositionStoreV2, + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + const allocate: TurnItemPositionStoreV2Shape["allocate"] = ({ + threadId, + turnItemId, + runId, + runOrdinal: suppliedRunOrdinal, + }) => + Effect.gen(function* () { + const runRows = + runId === null + ? [] + : yield* sql<{ readonly ordinal: number }>` + SELECT ordinal + FROM orchestration_v2_projection_runs + WHERE thread_id = ${threadId} AND run_id = ${runId} + LIMIT 1 + `; + const runOrdinal = suppliedRunOrdinal ?? runRows[0]?.ordinal ?? null; + const lowerBound = runOrdinal === null ? 0 : runOrdinal * 1_000_000; + const upperBound = runOrdinal === null ? 999_999 : lowerBound + 999_999; + yield* sql` + INSERT INTO orchestration_v2_turn_item_positions (thread_id, turn_item_id, ordinal) + SELECT + ${threadId}, + ${turnItemId}, + COALESCE(MAX(ordinal), ${lowerBound}) + 1 + FROM orchestration_v2_turn_item_positions + WHERE thread_id = ${threadId} + AND ordinal >= ${lowerBound} + AND ordinal <= ${upperBound} + ON CONFLICT(thread_id, turn_item_id) DO NOTHING + `; + const rows = yield* sql<{ readonly ordinal: number }>` + SELECT ordinal + FROM orchestration_v2_turn_item_positions + WHERE thread_id = ${threadId} AND turn_item_id = ${turnItemId} + LIMIT 1 + `; + const ordinal = rows[0]?.ordinal; + if (ordinal === undefined) { + return yield* new TurnItemPositionStoreError({ + threadId, + turnItemId, + cause: "Position allocation returned no row.", + }); + } + return ordinal; + }).pipe( + Effect.mapError((cause) => + Schema.is(TurnItemPositionStoreError)(cause) + ? cause + : new TurnItemPositionStoreError({ threadId, turnItemId, cause }), + ), + ); + + return TurnItemPositionStoreV2.of({ + allocate, + normalize: (item, runOrdinal) => + allocate({ + threadId: item.threadId, + turnItemId: item.id, + runId: item.runId, + ...(runOrdinal === undefined ? {} : { runOrdinal }), + }).pipe(Effect.map((ordinal) => (item.ordinal === ordinal ? item : { ...item, ordinal }))), + }); + }), +); diff --git a/apps/server/src/orchestration-v2/UserFacingErrors.test.ts b/apps/server/src/orchestration-v2/UserFacingErrors.test.ts new file mode 100644 index 00000000000..63e3a77a5b3 --- /dev/null +++ b/apps/server/src/orchestration-v2/UserFacingErrors.test.ts @@ -0,0 +1,35 @@ +import { assert, describe, it } from "@effect/vitest"; + +import { userFacingDispatchErrorMessage } from "./UserFacingErrors.ts"; + +describe("userFacingDispatchErrorMessage", () => { + it("returns the deepest actionable domain error instead of generic dispatch wrappers", () => { + const message = userFacingDispatchErrorMessage({ + message: "Failed to dispatch orchestration command checkpoint.rollback (command-1).", + cause: { + message: "Provider adapter failed while dispatching orchestration command command-1.", + cause: { + message: + "claudeAgent cannot satisfy rollback for command command-1: provider conversation rollback is unavailable", + }, + }, + }); + + assert.equal( + message, + "claudeAgent cannot satisfy rollback for command command-1: provider conversation rollback is unavailable", + ); + }); + + it("uses explicit detail fields as user-facing messages", () => { + assert.equal( + userFacingDispatchErrorMessage({ + message: "Failed to dispatch orchestration command message.dispatch (command-1).", + cause: { + detail: "Claude provider thread provider-thread-1 has no live query.", + }, + }), + "Claude provider thread provider-thread-1 has no live query.", + ); + }); +}); diff --git a/apps/server/src/orchestration-v2/UserFacingErrors.ts b/apps/server/src/orchestration-v2/UserFacingErrors.ts new file mode 100644 index 00000000000..3765312da39 --- /dev/null +++ b/apps/server/src/orchestration-v2/UserFacingErrors.ts @@ -0,0 +1,62 @@ +const GENERIC_ERROR_PREFIXES = [ + "Failed to dispatch orchestration V2 command", + "Failed to dispatch orchestration command ", + "Provider adapter failed while dispatching orchestration command ", +]; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function textValue(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; +} + +function messageFrom(value: unknown): string | undefined { + if (typeof value === "string") { + return textValue(value); + } + if (value instanceof Error) { + return textValue(value.message); + } + if (!isRecord(value)) { + return undefined; + } + return textValue(value.detail) ?? textValue(value.message); +} + +function isGenericErrorMessage(message: string): boolean { + return GENERIC_ERROR_PREFIXES.some((prefix) => message.startsWith(prefix)); +} + +function collectErrorMessages(value: unknown, seen: Set): ReadonlyArray { + if (value === undefined || value === null || seen.has(value)) { + return []; + } + seen.add(value); + + if (Array.isArray(value)) { + return value.flatMap((item) => collectErrorMessages(item, seen)); + } + + const message = messageFrom(value); + if (!isRecord(value)) { + return message === undefined ? [] : [message]; + } + + const nested = [ + ...collectErrorMessages(value.cause, seen), + ...collectErrorMessages(value.error, seen), + ...collectErrorMessages(value.errors, seen), + ]; + + return message === undefined ? nested : [message, ...nested]; +} + +export function userFacingDispatchErrorMessage(cause: unknown): string | undefined { + const messages = collectErrorMessages(cause, new Set()).filter( + (message, index, allMessages) => + !isGenericErrorMessage(message) && allMessages.indexOf(message) === index, + ); + return messages.at(-1); +} diff --git a/apps/server/src/orchestration-v2/V1ImportBoundary.test.ts b/apps/server/src/orchestration-v2/V1ImportBoundary.test.ts new file mode 100644 index 00000000000..9e7e45f00ba --- /dev/null +++ b/apps/server/src/orchestration-v2/V1ImportBoundary.test.ts @@ -0,0 +1,38 @@ +// @effect-diagnostics nodeBuiltinImport:off - Static architecture test scans source files. +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; + +import { assert, it } from "@effect/vitest"; + +const sourceRoot = NodePath.resolve(import.meta.dirname, ".."); +const forbiddenImport = + /from\s+["'][^"']*(?:ProviderService|ProviderSessionDirectory|ProviderSessionReaper|ProviderCommandReactor|ProviderRuntimeIngestion)[^"']*["']/; +const retiredAgentRuntimePaths = [ + "orchestration/Layers/ProviderCommandReactor.ts", + "orchestration/Layers/ProviderRuntimeIngestion.ts", + "orchestration/Services/ProviderCommandReactor.ts", + "orchestration/Services/ProviderRuntimeIngestion.ts", +] as const; + +function productionTypeScriptFiles(directory: string): ReadonlyArray { + return NodeFS.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const path = NodePath.join(directory, entry.name); + if (entry.isDirectory()) { + return entry.name === "legacy" ? [] : productionTypeScriptFiles(path); + } + return entry.isFile() && entry.name.endsWith(".ts") && !entry.name.includes(".test.") + ? [path] + : []; + }); +} + +it("retains the application data plane without restoring the V1 agent runtime", () => { + for (const relativePath of retiredAgentRuntimePaths) { + assert.isFalse(NodeFS.existsSync(NodePath.join(sourceRoot, relativePath))); + } + const violations = productionTypeScriptFiles(sourceRoot).flatMap((path) => { + const source = NodeFS.readFileSync(path, "utf8"); + return forbiddenImport.test(source) ? [NodePath.relative(sourceRoot, path)] : []; + }); + assert.deepEqual(violations, []); +}); diff --git a/apps/server/src/orchestration-v2/applicationLayer.ts b/apps/server/src/orchestration-v2/applicationLayer.ts new file mode 100644 index 00000000000..2a174a316f8 --- /dev/null +++ b/apps/server/src/orchestration-v2/applicationLayer.ts @@ -0,0 +1,23 @@ +import * as Layer from "effect/Layer"; + +import { ProjectionProjectRepositoryLive } from "../persistence/Layers/ProjectionProjects.ts"; +import { layer as projectServiceLayer } from "../project/ProjectService.ts"; +import { layer as threadLaunchServiceLayer } from "./ThreadLaunchService.ts"; +import { layer as threadLifecycleServiceLayer } from "./ThreadLifecycleService.ts"; +import { live as resourceCleanupLive } from "./ResourceCleanupService.ts"; +import { observerLive as runFinalizationObserverLive } from "./RunFinalizationService.ts"; + +const projectServiceProvided = projectServiceLayer.pipe( + Layer.provide(ProjectionProjectRepositoryLive), +); + +const applicationServices = Layer.mergeAll( + threadLaunchServiceLayer, + threadLifecycleServiceLayer, +).pipe(Layer.provideMerge(projectServiceProvided)); + +export const OrchestrationV2ApplicationLayer = Layer.mergeAll( + applicationServices, + resourceCleanupLive, + runFinalizationObserverLive, +); diff --git a/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts b/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts new file mode 100644 index 00000000000..1ef37a2bfa0 --- /dev/null +++ b/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts @@ -0,0 +1,47 @@ +import type { ProviderDriverKind } from "@t3tools/contracts"; + +import { + AcpRegistryAdapterV2Driver, + type AcpRegistryAdapterV2DriverEnv, +} from "./Adapters/AcpRegistryAdapterV2.ts"; +import { + ClaudeAdapterV2Driver, + type ClaudeAdapterV2DriverEnv, +} from "./Adapters/ClaudeAdapterV2.ts"; +import { CodexAdapterV2Driver, type CodexAdapterV2DriverEnv } from "./Adapters/CodexAdapterV2.ts"; +import { + CursorAdapterV2Driver, + type CursorAdapterV2DriverEnv, +} from "./Adapters/CursorAdapterV2.ts"; +import { GrokAdapterV2Driver, type GrokAdapterV2DriverEnv } from "./Adapters/GrokAdapterV2.ts"; +import { + OpenCodeAdapterV2Driver, + type OpenCodeAdapterV2DriverEnv, +} from "./Adapters/OpenCodeAdapterV2.ts"; +import type { AnyProviderAdapterDriver } from "./ProviderAdapterDriver.ts"; + +export type BuiltInProviderAdapterDriversV2Env = + | AcpRegistryAdapterV2DriverEnv + | ClaudeAdapterV2DriverEnv + | CodexAdapterV2DriverEnv + | CursorAdapterV2DriverEnv + | GrokAdapterV2DriverEnv + | OpenCodeAdapterV2DriverEnv; + +export const BUILT_IN_PROVIDER_ADAPTER_DRIVERS_V2: ReadonlyArray< + AnyProviderAdapterDriver +> = [ + CodexAdapterV2Driver, + ClaudeAdapterV2Driver, + CursorAdapterV2Driver, + OpenCodeAdapterV2Driver, + GrokAdapterV2Driver, + AcpRegistryAdapterV2Driver, +]; + +export const BUILT_IN_PROVIDER_ADAPTER_DRIVER_KINDS_V2: ReadonlySet = new Set( + BUILT_IN_PROVIDER_ADAPTER_DRIVERS_V2.map((driver) => driver.driverKind), +); + +export const isBuiltInProviderAdapterDriverV2 = (driver: ProviderDriverKind): boolean => + BUILT_IN_PROVIDER_ADAPTER_DRIVER_KINDS_V2.has(driver); diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts new file mode 100644 index 00000000000..73e915e4674 --- /dev/null +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -0,0 +1,357 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import { + type ApplicationStoredEvent, + CommandId, + type ModelSelection, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + ThreadId, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Queue from "effect/Queue"; +import * as Stream from "effect/Stream"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import * as CheckpointStore from "../checkpointing/CheckpointStore.ts"; +import { ServerConfig } from "../config.ts"; +import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts"; +import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts"; +import { OrchestrationLayerLive } from "../orchestration/runtimeLayer.ts"; +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import { OrchestrationEventStore } from "../persistence/Services/OrchestrationEventStore.ts"; +import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; +import { ServerSettingsService } from "../serverSettings.ts"; +import { layer as mcpSessionRegistryTestLayer } from "../mcp/McpSessionRegistry.testkit.ts"; +import { ProviderInstanceRegistry } from "../provider/Services/ProviderInstanceRegistry.ts"; +import type { ProviderInstance } from "../provider/ProviderDriver.ts"; +import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; +import * as VcsProcess from "../vcs/VcsProcess.ts"; +import { OrchestratorV2 } from "./Orchestrator.ts"; +import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts"; +import { OrchestrationV2LayerLive } from "./runtimeLayer.ts"; +import { shellStreamItemFromSnapshot } from "./ShellStream.ts"; +import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; + +const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { + prefix: "t3-orchestration-v2-runtime-layer-", +}); + +const modelSelection = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", +} satisfies ModelSelection; + +const VcsDriverRegistryTestLayer = VcsDriverRegistry.layer.pipe( + Layer.provide(VcsProcess.layer), + Layer.provide(ServerConfigLayer), + Layer.provide(NodeServices.layer), +); + +const CheckpointStoreTestLayer = CheckpointStore.layer.pipe( + Layer.provide(VcsDriverRegistryTestLayer), +); + +const driver = ProviderDriverKind.make("codex"); +const orchestrationAdapter = { + instanceId: modelSelection.instanceId, + driver, + getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2), + openSession: () => Effect.die("sessions are not used by lifecycle tests"), +} as ProviderAdapterV2Shape; +const providerInstance = { + instanceId: modelSelection.instanceId, + driverKind: driver, + continuationIdentity: { + driverKind: driver, + continuationKey: "codex:test", + }, + displayName: "Codex test", + enabled: true, + snapshot: {} as ProviderInstance["snapshot"], + orchestrationAdapter, + textGeneration: {} as ProviderInstance["textGeneration"], +} satisfies ProviderInstance; + +const TestProviderInstanceRegistry = Layer.succeed(ProviderInstanceRegistry, { + getInstance: (instanceId) => + Effect.succeed(instanceId === providerInstance.instanceId ? providerInstance : undefined), + listInstances: Effect.succeed([providerInstance]), + listUnavailable: Effect.succeed([]), + streamChanges: Stream.empty, + subscribeChanges: Effect.never, +}); + +const TestLayer = OrchestrationV2LayerLive.pipe( + Layer.provide(mcpSessionRegistryTestLayer), + Layer.provide(SqlitePersistenceMemory), + Layer.provide(CheckpointStoreTestLayer), + Layer.provide(ServerConfigLayer), + Layer.provide(ServerSettingsService.layerTest()), + Layer.provide(TestProviderInstanceRegistry), + Layer.provide(NodeServices.layer), +); + +const SharedApplicationDataPlaneTestLayer = Layer.merge( + OrchestrationLayerLive, + OrchestrationV2LayerLive, +).pipe( + Layer.provide( + Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, { + resolve: () => Effect.succeed(null), + }), + ), + Layer.provide(mcpSessionRegistryTestLayer), + Layer.provideMerge(SqlitePersistenceMemory), + Layer.provide(CheckpointStoreTestLayer), + Layer.provide(ServerConfigLayer), + Layer.provide(ServerSettingsService.layerTest()), + Layer.provide(TestProviderInstanceRegistry), + Layer.provide(NodeServices.layer), +); + +it.layer(TestLayer)("OrchestrationV2LayerLive", (it) => { + it.effect("creates and reads a thread through the production V2 composition", () => + Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + const threadId = ThreadId.make("runtime-layer-thread"); + const projectId = ProjectId.make("runtime-layer-project"); + + const result = yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("runtime-layer-create"), + threadId, + projectId, + title: "Runtime layer thread", + modelSelection: modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + + const projection = yield* orchestrator.getThreadProjection(threadId); + + assert.equal(result.sequence, 1); + assert.equal(projection.thread.id, threadId); + assert.equal(projection.thread.projectId, projectId); + assert.equal(projection.thread.providerInstanceId, "codex"); + assert.deepEqual(projection.runs, []); + }), + ); + + it.effect("applies lifecycle commands idempotently and emits archive/removal shell deltas", () => + Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + const threadId = ThreadId.make("runtime-layer-lifecycle-thread"); + const create = { + type: "thread.create" as const, + createdBy: "user" as const, + creationSource: "web" as const, + commandId: CommandId.make("runtime-layer-lifecycle-create"), + threadId, + projectId: ProjectId.make("runtime-layer-lifecycle-project"), + title: "Lifecycle thread", + modelSelection, + runtimeMode: "full-access" as const, + interactionMode: "default" as const, + branch: null, + worktreePath: null, + }; + + const firstCreate = yield* orchestrator.dispatch(create); + const retriedCreate = yield* orchestrator.dispatch(create); + assert.equal(retriedCreate.sequence, firstCreate.sequence); + assert.lengthOf(retriedCreate.storedEvents, 1); + + yield* orchestrator.dispatch({ + type: "thread.metadata.update", + commandId: CommandId.make("runtime-layer-lifecycle-metadata"), + threadId, + title: "Renamed lifecycle thread", + branch: "feature/v2", + worktreePath: "/tmp/t3-v2-worktree", + }); + yield* orchestrator.dispatch({ + type: "thread.runtime-mode.set", + commandId: CommandId.make("runtime-layer-lifecycle-runtime"), + threadId, + runtimeMode: "approval-required", + }); + yield* orchestrator.dispatch({ + type: "thread.interaction-mode.set", + commandId: CommandId.make("runtime-layer-lifecycle-interaction"), + threadId, + interactionMode: "plan", + }); + yield* orchestrator.dispatch({ + type: "thread.model-selection.set", + commandId: CommandId.make("runtime-layer-lifecycle-model"), + threadId, + modelSelection: { ...modelSelection, model: "gpt-5.5" }, + }); + + const archive = yield* orchestrator.dispatch({ + type: "thread.archive", + commandId: CommandId.make("runtime-layer-lifecycle-archive"), + threadId, + }); + const archivedShell = yield* orchestrator.getShellSnapshot(); + assert.notInclude( + archivedShell.threads.map((thread) => thread.id), + threadId, + ); + assert.include( + archivedShell.archivedThreads.map((thread) => thread.id), + threadId, + ); + assert.deepEqual( + shellStreamItemFromSnapshot({ + stored: archive.storedEvents[0]!, + snapshot: archivedShell, + }), + { + kind: "thread.updated", + sequence: archive.sequence, + location: "archive", + thread: archivedShell.archivedThreads[0]!, + }, + ); + + const remove = yield* orchestrator.dispatch({ + type: "thread.delete", + commandId: CommandId.make("runtime-layer-lifecycle-delete"), + threadId, + }); + const deletedShell = yield* orchestrator.getShellSnapshot(); + assert.notInclude( + deletedShell.threads.map((thread) => thread.id), + threadId, + ); + assert.notInclude( + deletedShell.archivedThreads.map((thread) => thread.id), + threadId, + ); + assert.deepEqual( + shellStreamItemFromSnapshot({ stored: remove.storedEvents[0]!, snapshot: deletedShell }), + { + kind: "thread.removed", + sequence: remove.sequence, + location: "archive", + threadId, + }, + ); + + const projection = yield* orchestrator.getThreadProjection(threadId); + assert.equal(projection.thread.title, "Renamed lifecycle thread"); + assert.equal(projection.thread.branch, "feature/v2"); + assert.equal(projection.thread.worktreePath, "/tmp/t3-v2-worktree"); + assert.equal(projection.thread.runtimeMode, "approval-required"); + assert.equal(projection.thread.interactionMode, "plan"); + assert.equal(projection.thread.modelSelection.model, "gpt-5.5"); + assert.isNotNull(projection.thread.archivedAt); + assert.isNotNull(projection.thread.deletedAt); + }), + ); + + it.effect("persists rejected command receipts across retries", () => + Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + const command = { + type: "thread.archive" as const, + commandId: CommandId.make("runtime-layer-rejected-command"), + threadId: ThreadId.make("runtime-layer-missing-thread"), + }; + + const first = yield* orchestrator.dispatch(command).pipe(Effect.flip); + const retry = yield* orchestrator.dispatch(command).pipe(Effect.flip); + + assert.equal(first._tag, "OrchestratorProjectionError"); + assert.equal(retry._tag, "OrchestratorCommandPreviouslyRejectedError"); + }), + ); +}); + +it.layer(SharedApplicationDataPlaneTestLayer)("shared application data plane", (it) => { + it.effect("orders retained project transactions and V2 thread transactions in one source", () => + Effect.gen(function* () { + const applicationEngine = yield* OrchestrationEngineService; + const applicationEvents = yield* OrchestrationEventStore; + const orchestrator = yield* OrchestratorV2; + const projectionSnapshot = yield* ProjectionSnapshotQuery; + const sql = yield* SqlClient.SqlClient; + const projectId = ProjectId.make("runtime-layer-shared-project"); + const threadId = ThreadId.make("runtime-layer-shared-thread"); + const projectCommand = { + type: "project.create" as const, + commandId: CommandId.make("runtime-layer-shared-project-create"), + projectId, + title: "Shared application source", + workspaceRoot: "/tmp/runtime-layer-shared-project", + defaultModelSelection: modelSelection, + scripts: [], + createdAt: "2026-06-20T00:00:00.000Z", + }; + + const projectResult = yield* applicationEngine.dispatch(projectCommand); + const projectRetry = yield* applicationEngine.dispatch(projectCommand); + assert.equal(projectRetry.sequence, projectResult.sequence); + + const delivered = yield* Queue.unbounded(); + yield* applicationEvents.streamApplicationEvents().pipe( + Stream.take(2), + Stream.runForEach((event) => Queue.offer(delivered, event)), + Effect.forkScoped, + ); + + const projectEvent = yield* Queue.take(delivered); + assert.equal(projectEvent.sequence, projectResult.sequence); + + const threadResult = yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("runtime-layer-shared-thread-create"), + threadId, + projectId, + title: "Shared thread", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + const threadEvent = yield* Queue.take(delivered); + + assert.equal(threadEvent.sequence, threadResult.sequence); + assert.isAbove(threadEvent.sequence, projectEvent.sequence); + assert.isTrue("aggregateKind" in projectEvent); + assert.isTrue("event" in threadEvent); + assert.equal((yield* projectionSnapshot.getProjectShellById(projectId))._tag, "Some"); + + const retainedReceipts = yield* sql<{ + readonly aggregate_kind: string; + readonly aggregate_id: string; + }>` + SELECT aggregate_kind, aggregate_id + FROM orchestration_command_receipts + ORDER BY result_sequence ASC + `; + assert.deepEqual(retainedReceipts, [ + { aggregate_kind: "project", aggregate_id: projectId }, + { aggregate_kind: "thread", aggregate_id: threadId }, + ]); + + const retiredWrites = yield* sql<{ readonly count: number }>` + SELECT + (SELECT COUNT(*) FROM orchestration_v2_events) + + (SELECT COUNT(*) FROM orchestration_v2_command_receipts) AS count + `; + assert.equal(retiredWrites[0]?.count, 0); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/runtimeLayer.ts b/apps/server/src/orchestration-v2/runtimeLayer.ts new file mode 100644 index 00000000000..4b10daff250 --- /dev/null +++ b/apps/server/src/orchestration-v2/runtimeLayer.ts @@ -0,0 +1,235 @@ +import * as Layer from "effect/Layer"; +import { + OrchestrationEventInfrastructureLayerLive, + OrchestrationLayerLive, +} from "../orchestration/runtimeLayer.ts"; +import { ProjectionProjectRepositoryLive } from "../persistence/Layers/ProjectionProjects.ts"; +import { layer as projectServiceLayer } from "../project/ProjectService.ts"; +import { layer as projectSetupScriptRunnerLayer } from "../project/ProjectSetupScriptRunner.ts"; +import { layer as checkpointCaptureServiceLayer } from "./CheckpointCaptureService.ts"; +import { layer as checkpointServiceLayer } from "./CheckpointService.ts"; +import { layer as checkpointRollbackServiceLayer } from "./CheckpointRollbackService.ts"; +import { layer as commandPolicyLayer } from "./CommandPolicy.ts"; +import { layerFromApplicationReceipts as commandReceiptStoreLayer } from "./CommandReceiptStore.ts"; +import { layer as contextHandoffServiceLayer } from "./ContextHandoffService.ts"; +import { layer as effectOutboxLayer } from "./EffectOutbox.ts"; +import { + executorLayer as effectExecutorLayer, + layer as effectWorkerLayer, +} from "./EffectWorker.ts"; +import { layerFromStores as eventSinkLayer } from "./EventSink.ts"; +import { layerFromOrchestrationEventStore as eventStoreLayer } from "./EventStore.ts"; +import { layer as idAllocatorLayer } from "./IdAllocator.ts"; +import { layer as orchestratorLayer } from "./Orchestrator.ts"; +import { layer as projectionStoreLayer } from "./ProjectionStore.ts"; +import { layer as projectionMaintenanceLayer } from "./ProjectionMaintenance.ts"; +import { layerFromProviderInstanceRegistry as providerAdapterRegistryLayerFromProviderInstances } from "./ProviderAdapterRegistry.ts"; +import { layer as providerEventIngestorLayer } from "./ProviderEventIngestor.ts"; +import { layer as providerSessionManagerLayer } from "./ProviderSessionManager.ts"; +import { layer as providerRuntimeRecoveryLayer } from "./ProviderRuntimeRecoveryService.ts"; +import { layer as providerSwitchServiceLayer } from "./ProviderSwitchService.ts"; +import { layer as providerTurnControlServiceLayer } from "./ProviderTurnControlService.ts"; +import { layer as providerTurnStartServiceLayer } from "./ProviderTurnStartService.ts"; +import { layer as runExecutionServiceLayer } from "./RunExecutionService.ts"; +import { layer as runFinalizationServiceLayer } from "./RunFinalizationService.ts"; +import { layerFromProjectRepository as runtimePolicyLayerFromProjectRepository } from "./RuntimePolicy.ts"; +import { layer as runtimeRequestServiceLayer } from "./RuntimeRequestService.ts"; +import { layer as threadManagementServiceLayer } from "./ThreadManagementService.ts"; +import { layer as threadLaunchServiceLayer } from "./ThreadLaunchService.ts"; +import { layer as threadLifecycleServiceLayer } from "./ThreadLifecycleService.ts"; +import { layer as threadForkServiceLayer } from "./ThreadForkService.ts"; +import { layer as turnItemPositionStoreLayer } from "./TurnItemPositionStore.ts"; + +export const ProjectServiceLayerLive = projectServiceLayer.pipe( + Layer.provide(Layer.merge(ProjectionProjectRepositoryLive, OrchestrationLayerLive)), +); +const runtimePolicyProvided = runtimePolicyLayerFromProjectRepository.pipe( + Layer.provide(ProjectionProjectRepositoryLive), +); + +const eventStoreProvided = eventStoreLayer.pipe( + Layer.provide(OrchestrationEventInfrastructureLayerLive), +); +const commandReceiptStoreProvided = commandReceiptStoreLayer.pipe( + Layer.provide(OrchestrationEventInfrastructureLayerLive), +); + +const storesLayer = Layer.mergeAll( + OrchestrationEventInfrastructureLayerLive, + eventStoreProvided, + projectionStoreLayer, + commandReceiptStoreProvided, + effectOutboxLayer, + turnItemPositionStoreLayer, +); + +const eventSinkProvided = eventSinkLayer.pipe(Layer.provide(storesLayer)); +const projectionMaintenanceProvided = projectionMaintenanceLayer.pipe(Layer.provide(storesLayer)); + +const providerEventIngestorProvided = providerEventIngestorLayer.pipe( + Layer.provide(Layer.mergeAll(eventSinkProvided, idAllocatorLayer)), +); + +const checkpointServiceProvided = checkpointServiceLayer.pipe(Layer.provide(idAllocatorLayer)); +const contextHandoffServiceProvided = contextHandoffServiceLayer.pipe( + Layer.provide(idAllocatorLayer), +); + +const providerAdapterRegistryProvided = providerAdapterRegistryLayerFromProviderInstances; +const providerSwitchServiceProvided = providerSwitchServiceLayer.pipe( + Layer.provide(providerAdapterRegistryProvided), +); + +const providerSessionManagerProvided = providerSessionManagerLayer.pipe( + Layer.provide( + Layer.mergeAll( + providerAdapterRegistryProvided, + eventSinkProvided, + idAllocatorLayer, + projectionStoreLayer, + ), + ), +); + +const runExecutionServiceProvided = runExecutionServiceLayer.pipe( + Layer.provide( + Layer.mergeAll( + checkpointServiceProvided, + eventSinkProvided, + idAllocatorLayer, + providerEventIngestorProvided, + ), + ), +); + +const providerTurnStartServiceProvided = providerTurnStartServiceLayer.pipe( + Layer.provide( + Layer.mergeAll( + contextHandoffServiceProvided, + eventSinkProvided, + idAllocatorLayer, + projectionStoreLayer, + providerSessionManagerProvided, + runExecutionServiceProvided, + runtimePolicyProvided, + ), + ), +); + +const providerTurnControlServiceProvided = providerTurnControlServiceLayer.pipe( + Layer.provide(Layer.merge(projectionStoreLayer, providerSessionManagerProvided)), +); +const runtimeRequestServiceProvided = runtimeRequestServiceLayer.pipe( + Layer.provide(Layer.merge(projectionStoreLayer, providerSessionManagerProvided)), +); +const checkpointRollbackServiceProvided = checkpointRollbackServiceLayer.pipe( + Layer.provide( + Layer.mergeAll( + checkpointServiceProvided, + eventSinkProvided, + idAllocatorLayer, + projectionStoreLayer, + providerSessionManagerProvided, + runtimePolicyProvided, + ), + ), +); +const checkpointCaptureServiceProvided = checkpointCaptureServiceLayer.pipe( + Layer.provide( + Layer.mergeAll( + checkpointServiceProvided, + eventSinkProvided, + idAllocatorLayer, + projectionStoreLayer, + ), + ), +); +const runFinalizationServiceProvided = runFinalizationServiceLayer.pipe( + Layer.provide(Layer.merge(checkpointCaptureServiceProvided, projectionStoreLayer)), +); + +const effectExecutorProvided = effectExecutorLayer.pipe( + Layer.provide( + Layer.mergeAll( + runFinalizationServiceProvided, + checkpointRollbackServiceProvided, + providerSessionManagerProvided, + providerTurnControlServiceProvided, + providerTurnStartServiceProvided, + runtimeRequestServiceProvided, + ), + ), +); +const effectWorkerProvided = effectWorkerLayer.pipe( + Layer.provide(Layer.merge(storesLayer, effectExecutorProvided)), +); +const providerRuntimeRecoveryProvided = providerRuntimeRecoveryLayer.pipe( + Layer.provide( + Layer.mergeAll( + effectWorkerProvided, + storesLayer, + eventSinkProvided, + idAllocatorLayer, + projectionStoreLayer, + providerSessionManagerProvided, + ), + ), +); + +const orchestratorProvided = orchestratorLayer.pipe( + Layer.provide( + Layer.mergeAll( + checkpointServiceProvided, + commandPolicyLayer, + storesLayer, + eventSinkProvided, + effectWorkerProvided, + commandReceiptStoreProvided, + contextHandoffServiceProvided, + idAllocatorLayer, + providerAdapterRegistryProvided, + providerEventIngestorProvided, + runtimePolicyProvided, + providerSessionManagerProvided, + providerSwitchServiceProvided, + runExecutionServiceProvided, + threadForkServiceLayer, + ), + ), +); + +const threadManagementProvided = threadManagementServiceLayer.pipe( + Layer.provide(orchestratorProvided), +); +export const ProjectSetupScriptRunnerLayerLive = projectSetupScriptRunnerLayer.pipe( + Layer.provide(ProjectServiceLayerLive), +); +const threadLaunchProvided = threadLaunchServiceLayer.pipe( + Layer.provide( + Layer.mergeAll( + ProjectServiceLayerLive, + ProjectSetupScriptRunnerLayerLive, + threadManagementProvided, + idAllocatorLayer, + ), + ), +); +const threadLifecycleProvided = threadLifecycleServiceLayer.pipe( + Layer.provide(threadManagementProvided), +); + +export const OrchestrationV2LayerLive = Layer.mergeAll( + orchestratorProvided, + threadManagementProvided, + effectWorkerProvided, + providerRuntimeRecoveryProvided, + projectionMaintenanceProvided, +); + +export const OrchestrationV2ProductionLayerLive = Layer.mergeAll( + OrchestrationLayerLive, + OrchestrationV2LayerLive, + ProjectServiceLayerLive, + threadLaunchProvided, + threadLifecycleProvided, +); diff --git a/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts new file mode 100644 index 00000000000..3f2ea0d8af3 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts @@ -0,0 +1,399 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; + +import { classifyClaudeNativeTool } from "../Adapters/ClaudeAdapterV2.ts"; +import { + ClaudeOrchestratorReplayHarness, + recordClaudeAgentSdkReplayTranscript, + replayClaudeAgentSdkTranscript, +} from "../Adapters/ClaudeAdapterV2.testkit.ts"; +import { ORCHESTRATOR_REPLAY_FIXTURES } from "./fixtures/index.ts"; +import { + MULTI_TURN_FIRST_PROMPT, + MULTI_TURN_SECOND_PROMPT, + SIMPLE_PROMPT, + THREAD_FORK_NATIVE_CONTINUE_FORK_MARKER, + THREAD_FORK_NATIVE_CONTINUE_RECALL, + THREAD_FORK_NATIVE_CONTINUE_SOURCE_MARKER, + THREAD_MERGE_BACK_FORK_MARKER, + THREAD_MERGE_BACK_RECALL, + THREAD_MERGE_BACK_SIBLINGS_FIRST_MARKER, + THREAD_MERGE_BACK_SIBLINGS_RECALL, + THREAD_MERGE_BACK_SIBLINGS_SECOND_MARKER, + THREAD_MERGE_BACK_SIBLINGS_SOURCE_MARKER, + THREAD_MERGE_BACK_SOURCE_MARKER, +} from "./fixtures/shared.ts"; +import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts"; +import { decodeProviderReplayNdjson } from "./ReplayTranscriptNdjson.ts"; + +const readTranscript = Effect.fn("readClaudeReplayFixture")(function* (file: URL) { + const fs = yield* FileSystem.FileSystem; + const text = yield* fs.readFileString(decodeURIComponent(file.pathname)); + return yield* decodeProviderReplayNdjson(text); +}, Effect.provide(NodeServices.layer)); + +function claudeFixture(name: string) { + const fixture = ORCHESTRATOR_REPLAY_FIXTURES.find((entry) => entry.name === name); + const provider = fixture?.providers.find((entry) => entry.driver === "claudeAgent"); + if (fixture === undefined || provider === undefined) { + throw new Error(`Missing ${name}/claudeAgent replay fixture.`); + } + return { fixture, provider }; +} + +function readClaudeTranscriptFixture(path: string) { + return readTranscript(new URL(`./fixtures/${path}/claude_transcript.ndjson`, import.meta.url)); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function metadataString(transcript: ProviderReplayTranscript, key: string): string { + const value = transcript.metadata?.[key]; + if (typeof value !== "string") { + throw new Error(`${transcript.scenario} metadata.${key} must be a string.`); + } + return value; +} + +function metadataStringArray( + transcript: ProviderReplayTranscript, + key: string, +): ReadonlyArray { + const value = transcript.metadata?.[key]; + if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) { + throw new Error(`${transcript.scenario} metadata.${key} must be a string array.`); + } + return value; +} + +type FramedReplayEntry = Extract< + ProviderReplayTranscript["entries"][number], + { readonly frame: unknown } +>; + +function frameRecord(entry: FramedReplayEntry): Record { + if (!isRecord(entry.frame)) { + throw new Error("Replay entry frame must be an object."); + } + return entry.frame; +} + +function findEntryFrame( + transcript: ProviderReplayTranscript, + label: string, +): Record { + const entry = transcript.entries.find( + (candidate): candidate is FramedReplayEntry => + "label" in candidate && candidate.label === label && "frame" in candidate, + ); + assert.isDefined(entry, `${transcript.scenario} must include replay entry ${label}`); + return frameRecord(entry); +} + +function successResultTexts(transcript: ProviderReplayTranscript): ReadonlyArray { + return transcript.entries.flatMap((entry) => { + if (entry.type !== "emit_inbound" || !isRecord(entry.frame)) { + return []; + } + if (entry.frame.type !== "result" || entry.frame.subtype !== "success") { + return []; + } + return typeof entry.frame.result === "string" ? [entry.frame.result] : []; + }); +} + +function claudeToolUseNamesFromTranscript( + transcript: ProviderReplayTranscript, +): ReadonlyArray { + return transcript.entries.flatMap((entry) => { + if ( + entry.type !== "emit_inbound" || + !isRecord(entry.frame) || + entry.frame.type !== "assistant" + ) { + return []; + } + + const message = entry.frame.message; + const content = isRecord(message) ? message.content : undefined; + if (!Array.isArray(content)) { + return []; + } + + return content.flatMap((part) => + isRecord(part) && + typeof part.id === "string" && + typeof part.name === "string" && + "input" in part + ? [part.name] + : [], + ); + }); +} + +describe("Claude Agent SDK replay fixtures", () => { + it.effect("classifies every Claude fixture tool use through the native tool table", () => + Effect.gen(function* () { + const unknownToolNames = new Set(); + const seenToolNames = new Set(); + + for (const fixture of ORCHESTRATOR_REPLAY_FIXTURES) { + for (const provider of fixture.providers) { + if (provider.driver !== "claudeAgent") { + continue; + } + + const transcript = yield* readTranscript(provider.transcriptFile); + for (const toolName of claudeToolUseNamesFromTranscript(transcript)) { + seenToolNames.add(toolName); + const classification = classifyClaudeNativeTool(toolName); + if (!classification.known) { + unknownToolNames.add(`${fixture.name}:${toolName}`); + } + } + } + } + + assert.isAtLeast(seenToolNames.size, 1, "expected Claude fixtures to contain tool uses"); + assert.deepEqual([...unknownToolNames], []); + }), + ); + + it.effect("keeps unregistered native conversation-state transcripts reviewable", () => + Effect.gen(function* () { + const rollback = yield* readClaudeTranscriptFixture("thread_rollback"); + assert.equal(rollback.metadata?.queryMode, "resume_at_cursor"); + const rollbackCursor = metadataString(rollback, "resumeSessionAt"); + const rollbackResumeFrame = findEntryFrame(rollback, "query.open:resume_at_cursor"); + const rollbackResumeOptions = rollbackResumeFrame.options; + if (!isRecord(rollbackResumeOptions)) { + throw new Error("Rollback resume query.open options must be an object."); + } + assert.equal(rollbackResumeOptions.resumeSessionAt, rollbackCursor); + const rollbackFinalText = successResultTexts(rollback).at(-1) ?? ""; + assert.include(rollbackFinalText, "rollback fixture first turn complete"); + assert.notInclude(rollbackFinalText, "rollback fixture second turn complete"); + + const latestFork = yield* readClaudeTranscriptFixture("thread_fork_native"); + assert.equal(latestFork.metadata?.queryMode, "fork_session"); + const latestForkedSessionId = metadataString(latestFork, "forkedNativeSessionId"); + const latestForkedFrame = findEntryFrame(latestFork, "session.forked"); + assert.equal(latestForkedFrame.sessionId, latestForkedSessionId); + + const priorFork = yield* readClaudeTranscriptFixture("thread_fork_native_prior_turn"); + assert.equal(priorFork.metadata?.queryMode, "fork_session_prior_turn"); + const priorForkCursor = metadataString(priorFork, "forkUpToMessageId"); + const priorForkFrame = findEntryFrame(priorFork, "session.fork"); + const priorForkOptions = priorForkFrame.options; + if (!isRecord(priorForkOptions)) { + throw new Error("Prior-turn fork options must be an object."); + } + assert.equal(priorForkOptions.upToMessageId, priorForkCursor); + const priorForkFinalText = successResultTexts(priorFork).at(-1) ?? ""; + assert.include(priorForkFinalText, "fork boundary alpha"); + assert.notInclude(priorForkFinalText, "fork boundary beta"); + + const continuedFork = yield* readClaudeTranscriptFixture("thread_fork_native_continue"); + assert.equal(continuedFork.metadata?.queryMode, "fork_session_continue"); + const continuedForkSessionId = metadataString(continuedFork, "forkedNativeSessionId"); + const continuedForkOpenFrame = findEntryFrame(continuedFork, "query.open:fork"); + const continuedForkOptions = continuedForkOpenFrame.options; + if (!isRecord(continuedForkOptions)) { + throw new Error("Continued fork query.open options must be an object."); + } + assert.equal(continuedForkOptions.resume, continuedForkSessionId); + const continuedForkResults = successResultTexts(continuedFork); + assert.deepEqual(continuedForkResults.slice(0, 2), [ + "source marker stored", + "fork marker stored", + ]); + assert.equal( + continuedForkResults.at(-1)?.replace(/\s*\|\s*/u, "|"), + THREAD_FORK_NATIVE_CONTINUE_RECALL, + ); + const recallPromptFrame = findEntryFrame(continuedFork, "prompt.offer:3"); + const recallMessage = recallPromptFrame.message; + const recallMessageBody = isRecord(recallMessage) ? recallMessage.message : undefined; + const recallPrompt = + isRecord(recallMessageBody) && typeof recallMessageBody.content === "string" + ? recallMessageBody.content + : ""; + assert.notInclude(recallPrompt, THREAD_FORK_NATIVE_CONTINUE_SOURCE_MARKER); + assert.notInclude(recallPrompt, THREAD_FORK_NATIVE_CONTINUE_FORK_MARKER); + + const siblingForks = yield* readClaudeTranscriptFixture("thread_fork_native_siblings"); + assert.equal(siblingForks.metadata?.queryMode, "fork_session_siblings"); + const siblingSessionIds = metadataStringArray(siblingForks, "forkedNativeSessionIds"); + assert.lengthOf(siblingSessionIds, 2); + assert.notEqual(siblingSessionIds[0], siblingSessionIds[1]); + const siblingResults = successResultTexts(siblingForks).map((text) => + text.replace(/\s*\|\s*/u, "|"), + ); + assert.deepEqual(siblingResults, [ + "sibling source stored", + "sibling-source-8R3D|sibling-first-5L2P", + "sibling-source-8R3D|sibling-second-9N6C", + ]); + assert.notInclude(siblingResults[1] ?? "", "sibling-second-9N6C"); + assert.notInclude(siblingResults[2] ?? "", "sibling-first-5L2P"); + + const mergeBack = yield* readClaudeTranscriptFixture("thread_merge_back_continue"); + assert.equal(mergeBack.metadata?.queryMode, "fork_session_merge_back"); + const mergeBackSourceSessionId = metadataString(mergeBack, "nativeSessionId"); + const mergeBackContinuationFrame = findEntryFrame( + mergeBack, + "query.open:source-continuation", + ); + const mergeBackContinuationOptions = mergeBackContinuationFrame.options; + if (!isRecord(mergeBackContinuationOptions)) { + throw new Error("Merge-back source continuation options must be an object."); + } + assert.equal(mergeBackContinuationOptions.resume, mergeBackSourceSessionId); + const mergeBackRecallFrame = findEntryFrame(mergeBack, "prompt.offer:4"); + const mergeBackRecallMessage = mergeBackRecallFrame.message; + const mergeBackRecallBody = isRecord(mergeBackRecallMessage) + ? mergeBackRecallMessage.message + : undefined; + const mergeBackRecallPrompt = + isRecord(mergeBackRecallBody) && typeof mergeBackRecallBody.content === "string" + ? mergeBackRecallBody.content + : ""; + assert.notInclude(mergeBackRecallPrompt, THREAD_MERGE_BACK_SOURCE_MARKER); + assert.notInclude(mergeBackRecallPrompt, THREAD_MERGE_BACK_FORK_MARKER); + assert.equal( + successResultTexts(mergeBack) + .at(-1) + ?.replace(/\s*\|\s*/gu, "|"), + THREAD_MERGE_BACK_RECALL, + ); + + const siblingMergeBack = yield* readClaudeTranscriptFixture("thread_merge_back_siblings"); + assert.equal(siblingMergeBack.metadata?.queryMode, "fork_session_merge_back_siblings"); + const siblingMergeSessionIds = metadataStringArray( + siblingMergeBack, + "forkedNativeSessionIds", + ); + assert.lengthOf(siblingMergeSessionIds, 2); + assert.notEqual(siblingMergeSessionIds[0], siblingMergeSessionIds[1]); + const siblingMergeRecallFrame = findEntryFrame(siblingMergeBack, "prompt.offer:6"); + const siblingMergeRecallMessage = siblingMergeRecallFrame.message; + const siblingMergeRecallBody = isRecord(siblingMergeRecallMessage) + ? siblingMergeRecallMessage.message + : undefined; + const siblingMergeRecallPrompt = + isRecord(siblingMergeRecallBody) && typeof siblingMergeRecallBody.content === "string" + ? siblingMergeRecallBody.content + : ""; + assert.notInclude(siblingMergeRecallPrompt, THREAD_MERGE_BACK_SIBLINGS_SOURCE_MARKER); + assert.notInclude(siblingMergeRecallPrompt, THREAD_MERGE_BACK_SIBLINGS_FIRST_MARKER); + assert.notInclude(siblingMergeRecallPrompt, THREAD_MERGE_BACK_SIBLINGS_SECOND_MARKER); + assert.equal( + successResultTexts(siblingMergeBack) + .at(-1) + ?.replace(/\s*\|\s*/gu, "|"), + THREAD_MERGE_BACK_SIBLINGS_RECALL, + ); + + const forkLocalRollback = yield* readClaudeTranscriptFixture( + "thread_fork_native_fork_local_rollback", + ); + assert.equal(forkLocalRollback.metadata?.queryMode, "fork_session_resume_at_fork_cursor"); + const forkLocalRollbackCursor = metadataString(forkLocalRollback, "resumeSessionAt"); + const forkLocalRollbackFrame = findEntryFrame( + forkLocalRollback, + "query.open:fork-resume-at-cursor", + ); + const forkLocalRollbackOptions = forkLocalRollbackFrame.options; + if (!isRecord(forkLocalRollbackOptions)) { + throw new Error("Fork-local rollback resume query.open options must be an object."); + } + assert.equal(forkLocalRollbackOptions.resumeSessionAt, forkLocalRollbackCursor); + const forkLocalRollbackFinalText = successResultTexts(forkLocalRollback).at(-1) ?? ""; + assert.include(forkLocalRollbackFinalText, "fork local source alpha"); + assert.include(forkLocalRollbackFinalText, "fork local first"); + assert.notInclude(forkLocalRollbackFinalText, "fork local second"); + }), + ); + + it.skipIf(process.env.T3_RECORD_CLAUDE_AGENT_SDK_FIXTURE !== "1")( + "records simple from real Claude Code query() output", + () => + Effect.scoped( + Effect.gen(function* () { + const { fixture, provider } = claudeFixture("simple"); + + const workspace = yield* checkpointWorkspace("claude-simple-record"); + const transcript = yield* Effect.promise(() => + recordClaudeAgentSdkReplayTranscript({ + scenario: fixture.name, + prompts: [SIMPLE_PROMPT], + modelSelection: provider.modelSelection, + cwd: workspace, + }), + ); + + assert.equal(transcript.provider, "claudeAgent"); + assert.equal(transcript.protocol, "claude-agent-sdk.query"); + assert.isAtLeast(transcript.entries.length, 3); + }), + ), + ); + + it.effect("replays simple as typed Claude Agent SDK query messages", () => + Effect.gen(function* () { + const { provider } = claudeFixture("simple"); + + const rawTranscript = yield* readTranscript(provider.transcriptFile); + const transcript = yield* ClaudeOrchestratorReplayHarness.decodeTranscript(rawTranscript); + + const messages = yield* Effect.promise(() => + replayClaudeAgentSdkTranscript({ + transcript, + prompts: [SIMPLE_PROMPT], + modelSelection: provider.modelSelection, + }), + ); + + assert.include( + messages + .filter((message) => message.type === "assistant") + .flatMap((message) => + message.message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])), + ) + .join(""), + "fixture simple ok", + ); + }), + ); + + it.effect("replays multi_turn as typed Claude Agent SDK query messages", () => + Effect.gen(function* () { + const { provider } = claudeFixture("multi_turn"); + + const rawTranscript = yield* readTranscript(provider.transcriptFile); + const transcript = yield* ClaudeOrchestratorReplayHarness.decodeTranscript(rawTranscript); + + const messages = yield* Effect.promise(() => + replayClaudeAgentSdkTranscript({ + transcript, + prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT], + modelSelection: provider.modelSelection, + }), + ); + + const assistantText = messages + .filter((message) => message.type === "assistant") + .flatMap((message) => + message.message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])), + ) + .join("\n"); + assert.include(assistantText, "first fixture turn complete"); + assert.include(assistantText, "second fixture turn complete"); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/CodexReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/CodexReplayFixtures.integration.test.ts new file mode 100644 index 00000000000..55a0906297f --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/CodexReplayFixtures.integration.test.ts @@ -0,0 +1,649 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; +import * as CodexReplay from "effect-codex-app-server/replay"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Schema from "effect/Schema"; + +import { ORCHESTRATOR_REPLAY_FIXTURES } from "./fixtures/index.ts"; +import { + THREAD_FORK_NATIVE_CONTINUE_FORK_MARKER, + THREAD_FORK_NATIVE_CONTINUE_RECALL, + THREAD_FORK_NATIVE_CONTINUE_SOURCE_MARKER, + THREAD_MERGE_BACK_FORK_MARKER, + THREAD_MERGE_BACK_RECALL, + THREAD_MERGE_BACK_SIBLINGS_FIRST_MARKER, + THREAD_MERGE_BACK_SIBLINGS_RECALL, + THREAD_MERGE_BACK_SIBLINGS_SECOND_MARKER, + THREAD_MERGE_BACK_SIBLINGS_SOURCE_MARKER, + THREAD_MERGE_BACK_SOURCE_MARKER, +} from "./fixtures/shared.ts"; +import { decodeProviderReplayNdjson } from "./ReplayTranscriptNdjson.ts"; + +const PROVIDER_THREAD_RESUME_FIRST_FINAL = "provider thread resume fixture first turn complete"; +const PROVIDER_THREAD_RESUME_SECOND_FINAL = "provider thread resume fixture second turn complete"; + +type ProtocolReplayEntry = Extract< + ProviderReplayTranscript["entries"][number], + { readonly type: "expect_outbound" | "emit_inbound" } +>; + +const CODEX_REPLAY_FIXTURES = ORCHESTRATOR_REPLAY_FIXTURES.flatMap((fixture) => + fixture.providers + .filter((provider) => provider.driver === "codex") + .map((provider) => ({ + scenario: fixture.name, + transcriptFile: provider.transcriptFile, + })), +).concat([ + { + scenario: "provider_thread_resume", + transcriptFile: new URL( + "./fixtures/provider_thread_resume/codex_transcript.ndjson", + import.meta.url, + ), + }, + { + scenario: "thread_fork_native_continue", + transcriptFile: new URL( + "./fixtures/thread_fork_native_continue/codex_transcript.ndjson", + import.meta.url, + ), + }, + { + scenario: "thread_fork_native_siblings", + transcriptFile: new URL( + "./fixtures/thread_fork_native_siblings/codex_transcript.ndjson", + import.meta.url, + ), + }, + { + scenario: "thread_merge_back_continue", + transcriptFile: new URL( + "./fixtures/thread_merge_back_continue/codex_transcript.ndjson", + import.meta.url, + ), + }, + { + scenario: "thread_merge_back_siblings", + transcriptFile: new URL( + "./fixtures/thread_merge_back_siblings/codex_transcript.ndjson", + import.meta.url, + ), + }, +]); + +const scenarioExpectations = { + simple: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: ["thread/started", "turn/started", "turn/completed"], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 0, + }, + tool_call_read_only_on_request: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: ["item/commandExecution/requestApproval", "serverRequest/resolved", "turn/completed"], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 1, + }, + tool_call_workspace_never: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: ["turn/completed"], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 0, + }, + tool_call_restricted_granular: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: [ + "item/fileChange/requestApproval", + "serverRequest/resolved", + "item/fileChange/outputDelta", + "turn/diff/updated", + "turn/completed", + ], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 1, + }, + subagent: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: ["turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 1, + turnCompletedCount: 3, + approvalRequestCount: 0, + }, + subagent_continue: { + outgoing: [ + "initialize", + "initialized", + "thread/start", + "turn/start/spawn", + "turn/start/continue", + ], + incoming: [ + "turn/started/root-1", + "turn/completed/child-1", + "turn/completed/root-1", + "turn/started/root-2", + "turn/completed/child-2", + "turn/completed/root-2", + ], + turnStartCount: 0, + turnCompletedCount: 0, + approvalRequestCount: 0, + }, + multi_turn: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: ["turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 2, + turnCompletedCount: 2, + approvalRequestCount: 0, + }, + plan_questions: { + outgoing: [ + "initialize", + "initialized", + "thread/start", + "turn/start", + "item/tool/requestUserInput", + ], + incoming: [ + "turn/started", + "turn/completed", + "item/tool/requestUserInput", + "serverRequest/resolved", + "item/agentMessage/delta", + ], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 0, + }, + proposed_plan: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: ["turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 0, + }, + provider_thread_resume: { + outgoing: ["initialize", "initialized", "thread/start", "thread/resume", "turn/start"], + incoming: ["thread/started", "turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 2, + turnCompletedCount: 2, + approvalRequestCount: 0, + }, + queued_turn: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: ["turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 2, + turnCompletedCount: 2, + approvalRequestCount: 0, + }, + message_steering: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start", "turn/steer"], + incoming: ["turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 0, + }, + turn_interrupt: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start", "turn/interrupt"], + incoming: ["turn/started", "turn/completed"], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 0, + }, + turn_interrupt_mid_tool: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start", "turn/interrupt"], + incoming: ["turn/started", "item/started", "turn/completed"], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 0, + }, + thread_rollback: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start", "thread/rollback"], + incoming: ["turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 3, + turnCompletedCount: 3, + approvalRequestCount: 0, + }, + thread_fork_native_continue: { + outgoing: ["initialize", "initialized", "thread/start", "thread/fork", "turn/start"], + incoming: ["thread/started", "turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 3, + turnCompletedCount: 3, + approvalRequestCount: 0, + }, + thread_fork_native_siblings: { + outgoing: ["initialize", "initialized", "thread/start", "thread/fork", "turn/start"], + incoming: ["thread/started", "turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 3, + turnCompletedCount: 3, + approvalRequestCount: 0, + }, + thread_merge_back_continue: { + outgoing: ["initialize", "initialized", "thread/start", "thread/fork", "turn/start"], + incoming: ["thread/started", "turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 4, + turnCompletedCount: 4, + approvalRequestCount: 0, + }, + thread_merge_back_siblings: { + outgoing: ["initialize", "initialized", "thread/start", "thread/fork", "turn/start"], + incoming: ["thread/started", "turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 6, + turnCompletedCount: 6, + approvalRequestCount: 0, + }, + todo_list: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: ["turn/started", "turn/completed", "turn/plan/updated", "item/agentMessage/delta"], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 0, + }, + web_search: { + outgoing: ["initialize", "initialized", "thread/start", "turn/start"], + incoming: ["turn/started", "turn/completed", "item/agentMessage/delta"], + turnStartCount: 1, + turnCompletedCount: 1, + approvalRequestCount: 0, + }, +} as const; + +const decodeCodexTranscript = Schema.decodeUnknownEffect( + CodexReplay.CodexAppServerReplayTranscript, +); +const readTranscript = Effect.fn("readCodexReplayFixture")(function* (file: URL) { + const fs = yield* FileSystem.FileSystem; + const text = yield* fs.readFileString(decodeURIComponent(file.pathname)); + return yield* decodeProviderReplayNdjson(text); +}, Effect.provide(NodeServices.layer)); + +function labels( + transcript: ProviderReplayTranscript, + type: "expect_outbound" | "emit_inbound", +): ReadonlyArray { + return transcript.entries.flatMap((entry) => { + if (entry.type !== type || entry.label === undefined) { + return []; + } + return [entry.label]; + }); +} + +function countLabel( + transcript: ProviderReplayTranscript, + type: "expect_outbound" | "emit_inbound", + label: string, +) { + return labels(transcript, type).filter((entryLabel) => entryLabel === label).length; +} + +function countApprovalRequests(transcript: ProviderReplayTranscript) { + return labels(transcript, "emit_inbound").filter((label) => label.endsWith("/requestApproval")) + .length; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function readPath(value: unknown, path: ReadonlyArray): unknown { + let current = value; + for (const segment of path) { + if (typeof segment === "number") { + if (!Array.isArray(current)) { + throw new Error(`Expected array while reading ${path.join(".")}.`); + } + current = current[segment]; + continue; + } + + if (!isRecord(current)) { + throw new Error(`Expected object while reading ${path.join(".")}.`); + } + current = current[segment]; + } + return current; +} + +function readString(value: unknown, path: ReadonlyArray): string { + const current = readPath(value, path); + if (typeof current !== "string") { + throw new Error(`Expected string at ${path.join(".")}.`); + } + return current; +} + +function readArray(value: unknown, path: ReadonlyArray): ReadonlyArray { + const current = readPath(value, path); + if (!Array.isArray(current)) { + throw new Error(`Expected array at ${path.join(".")}.`); + } + return current; +} + +function findProtocolEntry( + transcript: ProviderReplayTranscript, + type: "expect_outbound" | "emit_inbound", + label: string, + occurrence = 0, +): ProtocolReplayEntry { + const matches = transcript.entries.filter( + (entry): entry is ProtocolReplayEntry => entry.type === type && entry.label === label, + ); + const entry = matches[occurrence]; + if (!entry) { + throw new Error(`Missing ${type} ${label} occurrence ${occurrence}.`); + } + return entry; +} + +function agentMessageTexts(transcript: ProviderReplayTranscript): ReadonlyArray { + return transcript.entries.flatMap((entry) => { + if (entry.type !== "emit_inbound" || entry.label !== "item/completed") { + return []; + } + + const item = readPath(entry.frame, ["params", "item"]); + if (!isRecord(item) || item.type !== "agentMessage" || typeof item.text !== "string") { + return []; + } + return [item.text]; + }); +} + +function assertScenarioExpectations(transcript: ProviderReplayTranscript) { + const expectation = + scenarioExpectations[transcript.scenario as keyof typeof scenarioExpectations]; + const outgoingLabels = labels(transcript, "expect_outbound"); + const incomingLabels = labels(transcript, "emit_inbound"); + + assert.isDefined(expectation, `missing scenario expectation for ${transcript.scenario}`); + for (const label of expectation.outgoing) { + assert.include(outgoingLabels, label, `${transcript.scenario} missing outgoing ${label}`); + } + for (const label of expectation.incoming) { + assert.include(incomingLabels, label, `${transcript.scenario} missing incoming ${label}`); + } + + assert.equal(countLabel(transcript, "expect_outbound", "turn/start"), expectation.turnStartCount); + assert.equal( + countLabel(transcript, "emit_inbound", "turn/completed"), + expectation.turnCompletedCount, + ); + assert.equal(countApprovalRequests(transcript), expectation.approvalRequestCount); +} + +function assertProviderThreadResumeSemantics(transcript: ProviderReplayTranscript) { + if (transcript.scenario !== "provider_thread_resume") { + return; + } + + const startThreadId = readString( + findProtocolEntry(transcript, "emit_inbound", "thread/start").frame, + ["result", "thread", "id"], + ); + const resumeRequestedThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "thread/resume").frame, + ["params", "threadId"], + ); + const resumedThreadFrame = findProtocolEntry(transcript, "emit_inbound", "thread/resume").frame; + const resumedThreadId = readString(resumedThreadFrame, ["result", "thread", "id"]); + const resumedTurns = readArray(resumedThreadFrame, ["result", "thread", "turns"]); + const secondTurnThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "turn/start", 1).frame, + ["params", "threadId"], + ); + const texts = agentMessageTexts(transcript); + const secondFinalText = texts[1] ?? ""; + + assert.equal( + resumeRequestedThreadId, + startThreadId, + "thread/resume must request the provider thread created by thread/start", + ); + assert.equal( + resumedThreadId, + startThreadId, + "thread/resume must return the same provider thread id", + ); + assert.equal( + secondTurnThreadId, + startThreadId, + "turn after resume must run on the resumed provider thread", + ); + assert.isAtLeast(resumedTurns.length, 1, "thread/resume response must include prior turns"); + + const resumedFirstTurnItems = readArray(resumedTurns[0], ["items"]); + const resumedFirstTurnAgentText = resumedFirstTurnItems + .filter(isRecord) + .filter((item) => item.type === "agentMessage") + .map((item) => item.text) + .find((text): text is string => typeof text === "string"); + + assert.equal( + resumedFirstTurnAgentText, + PROVIDER_THREAD_RESUME_FIRST_FINAL, + "thread/resume response must hydrate the prior assistant answer", + ); + assert.include( + secondFinalText, + PROVIDER_THREAD_RESUME_FIRST_FINAL, + "second turn must demonstrate access to resumed conversation history", + ); + assert.include( + secondFinalText, + PROVIDER_THREAD_RESUME_SECOND_FINAL, + "second turn must include its own completion marker", + ); +} + +function assertContinuedForkSemantics(transcript: ProviderReplayTranscript) { + if (transcript.scenario !== "thread_fork_native_continue") { + return; + } + + const sourceThreadId = readString( + findProtocolEntry(transcript, "emit_inbound", "thread/start").frame, + ["result", "thread", "id"], + ); + const forkRequestThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "thread/fork").frame, + ["params", "threadId"], + ); + const forkThreadId = readString( + findProtocolEntry(transcript, "emit_inbound", "thread/fork").frame, + ["result", "thread", "id"], + ); + const firstForkTurnThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "turn/start", 1).frame, + ["params", "threadId"], + ); + const secondForkTurnThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "turn/start", 2).frame, + ["params", "threadId"], + ); + const recallPrompt = readString( + findProtocolEntry(transcript, "expect_outbound", "turn/start", 2).frame, + ["params", "input", 0, "text"], + ); + + assert.equal(forkRequestThreadId, sourceThreadId); + assert.notEqual(forkThreadId, sourceThreadId); + assert.equal(firstForkTurnThreadId, forkThreadId); + assert.equal(secondForkTurnThreadId, forkThreadId); + assert.notInclude(recallPrompt, THREAD_FORK_NATIVE_CONTINUE_SOURCE_MARKER); + assert.notInclude(recallPrompt, THREAD_FORK_NATIVE_CONTINUE_FORK_MARKER); + const texts = agentMessageTexts(transcript); + for (const expected of [ + "source marker stored", + "fork marker stored", + THREAD_FORK_NATIVE_CONTINUE_RECALL, + ]) { + assert.include(texts, expected); + } + assert.equal( + texts.at(-1), + THREAD_FORK_NATIVE_CONTINUE_RECALL, + "the second fork-local turn must recall both the inherited source marker and fork-local marker", + ); +} + +function assertSiblingForkSemantics(transcript: ProviderReplayTranscript) { + if (transcript.scenario !== "thread_fork_native_siblings") { + return; + } + + const sourceThreadId = readString( + findProtocolEntry(transcript, "emit_inbound", "thread/start").frame, + ["result", "thread", "id"], + ); + const firstForkFrame = findProtocolEntry(transcript, "emit_inbound", "thread/fork", 0).frame; + const secondForkFrame = findProtocolEntry(transcript, "emit_inbound", "thread/fork", 1).frame; + const firstForkId = readString(firstForkFrame, ["result", "thread", "id"]); + const secondForkId = readString(secondForkFrame, ["result", "thread", "id"]); + const firstForkParentId = readString(firstForkFrame, ["result", "thread", "forkedFromId"]); + const secondForkParentId = readString(secondForkFrame, ["result", "thread", "forkedFromId"]); + const firstForkTurnThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "turn/start", 1).frame, + ["params", "threadId"], + ); + const secondForkTurnThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "turn/start", 2).frame, + ["params", "threadId"], + ); + const texts = agentMessageTexts(transcript); + const firstRecall = texts[1]?.replace(/\s*\|\s*/u, "|") ?? ""; + const secondRecall = texts[2]?.replace(/\s*\|\s*/u, "|") ?? ""; + + assert.equal(firstForkParentId, sourceThreadId); + assert.equal(secondForkParentId, sourceThreadId); + assert.notEqual(firstForkId, secondForkId); + assert.equal(firstForkTurnThreadId, firstForkId); + assert.equal(secondForkTurnThreadId, secondForkId); + assert.equal(firstRecall, "sibling-source-8R3D|sibling-first-5L2P"); + assert.notInclude(firstRecall, "sibling-second-9N6C"); + assert.equal(secondRecall, "sibling-source-8R3D|sibling-second-9N6C"); + assert.notInclude(secondRecall, "sibling-first-5L2P"); +} + +function assertMergeBackSemantics(transcript: ProviderReplayTranscript) { + if (transcript.scenario !== "thread_merge_back_continue") { + return; + } + + const sourceThreadId = readString( + findProtocolEntry(transcript, "emit_inbound", "thread/start").frame, + ["result", "thread", "id"], + ); + const forkThreadId = readString( + findProtocolEntry(transcript, "emit_inbound", "thread/fork").frame, + ["result", "thread", "id"], + ); + const mergeThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "turn/start", 2).frame, + ["params", "threadId"], + ); + const recallFrame = findProtocolEntry(transcript, "expect_outbound", "turn/start", 3).frame; + const recallThreadId = readString(recallFrame, ["params", "threadId"]); + const recallPrompt = readString(recallFrame, ["params", "input", 0, "text"]); + const texts = agentMessageTexts(transcript); + + assert.notEqual(forkThreadId, sourceThreadId); + assert.equal(mergeThreadId, sourceThreadId); + assert.equal(recallThreadId, sourceThreadId); + assert.notInclude(recallPrompt, THREAD_MERGE_BACK_SOURCE_MARKER); + assert.notInclude(recallPrompt, THREAD_MERGE_BACK_FORK_MARKER); + assert.equal(texts.at(-1)?.replace(/\s*\|\s*/u, "|"), THREAD_MERGE_BACK_RECALL); +} + +function assertSiblingMergeBackSemantics(transcript: ProviderReplayTranscript) { + if (transcript.scenario !== "thread_merge_back_siblings") { + return; + } + + const sourceThreadId = readString( + findProtocolEntry(transcript, "emit_inbound", "thread/start").frame, + ["result", "thread", "id"], + ); + const firstForkId = readString( + findProtocolEntry(transcript, "emit_inbound", "thread/fork", 0).frame, + ["result", "thread", "id"], + ); + const secondForkId = readString( + findProtocolEntry(transcript, "emit_inbound", "thread/fork", 1).frame, + ["result", "thread", "id"], + ); + const firstMergeThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "turn/start", 3).frame, + ["params", "threadId"], + ); + const secondMergeThreadId = readString( + findProtocolEntry(transcript, "expect_outbound", "turn/start", 4).frame, + ["params", "threadId"], + ); + const recallFrame = findProtocolEntry(transcript, "expect_outbound", "turn/start", 5).frame; + const recallPrompt = readString(recallFrame, ["params", "input", 0, "text"]); + const finalText = agentMessageTexts(transcript) + .at(-1) + ?.replace(/\s*\|\s*/gu, "|"); + + assert.notEqual(firstForkId, secondForkId); + assert.equal(firstMergeThreadId, sourceThreadId); + assert.equal(secondMergeThreadId, sourceThreadId); + assert.notInclude(recallPrompt, THREAD_MERGE_BACK_SIBLINGS_SOURCE_MARKER); + assert.notInclude(recallPrompt, THREAD_MERGE_BACK_SIBLINGS_FIRST_MARKER); + assert.notInclude(recallPrompt, THREAD_MERGE_BACK_SIBLINGS_SECOND_MARKER); + assert.equal(finalText, THREAD_MERGE_BACK_SIBLINGS_RECALL); +} + +describe("Codex replay fixtures", () => { + it.effect("loads every current Codex fixture as a codex app-server replay transcript", () => + Effect.gen(function* () { + for (const fixture of CODEX_REPLAY_FIXTURES) { + const transcript = yield* readTranscript(fixture.transcriptFile); + const codexTranscript = yield* decodeCodexTranscript(transcript); + const first = transcript.entries[0]; + + assert.equal(codexTranscript.provider, "codex"); + assert.equal(codexTranscript.protocol, "codex.app-server"); + assert.equal(codexTranscript.scenario, fixture.scenario); + assert.deepEqual(codexTranscript.entries.at(-1), { + type: "runtime_exit", + status: "success", + }); + assert.equal(first?.type, "expect_outbound"); + if (first?.type !== "expect_outbound") { + throw new Error(`Expected ${fixture.scenario} to start with initialize outbound frame.`); + } + assert.equal(first.label, "initialize"); + + assertScenarioExpectations(transcript); + assertProviderThreadResumeSemantics(transcript); + assertContinuedForkSemantics(transcript); + assertSiblingForkSemantics(transcript); + assertMergeBackSemantics(transcript); + assertSiblingMergeBackSemantics(transcript); + } + }), + ); + + it.effect("covers the expected replay suite exactly", () => + Effect.gen(function* () { + const transcripts = yield* Effect.forEach(CODEX_REPLAY_FIXTURES, (fixture) => + readTranscript(fixture.transcriptFile), + ); + + assert.deepEqual( + transcripts.map((transcript) => transcript.scenario).toSorted(), + Object.keys(scenarioExpectations).toSorted(), + ); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/DeterministicRuntime.ts b/apps/server/src/orchestration-v2/testkit/DeterministicRuntime.ts new file mode 100644 index 00000000000..2c2187d64a7 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/DeterministicRuntime.ts @@ -0,0 +1,13 @@ +import * as Effect from "effect/Effect"; +import * as Random from "effect/Random"; +import { TestClock } from "effect/testing"; + +export function provideDeterministicTestRuntime( + effect: Effect.Effect, + options: { readonly randomSeed?: number } = {}, +) { + return effect.pipe( + Effect.provide(TestClock.layer()), + Random.withSeed(options.randomSeed ?? 0x1234_5678), + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.contract.test.ts b/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.contract.test.ts new file mode 100644 index 00000000000..a82947ba958 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.contract.test.ts @@ -0,0 +1,122 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { OrchestrationV2Command, ProviderInstanceId } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Schema from "effect/Schema"; + +import { layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts"; +import { ORCHESTRATOR_REPLAY_FIXTURES } from "./fixtures/index.ts"; +import { materializeFixtureInput } from "./fixtures/shared.ts"; +import { decodeProviderReplayNdjson } from "./ReplayTranscriptNdjson.ts"; + +const decodeCommand = Schema.decodeUnknownEffect(OrchestrationV2Command); +const readTranscript = Effect.fn("readOrchestratorReplayContractTranscript")(function* (file: URL) { + const fs = yield* FileSystem.FileSystem; + const text = yield* fs.readFileString(decodeURIComponent(file.pathname)); + return yield* decodeProviderReplayNdjson(text); +}, Effect.provide(NodeServices.layer)); + +function assertUnique(values: ReadonlyArray, label: string) { + assert.deepEqual(new Set(values).size, values.length, `${label} must be unique`); +} + +describe("orchestrator replay fixture contract", () => { + it.effect( + "defines one stable input and provider-specific replay/output contracts per scenario", + () => + Effect.gen(function* () { + assertUnique( + ORCHESTRATOR_REPLAY_FIXTURES.map((fixture) => fixture.name), + "fixture names", + ); + + for (const fixture of ORCHESTRATOR_REPLAY_FIXTURES) { + assert.isAtLeast(fixture.providers.length, 1, `${fixture.name} must have providers`); + assertUnique( + fixture.providers.map((provider) => provider.driver), + `${fixture.name} provider variants`, + ); + + for (const provider of fixture.providers) { + const transcript = yield* readTranscript(provider.transcriptFile); + const materialized = yield* materializeFixtureInput({ + scenario: fixture.name, + fixtureInput: fixture.buildInput(), + driver: provider.driver, + modelSelection: provider.modelSelection, + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + const firstCommand = materialized.commands[0]; + + assert.equal(transcript.scenario, fixture.name); + if (provider.driver === "acpRegistry") { + assert.include( + ["acpRegistry", "grok"], + transcript.provider, + "ACP Registry may retarget protocol-standard Grok ACP evidence", + ); + } else { + assert.equal(transcript.provider, provider.driver); + } + assert.equal( + provider.modelSelection.instanceId, + ProviderInstanceId.make(provider.driver), + ); + assert.isDefined(materialized.projectionThreadIds[0]); + assert.equal(firstCommand?.type, "thread.create"); + if (firstCommand?.type !== "thread.create") { + throw new Error(`${fixture.name}/${provider.driver} must start with thread.create`); + } + assert.equal(firstCommand.threadId, materialized.projectionThreadIds[0]); + assert.equal(materialized.commands.length, fixture.buildInput().steps.length + 1); + assert.isAtLeast(materialized.steps.length, materialized.commands.length); + assert.equal(typeof provider.assertOutput, "function"); + + assertUnique( + materialized.commands.map((command) => command.commandId), + `${fixture.name}/${provider.driver} command IDs`, + ); + + for (const command of materialized.commands) { + yield* decodeCommand(command); + } + + for (const command of materialized.commands) { + assert.isTrue( + materialized.steps.some( + (step) => + (step.type === "dispatch" && step.command === command) || + (step.type === "respond_to_next_runtime_request" && + step.commandId === command.commandId), + ), + `${fixture.name}/${provider.driver} command ${command.commandId} must appear in the timeline`, + ); + } + } + } + }), + ); + + it.effect("keeps Codex fixture transcripts at the codex app-server boundary", () => + Effect.gen(function* () { + for (const fixture of ORCHESTRATOR_REPLAY_FIXTURES) { + for (const provider of fixture.providers.filter((entry) => entry.driver === "codex")) { + const transcript = yield* readTranscript(provider.transcriptFile); + const first = transcript.entries[0]; + const last = transcript.entries.at(-1); + + assert.equal(transcript.protocol, "codex.app-server"); + assert.equal(first?.type, "expect_outbound"); + if (first?.type === "expect_outbound") { + assert.equal(first.label, "initialize"); + } + assert.deepEqual(last, { + type: "runtime_exit", + status: "success", + }); + } + } + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts new file mode 100644 index 00000000000..a0c1a4dd76d --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts @@ -0,0 +1,202 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import type { OrchestrationV2DomainEvent, ProviderReplayTranscript } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; + +import { ClaudeOrchestratorReplayHarness } from "../Adapters/ClaudeAdapterV2.testkit.ts"; +import { CodexOrchestratorReplayHarness } from "../Adapters/CodexAdapterV2.testkit.ts"; +import { CursorOrchestratorReplayHarness } from "../Adapters/CursorAdapterV2.testkit.ts"; +import { AcpRegistryOrchestratorReplayHarness } from "../Adapters/AcpRegistryAdapterV2.testkit.ts"; +import { GrokOrchestratorReplayHarness } from "../Adapters/GrokAdapterV2.testkit.ts"; +import { OpenCodeOrchestratorReplayHarness } from "../Adapters/OpenCodeAdapterV2.testkit.ts"; +import { layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts"; +import { ORCHESTRATOR_REPLAY_FIXTURES } from "./fixtures/index.ts"; +import { messageRestartInput } from "./fixtures/message_steering/input.ts"; +import { + materializeFixtureInput, + type OrchestratorFixtureInput, + type ProviderOrchestratorReplayVariant, +} from "./fixtures/shared.ts"; +import { + runOrchestratorV2ProviderReplayScenario, + type OrchestratorV2ProviderReplayHarness, +} from "./ProviderReplayHarness.ts"; +import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts"; +import { decodeProviderReplayNdjson } from "./ReplayTranscriptNdjson.ts"; + +const readTranscript = Effect.fn("readOrchestratorReplayTranscript")(function* (file: URL) { + const fs = yield* FileSystem.FileSystem; + const text = yield* fs.readFileString(decodeURIComponent(file.pathname)); + return yield* decodeProviderReplayNdjson(text); +}, Effect.provide(NodeServices.layer)); + +function normalizeTestError(cause: unknown): Error { + return cause instanceof Error ? cause : new Error(String(cause)); +} + +function isStreamingAssistantEvent(event: OrchestrationV2DomainEvent): boolean { + switch (event.type) { + case "node.updated": + return event.payload.kind === "assistant_message" && event.payload.status === "running"; + case "message.updated": + return event.payload.role === "assistant" && event.payload.streaming; + case "turn-item.updated": + return event.payload.type === "assistant_message" && event.payload.streaming; + default: + return false; + } +} + +const runFixtureProvider = Effect.fn("runOrchestratorReplayFixture")(function* < + Transcript extends ProviderReplayTranscript, + Error, +>(input: { + readonly fixtureName: string; + readonly buildInput: () => OrchestratorFixtureInput; + readonly driver: ProviderOrchestratorReplayVariant; + readonly harness: OrchestratorV2ProviderReplayHarness; + readonly enableAssistantStreaming?: boolean; +}) { + const rawTranscript = yield* readTranscript(input.driver.transcriptFile); + const transcript = yield* input.harness.decodeTranscript(rawTranscript); + const workspace = yield* checkpointWorkspace(input.fixtureName); + const materialized = yield* materializeFixtureInput({ + scenario: input.fixtureName, + fixtureInput: input.buildInput(), + driver: input.driver.driver, + modelSelection: input.driver.modelSelection, + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + const scenario = { + name: `${input.fixtureName}/${input.driver.driver}`, + transcript, + commands: materialized.commands, + steps: materialized.steps, + projectionThreadIds: materialized.projectionThreadIds, + runtimePolicyOverride: { + ...input.driver.runtimePolicyOverride, + cwd: workspace, + }, + }; + + const result = yield* runOrchestratorV2ProviderReplayScenario(scenario, input.harness, { + enableAssistantStreaming: input.enableAssistantStreaming ?? false, + }).pipe(provideDeterministicTestRuntime); + input.driver.assertOutput(result, transcript); + if (input.enableAssistantStreaming !== true) { + assert.isFalse( + result.domainEvents.some(isStreamingAssistantEvent), + "buffered delivery must not persist streaming assistant artifacts", + ); + } + const projectionThreadId = materialized.projectionThreadIds[0]; + assert.isDefined(projectionThreadId); + const projection = result.projections.get(projectionThreadId); + assert.isDefined(projection); + const latestRun = projection.runs.at(-1); + assert.deepEqual(latestRun?.modelSelection, input.driver.modelSelection); + return result; +}); + +function runFixtureProviderWithRegisteredHarness(input: { + readonly fixtureName: string; + readonly buildInput: () => OrchestratorFixtureInput; + readonly driver: ProviderOrchestratorReplayVariant; + readonly enableAssistantStreaming?: boolean; +}) { + switch (input.driver.driver) { + case "codex": + return runFixtureProvider({ + ...input, + harness: CodexOrchestratorReplayHarness, + }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); + case "claudeAgent": + return runFixtureProvider({ + ...input, + harness: ClaudeOrchestratorReplayHarness, + }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); + case "cursor": + return runFixtureProvider({ + ...input, + harness: CursorOrchestratorReplayHarness, + }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); + case "grok": + return runFixtureProvider({ + ...input, + harness: GrokOrchestratorReplayHarness, + }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); + case "acpRegistry": + return runFixtureProvider({ + ...input, + harness: AcpRegistryOrchestratorReplayHarness, + }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); + case "opencode": + return runFixtureProvider({ + ...input, + harness: OpenCodeOrchestratorReplayHarness, + }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); + default: + return Effect.die( + new Error(`No replay harness registered for provider ${input.driver.driver}.`), + ); + } +} + +describe("orchestrator replay fixtures", () => { + for (const fixture of ORCHESTRATOR_REPLAY_FIXTURES) { + for (const provider of fixture.providers) { + it.effect( + `runs ${fixture.name}/${provider.driver} through OrchestratorV2 using deterministic replay`, + () => + runFixtureProviderWithRegisteredHarness({ + fixtureName: fixture.name, + buildInput: fixture.buildInput, + driver: provider, + }), + ); + } + } + + const steeringFixture = ORCHESTRATOR_REPLAY_FIXTURES.find( + (fixture) => fixture.name === "message_steering", + ); + const cursorSteeringProvider = steeringFixture?.providers.find( + (provider) => provider.driver === "cursor", + ); + if (cursorSteeringProvider !== undefined) { + it.effect("executes explicit Cursor restart_active through the recorded SDK boundary", () => + runFixtureProviderWithRegisteredHarness({ + fixtureName: "message_steering", + buildInput: messageRestartInput, + driver: cursorSteeringProvider, + }), + ); + } + + const simpleFixture = ORCHESTRATOR_REPLAY_FIXTURES.find((fixture) => fixture.name === "simple"); + const simpleCursorProvider = simpleFixture?.providers.find( + (provider) => provider.driver === "cursor", + ); + if (simpleFixture !== undefined && simpleCursorProvider !== undefined) { + it.effect("streams Cursor assistant artifacts only when streaming is enabled", () => + Effect.gen(function* () { + const result = yield* runFixtureProviderWithRegisteredHarness({ + fixtureName: "simple-cursor-streaming", + buildInput: simpleFixture.buildInput, + driver: simpleCursorProvider, + enableAssistantStreaming: true, + }); + + assert.deepEqual( + Array.from( + new Set( + result.domainEvents.filter(isStreamingAssistantEvent).map((event) => event.type), + ), + ).toSorted(), + ["message.updated", "node.updated", "turn-item.updated"], + ); + }), + ); + } +}); diff --git a/apps/server/src/orchestration-v2/testkit/OrchestratorReplayRecovery.integration.test.ts b/apps/server/src/orchestration-v2/testkit/OrchestratorReplayRecovery.integration.test.ts new file mode 100644 index 00000000000..ad7cbc662f8 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/OrchestratorReplayRecovery.integration.test.ts @@ -0,0 +1,282 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import * as CodexReplay from "effect-codex-app-server/replay"; +import { ProviderDriverKind } from "@t3tools/contracts"; + +import { + CodexOrchestratorReplayHarness, + makeCodexProviderAdapterRegistryReplayLayer, +} from "../Adapters/CodexAdapterV2.testkit.ts"; +import { + type CursorAgentSdkReplayTranscript, + CursorOrchestratorReplayHarness, + makeCursorAgentSdkReplayRunner, + makeCursorProviderAdapterRegistryReplayLayer, +} from "../Adapters/CursorAdapterV2.testkit.ts"; +import { layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { makeSqlitePersistenceLive } from "../../persistence/Layers/Sqlite.ts"; +import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts"; +import { + CODEX_MODEL_SELECTION, + CURSOR_MODEL_SELECTION, + materializeFixtureInput, + type MaterializedOrchestratorFixtureInput, + PROVIDER_THREAD_RESUME_FIRST_PROMPT, + PROVIDER_THREAD_RESUME_SECOND_PROMPT, +} from "./fixtures/shared.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertConversationMessageRoles, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + projectionFor, +} from "./fixtures/shared.ts"; +import { runOrchestratorV2ProviderReplayScenario } from "./ProviderReplayHarness.ts"; +import { decodeProviderReplayNdjson } from "./ReplayTranscriptNdjson.ts"; + +const FIRST_FINAL = "provider thread resume fixture first turn complete"; +const SECOND_FINAL = "provider thread resume fixture second turn complete"; + +const decodeCodexTranscript = Schema.decodeUnknownEffect( + CodexReplay.CodexAppServerReplayTranscript, +); +const readRawTranscript = Effect.fn("readRecoveryTranscript")(function* (file: URL) { + const fs = yield* FileSystem.FileSystem; + const text = yield* fs.readFileString(decodeURIComponent(file.pathname)); + return yield* decodeProviderReplayNdjson(text); +}); +const readCodexTranscript = Effect.fn("readCodexRecoveryTranscript")(function* () { + const transcript = yield* readRawTranscript( + new URL("./fixtures/provider_thread_resume/codex_transcript.ndjson", import.meta.url), + ); + return yield* decodeCodexTranscript(transcript); +}); +const readCursorTranscript = Effect.fn("readCursorRecoveryTranscript")(function* () { + const transcript = yield* readRawTranscript( + new URL("./fixtures/provider_thread_resume/cursor_transcript.ndjson", import.meta.url), + ); + return yield* CursorOrchestratorReplayHarness.decodeTranscript(transcript); +}); + +function splitAfterFirstIdle(materialized: MaterializedOrchestratorFixtureInput) { + const splitIndex = materialized.steps.findIndex((step) => step.type === "await_thread_idle"); + if (splitIndex < 0) { + throw new Error("Expected fixture to contain await_thread_idle after the first turn."); + } + + const phase1Steps = materialized.steps.slice(0, splitIndex + 1); + const phase2Steps = materialized.steps.slice(splitIndex + 1); + return { + phase1Steps, + phase2Steps, + phase1Commands: phase1Steps.flatMap((step) => (step.type === "dispatch" ? [step.command] : [])), + phase2Commands: phase2Steps.flatMap((step) => (step.type === "dispatch" ? [step.command] : [])), + }; +} + +const runCursorRecovery = Effect.fn("runCursorRecovery")(function* (input: { + readonly transcript: CursorAgentSdkReplayTranscript; + readonly runner: ReturnType; +}) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const tempDir = yield* Effect.acquireRelease( + fs.makeTempDirectory({ + prefix: "t3-orchestration-v2-cursor-recovery-", + }), + (directory) => fs.remove(directory, { recursive: true, force: true }).pipe(Effect.orDie), + ); + yield* fs.makeDirectory(tempDir, { recursive: true }); + const dbPath = path.join(tempDir, "state.sqlite"); + const materialized = yield* materializeFixtureInput({ + scenario: "provider_thread_resume", + fixtureInput: { + steps: [ + { type: "message", text: PROVIDER_THREAD_RESUME_FIRST_PROMPT }, + { type: "message", text: PROVIDER_THREAD_RESUME_SECOND_PROMPT }, + ], + }, + driver: ProviderDriverKind.make("cursor"), + modelSelection: CURSOR_MODEL_SELECTION, + }); + const { phase1Commands, phase1Steps, phase2Commands, phase2Steps } = + splitAfterFirstIdle(materialized); + const options = { + databaseLayer: makeSqlitePersistenceLive(dbPath).pipe(Layer.provide(NodeServices.layer)), + }; + const harness = { + ...CursorOrchestratorReplayHarness, + makeProviderAdapterRegistryLayer: () => + makeCursorProviderAdapterRegistryReplayLayer(input.transcript, { + runner: input.runner, + assertCompleteOnFinalize: false, + }), + }; + + yield* Effect.scoped( + runOrchestratorV2ProviderReplayScenario( + { + name: "provider_thread_resume/cursor:first-runtime", + transcript: input.transcript, + commands: phase1Commands, + steps: phase1Steps, + projectionThreadIds: materialized.projectionThreadIds, + runtimePolicyOverride: { cwd: tempDir }, + }, + harness, + options, + ), + ); + + const result = yield* Effect.scoped( + runOrchestratorV2ProviderReplayScenario( + { + name: "provider_thread_resume/cursor:second-runtime", + transcript: input.transcript, + commands: phase2Commands, + steps: phase2Steps, + projectionThreadIds: materialized.projectionThreadIds, + runtimePolicyOverride: { cwd: tempDir }, + }, + harness, + options, + ), + ); + + assertBaseProjection({ + result, + transcript: input.transcript, + runCount: 2, + runStatuses: ["completed", "completed"], + }); + const projection = projectionFor(result, input.transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertRunOrdinals(projection, [1, 2]); + assertConversationMessageRoles(projection, ["user", "assistant", "user", "assistant"]); + assertTurnItemTypes(projection, ["user_message", "assistant_message"]); + assertUserMessagesInclude(projection, [ + PROVIDER_THREAD_RESUME_FIRST_PROMPT, + PROVIDER_THREAD_RESUME_SECOND_PROMPT, + ]); + assertAssistantTextIncludes(projection, FIRST_FINAL); + assertAssistantTextIncludes(projection, SECOND_FINAL); + assert.lengthOf(projection.providerThreads, 1); +}); + +describe("orchestrator replay recovery", () => { + it.effect( + "resumes a provider-native Codex thread after recreating the orchestrator runtime", + () => + Effect.scoped( + Effect.gen(function* () { + const transcript = yield* readCodexTranscript(); + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const tempDir = yield* Effect.acquireRelease( + fs.makeTempDirectory({ + prefix: "t3-orchestration-v2-recovery-", + }), + (directory) => + fs.remove(directory, { recursive: true, force: true }).pipe(Effect.orDie), + ); + yield* fs.makeDirectory(tempDir, { recursive: true }); + const dbPath = path.join(tempDir, "state.sqlite"); + const driver = yield* CodexReplay.makeReplayDriver(transcript); + const materialized = yield* materializeFixtureInput({ + scenario: "provider_thread_resume", + fixtureInput: { + steps: [ + { type: "message", text: PROVIDER_THREAD_RESUME_FIRST_PROMPT }, + { type: "message", text: PROVIDER_THREAD_RESUME_SECOND_PROMPT }, + ], + }, + driver: ProviderDriverKind.make("codex"), + modelSelection: CODEX_MODEL_SELECTION, + }); + const { phase1Commands, phase1Steps, phase2Commands, phase2Steps } = + splitAfterFirstIdle(materialized); + + const harness = { + ...CodexOrchestratorReplayHarness, + makeProviderAdapterRegistryLayer: () => + makeCodexProviderAdapterRegistryReplayLayer({ transcript, driver }), + }; + const options = { + databaseLayer: makeSqlitePersistenceLive(dbPath).pipe( + Layer.provide(NodeServices.layer), + ), + }; + + yield* runOrchestratorV2ProviderReplayScenario( + { + name: "provider_thread_resume/codex:first-runtime", + transcript, + commands: phase1Commands, + steps: phase1Steps, + projectionThreadIds: materialized.projectionThreadIds, + }, + harness, + options, + ); + + const result = yield* runOrchestratorV2ProviderReplayScenario( + { + name: "provider_thread_resume/codex:second-runtime", + transcript, + commands: phase2Commands, + steps: phase2Steps, + projectionThreadIds: materialized.projectionThreadIds, + }, + harness, + options, + ); + + assertBaseProjection({ + result, + transcript, + runCount: 2, + runStatuses: ["completed", "completed"], + }); + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertRunOrdinals(projection, [1, 2]); + assertConversationMessageRoles(projection, ["user", "assistant", "user", "assistant"]); + assertTurnItemTypes(projection, ["user_message", "assistant_message"]); + assertUserMessagesInclude(projection, [ + PROVIDER_THREAD_RESUME_FIRST_PROMPT, + PROVIDER_THREAD_RESUME_SECOND_PROMPT, + ]); + assertAssistantTextIncludes(projection, FIRST_FINAL); + assertAssistantTextIncludes(projection, SECOND_FINAL); + assert.lengthOf(projection.providerThreads, 1); + }).pipe( + provideDeterministicTestRuntime, + Effect.provide(Layer.merge(idAllocatorLayer, NodeServices.layer)), + ), + ), + ); + + it.effect( + "resumes a provider-native Cursor thread after recreating the orchestrator runtime", + () => + Effect.scoped( + Effect.gen(function* () { + const transcript = yield* readCursorTranscript(); + const runner = makeCursorAgentSdkReplayRunner(transcript); + yield* runCursorRecovery({ transcript, runner }); + yield* runner.assertComplete; + }).pipe( + provideDeterministicTestRuntime, + Effect.provide(Layer.merge(idAllocatorLayer, NodeServices.layer)), + ), + ), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/OrchestratorScenario.ts b/apps/server/src/orchestration-v2/testkit/OrchestratorScenario.ts new file mode 100644 index 00000000000..0b596c0a986 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/OrchestratorScenario.ts @@ -0,0 +1,396 @@ +import type { + OrchestrationV2Command, + OrchestrationV2DomainEvent, + OrchestrationV2RuntimeRequest, + OrchestrationV2Run, + OrchestrationV2ThreadShellSnapshot, + OrchestrationV2StoredEvent, + OrchestrationV2ThreadProjection, + OrchestrationV2TurnItem, + ProviderApprovalDecision, + ProviderUserInputAnswers, + CommandId, + ThreadId, +} from "@t3tools/contracts"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import { TestClock } from "effect/testing"; + +import { OrchestratorV2, type OrchestratorV2Error } from "../Orchestrator.ts"; + +export type OrchestratorV2ScenarioStep = + | { + readonly type: "dispatch"; + readonly command: OrchestrationV2Command; + readonly await?: boolean; + readonly key?: string; + } + | { + readonly type: "advance_clock"; + readonly duration: Duration.Input; + } + | { + readonly type: "await"; + readonly key: string; + } + | { + readonly type: "await_all"; + } + | { + readonly type: "await_thread_idle"; + readonly threadId: ThreadId; + } + | { + readonly type: "await_run_steerable"; + readonly threadId: ThreadId; + readonly runId: OrchestrationV2Run["id"]; + } + | { + readonly type: "await_run_turn_item"; + readonly threadId: ThreadId; + readonly runId: OrchestrationV2Run["id"]; + readonly itemType: OrchestrationV2TurnItem["type"]; + } + | { + readonly type: "respond_to_next_runtime_request"; + readonly threadId: ThreadId; + readonly commandId: CommandId; + readonly decision?: ProviderApprovalDecision; + readonly answers?: ProviderUserInputAnswers; + }; + +export interface OrchestratorV2Scenario { + readonly name: string; + readonly commands: ReadonlyArray; + readonly steps?: ReadonlyArray; + readonly projectionThreadIds?: ReadonlyArray; +} + +export interface OrchestratorV2ScenarioResult { + readonly storedEvents: ReadonlyArray; + readonly domainEvents: ReadonlyArray; + readonly projections: ReadonlyMap; + readonly shellSnapshot: OrchestrationV2ThreadShellSnapshot; +} + +export class OrchestratorV2ScenarioStepError extends Schema.TaggedErrorClass()( + "OrchestratorV2ScenarioStepError", + { + scenario: Schema.String, + step: Schema.String, + }, +) { + override get message(): string { + return `Invalid orchestrator scenario step ${this.step} in ${this.scenario}.`; + } +} + +function commandThreadIds(command: OrchestrationV2Command): ReadonlyArray { + switch (command.type) { + case "thread.create": + case "thread.archive": + case "thread.unarchive": + case "thread.delete": + case "thread.metadata.update": + case "thread.runtime-mode.set": + case "thread.interaction-mode.set": + case "thread.model-selection.set": + case "provider-session.detach": + case "message.dispatch": + case "run.interrupt": + case "queued-message.promote-to-steer": + case "queued-run.reorder": + case "runtime-request.respond": + case "checkpoint.rollback": + case "provider.switch": + return [command.threadId]; + case "delegated_task.request": + return [command.parentThreadId]; + case "thread.fork": + case "thread.merge_back": + return [command.sourceThreadId, command.targetThreadId]; + } +} + +function scenarioSteps( + scenario: OrchestratorV2Scenario, +): ReadonlyArray { + return ( + scenario.steps ?? + scenario.commands.map((command) => ({ + type: "dispatch" as const, + command, + await: true, + })) + ); +} + +function scenarioCommands(scenario: OrchestratorV2Scenario): ReadonlyArray { + return scenarioSteps(scenario).flatMap((step) => + step.type === "dispatch" ? [step.command] : [], + ); +} + +const findPendingRuntimeRequest = (projection: OrchestrationV2ThreadProjection) => + projection.runtimeRequests.find((request) => request.status === "pending"); + +const hasActiveRun = (projection: OrchestrationV2ThreadProjection) => + projection.runs.some((run) => ["queued", "starting", "running", "waiting"].includes(run.status)); + +const SCENARIO_WAIT_ATTEMPTS = 10_000; + +const yieldToRuntime = Effect.yieldNow.pipe( + Effect.andThen( + Effect.promise( + () => + new Promise((resolve) => { + setImmediate(resolve); + }), + ), + ), +); + +function collectProjectionThreadIds(scenario: OrchestratorV2Scenario): ReadonlyArray { + if (scenario.projectionThreadIds) { + return scenario.projectionThreadIds; + } + + const ids = new Set(); + for (const command of scenarioCommands(scenario)) { + for (const threadId of commandThreadIds(command)) { + ids.add(threadId); + } + } + return Array.from(ids); +} + +export function runOrchestratorV2Scenario( + scenario: OrchestratorV2Scenario, +): Effect.Effect< + OrchestratorV2ScenarioResult, + OrchestratorV2Error | OrchestratorV2ScenarioStepError, + OrchestratorV2 +> { + return Effect.scoped( + Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + const storedEventGroups: Array> = []; + const observedStoredEvents = yield* Ref.make>([]); + yield* orchestrator.streamStoredEvents.pipe( + Stream.runForEach((event) => + Ref.update(observedStoredEvents, (existing) => [...existing, event]), + ), + Effect.forkScoped, + ); + const backgroundDispatches = new Map< + string, + Fiber.Fiber, OrchestratorV2Error> + >(); + let anonymousBackgroundDispatchIndex = 0; + + const awaitDispatch = (key: string) => + Effect.gen(function* () { + const fiber = backgroundDispatches.get(key); + if (!fiber) { + return yield* new OrchestratorV2ScenarioStepError({ + scenario: scenario.name, + step: `await:${key}`, + }); + } + const events = yield* Fiber.join(fiber); + backgroundDispatches.delete(key); + storedEventGroups.push(events); + }); + + const waitForPendingRuntimeRequest = ( + threadId: ThreadId, + attemptsRemaining = SCENARIO_WAIT_ATTEMPTS, + ): Effect.Effect< + OrchestrationV2RuntimeRequest, + OrchestratorV2Error | OrchestratorV2ScenarioStepError, + never + > => + Effect.gen(function* () { + const projection = yield* orchestrator.getThreadProjection(threadId); + const request = findPendingRuntimeRequest(projection); + if (request !== undefined) { + return request; + } + if (attemptsRemaining <= 0) { + const runState = projection.runs.map((run) => `${run.id}:${run.status}`).join(","); + return yield* new OrchestratorV2ScenarioStepError({ + scenario: scenario.name, + step: `respond_to_next_runtime_request:${threadId}:runs=${runState}:providerTurns=${projection.providerTurns.length}`, + }); + } + yield* yieldToRuntime; + return yield* waitForPendingRuntimeRequest(threadId, attemptsRemaining - 1); + }); + + const waitForThreadIdle = ( + threadId: ThreadId, + attemptsRemaining = SCENARIO_WAIT_ATTEMPTS, + ): Effect.Effect => + Effect.gen(function* () { + const projection = yield* orchestrator.getThreadProjection(threadId); + if (!hasActiveRun(projection)) { + return; + } + if (attemptsRemaining <= 0) { + const activeRuns = projection.runs + .filter((run) => ["queued", "starting", "running", "waiting"].includes(run.status)) + .map((run) => `${run.id}:${run.status}`) + .join(","); + const pendingRequests = projection.runtimeRequests + .filter((request) => request.status === "pending") + .map((request) => `${request.id}:${request.kind}`) + .join(","); + return yield* new OrchestratorV2ScenarioStepError({ + scenario: scenario.name, + step: `await_thread_idle:${threadId}:runs=${activeRuns}:requests=${pendingRequests}`, + }); + } + yield* yieldToRuntime; + return yield* waitForThreadIdle(threadId, attemptsRemaining - 1); + }); + + const waitForRunSteerable = ( + threadId: ThreadId, + runId: OrchestrationV2Run["id"], + attemptsRemaining = SCENARIO_WAIT_ATTEMPTS, + ): Effect.Effect => + Effect.gen(function* () { + const projection = yield* orchestrator.getThreadProjection(threadId); + const run = projection.runs.find((candidate) => candidate.id === runId); + const providerTurn = projection.providerTurns.find( + (candidate) => + run?.activeAttemptId !== null && + candidate.runAttemptId === run?.activeAttemptId && + candidate.status === "running", + ); + if (run?.status === "running" && providerTurn !== undefined) { + return; + } + if (attemptsRemaining <= 0) { + return yield* new OrchestratorV2ScenarioStepError({ + scenario: scenario.name, + step: `await_run_steerable:${runId}`, + }); + } + yield* yieldToRuntime; + return yield* waitForRunSteerable(threadId, runId, attemptsRemaining - 1); + }); + + const waitForRunTurnItem = ( + threadId: ThreadId, + runId: OrchestrationV2Run["id"], + itemType: OrchestrationV2TurnItem["type"], + attemptsRemaining = SCENARIO_WAIT_ATTEMPTS, + ): Effect.Effect => + Effect.gen(function* () { + const projection = yield* orchestrator.getThreadProjection(threadId); + const hasTurnItem = projection.turnItems.some( + (item) => item.runId === runId && item.type === itemType, + ); + if (hasTurnItem) { + return; + } + if (attemptsRemaining <= 0) { + return yield* new OrchestratorV2ScenarioStepError({ + scenario: scenario.name, + step: `await_run_turn_item:${runId}:${itemType}`, + }); + } + yield* yieldToRuntime; + return yield* waitForRunTurnItem(threadId, runId, itemType, attemptsRemaining - 1); + }); + + for (const step of scenarioSteps(scenario)) { + switch (step.type) { + case "dispatch": { + if (step.await ?? true) { + const result = yield* orchestrator.dispatch(step.command); + storedEventGroups.push(result.storedEvents); + break; + } + + anonymousBackgroundDispatchIndex += 1; + const key = step.key ?? `dispatch:${anonymousBackgroundDispatchIndex}`; + backgroundDispatches.set( + key, + yield* orchestrator.dispatch(step.command).pipe( + Effect.map((result) => result.storedEvents), + Effect.forkScoped, + ), + ); + break; + } + case "advance_clock": + yield* TestClock.adjust(step.duration); + break; + case "await": + yield* awaitDispatch(step.key); + break; + case "await_all": + for (const key of Array.from(backgroundDispatches.keys())) { + yield* awaitDispatch(key); + } + break; + case "await_thread_idle": + yield* waitForThreadIdle(step.threadId); + break; + case "await_run_steerable": + yield* waitForRunSteerable(step.threadId, step.runId); + break; + case "await_run_turn_item": + yield* waitForRunTurnItem(step.threadId, step.runId, step.itemType); + break; + case "respond_to_next_runtime_request": { + const request = yield* waitForPendingRuntimeRequest(step.threadId); + const result = yield* orchestrator.dispatch({ + type: "runtime-request.respond", + commandId: step.commandId, + threadId: step.threadId, + requestId: request.id, + ...(step.decision === undefined ? {} : { decision: step.decision }), + ...(step.answers === undefined ? {} : { answers: step.answers }), + }); + storedEventGroups.push(result.storedEvents); + break; + } + } + } + + for (const key of Array.from(backgroundDispatches.keys())) { + yield* awaitDispatch(key); + } + + const shellSnapshot = yield* orchestrator.getShellSnapshot(); + const projectionThreadIds = new Set(collectProjectionThreadIds(scenario)); + for (const thread of shellSnapshot.threads) { + projectionThreadIds.add(thread.id); + } + const projections = new Map(); + for (const threadId of projectionThreadIds) { + projections.set(threadId, yield* orchestrator.getThreadProjection(threadId)); + } + + yield* Effect.yieldNow; + + const observedEvents = yield* Ref.get(observedStoredEvents); + const storedEvents = ( + observedEvents.length > 0 ? observedEvents : storedEventGroups.flat() + ).toSorted((left, right) => left.sequence - right.sequence); + return { + storedEvents, + domainEvents: storedEvents.map((stored) => stored.event), + projections, + shellSnapshot, + }; + }), + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts new file mode 100644 index 00000000000..056642d2c22 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts @@ -0,0 +1,369 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import type { ProviderDriverKind, ProviderReplayTranscript } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; +import type * as SqlClient from "effect/unstable/sql/SqlClient"; +import type { MigrationError } from "effect/unstable/sql/Migrator"; +import type { SqlError } from "effect/unstable/sql/SqlError"; + +import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts"; +import { ServerConfig } from "../../config.ts"; +import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; +import { ServerSettingsService } from "../../serverSettings.ts"; +import { layer as mcpSessionRegistryTestLayer } from "../../mcp/McpSessionRegistry.testkit.ts"; +import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts"; +import * as VcsProcess from "../../vcs/VcsProcess.ts"; +import { layer as checkpointCaptureServiceLayer } from "../CheckpointCaptureService.ts"; +import { layer as checkpointServiceLayer } from "../CheckpointService.ts"; +import { layer as checkpointRollbackServiceLayer } from "../CheckpointRollbackService.ts"; +import { layer as commandPolicyLayer } from "../CommandPolicy.ts"; +import { layer as commandReceiptStoreLayer } from "../CommandReceiptStore.ts"; +import { layer as contextHandoffServiceLayer } from "../ContextHandoffService.ts"; +import { layer as effectOutboxLayer } from "../EffectOutbox.ts"; +import { + daemonLayer as effectWorkerDaemonLayer, + executorLayer as effectExecutorLayer, + layer as effectWorkerLayer, +} from "../EffectWorker.ts"; +import { layerFromStores as eventSinkLayer } from "../EventSink.ts"; +import { layer as eventStoreLayer } from "../EventStore.ts"; +import { layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { layer as orchestratorLayer } from "../Orchestrator.ts"; +import { layer as projectionStoreLayer } from "../ProjectionStore.ts"; +import type { OrchestratorV2, OrchestratorV2Error } from "../Orchestrator.ts"; +import { ProviderAdapterRegistryV2 } from "../ProviderAdapterRegistry.ts"; +import { layer as providerEventIngestorLayer } from "../ProviderEventIngestor.ts"; +import { layerWithOptions as providerSessionManagerLayerWithOptions } from "../ProviderSessionManager.ts"; +import { layer as providerSwitchServiceLayer } from "../ProviderSwitchService.ts"; +import { layer as providerTurnControlServiceLayer } from "../ProviderTurnControlService.ts"; +import { layer as providerTurnStartServiceLayer } from "../ProviderTurnStartService.ts"; +import { layer as runExecutionServiceLayer } from "../RunExecutionService.ts"; +import { layer as runFinalizationServiceLayer } from "../RunFinalizationService.ts"; +import { + layer as runtimePolicyLayer, + layerWithOverride as runtimePolicyLayerWithOverride, + type RuntimePolicyV2Override, +} from "../RuntimePolicy.ts"; +import { layer as turnItemPositionStoreLayer } from "../TurnItemPositionStore.ts"; +import { layer as runtimeRequestServiceLayer } from "../RuntimeRequestService.ts"; +import { layer as threadForkServiceLayer } from "../ThreadForkService.ts"; +import { + runOrchestratorV2Scenario, + type OrchestratorV2ScenarioStepError, + type OrchestratorV2Scenario, + type OrchestratorV2ScenarioResult, +} from "./OrchestratorScenario.ts"; + +export function makeReplayServerConfig( + scenario: string, +): Effect.Effect< + ServerConfig["Service"], + PlatformError.PlatformError, + FileSystem.FileSystem | Path.Path +> { + const safeScenario = scenario.replace(/[^a-z0-9_-]+/gi, "-"); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = yield* fs.makeTempDirectory({ + prefix: `t3-orchestration-v2-replay-${safeScenario}-`, + }); + const stateDir = path.join(baseDir, "userdata"); + const logsDir = path.join(stateDir, "logs"); + const providerLogsDir = path.join(logsDir, "provider"); + const terminalLogsDir = path.join(logsDir, "terminals"); + const attachmentsDir = path.join(stateDir, "attachments"); + const worktreesDir = path.join(baseDir, "worktrees"); + const providerStatusCacheDir = path.join(baseDir, "caches"); + + for (const directory of [ + stateDir, + logsDir, + providerLogsDir, + terminalLogsDir, + attachmentsDir, + worktreesDir, + providerStatusCacheDir, + ]) { + yield* fs.makeDirectory(directory, { recursive: true }); + } + + return { + logLevel: "Error", + traceMinLevel: "Info", + traceTimingEnabled: true, + traceBatchWindowMs: 200, + traceMaxBytes: 10 * 1024 * 1024, + traceMaxFiles: 10, + otlpTracesUrl: undefined, + otlpMetricsUrl: undefined, + otlpExportIntervalMs: 10_000, + otlpServiceName: "t3-server", + mode: "web", + port: 0, + host: undefined, + cwd: process.cwd(), + baseDir, + staticDir: undefined, + devUrl: undefined, + noBrowser: false, + startupPresentation: "browser", + tailscaleServeEnabled: false, + tailscaleServePort: 443, + desktopBootstrapToken: undefined, + autoBootstrapProjectFromCwd: false, + logWebSocketEvents: false, + stateDir, + dbPath: path.join(stateDir, "state.sqlite"), + keybindingsConfigPath: path.join(stateDir, "keybindings.json"), + settingsPath: path.join(stateDir, "settings.json"), + providerStatusCacheDir, + worktreesDir, + attachmentsDir, + logsDir, + serverLogPath: path.join(logsDir, "server.log"), + serverTracePath: path.join(logsDir, "server.trace.ndjson"), + providerLogsDir, + providerEventLogPath: path.join(providerLogsDir, "events.log"), + terminalLogsDir, + anonymousIdPath: path.join(stateDir, "anonymous-id"), + environmentIdPath: path.join(stateDir, "environment-id"), + serverRuntimeStatePath: path.join(stateDir, "server-runtime.json"), + secretsDir: path.join(stateDir, "secrets"), + }; + }); +} + +export interface OrchestratorV2ProviderReplayScenario< + Transcript extends ProviderReplayTranscript = ProviderReplayTranscript, +> extends OrchestratorV2Scenario { + readonly transcript: Transcript; + readonly runtimePolicyOverride?: RuntimePolicyV2Override; +} + +export interface OrchestratorV2ProviderReplayHarness< + Transcript extends ProviderReplayTranscript = ProviderReplayTranscript, + Error = never, +> { + readonly driver: ProviderDriverKind; + readonly decodeTranscript: ( + transcript: ProviderReplayTranscript, + ) => Effect.Effect; + readonly makeProviderAdapterRegistryLayer: ( + transcript: Transcript, + ) => Layer.Layer; +} + +export function runOrchestratorV2ProviderReplayScenario< + Transcript extends ProviderReplayTranscript, + Error, +>( + scenario: OrchestratorV2ProviderReplayScenario, + harness: OrchestratorV2ProviderReplayHarness, + options: { + readonly databaseLayer?: Layer.Layer< + SqlClient.SqlClient, + MigrationError | PlatformError.PlatformError | SqlError + >; + readonly enableAssistantStreaming?: boolean; + } = {}, +): Effect.Effect< + OrchestratorV2ScenarioResult, + | OrchestratorV2Error + | OrchestratorV2ScenarioStepError + | Error + | MigrationError + | PlatformError.PlatformError + | SqlError, + never +> { + const layer = makeOrchestratorV2ProviderReplayLayer(scenario, harness, options); + + return runOrchestratorV2Scenario(scenario).pipe(Effect.provide(layer)); +} + +export function makeOrchestratorV2ProviderReplayLayer< + Transcript extends ProviderReplayTranscript, + Error, +>( + scenario: OrchestratorV2ProviderReplayScenario, + harness: OrchestratorV2ProviderReplayHarness, + options: { + readonly databaseLayer?: Layer.Layer< + SqlClient.SqlClient, + MigrationError | PlatformError.PlatformError | SqlError + >; + readonly enableAssistantStreaming?: boolean; + } = {}, +): Layer.Layer { + const registryLayer = harness.makeProviderAdapterRegistryLayer(scenario.transcript); + return makeOrchestratorV2ReplayLayerWithRegistry(scenario, registryLayer, options); +} + +export function makeOrchestratorV2ReplayLayerWithRegistry( + scenario: Pick, + registryLayer: Layer.Layer, + options: { + readonly databaseLayer?: Layer.Layer< + SqlClient.SqlClient, + MigrationError | PlatformError.PlatformError | SqlError + >; + readonly enableAssistantStreaming?: boolean; + } = {}, +): Layer.Layer { + const serverConfigLayer = Layer.effect( + ServerConfig, + makeReplayServerConfig(scenario.name).pipe(Effect.orDie), + ).pipe(Layer.provide(NodeServices.layer)); + const runtimeLayer = + scenario.runtimePolicyOverride === undefined + ? runtimePolicyLayer + : runtimePolicyLayerWithOverride(scenario.runtimePolicyOverride).pipe( + Layer.provide(runtimePolicyLayer), + ); + const databaseLayer = options.databaseLayer ?? SqlitePersistenceMemory; + const serverSettingsLayer = ServerSettingsService.layerTest({ + enableAssistantStreaming: options.enableAssistantStreaming ?? false, + }).pipe(Layer.orDie); + const storesLayer = Layer.mergeAll( + eventStoreLayer, + projectionStoreLayer, + commandReceiptStoreLayer, + effectOutboxLayer, + turnItemPositionStoreLayer, + ).pipe(Layer.provide(databaseLayer)); + const eventSinkProvided = eventSinkLayer.pipe( + Layer.provide(Layer.mergeAll(storesLayer, databaseLayer)), + ); + const commandReceiptStoreProvided = commandReceiptStoreLayer.pipe(Layer.provide(databaseLayer)); + const providerEventIngestorProvided = providerEventIngestorLayer.pipe( + Layer.provide(Layer.mergeAll(storesLayer, eventSinkProvided, idAllocatorLayer)), + ); + const vcsDriverRegistryLayer = VcsDriverRegistry.layer.pipe( + Layer.provide(VcsProcess.layer), + Layer.provide(serverConfigLayer), + Layer.provide(NodeServices.layer), + ); + const checkpointStoreLayer = CheckpointStore.layer.pipe( + Layer.provide(vcsDriverRegistryLayer), + Layer.provide(NodeServices.layer), + ); + const checkpointServiceProvided = checkpointServiceLayer.pipe( + Layer.provide(Layer.mergeAll(checkpointStoreLayer, idAllocatorLayer)), + ); + const contextHandoffServiceProvided = contextHandoffServiceLayer.pipe( + Layer.provide(idAllocatorLayer), + ); + const persistenceLayer = Layer.mergeAll( + storesLayer, + eventSinkProvided, + commandReceiptStoreProvided, + idAllocatorLayer, + providerEventIngestorProvided, + ); + const providerSessionManagerProvided = providerSessionManagerLayerWithOptions({ + configureMcp: false, + }).pipe( + Layer.provide( + Layer.mergeAll( + registryLayer, + eventSinkProvided, + idAllocatorLayer, + mcpSessionRegistryTestLayer, + storesLayer, + ), + ), + ); + const providerSwitchServiceProvided = providerSwitchServiceLayer.pipe( + Layer.provide(registryLayer), + ); + const runExecutionServiceProvided = runExecutionServiceLayer.pipe( + Layer.provide( + Layer.mergeAll( + checkpointServiceProvided, + eventSinkProvided, + idAllocatorLayer, + providerEventIngestorProvided, + serverSettingsLayer, + ), + ), + ); + const providerTurnStartServiceProvided = providerTurnStartServiceLayer.pipe( + Layer.provide( + Layer.mergeAll( + contextHandoffServiceProvided, + eventSinkProvided, + idAllocatorLayer, + storesLayer, + providerSessionManagerProvided, + runExecutionServiceProvided, + runtimeLayer, + ), + ), + ); + const providerTurnControlServiceProvided = providerTurnControlServiceLayer.pipe( + Layer.provide(Layer.merge(storesLayer, providerSessionManagerProvided)), + ); + const runtimeRequestServiceProvided = runtimeRequestServiceLayer.pipe( + Layer.provide(Layer.merge(storesLayer, providerSessionManagerProvided)), + ); + const checkpointRollbackServiceProvided = checkpointRollbackServiceLayer.pipe( + Layer.provide( + Layer.mergeAll( + checkpointServiceProvided, + eventSinkProvided, + idAllocatorLayer, + storesLayer, + providerSessionManagerProvided, + runtimeLayer, + ), + ), + ); + const checkpointCaptureServiceProvided = checkpointCaptureServiceLayer.pipe( + Layer.provide( + Layer.mergeAll(checkpointServiceProvided, eventSinkProvided, idAllocatorLayer, storesLayer), + ), + ); + const runFinalizationServiceProvided = runFinalizationServiceLayer.pipe( + Layer.provide(Layer.merge(checkpointCaptureServiceProvided, storesLayer)), + ); + const effectExecutorProvided = effectExecutorLayer.pipe( + Layer.provide( + Layer.mergeAll( + runFinalizationServiceProvided, + checkpointRollbackServiceProvided, + providerSessionManagerProvided, + providerTurnControlServiceProvided, + providerTurnStartServiceProvided, + runtimeRequestServiceProvided, + ), + ), + ); + const effectWorkerProvided = effectWorkerLayer.pipe( + Layer.provide(Layer.merge(storesLayer, effectExecutorProvided)), + ); + const effectWorkerDaemonProvided = effectWorkerDaemonLayer.pipe( + Layer.provide(effectWorkerProvided), + ); + const orchestratorProvided = orchestratorLayer.pipe( + Layer.provide( + Layer.mergeAll( + checkpointServiceProvided, + commandPolicyLayer, + contextHandoffServiceProvided, + effectWorkerProvided, + persistenceLayer, + registryLayer, + runtimeLayer, + providerSessionManagerProvided, + providerSwitchServiceProvided, + runExecutionServiceProvided, + threadForkServiceLayer, + ), + ), + ); + return Layer.merge(orchestratorProvided, effectWorkerDaemonProvided); +} diff --git a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts new file mode 100644 index 00000000000..41e31598352 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts @@ -0,0 +1,969 @@ +import { assert, describe, it } from "@effect/vitest"; +import { + CommandId, + MessageId, + type ModelSelection, + type OrchestrationV2Command, + type OrchestrationV2ProviderCapabilities, + type OrchestrationV2ProviderSession, + type OrchestrationV2ProviderThread, + ProjectId, + ProviderInstanceId, + ProviderThreadId, + ProviderTurnId, + ThreadId, + TurnItemId, + ProviderDriverKind, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as PubSub from "effect/PubSub"; +import * as Ref from "effect/Ref"; +import * as Stream from "effect/Stream"; + +import { ClaudeProviderCapabilitiesV2 } from "../Adapters/ClaudeAdapterV2.ts"; +import { CodexProviderCapabilitiesV2 } from "../Adapters/CodexAdapterV2.ts"; +import { CursorProviderCapabilitiesV2 } from "../Adapters/CursorAdapterV2.ts"; +import { OrchestratorV2 } from "../Orchestrator.ts"; +import { + type ProviderAdapterV2Event, + ProviderAdapterProtocolError, + type ProviderAdapterV2Shape, +} from "../ProviderAdapter.ts"; +import { makeLayer as makeProviderAdapterRegistryLayer } from "../ProviderAdapterRegistry.ts"; +import { + CLAUDE_MODEL_SELECTION, + CODEX_MODEL_SELECTION, + CURSOR_MODEL_SELECTION, +} from "./fixtures/shared.ts"; +import { makeOrchestratorV2ReplayLayerWithRegistry } from "./ProviderReplayHarness.ts"; +import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts"; + +const threadId = ThreadId.make("thread:provider-switch"); +const projectId = ProjectId.make("project:provider-switch"); +const firstPrompt = "Respond with exactly: codex before switch"; +const claudePrompt = "Respond with exactly: claude switched response"; +const returnPrompt = "Respond with exactly: codex after return"; +const CODEX_DRIVER = ProviderDriverKind.make("codex"); +const CLAUDE_DRIVER = ProviderDriverKind.make("claudeAgent"); +const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); + +interface CapturedTurn { + readonly driver: ProviderDriverKind; + readonly threadId: ThreadId; + readonly providerThreadId: ProviderThreadId; + readonly text: string; +} + +function unimplemented(driver: ProviderDriverKind, detail: string) { + return Effect.fail(new ProviderAdapterProtocolError({ driver, detail })); +} + +function makeTestAdapter(input: { + readonly instanceId: ProviderInstanceId; + readonly driver: ProviderDriverKind; + readonly capabilities: OrchestrationV2ProviderCapabilities; + readonly modelSelection: ModelSelection; + readonly responseByRunOrdinal: Readonly>; + readonly responseByThreadId?: Readonly>>>; + readonly capturedTurns: Ref.Ref>; + readonly failResume?: boolean; +}): ProviderAdapterV2Shape { + return { + instanceId: input.instanceId, + driver: input.driver, + getCapabilities: () => Effect.succeed(input.capabilities), + openSession: (sessionInput) => + Effect.gen(function* () { + const events = yield* PubSub.unbounded(); + const now = yield* DateTime.now; + const providerSession: OrchestrationV2ProviderSession = { + id: sessionInput.providerSessionId, + driver: input.driver, + providerInstanceId: input.instanceId, + status: "ready", + cwd: sessionInput.runtimePolicy.cwd ?? process.cwd(), + model: input.modelSelection.model, + capabilities: input.capabilities, + createdAt: now, + updatedAt: now, + lastError: null, + }; + + return { + instanceId: input.instanceId, + driver: input.driver, + providerSessionId: sessionInput.providerSessionId, + providerSession, + rawEvents: Stream.empty, + events: Stream.fromPubSub(events), + ensureThread: (threadInput) => + Effect.gen(function* () { + const createdAt = yield* DateTime.now; + const nativeThreadId = `${input.driver}:${threadInput.threadId}`; + return { + id: ProviderThreadId.make(`provider-thread:${nativeThreadId}`), + driver: input.driver, + providerInstanceId: input.instanceId, + providerSessionId: sessionInput.providerSessionId, + appThreadId: threadInput.threadId, + ownerNodeId: null, + nativeThreadRef: { + driver: input.driver, + nativeId: nativeThreadId, + strength: "strong", + }, + nativeConversationHeadRef: null, + status: "idle", + firstRunOrdinal: null, + lastRunOrdinal: null, + handoffIds: [], + forkedFrom: null, + createdAt, + updatedAt: createdAt, + } satisfies OrchestrationV2ProviderThread; + }), + resumeThread: ({ providerThread }) => + input.failResume + ? unimplemented(input.driver, "simulated native resume failure") + : Effect.succeed(providerThread), + startTurn: (turnInput) => + Effect.gen(function* () { + yield* Effect.yieldNow; + yield* Ref.update(input.capturedTurns, (turns) => [ + ...turns, + { + driver: input.driver, + threadId: turnInput.threadId, + providerThreadId: turnInput.providerThread.id, + text: turnInput.message.text, + }, + ]); + const eventTime = yield* DateTime.now; + const providerTurnId = ProviderTurnId.make( + `provider-turn:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}`, + ); + const response = + input.responseByThreadId?.[turnInput.threadId]?.[turnInput.runOrdinal] ?? + input.responseByRunOrdinal[turnInput.runOrdinal] ?? + `${input.driver} response for run ${turnInput.runOrdinal}`; + const providerEvents: ReadonlyArray = [ + { + type: "provider_turn.updated", + driver: input.driver, + providerTurn: { + id: providerTurnId, + providerThreadId: turnInput.providerThread.id, + nodeId: turnInput.rootNodeId, + runAttemptId: turnInput.attemptId, + nativeTurnRef: { + driver: input.driver, + nativeId: `native-turn:${turnInput.threadId}:${turnInput.runOrdinal}`, + strength: "strong", + }, + ordinal: turnInput.runOrdinal, + status: "completed", + startedAt: eventTime, + completedAt: eventTime, + }, + }, + { + type: "turn_item.updated", + driver: input.driver, + turnItem: { + id: TurnItemId.make( + `turn-item:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`, + ), + threadId: turnInput.threadId, + runId: turnInput.runId, + nodeId: turnInput.rootNodeId, + providerThreadId: turnInput.providerThread.id, + providerTurnId, + nativeItemRef: null, + parentItemId: null, + ordinal: turnInput.runOrdinal * 100 + 1, + status: "completed", + title: null, + startedAt: eventTime, + completedAt: eventTime, + updatedAt: eventTime, + type: "assistant_message", + messageId: MessageId.make( + `message:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`, + ), + text: response, + streaming: false, + }, + }, + { + type: "turn.terminal", + driver: input.driver, + providerTurnId, + status: "completed", + }, + ]; + for (const event of providerEvents) { + yield* PubSub.publish(events, event); + } + }), + steerTurn: () => Effect.void, + interruptTurn: () => Effect.void, + respondToRuntimeRequest: () => Effect.void, + readThreadSnapshot: () => + unimplemented(input.driver, "readThreadSnapshot unused in provider switch test"), + rollbackThread: () => + unimplemented(input.driver, "rollbackThread unused in provider switch test"), + forkThread: () => + unimplemented(input.driver, "forkThread unused in provider switch test"), + }; + }), + }; +} + +const waitForIdle = Effect.fn("ProviderSwitchTest.waitForIdle")(function* ( + targetThreadId: ThreadId, +) { + const orchestrator = yield* OrchestratorV2; + for (let attempt = 0; attempt < 1_000; attempt += 1) { + const projection = yield* orchestrator.getThreadProjection(targetThreadId); + if ( + projection.runs.every( + (run) => !["queued", "starting", "running", "waiting"].includes(run.status), + ) + ) { + return projection; + } + yield* Effect.sleep("5 millis"); + } + return yield* Effect.die(new Error("Provider switch test timed out waiting for idle")); +}); + +describe("orchestration v2 provider switching", () => { + it.live("uses portable fallback when native resume fails after a provider switch", () => + Effect.scoped( + Effect.gen(function* () { + const cwd = yield* checkpointWorkspace("provider-switch"); + const capturedTurns = yield* Ref.make>([]); + const registryLayer = makeProviderAdapterRegistryLayer([ + makeTestAdapter({ + instanceId: ProviderInstanceId.make("codex"), + driver: CODEX_DRIVER, + capabilities: CodexProviderCapabilitiesV2, + modelSelection: CODEX_MODEL_SELECTION, + responseByRunOrdinal: { + 1: "codex before switch", + 3: "codex after return", + }, + capturedTurns, + failResume: true, + }), + makeTestAdapter({ + instanceId: ProviderInstanceId.make("claudeAgent"), + driver: CLAUDE_DRIVER, + capabilities: ClaudeProviderCapabilitiesV2, + modelSelection: CLAUDE_MODEL_SELECTION, + responseByRunOrdinal: { 2: "claude switched response" }, + capturedTurns, + }), + ]); + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:provider-switch:create"), + threadId, + projectId, + title: "Provider switch", + modelSelection: CODEX_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:provider-switch:codex"), + threadId, + messageId: MessageId.make("message:provider-switch:codex"), + text: firstPrompt, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:provider-switch:claude"), + threadId, + messageId: MessageId.make("message:provider-switch:claude"), + text: claudePrompt, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:provider-switch:return"), + threadId, + messageId: MessageId.make("message:provider-switch:return"), + text: returnPrompt, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + const projection = yield* Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + yield* orchestrator.dispatch(commands[0]!); + yield* orchestrator.dispatch(commands[1]!); + yield* waitForIdle(threadId); + yield* orchestrator.dispatch(commands[2]!); + yield* waitForIdle(threadId); + yield* orchestrator.dispatch(commands[3]!); + return yield* waitForIdle(threadId); + }).pipe( + Effect.provide( + makeOrchestratorV2ReplayLayerWithRegistry( + { + name: "provider-switch", + runtimePolicyOverride: { + cwd, + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }, + }, + registryLayer, + ), + ), + ); + const turns = yield* Ref.get(capturedTurns); + + assert.deepEqual( + projection.runs.map((run) => [run.providerInstanceId, run.status]), + [ + ["codex", "completed"], + ["claudeAgent", "completed"], + ["codex", "completed"], + ], + ); + assert.lengthOf(projection.providerThreads, 2); + assert.equal(projection.runs[0]?.providerThreadId, projection.runs[2]?.providerThreadId); + assert.notEqual(projection.runs[0]?.providerThreadId, projection.runs[1]?.providerThreadId); + assert.deepEqual( + projection.contextHandoffs.map((handoff) => handoff.strategy), + ["full_thread_summary", "delta_since_target_last_seen"], + ); + assert.deepEqual( + projection.contextTransfers.map((transfer) => [ + transfer.type, + transfer.status, + transfer.resolution?.strategy, + ]), + [ + ["provider_handoff", "consumed", "portable_context"], + ["provider_handoff", "consumed", "delta_context"], + ], + ); + assert.deepEqual( + projection.turnItems + .filter((item) => item.type === "user_message") + .map((item) => item.text), + [firstPrompt, claudePrompt, returnPrompt], + ); + assert.deepEqual( + projection.providerThreads.map((providerThread) => [ + providerThread.driver, + providerThread.status, + providerThread.handoffIds.length, + ]), + [ + ["codex", "idle", 1], + ["claudeAgent", "idle", 1], + ], + ); + assert.equal(turns[0]?.text, firstPrompt); + assert.include(turns[1]?.text ?? "", "Context handoff (full_thread_summary):"); + assert.include(turns[1]?.text ?? "", "codex before switch"); + assert.include(turns[1]?.text ?? "", claudePrompt); + assert.include(turns[2]?.text ?? "", "Context handoff (delta_since_target_last_seen):"); + assert.include(turns[2]?.text ?? "", "claude switched response"); + assert.include(turns[2]?.text ?? "", returnPrompt); + assert.notInclude(turns[2]?.text ?? "", "codex before switch"); + assert.equal(turns[0]?.providerThreadId, turns[2]?.providerThreadId); + }), + ), + ); + + it.live("resolves a Claude fork into portable Codex context on first dispatch", () => + Effect.scoped( + Effect.gen(function* () { + const sourceThreadId = ThreadId.make("thread:cross-provider-fork:source"); + const targetThreadId = ThreadId.make("thread:cross-provider-fork:target"); + const sourcePrompt = "Remember that the release color is violet."; + const targetPrompt = "What release color did we choose?"; + const cwd = yield* checkpointWorkspace("cross-provider-fork"); + const capturedTurns = yield* Ref.make>([]); + const registryLayer = makeProviderAdapterRegistryLayer([ + makeTestAdapter({ + instanceId: ProviderInstanceId.make("codex"), + driver: CODEX_DRIVER, + capabilities: CodexProviderCapabilitiesV2, + modelSelection: CODEX_MODEL_SELECTION, + responseByRunOrdinal: { 1: "The release color is violet." }, + capturedTurns, + }), + makeTestAdapter({ + instanceId: ProviderInstanceId.make("claudeAgent"), + driver: CLAUDE_DRIVER, + capabilities: ClaudeProviderCapabilitiesV2, + modelSelection: CLAUDE_MODEL_SELECTION, + responseByRunOrdinal: { 1: "I will remember violet." }, + capturedTurns, + }), + ]); + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-fork:create"), + threadId: sourceThreadId, + projectId, + title: "Cross-provider fork source", + modelSelection: CLAUDE_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-fork:source"), + threadId: sourceThreadId, + messageId: MessageId.make("message:cross-provider-fork:source"), + text: sourcePrompt, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-fork:fork"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Cross-provider fork target", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-fork:target"), + threadId: targetThreadId, + messageId: MessageId.make("message:cross-provider-fork:target"), + text: targetPrompt, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + const targetProjection = yield* Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + yield* orchestrator.dispatch(commands[0]!); + yield* orchestrator.dispatch(commands[1]!); + yield* waitForIdle(sourceThreadId); + yield* orchestrator.dispatch(commands[2]!); + yield* orchestrator.dispatch(commands[3]!); + return yield* waitForIdle(targetThreadId); + }).pipe( + Effect.provide( + makeOrchestratorV2ReplayLayerWithRegistry( + { + name: "cross-provider-fork", + runtimePolicyOverride: { + cwd, + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }, + }, + registryLayer, + ), + ), + ); + const turns = yield* Ref.get(capturedTurns); + const targetTurn = turns.find((turn) => turn.threadId === targetThreadId); + + assert.deepEqual( + targetProjection.runs.map((run) => [run.providerInstanceId, run.status]), + [["codex", "completed"]], + ); + assert.lengthOf(targetProjection.providerThreads, 1); + assert.equal(targetProjection.providerThreads[0]?.driver, "codex"); + assert.isNull(targetProjection.providerThreads[0]?.forkedFrom); + assert.deepEqual( + targetProjection.contextTransfers.map((transfer) => [ + transfer.type, + transfer.status, + transfer.resolution?.strategy, + ]), + [["fork", "consumed", "portable_context"]], + ); + assert.deepEqual( + targetProjection.contextHandoffs.map((handoff) => handoff.strategy), + ["full_thread_summary"], + ); + assert.equal( + targetProjection.runs[0]?.contextHandoffId, + targetProjection.contextHandoffs[0]?.id, + ); + assert.include(targetTurn?.text ?? "", "Context handoff (full_thread_summary):"); + assert.include(targetTurn?.text ?? "", sourcePrompt); + assert.include(targetTurn?.text ?? "", "I will remember violet."); + assert.include(targetTurn?.text ?? "", targetPrompt); + }), + ), + ); + + it.live("resolves a same-provider Cursor fork with portable context", () => + Effect.scoped( + Effect.gen(function* () { + const sourceThreadId = ThreadId.make("thread:cursor-portable-fork:source"); + const targetThreadId = ThreadId.make("thread:cursor-portable-fork:target"); + const sourcePrompt = "Remember that the deployment marker is indigo."; + const sourceResponse = "I will remember indigo."; + const targetPrompt = "What deployment marker did we choose?"; + const cwd = yield* checkpointWorkspace("cursor-portable-fork"); + const capturedTurns = yield* Ref.make>([]); + const registryLayer = makeProviderAdapterRegistryLayer([ + makeTestAdapter({ + instanceId: ProviderInstanceId.make("cursor"), + driver: CURSOR_DRIVER, + capabilities: CursorProviderCapabilitiesV2, + modelSelection: CURSOR_MODEL_SELECTION, + responseByRunOrdinal: {}, + responseByThreadId: { + [sourceThreadId]: { 1: sourceResponse }, + [targetThreadId]: { 1: "The deployment marker is indigo." }, + }, + capturedTurns, + }), + ]); + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cursor-portable-fork:create"), + threadId: sourceThreadId, + projectId, + title: "Cursor portable fork source", + modelSelection: CURSOR_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cursor-portable-fork:source"), + threadId: sourceThreadId, + messageId: MessageId.make("message:cursor-portable-fork:source"), + text: sourcePrompt, + attachments: [], + modelSelection: CURSOR_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cursor-portable-fork:fork"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Cursor portable fork target", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cursor-portable-fork:target"), + threadId: targetThreadId, + messageId: MessageId.make("message:cursor-portable-fork:target"), + text: targetPrompt, + attachments: [], + modelSelection: CURSOR_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + const targetProjection = yield* Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + yield* orchestrator.dispatch(commands[0]!); + yield* orchestrator.dispatch(commands[1]!); + yield* waitForIdle(sourceThreadId); + yield* orchestrator.dispatch(commands[2]!); + yield* orchestrator.dispatch(commands[3]!); + return yield* waitForIdle(targetThreadId); + }).pipe( + Effect.provide( + makeOrchestratorV2ReplayLayerWithRegistry( + { + name: "cursor-portable-fork", + runtimePolicyOverride: { + cwd, + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }, + }, + registryLayer, + ), + ), + ); + const turns = yield* Ref.get(capturedTurns); + const targetTurn = turns.find((turn) => turn.threadId === targetThreadId); + + assert.deepEqual( + targetProjection.runs.map((run) => [run.providerInstanceId, run.status]), + [["cursor", "completed"]], + ); + assert.lengthOf(targetProjection.providerThreads, 1); + assert.equal(targetProjection.providerThreads[0]?.driver, "cursor"); + assert.isNull(targetProjection.providerThreads[0]?.forkedFrom); + assert.deepEqual( + targetProjection.contextTransfers.map((transfer) => [ + transfer.type, + transfer.status, + transfer.resolution?.strategy, + ]), + [["fork", "consumed", "portable_context"]], + ); + assert.deepEqual( + targetProjection.contextHandoffs.map((handoff) => handoff.strategy), + ["full_thread_summary"], + ); + assert.include(targetTurn?.text ?? "", "Context handoff (full_thread_summary):"); + assert.include(targetTurn?.text ?? "", sourcePrompt); + assert.include(targetTurn?.text ?? "", sourceResponse); + assert.include(targetTurn?.text ?? "", targetPrompt); + }), + ), + ); + + it.live("switches providers while consuming a pending cross-provider merge-back", () => + Effect.scoped( + Effect.gen(function* () { + const sourceThreadId = ThreadId.make("thread:cross-provider-merge:source"); + const forkThreadId = ThreadId.make("thread:cross-provider-merge:fork"); + const firstSourcePrompt = "Remember that the first source marker is amber."; + const secondSourcePrompt = "Remember that the second source marker is violet."; + const forkPrompt = "Remember that the fork marker is cobalt."; + const mergePrompt = "Report all three remembered markers."; + const cwd = yield* checkpointWorkspace("cross-provider-merge"); + const capturedTurns = yield* Ref.make>([]); + const registryLayer = makeProviderAdapterRegistryLayer([ + makeTestAdapter({ + instanceId: ProviderInstanceId.make("codex"), + driver: CODEX_DRIVER, + capabilities: CodexProviderCapabilitiesV2, + modelSelection: CODEX_MODEL_SELECTION, + responseByRunOrdinal: {}, + responseByThreadId: { + [sourceThreadId]: { + 1: "I will remember amber.", + 3: "The markers are amber, violet, and cobalt.", + }, + [forkThreadId]: { + 1: "I will remember cobalt.", + }, + }, + capturedTurns, + }), + makeTestAdapter({ + instanceId: ProviderInstanceId.make("claudeAgent"), + driver: CLAUDE_DRIVER, + capabilities: ClaudeProviderCapabilitiesV2, + modelSelection: CLAUDE_MODEL_SELECTION, + responseByRunOrdinal: { 2: "I will remember violet." }, + capturedTurns, + }), + ]); + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-merge:create"), + threadId: sourceThreadId, + projectId, + title: "Cross-provider merge source", + modelSelection: CODEX_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-merge:first-source"), + threadId: sourceThreadId, + messageId: MessageId.make("message:cross-provider-merge:first-source"), + text: firstSourcePrompt, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-merge:second-source"), + threadId: sourceThreadId, + messageId: MessageId.make("message:cross-provider-merge:second-source"), + text: secondSourcePrompt, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-merge:fork"), + sourceThreadId, + targetThreadId: forkThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Cross-provider merge fork", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-merge:fork-turn"), + threadId: forkThreadId, + messageId: MessageId.make("message:cross-provider-merge:fork-turn"), + text: forkPrompt, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.merge_back", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-merge:merge"), + sourceThreadId: forkThreadId, + targetThreadId: sourceThreadId, + sourcePoint: { type: "latest_stable" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command:cross-provider-merge:consume"), + threadId: sourceThreadId, + messageId: MessageId.make("message:cross-provider-merge:consume"), + text: mergePrompt, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + const projection = yield* Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + yield* orchestrator.dispatch(commands[0]!); + yield* orchestrator.dispatch(commands[1]!); + yield* waitForIdle(sourceThreadId); + yield* orchestrator.dispatch(commands[2]!); + yield* waitForIdle(sourceThreadId); + yield* orchestrator.dispatch(commands[3]!); + yield* orchestrator.dispatch(commands[4]!); + yield* waitForIdle(forkThreadId); + yield* orchestrator.dispatch(commands[5]!); + yield* orchestrator.dispatch(commands[6]!); + return yield* waitForIdle(sourceThreadId); + }).pipe( + Effect.provide( + makeOrchestratorV2ReplayLayerWithRegistry( + { + name: "cross-provider-merge", + runtimePolicyOverride: { + cwd, + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }, + }, + registryLayer, + ), + ), + ); + const turns = yield* Ref.get(capturedTurns); + const mergedTurn = turns.findLast( + (turn) => turn.threadId === sourceThreadId && turn.driver === "codex", + ); + const mergeTransfer = projection.contextTransfers.find( + (transfer) => transfer.type === "merge_back", + ); + + assert.isDefined(mergedTurn); + assert.include(mergedTurn.text, "Context handoff (full_thread_summary):"); + assert.include(mergedTurn.text, firstSourcePrompt); + assert.include(mergedTurn.text, "I will remember amber."); + assert.include(mergedTurn.text, secondSourcePrompt); + assert.include(mergedTurn.text, "I will remember violet."); + assert.include(mergedTurn.text, "Context handoff (merge_back / fork_delta_summary):"); + assert.include(mergedTurn.text, forkPrompt); + assert.include(mergedTurn.text, "I will remember cobalt."); + assert.include(mergedTurn.text, mergePrompt); + assert.isDefined(mergeTransfer); + assert.equal(mergeTransfer.status, "consumed"); + assert.equal(mergeTransfer.targetProviderInstanceId, "codex"); + assert.equal(mergeTransfer.resolution?.strategy, "fork_delta_context"); + }), + ), + ); + + it.live("routes two custom instances of the same driver independently", () => + Effect.scoped( + Effect.gen(function* () { + const personalThreadId = ThreadId.make("thread:custom-codex-personal"); + const workThreadId = ThreadId.make("thread:custom-codex-work"); + const personalSelection = { + instanceId: ProviderInstanceId.make("codex_personal"), + model: "gpt-5.4", + } satisfies ModelSelection; + const workSelection = { + instanceId: ProviderInstanceId.make("codex_work"), + model: "gpt-5.4", + } satisfies ModelSelection; + const cwd = yield* checkpointWorkspace("custom-codex-instances"); + const capturedTurns = yield* Ref.make>([]); + const registryLayer = makeProviderAdapterRegistryLayer([ + makeTestAdapter({ + instanceId: personalSelection.instanceId, + driver: CODEX_DRIVER, + capabilities: CodexProviderCapabilitiesV2, + modelSelection: personalSelection, + responseByRunOrdinal: { 1: "personal response" }, + capturedTurns, + }), + makeTestAdapter({ + instanceId: workSelection.instanceId, + driver: CODEX_DRIVER, + capabilities: CodexProviderCapabilitiesV2, + modelSelection: workSelection, + responseByRunOrdinal: { 1: "work response" }, + capturedTurns, + }), + ]); + + const [personal, work] = yield* Effect.gen(function* () { + const orchestrator = yield* OrchestratorV2; + for (const [targetThreadId, selection, suffix] of [ + [personalThreadId, personalSelection, "personal"], + [workThreadId, workSelection, "work"], + ] as const) { + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make(`command:custom-codex:${suffix}:create`), + threadId: targetThreadId, + projectId, + title: `Custom Codex ${suffix}`, + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make(`command:custom-codex:${suffix}:message`), + threadId: targetThreadId, + messageId: MessageId.make(`message:custom-codex:${suffix}`), + text: `${suffix} prompt`, + attachments: [], + modelSelection: selection, + dispatchMode: { type: "start_immediately" }, + }); + yield* waitForIdle(targetThreadId); + } + return yield* Effect.all([ + orchestrator.getThreadProjection(personalThreadId), + orchestrator.getThreadProjection(workThreadId), + ]); + }).pipe( + Effect.provide( + makeOrchestratorV2ReplayLayerWithRegistry( + { + name: "custom-codex-instances", + runtimePolicyOverride: { + cwd, + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }, + }, + registryLayer, + ), + ), + ); + + assert.equal(personal.runs[0]?.providerInstanceId, personalSelection.instanceId); + assert.equal( + personal.providerSessions[0]?.providerInstanceId, + personalSelection.instanceId, + ); + assert.equal(work.runs[0]?.providerInstanceId, workSelection.instanceId); + assert.equal(work.providerSessions[0]?.providerInstanceId, workSelection.instanceId); + assert.notEqual(personal.providerSessions[0]?.id, work.providerSessions[0]?.id); + assert.deepEqual( + (yield* Ref.get(capturedTurns)).map((turn) => [turn.threadId, turn.text]), + [ + [personalThreadId, "personal prompt"], + [workThreadId, "work prompt"], + ], + ); + }), + ), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayFixtureWorkspace.ts b/apps/server/src/orchestration-v2/testkit/ReplayFixtureWorkspace.ts new file mode 100644 index 00000000000..e5fa165374d --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ReplayFixtureWorkspace.ts @@ -0,0 +1,74 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; +import * as Schema from "effect/Schema"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; + +class ReplayFixtureGitCommandError extends Schema.TaggedErrorClass()( + "ReplayFixtureGitCommandError", + { + command: Schema.String, + exitCode: Schema.Number, + }, +) { + override get message(): string { + return `${this.command} failed with exit ${this.exitCode}.`; + } +} + +function runGit( + cwd: string, + args: ReadonlyArray, +): Effect.Effect< + void, + ReplayFixtureGitCommandError | PlatformError.PlatformError, + ChildProcessSpawner.ChildProcessSpawner +> { + return Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const exitCode = yield* spawner.exitCode(ChildProcess.make("git", args, { cwd })); + if (Number(exitCode) !== 0) { + return yield* new ReplayFixtureGitCommandError({ + command: `git ${args.join(" ")}`, + exitCode: Number(exitCode), + }); + } + }); +} + +export const makeCheckpointWorkspaceEffect = Effect.fn("makeCheckpointWorkspace")(function* ( + fixtureName: string, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const cwd = yield* fs.makeTempDirectory({ + prefix: `t3-orchestrator-v2-${fixtureName}-`, + }); + yield* runGit(cwd, ["init"]); + yield* runGit(cwd, ["config", "user.name", "T3 Code Test"]); + yield* runGit(cwd, ["config", "user.email", "t3code-test@example.com"]); + yield* fs.writeFileString(path.join(cwd, "README.md"), `# ${fixtureName}\n`); + yield* runGit(cwd, ["add", "README.md"]); + yield* runGit(cwd, ["commit", "-m", "initial"]); + return cwd; +}); + +export const removeCheckpointWorkspaceEffect = Effect.fn("removeCheckpointWorkspace")(function* ( + cwd: string, +) { + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(cwd, { recursive: true }); +}); + +export const checkpointWorkspace = (fixtureName: string) => + Effect.acquireRelease(makeCheckpointWorkspaceEffect(fixtureName), (cwd) => + removeCheckpointWorkspaceEffect(cwd).pipe(Effect.orDie), + ).pipe(Effect.provide(NodeServices.layer)); + +export async function makeCheckpointWorkspace(fixtureName: string): Promise { + return await Effect.runPromise( + makeCheckpointWorkspaceEffect(fixtureName).pipe(Effect.provide(NodeServices.layer)), + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/ReplayTranscriptNdjson.test.ts b/apps/server/src/orchestration-v2/testkit/ReplayTranscriptNdjson.test.ts new file mode 100644 index 00000000000..9e6c8b76a52 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ReplayTranscriptNdjson.test.ts @@ -0,0 +1,72 @@ +import { ProviderReplayNdjsonParseError } from "./ReplayTranscriptNdjson.ts"; +import { assert, describe, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +import { decodeProviderReplayNdjson } from "./ReplayTranscriptNdjson.ts"; + +const encodeParseError = Schema.encodeUnknownEffect(ProviderReplayNdjsonParseError); + +describe("decodeProviderReplayNdjson", () => { + it.effect("decodes a self-describing provider replay fixture", () => + Effect.gen(function* () { + const transcript = yield* decodeProviderReplayNdjson(` + {"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"simple"} + {"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{}}} + {"type":"emit_inbound","label":"initialized","afterMs":5,"frame":{"id":1,"result":{"ok":true}}} + {"type":"runtime_exit","status":"success"} + `); + + assert.equal(transcript.provider, "codex"); + assert.equal(transcript.protocol, "codex.app-server"); + assert.equal(transcript.scenario, "simple"); + assert.deepEqual( + transcript.entries.map((entry) => entry.type), + ["expect_outbound", "emit_inbound", "runtime_exit"], + ); + }), + ); + + it.effect("decodes entry-only fixtures when metadata is supplied by the test", () => + Effect.gen(function* () { + const transcript = yield* decodeProviderReplayNdjson( + ` + {"type":"emit_inbound","frame":{"method":"thread/created","params":{"id":"native-thread"}}} + {"type":"runtime_exit","status":"success"} + `, + { + provider: "claudeAgent", + protocol: "claude-agent-sdk", + version: "0.2.111", + scenario: "entry-only", + }, + ); + + assert.equal(transcript.provider, "claudeAgent"); + assert.equal(transcript.entries.length, 2); + }), + ); + + it.effect("returns a schema-serializable typed parse error", () => + Effect.gen(function* () { + const error = yield* decodeProviderReplayNdjson(`{"type":`).pipe(Effect.flip); + const encoded = yield* encodeParseError(error); + + assert.equal(error._tag, "ProviderReplayNdjsonLineParseError"); + assert.equal(encoded._tag, "ProviderReplayNdjsonLineParseError"); + if (encoded._tag !== "ProviderReplayNdjsonLineParseError") { + throw new Error("Expected line parse error encoding."); + } + assert.equal(encoded.lineNumber, 1); + assert.equal(encoded.line, '{"type":'); + const cause = encoded.cause; + if (typeof cause !== "object" || cause === null || Array.isArray(cause)) { + throw new Error("Expected encoded parse cause to be a JSON object."); + } + const causeRecord = cause as Record; + assert.equal(causeRecord.name, "SchemaError"); + assert.equal(causeRecord._tag, "SchemaError"); + assert.doesNotThrow(() => JSON.stringify(encoded)); + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayTranscriptNdjson.ts b/apps/server/src/orchestration-v2/testkit/ReplayTranscriptNdjson.ts new file mode 100644 index 00000000000..87dbfc6f693 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ReplayTranscriptNdjson.ts @@ -0,0 +1,121 @@ +import { + ProviderReplayEntry, + ProviderReplayNdjsonRecord, + ProviderReplayTranscript, + type ProviderReplayTranscriptHeader, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +export class ProviderReplayNdjsonLineParseError extends Schema.TaggedErrorClass()( + "ProviderReplayNdjsonLineParseError", + { + lineNumber: Schema.Number, + line: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return `Failed to parse provider replay NDJSON line ${this.lineNumber}.`; + } +} + +export class ProviderReplayNdjsonMissingHeaderError extends Schema.TaggedErrorClass()( + "ProviderReplayNdjsonMissingHeaderError", + {}, +) { + override get message(): string { + return "Provider replay NDJSON requires a transcript_start header or fallback transcript metadata."; + } +} + +export class ProviderReplayNdjsonEmptyError extends Schema.TaggedErrorClass()( + "ProviderReplayNdjsonEmptyError", + {}, +) { + override get message(): string { + return "Provider replay NDJSON did not contain any records."; + } +} + +export const ProviderReplayNdjsonParseError = Schema.Union([ + ProviderReplayNdjsonLineParseError, + ProviderReplayNdjsonMissingHeaderError, + ProviderReplayNdjsonEmptyError, +]); +export type ProviderReplayNdjsonParseError = typeof ProviderReplayNdjsonParseError.Type; + +export type ProviderReplayTranscriptMetadata = Omit; + +const decodeReplayRecord = Schema.decodeUnknownSync( + Schema.fromJsonString(ProviderReplayNdjsonRecord), +); +const decodeTranscript = Schema.decodeUnknownSync(ProviderReplayTranscript); + +function parseReplayRecord( + line: string, + lineNumber: number, +): Effect.Effect { + return Effect.try({ + try: () => decodeReplayRecord(line), + catch: (cause) => + new ProviderReplayNdjsonLineParseError({ + lineNumber, + line, + cause, + }), + }); +} + +function metadataFromHeader( + header: ProviderReplayTranscriptHeader, +): ProviderReplayTranscriptMetadata { + const { type: _type, ...metadata } = header; + return metadata; +} + +export function decodeProviderReplayNdjson( + input: string, + fallbackMetadata?: ProviderReplayTranscriptMetadata, +): Effect.Effect { + return Effect.gen(function* () { + const lines = input + .split(/\r?\n/u) + .map((line, index) => ({ line: line.trim(), lineNumber: index + 1 })) + .filter(({ line }) => line.length > 0); + + if (lines.length === 0) { + return yield* new ProviderReplayNdjsonEmptyError(); + } + + const firstRecord = yield* parseReplayRecord(lines[0]!.line, lines[0]!.lineNumber); + const metadata = + firstRecord.type === "transcript_start" ? metadataFromHeader(firstRecord) : fallbackMetadata; + + if (!metadata) { + return yield* new ProviderReplayNdjsonMissingHeaderError(); + } + + const entries: Array = []; + if (firstRecord.type !== "transcript_start") { + entries.push(firstRecord); + } + + for (const { line, lineNumber } of lines.slice(1)) { + const record = yield* parseReplayRecord(line, lineNumber); + if (record.type === "transcript_start") { + return yield* new ProviderReplayNdjsonLineParseError({ + lineNumber, + line, + cause: "transcript_start is only valid as the first replay record", + }); + } + entries.push(record); + } + + return decodeTranscript({ + ...metadata, + entries, + }); + }); +} diff --git a/apps/server/src/orchestration-v2/testkit/ThreadFork.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ThreadFork.integration.test.ts new file mode 100644 index 00000000000..b1e0c91f3d1 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ThreadFork.integration.test.ts @@ -0,0 +1,1751 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import { + CommandId, + MessageId, + type OrchestrationV2Command, + type OrchestrationV2ThreadProjection, + ProviderInstanceId, + type ProviderReplayEntry, + type ProviderReplayTranscript, + ThreadId, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; +import * as Schema from "effect/Schema"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; + +import { ClaudeOrchestratorReplayHarness } from "../Adapters/ClaudeAdapterV2.testkit.ts"; +import { CodexOrchestratorReplayHarness } from "../Adapters/CodexAdapterV2.testkit.ts"; +import { IdAllocatorV2, layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts"; +import { + THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT, + THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT, + THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT, + THREAD_FORK_NATIVE_SOURCE_PROMPT, + THREAD_FORK_NATIVE_TARGET_PROMPT, +} from "./fixtures/shared.ts"; +import { runOrchestratorV2ProviderReplayScenario } from "./ProviderReplayHarness.ts"; +import { decodeProviderReplayNdjson } from "./ReplayTranscriptNdjson.ts"; + +const CODEX_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", +} as const; +const CLAUDE_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-sonnet-4-6", +} as const; +const TRANSCRIPT_PATH = `${import.meta.dirname}/fixtures/thread_fork_native/codex_transcript.ndjson`; +const PRIOR_TURN_TRANSCRIPT_PATH = `${import.meta.dirname}/fixtures/thread_fork_native_prior_turn/codex_transcript.ndjson`; +const CLAUDE_TRANSCRIPT_PATH = `${import.meta.dirname}/fixtures/thread_fork_native/claude_transcript.ndjson`; +const CLAUDE_PRIOR_TURN_TRANSCRIPT_PATH = `${import.meta.dirname}/fixtures/thread_fork_native_prior_turn/claude_transcript.ndjson`; +const CLAUDE_FORK_LOCAL_ROLLBACK_TRANSCRIPT_PATH = `${import.meta.dirname}/fixtures/thread_fork_native_fork_local_rollback/claude_transcript.ndjson`; +const CODEX_READ_ONLY_NEVER_POLICY = { + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, +} as const; + +class ThreadForkGitCommandError extends Schema.TaggedErrorClass()( + "ThreadForkGitCommandError", + { + command: Schema.String, + exitCode: Schema.Number, + }, +) { + override get message(): string { + return `${this.command} failed with exit ${this.exitCode}.`; + } +} + +function runGit( + cwd: string, + args: ReadonlyArray, +): Effect.Effect< + void, + ThreadForkGitCommandError | PlatformError.PlatformError, + ChildProcessSpawner.ChildProcessSpawner +> { + return Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const exitCode = yield* spawner.exitCode(ChildProcess.make("git", args, { cwd })); + if (Number(exitCode) !== 0) { + return yield* new ThreadForkGitCommandError({ + command: `git ${args.join(" ")}`, + exitCode: Number(exitCode), + }); + } + }); +} + +const makeCheckpointWorkspace = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const cwd = yield* fs.makeTempDirectory({ prefix: "t3-orchestrator-v2-thread-fork-" }); + yield* runGit(cwd, ["init"]); + yield* runGit(cwd, ["config", "user.name", "T3 Code Test"]); + yield* runGit(cwd, ["config", "user.email", "t3code-test@example.com"]); + yield* fs.writeFileString(path.join(cwd, "README.md"), "# thread fork\n"); + yield* runGit(cwd, ["add", "README.md"]); + yield* runGit(cwd, ["commit", "-m", "initial"]); + return cwd; +}); + +function readTranscript(transcriptPath: string = TRANSCRIPT_PATH) { + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const text = yield* fs.readFileString(transcriptPath); + return yield* decodeProviderReplayNdjson(text); + }); +} + +function metadataString(transcript: ProviderReplayTranscript, key: string): string { + const value = transcript.metadata?.[key]; + if (typeof value !== "string") { + throw new Error(`Transcript ${transcript.scenario} is missing metadata string ${key}.`); + } + return value; +} + +function metadataStringArray( + transcript: ProviderReplayTranscript, + key: string, +): ReadonlyArray { + const value = transcript.metadata?.[key]; + if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) { + throw new Error(`Transcript ${transcript.scenario} is missing metadata string array ${key}.`); + } + return value; +} + +function userAndAssistantText( + projection: Pick, +): string { + return projection.visibleTurnItems + .flatMap((row) => { + const item = row.item; + return item.type === "user_message" || item.type === "assistant_message" ? [item.text] : []; + }) + .join("\n"); +} + +function compactExpectedText(text: string, maxLength = 240): string { + const compacted = text.replace(/\s+/g, " ").trim(); + if (compacted.length <= maxLength) { + return compacted; + } + return `${compacted.slice(0, maxLength - 3)}...`; +} + +function findCompletedAgentMessageText(input: { + readonly transcript: ProviderReplayTranscript; + readonly threadId: string; + readonly turnId: string; +}): string { + for (const entry of input.transcript.entries) { + if (entry.type !== "emit_inbound") { + continue; + } + const frame = entry.frame as { + readonly method?: unknown; + readonly params?: { + readonly threadId?: unknown; + readonly turnId?: unknown; + readonly item?: { + readonly type?: unknown; + readonly text?: unknown; + }; + }; + }; + if ( + frame.method === "item/completed" && + frame.params?.threadId === input.threadId && + frame.params.turnId === input.turnId && + frame.params.item?.type === "agentMessage" && + typeof frame.params.item.text === "string" + ) { + return frame.params.item.text; + } + } + throw new Error(`No completed agent message found for ${input.threadId}/${input.turnId}`); +} + +function makeExpectedForkDeltaSummary(input: { + readonly sourceThreadId: string; + readonly targetThreadId: string; + readonly forkUserText: string; + readonly forkAssistantText: string; +}): string { + return [ + "Merge-back context from forked conversation.", + `Source thread: ${input.sourceThreadId}`, + `Target thread: ${input.targetThreadId}`, + "Covered fork runs: 1-1", + "", + "Fork delta:", + `- User: ${compactExpectedText(input.forkUserText)}`, + `- Assistant: ${compactExpectedText(input.forkAssistantText)}`, + "- Checkpoint: 0 files", + ].join("\n"); +} + +function transcriptWithMergeBackContinuation(input: { + readonly transcript: ProviderReplayTranscript; + readonly providerMessageText: string; + readonly projectedUserText: string; + readonly assistantText: string; +}): ProviderReplayTranscript { + const sourceNativeThreadId = "019dd6ba-2681-7bf0-b051-141b0cbcbb27"; + const mergeBackNativeTurnId = "019dd6ba-5000-7000-8000-000000000001"; + const mergeBackUserItemId = "merge-back-user-message"; + const mergeBackAgentItemId = "merge-back-agent-message"; + const entriesWithoutExit = input.transcript.entries.filter( + (entry) => entry.type !== "runtime_exit", + ); + const continuation = [ + { + type: "expect_outbound", + label: "turn/start/merge-back-source", + frame: { + id: 9, + method: "turn/start", + params: { + threadId: sourceNativeThreadId, + input: [{ type: "text", text: input.providerMessageText }], + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, + }, + }, + }, + { + type: "emit_inbound", + label: "turn/start/merge-back-source", + frame: { + id: 9, + result: { + turn: { + id: mergeBackNativeTurnId, + items: [], + status: "inProgress", + error: null, + startedAt: 1777424041, + completedAt: null, + durationMs: null, + }, + }, + }, + }, + { + type: "emit_inbound", + label: "thread/status/changed/merge-back-source", + frame: { + method: "thread/status/changed", + params: { + threadId: sourceNativeThreadId, + status: { type: "active", activeFlags: [] }, + }, + }, + }, + { + type: "emit_inbound", + label: "turn/started/merge-back-source", + frame: { + method: "turn/started", + params: { + threadId: sourceNativeThreadId, + turn: { + id: mergeBackNativeTurnId, + items: [], + status: "inProgress", + error: null, + startedAt: 1777424041, + completedAt: null, + durationMs: null, + }, + }, + }, + }, + { + type: "emit_inbound", + label: "item/userMessage/started/merge-back-source", + frame: { + method: "item/started", + params: { + item: { + type: "userMessage", + id: mergeBackUserItemId, + content: [{ type: "text", text: input.projectedUserText, text_elements: [] }], + }, + threadId: sourceNativeThreadId, + turnId: mergeBackNativeTurnId, + }, + }, + }, + { + type: "emit_inbound", + label: "item/userMessage/completed/merge-back-source", + frame: { + method: "item/completed", + params: { + item: { + type: "userMessage", + id: mergeBackUserItemId, + content: [{ type: "text", text: input.projectedUserText, text_elements: [] }], + }, + threadId: sourceNativeThreadId, + turnId: mergeBackNativeTurnId, + }, + }, + }, + { + type: "emit_inbound", + label: "item/agentMessage/started/merge-back-source", + frame: { + method: "item/started", + params: { + item: { + type: "agentMessage", + id: mergeBackAgentItemId, + text: "", + phase: "final_answer", + memoryCitation: null, + }, + threadId: sourceNativeThreadId, + turnId: mergeBackNativeTurnId, + }, + }, + }, + { + type: "emit_inbound", + label: "item/agentMessage/delta/merge-back-source", + frame: { + method: "item/agentMessage/delta", + params: { + threadId: sourceNativeThreadId, + turnId: mergeBackNativeTurnId, + itemId: mergeBackAgentItemId, + delta: input.assistantText, + }, + }, + }, + { + type: "emit_inbound", + label: "item/agentMessage/completed/merge-back-source", + frame: { + method: "item/completed", + params: { + item: { + type: "agentMessage", + id: mergeBackAgentItemId, + text: input.assistantText, + phase: "final_answer", + memoryCitation: null, + }, + threadId: sourceNativeThreadId, + turnId: mergeBackNativeTurnId, + }, + }, + }, + { + type: "emit_inbound", + label: "thread/status/changed/merge-back-source", + frame: { + method: "thread/status/changed", + params: { threadId: sourceNativeThreadId, status: { type: "idle" } }, + }, + }, + { + type: "emit_inbound", + label: "turn/completed/merge-back-source", + frame: { + method: "turn/completed", + params: { + threadId: sourceNativeThreadId, + turn: { + id: mergeBackNativeTurnId, + items: [], + status: "completed", + error: null, + startedAt: 1777424041, + completedAt: 1777424042, + durationMs: 1000, + }, + }, + }, + }, + { type: "runtime_exit", status: "success" }, + ] satisfies ReadonlyArray; + + return { + ...input.transcript, + scenario: `${input.transcript.scenario}_merge_back`, + entries: [...entriesWithoutExit, ...continuation], + }; +} + +describe("orchestration V2 thread fork", () => { + it.effect( + "creates an idle app fork and resolves it with Codex native thread/fork on first dispatch", + () => + Effect.gen(function* () { + const rawTranscript = yield* readTranscript(); + const transcript = yield* CodexOrchestratorReplayHarness.decodeTranscript(rawTranscript); + const cwd = yield* Effect.acquireRelease(makeCheckpointWorkspace, (directory) => + Effect.service(FileSystem.FileSystem).pipe( + Effect.flatMap((fs) => fs.remove(directory, { recursive: true, force: true })), + Effect.orDie, + ), + ); + + const materialized = yield* Effect.gen(function* () { + const ids = yield* IdAllocatorV2; + const projectId = yield* ids.allocate.project({ fixtureName: "thread-fork-native" }); + const sourceThreadId = yield* ids.allocate.thread({ + fixtureName: "thread-fork-native-source", + projectId, + }); + const targetThreadId = ThreadId.make("thread-fork-native-target"); + + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native", + commandName: "thread-create-source", + }), + threadId: sourceThreadId, + projectId, + title: "Source thread", + modelSelection: CODEX_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native", + commandName: "source-message", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-source"), + text: THREAD_FORK_NATIVE_SOURCE_PROMPT, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-fork-native"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Forked thread", + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-fork-native"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Forked thread", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native", + commandName: "target-message", + }), + threadId: targetThreadId, + messageId: MessageId.make("message-thread-fork-native-target"), + text: THREAD_FORK_NATIVE_TARGET_PROMPT, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + return { + sourceThreadId, + targetThreadId, + commands, + }; + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + + const result = yield* runOrchestratorV2ProviderReplayScenario( + { + name: "thread_fork_native/codex", + transcript, + commands: materialized.commands, + steps: [ + { type: "dispatch", command: materialized.commands[0]!, await: true }, + { type: "advance_clock", duration: "1 millis" }, + { type: "dispatch", command: materialized.commands[1]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[2]!, await: true }, + { type: "dispatch", command: materialized.commands[3]!, await: true }, + { type: "dispatch", command: materialized.commands[4]!, await: true }, + { type: "await_thread_idle", threadId: materialized.targetThreadId }, + ], + projectionThreadIds: [materialized.sourceThreadId, materialized.targetThreadId], + runtimePolicyOverride: { cwd }, + }, + CodexOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime); + + const sourceProjection = result.projections.get(materialized.sourceThreadId); + const targetProjection = result.projections.get(materialized.targetThreadId); + assert.isDefined(sourceProjection); + assert.isDefined(targetProjection); + assert.equal(targetProjection.thread.lineage.parentThreadId, materialized.sourceThreadId); + assert.equal(targetProjection.thread.lineage.relationshipToParent, "fork"); + assert.lengthOf(targetProjection.providerSessions, 1); + assert.lengthOf(targetProjection.providerThreads, 1); + assert.equal( + targetProjection.providerThreads[0]?.nativeThreadRef?.nativeId, + "native-fork-thread", + ); + assert.equal( + targetProjection.providerThreads[0]?.forkedFrom?.providerThreadId, + sourceProjection.providerThreads[0]?.id, + ); + + const transfers = targetProjection.contextTransfers.filter( + (transfer) => transfer.targetThreadId === materialized.targetThreadId, + ); + assert.lengthOf(transfers, 1); + assert.equal(transfers[0]?.status, "consumed"); + assert.equal(transfers[0]?.resolution?.strategy, "native_fork"); + assert.equal(transfers[0]?.targetRunId, targetProjection.runs[0]?.id); + + const transferCreatedIndex = result.domainEvents.findIndex( + (event) => event.type === "context-transfer.created", + ); + const targetProviderSessionIndex = result.domainEvents.findIndex( + (event) => + event.threadId === materialized.targetThreadId && + event.type === "provider-session.updated", + ); + const targetRunCreatedIndex = result.domainEvents.findIndex( + (event) => event.threadId === materialized.targetThreadId && event.type === "run.created", + ); + assert.isAtLeast(transferCreatedIndex, 0); + assert.isAbove(targetProviderSessionIndex, transferCreatedIndex); + assert.isAbove( + targetProviderSessionIndex, + targetRunCreatedIndex, + "provider runtime state must be materialized only after the target run commits", + ); + + const forkEvents = result.domainEvents.filter( + (event) => event.type === "context-transfer.created", + ); + assert.lengthOf( + forkEvents, + 1, + "duplicate fork command must return the receipt without creating another transfer", + ); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + + it.effect( + "creates an idle app fork and resolves it with Claude native session fork on first dispatch", + () => + Effect.gen(function* () { + const rawTranscript = yield* readTranscript(CLAUDE_TRANSCRIPT_PATH); + const transcript = yield* ClaudeOrchestratorReplayHarness.decodeTranscript(rawTranscript); + const forkedNativeSessionId = metadataString(transcript, "forkedNativeSessionId"); + const cwd = yield* Effect.acquireRelease(makeCheckpointWorkspace, (directory) => + Effect.service(FileSystem.FileSystem).pipe( + Effect.flatMap((fs) => fs.remove(directory, { recursive: true, force: true })), + Effect.orDie, + ), + ); + + const materialized = yield* Effect.gen(function* () { + const ids = yield* IdAllocatorV2; + const projectId = yield* ids.allocate.project({ fixtureName: "thread-fork-native" }); + const sourceThreadId = yield* ids.allocate.thread({ + fixtureName: "thread-fork-native-source", + projectId, + }); + const targetThreadId = ThreadId.make("thread-fork-native-target"); + + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native", + commandName: "thread-create-source", + }), + threadId: sourceThreadId, + projectId, + title: "Source thread", + modelSelection: CLAUDE_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native", + commandName: "source-message", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-source"), + text: THREAD_FORK_NATIVE_SOURCE_PROMPT, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-fork-native"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Forked thread", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native", + commandName: "target-message", + }), + threadId: targetThreadId, + messageId: MessageId.make("message-thread-fork-native-target"), + text: THREAD_FORK_NATIVE_TARGET_PROMPT, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + return { + sourceThreadId, + targetThreadId, + commands, + }; + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + + const result = yield* runOrchestratorV2ProviderReplayScenario( + { + name: "thread_fork_native/claude", + transcript, + commands: materialized.commands, + steps: [ + { type: "dispatch", command: materialized.commands[0]!, await: true }, + { type: "advance_clock", duration: "1 millis" }, + { type: "dispatch", command: materialized.commands[1]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[2]!, await: true }, + { type: "dispatch", command: materialized.commands[3]!, await: true }, + { type: "await_thread_idle", threadId: materialized.targetThreadId }, + ], + projectionThreadIds: [materialized.sourceThreadId, materialized.targetThreadId], + runtimePolicyOverride: { cwd }, + }, + ClaudeOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime); + + const sourceProjection = result.projections.get(materialized.sourceThreadId); + const targetProjection = result.projections.get(materialized.targetThreadId); + assert.isDefined(sourceProjection); + assert.isDefined(targetProjection); + assert.equal( + targetProjection.providerThreads[0]?.nativeThreadRef?.nativeId, + forkedNativeSessionId, + ); + assert.equal( + targetProjection.providerThreads[0]?.forkedFrom?.providerThreadId, + sourceProjection.providerThreads[0]?.id, + ); + assert.include( + targetProjection.turnItems + .filter((item) => item.type === "assistant_message") + .map((item) => item.text) + .join("\n"), + "fork native ok", + ); + + const transfers = targetProjection.contextTransfers.filter( + (transfer) => transfer.targetThreadId === materialized.targetThreadId, + ); + assert.lengthOf(transfers, 1); + assert.equal(transfers[0]?.status, "consumed"); + assert.equal(transfers[0]?.resolution?.strategy, "native_fork"); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + + it.effect( + "rolls back a Codex native fork when forking from an earlier completed source turn", + () => + Effect.gen(function* () { + const rawTranscript = yield* readTranscript(PRIOR_TURN_TRANSCRIPT_PATH); + const transcript = yield* CodexOrchestratorReplayHarness.decodeTranscript(rawTranscript); + const cwd = yield* Effect.acquireRelease(makeCheckpointWorkspace, (directory) => + Effect.service(FileSystem.FileSystem).pipe( + Effect.flatMap((fs) => fs.remove(directory, { recursive: true, force: true })), + Effect.orDie, + ), + ); + + const materialized = yield* Effect.gen(function* () { + const ids = yield* IdAllocatorV2; + const projectId = yield* ids.allocate.project({ + fixtureName: "thread-fork-native-prior-turn", + }); + const sourceThreadId = yield* ids.allocate.thread({ + fixtureName: "thread-fork-native-prior-turn-source", + projectId, + }); + const targetThreadId = ThreadId.make("thread-fork-native-prior-turn-target"); + const firstRunId = ids.derive.run({ threadId: sourceThreadId, ordinal: 1 }); + + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "thread-create-source", + }), + threadId: sourceThreadId, + projectId, + title: "Source thread", + modelSelection: CODEX_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "source-message-alpha", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-alpha"), + text: THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "source-message-beta", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-beta"), + text: THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-fork-native-prior-turn"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "run", runId: firstRunId }, + title: "Forked from first response", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "target-message-repeat", + }), + threadId: targetThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-repeat"), + text: THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + return { + sourceThreadId, + targetThreadId, + commands, + }; + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + + const result = yield* runOrchestratorV2ProviderReplayScenario( + { + name: "thread_fork_native_prior_turn/codex", + transcript, + commands: materialized.commands, + steps: [ + { type: "dispatch", command: materialized.commands[0]!, await: true }, + { type: "advance_clock", duration: "1 millis" }, + { type: "dispatch", command: materialized.commands[1]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[2]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[3]!, await: true }, + { type: "dispatch", command: materialized.commands[4]!, await: true }, + { type: "await_thread_idle", threadId: materialized.targetThreadId }, + ], + projectionThreadIds: [materialized.sourceThreadId, materialized.targetThreadId], + runtimePolicyOverride: { cwd, ...CODEX_READ_ONLY_NEVER_POLICY }, + }, + CodexOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime); + + const targetProjection = result.projections.get(materialized.targetThreadId); + assert.isDefined(targetProjection); + const targetAssistantText = targetProjection.turnItems + .filter((item) => item.type === "assistant_message") + .map((item) => item.text) + .join("\n"); + assert.include(targetAssistantText, "fork boundary alpha"); + assert.notInclude( + targetAssistantText, + "fork boundary beta", + "forking from the first source run must not preserve later source turns in native Codex context", + ); + assert.equal(targetProjection.contextTransfers[0]?.resolution?.strategy, "native_fork"); + + const visibleItems = targetProjection.visibleTurnItems.map((row) => row.item); + assert.deepEqual( + visibleItems.slice(0, 2).map((item) => item.type), + ["user_message", "assistant_message"], + "fork target projection should expose inherited source history through the fork point", + ); + assert.equal( + visibleItems[0]?.type === "user_message" ? visibleItems[0].inputIntent : undefined, + "turn_start", + "inherited fork history should preserve source message intent", + ); + assert.equal(targetProjection.visibleTurnItems[0]?.visibility, "inherited"); + assert.equal(targetProjection.visibleTurnItems[1]?.visibility, "inherited"); + const forkMarker = targetProjection.visibleTurnItems.find( + (row) => row.item.type === "fork", + ); + assert.isDefined(forkMarker, "fork target projection should include a visible fork marker"); + assert.equal(forkMarker.visibility, "synthetic"); + const targetShell = result.shellSnapshot.threads.find( + (thread) => thread.id === materialized.targetThreadId, + ); + assert.isDefined(targetShell, "shell snapshot should include the fork target thread"); + assert.equal(targetShell.visibleItemCount, targetProjection.visibleTurnItems.length); + assert.equal(targetShell.lineage.relationshipToParent, "fork"); + assert.equal(targetShell.forkedFrom?.type, "run"); + + const visibleText = visibleItems + .filter((item) => item.type === "user_message" || item.type === "assistant_message") + .map((item) => item.text) + .join("\n"); + assert.include(visibleText, "fork boundary alpha"); + assert.notInclude( + visibleText, + "fork boundary beta", + "fork target visible projection must not inherit source turns after the fork point", + ); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + + it.effect("forks a Claude native session from an earlier completed source turn", () => + Effect.gen(function* () { + const rawTranscript = yield* readTranscript(CLAUDE_PRIOR_TURN_TRANSCRIPT_PATH); + const transcript = yield* ClaudeOrchestratorReplayHarness.decodeTranscript(rawTranscript); + const forkedNativeSessionId = metadataString(transcript, "forkedNativeSessionId"); + const cwd = yield* Effect.acquireRelease(makeCheckpointWorkspace, (directory) => + Effect.service(FileSystem.FileSystem).pipe( + Effect.flatMap((fs) => fs.remove(directory, { recursive: true, force: true })), + Effect.orDie, + ), + ); + + const materialized = yield* Effect.gen(function* () { + const ids = yield* IdAllocatorV2; + const projectId = yield* ids.allocate.project({ + fixtureName: "thread-fork-native-prior-turn", + }); + const sourceThreadId = yield* ids.allocate.thread({ + fixtureName: "thread-fork-native-prior-turn-source", + projectId, + }); + const targetThreadId = ThreadId.make("thread-fork-native-prior-turn-target"); + const firstRunId = ids.derive.run({ threadId: sourceThreadId, ordinal: 1 }); + + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "thread-create-source", + }), + threadId: sourceThreadId, + projectId, + title: "Source thread", + modelSelection: CLAUDE_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "source-message-alpha", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-alpha"), + text: THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "source-message-beta", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-beta"), + text: THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-fork-native-prior-turn"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "run", runId: firstRunId }, + title: "Forked from first response", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "target-message-repeat", + }), + threadId: targetThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-repeat"), + text: THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + return { + sourceThreadId, + targetThreadId, + commands, + }; + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + + const result = yield* runOrchestratorV2ProviderReplayScenario( + { + name: "thread_fork_native_prior_turn/claude", + transcript, + commands: materialized.commands, + steps: [ + { type: "dispatch", command: materialized.commands[0]!, await: true }, + { type: "advance_clock", duration: "1 millis" }, + { type: "dispatch", command: materialized.commands[1]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[2]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[3]!, await: true }, + { type: "dispatch", command: materialized.commands[4]!, await: true }, + { type: "await_thread_idle", threadId: materialized.targetThreadId }, + ], + projectionThreadIds: [materialized.sourceThreadId, materialized.targetThreadId], + runtimePolicyOverride: { cwd }, + }, + ClaudeOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime); + + const targetProjection = result.projections.get(materialized.targetThreadId); + assert.isDefined(targetProjection); + assert.equal( + targetProjection.providerThreads[0]?.nativeThreadRef?.nativeId, + forkedNativeSessionId, + ); + const targetAssistantText = targetProjection.turnItems + .filter((item) => item.type === "assistant_message") + .map((item) => item.text) + .join("\n"); + assert.include(targetAssistantText, "fork boundary alpha"); + assert.notInclude( + targetAssistantText, + "fork boundary beta", + "forking from the first source run must not preserve later source turns in native Claude context", + ); + assert.equal(targetProjection.contextTransfers[0]?.resolution?.strategy, "native_fork"); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + + it.effect("keeps a Claude native fork stable when the source thread rolls back", () => + Effect.gen(function* () { + const rawTranscript = yield* readTranscript(CLAUDE_PRIOR_TURN_TRANSCRIPT_PATH); + const transcript = yield* ClaudeOrchestratorReplayHarness.decodeTranscript(rawTranscript); + const forkedNativeSessionId = metadataString(transcript, "forkedNativeSessionId"); + const sourceAssistantMessageUuids = metadataStringArray( + transcript, + "sourceAssistantMessageUuids", + ); + const cwd = yield* Effect.acquireRelease(makeCheckpointWorkspace, (directory) => + Effect.service(FileSystem.FileSystem).pipe( + Effect.flatMap((fs) => fs.remove(directory, { recursive: true, force: true })), + Effect.orDie, + ), + ); + + const materialized = yield* Effect.gen(function* () { + const ids = yield* IdAllocatorV2; + const projectId = yield* ids.allocate.project({ + fixtureName: "thread-fork-native-prior-turn-source-rollback", + }); + const sourceThreadId = yield* ids.allocate.thread({ + fixtureName: "thread-fork-native-prior-turn-source-rollback-source", + projectId, + }); + const targetThreadId = ThreadId.make( + "thread-fork-native-prior-turn-source-rollback-target", + ); + const firstRunId = ids.derive.run({ threadId: sourceThreadId, ordinal: 1 }); + const checkpointScopeId = yield* ids.allocate.checkpointScope({ + threadId: sourceThreadId, + name: "root", + }); + const firstCheckpointId = yield* ids.allocate.checkpoint({ + checkpointScopeId, + name: "1", + }); + + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn-source-rollback", + commandName: "thread-create-source", + }), + threadId: sourceThreadId, + projectId, + title: "Source thread", + modelSelection: CLAUDE_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn-source-rollback", + commandName: "source-message-alpha", + }), + threadId: sourceThreadId, + messageId: MessageId.make( + "message-thread-fork-native-prior-turn-source-rollback-alpha", + ), + text: THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn-source-rollback", + commandName: "source-message-beta", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-source-rollback-beta"), + text: THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-fork-native-prior-turn-source-rollback"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "run", runId: firstRunId }, + title: "Forked from first response", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn-source-rollback", + commandName: "target-message-repeat", + }), + threadId: targetThreadId, + messageId: MessageId.make( + "message-thread-fork-native-prior-turn-source-rollback-repeat", + ), + text: THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "checkpoint.rollback", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn-source-rollback", + commandName: "rollback-source-to-alpha", + }), + threadId: sourceThreadId, + scopeId: checkpointScopeId, + checkpointId: firstCheckpointId, + }, + ] satisfies ReadonlyArray; + + return { + sourceThreadId, + targetThreadId, + commands, + }; + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + + const result = yield* runOrchestratorV2ProviderReplayScenario( + { + name: "thread_fork_native_prior_turn_source_rollback/claude", + transcript, + commands: materialized.commands, + steps: [ + { type: "dispatch", command: materialized.commands[0]!, await: true }, + { type: "advance_clock", duration: "1 millis" }, + { type: "dispatch", command: materialized.commands[1]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[2]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[3]!, await: true }, + { type: "dispatch", command: materialized.commands[4]!, await: true }, + { type: "await_thread_idle", threadId: materialized.targetThreadId }, + { type: "dispatch", command: materialized.commands[5]!, await: true }, + ], + projectionThreadIds: [materialized.sourceThreadId, materialized.targetThreadId], + runtimePolicyOverride: { cwd }, + }, + ClaudeOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime); + + const sourceProjection = result.projections.get(materialized.sourceThreadId); + const targetProjection = result.projections.get(materialized.targetThreadId); + assert.isDefined(sourceProjection); + assert.isDefined(targetProjection); + + assert.equal( + sourceProjection.runs.map((run) => run.status).join(","), + "completed,rolled_back", + ); + assert.equal( + sourceProjection.providerThreads[0]?.nativeConversationHeadRef?.nativeId, + sourceAssistantMessageUuids[0], + "source rollback should persist the Claude resume cursor for the first assistant message", + ); + assert.equal( + targetProjection.providerThreads[0]?.nativeThreadRef?.nativeId, + forkedNativeSessionId, + ); + assert.equal(targetProjection.providerThreads[0]?.nativeConversationHeadRef, null); + + const sourceVisibleText = userAndAssistantText(sourceProjection); + assert.include(sourceVisibleText, "fork boundary alpha"); + assert.notInclude(sourceVisibleText, "fork boundary beta"); + + const targetVisibleText = userAndAssistantText(targetProjection); + assert.include(targetVisibleText, "fork boundary alpha"); + assert.notInclude( + targetVisibleText, + "fork boundary beta", + "source rollback must not cause the fork target to inherit turns past its fork point", + ); + assert.equal(targetProjection.contextTransfers[0]?.resolution?.strategy, "native_fork"); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + + it.effect("rolls back a Claude native fork to an earlier fork-local turn", () => + Effect.gen(function* () { + const rawTranscript = yield* readTranscript(CLAUDE_FORK_LOCAL_ROLLBACK_TRANSCRIPT_PATH); + const transcript = yield* ClaudeOrchestratorReplayHarness.decodeTranscript(rawTranscript); + const forkedNativeSessionId = metadataString(transcript, "forkedNativeSessionId"); + const resumeSessionAt = metadataString(transcript, "resumeSessionAt"); + const prompts = metadataStringArray(transcript, "prompts"); + const [sourcePrompt, forkFirstPrompt, forkSecondPrompt, repeatPrompt] = prompts; + if ( + sourcePrompt === undefined || + forkFirstPrompt === undefined || + forkSecondPrompt === undefined || + repeatPrompt === undefined + ) { + throw new Error("Claude fork-local rollback transcript is missing expected prompts."); + } + + const cwd = yield* Effect.acquireRelease(makeCheckpointWorkspace, (directory) => + Effect.service(FileSystem.FileSystem).pipe( + Effect.flatMap((fs) => fs.remove(directory, { recursive: true, force: true })), + Effect.orDie, + ), + ); + + const materialized = yield* Effect.gen(function* () { + const ids = yield* IdAllocatorV2; + const projectId = yield* ids.allocate.project({ + fixtureName: "thread-fork-native-fork-local-rollback", + }); + const sourceThreadId = yield* ids.allocate.thread({ + fixtureName: "thread-fork-native-fork-local-rollback-source", + projectId, + }); + const targetThreadId = ThreadId.make("thread-fork-native-fork-local-rollback-target"); + const targetCheckpointScopeId = yield* ids.allocate.checkpointScope({ + threadId: targetThreadId, + name: "root", + }); + const targetFirstCheckpointId = yield* ids.allocate.checkpoint({ + checkpointScopeId: targetCheckpointScopeId, + name: "1", + }); + + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-fork-local-rollback", + commandName: "thread-create-source", + }), + threadId: sourceThreadId, + projectId, + title: "Source thread", + modelSelection: CLAUDE_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-fork-local-rollback", + commandName: "source-message", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-fork-local-rollback-source"), + text: sourcePrompt, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-fork-native-fork-local-rollback"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Forked thread", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-fork-local-rollback", + commandName: "fork-first-message", + }), + threadId: targetThreadId, + messageId: MessageId.make("message-thread-fork-native-fork-local-rollback-first"), + text: forkFirstPrompt, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-fork-local-rollback", + commandName: "fork-second-message", + }), + threadId: targetThreadId, + messageId: MessageId.make("message-thread-fork-native-fork-local-rollback-second"), + text: forkSecondPrompt, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "checkpoint.rollback", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-fork-local-rollback", + commandName: "rollback-fork-to-first", + }), + threadId: targetThreadId, + scopeId: targetCheckpointScopeId, + checkpointId: targetFirstCheckpointId, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-fork-local-rollback", + commandName: "fork-repeat-after-rollback", + }), + threadId: targetThreadId, + messageId: MessageId.make("message-thread-fork-native-fork-local-rollback-repeat"), + text: repeatPrompt, + attachments: [], + modelSelection: CLAUDE_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + return { + sourceThreadId, + targetThreadId, + commands, + }; + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + + const result = yield* runOrchestratorV2ProviderReplayScenario( + { + name: "thread_fork_native_fork_local_rollback/claude", + transcript, + commands: materialized.commands, + steps: [ + { type: "dispatch", command: materialized.commands[0]!, await: true }, + { type: "advance_clock", duration: "1 millis" }, + { type: "dispatch", command: materialized.commands[1]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[2]!, await: true }, + { type: "dispatch", command: materialized.commands[3]!, await: true }, + { type: "await_thread_idle", threadId: materialized.targetThreadId }, + { type: "dispatch", command: materialized.commands[4]!, await: true }, + { type: "await_thread_idle", threadId: materialized.targetThreadId }, + { type: "dispatch", command: materialized.commands[5]!, await: true }, + { type: "dispatch", command: materialized.commands[6]!, await: true }, + { type: "await_thread_idle", threadId: materialized.targetThreadId }, + ], + projectionThreadIds: [materialized.sourceThreadId, materialized.targetThreadId], + runtimePolicyOverride: { cwd }, + }, + ClaudeOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime); + + const targetProjection = result.projections.get(materialized.targetThreadId); + assert.isDefined(targetProjection); + assert.equal( + targetProjection.providerThreads[0]?.nativeThreadRef?.nativeId, + forkedNativeSessionId, + ); + assert.isString(resumeSessionAt); + + const targetVisibleText = userAndAssistantText(targetProjection); + assert.include(targetVisibleText, "fork local source alpha"); + assert.include(targetVisibleText, "fork local first"); + assert.notInclude( + targetVisibleText, + "fork local second", + "rolled back fork-local turns must disappear from the projected fork thread", + ); + + const targetVisibleAssistantText = targetProjection.visibleTurnItems + .map((row) => row.item) + .filter((item) => item.type === "assistant_message") + .map((item) => item.text) + .join("\n"); + assert.notInclude( + targetVisibleAssistantText, + "fork local second", + "resumeSessionAt must reopen the Claude fork before the rolled-back second fork turn", + ); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + + // Covered with recorded Codex and Claude provider transcripts in ThreadMergeBack.integration. + it.skip("merges a fork delta back into the source thread through context handoff", () => + Effect.gen(function* () { + const rawTranscript = yield* readTranscript(PRIOR_TURN_TRANSCRIPT_PATH); + const forkNativeThreadId = "019dd6ba-47b7-7092-8688-9cf7fe5f6498"; + const sourceNativeThreadId = "019dd6ba-2681-7bf0-b051-141b0cbcbb27"; + const forkRepeatNativeTurnId = "019dd6ba-47eb-7041-ad45-5abe752c28c9"; + const forkPrompt = THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT; + const mergeBackPrompt = "Acknowledge the fork context with exactly: merge back acknowledged"; + const mergeBackAssistantText = "merge back acknowledged"; + const materialized = yield* Effect.gen(function* () { + const ids = yield* IdAllocatorV2; + const projectId = yield* ids.allocate.project({ + fixtureName: "thread-fork-native-prior-turn", + }); + const sourceThreadId = yield* ids.allocate.thread({ + fixtureName: "thread-fork-native-prior-turn-source", + projectId, + }); + const targetThreadId = ThreadId.make("thread-fork-native-prior-turn-target"); + const firstRunId = ids.derive.run({ threadId: sourceThreadId, ordinal: 1 }); + const forkRunId = ids.derive.run({ threadId: targetThreadId, ordinal: 1 }); + + const commands = [ + { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "thread-create-source", + }), + threadId: sourceThreadId, + projectId, + title: "Source thread", + modelSelection: CODEX_MODEL_SELECTION, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "source-message-alpha", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-alpha"), + text: THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "source-message-beta", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-beta"), + text: THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-fork-native-prior-turn"), + sourceThreadId, + targetThreadId, + sourcePoint: { type: "run", runId: firstRunId }, + title: "Forked from first response", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "target-message-repeat", + }), + threadId: targetThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-repeat"), + text: forkPrompt, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.merge_back", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-merge-back-native-prior-turn-stale"), + sourceThreadId: targetThreadId, + targetThreadId: sourceThreadId, + sourcePoint: { type: "run", runId: forkRunId }, + }, + { + type: "thread.merge_back", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("command-thread-merge-back-native-prior-turn"), + sourceThreadId: targetThreadId, + targetThreadId: sourceThreadId, + sourcePoint: { type: "run", runId: forkRunId }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: "thread-fork-native-prior-turn", + commandName: "source-message-merge-back", + }), + threadId: sourceThreadId, + messageId: MessageId.make("message-thread-fork-native-prior-turn-merge-back"), + text: mergeBackPrompt, + attachments: [], + modelSelection: CODEX_MODEL_SELECTION, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + + return { + sourceThreadId, + targetThreadId, + firstRunId, + forkRunId, + commands, + }; + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + const expectedSummary = makeExpectedForkDeltaSummary({ + sourceThreadId: materialized.targetThreadId, + targetThreadId: materialized.sourceThreadId, + forkUserText: forkPrompt, + forkAssistantText: findCompletedAgentMessageText({ + transcript: rawTranscript, + threadId: forkNativeThreadId, + turnId: forkRepeatNativeTurnId, + }), + }); + const providerMessageText = [ + "Context handoff (merge_back / fork_delta_summary):", + expectedSummary, + "", + "User message:", + mergeBackPrompt, + ].join("\n"); + const transcript = yield* CodexOrchestratorReplayHarness.decodeTranscript( + transcriptWithMergeBackContinuation({ + transcript: rawTranscript, + providerMessageText, + projectedUserText: mergeBackPrompt, + assistantText: mergeBackAssistantText, + }), + ); + const cwd = yield* Effect.acquireRelease(makeCheckpointWorkspace, (directory) => + Effect.service(FileSystem.FileSystem).pipe( + Effect.flatMap((fs) => fs.remove(directory, { recursive: true, force: true })), + Effect.orDie, + ), + ); + + const result = yield* runOrchestratorV2ProviderReplayScenario( + { + name: "thread_fork_native_prior_turn_merge_back/codex", + transcript, + commands: materialized.commands, + steps: [ + { type: "dispatch", command: materialized.commands[0]!, await: true }, + { type: "advance_clock", duration: "1 millis" }, + { type: "dispatch", command: materialized.commands[1]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[2]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + { type: "dispatch", command: materialized.commands[3]!, await: true }, + { type: "dispatch", command: materialized.commands[4]!, await: true }, + { type: "await_thread_idle", threadId: materialized.targetThreadId }, + { type: "dispatch", command: materialized.commands[5]!, await: true }, + { type: "dispatch", command: materialized.commands[6]!, await: true }, + { type: "dispatch", command: materialized.commands[7]!, await: true }, + { type: "await_thread_idle", threadId: materialized.sourceThreadId }, + ], + projectionThreadIds: [materialized.sourceThreadId, materialized.targetThreadId], + runtimePolicyOverride: { cwd, ...CODEX_READ_ONLY_NEVER_POLICY }, + }, + CodexOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime); + + const sourceProjection = result.projections.get(materialized.sourceThreadId); + const forkProjection = result.projections.get(materialized.targetThreadId); + assert.isDefined(sourceProjection); + assert.isDefined(forkProjection); + + assert.equal( + sourceProjection.providerThreads[0]?.nativeThreadRef?.nativeId, + sourceNativeThreadId, + "merge-back should continue the original source provider thread", + ); + assert.lengthOf(sourceProjection.runs, 3); + assert.equal( + sourceProjection.runs[2]?.contextHandoffId, + sourceProjection.contextHandoffs[0]?.id, + ); + assert.lengthOf(sourceProjection.contextHandoffs, 1); + const handoff = sourceProjection.contextHandoffs[0]!; + assert.equal(handoff.strategy, "fork_delta_summary"); + assert.equal(handoff.status, "ready"); + assert.equal(handoff.threadId, materialized.sourceThreadId); + assert.equal(handoff.targetRunId, sourceProjection.runs[2]?.id); + assert.deepEqual(handoff.coveredRunOrdinals, { from: 1, to: 1 }); + assert.include(handoff.summaryText, "Merge-back context from forked conversation."); + assert.include(handoff.summaryText, "Repeat the user-visible conversation"); + assert.notInclude( + handoff.summaryText, + "fork boundary beta", + "merge-back context should summarize only fork-local delta after the source point", + ); + assert.equal(handoff.summaryText, expectedSummary); + + const mergeBackTransfers = sourceProjection.contextTransfers.filter( + (transfer) => transfer.type === "merge_back", + ); + assert.lengthOf(mergeBackTransfers, 2); + const supersededTransfer = mergeBackTransfers.find( + (transfer) => transfer.status === "superseded", + ); + const mergeBackTransfer = mergeBackTransfers.find( + (transfer) => transfer.status === "consumed", + ); + assert.isDefined(supersededTransfer); + assert.isDefined(mergeBackTransfer); + assert.include( + supersededTransfer.error ?? "", + mergeBackTransfer.id, + "newer merge-back preparation should supersede the previous pending transfer", + ); + assert.equal(mergeBackTransfer.sourceThreadId, materialized.targetThreadId); + assert.equal(mergeBackTransfer.targetThreadId, materialized.sourceThreadId); + assert.equal(mergeBackTransfer.sourcePoint.runId, materialized.forkRunId); + assert.equal(mergeBackTransfer.basePoint?.runId, materialized.firstRunId); + assert.equal(mergeBackTransfer.status, "consumed"); + assert.equal(mergeBackTransfer.targetRunId, sourceProjection.runs[2]?.id); + assert.equal(mergeBackTransfer.resolution?.strategy, "fork_delta_context"); + assert.equal( + mergeBackTransfer.resolution?.strategy === "fork_delta_context" + ? mergeBackTransfer.resolution.contextHandoffId + : null, + handoff.id, + ); + + const mergeBackRunItems = sourceProjection.turnItems.filter( + (item) => item.runId === sourceProjection.runs[2]?.id, + ); + assert.deepEqual( + mergeBackRunItems.map((item) => item.type), + ["handoff", "user_message", "assistant_message", "checkpoint"], + ); + const handoffItem = mergeBackRunItems[0]; + assert.equal(handoffItem?.type, "handoff"); + assert.equal( + handoffItem?.type === "handoff" ? handoffItem.contextHandoffId : null, + handoff.id, + ); + assert.equal(handoffItem?.ordinal, 299); + const mergeUserItem = mergeBackRunItems.find((item) => item.type === "user_message"); + assert.equal( + mergeUserItem?.type === "user_message" ? mergeUserItem.text : null, + mergeBackPrompt, + ); + assert.notInclude( + sourceProjection.turnItems + .filter((item) => item.type === "user_message") + .map((item) => item.text) + .join("\n"), + "Context handoff", + "context handoff text should be provider input only, not projected user-visible message text", + ); + assert.include( + sourceProjection.turnItems + .filter((item) => item.type === "assistant_message") + .map((item) => item.text) + .join("\n"), + mergeBackAssistantText, + ); + + const visibleTypes = sourceProjection.visibleTurnItems.map((row) => row.item.type); + assert.includeMembers(visibleTypes, ["handoff", "user_message", "assistant_message"]); + const sourceShell = result.shellSnapshot.threads.find( + (thread) => thread.id === materialized.sourceThreadId, + ); + assert.isDefined(sourceShell); + assert.equal(sourceShell.visibleItemCount, sourceProjection.visibleTurnItems.length); + + assert.include( + forkProjection.turnItems + .filter((item) => item.type === "user_message") + .map((item) => item.text) + .join("\n"), + forkPrompt, + "merge-back should not remove fork-local history", + ); + assert.equal(forkProjection.contextTransfers[0]?.resolution?.strategy, "native_fork"); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer))); +}); diff --git a/apps/server/src/orchestration-v2/testkit/ThreadMergeBack.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ThreadMergeBack.integration.test.ts new file mode 100644 index 00000000000..736428f9c3c --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ThreadMergeBack.integration.test.ts @@ -0,0 +1,662 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import { + CommandId, + MessageId, + type ModelSelection, + type OrchestrationV2Command, + type OrchestrationV2ThreadProjection, + ProjectId, + ProviderInstanceId, + ProviderDriverKind, + type ProviderReplayTranscript, + ThreadId, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; + +import { ClaudeOrchestratorReplayHarness } from "../Adapters/ClaudeAdapterV2.testkit.ts"; +import { CodexOrchestratorReplayHarness } from "../Adapters/CodexAdapterV2.testkit.ts"; +import { IdAllocatorV2, layer as idAllocatorLayer } from "../IdAllocator.ts"; +import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts"; +import { + THREAD_MERGE_BACK_FORK_PROMPT, + THREAD_MERGE_BACK_HANDOFF_PROMPT, + THREAD_MERGE_BACK_RECALL, + THREAD_MERGE_BACK_RECALL_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_RECALL, + THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT, + THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT, + THREAD_MERGE_BACK_SOURCE_PROMPT, +} from "./fixtures/shared.ts"; +import { runOrchestratorV2ProviderReplayScenario } from "./ProviderReplayHarness.ts"; +import { makeCheckpointWorkspace } from "./ReplayFixtureWorkspace.ts"; +import { decodeProviderReplayNdjson } from "./ReplayTranscriptNdjson.ts"; + +const CODEX_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", +} as const; +const CLAUDE_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-sonnet-4-6", +} as const; +const MERGE_BACK_USER_TEXT = + "Retain the transferred fork marker for later. Respond with exactly: merge delta stored"; +const FIRST_SIBLING_MERGE_USER_TEXT = + "Retain the first transferred marker for later. Respond with exactly: first merge delta stored"; +const SECOND_SIBLING_MERGE_USER_TEXT = + "Retain the second transferred marker for later. Respond with exactly: second merge delta stored"; + +interface ProviderVariant { + readonly driver: ProviderDriverKind; + readonly modelSelection: ModelSelection; +} + +const PROVIDERS: ReadonlyArray = [ + { + driver: ProviderDriverKind.make("codex"), + modelSelection: CODEX_MODEL_SELECTION, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + modelSelection: CLAUDE_MODEL_SELECTION, + }, +]; + +function transcriptPath(scenario: string, driver: ProviderDriverKind): string { + const fileName = driver === "codex" ? "codex_transcript.ndjson" : "claude_transcript.ndjson"; + return `${import.meta.dirname}/fixtures/${scenario}/${fileName}`; +} + +function readTranscript(scenario: string, driver: ProviderDriverKind) { + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const text = yield* fs.readFileString(transcriptPath(scenario, driver)); + return yield* decodeProviderReplayNdjson(text); + }); +} + +function compactExpectedText(text: string, maxLength = 240): string { + const compacted = text.replace(/\s+/gu, " ").trim(); + return compacted.length <= maxLength ? compacted : `${compacted.slice(0, maxLength - 3)}...`; +} + +function forkDeltaSummary(input: { + readonly sourceThreadId: ThreadId; + readonly targetThreadId: ThreadId; + readonly forkUserText: string; + readonly forkAssistantText: string; +}): string { + return [ + "Merge-back context from forked conversation.", + `Source thread: ${input.sourceThreadId}`, + `Target thread: ${input.targetThreadId}`, + "Covered fork runs: 1-1", + "", + "Fork delta:", + `- User: ${compactExpectedText(input.forkUserText)}`, + `- Assistant: ${compactExpectedText(input.forkAssistantText)}`, + "- Checkpoint: 0 files", + ].join("\n"); +} + +function providerMessage(summary: string, userText: string): string { + return [ + "Context handoff (merge_back / fork_delta_summary):", + summary, + "", + "User message:", + userText, + ].join("\n"); +} + +function replaceExactString(value: unknown, from: string, to: string): unknown { + if (value === from) { + return to; + } + if (Array.isArray(value)) { + return value.map((entry) => replaceExactString(entry, from, to)); + } + if (typeof value !== "object" || value === null) { + return value; + } + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [key, replaceExactString(entry, from, to)]), + ); +} + +function parameterizeHandoffs( + transcript: ProviderReplayTranscript, + replacements: ReadonlyArray, +): ProviderReplayTranscript { + return replacements.reduce( + (current, [recorded, generated]) => + replaceExactString(current, recorded, generated) as ProviderReplayTranscript, + transcript, + ); +} + +function nativeSourceThreadId(transcript: ProviderReplayTranscript): string { + if (transcript.provider === "claudeAgent") { + const value = transcript.metadata?.nativeSessionId; + if (typeof value !== "string") { + throw new Error(`${transcript.scenario} is missing metadata.nativeSessionId.`); + } + return value; + } + for (const entry of transcript.entries) { + if (entry.type !== "emit_inbound") { + continue; + } + const frame = entry.frame as { + readonly method?: unknown; + readonly params?: { readonly thread?: { readonly id?: unknown } }; + }; + if (frame.method === "thread/started" && typeof frame.params?.thread?.id === "string") { + return frame.params.thread.id; + } + } + throw new Error(`${transcript.scenario} is missing the source native thread id.`); +} + +function visibleConversationText(projection: OrchestrationV2ThreadProjection): string { + return projection.visibleTurnItems + .flatMap(({ item }) => + item.type === "user_message" || item.type === "assistant_message" ? [item.text] : [], + ) + .join("\n"); +} + +function normalizePipeSpacing(text: string): string { + return text.replace(/\s*\|\s*/gu, "|"); +} + +function makeCreateCommand(input: { + readonly commandId: CommandId; + readonly threadId: ThreadId; + readonly projectId: ProjectId; + readonly modelSelection: ModelSelection; +}): OrchestrationV2Command { + return { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: input.commandId, + threadId: input.threadId, + projectId: input.projectId, + title: "Merge-back source", + modelSelection: input.modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }; +} + +describe("orchestration V2 merge-back provider replay", () => { + for (const variant of PROVIDERS) { + it.effect(`merges one fork delta back into the original ${variant.driver} thread`, () => + Effect.gen(function* () { + const rawTranscript = yield* readTranscript("thread_merge_back_continue", variant.driver); + const materialized = yield* Effect.gen(function* () { + const ids = yield* IdAllocatorV2; + const projectId = yield* ids.allocate.project({ + fixtureName: `thread-merge-back-${variant.driver}`, + }); + const sourceThreadId = yield* ids.allocate.thread({ + fixtureName: `thread-merge-back-${variant.driver}-source`, + projectId, + }); + const forkThreadId = ThreadId.make(`thread-merge-back-${variant.driver}-fork`); + const forkRunId = ids.derive.run({ threadId: forkThreadId, ordinal: 1 }); + const commands = [ + makeCreateCommand({ + commandId: yield* ids.allocate.command({ + fixtureName: `thread-merge-back-${variant.driver}`, + commandName: "create", + }), + threadId: sourceThreadId, + projectId, + modelSelection: variant.modelSelection, + }), + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: `thread-merge-back-${variant.driver}`, + commandName: "source", + }), + threadId: sourceThreadId, + messageId: MessageId.make(`message-merge-source-${variant.driver}`), + text: THREAD_MERGE_BACK_SOURCE_PROMPT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make(`command-merge-fork-${variant.driver}`), + sourceThreadId, + targetThreadId: forkThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Merge-back fork", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: `thread-merge-back-${variant.driver}`, + commandName: "fork-delta", + }), + threadId: forkThreadId, + messageId: MessageId.make(`message-merge-fork-${variant.driver}`), + text: THREAD_MERGE_BACK_FORK_PROMPT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.merge_back", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make(`command-merge-back-${variant.driver}`), + sourceThreadId: forkThreadId, + targetThreadId: sourceThreadId, + sourcePoint: { type: "run", runId: forkRunId }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: `thread-merge-back-${variant.driver}`, + commandName: "consume-merge", + }), + threadId: sourceThreadId, + messageId: MessageId.make(`message-consume-merge-${variant.driver}`), + text: MERGE_BACK_USER_TEXT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* ids.allocate.command({ + fixtureName: `thread-merge-back-${variant.driver}`, + commandName: "recall", + }), + threadId: sourceThreadId, + messageId: MessageId.make(`message-merge-recall-${variant.driver}`), + text: THREAD_MERGE_BACK_RECALL_PROMPT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + return { commands, sourceThreadId, forkThreadId, forkRunId }; + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + const summary = forkDeltaSummary({ + sourceThreadId: materialized.forkThreadId, + targetThreadId: materialized.sourceThreadId, + forkUserText: THREAD_MERGE_BACK_FORK_PROMPT, + forkAssistantText: "merge fork stored", + }); + const generatedProviderMessage = providerMessage(summary, MERGE_BACK_USER_TEXT); + const parameterizedTranscript = parameterizeHandoffs(rawTranscript, [ + [THREAD_MERGE_BACK_HANDOFF_PROMPT, generatedProviderMessage], + ]); + const cwd = yield* Effect.acquireRelease( + Effect.promise(() => makeCheckpointWorkspace(`merge-back-${variant.driver}`)), + (directory) => + Effect.service(FileSystem.FileSystem).pipe( + Effect.flatMap((fs) => fs.remove(directory, { recursive: true, force: true })), + Effect.orDie, + ), + ); + const scenario = { + name: `thread_merge_back_continue/${variant.driver}`, + commands: materialized.commands, + steps: [ + { type: "dispatch" as const, command: materialized.commands[0]!, await: true }, + { type: "advance_clock" as const, duration: "1 millis" as const }, + { type: "dispatch" as const, command: materialized.commands[1]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.sourceThreadId }, + { type: "dispatch" as const, command: materialized.commands[2]!, await: true }, + { type: "dispatch" as const, command: materialized.commands[3]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.forkThreadId }, + { type: "dispatch" as const, command: materialized.commands[4]!, await: true }, + { type: "dispatch" as const, command: materialized.commands[5]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.sourceThreadId }, + { type: "dispatch" as const, command: materialized.commands[6]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.sourceThreadId }, + ], + projectionThreadIds: [materialized.sourceThreadId, materialized.forkThreadId], + runtimePolicyOverride: { cwd }, + }; + const result = + variant.driver === "codex" + ? yield* runOrchestratorV2ProviderReplayScenario( + { + ...scenario, + transcript: + yield* CodexOrchestratorReplayHarness.decodeTranscript(parameterizedTranscript), + }, + CodexOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime) + : yield* runOrchestratorV2ProviderReplayScenario( + { + ...scenario, + transcript: + yield* ClaudeOrchestratorReplayHarness.decodeTranscript( + parameterizedTranscript, + ), + }, + ClaudeOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime); + + const source = result.projections.get(materialized.sourceThreadId); + const fork = result.projections.get(materialized.forkThreadId); + assert.isDefined(source); + assert.isDefined(fork); + assert.equal( + source.providerThreads[0]?.nativeThreadRef?.nativeId, + nativeSourceThreadId(rawTranscript), + ); + assert.lengthOf(source.contextHandoffs, 1); + assert.equal(source.contextHandoffs[0]?.summaryText, summary); + assert.equal(source.contextHandoffs[0]?.strategy, "fork_delta_summary"); + const mergeTransfer = source.contextTransfers.find( + (transfer) => transfer.type === "merge_back", + ); + assert.isDefined(mergeTransfer); + assert.equal(mergeTransfer.status, "consumed"); + assert.equal(mergeTransfer.sourcePoint.runId, materialized.forkRunId); + assert.equal(mergeTransfer.resolution?.strategy, "fork_delta_context"); + assert.include( + normalizePipeSpacing(visibleConversationText(source)), + THREAD_MERGE_BACK_RECALL, + ); + assert.notInclude(visibleConversationText(source), "Context handoff ("); + assert.include(visibleConversationText(fork), "merge fork stored"); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + + it.effect(`merges two sibling fork deltas into the original ${variant.driver} thread`, () => + Effect.gen(function* () { + const rawTranscript = yield* readTranscript("thread_merge_back_siblings", variant.driver); + const materialized = yield* Effect.gen(function* () { + const ids = yield* IdAllocatorV2; + const fixtureName = `thread-merge-back-siblings-${variant.driver}`; + const projectId = yield* ids.allocate.project({ fixtureName }); + const sourceThreadId = yield* ids.allocate.thread({ + fixtureName: `${fixtureName}-source`, + projectId, + }); + const firstForkThreadId = ThreadId.make(`${fixtureName}-first-fork`); + const secondForkThreadId = ThreadId.make(`${fixtureName}-second-fork`); + const firstForkRunId = ids.derive.run({ threadId: firstForkThreadId, ordinal: 1 }); + const secondForkRunId = ids.derive.run({ threadId: secondForkThreadId, ordinal: 1 }); + const commandId = (commandName: string) => + ids.allocate.command({ fixtureName, commandName }); + const commands = [ + makeCreateCommand({ + commandId: yield* commandId("create"), + threadId: sourceThreadId, + projectId, + modelSelection: variant.modelSelection, + }), + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("source"), + threadId: sourceThreadId, + messageId: MessageId.make(`message-${fixtureName}-source`), + text: THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("first-fork"), + sourceThreadId, + targetThreadId: firstForkThreadId, + sourcePoint: { type: "latest_stable" }, + title: "First merge-back sibling", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("first-fork-delta"), + threadId: firstForkThreadId, + messageId: MessageId.make(`message-${fixtureName}-first-fork`), + text: THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("second-fork"), + sourceThreadId, + targetThreadId: secondForkThreadId, + sourcePoint: { type: "latest_stable" }, + title: "Second merge-back sibling", + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("second-fork-delta"), + threadId: secondForkThreadId, + messageId: MessageId.make(`message-${fixtureName}-second-fork`), + text: THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.merge_back", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("merge-first"), + sourceThreadId: firstForkThreadId, + targetThreadId: sourceThreadId, + sourcePoint: { type: "run", runId: firstForkRunId }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("consume-first"), + threadId: sourceThreadId, + messageId: MessageId.make(`message-${fixtureName}-consume-first`), + text: FIRST_SIBLING_MERGE_USER_TEXT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "thread.merge_back", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("merge-second"), + sourceThreadId: secondForkThreadId, + targetThreadId: sourceThreadId, + sourcePoint: { type: "run", runId: secondForkRunId }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("consume-second"), + threadId: sourceThreadId, + messageId: MessageId.make(`message-${fixtureName}-consume-second`), + text: SECOND_SIBLING_MERGE_USER_TEXT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: yield* commandId("recall"), + threadId: sourceThreadId, + messageId: MessageId.make(`message-${fixtureName}-recall`), + text: THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT, + attachments: [], + modelSelection: variant.modelSelection, + dispatchMode: { type: "start_immediately" }, + }, + ] satisfies ReadonlyArray; + return { + commands, + sourceThreadId, + firstForkThreadId, + secondForkThreadId, + firstForkRunId, + secondForkRunId, + }; + }).pipe(Effect.provide(idAllocatorLayer), provideDeterministicTestRuntime); + const firstSummary = forkDeltaSummary({ + sourceThreadId: materialized.firstForkThreadId, + targetThreadId: materialized.sourceThreadId, + forkUserText: THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT, + forkAssistantText: "first merge sibling stored", + }); + const secondSummary = forkDeltaSummary({ + sourceThreadId: materialized.secondForkThreadId, + targetThreadId: materialized.sourceThreadId, + forkUserText: THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT, + forkAssistantText: "second merge sibling stored", + }); + const transcript = parameterizeHandoffs(rawTranscript, [ + [ + THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT, + providerMessage(firstSummary, FIRST_SIBLING_MERGE_USER_TEXT), + ], + [ + THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT, + providerMessage(secondSummary, SECOND_SIBLING_MERGE_USER_TEXT), + ], + ]); + const cwd = yield* Effect.acquireRelease( + Effect.promise(() => makeCheckpointWorkspace(`merge-back-siblings-${variant.driver}`)), + (directory) => + Effect.service(FileSystem.FileSystem).pipe( + Effect.flatMap((fs) => fs.remove(directory, { recursive: true, force: true })), + Effect.orDie, + ), + ); + const steps = [ + { type: "dispatch" as const, command: materialized.commands[0]!, await: true }, + { type: "advance_clock" as const, duration: "1 millis" as const }, + { type: "dispatch" as const, command: materialized.commands[1]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.sourceThreadId }, + { type: "dispatch" as const, command: materialized.commands[2]!, await: true }, + { type: "dispatch" as const, command: materialized.commands[3]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.firstForkThreadId }, + { type: "dispatch" as const, command: materialized.commands[4]!, await: true }, + { type: "dispatch" as const, command: materialized.commands[5]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.secondForkThreadId }, + { type: "dispatch" as const, command: materialized.commands[6]!, await: true }, + { type: "dispatch" as const, command: materialized.commands[7]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.sourceThreadId }, + { type: "dispatch" as const, command: materialized.commands[8]!, await: true }, + { type: "dispatch" as const, command: materialized.commands[9]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.sourceThreadId }, + { type: "dispatch" as const, command: materialized.commands[10]!, await: true }, + { type: "await_thread_idle" as const, threadId: materialized.sourceThreadId }, + ]; + const scenario = { + name: `thread_merge_back_siblings/${variant.driver}`, + commands: materialized.commands, + steps, + projectionThreadIds: [ + materialized.sourceThreadId, + materialized.firstForkThreadId, + materialized.secondForkThreadId, + ], + runtimePolicyOverride: { cwd }, + }; + const result = + variant.driver === "codex" + ? yield* runOrchestratorV2ProviderReplayScenario( + { + ...scenario, + transcript: yield* CodexOrchestratorReplayHarness.decodeTranscript(transcript), + }, + CodexOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime) + : yield* runOrchestratorV2ProviderReplayScenario( + { + ...scenario, + transcript: yield* ClaudeOrchestratorReplayHarness.decodeTranscript(transcript), + }, + ClaudeOrchestratorReplayHarness, + ).pipe(provideDeterministicTestRuntime); + + const source = result.projections.get(materialized.sourceThreadId); + const firstFork = result.projections.get(materialized.firstForkThreadId); + const secondFork = result.projections.get(materialized.secondForkThreadId); + assert.isDefined(source); + assert.isDefined(firstFork); + assert.isDefined(secondFork); + assert.equal( + source.providerThreads[0]?.nativeThreadRef?.nativeId, + nativeSourceThreadId(rawTranscript), + ); + const firstForkNativeId = firstFork.providerThreads[0]?.nativeThreadRef?.nativeId; + const secondForkNativeId = secondFork.providerThreads[0]?.nativeThreadRef?.nativeId; + assert.isDefined(firstForkNativeId); + assert.isDefined(secondForkNativeId); + assert.notEqual(firstForkNativeId, secondForkNativeId); + assert.notEqual(firstForkNativeId, source.providerThreads[0]?.nativeThreadRef?.nativeId); + assert.notEqual(secondForkNativeId, source.providerThreads[0]?.nativeThreadRef?.nativeId); + assert.deepEqual( + source.contextHandoffs.map((handoff) => handoff.summaryText), + [firstSummary, secondSummary], + ); + const mergeTransfers = source.contextTransfers.filter( + (transfer) => transfer.type === "merge_back", + ); + assert.lengthOf(mergeTransfers, 2); + assert.deepEqual( + mergeTransfers.map((transfer) => transfer.status), + ["consumed", "consumed"], + ); + assert.deepEqual( + mergeTransfers.map((transfer) => transfer.sourcePoint.runId), + [materialized.firstForkRunId, materialized.secondForkRunId], + ); + assert.include( + normalizePipeSpacing(visibleConversationText(source)), + THREAD_MERGE_BACK_SIBLINGS_RECALL, + ); + assert.notInclude(visibleConversationText(source), "Context handoff ("); + assert.include(visibleConversationText(firstFork), "first merge sibling stored"); + assert.notInclude(visibleConversationText(firstFork), "second merge sibling stored"); + assert.include(visibleConversationText(secondFork), "second merge sibling stored"); + assert.notInclude(visibleConversationText(secondFork), "first merge sibling stored"); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + } +}); diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/acp_elicitation/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/acp_elicitation/grok_transcript.ndjson new file mode 100644 index 00000000000..674723056c5 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/acp_elicitation/grok_transcript.ndjson @@ -0,0 +1,11 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"acp_elicitation","metadata":{"generatedBy":"protocol-semantic-fixture","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"generic-acp-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"}]}}}} +{"type":"expect_outbound","label":"session.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Use request_user_input to ask one multiple-choice clarifying question about whether this fixture should prefer strict schemas or UI flexibility. After receiving the answer, respond exactly: plan questions fixture complete"}]}}} +{"type":"emit_inbound","label":"elicitation.request","frame":{"kind":"request","method":"session/elicitation","params":{"sessionId":"grok-replay-session-1","mode":"form","message":"Choose the fixture preference.","requestedSchema":{"type":"object","title":"Fixture preference","properties":{"schema_vs_ui_flexibility":{"type":"string","title":"Fixture","description":"Should this fixture prefer strict schemas or UI flexibility?","enum":["Strict schemas (Recommended)","UI flexibility"]}},"required":["schema_vs_ui_flexibility"]}}}} +{"type":"expect_outbound","label":"elicitation.response","frame":{"kind":"response","method":"session/elicitation","result":{"action":{"action":"accept","content":{"schema_vs_ui_flexibility":"Strict schemas (Recommended)"}}}}} +{"type":"emit_inbound","label":"assistant.final","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"plan questions fixture complete"}}}}} +{"type":"emit_inbound","label":"prompt.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/index.ts b/apps/server/src/orchestration-v2/testkit/fixtures/index.ts new file mode 100644 index 00000000000..ba29043da67 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/index.ts @@ -0,0 +1,665 @@ +import { ProviderDriverKind } from "@t3tools/contracts"; + +import { assertClaudeMessageSteeringOutput } from "./message_steering/claude_output.ts"; +import { assertMessageSteeringOutput } from "./message_steering/codex_output.ts"; +import { assertCursorMessageSteeringOutput } from "./message_steering/cursor_output.ts"; +import { assertGrokMessageSteeringOutput } from "./message_steering/grok_output.ts"; +import { messageSteeringInput } from "./message_steering/input.ts"; +import { assertMultiTurnClaudeOutput } from "./multi_turn/claude_output.ts"; +import { assertMultiTurnOutput } from "./multi_turn/codex_output.ts"; +import { multiTurnInput } from "./multi_turn/input.ts"; +import { openCodeSubagentInput } from "./opencode_subagent/input.ts"; +import { assertOpenCodeSubagentOutput } from "./opencode_subagent/output.ts"; +import { assertPlanQuestionsOutput } from "./plan_questions/codex_output.ts"; +import { assertOpenCodePlanQuestionsOutput } from "./plan_questions/opencode_output.ts"; +import { planQuestionsInput } from "./plan_questions/input.ts"; +import { assertProposedPlanOutput } from "./proposed_plan/codex_output.ts"; +import { assertProposedPlanCursorOutput } from "./proposed_plan/cursor_output.ts"; +import { proposedPlanInput } from "./proposed_plan/input.ts"; +import { assertQueuedTurnOutput } from "./queued_turn/codex_output.ts"; +import { queuedTurnInput } from "./queued_turn/input.ts"; +import { assertSimpleClaudeOutput } from "./simple/claude_output.ts"; +import { assertSimpleOutput } from "./simple/codex_output.ts"; +import { simpleInput } from "./simple/input.ts"; +import { assertSubagentOutput } from "./subagent/codex_output.ts"; +import { assertClaudeSubagentOutput } from "./subagent/claude_output.ts"; +import { subagentInput } from "./subagent/input.ts"; +import { assertCursorSubagentOutput } from "./subagent/cursor_output.ts"; +import { assertSubagentContinueOutput } from "./subagent_continue/codex_output.ts"; +import { subagentContinueInput } from "./subagent_continue/input.ts"; +import { assertClaudeThreadRollbackOutput } from "./thread_rollback/claude_output.ts"; +import { assertThreadRollbackOutput } from "./thread_rollback/codex_output.ts"; +import { threadRollbackInput } from "./thread_rollback/input.ts"; +import { assertTodoListOutput } from "./todo_list/codex_output.ts"; +import { assertTodoListCursorOutput } from "./todo_list/cursor_output.ts"; +import { assertTodoListGrokOutput } from "./todo_list/grok_output.ts"; +import { todoListInput } from "./todo_list/input.ts"; +import { assertToolCallReadOnlyClaudeOutput } from "./tool_call_read_only/claude_output.ts"; +import { assertToolCallReadOnlyCursorOutput } from "./tool_call_read_only/cursor_output.ts"; +import { toolCallReadOnlyInput } from "./tool_call_read_only/input.ts"; +import { assertToolCallReadOnlyOnRequestClaudeOutput } from "./tool_call_read_only_on_request/claude_output.ts"; +import { assertToolCallReadOnlyOnRequestOutput } from "./tool_call_read_only_on_request/codex_output.ts"; +import { toolCallReadOnlyOnRequestInput } from "./tool_call_read_only_on_request/input.ts"; +import { assertToolCallRestrictedGranularClaudeOutput } from "./tool_call_restricted_granular/claude_output.ts"; +import { assertToolCallRestrictedGranularOutput } from "./tool_call_restricted_granular/codex_output.ts"; +import { toolCallRestrictedGranularInput } from "./tool_call_restricted_granular/input.ts"; +import { assertToolCallWorkspaceNeverClaudeOutput } from "./tool_call_workspace_never/claude_output.ts"; +import { assertToolCallWorkspaceNeverOutput } from "./tool_call_workspace_never/codex_output.ts"; +import { toolCallWorkspaceNeverInput } from "./tool_call_workspace_never/input.ts"; +import { assertTurnInterruptClaudeOutput } from "./turn_interrupt/claude_output.ts"; +import { assertTurnInterruptOutput } from "./turn_interrupt/codex_output.ts"; +import { turnInterruptInput } from "./turn_interrupt/input.ts"; +import { assertTurnInterruptMidToolClaudeOutput } from "./turn_interrupt_mid_tool/claude_output.ts"; +import { assertTurnInterruptMidToolCodexOutput } from "./turn_interrupt_mid_tool/codex_output.ts"; +import { assertTurnInterruptMidToolCursorOutput } from "./turn_interrupt_mid_tool/cursor_output.ts"; +import { turnInterruptMidToolInput } from "./turn_interrupt_mid_tool/input.ts"; +import { assertTurnInterruptRestartClaudeOutput } from "./turn_interrupt_restart/claude_output.ts"; +import { turnInterruptRestartInput } from "./turn_interrupt_restart/input.ts"; +import { assertClaudeWebSearchOutput } from "./web_search/claude_output.ts"; +import { assertWebSearchOutput } from "./web_search/codex_output.ts"; +import { webSearchInput } from "./web_search/input.ts"; +import { + ACP_REGISTRY_MODEL_SELECTION, + CLAUDE_MODEL_SELECTION, + CODEX_MODEL_SELECTION, + CURSOR_MODEL_SELECTION, + GROK_MODEL_SELECTION, + OPENCODE_MODEL_SELECTION, + READ_ONLY_NEVER_POLICY, + READ_ONLY_ON_REQUEST_POLICY, + RESTRICTED_GRANULAR_POLICY, + type OrchestratorReplayFixture, + WORKSPACE_NEVER_POLICY, +} from "./shared.ts"; + +export const ORCHESTRATOR_REPLAY_FIXTURES = [ + { + name: "acp_elicitation", + buildInput: planQuestionsInput, + providers: [ + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL("./acp_elicitation/grok_transcript.ndjson", import.meta.url), + modelSelection: GROK_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertPlanQuestionsOutput, + }, + { + driver: ProviderDriverKind.make("acpRegistry"), + transcriptFile: new URL("./acp_elicitation/grok_transcript.ndjson", import.meta.url), + modelSelection: ACP_REGISTRY_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertPlanQuestionsOutput, + }, + ], + }, + { + name: "simple", + buildInput: simpleInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./simple/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + assertOutput: assertSimpleOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./simple/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + assertOutput: assertSimpleClaudeOutput, + }, + { + driver: ProviderDriverKind.make("cursor"), + transcriptFile: new URL("./simple/cursor_transcript.ndjson", import.meta.url), + modelSelection: CURSOR_MODEL_SELECTION, + assertOutput: assertSimpleOutput, + }, + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL("./simple/grok_transcript.ndjson", import.meta.url), + modelSelection: GROK_MODEL_SELECTION, + assertOutput: assertSimpleOutput, + }, + { + driver: ProviderDriverKind.make("acpRegistry"), + transcriptFile: new URL("./simple/grok_transcript.ndjson", import.meta.url), + modelSelection: ACP_REGISTRY_MODEL_SELECTION, + assertOutput: assertSimpleOutput, + }, + { + driver: ProviderDriverKind.make("opencode"), + transcriptFile: new URL("./simple/opencode_transcript.ndjson", import.meta.url), + modelSelection: OPENCODE_MODEL_SELECTION, + assertOutput: assertSimpleOutput, + }, + ], + }, + { + name: "tool_call_read_only", + buildInput: toolCallReadOnlyInput, + providers: [ + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./tool_call_read_only/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertToolCallReadOnlyClaudeOutput, + }, + { + driver: ProviderDriverKind.make("cursor"), + transcriptFile: new URL("./tool_call_read_only/cursor_transcript.ndjson", import.meta.url), + modelSelection: CURSOR_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertToolCallReadOnlyCursorOutput, + }, + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL("./tool_call_read_only/grok_transcript.ndjson", import.meta.url), + modelSelection: GROK_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertToolCallReadOnlyCursorOutput, + }, + { + driver: ProviderDriverKind.make("acpRegistry"), + transcriptFile: new URL("./tool_call_read_only/grok_transcript.ndjson", import.meta.url), + modelSelection: ACP_REGISTRY_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertToolCallReadOnlyCursorOutput, + }, + ], + }, + { + name: "tool_call_read_only_on_request", + buildInput: toolCallReadOnlyOnRequestInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL( + "./tool_call_read_only_on_request/codex_transcript.ndjson", + import.meta.url, + ), + modelSelection: CODEX_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_ON_REQUEST_POLICY, + assertOutput: assertToolCallReadOnlyOnRequestOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL( + "./tool_call_read_only_on_request/claude_transcript.ndjson", + import.meta.url, + ), + modelSelection: CLAUDE_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_ON_REQUEST_POLICY, + assertOutput: assertToolCallReadOnlyOnRequestClaudeOutput, + }, + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL( + "./tool_call_read_only_on_request/grok_transcript.ndjson", + import.meta.url, + ), + modelSelection: GROK_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_ON_REQUEST_POLICY, + assertOutput: assertToolCallReadOnlyOnRequestOutput, + }, + { + driver: ProviderDriverKind.make("acpRegistry"), + transcriptFile: new URL( + "./tool_call_read_only_on_request/grok_transcript.ndjson", + import.meta.url, + ), + modelSelection: ACP_REGISTRY_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_ON_REQUEST_POLICY, + assertOutput: assertToolCallReadOnlyOnRequestOutput, + }, + ], + }, + { + name: "tool_call_workspace_never", + buildInput: toolCallWorkspaceNeverInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL( + "./tool_call_workspace_never/codex_transcript.ndjson", + import.meta.url, + ), + modelSelection: CODEX_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertToolCallWorkspaceNeverOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL( + "./tool_call_workspace_never/claude_transcript.ndjson", + import.meta.url, + ), + modelSelection: CLAUDE_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertToolCallWorkspaceNeverClaudeOutput, + }, + ], + }, + { + name: "tool_call_restricted_granular", + buildInput: toolCallRestrictedGranularInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL( + "./tool_call_restricted_granular/codex_transcript.ndjson", + import.meta.url, + ), + modelSelection: CODEX_MODEL_SELECTION, + runtimePolicyOverride: RESTRICTED_GRANULAR_POLICY, + assertOutput: assertToolCallRestrictedGranularOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL( + "./tool_call_restricted_granular/claude_transcript.ndjson", + import.meta.url, + ), + modelSelection: CLAUDE_MODEL_SELECTION, + runtimePolicyOverride: RESTRICTED_GRANULAR_POLICY, + assertOutput: assertToolCallRestrictedGranularClaudeOutput, + }, + ], + }, + { + name: "subagent", + buildInput: subagentInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./subagent/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_ON_REQUEST_POLICY, + assertOutput: assertSubagentOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./subagent/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + assertOutput: assertClaudeSubagentOutput, + }, + { + driver: ProviderDriverKind.make("cursor"), + transcriptFile: new URL("./subagent/cursor_transcript.ndjson", import.meta.url), + modelSelection: CURSOR_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertCursorSubagentOutput, + }, + ], + }, + { + name: "subagent_continue", + buildInput: subagentContinueInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./subagent_continue/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + assertOutput: assertSubagentContinueOutput, + }, + ], + }, + { + name: "opencode_subagent", + buildInput: openCodeSubagentInput, + providers: [ + { + driver: ProviderDriverKind.make("opencode"), + transcriptFile: new URL("./opencode_subagent/opencode_transcript.ndjson", import.meta.url), + modelSelection: OPENCODE_MODEL_SELECTION, + assertOutput: assertOpenCodeSubagentOutput, + }, + ], + }, + { + name: "multi_turn", + buildInput: multiTurnInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./multi_turn/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + assertOutput: assertMultiTurnOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./multi_turn/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + assertOutput: assertMultiTurnClaudeOutput, + }, + { + driver: ProviderDriverKind.make("cursor"), + transcriptFile: new URL("./multi_turn/cursor_transcript.ndjson", import.meta.url), + modelSelection: CURSOR_MODEL_SELECTION, + assertOutput: assertMultiTurnOutput, + }, + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL("./multi_turn/grok_transcript.ndjson", import.meta.url), + modelSelection: GROK_MODEL_SELECTION, + assertOutput: assertMultiTurnOutput, + }, + { + driver: ProviderDriverKind.make("acpRegistry"), + transcriptFile: new URL("./multi_turn/grok_transcript.ndjson", import.meta.url), + modelSelection: ACP_REGISTRY_MODEL_SELECTION, + assertOutput: assertMultiTurnOutput, + }, + ], + }, + { + name: "multi_turn_restart", + buildInput: multiTurnInput, + providers: [ + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./multi_turn_restart/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + assertOutput: assertMultiTurnClaudeOutput, + }, + ], + }, + { + name: "queued_turn", + buildInput: queuedTurnInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./queued_turn/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + assertOutput: assertQueuedTurnOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./queued_turn/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + assertOutput: assertQueuedTurnOutput, + }, + { + driver: ProviderDriverKind.make("cursor"), + transcriptFile: new URL("./queued_turn/cursor_transcript.ndjson", import.meta.url), + modelSelection: CURSOR_MODEL_SELECTION, + assertOutput: assertQueuedTurnOutput, + }, + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL("./queued_turn/grok_transcript.ndjson", import.meta.url), + modelSelection: GROK_MODEL_SELECTION, + assertOutput: assertQueuedTurnOutput, + }, + { + driver: ProviderDriverKind.make("acpRegistry"), + transcriptFile: new URL("./queued_turn/grok_transcript.ndjson", import.meta.url), + modelSelection: ACP_REGISTRY_MODEL_SELECTION, + assertOutput: assertQueuedTurnOutput, + }, + ], + }, + { + name: "todo_list", + buildInput: todoListInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./todo_list/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertTodoListOutput, + }, + { + driver: ProviderDriverKind.make("cursor"), + transcriptFile: new URL("./todo_list/cursor_transcript.ndjson", import.meta.url), + modelSelection: CURSOR_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertTodoListCursorOutput, + }, + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL("./todo_list/grok_transcript.ndjson", import.meta.url), + modelSelection: GROK_MODEL_SELECTION, + assertOutput: assertTodoListGrokOutput, + }, + { + driver: ProviderDriverKind.make("acpRegistry"), + transcriptFile: new URL("./todo_list/grok_transcript.ndjson", import.meta.url), + modelSelection: ACP_REGISTRY_MODEL_SELECTION, + assertOutput: assertTodoListGrokOutput, + }, + ], + }, + { + name: "web_search", + buildInput: webSearchInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./web_search/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + assertOutput: assertWebSearchOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./web_search/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + assertOutput: assertClaudeWebSearchOutput, + }, + ], + }, + { + name: "plan_questions", + buildInput: planQuestionsInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./plan_questions/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertPlanQuestionsOutput, + }, + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL("./plan_questions/grok_transcript.ndjson", import.meta.url), + modelSelection: GROK_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertPlanQuestionsOutput, + }, + { + driver: ProviderDriverKind.make("opencode"), + transcriptFile: new URL("./plan_questions/opencode_transcript.ndjson", import.meta.url), + modelSelection: OPENCODE_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertOpenCodePlanQuestionsOutput, + }, + ], + }, + { + name: "proposed_plan", + buildInput: proposedPlanInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./proposed_plan/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertProposedPlanOutput, + }, + { + driver: ProviderDriverKind.make("cursor"), + transcriptFile: new URL("./proposed_plan/cursor_transcript.ndjson", import.meta.url), + modelSelection: CURSOR_MODEL_SELECTION, + runtimePolicyOverride: READ_ONLY_NEVER_POLICY, + assertOutput: assertProposedPlanCursorOutput, + }, + ], + }, + { + name: "message_steering", + buildInput: messageSteeringInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./message_steering/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + assertOutput: assertMessageSteeringOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./message_steering/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + assertOutput: assertClaudeMessageSteeringOutput, + }, + { + driver: ProviderDriverKind.make("cursor"), + transcriptFile: new URL("./message_steering/cursor_transcript.ndjson", import.meta.url), + modelSelection: CURSOR_MODEL_SELECTION, + assertOutput: assertCursorMessageSteeringOutput, + }, + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL("./message_steering/grok_transcript.ndjson", import.meta.url), + modelSelection: GROK_MODEL_SELECTION, + assertOutput: assertGrokMessageSteeringOutput, + }, + { + driver: ProviderDriverKind.make("acpRegistry"), + transcriptFile: new URL("./message_steering/grok_transcript.ndjson", import.meta.url), + modelSelection: ACP_REGISTRY_MODEL_SELECTION, + assertOutput: assertGrokMessageSteeringOutput, + }, + ], + }, + { + name: "turn_interrupt", + buildInput: turnInterruptInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./turn_interrupt/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertTurnInterruptOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./turn_interrupt/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertTurnInterruptClaudeOutput, + }, + { + driver: ProviderDriverKind.make("grok"), + transcriptFile: new URL("./turn_interrupt/grok_transcript.ndjson", import.meta.url), + modelSelection: GROK_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertTurnInterruptOutput, + }, + { + driver: ProviderDriverKind.make("acpRegistry"), + transcriptFile: new URL("./turn_interrupt/grok_transcript.ndjson", import.meta.url), + modelSelection: ACP_REGISTRY_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertTurnInterruptOutput, + }, + { + driver: ProviderDriverKind.make("opencode"), + transcriptFile: new URL("./turn_interrupt/opencode_transcript.ndjson", import.meta.url), + modelSelection: OPENCODE_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertTurnInterruptOutput, + }, + ], + }, + { + name: "turn_interrupt_mid_tool", + buildInput: turnInterruptMidToolInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL( + "./turn_interrupt_mid_tool/codex_transcript.ndjson", + import.meta.url, + ), + modelSelection: CODEX_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertTurnInterruptMidToolCodexOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL( + "./turn_interrupt_mid_tool/claude_transcript.ndjson", + import.meta.url, + ), + modelSelection: CLAUDE_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertTurnInterruptMidToolClaudeOutput, + }, + { + driver: ProviderDriverKind.make("cursor"), + transcriptFile: new URL( + "./turn_interrupt_mid_tool/cursor_transcript.ndjson", + import.meta.url, + ), + modelSelection: CURSOR_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertTurnInterruptMidToolCursorOutput, + }, + ], + }, + { + name: "turn_interrupt_restart", + buildInput: turnInterruptRestartInput, + providers: [ + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL( + "./turn_interrupt_restart/claude_transcript.ndjson", + import.meta.url, + ), + modelSelection: CLAUDE_MODEL_SELECTION, + runtimePolicyOverride: WORKSPACE_NEVER_POLICY, + assertOutput: assertTurnInterruptRestartClaudeOutput, + }, + ], + }, + { + name: "thread_rollback", + buildInput: threadRollbackInput, + providers: [ + { + driver: ProviderDriverKind.make("codex"), + transcriptFile: new URL("./thread_rollback/codex_transcript.ndjson", import.meta.url), + modelSelection: CODEX_MODEL_SELECTION, + assertOutput: assertThreadRollbackOutput, + }, + { + driver: ProviderDriverKind.make("claudeAgent"), + transcriptFile: new URL("./thread_rollback/claude_transcript.ndjson", import.meta.url), + modelSelection: CLAUDE_MODEL_SELECTION, + assertOutput: assertClaudeThreadRollbackOutput, + }, + ], + }, +] satisfies ReadonlyArray; + +// TODO(claude-v2/approvals-denied): add denied write fixtures after the live query runner records +// Claude denial callback responses. Cross-reference +// `tool_call_read_only_on_request/claude_transcript.ndjson`, +// `tool_call_workspace_never/claude_transcript.ndjson`, +// `tool_call_restricted_granular/claude_transcript.ndjson`, and +// docs/orchestration-v2/provider-capability-system.md. + +// TODO(claude-v2/context-transfer): add provider-switch handoff and return fixtures when portable +// context handoff is implemented. Cross-reference docs/orchestration-v2/provider-switching-and-context.md +// and docs/orchestration-v2/thread-lineage-and-context-transfer.md. The return fixture should +// prefer a delta handoff into an existing Claude provider thread. + +// TODO(claude-v2/context-transfer-fixtures): register provider-switch, merge-back, and cross-provider +// fork fixtures after each path has a real provider transcript. Cross-reference +// docs/orchestration-v2/provider-switching-and-context.md and +// docs/orchestration-v2/thread-lineage-and-context-transfer.md. diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_output.ts new file mode 100644 index 00000000000..26c2646bbb1 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_output.ts @@ -0,0 +1,43 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessageInputIntents, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + projectionFor, +} from "../shared.ts"; + +export function assertClaudeMessageSteeringOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assert.equal(transcript.provider, "claudeAgent"); + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, ["user_message", "assistant_message"]); + assertRuntimeRequestCounts(projection, { total: 0 }); + assertUserMessagesInclude(projection, [ + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + ]); + assertUserMessageInputIntents(projection, ["turn_start", "steer"]); + assertAssistantTextIncludes(projection, "steering fixture observed"); + assert.equal(projection.runs.length, 1, "steering must attach to the active run"); + assert.equal( + projection.providerTurns.length, + 1, + "active steering must not create a new provider turn", + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_transcript.ndjson new file mode 100644 index 00000000000..f534330245e --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/claude_transcript.ndjson @@ -0,0 +1,13 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"message_steering","metadata":{"prompts":["Respond with exactly: steering fixture initial response","Actually, respond with exactly: steering fixture observed"],"model":"claude-sonnet-4-6","nativeSessionId":"05f72f7d-81d5-4db9-a3cf-234191cf79f9","queryMode":"active_steering","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: steering fixture initial response"},"parent_tool_use_id":null}}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Actually, respond with exactly: steering fixture observed"},"parent_tool_use_id":null,"priority":"now"}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"2961831d-241e-47c4-8099-b8779c6b3878","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"d45438eb-d485-490f-9668-a02c5721a6b3","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"2961831d-241e-47c4-8099-b8779c6b3878","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"40ef0c52-1168-4049-99af-4c3beb507994","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-message_steering","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"71579637-9bc5-4182-b104-4ebb8f2bda0d","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"error_during_execution","duration_ms":747,"duration_api_ms":0,"is_error":true,"num_turns":1,"stop_reason":null,"session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"aborted_streaming","fast_mode_state":"off","uuid":"781fa8c7-98dd-4679-bb99-82d4c77c9b0c","errors":["[ede_diagnostic] result_type=user last_content_type=n/a stop_reason=null","Error: Request was aborted.\n at vn8 (/tmp/claude-replay-message_steering/node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.111+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/cli.js:5550:1432)\n at next (native:1:11)\n at a85 (/tmp/claude-replay-message_steering/node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.111+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/cli.js:8321:5086)\n at processTicksAndRejections (native:7:39)"]}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-message_steering","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"60494b15-57a4-417d-a0d0-0ce42590c36e","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01Cjp5MnmN6F7wwrzNss9zVW","type":"message","role":"assistant","content":[{"type":"text","text":"steering fixture observed"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2347,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2347},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9","uuid":"2faa9bee-1c72-4b8f-ab04-9796a7ecf6a2"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"b0bfc7e0-1698-4480-821d-16262ceb4448","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":3836,"duration_api_ms":1808,"num_turns":1,"result":"steering fixture observed","stop_reason":"end_turn","session_id":"05f72f7d-81d5-4db9-a3cf-234191cf79f9","total_cost_usd":0.01173675,"usage":{"input_tokens":3,"cache_creation_input_tokens":2347,"cache_read_input_tokens":9455,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2347,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":9455,"cache_creation_input_tokens":2347,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2347},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":9455,"cacheCreationInputTokens":2347,"webSearchRequests":0,"costUSD":0.01173675,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"7ac0393a-c862-449c-95f7-bbf391264476"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/codex_output.ts new file mode 100644 index 00000000000..1a238beea3a --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/codex_output.ts @@ -0,0 +1,43 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessageInputIntents, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + projectionFor, +} from "../shared.ts"; + +export function assertMessageSteeringOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assert.equal(transcript.provider, "codex"); + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, ["user_message", "assistant_message"]); + assertRuntimeRequestCounts(projection, { total: 0 }); + assertUserMessagesInclude(projection, [ + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + ]); + assertUserMessageInputIntents(projection, ["turn_start", "steer"]); + assertAssistantTextIncludes(projection, "steering fixture observed"); + assert.equal(projection.runs.length, 1, "steering must attach to the active run"); + assert.equal( + projection.providerTurns.length, + 1, + "active steering must not create a new provider turn", + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/codex_transcript.ndjson new file mode 100644 index 00000000000..c9b76c13a2f --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/codex_transcript.ndjson @@ -0,0 +1,51 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.128.0","scenario":"message_steering","metadata":{"source":"record-codex-app-server-replay-fixture","fileName":"message_steering.ndjson","description":"One active turn receives an immediate turn/steer request."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.128.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex_p","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"remoteControl/status/changed","frame":{"method":"remoteControl/status/changed","params":{"status":"disabled","environmentId":null}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019e014d-b214-71f0-9538-23527e8247ca","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1778138329,"updatedAt":1778138329,"status":{"type":"idle"},"path":"/Users/julius/.codex_p/sessions/2026/05/07/rollout-2026-05-07T00-18-49-019e014d-b214-71f0-9538-23527e8247ca.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/apps/server","cliVersion":"0.128.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.5","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/apps/server","instructionSources":["/Users/julius/.codex_p/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex_p/memories"],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"permissionProfile":{"type":"managed","network":{"enabled":false},"fileSystem":{"type":"restricted","entries":[{"path":{"type":"special","value":{"kind":"root"}},"access":"read"},{"path":{"type":"special","value":{"kind":"project_roots","subpath":null}},"access":"write"},{"path":{"type":"special","value":{"kind":"slash_tmp"}},"access":"write"},{"path":{"type":"special","value":{"kind":"tmpdir"}},"access":"write"},{"path":{"type":"special","value":{"kind":"project_roots","subpath":".git"}},"access":"read"},{"path":{"type":"special","value":{"kind":"project_roots","subpath":".agents"}},"access":"read"},{"path":{"type":"special","value":{"kind":"project_roots","subpath":".codex"}},"access":"read"},{"path":{"type":"path","path":"/Users/julius/.codex_p/memories"},"access":"write"}]}},"activePermissionProfile":{"id":":workspace","extends":null,"modifications":[]},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Respond with exactly: steering fixture initial response","type":"text"}],"threadId":"019e014d-b214-71f0-9538-23527e8247ca"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019e014d-b214-71f0-9538-23527e8247ca","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1778138329,"updatedAt":1778138329,"status":{"type":"idle"},"path":"/Users/julius/.codex_p/sessions/2026/05/07/rollout-2026-05-07T00-18-49-019e014d-b214-71f0-9538-23527e8247ca.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/apps/server","cliVersion":"0.128.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"warning","frame":{"method":"warning","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","message":"Under-development features enabled: apply_patch_freeform. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in /Users/julius/.codex_p/config.toml."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019e014d-b227-7af1-a889-6c7d828eb1a2","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turn":{"id":"019e014d-b227-7af1-a889-6c7d828eb1a2","items":[],"status":"inProgress","error":null,"startedAt":1778138329,"completedAt":null,"durationMs":null}}}} +{"type":"expect_outbound","label":"turn/steer","frame":{"id":4,"method":"turn/steer","params":{"expectedTurnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","input":[{"text":"Actually, respond with exactly: steering fixture observed","type":"text"}],"threadId":"019e014d-b214-71f0-9538-23527e8247ca"}}} +{"type":"emit_inbound","label":"turn/steer","frame":{"id":4,"result":{"turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"4fac8685-35e8-475c-96b6-9adb33c68158","content":[{"type":"text","text":"Respond with exactly: steering fixture initial response","text_elements":[]}]},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"4fac8685-35e8-475c-96b6-9adb33c68158","content":[{"type":"text","text":"Respond with exactly: steering fixture initial response","text_elements":[]}]},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":19,"windowDurationMins":300,"resetsAt":1778142198},"secondary":{"usedPercent":31,"windowDurationMins":10080,"resetsAt":1778539136},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_010631d904f9e19a0169fc3cdcb670819486fcc6dc7b883f58","summary":[],"content":[]},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_010631d904f9e19a0169fc3cdcb670819486fcc6dc7b883f58","summary":[],"content":[]},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_010631d904f9e19a0169fc3cdce8ec8194a7d8a5bb50424c09","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","itemId":"msg_010631d904f9e19a0169fc3cdce8ec8194a7d8a5bb50424c09","delta":"ste"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","itemId":"msg_010631d904f9e19a0169fc3cdce8ec8194a7d8a5bb50424c09","delta":"ering"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","itemId":"msg_010631d904f9e19a0169fc3cdce8ec8194a7d8a5bb50424c09","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","itemId":"msg_010631d904f9e19a0169fc3cdce8ec8194a7d8a5bb50424c09","delta":" initial"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","itemId":"msg_010631d904f9e19a0169fc3cdce8ec8194a7d8a5bb50424c09","delta":" response"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_010631d904f9e19a0169fc3cdce8ec8194a7d8a5bb50424c09","text":"steering fixture initial response","phase":"final_answer","memoryCitation":null},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","tokenUsage":{"total":{"totalTokens":18900,"inputTokens":18880,"cachedInputTokens":7552,"outputTokens":20,"reasoningOutputTokens":9},"last":{"totalTokens":18900,"inputTokens":18880,"cachedInputTokens":7552,"outputTokens":20,"reasoningOutputTokens":9},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":19,"windowDurationMins":300,"resetsAt":1778142198},"secondary":{"usedPercent":31,"windowDurationMins":10080,"resetsAt":1778539136},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"3d8198bc-53c8-4885-8dfe-c5158eb2cfb3","content":[{"type":"text","text":"Actually, respond with exactly: steering fixture observed","text_elements":[]}]},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"3d8198bc-53c8-4885-8dfe-c5158eb2cfb3","content":[{"type":"text","text":"Actually, respond with exactly: steering fixture observed","text_elements":[]}]},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","tokenUsage":{"total":{"totalTokens":18900,"inputTokens":18880,"cachedInputTokens":7552,"outputTokens":20,"reasoningOutputTokens":9},"last":{"totalTokens":18900,"inputTokens":18880,"cachedInputTokens":7552,"outputTokens":20,"reasoningOutputTokens":9},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":19,"windowDurationMins":300,"resetsAt":1778142198},"secondary":{"usedPercent":31,"windowDurationMins":10080,"resetsAt":1778539136},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_010631d904f9e19a0169fc3cddd2b08194ba097f57a2fd6332","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","itemId":"msg_010631d904f9e19a0169fc3cddd2b08194ba097f57a2fd6332","delta":"ste"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","itemId":"msg_010631d904f9e19a0169fc3cddd2b08194ba097f57a2fd6332","delta":"ering"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","itemId":"msg_010631d904f9e19a0169fc3cddd2b08194ba097f57a2fd6332","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","itemId":"msg_010631d904f9e19a0169fc3cddd2b08194ba097f57a2fd6332","delta":" observed"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_010631d904f9e19a0169fc3cddd2b08194ba097f57a2fd6332","text":"steering fixture observed","phase":"final_answer","memoryCitation":null},"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turnId":"019e014d-b227-7af1-a889-6c7d828eb1a2","tokenUsage":{"total":{"totalTokens":37812,"inputTokens":37784,"cachedInputTokens":26368,"outputTokens":28,"reasoningOutputTokens":9},"last":{"totalTokens":18912,"inputTokens":18904,"cachedInputTokens":18816,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":19,"windowDurationMins":300,"resetsAt":1778142198},"secondary":{"usedPercent":31,"windowDurationMins":10080,"resetsAt":1778539136},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019e014d-b214-71f0-9538-23527e8247ca","turn":{"id":"019e014d-b227-7af1-a889-6c7d828eb1a2","items":[],"status":"completed","error":null,"startedAt":1778138329,"completedAt":1778138333,"durationMs":4311}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/cursor_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/cursor_output.ts new file mode 100644 index 00000000000..b4482f762d3 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/cursor_output.ts @@ -0,0 +1,55 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessageInputIntents, + assertUserMessagesInclude, + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + projectionFor, +} from "../shared.ts"; + +export function assertCursorMessageSteeringOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assert.equal(transcript.provider, "cursor"); + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertTurnItemTypes(projection, ["user_message", "run_interrupt_result", "assistant_message"]); + assertRuntimeRequestCounts(projection, { total: 0 }); + assertUserMessagesInclude(projection, [ + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + ]); + assertUserMessageInputIntents(projection, ["turn_start", "steer"]); + assertAssistantTextIncludes(projection, "steering fixture observed"); + + assert.lengthOf(projection.runs, 1, "steering must preserve the app run"); + assert.deepEqual( + projection.attempts.map((attempt) => [attempt.reason, attempt.status]), + [ + ["initial", "superseded"], + ["steering_restart", "completed"], + ], + ); + assert.deepEqual( + projection.providerTurns.map((turn) => turn.status), + ["interrupted", "completed"], + ); + assert.equal(projection.runs[0]?.activeAttemptId, projection.attempts[1]?.id); + assert.equal(projection.runs[0]?.rootNodeId, projection.attempts[1]?.rootNodeId); + assert.notInclude( + projection.visibleTurnItems.map((row) => row.item.type), + "run_interrupt_result", + "restart steering must keep its internal interruption out of visible chat history", + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/cursor_transcript.ndjson new file mode 100644 index 00000000000..4a2357eebc8 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/cursor_transcript.ndjson @@ -0,0 +1,21 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"message_steering","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-95a210ed-ddf9-4b36-b265-9a2b02e46050"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":false,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-95a210ed-ddf9-4b36-b265-9a2b02e46050"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Respond with exactly: steering fixture initial response","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-c794f71d-f53a-47ba-b127-b9f26cf64ce1","agentId":"agent-95a210ed-ddf9-4b36-b265-9a2b02e46050"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-c794f71d-f53a-47ba-b127-b9f26cf64ce1","update":{"type":"text-delta","text":"ste"}}} +{"type":"expect_outbound","label":"run.cancel:1","frame":{"type":"run.cancel","runId":"run-c794f71d-f53a-47ba-b127-b9f26cf64ce1"}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-c794f71d-f53a-47ba-b127-b9f26cf64ce1","requestId":"60f12d12-3cd5-47f1-a4dc-bda43dc5a136","status":"cancelled","model":{"id":"composer-2.5"},"durationMs":4219}}} +{"type":"expect_outbound","label":"run.start:2","frame":{"type":"run.start","message":"Actually, respond with exactly: steering fixture observed","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:2","frame":{"type":"run.started","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","agentId":"agent-95a210ed-ddf9-4b36-b265-9a2b02e46050"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","update":{"type":"text-delta","text":"ste"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","update":{"type":"text-delta","text":"ering"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","update":{"type":"text-delta","text":" fixture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","update":{"type":"text-delta","text":" observed"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","update":{"type":"step-completed","stepId":1,"stepDurationMs":1055}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","update":{"type":"turn-ended","usage":{"inputTokens":10270,"outputTokens":42,"cacheReadTokens":7441,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:2","frame":{"type":"run.completed","result":{"id":"run-c93ec26d-f38b-4a58-9005-1ed91ad98b9c","requestId":"21e1995e-2649-4e30-aec6-d967e9bce64c","status":"finished","result":"steering fixture observed","model":{"id":"composer-2.5"},"durationMs":1465}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-95a210ed-ddf9-4b36-b265-9a2b02e46050"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_output.ts new file mode 100644 index 00000000000..7fbd5d1f643 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_output.ts @@ -0,0 +1,54 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessageInputIntents, + assertUserMessagesInclude, + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + projectionFor, +} from "../shared.ts"; + +export function assertGrokMessageSteeringOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertTurnItemTypes(projection, ["user_message", "run_interrupt_result", "assistant_message"]); + assertRuntimeRequestCounts(projection, { total: 0 }); + assertUserMessagesInclude(projection, [ + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + ]); + assertUserMessageInputIntents(projection, ["turn_start", "steer"]); + assertAssistantTextIncludes(projection, "steering fixture observed"); + + assert.lengthOf(projection.runs, 1, "steering must preserve the app run"); + assert.deepEqual( + projection.attempts.map((attempt) => [attempt.reason, attempt.status]), + [ + ["initial", "superseded"], + ["steering_restart", "completed"], + ], + ); + assert.deepEqual( + projection.providerTurns.map((turn) => turn.status), + ["interrupted", "completed"], + ); + assert.equal(projection.runs[0]?.activeAttemptId, projection.attempts[1]?.id); + assert.equal(projection.runs[0]?.rootNodeId, projection.attempts[1]?.rootNodeId); + assert.notInclude( + projection.visibleTurnItems.map((row) => row.item.type), + "run_interrupt_result", + "restart steering must keep its internal interruption out of visible chat history", + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_transcript.ndjson new file mode 100644 index 00000000000..b50308c07f8 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_transcript.ndjson @@ -0,0 +1,14 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"message_steering","metadata":{"generatedBy":"live-grok-shape-probe","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"grok-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"}]}}}} +{"type":"expect_outbound","label":"session.prompt.1","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Respond with exactly: steering fixture initial response"}]}}} +{"type":"emit_inbound","label":"assistant.partial","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ste"}}}}} +{"type":"expect_outbound","label":"session.cancel","frame":{"kind":"notification","method":"session/cancel","params":{"sessionId":"grok-replay-session-1"}}} +{"type":"emit_inbound","label":"prompt.cancelled","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"cancelled"}}} +{"type":"expect_outbound","label":"session.prompt.2","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Actually, respond with exactly: steering fixture observed"}]}}} +{"type":"emit_inbound","label":"assistant.1","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"steering fixture "}}}}} +{"type":"emit_inbound","label":"assistant.2","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"observed"}}}}} +{"type":"emit_inbound","label":"prompt.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/input.ts new file mode 100644 index 00000000000..185d2726143 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/input.ts @@ -0,0 +1,31 @@ +import { + MESSAGE_STEERING_INITIAL_PROMPT, + MESSAGE_STEERING_STEER_PROMPT, + type OrchestratorFixtureInput, +} from "../shared.ts"; + +export function messageSteeringInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: MESSAGE_STEERING_INITIAL_PROMPT }, + { + type: "steer", + text: MESSAGE_STEERING_STEER_PROMPT, + targetRunIndex: 1, + }, + ], + }; +} + +export function messageRestartInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: MESSAGE_STEERING_INITIAL_PROMPT }, + { + type: "restart", + text: MESSAGE_STEERING_STEER_PROMPT, + targetRunIndex: 1, + }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/claude_output.ts new file mode 100644 index 00000000000..0c1c374c191 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/claude_output.ts @@ -0,0 +1,53 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { assertMultiTurnOutput } from "./codex_output.ts"; +import { projectionFor } from "../shared.ts"; + +function isReplayFrameWithType( + frame: unknown, + type: string, +): frame is { readonly type: string; readonly options?: Record } { + return ( + typeof frame === "object" && + frame !== null && + "type" in frame && + (frame as { readonly type?: unknown }).type === type + ); +} + +export function assertMultiTurnClaudeOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertMultiTurnOutput(result, transcript); + + const projection = projectionFor(result, transcript.scenario); + assert.lengthOf(projection.providerThreads, 1); + const providerThread = projection.providerThreads[0]; + assert.isDefined(providerThread); + assert.isNotNull(providerThread.nativeThreadRef); + assert.deepEqual( + projection.runs.map((run) => run.providerThreadId), + projection.runs.map(() => providerThread.id), + ); + + const outboundFrames = transcript.entries.flatMap((entry) => + entry.type === "expect_outbound" ? [entry.frame] : [], + ); + const queryOpenFrames = outboundFrames.filter((frame) => + isReplayFrameWithType(frame, "query.open"), + ); + const promptOfferFrames = outboundFrames.filter((frame) => + isReplayFrameWithType(frame, "prompt.offer"), + ); + + assert.lengthOf(promptOfferFrames, 2); + if (transcript.scenario === "multi_turn_restart") { + assert.lengthOf(queryOpenFrames, 2); + assert.isString(queryOpenFrames[1]?.options?.resume); + } else { + assert.lengthOf(queryOpenFrames, 1); + } +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/claude_transcript.ndjson new file mode 100644 index 00000000000..d905455eeba --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/claude_transcript.ndjson @@ -0,0 +1,14 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"multi_turn","metadata":{"prompts":["Respond with exactly: first fixture turn complete","Respond with exactly: second fixture turn complete"],"model":"claude-sonnet-4-6","nativeSessionId":"01899f29-45f7-43c3-b963-0f9181ed013f","queryMode":"streaming","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"01899f29-45f7-43c3-b963-0f9181ed013f"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: first fixture turn complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"a8f3420b-6e02-4f06-8b20-139030128614","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"91827846-9643-43f7-802a-7622a4d59945","session_id":"01899f29-45f7-43c3-b963-0f9181ed013f"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"a8f3420b-6e02-4f06-8b20-139030128614","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"3f1f1a2b-1ddb-4e07-8f62-d3e2020a1bf0","session_id":"01899f29-45f7-43c3-b963-0f9181ed013f"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-multi_turn","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"dbef42fe-0daa-40e2-8b4a-b8fbf39191bc","session_id":"01899f29-45f7-43c3-b963-0f9181ed013f"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_018QDgm81Jgo5XonK3Bm8dEc","type":"message","role":"assistant","content":[{"type":"text","text":"first fixture turn complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2337,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2337},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"01899f29-45f7-43c3-b963-0f9181ed013f","uuid":"d513887e-fb2d-47a1-adbb-e8678b42ad9a"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"53a27a3d-676a-4781-9c94-abe4a8af8fdd","session_id":"01899f29-45f7-43c3-b963-0f9181ed013f"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":1877,"duration_api_ms":1400,"num_turns":1,"result":"first fixture turn complete","stop_reason":"end_turn","session_id":"01899f29-45f7-43c3-b963-0f9181ed013f","total_cost_usd":0.01171425,"usage":{"input_tokens":3,"cache_creation_input_tokens":2337,"cache_read_input_tokens":9455,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2337,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":9455,"cache_creation_input_tokens":2337,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2337},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":7,"cacheReadInputTokens":9455,"cacheCreationInputTokens":2337,"webSearchRequests":0,"costUSD":0.01171425,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"bd7da0d2-bc9e-42db-841e-4285de52d9a2"}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: second fixture turn complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-multi_turn","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"09aafcb5-80e8-465c-b99f-14185f9e737d","session_id":"01899f29-45f7-43c3-b963-0f9181ed013f"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01PifXzuLoccLsnMjRC9hXJ8","type":"message","role":"assistant","content":[{"type":"text","text":"second fixture turn complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":19,"cache_read_input_tokens":11792,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":19},"output_tokens":7,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"01899f29-45f7-43c3-b963-0f9181ed013f","uuid":"8553017b-6ade-4de7-913a-0beacfd3728c"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":1415,"duration_api_ms":2278,"num_turns":1,"result":"second fixture turn complete","stop_reason":"end_turn","session_id":"01899f29-45f7-43c3-b963-0f9181ed013f","total_cost_usd":0.0154371,"usage":{"input_tokens":3,"cache_creation_input_tokens":19,"cache_read_input_tokens":11792,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":19,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":11792,"cache_creation_input_tokens":19,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":19},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":6,"outputTokens":14,"cacheReadInputTokens":21247,"cacheCreationInputTokens":2356,"webSearchRequests":0,"costUSD":0.0154371,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"f57bf2b0-a6e3-4701-9898-cd3be185a5d0"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/codex_output.ts new file mode 100644 index 00000000000..82bda348d38 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/codex_output.ts @@ -0,0 +1,40 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertConversationMessageRoles, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + MULTI_TURN_FIRST_PROMPT, + MULTI_TURN_SECOND_PROMPT, + projectionFor, +} from "../shared.ts"; + +export function assertMultiTurnOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ + result, + transcript, + runCount: 2, + runStatuses: ["completed", "completed"], + }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1, 2]); + assertConversationMessageRoles(projection, ["user", "assistant", "user", "assistant"]); + assertTurnItemTypes(projection, ["user_message", "assistant_message"]); + assertUserMessagesInclude(projection, [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT]); + assert.equal(projection.turnItems.filter((item) => item.type === "user_message").length, 2); + assertAssistantTextIncludes(projection, "first fixture turn complete"); + assertAssistantTextIncludes(projection, "second fixture turn complete"); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/codex_transcript.ndjson new file mode 100644 index 00000000000..a6a0dc4bc74 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/codex_transcript.ndjson @@ -0,0 +1,52 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"multi_turn","metadata":{"source":"codex-app-server-probe","fileName":"multi_turn.ndjson","description":"One thread with two sequential user turns."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019dadec-c14b-7b13-b96c-92c98ccfc342","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739467,"updatedAt":1776739467,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-44-27-019dadec-c14b-7b13-b96c-92c98ccfc342.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Respond with exactly: first fixture turn complete","type":"text"}],"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019dadec-c14b-7b13-b96c-92c98ccfc342","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739467,"updatedAt":1776739467,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-44-27-019dadec-c14b-7b13-b96c-92c98ccfc342.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019dadec-c155-74a2-8acc-f9f4ff86c15b","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turn":{"id":"019dadec-c155-74a2-8acc-f9f4ff86c15b","items":[],"status":"inProgress","error":null,"startedAt":1776739467,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"2562fa0b-0d7c-4030-9be5-ece4962d27bd","content":[{"type":"text","text":"Respond with exactly: first fixture turn complete","text_elements":[]}]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"2562fa0b-0d7c-4030-9be5-ece4962d27bd","content":[{"type":"text","text":"Respond with exactly: first fixture turn complete","text_elements":[]}]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0110066f30127def0169e6e4920934819996efbb3a16f7bded","summary":[],"content":[]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0110066f30127def0169e6e4920934819996efbb3a16f7bded","summary":[],"content":[]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","itemId":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","delta":"first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","itemId":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","itemId":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","itemId":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","text":"first fixture turn complete","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","tokenUsage":{"total":{"totalTokens":28248,"inputTokens":28222,"cachedInputTokens":28032,"outputTokens":26,"reasoningOutputTokens":16},"last":{"totalTokens":28248,"inputTokens":28222,"cachedInputTokens":28032,"outputTokens":26,"reasoningOutputTokens":16},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turn":{"id":"019dadec-c155-74a2-8acc-f9f4ff86c15b","items":[],"status":"completed","error":null,"startedAt":1776739467,"completedAt":1776739474,"durationMs":6986}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":4,"method":"turn/start","params":{"input":[{"text":"Respond with exactly: second fixture turn complete","type":"text"}],"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":4,"result":{"turn":{"id":"019dadec-dca0-7df0-9bc1-5a44135d8735","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turn":{"id":"019dadec-dca0-7df0-9bc1-5a44135d8735","items":[],"status":"inProgress","error":null,"startedAt":1776739474,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"0c4bed3e-fdd2-422c-bc28-4d688a5a9289","content":[{"type":"text","text":"Respond with exactly: second fixture turn complete","text_elements":[]}]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"0c4bed3e-fdd2-422c-bc28-4d688a5a9289","content":[{"type":"text","text":"Respond with exactly: second fixture turn complete","text_elements":[]}]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","tokenUsage":{"total":{"totalTokens":28248,"inputTokens":28222,"cachedInputTokens":28032,"outputTokens":26,"reasoningOutputTokens":16},"last":{"totalTokens":28248,"inputTokens":28222,"cachedInputTokens":28032,"outputTokens":26,"reasoningOutputTokens":16},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748633},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335433},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0110066f30127def0169e6e495c82481998e6c7e0925c86cfa","summary":[],"content":[]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0110066f30127def0169e6e495c82481998e6c7e0925c86cfa","summary":[],"content":[]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","itemId":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","delta":"second"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","itemId":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","itemId":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","itemId":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","text":"second fixture turn complete","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","tokenUsage":{"total":{"totalTokens":56514,"inputTokens":56466,"cachedInputTokens":56064,"outputTokens":48,"reasoningOutputTokens":28},"last":{"totalTokens":28266,"inputTokens":28244,"cachedInputTokens":28032,"outputTokens":22,"reasoningOutputTokens":12},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748633},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335433},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turn":{"id":"019dadec-dca0-7df0-9bc1-5a44135d8735","items":[],"status":"completed","error":null,"startedAt":1776739474,"completedAt":1776739478,"durationMs":3700}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/cursor_transcript.ndjson new file mode 100644 index 00000000000..c7c450e7b29 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/cursor_transcript.ndjson @@ -0,0 +1,30 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"multi_turn","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-93ba82d8-7489-48f7-9b93-b9f65f425666"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":false,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-93ba82d8-7489-48f7-9b93-b9f65f425666"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Respond with exactly: first fixture turn complete","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-e94938bb-f461-4834-b729-c4562b715945","agentId":"agent-93ba82d8-7489-48f7-9b93-b9f65f425666"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"text-delta","text":"first"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"text-delta","text":" fixture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"text-delta","text":" turn"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"text-delta","text":" complete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"step-completed","stepId":1,"stepDurationMs":969}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-e94938bb-f461-4834-b729-c4562b715945","update":{"type":"turn-ended","usage":{"inputTokens":10403,"outputTokens":38,"cacheReadTokens":7392,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-e94938bb-f461-4834-b729-c4562b715945","requestId":"ef58f76f-ef8f-475e-bff1-568e8aed367c","status":"finished","result":"first fixture turn complete","model":{"id":"composer-2.5"},"durationMs":3891}}} +{"type":"expect_outbound","label":"run.start:2","frame":{"type":"run.start","message":"Respond with exactly: second fixture turn complete","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:2","frame":{"type":"run.started","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","agentId":"agent-93ba82d8-7489-48f7-9b93-b9f65f425666"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"text-delta","text":"second"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"text-delta","text":" fixture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"text-delta","text":" turn"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"text-delta","text":" complete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"step-completed","stepId":1,"stepDurationMs":816}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","update":{"type":"turn-ended","usage":{"inputTokens":10461,"outputTokens":28,"cacheReadTokens":10402,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:2","frame":{"type":"run.completed","result":{"id":"run-2fefd1d7-40f3-4ffc-bbd3-30e709a85935","requestId":"94d9bed1-7b67-4093-ac41-64a43014290e","status":"finished","result":"second fixture turn complete","model":{"id":"composer-2.5"},"durationMs":1727}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-93ba82d8-7489-48f7-9b93-b9f65f425666"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/grok_transcript.ndjson new file mode 100644 index 00000000000..541d21f79c1 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/grok_transcript.ndjson @@ -0,0 +1,12 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"multi_turn","metadata":{"generatedBy":"protocol-semantic-fixture","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"grok-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"}]}}}} +{"type":"expect_outbound","label":"first.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Respond with exactly: first fixture turn complete"}]}}} +{"type":"emit_inbound","label":"first.assistant","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"first fixture turn complete"}}}}} +{"type":"emit_inbound","label":"first.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"expect_outbound","label":"second.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Respond with exactly: second fixture turn complete"}]}}} +{"type":"emit_inbound","label":"second.assistant","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"second fixture turn complete"}}}}} +{"type":"emit_inbound","label":"second.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/input.ts new file mode 100644 index 00000000000..8646fb6be2a --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/input.ts @@ -0,0 +1,14 @@ +import { + MULTI_TURN_FIRST_PROMPT, + MULTI_TURN_SECOND_PROMPT, + type OrchestratorFixtureInput, +} from "../shared.ts"; + +export function multiTurnInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: MULTI_TURN_FIRST_PROMPT }, + { type: "message", text: MULTI_TURN_SECOND_PROMPT }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn_restart/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn_restart/claude_transcript.ndjson new file mode 100644 index 00000000000..fef5d72c0e3 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn_restart/claude_transcript.ndjson @@ -0,0 +1,19 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"multi_turn_restart","metadata":{"prompts":["Respond with exactly: first fixture turn complete","Respond with exactly: second fixture turn complete"],"model":"claude-sonnet-4-6","nativeSessionId":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f","queryMode":"restart","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open:1","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: first fixture turn complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"96353a54-8984-4e77-8060-b1dc6b030cf6","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"ffbfcc28-dd82-4c2a-a99f-58e5af6e393e","session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"96353a54-8984-4e77-8060-b1dc6b030cf6","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"bbf29fb9-d4b3-4ab2-b0e3-9d2aa833099c","session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-multi_turn_restart","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"f468f37d-e0e6-43cf-bcd7-71e34045083e","session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01DKpWHtEMhugcX5nJwxKfeM","type":"message","role":"assistant","content":[{"type":"text","text":"first fixture turn complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11792,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f","uuid":"7e96c9db-0543-4276-b97d-ced7f53b93c4"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"28620e49-aa10-42fa-a7f9-768c9c79c093","session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2809,"duration_api_ms":2523,"num_turns":1,"result":"first fixture turn complete","stop_reason":"end_turn","session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f","total_cost_usd":0.0036516,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11792,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":11792,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":7,"cacheReadInputTokens":11792,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0036516,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"d88c31ae-a8fd-4ed4-a168-96cfd3b0edd7"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"query.open:2","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f"}}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: second fixture turn complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"4db1e61e-de28-49ee-b7ef-c375670d0aae","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"66e900fe-b02e-41a0-ac08-92260125b13d","session_id":"377e06a0-fe46-420e-bfaa-b4b9fcc8b871"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"4db1e61e-de28-49ee-b7ef-c375670d0aae","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"03f2754d-daee-4128-b180-c1a39faa03f0","session_id":"377e06a0-fe46-420e-bfaa-b4b9fcc8b871"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-multi_turn_restart","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"5e1d64c9-a2ca-4ad1-ba2c-546ba18d1785","session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01CiTVsN2HCfqr6Ge166z8hv","type":"message","role":"assistant","content":[{"type":"text","text":"second fixture turn complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11811,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":7,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f","uuid":"e821cdff-a217-4fb3-b1d0-f89387dbac41"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"21e2dfd3-a7f5-48d1-a343-ddb1be3198c8","session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2103,"duration_api_ms":1644,"num_turns":1,"result":"second fixture turn complete","stop_reason":"end_turn","session_id":"a9b9a331-6bf3-4753-93dd-b14ff7130f3f","total_cost_usd":0.0036573,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11811,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":11811,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":7,"cacheReadInputTokens":11811,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0036573,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"a876579c-a18c-4237-ba4e-6105b2162bbb"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode_subagent/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode_subagent/input.ts new file mode 100644 index 00000000000..29fdd8910b4 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode_subagent/input.ts @@ -0,0 +1,7 @@ +import { OPENCODE_SUBAGENT_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function openCodeSubagentInput(): OrchestratorFixtureInput { + return { + steps: [{ type: "message", text: OPENCODE_SUBAGENT_PROMPT }], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode_subagent/opencode_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode_subagent/opencode_transcript.ndjson new file mode 100644 index 00000000000..f8251799646 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode_subagent/opencode_transcript.ndjson @@ -0,0 +1,30 @@ +{"type":"transcript_start","provider":"opencode","protocol":"opencode-sdk.sse","version":"1.14.39","scenario":"opencode_subagent","metadata":{"source":"authenticated-opencode-sdk-probe","capturedAt":"2026-06-18","nativeSessionId":"ses_1235df8f8ffekByXP7gVnnXA3P","nativeChildSessionId":"ses_1235debc9ffe9MNk00UheV2j0q","model":"openai/gpt-5.4-mini","description":"One real OpenCode task-tool delegation. The session.update boundary records the adapter policy propagation required before projecting buffered child events."}} +{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}} +{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"title":"","permission":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_1235df8f8ffekByXP7gVnnXA3P","slug":"kind-tiger","version":"1.14.39","projectID":"global","directory":"/private/tmp/t3-opencode-probe","path":"private/tmp/t3-opencode-probe","title":"T3 OpenCode v2 probe","time":{"created":1781818066695,"updated":1781818066695}}}} +{"type":"expect_outbound","label":"session.promptAsync","frame":{"type":"session.promptAsync","input":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","model":{"providerID":"openai","modelID":"gpt-5.4-mini"},"agent":"build","parts":[{"type":"text","text":"Use the task tool exactly once. Delegate to the general subagent with this prompt: Respond exactly CHILD_OK. After the task completes, respond exactly PARENT_OK."}]}}} +{"type":"emit_inbound","label":"session.promptAsync.response","frame":{"type":"sdk.response","operation":"session.promptAsync","data":null}} +{"type":"emit_inbound","label":"root.user","frame":{"type":"sdk.event","event":{"id":"evt_edca20735003a32yj1UtyUpHkU","type":"message.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","info":{"id":"msg_edca20735001Esh39V762TEx7f","role":"user","sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","time":{"created":1781818066741},"agent":"build","model":{"providerID":"openai","modelID":"gpt-5.4-mini"}}}}}} +{"type":"emit_inbound","label":"root.user.text","frame":{"type":"sdk.event","event":{"id":"evt_edca207360015UIWeYXmKllHo3","type":"message.part.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","part":{"type":"text","text":"Use the task tool exactly once. Delegate to the general subagent with this prompt: Respond exactly CHILD_OK. After the task completes, respond exactly PARENT_OK.","messageID":"msg_edca20735001Esh39V762TEx7f","sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","id":"prt_edca20735002U603TIFRMOYd9M"},"time":1781818066742}}}} +{"type":"emit_inbound","label":"root.busy","frame":{"type":"sdk.event","event":{"id":"evt_edca207370013ecC1B3n7HWHF0","type":"session.status","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","status":{"type":"busy"}}}}} +{"type":"emit_inbound","label":"root.assistant.tool-step","frame":{"type":"sdk.event","event":{"id":"evt_edca207380024rQ5C0LdF5jkgv","type":"message.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","info":{"id":"msg_edca20738001L0AkDDqxC8ipdK","parentID":"msg_edca20735001Esh39V762TEx7f","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781818066744},"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P"}}}}} +{"type":"emit_inbound","label":"task.pending","frame":{"type":"sdk.event","event":{"id":"evt_edca213fb002A7172u3WxPY2Jy","type":"message.part.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","part":{"id":"prt_edca213fb001pDRIA33EBmsHOT","messageID":"msg_edca20738001L0AkDDqxC8ipdK","sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","type":"tool","tool":"task","callID":"call_MMPbMUvLDj12T5aimkDZC8Sl","state":{"status":"pending","input":{},"raw":""}},"time":1781818070011}}}} +{"type":"emit_inbound","label":"task.running","frame":{"type":"sdk.event","event":{"id":"evt_edca21438001pcNOWgyXdKFyaI","type":"message.part.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","part":{"type":"tool","tool":"task","callID":"call_MMPbMUvLDj12T5aimkDZC8Sl","state":{"title":"Return CHILD_OK","metadata":{"sessionId":"ses_1235debc9ffe9MNk00UheV2j0q","model":{"modelID":"gpt-5.4-mini","providerID":"openai"}},"status":"running","input":{"description":"Return CHILD_OK","prompt":"Respond exactly CHILD_OK.","subagent_type":"general","task_id":"","command":"delegate exact response"},"time":{"start":1781818070072}},"id":"prt_edca213fb001pDRIA33EBmsHOT","sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","messageID":"msg_edca20738001L0AkDDqxC8ipdK"},"time":1781818070072}}}} +{"type":"expect_outbound","label":"session.get.child-policy","frame":{"type":"session.get","input":{"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q"}}} +{"type":"emit_inbound","label":"session.get.child-policy.response","frame":{"type":"sdk.response","operation":"session.get","data":{"id":"ses_1235debc9ffe9MNk00UheV2j0q","slug":"tidy-engine","projectID":"global","directory":"/private/tmp/t3-opencode-probe","path":"private/tmp/t3-opencode-probe","parentID":"ses_1235df8f8ffekByXP7gVnnXA3P","title":"Return CHILD_OK (@general subagent)","version":"1.14.39","permission":[{"permission":"task","action":"deny","pattern":"*"}],"time":{"created":1781818070070,"updated":1781818070072}}}} +{"type":"expect_outbound","label":"session.update.child-policy","frame":{"type":"session.update","input":{"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","permission":""}}} +{"type":"emit_inbound","label":"session.update.child-policy.response","frame":{"type":"sdk.response","operation":"session.update","data":{"id":"ses_1235debc9ffe9MNk00UheV2j0q","permission":[{"permission":"*","pattern":"*","action":"allow"},{"permission":"task","pattern":"*","action":"deny"}]}}} +{"type":"emit_inbound","label":"child.user","frame":{"type":"sdk.event","event":{"id":"evt_edca21439002NEQt3QTkDp4ENI","type":"message.updated","properties":{"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","info":{"id":"msg_edca21438002r6NiMUSoqNKyyO","role":"user","sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","time":{"created":1781818070073},"tools":{"task":false},"agent":"general","model":{"providerID":"openai","modelID":"gpt-5.4-mini"}}}}}} +{"type":"emit_inbound","label":"child.user.text","frame":{"type":"sdk.event","event":{"id":"evt_edca21439003pseKePjnoGiTMW","type":"message.part.updated","properties":{"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","part":{"type":"text","text":"Respond exactly CHILD_OK.","messageID":"msg_edca21438002r6NiMUSoqNKyyO","sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","id":"prt_edca214390014Naiz0G5HjlDbr"},"time":1781818070073}}}} +{"type":"emit_inbound","label":"child.busy","frame":{"type":"sdk.event","event":{"id":"evt_edca2143c0013lzfo6Odo5FMzE","type":"session.status","properties":{"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","status":{"type":"busy"}}}}} +{"type":"emit_inbound","label":"child.assistant","frame":{"type":"sdk.event","event":{"id":"evt_edca2143c0033666Mhyl3cAlhK","type":"message.updated","properties":{"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","info":{"id":"msg_edca2143c002Eu46Ey4Vg2BbuM","parentID":"msg_edca21438002r6NiMUSoqNKyyO","role":"assistant","mode":"general","agent":"general","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781818070076},"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q"}}}}} +{"type":"emit_inbound","label":"child.assistant.text","frame":{"type":"sdk.event","event":{"id":"evt_edca21a3f001VbjReH1JvfruIc","type":"message.part.updated","properties":{"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","part":{"id":"prt_edca21a22001xAMVOfMPeu3Pe5","messageID":"msg_edca2143c002Eu46Ey4Vg2BbuM","sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","type":"text","text":"CHILD_OK","time":{"start":1781818071586,"end":1781818071615},"metadata":{"openai":{"itemId":"msg_08dabd7d4c704130016a3462d793f48196a3764bab4f871b8d","phase":"final_answer"}}},"time":1781818071615}}}} +{"type":"emit_inbound","label":"child.assistant.completed","frame":{"type":"sdk.event","event":{"id":"evt_edca21a5d001RVnmfbE88crcg3","type":"message.updated","properties":{"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","info":{"id":"msg_edca2143c002Eu46Ey4Vg2BbuM","parentID":"msg_edca21438002r6NiMUSoqNKyyO","role":"assistant","mode":"general","agent":"general","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"total":7538,"input":7515,"output":9,"reasoning":14,"cache":{"write":0,"read":0}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781818070076,"completed":1781818071645},"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","finish":"stop"}}}}} +{"type":"emit_inbound","label":"child.idle","frame":{"type":"sdk.event","event":{"id":"evt_edca21a5d0033WtPpZQdeaACpg","type":"session.status","properties":{"sessionID":"ses_1235debc9ffe9MNk00UheV2j0q","status":{"type":"idle"}}}}} +{"type":"emit_inbound","label":"task.completed","frame":{"type":"sdk.event","event":{"id":"evt_edca21a5e001nGDHspvGlDIOu3","type":"message.part.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","part":{"type":"tool","tool":"task","callID":"call_MMPbMUvLDj12T5aimkDZC8Sl","state":{"status":"completed","input":{"description":"Return CHILD_OK","prompt":"Respond exactly CHILD_OK.","subagent_type":"general","task_id":"","command":"delegate exact response"},"output":"task_id: ses_1235debc9ffe9MNk00UheV2j0q (for resuming to continue this task if needed)\n\n\nCHILD_OK\n","metadata":{"sessionId":"ses_1235debc9ffe9MNk00UheV2j0q","model":{"modelID":"gpt-5.4-mini","providerID":"openai"},"truncated":false},"title":"Return CHILD_OK","time":{"start":1781818070075,"end":1781818071646}},"metadata":{"openai":{"itemId":"fc_0f59e88787b4bf7e016a3462d5f5d08196893b4fa241992c81"}},"id":"prt_edca213fb001pDRIA33EBmsHOT","sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","messageID":"msg_edca20738001L0AkDDqxC8ipdK"},"time":1781818071646}}}} +{"type":"emit_inbound","label":"root.assistant.tool-step.completed","frame":{"type":"sdk.event","event":{"id":"evt_edca21a5f001I6X2xOrOgrkGi4","type":"message.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","info":{"id":"msg_edca20738001L0AkDDqxC8ipdK","parentID":"msg_edca20735001Esh39V762TEx7f","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"total":10945,"input":10835,"output":43,"reasoning":67,"cache":{"write":0,"read":0}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781818066744,"completed":1781818071647},"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","finish":"tool-calls"}}}}} +{"type":"emit_inbound","label":"root.assistant.final-step","frame":{"type":"sdk.event","event":{"id":"evt_edca21a5f004jTKIwlcQchMbZY","type":"message.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","info":{"id":"msg_edca21a5f003lreMfrQqG0qWIe","parentID":"msg_edca20735001Esh39V762TEx7f","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781818071647},"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P"}}}}} +{"type":"emit_inbound","label":"root.assistant.text","frame":{"type":"sdk.event","event":{"id":"evt_edca21e36001X0l8t5UPa1gOe3","type":"message.part.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","part":{"id":"prt_edca21d9e001I40GbbzvlzhQ6Z","messageID":"msg_edca21a5f003lreMfrQqG0qWIe","sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","type":"text","text":"PARENT_OK","time":{"start":1781818072478,"end":1781818072630},"metadata":{"openai":{"itemId":"msg_06e59296cb53fc7c016a3462d879288196aead5e9838c741df","phase":"final_answer"}}},"time":1781818072630}}}} +{"type":"emit_inbound","label":"root.assistant.completed","frame":{"type":"sdk.event","event":{"id":"evt_edca21e3800181OyYIBTDTTK3r","type":"message.updated","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","info":{"id":"msg_edca21a5f003lreMfrQqG0qWIe","parentID":"msg_edca20735001Esh39V762TEx7f","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"total":11007,"input":248,"output":7,"reasoning":0,"cache":{"write":0,"read":10752}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781818071647,"completed":1781818072632},"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","finish":"stop"}}}}} +{"type":"emit_inbound","label":"root.idle","frame":{"type":"sdk.event","event":{"id":"evt_edca21e39001lo71BB2Ym4WFC2","type":"session.status","properties":{"sessionID":"ses_1235df8f8ffekByXP7gVnnXA3P","status":{"type":"idle"}}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode_subagent/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode_subagent/output.ts new file mode 100644 index 00000000000..c6ea4f4b867 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode_subagent/output.ts @@ -0,0 +1,71 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertExecutionNodeKinds, + assertNoExtraAppRunsForProviderChildren, + assertRunProviderTurnCardinality, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + OPENCODE_SUBAGENT_PROMPT, + projectionFor, +} from "../shared.ts"; + +export function assertOpenCodeSubagentOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertExecutionNodeKinds(projection, ["root_turn", "subagent", "assistant_message"]); + assertTurnItemTypes(projection, ["user_message", "subagent", "assistant_message"]); + assertRunProviderTurnCardinality({ projection, rootRunCount: 1 }); + assertNoExtraAppRunsForProviderChildren({ projection, expectedAppRuns: 1 }); + assertUserMessagesInclude(projection, [OPENCODE_SUBAGENT_PROMPT]); + assertAssistantTextIncludes(projection, "PARENT_OK"); + + assert.lengthOf(projection.subagents, 1); + assert.lengthOf(result.shellSnapshot.threads, 2); + const subagent = projection.subagents[0]; + assert.isDefined(subagent); + assert.equal(subagent.origin, "provider_native"); + assert.equal(subagent.createdBy, "agent"); + assert.equal(subagent.driver, "opencode"); + assert.equal(subagent.status, "completed"); + assert.equal(subagent.prompt, "Respond exactly CHILD_OK."); + assert.include(subagent.result ?? "", "CHILD_OK"); + assert.isNotNull(subagent.childThreadId); + assert.isNotNull(subagent.providerThreadId); + assert.isNotNull(subagent.nativeTaskRef); + assert.isNotNull(subagent.completedAt); + if (subagent.childThreadId === null || subagent.providerThreadId === null) { + throw new Error("OpenCode subagent is missing its child thread identity"); + } + + const providerThread = projection.providerThreads.find( + (thread) => thread.id === subagent.providerThreadId, + ); + assert.isDefined(providerThread); + assert.equal(providerThread.appThreadId, subagent.childThreadId); + assert.equal(providerThread.ownerNodeId, subagent.id); + + const childProjection = result.projections.get(subagent.childThreadId); + assert.isDefined(childProjection); + assert.equal(childProjection.thread.lineage.parentThreadId, projection.thread.id); + assert.equal(childProjection.thread.lineage.relationshipToParent, "subagent"); + assert.equal(childProjection.thread.activeProviderThreadId, providerThread.id); + assert.lengthOf(childProjection.runs, 0); + assert.lengthOf(childProjection.providerThreads, 1); + assert.lengthOf(childProjection.providerTurns, 1); + assert.equal(childProjection.providerTurns[0]?.status, "completed"); + assertExecutionNodeKinds(childProjection, ["root_turn", "assistant_message"]); + assertTurnItemTypes(childProjection, ["user_message", "assistant_message"]); + assertUserMessagesInclude(childProjection, ["Respond exactly CHILD_OK."]); + assertAssistantTextIncludes(childProjection, "CHILD_OK"); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/codex_output.ts new file mode 100644 index 00000000000..8b1abc5c4db --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/codex_output.ts @@ -0,0 +1,50 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAllRuntimeRequestsResolved, + assertAssistantTextIncludes, + assertBaseProjection, + assertExecutionNodeKinds, + assertRunOrdinals, + assertRuntimeRequestCounts, + assertRuntimeRequestKinds, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + PLAN_QUESTIONS_PROMPT, + projectionFor, +} from "../shared.ts"; + +export function assertPlanQuestionsOutputBase( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1]); + assertExecutionNodeKinds(projection, ["root_turn", "user_input_request", "assistant_message"]); + assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 }); + assertRuntimeRequestKinds(projection, ["user_input"]); + assertAllRuntimeRequestsResolved(projection); + assertTurnItemTypes(projection, ["user_message", "user_input_request", "assistant_message"]); + assertUserMessagesInclude(projection, [PLAN_QUESTIONS_PROMPT]); + assertAssistantTextIncludes(projection, "plan questions fixture complete"); +} + +export function assertPlanQuestionsOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertPlanQuestionsOutputBase(result, transcript); + + const projection = projectionFor(result, transcript.scenario); + const requestItem = projection.turnItems.find((item) => item.type === "user_input_request"); + assert.isDefined(requestItem); + assert.equal(requestItem?.questions[0]?.id, "schema_vs_ui_flexibility"); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/codex_transcript.ndjson new file mode 100644 index 00000000000..e4157905f64 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/codex_transcript.ndjson @@ -0,0 +1,67 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"plan_questions","metadata":{"source":"codex-app-server-probe","fileName":"plan_questions.ndjson","description":"One plan-mode turn that asks a structured clarifying question."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019db22a-e824-7ac3-bbf7-994a4aa087e5","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776810649,"updatedAt":1776810649,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T15-30-49-019db22a-e824-7ac3-bbf7-994a4aa087e5.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.3-codex","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"approvalPolicy":"never","input":[{"text":"Use request_user_input to ask one multiple-choice clarifying question about whether this fixture should prefer strict schemas or UI flexibility. After receiving the answer, respond exactly: plan questions fixture complete","type":"text"}],"sandboxPolicy":{"networkAccess":false,"type":"readOnly"},"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","collaborationMode":{"mode":"plan","settings":{"developer_instructions":"# Plan Mode (Conversational)\n\nYou work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.\n\n## Mode rules (strict)\n\nYou are in **Plan Mode** until a developer message explicitly ends it.\n\nPlan Mode is not changed by user intent, tone, or imperative language. If a user asks for execution while still in Plan Mode, treat it as a request to **plan the execution**, not perform it.\n\n## Plan Mode vs update_plan tool\n\nPlan Mode is a collaboration mode that can involve requesting user input and eventually issuing a `` block.\n\nSeparately, `update_plan` is a checklist/progress/TODOs tool; it does not enter or exit Plan Mode. Do not confuse it with Plan mode or try to use it while in Plan mode. If you try to use `update_plan` in Plan mode, it will return an error.\n\n## Execution vs. mutation in Plan Mode\n\nYou may explore and execute **non-mutating** actions that improve the plan. You must not perform **mutating** actions.\n\n### Allowed (non-mutating, plan-improving)\n\nActions that gather truth, reduce ambiguity, or validate feasibility without changing repo-tracked state. Examples:\n\n* Reading or searching files, configs, schemas, types, manifests, and docs\n* Static analysis, inspection, and repo exploration\n* Dry-run style commands when they do not edit repo-tracked files\n* Tests, builds, or checks that may write to caches or build artifacts (for example, `target/`, `.cache/`, or snapshots) so long as they do not edit repo-tracked files\n\n### Not allowed (mutating, plan-executing)\n\nActions that implement the plan or change repo-tracked state. Examples:\n\n* Editing or writing files\n* Running formatters or linters that rewrite files\n* Applying patches, migrations, or codegen that updates repo-tracked files\n* Side-effectful commands whose purpose is to carry out the plan rather than refine it\n\nWhen in doubt: if the action would reasonably be described as \"doing the work\" rather than \"planning the work,\" do not do it.\n\n## PHASE 1 - Ground in the environment (explore first, ask second)\n\nBegin by grounding yourself in the actual environment. Eliminate unknowns in the prompt by discovering facts, not by asking the user. Resolve all questions that can be answered through exploration or inspection. Identify missing or ambiguous details only if they cannot be derived from the environment. Silent exploration between turns is allowed and encouraged.\n\nBefore asking the user any question, perform at least one targeted non-mutating exploration pass (for example: search relevant files, inspect likely entrypoints/configs, confirm current implementation shape), unless no local environment/repo is available.\n\nException: you may ask clarifying questions about the user's prompt before exploring, ONLY if there are obvious ambiguities or contradictions in the prompt itself. However, if ambiguity might be resolved by exploring, always prefer exploring first.\n\nDo not ask questions that can be answered from the repo or system (for example, \"where is this struct?\" or \"which UI component should we use?\" when exploration can make it clear). Only ask once you have exhausted reasonable non-mutating exploration.\n\n## PHASE 2 - Intent chat (what they actually want)\n\n* Keep asking until you can clearly state: goal + success criteria, audience, in/out of scope, constraints, current state, and the key preferences/tradeoffs.\n* Bias toward questions over guessing: if any high-impact ambiguity remains, do NOT plan yet-ask.\n\n## PHASE 3 - Implementation chat (what/how we'll build)\n\n* Once intent is stable, keep asking until the spec is decision complete: approach, interfaces (APIs/schemas/I/O), data flow, edge cases/failure modes, testing + acceptance criteria, rollout/monitoring, and any migrations/compat constraints.\n\n## Asking questions\n\nCritical rules:\n\n* Strongly prefer using the `request_user_input` tool to ask any questions.\n* Offer only meaningful multiple-choice options; don't include filler choices that are obviously wrong or irrelevant.\n* In rare cases where an unavoidable, important question can't be expressed with reasonable multiple-choice options (due to extreme ambiguity), you may ask it directly without the tool.\n\nYou SHOULD ask many questions, but each question must:\n\n* materially change the spec/plan, OR\n* confirm/lock an assumption, OR\n* choose between meaningful tradeoffs.\n* not be answerable by non-mutating commands.\n\nUse the `request_user_input` tool only for decisions that materially change the plan, for confirming important assumptions, or for information that cannot be discovered via non-mutating exploration.\n\n## Two kinds of unknowns (treat differently)\n\n1. **Discoverable facts** (repo/system truth): explore first.\n\n * Before asking, run targeted searches and check likely sources of truth (configs/manifests/entrypoints/schemas/types/constants).\n * Ask only if: multiple plausible candidates; nothing found but you need a missing identifier/context; or ambiguity is actually product intent.\n * If asking, present concrete candidates (paths/service names) + recommend one.\n * Never ask questions you can answer from your environment (e.g., \"where is this struct\").\n\n2. **Preferences/tradeoffs** (not discoverable): ask early.\n\n * These are intent or implementation preferences that cannot be derived from exploration.\n * Provide 2-4 mutually exclusive options + a recommended default.\n * If unanswered, proceed with the recommended option and record it as an assumption in the final plan.\n\n## Finalization rule\n\nOnly output the final plan when it is decision complete and leaves no decisions to the implementer.\n\nWhen you present the official plan, wrap it in a `` block so the client can render it specially:\n\n1) The opening tag must be on its own line.\n2) Start the plan content on the next line (no text on the same line as the tag).\n3) The closing tag must be on its own line.\n4) Use Markdown inside the block.\n5) Keep the tags exactly as `` and `` (do not translate or rename them), even if the plan content is in another language.\n\nExample:\n\n\nplan content\n\n\nplan content should be human and agent digestible. The final plan must be plan-only and include:\n\n* A clear title\n* A brief summary section\n* Important changes or additions to public APIs/interfaces/types\n* Test cases and scenarios\n* Explicit assumptions and defaults chosen where needed\n\nDo not ask \"should I proceed?\" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a `` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan.\n\nOnly produce at most one `` block per turn, and only when you are presenting a complete spec.\n","model":"gpt-5.4","reasoning_effort":"medium"}}}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019db22a-e824-7ac3-bbf7-994a4aa087e5","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776810649,"updatedAt":1776810649,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T15-30-49-019db22a-e824-7ac3-bbf7-994a4aa087e5.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019db22a-e831-7b60-82fd-9eb12d353264","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turn":{"id":"019db22a-e831-7b60-82fd-9eb12d353264","items":[],"status":"inProgress","error":null,"startedAt":1776810649,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"c4e1268e-2e0c-46bb-9084-06e0cfe431c5","content":[{"type":"text","text":"Use request_user_input to ask one multiple-choice clarifying question about whether this fixture should prefer strict schemas or UI flexibility. After receiving the answer, respond exactly: plan questions fixture complete","text_elements":[]}]},"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"c4e1268e-2e0c-46bb-9084-06e0cfe431c5","content":[{"type":"text","text":"Use request_user_input to ask one multiple-choice clarifying question about whether this fixture should prefer strict schemas or UI flexibility. After receiving the answer, respond exactly: plan questions fixture complete","text_elements":[]}]},"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","text":"","phase":"commentary","memoryCitation":null},"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":"I"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":"’ll"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" ask"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" requested"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" multiple"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":"-choice"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" clarification"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" using"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":"request"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":"_user"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":"_input"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" then"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" wait"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" for"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" selected"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" preference"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" before"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" returning"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" exact"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" completion"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":" text"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_030666e5f3da47180169e7fa9ec6948199bec1205ddf98adea","text":"I’ll ask the requested multiple-choice clarification using `request_user_input`, then wait for the selected preference before returning the exact fixture completion text.","phase":"commentary","memoryCitation":null},"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264"}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","status":{"type":"active","activeFlags":["waitingOnUserInput"]}}}} +{"type":"emit_inbound","label":"item/tool/requestUserInput","frame":{"method":"item/tool/requestUserInput","id":0,"params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"call_cG9h11JYLbWjw78z3xcup9JJ","questions":[{"id":"schema_vs_ui_flexibility","header":"Fixture","question":"For this fixture, should the plan prefer strict schemas or UI flexibility?","isOther":true,"isSecret":false,"options":[{"label":"Strict schemas (Recommended)","description":"Favor precise contracts and validation even if UI changes require schema updates."},{"label":"UI flexibility","description":"Favor looser fixture shapes so the UI can evolve with less schema churn."}]}]}}} +{"type":"expect_outbound","label":"item/tool/requestUserInput","frame":{"id":0,"result":{"answers":{"schema_vs_ui_flexibility":{"answers":["Strict schemas (Recommended)"]}}}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","tokenUsage":{"total":{"totalTokens":26283,"inputTokens":26148,"cachedInputTokens":15744,"outputTokens":135,"reasoningOutputTokens":0},"last":{"totalTokens":26283,"inputTokens":26148,"cachedInputTokens":15744,"outputTokens":135,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"serverRequest/resolved","frame":{"method":"serverRequest/resolved","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","requestId":0}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_030666e5f3da47180169e7faa1bef481998a4a44763d13f9ec","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7faa1bef481998a4a44763d13f9ec","delta":"plan"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7faa1bef481998a4a44763d13f9ec","delta":" questions"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7faa1bef481998a4a44763d13f9ec","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","itemId":"msg_030666e5f3da47180169e7faa1bef481998a4a44763d13f9ec","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_030666e5f3da47180169e7faa1bef481998a4a44763d13f9ec","text":"plan questions fixture complete","phase":"final_answer","memoryCitation":null},"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turnId":"019db22a-e831-7b60-82fd-9eb12d353264","tokenUsage":{"total":{"totalTokens":52606,"inputTokens":52463,"cachedInputTokens":31488,"outputTokens":143,"reasoningOutputTokens":0},"last":{"totalTokens":26323,"inputTokens":26315,"cachedInputTokens":15744,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019db22a-e824-7ac3-bbf7-994a4aa087e5","turn":{"id":"019db22a-e831-7b60-82fd-9eb12d353264","items":[],"status":"completed","error":null,"startedAt":1776810649,"completedAt":1776810657,"durationMs":8310}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/grok_transcript.ndjson new file mode 100644 index 00000000000..be5d2bb29f6 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/grok_transcript.ndjson @@ -0,0 +1,11 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"plan_questions","metadata":{"generatedBy":"live-grok-shape-probe","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"grok-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"}]}}}} +{"type":"expect_outbound","label":"session.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Use request_user_input to ask one multiple-choice clarifying question about whether this fixture should prefer strict schemas or UI flexibility. After receiving the answer, respond exactly: plan questions fixture complete"}]}}} +{"type":"emit_inbound","label":"question.request","frame":{"kind":"request","method":"_x.ai/ask_user_question","params":{"method":"x.ai/ask_user_question","params":{"sessionId":"grok-replay-session-1","toolCallId":"ask-user-question-tool-call-1","questions":[{"id":"schema_vs_ui_flexibility","question":"For this fixture, should the plan prefer strict schemas or UI flexibility?","multiSelect":null,"options":[{"label":"Strict schemas (Recommended)","description":"Favor precise contracts and validation."},{"label":"UI flexibility","description":"Favor looser fixture shapes."}]}],"mode":"plan"}}}} +{"type":"expect_outbound","label":"question.response","frame":{"kind":"response","method":"_x.ai/ask_user_question","result":{"outcome":"accepted","answers":{"For this fixture, should the plan prefer strict schemas or UI flexibility?":["Strict schemas (Recommended)"]}}}} +{"type":"emit_inbound","label":"assistant.final","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"plan questions fixture complete"}}}}} +{"type":"emit_inbound","label":"prompt.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/input.ts new file mode 100644 index 00000000000..a3442ca6f5d --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/input.ts @@ -0,0 +1,17 @@ +import { PLAN_QUESTIONS_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function planQuestionsInput(): OrchestratorFixtureInput { + return { + interactionMode: "plan", + steps: [ + { type: "message", text: PLAN_QUESTIONS_PROMPT }, + { + type: "answer_next_user_input_request", + answers: { + schema_vs_ui_flexibility: "Strict schemas (Recommended)", + "question-0-schema-vs-flexibility": "Strict schemas", + }, + }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/opencode_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/opencode_output.ts new file mode 100644 index 00000000000..790b74379b1 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/opencode_output.ts @@ -0,0 +1,26 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { assertPlanQuestionsOutputBase } from "./codex_output.ts"; +import { projectionFor } from "../shared.ts"; + +export function assertOpenCodePlanQuestionsOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + // The shared assertions cover the runtime-request lifecycle. OpenCode owns + // its question identifiers, so assert its normalized native header instead + // of Codex's fixture-specific id. + assertPlanQuestionsOutputBase(result, transcript); + const projection = projectionFor(result, transcript.scenario); + const requestItem = projection.turnItems.find((item) => item.type === "user_input_request"); + assert.equal(requestItem?.questions[0]?.id, "question-0-schema-vs-flexibility"); + assert.equal(requestItem?.status, "completed"); + assert.lengthOf( + projection.turnItems.filter( + (item) => item.type === "dynamic_tool" && item.toolName === "question", + ), + 0, + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/opencode_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/opencode_transcript.ndjson new file mode 100644 index 00000000000..c6e7bb8e530 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/plan_questions/opencode_transcript.ndjson @@ -0,0 +1,23 @@ +{"type":"transcript_start","provider":"opencode","protocol":"opencode-sdk.sse","version":"1.14.39","scenario":"plan_questions","metadata":{"source":"authenticated-opencode-sdk-probe","capturedAt":"2026-06-18","nativeSessionId":"ses_12369ea4cffePl0BsyVg3hGlmZ","model":"openai/gpt-5.4-mini","filteredToRelevantEvents":true,"description":"Real structured-question request/reply and continuation across two assistant steps."}} +{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}} +{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"title":"","permission":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_12369ea4cffePl0BsyVg3hGlmZ","slug":"mighty-harbor","version":"1.14.39","projectID":"global","directory":"/private/tmp/t3-opencode-probe","path":"private/tmp/t3-opencode-probe","title":"T3 OpenCode v2 probe","time":{"created":1781817284019,"updated":1781817284019}}}} +{"type":"expect_outbound","label":"session.promptAsync","frame":{"type":"session.promptAsync","input":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","model":{"providerID":"openai","modelID":"gpt-5.4-mini"},"agent":"build","parts":[{"type":"text","text":"Use request_user_input to ask one multiple-choice clarifying question about whether this fixture should prefer strict schemas or UI flexibility. After receiving the answer, respond exactly: plan questions fixture complete"}]}}} +{"type":"emit_inbound","label":"message.updated.user","frame":{"type":"sdk.event","event":{"id":"evt_edc9615bb003WEUXUNfpZxMZDD","type":"message.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","info":{"id":"msg_edc9615bb001iCnWab1qzSeRA3","role":"user","sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","time":{"created":1781817284027},"agent":"build","model":{"providerID":"openai","modelID":"gpt-5.4-mini"}}}}}} +{"type":"emit_inbound","label":"message.part.updated.user","frame":{"type":"sdk.event","event":{"id":"evt_edc9615bc001l8KO7edlPViXO0","type":"message.part.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","part":{"type":"text","text":"Use request_user_input to ask one multiple-choice clarifying question about whether this fixture should prefer strict schemas or UI flexibility. After receiving the answer, respond exactly: plan questions fixture complete","messageID":"msg_edc9615bb001iCnWab1qzSeRA3","sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","id":"prt_edc9615bb002fjGpO5oeQE22B9"},"time":1781817284028}}}} +{"type":"emit_inbound","label":"session.promptAsync.response","frame":{"type":"sdk.response","operation":"session.promptAsync","data":null}} +{"type":"emit_inbound","label":"session.status.busy","frame":{"type":"sdk.event","event":{"id":"evt_edc9615bd0012brtruCEXYmNyX","type":"session.status","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","status":{"type":"busy"}}}}} +{"type":"emit_inbound","label":"message.updated.assistant","frame":{"type":"sdk.event","event":{"id":"evt_edc9615bd003GQj8GzeHgoMlZ2","type":"message.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","info":{"id":"msg_edc9615bd002qb49gh9N1krbX2","parentID":"msg_edc9615bb001iCnWab1qzSeRA3","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781817284029},"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ"}}}}} +{"type":"emit_inbound","label":"reasoning.completed","frame":{"type":"sdk.event","event":{"id":"evt_edc96244e001ez77NBs6gJaPZh","type":"message.part.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","part":{"id":"prt_edc961988001aollEeE48XSJDg","messageID":"msg_edc9615bd002qb49gh9N1krbX2","sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","type":"reasoning","text":"Clarifying user input before asking the structured question.","time":{"start":1781817285000,"end":1781817287757}},"time":1781817287757}}}} +{"type":"emit_inbound","label":"question.tool.pending","frame":{"type":"sdk.event","event":{"id":"evt_edc96244e003ljcUzfVu0WTFc2","type":"message.part.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","part":{"id":"prt_edc96244e002RBYg9ASwc10E8R","messageID":"msg_edc9615bd002qb49gh9N1krbX2","sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","type":"tool","tool":"question","callID":"call_XU0dxWGu2ioLZscBtYAIqGfT","state":{"status":"pending","input":{},"raw":""}},"time":1781817287758}}}} +{"type":"emit_inbound","label":"question.asked","frame":{"type":"sdk.event","event":{"id":"evt_edc9624d90022x2I3Rv9S6t1tt","type":"question.asked","properties":{"id":"que_edc9624d9001e2GFKX724Z8b5Z","sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","questions":[{"question":"Should this fixture prefer strict schemas or UI flexibility?","header":"Schema vs Flexibility","options":[{"label":"Strict schemas","description":"Prioritize validation and exact structure over visual freedom."},{"label":"UI flexibility","description":"Prioritize adaptable rendering and tolerant input handling."}],"multiple":false}],"tool":{"messageID":"msg_edc9615bd002qb49gh9N1krbX2","callID":"call_XU0dxWGu2ioLZscBtYAIqGfT"}}}}} +{"type":"expect_outbound","label":"question.reply","frame":{"type":"question.reply","input":{"requestID":"que_edc9624d9001e2GFKX724Z8b5Z","answers":[["Strict schemas"]]}}} +{"type":"emit_inbound","label":"question.reply.response","frame":{"type":"sdk.response","operation":"question.reply","data":true}} +{"type":"emit_inbound","label":"question.tool.running","frame":{"type":"sdk.event","event":{"id":"evt_edc9624da001qamTLzcYoY2CPY","type":"message.part.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","part":{"type":"tool","tool":"question","callID":"call_XU0dxWGu2ioLZscBtYAIqGfT","state":{"status":"running","input":{"questions":[{"question":"Should this fixture prefer strict schemas or UI flexibility?","header":"Schema vs Flexibility","options":[{"label":"Strict schemas","description":"Prioritize validation and exact structure over visual freedom."},{"label":"UI flexibility","description":"Prioritize adaptable rendering and tolerant input handling."}],"multiple":false}]},"raw":"","time":{"start":1781817287898}},"id":"prt_edc96244e002RBYg9ASwc10E8R","sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","messageID":"msg_edc9615bd002qb49gh9N1krbX2"},"time":1781817287898}}}} +{"type":"emit_inbound","label":"question.replied","frame":{"type":"sdk.event","event":{"id":"evt_edc9624de001JBbX1qaXzgfeKl","type":"question.replied","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","requestID":"que_edc9624d9001e2GFKX724Z8b5Z","answers":[["Strict schemas"]]}}}} +{"type":"emit_inbound","label":"question.tool.completed","frame":{"type":"sdk.event","event":{"id":"evt_edc9624df001Pw6H7GGa8tCa3V","type":"message.part.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","part":{"type":"tool","tool":"question","callID":"call_XU0dxWGu2ioLZscBtYAIqGfT","state":{"status":"completed","input":{"questions":[{"question":"Should this fixture prefer strict schemas or UI flexibility?","header":"Schema vs Flexibility","options":[{"label":"Strict schemas","description":"Prioritize validation and exact structure over visual freedom."},{"label":"UI flexibility","description":"Prioritize adaptable rendering and tolerant input handling."}],"multiple":false}]},"output":"User answered: Strict schemas","metadata":{"answers":[["Strict schemas"]],"truncated":false},"title":"Asked 1 question","time":{"start":1781817287898,"end":1781817287903}},"id":"prt_edc96244e002RBYg9ASwc10E8R","sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","messageID":"msg_edc9615bd002qb49gh9N1krbX2"},"time":1781817287903}}}} +{"type":"emit_inbound","label":"message.updated.assistant.second-step","frame":{"type":"sdk.event","event":{"id":"evt_edc9624fa002nuPVbTfhxoDpJh","type":"message.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","info":{"id":"msg_edc9624fa0015O6R1rRjuqc4uS","parentID":"msg_edc9615bb001iCnWab1qzSeRA3","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781817287930},"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ"}}}}} +{"type":"emit_inbound","label":"assistant.completed","frame":{"type":"sdk.event","event":{"id":"evt_edc9628b5001owQkxpRhqtRF5N","type":"message.part.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","part":{"id":"prt_edc962867001us1nuabvajQrgu","messageID":"msg_edc9624fa0015O6R1rRjuqc4uS","sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","type":"text","text":"plan questions fixture complete","time":{"start":1781817288807,"end":1781817288885}},"time":1781817288885}}}} +{"type":"emit_inbound","label":"message.updated.assistant.completed","frame":{"type":"sdk.event","event":{"id":"evt_edc9628bf002uUONicDkn1X7YA","type":"message.updated","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","info":{"id":"msg_edc9624fa0015O6R1rRjuqc4uS","parentID":"msg_edc9615bb001iCnWab1qzSeRA3","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"total":11041,"input":281,"output":8,"reasoning":0,"cache":{"write":0,"read":10752}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781817287930,"completed":1781817288895},"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","finish":"stop"}}}}} +{"type":"emit_inbound","label":"session.status.idle","frame":{"type":"sdk.event","event":{"id":"evt_edc9628c00021lW2ZypKwU71AE","type":"session.status","properties":{"sessionID":"ses_12369ea4cffePl0BsyVg3hGlmZ","status":{"type":"idle"}}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/codex_output.ts new file mode 100644 index 00000000000..935ebf841df --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/codex_output.ts @@ -0,0 +1,38 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertExecutionNodeKinds, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + PROPOSED_PLAN_PROMPT, +} from "../shared.ts"; + +export function assertProposedPlanOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1]); + assertExecutionNodeKinds(projection, ["root_turn", "plan"]); + assertTurnItemTypes(projection, ["user_message", "proposed_plan"]); + assertUserMessagesInclude(projection, [PROPOSED_PLAN_PROMPT]); + + const proposedPlans = projection.plans.filter((plan) => plan.kind === "proposed_plan"); + assert.isAtLeast(proposedPlans.length, 1); + assert.include(proposedPlans.at(-1)?.markdown, "Deterministic Replay Fixtures"); + + const proposedPlanItems = projection.turnItems.filter((item) => item.type === "proposed_plan"); + assert.isAtLeast(proposedPlanItems.length, 1); + assert.equal(proposedPlanItems.at(-1)?.streaming, false); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/codex_transcript.ndjson new file mode 100644 index 00000000000..df396322633 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/codex_transcript.ndjson @@ -0,0 +1,619 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"proposed_plan","metadata":{"source":"codex-app-server-probe","fileName":"proposed_plan.ndjson","description":"One plan-mode turn that emits a proposed plan document."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019db22b-08cd-7e12-b967-cf6ab32dd408","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776810658,"updatedAt":1776810658,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T15-30-58-019db22b-08cd-7e12-b967-cf6ab32dd408.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.3-codex","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"approvalPolicy":"never","input":[{"text":"Create a short implementation plan for adding deterministic replay fixtures. Do not ask questions. Present the final plan in a proposed plan block.","type":"text"}],"sandboxPolicy":{"networkAccess":false,"type":"readOnly"},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","collaborationMode":{"mode":"plan","settings":{"developer_instructions":"# Plan Mode (Conversational)\n\nYou work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.\n\n## Mode rules (strict)\n\nYou are in **Plan Mode** until a developer message explicitly ends it.\n\nPlan Mode is not changed by user intent, tone, or imperative language. If a user asks for execution while still in Plan Mode, treat it as a request to **plan the execution**, not perform it.\n\n## Plan Mode vs update_plan tool\n\nPlan Mode is a collaboration mode that can involve requesting user input and eventually issuing a `` block.\n\nSeparately, `update_plan` is a checklist/progress/TODOs tool; it does not enter or exit Plan Mode. Do not confuse it with Plan mode or try to use it while in Plan mode. If you try to use `update_plan` in Plan mode, it will return an error.\n\n## Execution vs. mutation in Plan Mode\n\nYou may explore and execute **non-mutating** actions that improve the plan. You must not perform **mutating** actions.\n\n### Allowed (non-mutating, plan-improving)\n\nActions that gather truth, reduce ambiguity, or validate feasibility without changing repo-tracked state. Examples:\n\n* Reading or searching files, configs, schemas, types, manifests, and docs\n* Static analysis, inspection, and repo exploration\n* Dry-run style commands when they do not edit repo-tracked files\n* Tests, builds, or checks that may write to caches or build artifacts (for example, `target/`, `.cache/`, or snapshots) so long as they do not edit repo-tracked files\n\n### Not allowed (mutating, plan-executing)\n\nActions that implement the plan or change repo-tracked state. Examples:\n\n* Editing or writing files\n* Running formatters or linters that rewrite files\n* Applying patches, migrations, or codegen that updates repo-tracked files\n* Side-effectful commands whose purpose is to carry out the plan rather than refine it\n\nWhen in doubt: if the action would reasonably be described as \"doing the work\" rather than \"planning the work,\" do not do it.\n\n## PHASE 1 - Ground in the environment (explore first, ask second)\n\nBegin by grounding yourself in the actual environment. Eliminate unknowns in the prompt by discovering facts, not by asking the user. Resolve all questions that can be answered through exploration or inspection. Identify missing or ambiguous details only if they cannot be derived from the environment. Silent exploration between turns is allowed and encouraged.\n\nBefore asking the user any question, perform at least one targeted non-mutating exploration pass (for example: search relevant files, inspect likely entrypoints/configs, confirm current implementation shape), unless no local environment/repo is available.\n\nException: you may ask clarifying questions about the user's prompt before exploring, ONLY if there are obvious ambiguities or contradictions in the prompt itself. However, if ambiguity might be resolved by exploring, always prefer exploring first.\n\nDo not ask questions that can be answered from the repo or system (for example, \"where is this struct?\" or \"which UI component should we use?\" when exploration can make it clear). Only ask once you have exhausted reasonable non-mutating exploration.\n\n## PHASE 2 - Intent chat (what they actually want)\n\n* Keep asking until you can clearly state: goal + success criteria, audience, in/out of scope, constraints, current state, and the key preferences/tradeoffs.\n* Bias toward questions over guessing: if any high-impact ambiguity remains, do NOT plan yet-ask.\n\n## PHASE 3 - Implementation chat (what/how we'll build)\n\n* Once intent is stable, keep asking until the spec is decision complete: approach, interfaces (APIs/schemas/I/O), data flow, edge cases/failure modes, testing + acceptance criteria, rollout/monitoring, and any migrations/compat constraints.\n\n## Asking questions\n\nCritical rules:\n\n* Strongly prefer using the `request_user_input` tool to ask any questions.\n* Offer only meaningful multiple-choice options; don't include filler choices that are obviously wrong or irrelevant.\n* In rare cases where an unavoidable, important question can't be expressed with reasonable multiple-choice options (due to extreme ambiguity), you may ask it directly without the tool.\n\nYou SHOULD ask many questions, but each question must:\n\n* materially change the spec/plan, OR\n* confirm/lock an assumption, OR\n* choose between meaningful tradeoffs.\n* not be answerable by non-mutating commands.\n\nUse the `request_user_input` tool only for decisions that materially change the plan, for confirming important assumptions, or for information that cannot be discovered via non-mutating exploration.\n\n## Two kinds of unknowns (treat differently)\n\n1. **Discoverable facts** (repo/system truth): explore first.\n\n * Before asking, run targeted searches and check likely sources of truth (configs/manifests/entrypoints/schemas/types/constants).\n * Ask only if: multiple plausible candidates; nothing found but you need a missing identifier/context; or ambiguity is actually product intent.\n * If asking, present concrete candidates (paths/service names) + recommend one.\n * Never ask questions you can answer from your environment (e.g., \"where is this struct\").\n\n2. **Preferences/tradeoffs** (not discoverable): ask early.\n\n * These are intent or implementation preferences that cannot be derived from exploration.\n * Provide 2-4 mutually exclusive options + a recommended default.\n * If unanswered, proceed with the recommended option and record it as an assumption in the final plan.\n\n## Finalization rule\n\nOnly output the final plan when it is decision complete and leaves no decisions to the implementer.\n\nWhen you present the official plan, wrap it in a `` block so the client can render it specially:\n\n1) The opening tag must be on its own line.\n2) Start the plan content on the next line (no text on the same line as the tag).\n3) The closing tag must be on its own line.\n4) Use Markdown inside the block.\n5) Keep the tags exactly as `` and `` (do not translate or rename them), even if the plan content is in another language.\n\nExample:\n\n\nplan content\n\n\nplan content should be human and agent digestible. The final plan must be plan-only and include:\n\n* A clear title\n* A brief summary section\n* Important changes or additions to public APIs/interfaces/types\n* Test cases and scenarios\n* Explicit assumptions and defaults chosen where needed\n\nDo not ask \"should I proceed?\" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a `` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan.\n\nOnly produce at most one `` block per turn, and only when you are presenting a complete spec.\n","model":"gpt-5.4","reasoning_effort":"medium"}}}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019db22b-08cd-7e12-b967-cf6ab32dd408","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776810658,"updatedAt":1776810658,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T15-30-58-019db22b-08cd-7e12-b967-cf6ab32dd408.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turn":{"id":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","items":[],"status":"inProgress","error":null,"startedAt":1776810658,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"ab25b70d-641e-4c17-af07-1c8d78d88b0a","content":[{"type":"text","text":"Create a short implementation plan for adding deterministic replay fixtures. Do not ask questions. Present the final plan in a proposed plan block.","text_elements":[]}]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"ab25b70d-641e-4c17-af07-1c8d78d88b0a","content":[{"type":"text","text":"Create a short implementation plan for adding deterministic replay fixtures. Do not ask questions. Present the final plan in a proposed plan block.","text_elements":[]}]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0b2955d3a75fd8630169e7faa644c08199bc55546beda2214b","summary":[],"content":[]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0b2955d3a75fd8630169e7faa644c08199bc55546beda2214b","summary":[],"content":[]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","text":"","phase":"commentary","memoryCitation":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":"I"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":"’ll"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" ground"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" plan"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" in"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" structure"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" then"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" produce"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" concise"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" decision"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":"-com"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":"plete"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" implementation"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" plan"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" without"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":" questions"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0b2955d3a75fd8630169e7faa6b5208199bd94e7bea8298058","text":"I’ll ground the plan in the package structure first, then produce a concise decision-complete implementation plan without questions.","phase":"commentary","memoryCitation":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","tokenUsage":{"total":{"totalTokens":26377,"inputTokens":26138,"cachedInputTokens":15744,"outputTokens":239,"reasoningOutputTokens":33},"last":{"totalTokens":26377,"inputTokens":26138,"cachedInputTokens":15744,"outputTokens":239,"reasoningOutputTokens":33},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_tEQUu764R8mtF1ZCsFOt2XaV","command":"/bin/zsh -lc 'find .. -name AGENTS.md -print'","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"55615","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"search","command":"find .. -name AGENTS.md -print","query":"AGENTS.md","path":".."}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_eyjz6BPmvc8LMkoXbSFsGlAw","command":"/bin/zsh -lc \"pwd && rg --files | sed -n '1,120p'\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"17756","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"unknown","command":"pwd && rg --files | sed -n '1,120p'"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_tEQUu764R8mtF1ZCsFOt2XaV","command":"/bin/zsh -lc 'find .. -name AGENTS.md -print'","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"55615","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"search","command":"find .. -name AGENTS.md -print","query":"AGENTS.md","path":".."}],"aggregatedOutput":null,"exitCode":0,"durationMs":0},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_eyjz6BPmvc8LMkoXbSFsGlAw","command":"/bin/zsh -lc \"pwd && rg --files | sed -n '1,120p'\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"17756","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"unknown","command":"pwd && rg --files | sed -n '1,120p'"}],"aggregatedOutput":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server\nsrc/_generated/namespaces.gen.ts\nsrc/_generated/schema.gen.ts\nsrc/_generated/meta.gen.ts\nsrc/client.test.ts\nsrc/replay.test.ts\nsrc/protocol.ts\nsrc/client.ts\nsrc/replay.ts\nsrc/rpc.ts\nsrc/errors.ts\nsrc/_internal/shared.ts\nsrc/_internal/stdio.ts\nsrc/schema.ts\nsrc/protocol.test.ts\ntsconfig.json\nscripts/generate.ts\npackage.json\ntest/fixtures/codex-app-server-mock-peer.ts\ntest/fixtures/codex-app-server-probes/tool_call_read_only_on_request.ndjson\ntest/fixtures/codex-app-server-probes/thread_rollback.ndjson\ntest/fixtures/codex-app-server-probes/simple.ndjson\ntest/fixtures/codex-app-server-probes/todo_list.ndjson\ntest/fixtures/codex-app-server-probes/plan_questions.ndjson\ntest/fixtures/codex-app-server-probes/turn_interrupt.ndjson\ntest/fixtures/codex-app-server-probes/tool_call_restricted_granular.ndjson\ntest/fixtures/codex-app-server-probes/multi_turn.ndjson\ntest/fixtures/codex-app-server-probes/proposed_plan.ndjson\ntest/fixtures/codex-app-server-probes/message_steering.ndjson\ntest/fixtures/codex-app-server-probes/subagent.ndjson\ntest/fixtures/codex-app-server-probes/tool_call_workspace_never.ndjson\ntest/examples/codex-app-server-probe.ts\n","exitCode":0,"durationMs":0},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0b2955d3a75fd8630169e7faaa1df08199b13b72cd3ef95057","summary":[],"content":[]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0b2955d3a75fd8630169e7faaa1df08199b13b72cd3ef95057","summary":[],"content":[]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","text":"","phase":"commentary","memoryCitation":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":"The"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" already"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" has"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" replay"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":"-oriented"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" code"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" probe"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" ND"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":"JSON"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" fixtures"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" so"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" I"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":"’m"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" checking"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" those"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" shapes"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" make"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" plan"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" target"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" existing"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" abstra"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":"ctions"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" rather"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" than"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" invent"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" parallel"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":" system"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0b2955d3a75fd8630169e7faaa89188199b0743576f83c9470","text":"The package already has replay-oriented code and probe NDJSON fixtures, so I’m checking those shapes to make the plan target the existing abstractions rather than invent a parallel fixture system.","phase":"commentary","memoryCitation":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","tokenUsage":{"total":{"totalTokens":53584,"inputTokens":52954,"cachedInputTokens":41728,"outputTokens":630,"reasoningOutputTokens":43},"last":{"totalTokens":27207,"inputTokens":26816,"cachedInputTokens":25984,"outputTokens":391,"reasoningOutputTokens":10},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_0B2ieGRdMDXmQA8peWORTcTz","command":"/bin/zsh -lc \"sed -n '1,220p' src/replay.ts && printf '\\\\n--- replay.test ---\\\\n' && sed -n '1,240p' src/replay.test.ts\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"16103","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"read","command":"sed -n '1,220p' src/replay.ts","name":"replay.ts","path":"src/replay.ts"},{"type":"read","command":"sed -n '1,240p' src/replay.test.ts","name":"replay.test.ts","path":"src/replay.test.ts"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_A9NEOfTYCCXkUPLqxfWsCNE5","command":"/bin/zsh -lc \"cat package.json && printf '\\\\n--- protocol ---\\\\n' && sed -n '1,220p' src/protocol.ts\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"24242","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"read","command":"cat package.json","name":"package.json","path":"package.json"},{"type":"read","command":"sed -n '1,220p' src/protocol.ts","name":"protocol.ts","path":"src/protocol.ts"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_yp787C983Bb0Klnj9F8zRBDd","command":"/bin/zsh -lc \"sed -n '1,220p' test/fixtures/codex-app-server-mock-peer.ts && printf '\\\\n--- sample fixture ---\\\\n' && sed -n '1,80p' test/fixtures/codex-app-server-probes/simple.ndjson\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"80213","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"read","command":"sed -n '1,220p' test/fixtures/codex-app-server-mock-peer.ts","name":"codex-app-server-mock-peer.ts","path":"test/fixtures/codex-app-server-mock-peer.ts"},{"type":"read","command":"sed -n '1,80p' test/fixtures/codex-app-server-probes/simple.ndjson","name":"simple.ndjson","path":"test/fixtures/codex-app-server-probes/simple.ndjson"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_A9NEOfTYCCXkUPLqxfWsCNE5","command":"/bin/zsh -lc \"cat package.json && printf '\\\\n--- protocol ---\\\\n' && sed -n '1,220p' src/protocol.ts\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"24242","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"read","command":"cat package.json","name":"package.json","path":"package.json"},{"type":"read","command":"sed -n '1,220p' src/protocol.ts","name":"protocol.ts","path":"src/protocol.ts"}],"aggregatedOutput":"{\n \"name\": \"effect-codex-app-server\",\n \"private\": true,\n \"type\": \"module\",\n \"exports\": {\n \"./client\": {\n \"types\": \"./src/client.ts\",\n \"import\": \"./src/client.ts\"\n },\n \"./schema\": {\n \"types\": \"./src/schema.ts\",\n \"import\": \"./src/schema.ts\"\n },\n \"./rpc\": {\n \"types\": \"./src/rpc.ts\",\n \"import\": \"./src/rpc.ts\"\n },\n \"./protocol\": {\n \"types\": \"./src/protocol.ts\",\n \"import\": \"./src/protocol.ts\"\n },\n \"./replay\": {\n \"types\": \"./src/replay.ts\",\n \"import\": \"./src/replay.ts\"\n },\n \"./errors\": {\n \"types\": \"./src/errors.ts\",\n \"import\": \"./src/errors.ts\"\n }\n },\n \"scripts\": {\n \"dev\": \"tsdown src/client.ts src/rpc.ts src/protocol.ts src/schema.ts --format esm,cjs --dts --watch --clean\",\n \"build\": \"tsdown src/client.ts src/rpc.ts src/protocol.ts src/schema.ts --format esm,cjs --dts --clean\",\n \"prepare\": \"effect-language-service patch\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"generate\": \"bun run scripts/generate.ts\",\n \"probe\": \"bun run test/examples/codex-app-server-probe.ts\"\n },\n \"dependencies\": {\n \"effect\": \"catalog:\"\n },\n \"devDependencies\": {\n \"@effect/language-service\": \"catalog:\",\n \"@effect/openapi-generator\": \"catalog:\",\n \"@effect/platform-node\": \"catalog:\",\n \"@effect/vitest\": \"catalog:\",\n \"tsdown\": \"catalog:\",\n \"typescript\": \"catalog:\",\n \"vitest\": \"catalog:\"\n }\n}\n\n--- protocol ---\nimport * as Cause from \"effect/Cause\";\nimport * as Deferred from \"effect/Deferred\";\nimport * as Effect from \"effect/Effect\";\nimport * as Queue from \"effect/Queue\";\nimport * as Ref from \"effect/Ref\";\nimport * as Scope from \"effect/Scope\";\nimport * as Schema from \"effect/Schema\";\nimport * as Stdio from \"effect/Stdio\";\nimport * as Stream from \"effect/Stream\";\n\nimport * as CodexError from \"./errors.ts\";\nimport { JsonRpcId, JsonRpcResponseEnvelope } from \"./_internal/shared.ts\";\n\nexport interface CodexAppServerProtocolLogEvent {\n readonly direction: \"incoming\" | \"outgoing\";\n readonly stage: \"raw\" | \"decoded\" | \"decode_failed\";\n readonly payload: unknown;\n}\n\nexport interface CodexAppServerIncomingNotification {\n readonly method: string;\n readonly params?: unknown;\n}\n\nexport interface CodexAppServerIncomingRequest {\n readonly id: string | number;\n readonly method: string;\n readonly params?: unknown;\n}\n\nexport interface CodexAppServerPatchedProtocolOptions {\n readonly stdio: Stdio.Stdio;\n readonly terminationError?: Effect.Effect;\n readonly logIncoming?: boolean;\n readonly logOutgoing?: boolean;\n readonly logger?: (event: CodexAppServerProtocolLogEvent) => Effect.Effect;\n readonly onNotification?: (\n notification: CodexAppServerIncomingNotification,\n ) => Effect.Effect;\n readonly onRequest?: (\n request: CodexAppServerIncomingRequest,\n ) => Effect.Effect;\n readonly onTermination?: (error: CodexError.CodexAppServerError) => Effect.Effect;\n}\n\nexport interface CodexAppServerPatchedProtocol {\n readonly incomingNotifications: Stream.Stream;\n readonly incomingRequests: Stream.Stream;\n readonly request: (\n method: string,\n payload?: unknown,\n ) => Effect.Effect;\n readonly notify: (\n method: string,\n payload?: unknown,\n ) => Effect.Effect;\n readonly respond: (\n requestId: string | number,\n result: unknown,\n ) => Effect.Effect;\n readonly respondError: (\n requestId: string | number,\n error: CodexError.CodexAppServerRequestError,\n ) => Effect.Effect;\n}\n\nfunction isObject(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isIncomingRequest(value: unknown): value is CodexAppServerIncomingRequest {\n if (!isObject(value) || typeof value.method !== \"string\") {\n return false;\n }\n return Schema.is(JsonRpcId)(value.id);\n}\n\nfunction isIncomingNotification(value: unknown): value is CodexAppServerIncomingNotification {\n return isObject(value) && typeof value.method === \"string\" && !(\"id\" in value);\n}\n\nfunction isIncomingResponse(value: unknown): value is typeof JsonRpcResponseEnvelope.Type {\n return Schema.is(JsonRpcResponseEnvelope)(value);\n}\n\nconst encodeWireMessage = (\n message: Record,\n): Effect.Effect =>\n Effect.try({\n try: () => `${JSON.stringify(message)}\\n`,\n catch: (cause) =>\n new CodexError.CodexAppServerProtocolParseError({\n detail: \"Failed to encode Codex App Server message\",\n cause,\n }),\n });\n\nconst normalizeIncomingError = (error: unknown, detail: string): CodexError.CodexAppServerError =>\n Schema.is(CodexError.CodexAppServerError)(error)\n ? error\n : new CodexError.CodexAppServerTransportError({\n detail,\n cause: error,\n });\n\nconst toProtocolMessage = (\n requestId: string | number,\n fields: {\n readonly result?: unknown;\n readonly error?: CodexError.CodexAppServerProtocolErrorShape;\n },\n): { readonly [key: string]: unknown } => ({\n id: requestId,\n ...(fields.result !== undefined ? { result: fields.result } : {}),\n ...(fields.error !== undefined ? { error: fields.error } : {}),\n});\n\nexport const makeCodexAppServerPatchedProtocol = Effect.fn(\"makeCodexAppServerPatchedProtocol\")(\n function* (\n options: CodexAppServerPatchedProtocolOptions,\n ): Effect.fn.Return {\n const outgoing = yield* Queue.unbounded>();\n const incomingNotifications = yield* Queue.unbounded();\n const incomingRequests = yield* Queue.unbounded();\n const pending = yield* Ref.make(\n new Map>(),\n );\n const nextRequestId = yield* Ref.make(1);\n const remainder = yield* Ref.make(\"\");\n const terminationHandled = yield* Ref.make(false);\n\n const logProtocol = (event: CodexAppServerProtocolLogEvent) => {\n if (event.direction === \"incoming\" && !options.logIncoming) {\n return Effect.void;\n }\n if (event.direction === \"outgoing\" && !options.logOutgoing) {\n return Effect.void;\n }\n return (\n options.logger?.(event) ??\n Effect.logDebug(\"Codex App Server protocol event\").pipe(Effect.annotateLogs({ event }))\n );\n };\n\n const failAllPending = (error: CodexError.CodexAppServerError) =>\n Ref.get(pending).pipe(\n Effect.flatMap((current) =>\n Effect.forEach([...current.values()], (deferred) => Deferred.fail(deferred, error), {\n discard: true,\n }),\n ),\n Effect.andThen(Ref.set(pending, new Map())),\n );\n\n const handleTermination = (classify: () => Effect.Effect) =>\n Ref.modify(terminationHandled, (handled) => {\n if (handled) {\n return [Effect.void, true] as const;\n }\n return [\n Effect.gen(function* () {\n const error = yield* classify();\n yield* failAllPending(error);\n yield* Queue.end(outgoing);\n if (options.onTermination) {\n yield* options.onTermination(error);\n }\n }),\n true,\n ] as const;\n }).pipe(Effect.flatten);\n\n const offerOutgoing = (message: Record) =>\n Effect.gen(function* () {\n yield* logProtocol({\n direction: \"outgoing\",\n stage: \"decoded\",\n payload: message,\n });\n const encoded = yield* encodeWireMessage(message);\n yield* logProtocol({\n direction: \"outgoing\",\n stage: \"raw\",\n payload: encoded,\n });\n yield* Queue.offer(outgoing, encoded).pipe(Effect.asVoid);\n });\n\n const removePending = (requestId: string) =>\n Ref.update(pending, (current) => {\n if (!current.has(requestId)) {\n return current;\n }\n const next = new Map(current);\n next.delete(requestId);\n return next;\n });\n\n const resolvePending = (\n requestId: string,\n handler: (\n deferred: Deferred.Deferred,\n ) => Effect.Effect,\n ) =>\n Ref.modify(pending, (current) => {\n const deferred = current.get(requestId);\n if (!deferred) {\n return [Effect.void, current] as const;\n }\n const next = new Map(current);\n next.delete(requestId);\n return [handler(deferred), next] as const;\n }).pipe(Effect.flatten);\n\n const respond = (requestId: string | number, result: unknown) =>\n offerOutgoing(toProtocolMessage(requestId, { result }));\n\n const respondError = (\n requestId: string | number,\n error: CodexError.CodexAppServerRequestError,\n","exitCode":0,"durationMs":0},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_0B2ieGRdMDXmQA8peWORTcTz","command":"/bin/zsh -lc \"sed -n '1,220p' src/replay.ts && printf '\\\\n--- replay.test ---\\\\n' && sed -n '1,240p' src/replay.test.ts\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"16103","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"read","command":"sed -n '1,220p' src/replay.ts","name":"replay.ts","path":"src/replay.ts"},{"type":"read","command":"sed -n '1,240p' src/replay.test.ts","name":"replay.test.ts","path":"src/replay.test.ts"}],"aggregatedOutput":"import * as Cause from \"effect/Cause\";\nimport * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as Queue from \"effect/Queue\";\nimport * as Ref from \"effect/Ref\";\nimport * as Schema from \"effect/Schema\";\nimport * as Sink from \"effect/Sink\";\nimport * as Stdio from \"effect/Stdio\";\nimport * as Stream from \"effect/Stream\";\n\nimport { CodexAppServerClient } from \"./client.ts\";\nimport * as CodexClient from \"./client.ts\";\nimport * as CodexError from \"./errors.ts\";\n\nexport const CodexAppServerReplayEntry = Schema.Union([\n Schema.Struct({\n type: Schema.Literal(\"expect_outbound\"),\n label: Schema.optional(Schema.String),\n frame: Schema.Unknown,\n }),\n Schema.Struct({\n type: Schema.Literal(\"emit_inbound\"),\n label: Schema.optional(Schema.String),\n frame: Schema.Unknown,\n afterMs: Schema.optional(Schema.Number),\n }),\n Schema.Struct({\n type: Schema.Literal(\"runtime_exit\"),\n status: Schema.Literals([\"success\", \"error\", \"cancelled\"]),\n error: Schema.optional(Schema.Unknown),\n }),\n]);\nexport type CodexAppServerReplayEntry = typeof CodexAppServerReplayEntry.Type;\n\nexport const CodexAppServerReplayTranscript = Schema.Struct({\n driver: Schema.Literal(\"codex\"),\n protocol: Schema.Literal(\"codex.app-server\"),\n version: Schema.String,\n scenario: Schema.String,\n metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),\n entries: Schema.Array(CodexAppServerReplayEntry),\n});\nexport type CodexAppServerReplayTranscript = typeof CodexAppServerReplayTranscript.Type;\n\nexport class CodexAppServerReplayJsonParseError extends Schema.TaggedErrorClass()(\n \"CodexAppServerReplayJsonParseError\",\n {\n scenario: Schema.String,\n line: Schema.String,\n cause: Schema.Defect,\n },\n) {\n override get message(): string {\n return `Failed to parse outbound Codex app-server replay frame for scenario ${this.scenario}.`;\n }\n}\n\nexport class CodexAppServerReplayExhaustedError extends Schema.TaggedErrorClass()(\n \"CodexAppServerReplayExhaustedError\",\n {\n scenario: Schema.String,\n cursor: Schema.Number,\n actual: Schema.Unknown,\n },\n) {\n override get message(): string {\n return `Codex app-server replay transcript exhausted before outbound frame ${this.cursor} in scenario ${this.scenario}.`;\n }\n}\n\nexport class CodexAppServerReplayUnexpectedOutboundError extends Schema.TaggedErrorClass()(\n \"CodexAppServerReplayUnexpectedOutboundError\",\n {\n scenario: Schema.String,\n cursor: Schema.Number,\n expectedType: Schema.String,\n actual: Schema.Unknown,\n },\n) {\n override get message(): string {\n return `Unexpected outbound Codex app-server frame at replay cursor ${this.cursor} in scenario ${this.scenario}.`;\n }\n}\n\nexport class CodexAppServerReplayFrameMismatchError extends Schema.TaggedErrorClass()(\n \"CodexAppServerReplayFrameMismatchError\",\n {\n scenario: Schema.String,\n cursor: Schema.Number,\n label: Schema.optional(Schema.String),\n expected: Schema.Unknown,\n actual: Schema.Unknown,\n },\n) {\n override get message(): string {\n return `Outbound Codex app-server frame did not match replay cursor ${this.cursor} in scenario ${this.scenario}.`;\n }\n}\n\nexport class CodexAppServerReplayRuntimeExitError extends Schema.TaggedErrorClass()(\n \"CodexAppServerReplayRuntimeExitError\",\n {\n scenario: Schema.String,\n cursor: Schema.Number,\n status: Schema.Literals([\"error\", \"cancelled\"]),\n error: Schema.optional(Schema.Unknown),\n },\n) {\n override get message(): string {\n return `Codex app-server replay exited with status ${this.status} at cursor ${this.cursor} in scenario ${this.scenario}.`;\n }\n}\n\nexport class CodexAppServerReplayIncompleteError extends Schema.TaggedErrorClass()(\n \"CodexAppServerReplayIncompleteError\",\n {\n scenario: Schema.String,\n cursor: Schema.Number,\n remaining: Schema.Number,\n },\n) {\n override get message(): string {\n return `Codex app-server replay ended with ${this.remaining} unconsumed entries in scenario ${this.scenario}.`;\n }\n}\n\nexport const CodexAppServerReplayError = Schema.Union([\n CodexAppServerReplayJsonParseError,\n CodexAppServerReplayExhaustedError,\n CodexAppServerReplayUnexpectedOutboundError,\n CodexAppServerReplayFrameMismatchError,\n CodexAppServerReplayRuntimeExitError,\n CodexAppServerReplayIncompleteError,\n]);\nexport type CodexAppServerReplayError = typeof CodexAppServerReplayError.Type;\n\nexport interface CodexAppServerReplayState {\n readonly cursor: number;\n readonly failure: CodexAppServerReplayError | null;\n}\n\nexport interface CodexAppServerReplayDriver {\n readonly transcript: CodexAppServerReplayTranscript;\n readonly state: Ref.Ref;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n if (typeof value === \"object\" && value !== null) {\n const record = value as Record;\n return `{${Object.keys(record)\n .toSorted()\n .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)\n .join(\",\")}}`;\n }\n return JSON.stringify(value);\n}\n\nfunction normalizeReplayFrame(value: unknown): unknown {\n if (typeof value !== \"object\" || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map(normalizeReplayFrame);\n }\n\n const record = value as Record;\n const normalized = Object.fromEntries(\n Object.entries(record).map(([key, entry]) => [key, normalizeReplayFrame(entry)]),\n );\n\n if (\n normalized.method === \"initialize\" &&\n typeof normalized.params === \"object\" &&\n normalized.params !== null\n ) {\n const params = normalized.params as Record;\n if (typeof params.clientInfo === \"object\" && params.clientInfo !== null) {\n normalized.params = {\n ...params,\n clientInfo: {\n ...(params.clientInfo as Record),\n version: \"\",\n },\n };\n }\n }\n\n return normalized;\n}\n\nfunction sameFrame(left: unknown, right: unknown): boolean {\n return (\n stableStringify(normalizeReplayFrame(left)) === stableStringify(normalizeReplayFrame(right))\n );\n}\n\nfunction encodeInboundFrame(frame: unknown): Uint8Array {\n return encoder.encode(`${JSON.stringify(frame)}\\n`);\n}\n\nfunction replayTransportError(\n transcript: CodexAppServerReplayTranscript,\n error: CodexAppServerReplayError,\n): CodexError.CodexAppServerTransportError {\n return new CodexError.CodexAppServerTransportError({\n detail: error.message,\n cause: error,\n });\n}\n\nexport function layerReplay(\n transcript: CodexAppServerReplayTranscript,\n): Layer.Layer {\n\n--- replay.test ---\nimport * as Exit from \"effect/Exit\";\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as Schema from \"effect/Schema\";\nimport * as Scope from \"effect/Scope\";\n\nimport { assert, it } from \"@effect/vitest\";\n\nimport * as CodexClient from \"./client.ts\";\nimport * as CodexError from \"./errors.ts\";\nimport * as CodexReplay from \"./replay.ts\";\n\nconst initializeParams = {\n clientInfo: {\n name: \"effect-codex-app-server-test\",\n title: \"Effect Codex App Server Test\",\n version: \"0.0.0\",\n },\n capabilities: {\n experimentalApi: true,\n optOutNotificationMethods: null,\n },\n} as const;\n\nconst initializeResponse = {\n userAgent: \"replay-codex-app-server\",\n codexHome: \"/tmp/codex-home\",\n platformFamily: \"unix\",\n platformOs: \"macos\",\n} as const;\n\nfunction buildContext(transcript: CodexReplay.CodexAppServerReplayTranscript) {\n return Effect.gen(function* () {\n const scope = yield* Scope.make();\n const context = yield* Layer.buildWithScope(CodexReplay.layerReplay(transcript), scope);\n return { context, scope };\n });\n}\n\nit.effect(\"replays Codex app-server frames through the real client protocol\", () =>\n Effect.gen(function* () {\n const { context, scope } = yield* buildContext({\n driver: \"codex\",\n protocol: \"codex.app-server\",\n version: \"test\",\n scenario: \"initialize\",\n entries: [\n {\n type: \"expect_outbound\",\n label: \"initialize\",\n frame: {\n id: 1,\n method: \"initialize\",\n params: {\n ...initializeParams,\n clientInfo: {\n ...initializeParams.clientInfo,\n version: \"older-fixture-version\",\n },\n },\n },\n },\n {\n type: \"emit_inbound\",\n label: \"initialize\",\n frame: {\n id: 1,\n result: initializeResponse,\n },\n },\n {\n type: \"expect_outbound\",\n label: \"initialized\",\n frame: {\n method: \"initialized\",\n },\n },\n {\n type: \"runtime_exit\",\n status: \"success\",\n },\n ],\n });\n\n yield* Effect.gen(function* () {\n const client = yield* CodexClient.CodexAppServerClient;\n assert.deepEqual(yield* client.request(\"initialize\", initializeParams), initializeResponse);\n yield* client.notify(\"initialized\", undefined);\n }).pipe(Effect.provide(context), Effect.ensuring(Scope.close(scope, Exit.void)));\n }),\n);\n\nit.effect(\"fails pending client requests with a schema-serializable replay mismatch\", () =>\n Effect.gen(function* () {\n const { context, scope } = yield* buildContext({\n driver: \"codex\",\n protocol: \"codex.app-server\",\n version: \"test\",\n scenario: \"mismatch\",\n entries: [\n {\n type: \"expect_outbound\",\n label: \"initialize\",\n frame: {\n id: 1,\n method: \"initialize\",\n params: initializeParams,\n },\n },\n {\n type: \"runtime_exit\",\n status: \"success\",\n },\n ],\n });\n\n const error = yield* Effect.gen(function* () {\n const client = yield* CodexClient.CodexAppServerClient;\n return yield* client.request(\"account/read\", {});\n }).pipe(Effect.provide(context), Effect.flip, Effect.ensuring(Scope.close(scope, Exit.void)));\n\n assert.equal(error._tag, \"CodexAppServerTransportError\");\n if (!Schema.is(CodexError.CodexAppServerTransportError)(error)) {\n throw new Error(\"Expected transport error.\");\n }\n\n const replayError = Schema.decodeUnknownSync(CodexReplay.CodexAppServerReplayError)(\n error.cause,\n );\n const encoded = Schema.encodeUnknownSync(CodexReplay.CodexAppServerReplayError)(replayError);\n\n assert.equal(encoded._tag, \"CodexAppServerReplayFrameMismatchError\");\n if (encoded._tag !== \"CodexAppServerReplayFrameMismatchError\") {\n throw new Error(\"Expected frame mismatch error.\");\n }\n assert.equal(encoded.scenario, \"mismatch\");\n assert.equal(encoded.cursor, 0);\n assert.deepEqual(encoded.actual, {\n id: 1,\n method: \"account/read\",\n params: {},\n });\n }),\n);\n","exitCode":0,"durationMs":0},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_yp787C983Bb0Klnj9F8zRBDd","command":"/bin/zsh -lc \"sed -n '1,220p' test/fixtures/codex-app-server-mock-peer.ts && printf '\\\\n--- sample fixture ---\\\\n' && sed -n '1,80p' test/fixtures/codex-app-server-probes/simple.ndjson\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"80213","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"read","command":"sed -n '1,220p' test/fixtures/codex-app-server-mock-peer.ts","name":"codex-app-server-mock-peer.ts","path":"test/fixtures/codex-app-server-mock-peer.ts"},{"type":"read","command":"sed -n '1,80p' test/fixtures/codex-app-server-probes/simple.ndjson","name":"simple.ndjson","path":"test/fixtures/codex-app-server-probes/simple.ndjson"}],"aggregatedOutput":"let nextServerRequestId = 10_000;\nlet pendingSkillsListRequestId: number | string | null = null;\nlet pendingUserInputRequestId: number | null = null;\n\nconst writeMessage = (message: unknown) => {\n process.stdout.write(`${JSON.stringify(message)}\\n`);\n};\n\nconst respond = (id: number | string, result: unknown) => {\n writeMessage({ id, result });\n};\n\nconst respondError = (id: number | string, code: number, message: string) => {\n writeMessage({\n id,\n error: {\n code,\n message,\n },\n });\n};\n\nconst sendRequest = (method: string, params: unknown) => {\n const id = nextServerRequestId++;\n writeMessage({ id, method, params });\n return id;\n};\n\nconst handleMethod = (message: Record) => {\n const method = message.method;\n if (typeof method !== \"string\") {\n return;\n }\n\n switch (method) {\n case \"initialize\": {\n respond(message.id as number | string, {\n userAgent: \"mock-codex-app-server\",\n codexHome: process.cwd(),\n platformFamily: process.platform === \"win32\" ? \"windows\" : \"unix\",\n platformOs: process.platform === \"darwin\" ? \"macos\" : process.platform,\n });\n return;\n }\n case \"initialized\": {\n writeMessage({\n method: \"item/agentMessage/delta\",\n params: {\n delta: \"Mock server is ready.\",\n itemId: \"item-1\",\n threadId: \"thread-1\",\n turnId: \"turn-1\",\n },\n });\n return;\n }\n case \"account/read\": {\n respond(message.id as number | string, {\n account: {\n type: \"chatgpt\",\n email: \"mock@example.com\",\n planType: \"plus\",\n },\n requiresOpenaiAuth: false,\n });\n return;\n }\n case \"skills/list\": {\n pendingSkillsListRequestId = message.id as number | string;\n pendingUserInputRequestId = sendRequest(\"item/tool/requestUserInput\", {\n itemId: \"item-approval-1\",\n threadId: \"thread-1\",\n turnId: \"turn-1\",\n questions: [\n {\n id: \"approved\",\n header: \"Approve\",\n question: \"Continue with the mock skills request?\",\n options: [\n {\n label: \"yes\",\n description: \"Approve the request\",\n },\n ],\n },\n ],\n });\n return;\n }\n default: {\n if (message.id !== undefined) {\n respondError(message.id as number | string, -32601, `Unhandled request: ${method}`);\n }\n }\n }\n};\n\nconst handleResponse = (message: Record) => {\n if (message.id !== pendingUserInputRequestId) {\n return;\n }\n\n pendingUserInputRequestId = null;\n\n respond(pendingSkillsListRequestId!, {\n data: [\n {\n cwd: process.cwd(),\n errors: [],\n skills: [],\n },\n ],\n });\n pendingSkillsListRequestId = null;\n};\n\nlet remainder = \"\";\n\nprocess.stdin.setEncoding(\"utf8\");\nprocess.stdin.on(\"data\", (chunk) => {\n remainder += chunk;\n const lines = remainder.split(\"\\n\");\n remainder = lines.pop() ?? \"\";\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed.length === 0) {\n continue;\n }\n\n const message = JSON.parse(trimmed) as Record;\n if (\"method\" in message) {\n handleMethod(message);\n continue;\n }\n if (\"id\" in message) {\n handleResponse(message);\n }\n }\n});\n\nprocess.stdin.on(\"end\", () => {\n process.exit(0);\n});\n\n--- sample fixture ---\n{\"type\":\"transcript_start\",\"provider\":\"codex\",\"protocol\":\"codex.app-server\",\"version\":\"0.120.0\",\"scenario\":\"simple\",\"metadata\":{\"source\":\"codex-app-server-probe\",\"fileName\":\"simple.ndjson\",\"description\":\"One thread and one turn with a deterministic text-only response.\"}}\n{\"type\":\"expect_outbound\",\"label\":\"initialize\",\"frame\":{\"id\":1,\"method\":\"initialize\",\"params\":{\"capabilities\":{\"experimentalApi\":true,\"optOutNotificationMethods\":null},\"clientInfo\":{\"name\":\"effect-codex-app-server-probe\",\"title\":\"Effect Codex App Server Probe\",\"version\":\"0.0.0\"}}}}\n{\"type\":\"emit_inbound\",\"label\":\"initialize\",\"frame\":{\"id\":1,\"result\":{\"userAgent\":\"effect-codex-app-server-probe/0.120.0 (Mac OS 26.4.1; arm64) dumb (effect-codex-app-server-probe; 0.0.0)\",\"codexHome\":\"/Users/julius/.codex\",\"platformFamily\":\"unix\",\"platformOs\":\"macos\"}}}\n{\"type\":\"expect_outbound\",\"label\":\"initialized\",\"frame\":{\"method\":\"initialized\"}}\n{\"type\":\"expect_outbound\",\"label\":\"thread/start\",\"frame\":{\"id\":2,\"method\":\"thread/start\",\"params\":{}}}\n{\"type\":\"emit_inbound\",\"label\":\"thread/start\",\"frame\":{\"id\":2,\"result\":{\"thread\":{\"id\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"forkedFromId\":null,\"preview\":\"\",\"ephemeral\":false,\"modelProvider\":\"openai\",\"createdAt\":1776725627,\"updatedAt\":1776725627,\"status\":{\"type\":\"idle\"},\"path\":\"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T15-53-47-019dad19-92e1-7460-8c82-034802b2a6cc.jsonl\",\"cwd\":\"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server\",\"cliVersion\":\"0.120.0\",\"source\":\"vscode\",\"agentNickname\":null,\"agentRole\":null,\"gitInfo\":null,\"name\":null,\"turns\":[]},\"model\":\"gpt-5.4\",\"modelProvider\":\"openai\",\"serviceTier\":\"fast\",\"cwd\":\"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server\",\"approvalPolicy\":\"on-request\",\"approvalsReviewer\":\"user\",\"sandbox\":{\"type\":\"workspaceWrite\",\"writableRoots\":[\"/Users/julius/.codex/memories\"],\"readOnlyAccess\":{\"type\":\"fullAccess\"},\"networkAccess\":false,\"excludeTmpdirEnvVar\":false,\"excludeSlashTmp\":false},\"reasoningEffort\":\"xhigh\"}}}\n{\"type\":\"expect_outbound\",\"label\":\"turn/start\",\"frame\":{\"id\":3,\"method\":\"turn/start\",\"params\":{\"input\":[{\"text\":\"Respond with the following text: fixture simple ok\",\"type\":\"text\"}],\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"thread/started\",\"frame\":{\"method\":\"thread/started\",\"params\":{\"thread\":{\"id\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"forkedFromId\":null,\"preview\":\"\",\"ephemeral\":false,\"modelProvider\":\"openai\",\"createdAt\":1776725627,\"updatedAt\":1776725627,\"status\":{\"type\":\"idle\"},\"path\":\"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T15-53-47-019dad19-92e1-7460-8c82-034802b2a6cc.jsonl\",\"cwd\":\"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server\",\"cliVersion\":\"0.120.0\",\"source\":\"vscode\",\"agentNickname\":null,\"agentRole\":null,\"gitInfo\":null,\"name\":null,\"turns\":[]}}}}\n{\"type\":\"emit_inbound\",\"label\":\"deprecationNotice\",\"frame\":{\"method\":\"deprecationNotice\",\"params\":{\"summary\":\"`[features].collab` is deprecated. Use `[features].multi_agent` instead.\",\"details\":\"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details.\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"mcpServer/startupStatus/updated\",\"frame\":{\"method\":\"mcpServer/startupStatus/updated\",\"params\":{\"name\":\"codex_apps\",\"status\":\"starting\",\"error\":null}}}\n{\"type\":\"emit_inbound\",\"label\":\"mcpServer/startupStatus/updated\",\"frame\":{\"method\":\"mcpServer/startupStatus/updated\",\"params\":{\"name\":\"uidotsh\",\"status\":\"starting\",\"error\":null}}}\n{\"type\":\"emit_inbound\",\"label\":\"turn/start\",\"frame\":{\"id\":3,\"result\":{\"turn\":{\"id\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\",\"items\":[],\"status\":\"inProgress\",\"error\":null,\"startedAt\":null,\"completedAt\":null,\"durationMs\":null}}}}\n{\"type\":\"emit_inbound\",\"label\":\"thread/status/changed\",\"frame\":{\"method\":\"thread/status/changed\",\"params\":{\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"status\":{\"type\":\"active\",\"activeFlags\":[]}}}}\n{\"type\":\"emit_inbound\",\"label\":\"turn/started\",\"frame\":{\"method\":\"turn/started\",\"params\":{\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turn\":{\"id\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\",\"items\":[],\"status\":\"inProgress\",\"error\":null,\"startedAt\":1776725627,\"completedAt\":null,\"durationMs\":null}}}}\n{\"type\":\"emit_inbound\",\"label\":\"mcpServer/startupStatus/updated\",\"frame\":{\"method\":\"mcpServer/startupStatus/updated\",\"params\":{\"name\":\"codex_apps\",\"status\":\"ready\",\"error\":null}}}\n{\"type\":\"emit_inbound\",\"label\":\"mcpServer/startupStatus/updated\",\"frame\":{\"method\":\"mcpServer/startupStatus/updated\",\"params\":{\"name\":\"uidotsh\",\"status\":\"ready\",\"error\":null}}}\n{\"type\":\"emit_inbound\",\"label\":\"item/started\",\"frame\":{\"method\":\"item/started\",\"params\":{\"item\":{\"type\":\"userMessage\",\"id\":\"28c8a871-c12e-4d6a-af88-7a53c03b748e\",\"content\":[{\"type\":\"text\",\"text\":\"Respond with the following text: fixture simple ok\",\"text_elements\":[]}]},\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"item/completed\",\"frame\":{\"method\":\"item/completed\",\"params\":{\"item\":{\"type\":\"userMessage\",\"id\":\"28c8a871-c12e-4d6a-af88-7a53c03b748e\",\"content\":[{\"type\":\"text\",\"text\":\"Respond with the following text: fixture simple ok\",\"text_elements\":[]}]},\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"account/rateLimits/updated\",\"frame\":{\"method\":\"account/rateLimits/updated\",\"params\":{\"rateLimits\":{\"limitId\":\"codex\",\"limitName\":null,\"primary\":{\"usedPercent\":0,\"windowDurationMins\":300,\"resetsAt\":1776738279},\"secondary\":{\"usedPercent\":34,\"windowDurationMins\":10080,\"resetsAt\":1776893129},\"credits\":null,\"planType\":\"pro\"}}}}\n{\"type\":\"emit_inbound\",\"label\":\"item/started\",\"frame\":{\"method\":\"item/started\",\"params\":{\"item\":{\"type\":\"reasoning\",\"id\":\"rs_0e6f3b2a309a10b40169e6ae80f20c819bbe4ad96641fe124e\",\"summary\":[],\"content\":[]},\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"item/completed\",\"frame\":{\"method\":\"item/completed\",\"params\":{\"item\":{\"type\":\"reasoning\",\"id\":\"rs_0e6f3b2a309a10b40169e6ae80f20c819bbe4ad96641fe124e\",\"summary\":[],\"content\":[]},\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"item/started\",\"frame\":{\"method\":\"item/started\",\"params\":{\"item\":{\"type\":\"agentMessage\",\"id\":\"msg_0e6f3b2a309a10b40169e6ae814a88819bb76ddc2495f25a5a\",\"text\":\"\",\"phase\":\"final_answer\",\"memoryCitation\":null},\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"item/agentMessage/delta\",\"frame\":{\"method\":\"item/agentMessage/delta\",\"params\":{\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\",\"itemId\":\"msg_0e6f3b2a309a10b40169e6ae814a88819bb76ddc2495f25a5a\",\"delta\":\"fixture\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"item/agentMessage/delta\",\"frame\":{\"method\":\"item/agentMessage/delta\",\"params\":{\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\",\"itemId\":\"msg_0e6f3b2a309a10b40169e6ae814a88819bb76ddc2495f25a5a\",\"delta\":\" simple\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"item/agentMessage/delta\",\"frame\":{\"method\":\"item/agentMessage/delta\",\"params\":{\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\",\"itemId\":\"msg_0e6f3b2a309a10b40169e6ae814a88819bb76ddc2495f25a5a\",\"delta\":\" ok\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"item/completed\",\"frame\":{\"method\":\"item/completed\",\"params\":{\"item\":{\"type\":\"agentMessage\",\"id\":\"msg_0e6f3b2a309a10b40169e6ae814a88819bb76ddc2495f25a5a\",\"text\":\"fixture simple ok\",\"phase\":\"final_answer\",\"memoryCitation\":null},\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\"}}}\n{\"type\":\"emit_inbound\",\"label\":\"thread/tokenUsage/updated\",\"frame\":{\"method\":\"thread/tokenUsage/updated\",\"params\":{\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turnId\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\",\"tokenUsage\":{\"total\":{\"totalTokens\":28263,\"inputTokens\":28223,\"cachedInputTokens\":28032,\"outputTokens\":40,\"reasoningOutputTokens\":31},\"last\":{\"totalTokens\":28263,\"inputTokens\":28223,\"cachedInputTokens\":28032,\"outputTokens\":40,\"reasoningOutputTokens\":31},\"modelContextWindow\":258400}}}}\n{\"type\":\"emit_inbound\",\"label\":\"account/rateLimits/updated\",\"frame\":{\"method\":\"account/rateLimits/updated\",\"params\":{\"rateLimits\":{\"limitId\":\"codex\",\"limitName\":null,\"primary\":{\"usedPercent\":0,\"windowDurationMins\":300,\"resetsAt\":1776738279},\"secondary\":{\"usedPercent\":34,\"windowDurationMins\":10080,\"resetsAt\":1776893129},\"credits\":null,\"planType\":\"pro\"}}}}\n{\"type\":\"emit_inbound\",\"label\":\"thread/status/changed\",\"frame\":{\"method\":\"thread/status/changed\",\"params\":{\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"status\":{\"type\":\"idle\"}}}}\n{\"type\":\"emit_inbound\",\"label\":\"turn/completed\",\"frame\":{\"method\":\"turn/completed\",\"params\":{\"threadId\":\"019dad19-92e1-7460-8c82-034802b2a6cc\",\"turn\":{\"id\":\"019dad19-92ef-7e13-b919-c01c6abf3d0c\",\"items\":[],\"status\":\"completed\",\"error\":null,\"startedAt\":1776725627,\"completedAt\":1776725633,\"durationMs\":5852}}}}\n{\"type\":\"runtime_exit\",\"status\":\"success\"}\n","exitCode":0,"durationMs":0},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0b2955d3a75fd8630169e7fab1e01481999f57bbddc11af4b5","summary":[],"content":[]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0b2955d3a75fd8630169e7fab1e01481999f57bbddc11af4b5","summary":[],"content":[]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","tokenUsage":{"total":{"totalTokens":89827,"inputTokens":89103,"cachedInputTokens":68224,"outputTokens":724,"reasoningOutputTokens":53},"last":{"totalTokens":36243,"inputTokens":36149,"cachedInputTokens":26496,"outputTokens":94,"reasoningOutputTokens":10},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_2fPDBzc8uv95TwAdZy6xNyRd","command":"/bin/zsh -lc \"sed -n '120,360p' src/replay.ts\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"40798","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"read","command":"sed -n '120,360p' src/replay.ts","name":"replay.ts","path":"src/replay.ts"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_2fPDBzc8uv95TwAdZy6xNyRd","command":"/bin/zsh -lc \"sed -n '120,360p' src/replay.ts\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"40798","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"read","command":"sed -n '120,360p' src/replay.ts","name":"replay.ts","path":"src/replay.ts"}],"aggregatedOutput":" remaining: Schema.Number,\n },\n) {\n override get message(): string {\n return `Codex app-server replay ended with ${this.remaining} unconsumed entries in scenario ${this.scenario}.`;\n }\n}\n\nexport const CodexAppServerReplayError = Schema.Union([\n CodexAppServerReplayJsonParseError,\n CodexAppServerReplayExhaustedError,\n CodexAppServerReplayUnexpectedOutboundError,\n CodexAppServerReplayFrameMismatchError,\n CodexAppServerReplayRuntimeExitError,\n CodexAppServerReplayIncompleteError,\n]);\nexport type CodexAppServerReplayError = typeof CodexAppServerReplayError.Type;\n\nexport interface CodexAppServerReplayState {\n readonly cursor: number;\n readonly failure: CodexAppServerReplayError | null;\n}\n\nexport interface CodexAppServerReplayDriver {\n readonly transcript: CodexAppServerReplayTranscript;\n readonly state: Ref.Ref;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n if (typeof value === \"object\" && value !== null) {\n const record = value as Record;\n return `{${Object.keys(record)\n .toSorted()\n .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)\n .join(\",\")}}`;\n }\n return JSON.stringify(value);\n}\n\nfunction normalizeReplayFrame(value: unknown): unknown {\n if (typeof value !== \"object\" || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map(normalizeReplayFrame);\n }\n\n const record = value as Record;\n const normalized = Object.fromEntries(\n Object.entries(record).map(([key, entry]) => [key, normalizeReplayFrame(entry)]),\n );\n\n if (\n normalized.method === \"initialize\" &&\n typeof normalized.params === \"object\" &&\n normalized.params !== null\n ) {\n const params = normalized.params as Record;\n if (typeof params.clientInfo === \"object\" && params.clientInfo !== null) {\n normalized.params = {\n ...params,\n clientInfo: {\n ...(params.clientInfo as Record),\n version: \"\",\n },\n };\n }\n }\n\n return normalized;\n}\n\nfunction sameFrame(left: unknown, right: unknown): boolean {\n return (\n stableStringify(normalizeReplayFrame(left)) === stableStringify(normalizeReplayFrame(right))\n );\n}\n\nfunction encodeInboundFrame(frame: unknown): Uint8Array {\n return encoder.encode(`${JSON.stringify(frame)}\\n`);\n}\n\nfunction replayTransportError(\n transcript: CodexAppServerReplayTranscript,\n error: CodexAppServerReplayError,\n): CodexError.CodexAppServerTransportError {\n return new CodexError.CodexAppServerTransportError({\n detail: error.message,\n cause: error,\n });\n}\n\nexport function layerReplay(\n transcript: CodexAppServerReplayTranscript,\n): Layer.Layer {\n return Layer.effect(CodexAppServerClient, makeReplayClient(transcript));\n}\n\nexport const makeReplayDriver = Effect.fn(\"effect-codex-app-server/replay.makeReplayDriver\")(\n function* (transcript: CodexAppServerReplayTranscript) {\n return {\n transcript,\n state: yield* Ref.make({ cursor: 0, failure: null }),\n } satisfies CodexAppServerReplayDriver;\n },\n);\n\nexport function layerReplayWithDriver(\n driver: CodexAppServerReplayDriver,\n): Layer.Layer {\n return Layer.effect(CodexAppServerClient, makeReplayClientWithState(driver));\n}\n\nexport const makeReplayClient = Effect.fn(\"effect-codex-app-server/replay.makeReplayClient\")(\n function* (\n transcript: CodexAppServerReplayTranscript,\n options: CodexClient.CodexAppServerClientOptions = {},\n ) {\n const driver = yield* makeReplayDriver(transcript);\n return yield* makeReplayClientWithState(driver, options);\n },\n);\n\nconst makeReplayClientWithState = Effect.fn(\n \"effect-codex-app-server/replay.makeReplayClientWithState\",\n)(function* (\n driver: CodexAppServerReplayDriver,\n options: CodexClient.CodexAppServerClientOptions = {},\n) {\n const transcript = driver.transcript;\n const input = yield* Queue.unbounded>();\n const state = driver.state;\n const outboundRemainder = yield* Ref.make(\"\");\n\n const failReplay = (error: CodexAppServerReplayError) =>\n Ref.update(state, (current) => ({\n ...current,\n failure: current.failure ?? error,\n })).pipe(Effect.andThen(Queue.end(input)));\n\n const drainInbound = Effect.fn(\"effect-codex-app-server/replay.drainInbound\")(function* () {\n while (true) {\n const current = yield* Ref.get(state);\n if (current.failure) {\n return;\n }\n\n const entry = transcript.entries[current.cursor];\n if (!entry) {\n return;\n }\n\n if (entry.type === \"emit_inbound\") {\n if (entry.afterMs !== undefined && entry.afterMs > 0) {\n yield* Effect.sleep(Duration.millis(entry.afterMs));\n }\n yield* Queue.offer(input, encodeInboundFrame(entry.frame));\n yield* Ref.update(state, (latest) => ({ ...latest, cursor: latest.cursor + 1 }));\n continue;\n }\n\n if (entry.type === \"runtime_exit\") {\n yield* Ref.update(state, (latest) => ({ ...latest, cursor: latest.cursor + 1 }));\n if (entry.status === \"success\") {\n yield* Queue.end(input);\n return;\n }\n yield* failReplay(\n new CodexAppServerReplayRuntimeExitError({\n scenario: transcript.scenario,\n cursor: current.cursor,\n status: entry.status,\n ...(entry.error === undefined ? {} : { error: entry.error }),\n }),\n );\n return;\n }\n\n return;\n }\n });\n\n const processOutboundFrame = (actual: unknown) =>\n drainInbound().pipe(\n Effect.andThen(\n Effect.gen(function* () {\n const current = yield* Ref.get(state);\n if (current.failure) {\n return;\n }\n\n const entry = transcript.entries[current.cursor];\n if (!entry) {\n yield* failReplay(\n new CodexAppServerReplayExhaustedError({\n scenario: transcript.scenario,\n cursor: current.cursor,\n actual,\n }),\n );\n return;\n }\n\n if (entry.type !== \"expect_outbound\") {\n yield* failReplay(\n new CodexAppServerReplayUnexpectedOutboundError({\n scenario: transcript.scenario,\n cursor: current.cursor,\n expectedType: entry.type,\n actual,\n }),\n );\n return;\n }\n\n if (!sameFrame(entry.frame, actual)) {\n yield* failReplay(\n new CodexAppServerReplayFrameMismatchError({\n scenario: transcript.scenario,\n cursor: current.cursor,\n ...(entry.label === undefined ? {} : { label: entry.label }),\n expected: entry.frame,\n actual,\n }),\n );\n return;\n }\n\n yield* Ref.update(state, (latest) => ({ ...latest, cursor: latest.cursor + 1 }));\n yield* drainInbound();\n }),\n ),\n );\n\n const processOutboundLine = (line: string) => {\n","exitCode":0,"durationMs":0},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","tokenUsage":{"total":{"totalTokens":127847,"inputTokens":127041,"cachedInputTokens":103936,"outputTokens":806,"reasoningOutputTokens":53},"last":{"totalTokens":38020,"inputTokens":37938,"cachedInputTokens":35712,"outputTokens":82,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_WeQ2VXeRTLB3MyVSeGxPdVda","command":"/bin/zsh -lc \"sed -n '360,720p' src/replay.ts\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"73456","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"read","command":"sed -n '360,720p' src/replay.ts","name":"replay.ts","path":"src/replay.ts"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_WeQ2VXeRTLB3MyVSeGxPdVda","command":"/bin/zsh -lc \"sed -n '360,720p' src/replay.ts\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"73456","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"read","command":"sed -n '360,720p' src/replay.ts","name":"replay.ts","path":"src/replay.ts"}],"aggregatedOutput":" const processOutboundLine = (line: string) => {\n const trimmed = line.trim();\n if (trimmed.length === 0) {\n return Effect.void;\n }\n return Effect.try({\n try: () => JSON.parse(trimmed),\n catch: (cause) =>\n new CodexAppServerReplayJsonParseError({\n scenario: transcript.scenario,\n line: trimmed,\n cause,\n }),\n }).pipe(\n Effect.matchEffect({\n onFailure: failReplay,\n onSuccess: processOutboundFrame,\n }),\n );\n };\n\n const processOutboundChunk = (chunk: string | Uint8Array) =>\n Ref.modify(outboundRemainder, (current) => {\n const text = typeof chunk === \"string\" ? chunk : decoder.decode(chunk, { stream: true });\n const combined = current + text;\n const lines = combined.split(\"\\n\");\n const nextRemainder = lines.pop() ?? \"\";\n return [lines.map((line) => line.replace(/\\r$/, \"\")), nextRemainder] as const;\n }).pipe(\n Effect.flatMap((lines) => Effect.forEach(lines, processOutboundLine, { discard: true })),\n );\n\n const terminationError: Effect.Effect = Ref.get(state).pipe(\n Effect.map((current) =>\n current.failure\n ? replayTransportError(transcript, current.failure)\n : new CodexError.CodexAppServerProcessExitedError({ code: 0 }),\n ),\n );\n\n const stdio = Stdio.make({\n args: Effect.succeed([]),\n stdin: Stream.fromQueue(input),\n stdout: () => Sink.forEach(processOutboundChunk),\n stderr: () => Sink.drain,\n });\n\n yield* drainInbound();\n\n return yield* CodexClient.make(stdio, options, terminationError);\n});\n","exitCode":0,"durationMs":0},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0b2955d3a75fd8630169e7fab6175881998551059b68f6c342","summary":[],"content":[]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0b2955d3a75fd8630169e7fab6175881998551059b68f6c342","summary":[],"content":[]},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"plan","id":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","text":""},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"#"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Determ"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"in"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"istic"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Fixtures"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"##"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Summary"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Add"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" first"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-class"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" deterministic"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" support"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" to"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"packages"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"/e"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ffect"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-c"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"od"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ex"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-app"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-server"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" by"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" loading"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" existing"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" ND"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"JSON"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" probe"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" transcripts"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" into"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" the"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" current"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Cod"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ex"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"App"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Server"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Transcript"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" model"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" and"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" validating"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" them"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" through"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" the"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" real"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" client"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"##"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Implementation"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"1"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Add"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" a"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" loader"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" module"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":","}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" likely"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"src"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"/re"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"play"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Fixtures"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".ts"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`.\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"2"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Parse"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" ND"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"JSON"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" fixtures"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" with"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":":\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" -"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" first"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" line"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":":"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"trans"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"cript"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"_start"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" -"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" remaining"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" lines"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":":"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" entries"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" already"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" compatible"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" with"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Cod"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ex"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"App"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Server"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Entry"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"3"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Convert"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" each"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" file"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" into"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Cod"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ex"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"App"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Server"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Transcript"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`.\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"4"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Validate"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" all"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" lines"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" through"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Effect"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Schema"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":","}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" returning"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" schema"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"serial"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"izable"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" errors"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" with"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" file"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" path"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":","}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" line"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" number"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":","}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" scenario"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":","}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" and"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" parse"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"/schema"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" cause"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"5"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Export"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" helpers"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" from"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"package"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".json"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" as"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"./"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"re"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"play"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-fi"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"xtures"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`.\n\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"##"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Public"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" API"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Add"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":":\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"```"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ts"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"load"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"(path"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":":"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" string"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"):"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Effect"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".E"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ffect"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"load"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Fixtures"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"(dir"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":":"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" string"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"):"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Effect"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".E"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ffect"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":","}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Error"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":">\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"``"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`\n\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Add"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" error"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" types"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":":\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"```"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ts"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Read"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Error"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Json"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Parse"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Error"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Schema"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Error"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Empty"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Error"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Missing"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Transcript"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Start"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Error"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"``"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`\n\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"##"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Tests"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Add"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" tests"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" covering"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":":\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"1"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"simple"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".nd"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"json"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" loads"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" into"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" a"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" valid"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" transcript"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"2"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Loaded"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"simple"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".nd"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"json"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" re"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"plays"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" successfully"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" through"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"layer"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`.\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"3"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Mal"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"formed"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" JSON"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" reports"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" file"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" and"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" line"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" number"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"4"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Missing"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"trans"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"cript"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"_start"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" fails"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" determin"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"istically"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"5"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Invalid"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" entry"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" shape"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" fails"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" schema"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" validation"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"6"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" loading"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" preserves"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" entry"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" order"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" exactly"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"##"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Determ"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"in"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ism"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Rules"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"1"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Do"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" not"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" rewrite"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" existing"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" contents"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" during"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"2"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Keep"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" normalization"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" limited"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" to"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" existing"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" comparison"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" logic"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"3"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" loader"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" must"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" not"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" depend"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" on"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" wall"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-clock"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" time"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":","}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" current"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" cwd"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":","}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" network"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":","}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" or"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" process"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" environment"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" except"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" the"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" explicit"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" file"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" path"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" input"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"4"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" order"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" from"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" directory"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" loading"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" should"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" be"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" lex"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ic"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ographic"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" by"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" filename"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"##"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Verification"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"Run"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" before"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" completion"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":":\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"```"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"sh"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"bun"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" fmt"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"bun"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" lint"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"bun"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" type"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"check"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"bun"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" run"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" test"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"``"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`\n\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"##"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Ass"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"um"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ptions"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"1"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Existing"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" `"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"test"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"/"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"fixtures"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"/c"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"od"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"ex"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-app"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-server"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-pro"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"bes"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"/*."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"nd"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"json"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"`"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" files"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" are"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" the"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" canonical"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" initial"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" deterministic"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" corpus"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"2"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" The"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" current"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" replay"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" engine"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" remains"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" responsible"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" for"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" protocol"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-level"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" matching"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"3"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"."}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" Fixture"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" support"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" is"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" package"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"-local"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" and"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" should"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" not"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" introduce"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" runtime"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" logic"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" into"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" contracts"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":"/shared"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":" packages"}}} +{"type":"emit_inbound","label":"item/plan/delta","frame":{"method":"item/plan/delta","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","itemId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","delta":".\n"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"plan","id":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04-plan","text":"# Deterministic Replay Fixtures\n\n## Summary\nAdd first-class deterministic replay fixture support to `packages/effect-codex-app-server` by loading existing NDJSON probe transcripts into the current `CodexAppServerReplayTranscript` model and validating them through the real replay client.\n\n## Implementation\n1. Add a fixture loader module, likely `src/replayFixtures.ts`.\n2. Parse NDJSON fixtures with:\n - first line: `transcript_start`\n - remaining lines: replay entries already compatible with `CodexAppServerReplayEntry`\n3. Convert each file into `CodexAppServerReplayTranscript`.\n4. Validate all lines through Effect Schema, returning schema-serializable errors with file path, line number, scenario, and parse/schema cause.\n5. Export fixture helpers from `package.json` as `./replay-fixtures`.\n\n## Public API\nAdd:\n```ts\nloadReplayFixture(path: string): Effect.Effect\nloadReplayFixtures(dir: string): Effect.Effect, ReplayFixtureError>\n```\n\nAdd error types:\n```ts\nReplayFixtureReadError\nReplayFixtureJsonParseError\nReplayFixtureSchemaError\nReplayFixtureEmptyError\nReplayFixtureMissingTranscriptStartError\n```\n\n## Tests\nAdd tests covering:\n1. `simple.ndjson` loads into a valid transcript.\n2. Loaded `simple.ndjson` replays successfully through `layerReplay`.\n3. Malformed JSON reports file and line number.\n4. Missing `transcript_start` fails deterministically.\n5. Invalid entry shape fails schema validation.\n6. Fixture loading preserves entry order exactly.\n\n## Determinism Rules\n1. Do not rewrite existing fixture contents during replay.\n2. Keep normalization limited to existing replay comparison logic.\n3. Fixture loader must not depend on wall-clock time, current cwd, network, or process environment except the explicit file path input.\n4. Fixture order from directory loading should be lexicographic by filename.\n\n## Verification\nRun before completion:\n```sh\nbun fmt\nbun lint\nbun typecheck\nbun run test\n```\n\n## Assumptions\n1. Existing `test/fixtures/codex-app-server-probes/*.ndjson` files are the canonical initial deterministic fixture corpus.\n2. The current replay engine remains responsible for protocol-level matching.\n3. Fixture support is package-local and should not introduce runtime logic into contracts/shared packages.\n"},"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turnId":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","tokenUsage":{"total":{"totalTokens":166935,"inputTokens":165504,"cachedInputTokens":141696,"outputTokens":1431,"reasoningOutputTokens":159},"last":{"totalTokens":39088,"inputTokens":38463,"cachedInputTokens":37760,"outputTokens":625,"reasoningOutputTokens":106},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019db22b-08cd-7e12-b967-cf6ab32dd408","turn":{"id":"019db22b-08d6-7bb3-9b7d-2ae5b8661a04","items":[],"status":"completed","error":null,"startedAt":1776810658,"completedAt":1776810693,"durationMs":35554}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/cursor_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/cursor_output.ts new file mode 100644 index 00000000000..5b6c3ae2126 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/cursor_output.ts @@ -0,0 +1,39 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertExecutionNodeKinds, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + PROPOSED_PLAN_PROMPT, +} from "../shared.ts"; + +export function assertProposedPlanCursorOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1]); + assertExecutionNodeKinds(projection, ["root_turn", "plan", "subagent", "tool_call"]); + assertTurnItemTypes(projection, ["user_message", "subagent", "file_search", "proposed_plan"]); + assertUserMessagesInclude(projection, [PROPOSED_PLAN_PROMPT]); + + const proposedPlans = projection.plans.filter((plan) => plan.kind === "proposed_plan"); + assert.isAtLeast(proposedPlans.length, 1); + assert.include(proposedPlans.at(-1)?.markdown, "Deterministic Replay Fixtures"); + + const proposedPlanItems = projection.turnItems.filter((item) => item.type === "proposed_plan"); + assert.isAtLeast(proposedPlanItems.length, 1); + assert.equal(proposedPlanItems.at(-1)?.streaming, false); + assert.isAtLeast(projection.subagents.length, 1); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/cursor_transcript.ndjson new file mode 100644 index 00000000000..701111f13ac --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/cursor_transcript.ndjson @@ -0,0 +1,132 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"proposed_plan","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-bc5d59e0-bc8b-441a-9292-b49dfff962d3"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"plan","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":true,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-bc5d59e0-bc8b-441a-9292-b49dfff962d3"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Create a short implementation plan for adding deterministic replay fixtures. Do not ask questions. Present the final plan in a proposed plan block.","options":{"model":{"id":"composer-2.5"},"mode":"plan"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","agentId":"agent-bc5d59e0-bc8b-441a-9292-b49dfff962d3"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":"Exploring the"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" codebase for replay"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":", fixtures, and"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" related"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" patterns"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" to"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" draft a concrete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" plan"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":".\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":8}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":183}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"partial-tool-call","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"partial-tool-call","callId":"tool_c651551c-9935-48e7-b5c4-28bd5ce2909","toolCall":{"type":"task","args":{"description":"Explore replay/fixture patterns","prompt":"Explore this codebase thoroughly to understand:\n1. What is this project (orchestrator, cursor agent SDK, etc.)?\n2. Any existing replay, recording, or fixture infrastructure\n3. Test patterns, mock patterns, deterministic testing approaches\n4. Any references to \"proposed_plan\", \"record\", \"replay\", \"fixture\"\n\nSearch for keywords: replay, fixture, record, deterministic, snapshot, mock, proposed_plan\n\nReturn:\n- Project structure overview\n- Relevant file paths and their roles\n- Current gaps for deterministic replay fixtures\n- Any existing types/interfaces that would need extension\n- Suggested integration points based on actual code\n\nBe specific with file paths and code snippets. Read-only exploration only.","subagentType":{"kind":"explore"},"model":"composer-2.5-fast","agentId":"95b8b4ba-cc91-4455-9389-9729e569f76b","mode":"unspecified"}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-0-yjwv"}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-started","callId":"tool_c651551c-9935-48e7-b5c4-28bd5ce2909","toolCall":{"type":"task","args":{"description":"Explore replay/fixture patterns","prompt":"Explore this codebase thoroughly to understand:\n1. What is this project (orchestrator, cursor agent SDK, etc.)?\n2. Any existing replay, recording, or fixture infrastructure\n3. Test patterns, mock patterns, deterministic testing approaches\n4. Any references to \"proposed_plan\", \"record\", \"replay\", \"fixture\"\n\nSearch for keywords: replay, fixture, record, deterministic, snapshot, mock, proposed_plan\n\nReturn:\n- Project structure overview\n- Relevant file paths and their roles\n- Current gaps for deterministic replay fixtures\n- Any existing types/interfaces that would need extension\n- Suggested integration points based on actual code\n\nBe specific with file paths and code snippets. Read-only exploration only.","subagentType":{"kind":"explore"},"model":"composer-2.5-fast","agentId":"95b8b4ba-cc91-4455-9389-9729e569f76b","mode":"unspecified"}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-0-yjwv"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-started","callId":"tool_acdbbf78-c550-4f16-9311-7e10a3c2d03","toolCall":{"type":"glob","args":{"globPattern":"**/*"}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-0-yjwv"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":11}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-started","callId":"tool_e7511f26-8602-4658-81d3-139a335230c","toolCall":{"type":"grep","args":{"pattern":"replay|fixture|proposed_plan|deterministic","caseInsensitive":true,"offset":0}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-0-yjwv"}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-completed","callId":"tool_e7511f26-8602-4658-81d3-139a335230c","toolCall":{"type":"grep","args":{"pattern":"replay|fixture|proposed_plan|deterministic","caseInsensitive":true,"offset":0},"result":{"status":"success","value":{"workspaceResults":{"/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW":{"type":"content","output":{"matches":[{"file":"./README.md"}],"totalMatches":0}}}}}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-0-yjwv"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":56}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-completed","callId":"tool_acdbbf78-c550-4f16-9311-7e10a3c2d03","toolCall":{"type":"glob","args":{"globPattern":"**/*"},"result":{"status":"success","value":{"files":["./.git/logs/refs/heads/main","./.git/logs/HEAD","./.git/refs/heads/main","./.git/objects/fc/daa658b54f15c125d37542fbc5c39063cc8ae5","./.git/COMMIT_EDITMSG","./README.md","./.git/config","./.git/HEAD","./.git/hooks/push-to-checkout.sample","./.git/hooks/update.sample","./.git/hooks/pre-push.sample","./.git/hooks/pre-applypatch.sample","./.git/hooks/pre-merge-commit.sample","./.git/hooks/post-update.sample","./.git/hooks/prepare-commit-msg.sample","./.git/hooks/pre-receive.sample","./.git/hooks/fsmonitor-watchman.sample","./.git/hooks/applypatch-msg.sample","./.git/hooks/pre-commit.sample","./.git/hooks/sendemail-validate.sample","./.git/hooks/pre-rebase.sample","./.git/hooks/commit-msg.sample","./.git/description","./.git/info/exclude"],"totalFiles":24,"clientTruncated":false,"ripgrepTruncated":false}}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-0-yjwv"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":197}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-completed","callId":"tool_c651551c-9935-48e7-b5c4-28bd5ce2909","toolCall":{"type":"task","args":{"description":"Explore replay/fixture patterns","prompt":"Explore this codebase thoroughly to understand:\n1. What is this project (orchestrator, cursor agent SDK, etc.)?\n2. Any existing replay, recording, or fixture infrastructure\n3. Test patterns, mock patterns, deterministic testing approaches\n4. Any references to \"proposed_plan\", \"record\", \"replay\", \"fixture\"\n\nSearch for keywords: replay, fixture, record, deterministic, snapshot, mock, proposed_plan\n\nReturn:\n- Project structure overview\n- Relevant file paths and their roles\n- Current gaps for deterministic replay fixtures\n- Any existing types/interfaces that would need extension\n- Suggested integration points based on actual code\n\nBe specific with file paths and code snippets. Read-only exploration only.","subagentType":{"kind":"explore"},"model":"composer-2.5-fast","agentId":"95b8b4ba-cc91-4455-9389-9729e569f76b","mode":"unspecified"},"result":{"status":"success","value":{"conversationSteps":[{"assistantMessage":{"text":"I'll explore the codebase structure and search for replay, recording, fixture, and related patterns.\n"}},{"toolCall":{"readToolCall":{"args":{"path":"/tmp/cursor-replay-proposed_plan/README.md"},"result":{"success":{"content":"# cursor-agent-sdk-record-proposed_plan\n","totalLines":2,"fileSize":40,"path":"/tmp/cursor-replay-proposed_plan/README.md","readRange":{"startLine":1,"endLine":2}}}},"toolCallId":"tool_6b944780-b235-43eb-87b1-f1b31ec1df5"}},{"toolCall":{"grepToolCall":{"args":{"pattern":"proposed_plan|replay|fixture|record|deterministic|snapshot|mock","outputMode":"content","caseInsensitive":true,"multiline":false,"toolCallId":"tool_9703f1e1-6268-4d6f-b68f-3832af37dbe","offset":0},"result":{"success":{"pattern":"proposed_plan|replay|fixture|record|deterministic|snapshot|mock","outputMode":"content","workspaceResults":{"/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW":{"content":{"matches":[{"file":"./README.md","matches":[{"lineNumber":1,"content":"# cursor-agent-sdk-record-proposed_plan"}]}],"totalLines":1,"totalMatchedLines":1}}}}}},"toolCallId":"tool_9703f1e1-6268-4d6f-b68f-3832af37dbe"}},{"toolCall":{"globToolCall":{"args":{"globPattern":"**/*"},"result":{"success":{"files":["./.git/logs/refs/heads/main","./.git/logs/HEAD","./.git/refs/heads/main","./.git/objects/fc/daa658b54f15c125d37542fbc5c39063cc8ae5","./.git/COMMIT_EDITMSG","./README.md","./.git/config","./.git/HEAD","./.git/hooks/push-to-checkout.sample","./.git/hooks/update.sample","./.git/hooks/pre-push.sample","./.git/hooks/pre-applypatch.sample","./.git/hooks/pre-merge-commit.sample","./.git/hooks/post-update.sample","./.git/hooks/prepare-commit-msg.sample","./.git/hooks/pre-receive.sample","./.git/hooks/fsmonitor-watchman.sample","./.git/hooks/applypatch-msg.sample","./.git/hooks/pre-commit.sample","./.git/hooks/sendemail-validate.sample","./.git/hooks/pre-rebase.sample","./.git/hooks/commit-msg.sample","./.git/description","./.git/info/exclude"],"totalFiles":24}}},"toolCallId":"tool_2258753f-1e24-4b68-8f55-f5cc37d0df7"}},{"assistantMessage":{"text":"The repo looks minimal; I'll dig into git history and any hidden files for more context.\n"}},{"toolCall":{"readToolCall":{"args":{"path":"/tmp/cursor-replay-proposed_plan/.git/COMMIT_EDITMSG"},"result":{"success":{"content":"initial\n","totalLines":2,"fileSize":8,"path":"/tmp/cursor-replay-proposed_plan/.git/COMMIT_EDITMSG","readRange":{"startLine":1,"endLine":2}}}},"toolCallId":"tool_d7b3c0a2-3478-4188-b075-473d62608b5"}},{"toolCall":{"shellToolCall":{"result":{"permissionDenied":{"command":"git -C \"/tmp/cursor-replay-proposed_plan\" log --oneline -5 && echo \"---\" && git -C \"/tmp/cursor-replay-proposed_plan\" show --stat HEAD","error":"This operation is not allowed in readonly mode. The subagent was launched with readonly: true, which restricts write operations.","isReadonly":true}}},"toolCallId":"tool_1a463236-a1f4-46c9-9858-40ecb97beef"}},{"toolCall":{"globToolCall":{"args":{"targetDirectory":"/tmp/cursor-replay-proposed_plan","globPattern":"**/*.*"},"result":{"success":{"path":"/tmp/cursor-replay-proposed_plan","files":["README.md",".git/hooks/push-to-checkout.sample",".git/hooks/update.sample",".git/hooks/pre-push.sample",".git/hooks/pre-applypatch.sample",".git/hooks/pre-merge-commit.sample",".git/hooks/post-update.sample",".git/hooks/prepare-commit-msg.sample",".git/hooks/pre-receive.sample",".git/hooks/fsmonitor-watchman.sample",".git/hooks/applypatch-msg.sample",".git/hooks/pre-commit.sample",".git/hooks/sendemail-validate.sample",".git/hooks/pre-rebase.sample",".git/hooks/commit-msg.sample"],"totalFiles":15}}},"toolCallId":"tool_8b855f7c-7d59-4558-bbcc-33ed95edbf6"}},{"thinkingMessage":{"text":"The workspace contains only a README.md file with no other files present.","durationMs":741}},{"toolCall":{"grepToolCall":{"result":{"error":{"error":"rg: ../../../../../../Users/julius/.cursor/projects/var-folders-b3-b51-pdxj7dl0t981zpqkxxhr0000gn-T-t3-orchestrator-v2-cursor-agent-sdk-record-proposed-plan-ggwFWW: IO error for operation on ../../../../../../Users/julius/.cursor/projects/var-folders-b3-b51-pdxj7dl0t981zpqkxxhr0000gn-T-t3-orchestrator-v2-cursor-agent-sdk-record-proposed-plan-ggwFWW: No such file or directory (os error 2)\n"}}},"toolCallId":"tool_6483ec94-58b4-4995-948f-7929c5308a6"}},{"toolCall":{"readToolCall":{"args":{"path":"/tmp/cursor-replay-proposed_plan/.git/logs/HEAD"},"result":{"success":{"content":"0000000000000000000000000000000000000000 fcdaa658b54f15c125d37542fbc5c39063cc8ae5 T3 Code Test 1781727514 -0700\tcommit (initial): initial\n","totalLines":2,"fileSize":164,"path":"/tmp/cursor-replay-proposed_plan/.git/logs/HEAD","readRange":{"startLine":1,"endLine":2}}}},"toolCallId":"tool_40fcc6a5-6ed4-41ef-92fa-a7f2636844e"}},{"toolCall":{"globToolCall":{"args":{"targetDirectory":"/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T","globPattern":"**/*"},"result":{"success":{"path":"/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T","files":["t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/logs/refs/heads/main","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/logs/HEAD","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/refs/heads/main","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/objects/fc/daa658b54f15c125d37542fbc5c39063cc8ae5","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/COMMIT_EDITMSG","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/README.md","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/config","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/HEAD","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/push-to-checkout.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/update.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/pre-push.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/pre-applypatch.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/pre-merge-commit.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/post-update.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/prepare-commit-msg.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/pre-receive.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/fsmonitor-watchman.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/applypatch-msg.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/pre-commit.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/sendemail-validate.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/pre-rebase.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/hooks/commit-msg.sample","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/description","t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/.git/info/exclude","CCqK1MQUAekfYNUa51kXl/ssr/1b6ea0b090b16ca68587e35e5691757979c4dbb9","CCqK1MQUAekfYNUa51kXl/ssr/79bb62c3ed83171a3b36a1eb963082420016cb70","CCqK1MQUAekfYNUa51kXl/ssr/0c2da54c3119b606e8c2905c547acca7af45aa39","CCqK1MQUAekfYNUa51kXl/ssr/f20478f9c64f4cca804b8b7f576affd083776c07","CCqK1MQUAekfYNUa51kXl/ssr/108e7a223d191f879a4c8051f311e7b1c0ebfdce","CCqK1MQUAekfYNUa51kXl/ssr/2de7d3205b4ee297e3dfa788676de6c69fb8007e","CCqK1MQUAekfYNUa51kXl/ssr/457946fa6d3ce4d97e71b48f1750bf93f99c06bc","CCqK1MQUAekfYNUa51kXl/ssr/f1520271a08811d17d2c4880b5afdbf7ac9da618","CCqK1MQUAekfYNUa51kXl/ssr/d437b342aa504cc264cd6c5db9cdc521fd6e0821","CCqK1MQUAekfYNUa51kXl/ssr/d047bef17077dca83ce4a251e28c4063c0e538f4","CCqK1MQUAekfYNUa51kXl/ssr/f3a9e0af27dd4b49f4165fbaf27662c271048005","CCqK1MQUAekfYNUa51kXl/ssr/83a2d7c41f465a99bfd7c736de2459cd83029320","CCqK1MQUAekfYNUa51kXl/ssr/e736de55c464a13c9c14854dccbcf4bbb57a4b7f","CCqK1MQUAekfYNUa51kXl/ssr/0b7c9ad62ad69946d442ba006c8980f0cd629483","CCqK1MQUAekfYNUa51kXl/ssr/4d2e48b21fd3e56cf2c131c12b7128d5e313260e","CCqK1MQUAekfYNUa51kXl/ssr/880431d601e71a19cb978e237d4b2b592fd72bc7","CCqK1MQUAekfYNUa51kXl/ssr/ab5dcf7ac7bd5b000cefacc297199d4af8fd245a","CCqK1MQUAekfYNUa51kXl/ssr/b60425009385208a49a7cefb90e43ae3aef5dd82","CCqK1MQUAekfYNUa51kXl/ssr/440915f8846468272bb1dafcd23646f0365cb3f5","CCqK1MQUAekfYNUa51kXl/ssr/b2e4b867420b48a4c6f59a9b1ae3c42137d201a7","CCqK1MQUAekfYNUa51kXl/ssr/5f9efd0f68ba7680aa015462154ed5e43d055cc9","CCqK1MQUAekfYNUa51kXl/ssr/b222b9ce36afa608b234f80fd072710735675e3e","CCqK1MQUAekfYNUa51kXl/ssr/132a433c77c4569a5fd8934ec346367cb8c08fdd","CCqK1MQUAekfYNUa51kXl/ssr/595a7f12f587232e6859c0418f1d8dc252d146cf","CCqK1MQUAekfYNUa51kXl/ssr/e82e8b073eed72f7e57650dd089959ec97a47613","CCqK1MQUAekfYNUa51kXl/ssr/d6ca2de59e50d4c6c10f5f1bcd223bdce815e837","CCqK1MQUAekfYNUa51kXl/ssr/589a2245dfe67b5e830d21e12c50fc2c8baf5aee","CCqK1MQUAekfYNUa51kXl/ssr/117216c8617289ba3d5d6dfdf1ac301e9faf58f2","CCqK1MQUAekfYNUa51kXl/ssr/e5a8a7f1e54634c961d1f2188fe1f70709215ff2","CCqK1MQUAekfYNUa51kXl/ssr/ab4e9c0c155519fc53e566fab2965635c528137b","CCqK1MQUAekfYNUa51kXl/ssr/bb63df4de73c4e5f82faec56a3ab08ec66ffbf69","CCqK1MQUAekfYNUa51kXl/ssr/d1b6e51017eb1883023eebce64ac14e6f0d82b39","CCqK1MQUAekfYNUa51kXl/ssr/bd4a74d492060a9dbdc8f631170721ec5b72156b","CCqK1MQUAekfYNUa51kXl/ssr/b5f664a38161e78eaee69fef923422f6816b958f","CCqK1MQUAekfYNUa51kXl/ssr/4d66c4e0b61ac9d511e9be435abacbb831477bd2","CCqK1MQUAekfYNUa51kXl/ssr/22858e0edba23ff7b4e99e0bb854c1e6eb5d67e5","CCqK1MQUAekfYNUa51kXl/ssr/57937a82893c775976f44de35796090eefc3fd8f","CCqK1MQUAekfYNUa51kXl/ssr/7dd483e15d4a4afef1e8aae204761a909ae1bc43","CCqK1MQUAekfYNUa51kXl/ssr/bc69a12ac750e779e589e094f49e77031f53f05a","CCqK1MQUAekfYNUa51kXl/ssr/8432da594202b10f98b51ec2c1112963d7f705c5","CCqK1MQUAekfYNUa51kXl/ssr/ca1b9e023995c6fcb85084759a964a0062faf1a9","CCqK1MQUAekfYNUa51kXl/ssr/f34155c05d008fce01a6cf1750e9535f89c308b3","CCqK1MQUAekfYNUa51kXl/ssr/970972bec3921402c7785a22824f2c0198efc88f","CCqK1MQUAekfYNUa51kXl/ssr/08c41285ab65807254c5ea4923cf741b173a96b4","CCqK1MQUAekfYNUa51kXl/ssr/e675afb01ffc8b54f5863d95ae38c908d884912b","CCqK1MQUAekfYNUa51kXl/ssr/3062def690e5fe2976775c2d28642b6d0d5507ef","CCqK1MQUAekfYNUa51kXl/ssr/e735fe699fdf6dedab877dd1ee8036991c50d3e9","CCqK1MQUAekfYNUa51kXl/ssr/14be6fc6943f81092358c3f62356826467e8e52d","CCqK1MQUAekfYNUa51kXl/ssr/6c654a0e59eb3bc4c1b967f2318eb33edcb2ab81","CCqK1MQUAekfYNUa51kXl/ssr/1b7ad70ecb70da350c3de11228ba9ed61cc4151d","CCqK1MQUAekfYNUa51kXl/ssr/8242d1067cc0ec7384bbd2ae0a4aedf811a3a3b7","CCqK1MQUAekfYNUa51kXl/ssr/8ce9c3448eb1f4932b032e0ca15d292fc4e3bd9a","CCqK1MQUAekfYNUa51kXl/ssr/b73faaf68ad485afad44313585b03fc354f06423","CCqK1MQUAekfYNUa51kXl/ssr/45accde829b4cdcbe11fd7d765787a2b3b9a8011","CCqK1MQUAekfYNUa51kXl/ssr/61f658e66a7d263036ad5585d74a5571f60bf17e","CCqK1MQUAekfYNUa51kXl/ssr/6e9748916c3cf1c002e477c02ecbae3a2eab474d","t3code-git-manager-SKlXJL/README.md","t3code-git-manager-SKlXJL/.git/config","t3code-git-manager-SKlXJL/.git/HEAD","t3code-git-manager-SKlXJL/.git/hooks/push-to-checkout.sample","t3code-git-manager-SKlXJL/.git/hooks/update.sample","t3code-git-manager-SKlXJL/.git/hooks/pre-push.sample","t3code-git-manager-SKlXJL/.git/hooks/pre-applypatch.sample","t3code-git-manager-SKlXJL/.git/hooks/pre-merge-commit.sample","t3code-git-manager-SKlXJL/.git/hooks/post-update.sample","t3code-git-manager-SKlXJL/.git/hooks/prepare-commit-msg.sample","t3code-git-manager-SKlXJL/.git/hooks/pre-receive.sample","t3code-git-manager-SKlXJL/.git/hooks/fsmonitor-watchman.sample","t3code-git-manager-SKlXJL/.git/hooks/applypatch-msg.sample","t3code-git-manager-SKlXJL/.git/hooks/pre-commit.sample","t3code-git-manager-SKlXJL/.git/hooks/sendemail-validate.sample","t3code-git-manager-SKlXJL/.git/hooks/pre-rebase.sample","t3code-git-manager-SKlXJL/.git/hooks/commit-msg.sample","t3code-git-manager-SKlXJL/.git/description","t3code-git-manager-SKlXJL/.git/info/exclude","Sv70zJ_VefJwjnnkCDctC/ssr/4e8947ba6e8c68b58749bcf9dc89d6b388dbfda2","Sv70zJ_VefJwjnnkCDctC/ssr/03512668e6e78af858678dc39f94b4ebfd9a1636","Sv70zJ_VefJwjnnkCDctC/ssr/6bec7b7400c14152f326930c9f12e90765694bf6","Sv70zJ_VefJwjnnkCDctC/ssr/8a44f7672da6886d5d4c779e40a57c69e21f3700","Sv70zJ_VefJwjnnkCDctC/ssr/6170d80995eb5bf2450aa08c44d6034de5d7a6fa","Sv70zJ_VefJwjnnkCDctC/ssr/2af0d4320498e93b1fd64a2bb25dc03500380209","Sv70zJ_VefJwjnnkCDctC/ssr/105aedadccbab6c1a6220826b051cf6e22a394f5","Sv70zJ_VefJwjnnkCDctC/ssr/a419dc28f487a0ba1a715f2807e517597cb4fd14","Sv70zJ_VefJwjnnkCDctC/ssr/7c0a017158f766e8a561c3153c97e0248bd7c88e","Sv70zJ_VefJwjnnkCDctC/ssr/3bc130f687ccea4b9889ab5a7defd7161b03df9f","Sv70zJ_VefJwjnnkCDctC/ssr/c3038c415b2ee8e907ee87ed638e3646b9e6473e","Sv70zJ_VefJwjnnkCDctC/ssr/6ab12736b2568e78e4c8c91eac42fb1fcce6e963","Sv70zJ_VefJwjnnkCDctC/ssr/91f9f66076592f19082d40e122566775834e4f72","Sv70zJ_VefJwjnnkCDctC/ssr/c0d5abd5f318459829f34025ccf9b24cad610c73","Sv70zJ_VefJwjnnkCDctC/ssr/f2aabcdba3747cebeb6e6dc99aa0b719a5baea7f","Sv70zJ_VefJwjnnkCDctC/ssr/568a349648f6ea825a685f7ab04aace2bce709ea","Sv70zJ_VefJwjnnkCDctC/ssr/ef6de42efc4ca71ed7bb5c04f3c6264257ea8723","Sv70zJ_VefJwjnnkCDctC/ssr/9b37ab18399be392930f2d2d3b93efd7e9f808b8","Sv70zJ_VefJwjnnkCDctC/ssr/fb37d378e9a3db2548e86d2bbbccde9e95187d38","Sv70zJ_VefJwjnnkCDctC/ssr/2781fbcf333100abf170395a4bee351395a19a02","Sv70zJ_VefJwjnnkCDctC/ssr/af7e964c307af042ae151df647376565fada4fa5","Sv70zJ_VefJwjnnkCDctC/ssr/0a847a5b9eea2b489f04be234114b569dc91dc0f","Sv70zJ_VefJwjnnkCDctC/ssr/924cf181258915d6beb4dcc76cca729818f170ea","Sv70zJ_VefJwjnnkCDctC/ssr/a8e7395fe8e3d1c7b0182175274ff19af4dece24","Sv70zJ_VefJwjnnkCDctC/ssr/1d65f2e89b8107d7c0ab9ff7e6dcea1627ffa2a2","Sv70zJ_VefJwjnnkCDctC/ssr/04d71a2e0660948234564261fcbfe69fae8eee5a","Sv70zJ_VefJwjnnkCDctC/ssr/869435fe5e1ec79357698c530a40319ef5f70812","Sv70zJ_VefJwjnnkCDctC/ssr/99e8e0f481e7e0b86ec88bb0146c16634fc326bb","Sv70zJ_VefJwjnnkCDctC/ssr/b29b5b1a1d9b44ee5ec5c3fdcf7ea8e1e06d827b","Sv70zJ_VefJwjnnkCDctC/ssr/73b5f379e83c6d6f8a620bbd6bbb051c98cce470","Sv70zJ_VefJwjnnkCDctC/ssr/67e8ebb3b24ed6cd07652522b9a48d2dbc9effd9","Sv70zJ_VefJwjnnkCDctC/ssr/e0d855cd53346d51344d4e9b1aa653582598a289","Sv70zJ_VefJwjnnkCDctC/ssr/66bfafe9668a4d37928fc59445af54ecd42a0d7f","Sv70zJ_VefJwjnnkCDctC/ssr/38f2ff294448561a12240bce7bdd9dfd16b914ad","Sv70zJ_VefJwjnnkCDctC/ssr/9a7cb78cb97573a8ef99ba178c76fc6baa4816bf","Sv70zJ_VefJwjnnkCDctC/ssr/845b214c955416e370dfa851c8ab46e8709bd51f","Sv70zJ_VefJwjnnkCDctC/ssr/34090bc2870a8ede0205408322b27efa569b96b8","Sv70zJ_VefJwjnnkCDctC/ssr/76dffe916b65cb9397c31ae8f513dc664faa5328","Sv70zJ_VefJwjnnkCDctC/ssr/0aad60b4bd76a5d0cd6bb975bad327c3999f206b","Sv70zJ_VefJwjnnkCDctC/ssr/604048c2336981e74492550ed0f454ee40b5c8f0","Sv70zJ_VefJwjnnkCDctC/ssr/43de3cc0230dd481864fe855998b3e40f3c2fccd","Sv70zJ_VefJwjnnkCDctC/ssr/dc11b24e72820253f410319237f6c1c7d3d36a91","Sv70zJ_VefJwjnnkCDctC/ssr/b5ac96e942e73e3e012c359c83da2f3bfe64794f","Sv70zJ_VefJwjnnkCDctC/ssr/cb79d2fc362b7924c747404a2ebd2e9fb6c4f6af","Sv70zJ_VefJwjnnkCDctC/ssr/947a07dab01d2a431253095aea1c68661be705d4","Sv70zJ_VefJwjnnkCDctC/ssr/c6f320061bb2cccfd10e681ff2080a971f671d63","Sv70zJ_VefJwjnnkCDctC/ssr/bb5037638f3321c5ee72472b6ee61f4a7c241d5d","Sv70zJ_VefJwjnnkCDctC/ssr/bf54ab6d6fd67b848aa9f30c43ab1c56d794e148","Sv70zJ_VefJwjnnkCDctC/ssr/807fedcadca300a7d914de715be8c9b00e596c38","Sv70zJ_VefJwjnnkCDctC/ssr/7e2ac7117f27ee9bdb03e47f13159476ca1ca545","Sv70zJ_VefJwjnnkCDctC/ssr/aad96267a6612b94322a105105992df1bc0fa7d3","Sv70zJ_VefJwjnnkCDctC/ssr/8161aba1340f9a1495f03d2373abb9e77b791c7b","Sv70zJ_VefJwjnnkCDctC/ssr/ecf71a5d3255bac0fffab581b93ca2932c3644ac","Sv70zJ_VefJwjnnkCDctC/ssr/07410e3be2a60c549f8942ea734ecb7f013e23f6","Sv70zJ_VefJwjnnkCDctC/ssr/10e7b0808d63aeb799c1d91548159cf08f632cad","Sv70zJ_VefJwjnnkCDctC/ssr/050803c975e5c3d0ec842523bdb2e28520faf721","Sv70zJ_VefJwjnnkCDctC/ssr/9deeea88026e431b03d5d4030484a59e3ada96e5","Sv70zJ_VefJwjnnkCDctC/ssr/ac132509ddf5c65f579b5e623bcdf7594d75395c","Sv70zJ_VefJwjnnkCDctC/ssr/2976a59021fcc289405d177183422f0618d603bd","Sv70zJ_VefJwjnnkCDctC/ssr/ad2cd083c434b347d0069c3c506a217f521e4196","Sv70zJ_VefJwjnnkCDctC/ssr/679dbb0643fb081c8343e6abd36984e510b09261","Sv70zJ_VefJwjnnkCDctC/ssr/46466b0d07ebc7b40ccfdfe55d13dd0537ba704d","Sv70zJ_VefJwjnnkCDctC/ssr/dded9812fd2e21a0928170fbee3d0448d387d97d","Sv70zJ_VefJwjnnkCDctC/ssr/8131fc571317c963c3cc61e95bfb5ff5b520170b","Sv70zJ_VefJwjnnkCDctC/ssr/4695aeaedcb667ba0232f17ce1f40eb268219742","Sv70zJ_VefJwjnnkCDctC/ssr/658e9a921ebf496fca62e5156ba25dcd6be72ea4","Sv70zJ_VefJwjnnkCDctC/ssr/7518e120614ce4a212333b60c74054dfd17d54d8","Sv70zJ_VefJwjnnkCDctC/ssr/47f8fb2701a4c395716a64e5b2cbdd72ce965a9e","Sv70zJ_VefJwjnnkCDctC/ssr/3e61c25af65ba0676189d882183712746f32bd6b","Sv70zJ_VefJwjnnkCDctC/ssr/bb1194a208db95b464003f0b0e5d5c018a743d3a","Sv70zJ_VefJwjnnkCDctC/ssr/39f2b2496a50b8ca65f29b911935458a93a57db8","Sv70zJ_VefJwjnnkCDctC/ssr/6d0093e0e7c63bfbf24d50739c0ac90d94ba9847","Sv70zJ_VefJwjnnkCDctC/ssr/e4957b5198cb7f41cec1b2aedf09649a60dd6e4a","Sv70zJ_VefJwjnnkCDctC/ssr/bcb868f22f818c5254a289e9c92bc08fefc14ab5","Sv70zJ_VefJwjnnkCDctC/ssr/42e9a1d6105278e1340f8cfc335c9d607c6862fd","Sv70zJ_VefJwjnnkCDctC/ssr/024eef6cba335cd547a54ebdb5b4cfdf4fe05ee5","Sv70zJ_VefJwjnnkCDctC/ssr/95a967c3c8f62f9f3cab195d4299233dbb6d17db","Sv70zJ_VefJwjnnkCDctC/ssr/905fd6ad08354e303f1196aaf38a2620648a7309","Sv70zJ_VefJwjnnkCDctC/ssr/84050534eacbc50aaec19fe12fbbc883180e5122","Sv70zJ_VefJwjnnkCDctC/ssr/2cf3599409190ff6e1b334e211e166ce5d3d2d0a","Sv70zJ_VefJwjnnkCDctC/ssr/3c4fef3cf4f7a1481992dec6aa61025255a4c5b2","Sv70zJ_VefJwjnnkCDctC/ssr/53eb8909614c5d8e7453c5c78531894062722d6c","Sv70zJ_VefJwjnnkCDctC/ssr/bfb9910fe809ecd0ab264e1fc7001642e009c133","Sv70zJ_VefJwjnnkCDctC/ssr/16df9be6fe74dd6cacc1d884c1538b6af35c4a1d","Sv70zJ_VefJwjnnkCDctC/ssr/bbe2983d3c312d474d463a92a4359c611452b730","Sv70zJ_VefJwjnnkCDctC/ssr/e7a92a185a34b19bda41eba0ad256e370ea10c59","Sv70zJ_VefJwjnnkCDctC/ssr/58f558f67ce1473475dee602195610641bba1a62","Sv70zJ_VefJwjnnkCDctC/ssr/4329f1b90a043569cc157412bb5fb42300c4d540","Sv70zJ_VefJwjnnkCDctC/ssr/f65f1afed8f26237c2aa2bbfd5b9470f58e3a712","Sv70zJ_VefJwjnnkCDctC/ssr/761ec34282c325d5706f27011bf00fa4975284e9","Sv70zJ_VefJwjnnkCDctC/ssr/2044def5ddd39b7602ef276ccc150a5e8fa711d7","Sv70zJ_VefJwjnnkCDctC/ssr/1292293d36ee2bb22a34924c518920df546e7fed","Sv70zJ_VefJwjnnkCDctC/ssr/15b07e765340c56f231a1bef27f039ba6a79f278","Sv70zJ_VefJwjnnkCDctC/ssr/f58e7e5e21adb0067ee56a42ab214ceb2c7b3a8a","Sv70zJ_VefJwjnnkCDctC/ssr/d13e06a3757d19e7e4837c828a614ce83daf9c38","Sv70zJ_VefJwjnnkCDctC/ssr/d46483bd29468b1cc14e4ba5e326254c6b966668","Sv70zJ_VefJwjnnkCDctC/ssr/8b567e0d6a0328a0c247114c5813fbb69ccdc970","Sv70zJ_VefJwjnnkCDctC/ssr/9712582533cbc72df7ea4f0be827de0bf4021854","Sv70zJ_VefJwjnnkCDctC/ssr/2ef7301f6f8e541e046e393e338fba360e46477d","Sv70zJ_VefJwjnnkCDctC/ssr/234453211a3f5552a0460ff23201f9295824b84c","Sv70zJ_VefJwjnnkCDctC/ssr/aeed46aa94878a41de42b018571d9b1ecbe69580","Sv70zJ_VefJwjnnkCDctC/ssr/7b54aa2700d8346b4bbe1256e129c0deef5414ba","Sv70zJ_VefJwjnnkCDctC/ssr/45b6672c00a36b755554b1d29abd4cec548e4ca8","Sv70zJ_VefJwjnnkCDctC/ssr/d295cf3b93be748ee79877c07e1f48de909ecb91","Sv70zJ_VefJwjnnkCDctC/ssr/ac0e5be012551140c406a76787fadaae319cd863","Sv70zJ_VefJwjnnkCDctC/ssr/248e5748e11178cb2eb7f7d1627ab8eeb239011e","Sv70zJ_VefJwjnnkCDctC/ssr/7edd5b85415c10828833f76d43460ea4de8fb652","Sv70zJ_VefJwjnnkCDctC/ssr/bebfeca96c099e1a25c229e61f5dc3d4c4e3c173","Sv70zJ_VefJwjnnkCDctC/ssr/5dabfb3632bce2e970dca32dd35b14b53fdf7d6e","Sv70zJ_VefJwjnnkCDctC/ssr/a07d4f8d6214fb049f440daa96f99f647c834a64","Sv70zJ_VefJwjnnkCDctC/ssr/f0cd0d1b65dc92c7bed4e0d24b9fdd202d3bb0f2","Sv70zJ_VefJwjnnkCDctC/ssr/21c434d1616216c1c252da8cf7157dc5d9dba585","Sv70zJ_VefJwjnnkCDctC/ssr/88e2c1ffa1689a369c8125fdc52653c3de49553a","Sv70zJ_VefJwjnnkCDctC/ssr/dcabbc69a49bfcfe87a35c29db0e9b9508e2d737","Sv70zJ_VefJwjnnkCDctC/ssr/1a665cdeb85601518de641e9cfda350a4a4b2cba","Sv70zJ_VefJwjnnkCDctC/ssr/8d65c08f8445cfa1b7259bbdbc93d553d9f28959","Sv70zJ_VefJwjnnkCDctC/ssr/179a276c9157b91392bda5540d11abfae3515e9c","Sv70zJ_VefJwjnnkCDctC/ssr/e06f83ec6b5c8314a46cfb08a9803ed9dc37ed48","Sv70zJ_VefJwjnnkCDctC/ssr/db079c9fd366c30990dcedeb7c88cf28bc316818","Sv70zJ_VefJwjnnkCDctC/ssr/0b64023b4d1bed0f1f63f8a93ca546e8a9650e42","Sv70zJ_VefJwjnnkCDctC/ssr/c0feee60cbb354fca689602e17e27212509952fe","Sv70zJ_VefJwjnnkCDctC/ssr/9cdf98ed1585642c86efcf9121fe1a31a64482e1","Sv70zJ_VefJwjnnkCDctC/ssr/d7c340049aa7f8e97c14070fe52e62933ea7cff0","Sv70zJ_VefJwjnnkCDctC/ssr/b52112ed80c2d4ba2bdf2795387205719404470b","Sv70zJ_VefJwjnnkCDctC/ssr/fdca1191087ab6421b7b9075726e38683a67e680","Sv70zJ_VefJwjnnkCDctC/ssr/78dab201f4b26f9b3aa2d038bb8dcf749084fb36","Sv70zJ_VefJwjnnkCDctC/ssr/0a62aecdc01fbbebfb7da14f3936bd9ba99c3d5a","Sv70zJ_VefJwjnnkCDctC/ssr/91dd671fccccb8ebc483c22ce477d732762d7a50","Sv70zJ_VefJwjnnkCDctC/ssr/cd2781184fd12c967eb571b85d56cd69642c0f7b","Sv70zJ_VefJwjnnkCDctC/ssr/507f00bc80b5f9b08b01407a2b9db910a736a458","Sv70zJ_VefJwjnnkCDctC/ssr/bb1929c1030cf1800eb2fc409bf8f8edd4b7ff60","Sv70zJ_VefJwjnnkCDctC/ssr/82224c0f6fa407944c819118a2b9b9ccd54b98d5","Sv70zJ_VefJwjnnkCDctC/ssr/4230cbf5f9e3b638ce57eaa7d8820ed43588cfde","Sv70zJ_VefJwjnnkCDctC/ssr/a5e3d77f7bf363d10d3c5f856b01b83ea4cd480d","Sv70zJ_VefJwjnnkCDctC/ssr/e62038f7b31af248a683a27c507974783791afd0","Sv70zJ_VefJwjnnkCDctC/ssr/534b7c1fe05e6733a2b0723050177a5b3a94dd20","Sv70zJ_VefJwjnnkCDctC/ssr/c6338f5cbcb4476872c42c16e4a951e2b00fbc37","Sv70zJ_VefJwjnnkCDctC/ssr/93e2bb9dd21dae3cbb43930bb27c4229af9a7470","Sv70zJ_VefJwjnnkCDctC/ssr/e6abf249b2708845ac6f6c15e407822dae517223","Sv70zJ_VefJwjnnkCDctC/ssr/c8208873297ab5ec63a27f6c5d058313a402bbe0","Sv70zJ_VefJwjnnkCDctC/ssr/9bcbe270ac22d3fa0a6cfd0defe9b9124f016d3f","Sv70zJ_VefJwjnnkCDctC/ssr/d478d4f1ebfbb5f487b14f9c68fa6445f969e1df","Sv70zJ_VefJwjnnkCDctC/ssr/c636629172b3ea1502146817f54a5da25ebcf337","Sv70zJ_VefJwjnnkCDctC/ssr/5cd925dd4034fc0a614c10d410631072f255f173","Sv70zJ_VefJwjnnkCDctC/ssr/86316717a491a8f273f5182d5ccbaee6623de4da","Sv70zJ_VefJwjnnkCDctC/ssr/25257d03950239399a7ceafdd780caafe6129a7c","Sv70zJ_VefJwjnnkCDctC/ssr/31c276668d4592b51d2c9e711c6c8292de1503de","Sv70zJ_VefJwjnnkCDctC/ssr/7dd031c2de5f39ae5cdf78bd1b233660cfe747bd","Sv70zJ_VefJwjnnkCDctC/ssr/62ee5cc18542a9a90f4aefe86c71e7ece1a46200","Sv70zJ_VefJwjnnkCDctC/ssr/69f67decdee21d1932b9f5e53748991232675b40","Sv70zJ_VefJwjnnkCDctC/ssr/52ecc183b63d00e54e83752ca85d8627290f46e5","Sv70zJ_VefJwjnnkCDctC/ssr/100f3d5f7d9b2616380643ce62541454d858d97b","Sv70zJ_VefJwjnnkCDctC/ssr/cca1841223796e51d33b39529dabb09734bfe60b","Sv70zJ_VefJwjnnkCDctC/ssr/8e12da0adce85220790ff575de972c48d5258beb","Sv70zJ_VefJwjnnkCDctC/ssr/2d4d2626746953deac0b15dbf8b698fbb21ca373","Sv70zJ_VefJwjnnkCDctC/ssr/b74a42a7d88110d59b3599b917bef8dcd1faa014","Sv70zJ_VefJwjnnkCDctC/ssr/15c04ddc4b71b3171a9968af32a09a3b1b9cbc1d","Sv70zJ_VefJwjnnkCDctC/ssr/2c0885e21bf350e47164e280362c073f8451f471","Sv70zJ_VefJwjnnkCDctC/ssr/e41d57f867d537f1ab21a5aa82819e382e857071","Sv70zJ_VefJwjnnkCDctC/ssr/a36c28f367c6ec6010eff776865da0f1c9a680ce","Sv70zJ_VefJwjnnkCDctC/ssr/b649a44bbfddef9eccda289930b8843b10d8e05c","Sv70zJ_VefJwjnnkCDctC/ssr/231d6cd2e8e9d2d5058643dc561ea67c887c83e9","Sv70zJ_VefJwjnnkCDctC/ssr/44ad3ea2a0c5f8ee15586ec584a5c3db6964ab1d","Sv70zJ_VefJwjnnkCDctC/ssr/c5a104b61863ade78ec2ddca2bfbb681bd4f2042","Sv70zJ_VefJwjnnkCDctC/ssr/fa4b97c694bc17346cf3bd27368b061dfc9da048","Sv70zJ_VefJwjnnkCDctC/ssr/9ba96505ccde7eeae7de2d9a13c2bf51ed82b6e3","Sv70zJ_VefJwjnnkCDctC/ssr/95e7ebf9f4b4557446373108f84a594771e20af5","Sv70zJ_VefJwjnnkCDctC/ssr/c5bb4f29113c05568f114a5a22da36324ff29b50","Sv70zJ_VefJwjnnkCDctC/ssr/ec966d218efabe3d5c6699ef6d4bf644da5056f1","Sv70zJ_VefJwjnnkCDctC/ssr/d36974c37297ed382699c6a2f3f4a03750060098","Sv70zJ_VefJwjnnkCDctC/ssr/c5180db9e5bf973acb250efacc01222345eb4b66","Sv70zJ_VefJwjnnkCDctC/ssr/6886a7685f171cf978e665404f51114250ecf13f","Sv70zJ_VefJwjnnkCDctC/ssr/32f61621a028cde3fb6f4a918512932d1b115ac0","Sv70zJ_VefJwjnnkCDctC/ssr/5bd554840430b34e5df04093aac733ade8e577e0","Sv70zJ_VefJwjnnkCDctC/ssr/5100ff82f79b4abb91fb4234e9b12c774804ca5f","Sv70zJ_VefJwjnnkCDctC/ssr/9462ccb273484faf6bbde34e124c406b1a1bdca8","Sv70zJ_VefJwjnnkCDctC/ssr/775ec3abe151bf53f926fad8e65f6141da32062c","Sv70zJ_VefJwjnnkCDctC/ssr/d57130c2eeb6e9188dadb60541149957ac608485","Sv70zJ_VefJwjnnkCDctC/ssr/c640488eec74e9acafcc01c25df8e12c97beb905","Sv70zJ_VefJwjnnkCDctC/ssr/a8f78580bb9b75ece06c5f9eaabe83bca684e4cc","Sv70zJ_VefJwjnnkCDctC/ssr/969ab7b73ea371b44b63d7bb3aefef749800e4b5","Sv70zJ_VefJwjnnkCDctC/ssr/5030ee88e2d1a049299344026a86a9245b8b82ae","Sv70zJ_VefJwjnnkCDctC/ssr/71bccaeb5ebd68d9ff0c9cf88530db5c1e1465c8","Sv70zJ_VefJwjnnkCDctC/ssr/56233dd266c3f433c826003417df7a5f13dce64a","Sv70zJ_VefJwjnnkCDctC/ssr/817486c70c0f16bebab0a6c46cca1e90840a33b8","Sv70zJ_VefJwjnnkCDctC/ssr/1dcd77ede93f725b09e31931870c2a7a698b271a","Sv70zJ_VefJwjnnkCDctC/ssr/3a8e68edc1acb3092cf4e25c1c1e808de940f49a","Sv70zJ_VefJwjnnkCDctC/ssr/a61fd36d08daaa8f09fcca8e8f210d1e9dc9ddcb","Sv70zJ_VefJwjnnkCDctC/ssr/d957b24663e031714d8d6eac6c9dec28028bcc16","Sv70zJ_VefJwjnnkCDctC/ssr/a7f44410184c826b9c130089f60c616da12861b8","Sv70zJ_VefJwjnnkCDctC/ssr/090c801a6f51853090b398f86c929a497e877a25","Sv70zJ_VefJwjnnkCDctC/ssr/e9848f684a9c4500cfceb82354e4cda695480a3f","Sv70zJ_VefJwjnnkCDctC/ssr/8732cbce972931f1ae92b31fc0ac3c54913b9540","Sv70zJ_VefJwjnnkCDctC/ssr/e9b06953c6ab57baffd7e084c3ec51924fff2cd9","Sv70zJ_VefJwjnnkCDctC/ssr/39ae6570a0c093f48d7dd5e24fd2a7a268686a15","Sv70zJ_VefJwjnnkCDctC/ssr/3f4824e976165b406aa3e4fa3ed26ef5011ae0c3","Sv70zJ_VefJwjnnkCDctC/ssr/0c4735a49daa28f7a5e3605e1d8a4361872ebddc","Sv70zJ_VefJwjnnkCDctC/ssr/0d3155f9476d78c60bd6ff3274ad32fef742bb1a","Sv70zJ_VefJwjnnkCDctC/ssr/2068d4a1a12598ee736582e3bb69ceb83f08d9c7","Sv70zJ_VefJwjnnkCDctC/ssr/34fc927394c13d776d08367ebdb830f978993781","Sv70zJ_VefJwjnnkCDctC/ssr/a25ceb5fe4996bfd9f7e1b6b05f5e6fdf7a4887e","Sv70zJ_VefJwjnnkCDctC/ssr/be9b4a5bc5c79b5dbb2f076f4b48fa53f011eb30","Sv70zJ_VefJwjnnkCDctC/ssr/4911a8faffda0641ee2f30ac5ae327dae2396f06","Sv70zJ_VefJwjnnkCDctC/ssr/9cdb646b02792688dec5c37dca66f3f932bb88f1","Sv70zJ_VefJwjnnkCDctC/ssr/6ff46b6b6314d3dfb07ed7c4b6959e2da58d5510","Sv70zJ_VefJwjnnkCDctC/ssr/008328a40b4c1bf48e4d4a52a0140504266122b0","Sv70zJ_VefJwjnnkCDctC/ssr/d5364be67060739e798b692902b26eb83702ed4f","Sv70zJ_VefJwjnnkCDctC/ssr/31455ce5303a502a5c291c9c998c2c31fa63587d","Sv70zJ_VefJwjnnkCDctC/ssr/2c7162d3a6ae81897fb41d55701c73cf6db571c4","Sv70zJ_VefJwjnnkCDctC/ssr/88f021b5affca9330e1a98bf685b7a25ea1905cd","Sv70zJ_VefJwjnnkCDctC/ssr/8ccbc56708df217db4c9c0d52e6a48d8d59e093e","Sv70zJ_VefJwjnnkCDctC/ssr/81d2fccf64cbe765eb6e0393e2f1d7b5556248dc","Sv70zJ_VefJwjnnkCDctC/ssr/fab576abe2912e4adba53b6deba59e56f814e26a","Sv70zJ_VefJwjnnkCDctC/ssr/da516fa23a5d2553514372961d880948663398e1","Sv70zJ_VefJwjnnkCDctC/ssr/76de13f05026829e30b5fdd5914c38f5c5d6e2f8","Sv70zJ_VefJwjnnkCDctC/ssr/35b7dba0d675903d97318f7c832e763fa2c07cd3","Sv70zJ_VefJwjnnkCDctC/ssr/08a42571fb0c8804b9d6cffab51e7e8b8d15ef34","Sv70zJ_VefJwjnnkCDctC/ssr/162d415198735858141d31bd17c4adf7d6750e85","Sv70zJ_VefJwjnnkCDctC/ssr/31411a690c6a6199ffc0f9e89ab2f45a569e05d0","Sv70zJ_VefJwjnnkCDctC/ssr/ae9c2f564d57601b5973c252189b305c2dd0766d","Sv70zJ_VefJwjnnkCDctC/ssr/ed13c14b1fc821ab883c468dc4a31363ed953961","Sv70zJ_VefJwjnnkCDctC/ssr/af50f1185b48d62addac5c348f1a57074c040d71","Sv70zJ_VefJwjnnkCDctC/ssr/8c4f85c7417bed8cb4036f22466e0d924afe8efe","Sv70zJ_VefJwjnnkCDctC/ssr/bbb5513fd48ec448415cc832ed07aa99ce3dbfb1","Sv70zJ_VefJwjnnkCDctC/ssr/894a63336f8fa24c61e7b7bfe49e62b41fc1fec4","Sv70zJ_VefJwjnnkCDctC/ssr/9c2fc4a1f5aa128e15be29eb66a9b222a7edd8fd","Sv70zJ_VefJwjnnkCDctC/ssr/c2adedc70899c190d32d80100716bac7149843c4","Sv70zJ_VefJwjnnkCDctC/ssr/c6825d8696aa55b8533f19f5afa7623ccfa3e147","Sv70zJ_VefJwjnnkCDctC/ssr/9c466febf16e79aeea76db23c01564cc821b42aa","Sv70zJ_VefJwjnnkCDctC/ssr/158ffa51785aa7fbd6ee212bdd4b2df3dc75cc86","Sv70zJ_VefJwjnnkCDctC/ssr/5c9ee3f6838f0c667f00e43abda84d96b022be14","Sv70zJ_VefJwjnnkCDctC/ssr/628e1c5942d2468f3b3ca5f8199e4aa528e8f581","Sv70zJ_VefJwjnnkCDctC/ssr/f6c75a4f3e6cf647fbbaefc3587c21c343e609a0","Sv70zJ_VefJwjnnkCDctC/ssr/351229eaf479413b8f71070e085a4faf5c1432dc","Sv70zJ_VefJwjnnkCDctC/ssr/ad2a705e5bb442826b685e8f634600fb08d13a42","Sv70zJ_VefJwjnnkCDctC/ssr/3f3f4621a370e9dfe62e3c5f44d7dceb9526eac6","Sv70zJ_VefJwjnnkCDctC/ssr/c1aa3883410adffe95cd003833b044eee0131b2f","Sv70zJ_VefJwjnnkCDctC/ssr/d562100289e9d26bf0f318056f977e6b14105f24","Sv70zJ_VefJwjnnkCDctC/ssr/452355ec5ff93c66a015263b2c948968349d36d3","Sv70zJ_VefJwjnnkCDctC/ssr/09ea9f2ef190a2c44b791e9d37450699758f46af","Sv70zJ_VefJwjnnkCDctC/ssr/d515d4ffc2e203d9d3e7c1fc096471bbdc0c4aaa","Sv70zJ_VefJwjnnkCDctC/ssr/0eb5a2c93c645dd29fc66012d901405cc20c338b","Sv70zJ_VefJwjnnkCDctC/ssr/170882a72561807b8a346067e0a2da57b8b1000c","Sv70zJ_VefJwjnnkCDctC/ssr/254717b6d9263dc47f4b688cb1e39272676091af","Sv70zJ_VefJwjnnkCDctC/ssr/820f00c202350210c65051904ab413462e3eebb8","Sv70zJ_VefJwjnnkCDctC/ssr/663176d39f8fead3db22f34c002ee2bd488033cb","Sv70zJ_VefJwjnnkCDctC/ssr/9a4398512abcfaa3ac351f7ac6e2baffd3a81379","Sv70zJ_VefJwjnnkCDctC/ssr/0b164bbc5e0b194f10985360f88cd0aa3386c5d4","Sv70zJ_VefJwjnnkCDctC/ssr/ce7a1fa56c53811127835c9a49de1c7d1a660323","Sv70zJ_VefJwjnnkCDctC/ssr/bf652a35d42ca6df89bb1ed8469160412fd1158d","Sv70zJ_VefJwjnnkCDctC/ssr/996cec74552f48ba4b96ba1266ec732bc32f9e52","Sv70zJ_VefJwjnnkCDctC/ssr/223858851e2e3c9ef3b6988abdccfe75ba03fc3f","Sv70zJ_VefJwjnnkCDctC/ssr/6c1b9463133f2ed96e6e7c9848606cf4e98f5349","Sv70zJ_VefJwjnnkCDctC/ssr/34df0ef1330a29eb2af138803f25778ecc0119b1","Sv70zJ_VefJwjnnkCDctC/ssr/c48f49331b8d47b61b7d1002f7dfec1898e5fe68","Sv70zJ_VefJwjnnkCDctC/ssr/f0a68998883dce361b13abae7c910adef144df83","Sv70zJ_VefJwjnnkCDctC/ssr/a6cbefb7a412277b882366e9e236697e619f328f","Sv70zJ_VefJwjnnkCDctC/ssr/5b008a3507cfa6ade71132b321cb39233ab8533e","Sv70zJ_VefJwjnnkCDctC/ssr/d884027d3b10fb57307eeedb3e51b3acc5e0fc2d","Sv70zJ_VefJwjnnkCDctC/ssr/fdfdcc1f950624fdf41d263586b85d083a43aa52","Sv70zJ_VefJwjnnkCDctC/ssr/b1f82d09bec7ab9fc5e5625ac1f89de7c9a78282","Sv70zJ_VefJwjnnkCDctC/ssr/ac7d67a28e34fbf74670b344822ec819705c72c8","Sv70zJ_VefJwjnnkCDctC/ssr/e515a77eaf480b778ad89ec036f7e437ae5e9609","Sv70zJ_VefJwjnnkCDctC/ssr/0551f4c98fc518c371aa306e3ddbad88c20e1d55","Sv70zJ_VefJwjnnkCDctC/ssr/8e4388c50c710c042e22ec1af4390e279ebc6648","Sv70zJ_VefJwjnnkCDctC/ssr/34624b1d1de27fcd5081defc4f42bcc5a107cad3","Sv70zJ_VefJwjnnkCDctC/ssr/cd66063ed4fe0d4de42a2b32fb2c341813252b55","Sv70zJ_VefJwjnnkCDctC/ssr/0aecaf88ba7ae6f5fabb7470190c09818a245ed9","Sv70zJ_VefJwjnnkCDctC/ssr/2bea1ef0ffeac0c9c93ae42e5db78db5e44b587e","Sv70zJ_VefJwjnnkCDctC/ssr/838cb839340a07873d557412c5e49aa4e9119c29","Sv70zJ_VefJwjnnkCDctC/ssr/58fb45c9eb05a961625c5c6e41583c0732184cbf","Sv70zJ_VefJwjnnkCDctC/ssr/ba8a90b824592ceda0530a2cd3102e59ba6f91fe","Sv70zJ_VefJwjnnkCDctC/ssr/755216c401bc86e57923d13d970b3add9e765a4f","Sv70zJ_VefJwjnnkCDctC/ssr/9a3bf53517d2a4c3addc236ff717f1304caee9bf","Sv70zJ_VefJwjnnkCDctC/ssr/74ed6f2492ce5b3974aa3531ecdd7969d5802f5c","Sv70zJ_VefJwjnnkCDctC/ssr/36c35a7831ac6a12406b0071bbc01c85a64c2f09","Sv70zJ_VefJwjnnkCDctC/ssr/a69643eaa40e41bd8aa4603f3a1d1cbd4e158dce","Sv70zJ_VefJwjnnkCDctC/ssr/c603b8fb065f1627cadccb40958ee7f81114ef4d","Sv70zJ_VefJwjnnkCDctC/ssr/26de644d4995473009b7631a28db744864c4764b","Sv70zJ_VefJwjnnkCDctC/ssr/9b2f26b3936d13542c1f9f6c6efbacd1fed3b39d","Sv70zJ_VefJwjnnkCDctC/ssr/6e3d553be2d2f4a7fbccd992187ebfbe43b4e8a9","Sv70zJ_VefJwjnnkCDctC/ssr/5a2b0df0cb7fd278bb3d426ec69b61f0377e5b15","Sv70zJ_VefJwjnnkCDctC/ssr/b82c021e58fb56dff2861bdc4631752c0b56acc1","Sv70zJ_VefJwjnnkCDctC/ssr/6698ce88403439ccc8dfcb4987510d1714b22c79","Sv70zJ_VefJwjnnkCDctC/ssr/7e64cff11ee61529273a28f16e66545e3c10ed7c","Sv70zJ_VefJwjnnkCDctC/ssr/9a741f3dd6fd5809166de2f0c022f98f2a19b743","Sv70zJ_VefJwjnnkCDctC/ssr/629d47f9a3a86fce99de315c045c5938e8be765f","Sv70zJ_VefJwjnnkCDctC/ssr/b941acdbfdac60db060c25f59264a98eec151055","Sv70zJ_VefJwjnnkCDctC/ssr/48d89bacab2af0dedf4f009a204e4438ef3db6eb","Sv70zJ_VefJwjnnkCDctC/ssr/9572c9465bbad942e5917c5b9ee6018efc4305fa","Sv70zJ_VefJwjnnkCDctC/ssr/a9010b078fa9d9c0e11a2a49b32e7ad761ce8aed","Sv70zJ_VefJwjnnkCDctC/ssr/492ddead3d10a6520babbc58aa579871a8aa74bf","Sv70zJ_VefJwjnnkCDctC/ssr/4d806a05ae6f765a5e5f45ce7dee4af787b87360","Sv70zJ_VefJwjnnkCDctC/ssr/2423f5a2ee7a6602d047bcdb473628462bc3cfdb","Sv70zJ_VefJwjnnkCDctC/ssr/71ebda753b30298ee8df65705da598dacc90e576","Sv70zJ_VefJwjnnkCDctC/ssr/db9374acb79c55a348d1e0cb91596bd6ceb10a46","Sv70zJ_VefJwjnnkCDctC/ssr/04c31cd9d29e65042eb819a92c00db419b6374ec","Sv70zJ_VefJwjnnkCDctC/ssr/2433d9b37ebfd55b5a22d8165c058d4640718980","Sv70zJ_VefJwjnnkCDctC/ssr/610ef813701155348a6ae348d77ff02d8d90625b","Sv70zJ_VefJwjnnkCDctC/ssr/424c11e49966d5f0e28e568d2fd38071338cf583","Sv70zJ_VefJwjnnkCDctC/ssr/ef4b1551065fa9fbc8a4f3960477438a4124ebfc","Sv70zJ_VefJwjnnkCDctC/ssr/209ef97a57202af3c590245a2a6459b05ed2bc76","Sv70zJ_VefJwjnnkCDctC/ssr/f676ab4f721dad1ae78872ff557ad634494fc40a","Sv70zJ_VefJwjnnkCDctC/ssr/5b64af36c9c42eb30d430c166b2774ac74b8b363","Sv70zJ_VefJwjnnkCDctC/ssr/93ed55c3ae31357f7e80c7f369c8c59d9f0112cb","Sv70zJ_VefJwjnnkCDctC/ssr/83cbd66c88465d3b2679eaaab4407cf80721d7bc","Sv70zJ_VefJwjnnkCDctC/ssr/8b32ccb8244da40b454a2ac27bda2e4aee749fbf","Sv70zJ_VefJwjnnkCDctC/ssr/38f5bfa52834c928215381c9f9329e447c0d06cc","Sv70zJ_VefJwjnnkCDctC/ssr/dae2d57161ff35b6af32fb5f6ebc40ae6fcf37ad","Sv70zJ_VefJwjnnkCDctC/ssr/605c73e2c8230b73996cb7980cce2b0095f5be1c","Sv70zJ_VefJwjnnkCDctC/ssr/6698fc6c9bf9b4d90409b243a6f828d919fac87e","Sv70zJ_VefJwjnnkCDctC/ssr/ae21b8913f01a4ed7ffcd98ebc3182c1b01bbf70","Sv70zJ_VefJwjnnkCDctC/ssr/43bca728b1f279e9d412b1fcd2030e315b971722","Sv70zJ_VefJwjnnkCDctC/ssr/b6020c6f92e349604cdfdd2d6d579fcfcd5c31f8","Sv70zJ_VefJwjnnkCDctC/ssr/a617e5838c576056d39d4230104d06393f35bb6e","Sv70zJ_VefJwjnnkCDctC/ssr/3986e4877b58d07219946b2108421ce55ad6f959","Sv70zJ_VefJwjnnkCDctC/ssr/1feae06d3c90ed2e3875a1ce044dc373a3252ccf","Sv70zJ_VefJwjnnkCDctC/ssr/146991546b310b2af6bb6de755ad7d0b1cf38f92","Sv70zJ_VefJwjnnkCDctC/ssr/98ce595f163e64b5099e998d96a1c020d890587d","Sv70zJ_VefJwjnnkCDctC/ssr/b26f46f7885db47c3eba2bffc6d45f6e0ce7a9b5","Sv70zJ_VefJwjnnkCDctC/ssr/b63bf13d0ef6e1dd1842ec1cbf15a3f65532b66b","Sv70zJ_VefJwjnnkCDctC/ssr/b1d331711b6511dc7892829a21ca4e2cd9c80975","Sv70zJ_VefJwjnnkCDctC/ssr/e0aaf6150b6ea14c8d4e6dce5954c0e9d8998144","Sv70zJ_VefJwjnnkCDctC/ssr/f680c1cee894d5bab7fd249a549e3ab7f57571d9","Sv70zJ_VefJwjnnkCDctC/ssr/69acf08728a2467c4d8e6de2b923c5c0182bba75","Sv70zJ_VefJwjnnkCDctC/ssr/356c1bc00d454ec31a7e8deba884081cf00e912f","Sv70zJ_VefJwjnnkCDctC/ssr/43846eb0b29320bffbddea62d57d06a060ec6756","Sv70zJ_VefJwjnnkCDctC/ssr/8d0cafa0b66ed744e46194ecc9b0d80b4942450c","Sv70zJ_VefJwjnnkCDctC/ssr/08fa404ad5186677dedfa098a02f9825aea9e620","Sv70zJ_VefJwjnnkCDctC/ssr/dea35a265fa2fceacc8c4e3154f1603e4ff93673","Sv70zJ_VefJwjnnkCDctC/ssr/e86830316da1ab792a08457dac1f03fdda992fd3","Sv70zJ_VefJwjnnkCDctC/ssr/37146fe439fee0c8b935bbb43e284e4caae40979","Sv70zJ_VefJwjnnkCDctC/ssr/3688c24bfc01d628a53212f950efccce099117ab","Sv70zJ_VefJwjnnkCDctC/ssr/7868bef71390d9ea30f4a8cd5e68688ee720b256","Sv70zJ_VefJwjnnkCDctC/ssr/0e55eb0d05466eeea0d82e9bbd8003a0ea278e13","Sv70zJ_VefJwjnnkCDctC/ssr/83aa992657c5f8f7db14c66c8434fc501b4868ff","Sv70zJ_VefJwjnnkCDctC/ssr/7eb3dfcee8fedbdf288c8fc09086476c32cee5d5","Sv70zJ_VefJwjnnkCDctC/ssr/9e6d4bb86d2579762e7709636fe1e053d3cda002","Sv70zJ_VefJwjnnkCDctC/ssr/07509d348750849e4c46686c31ad075782410c75","Sv70zJ_VefJwjnnkCDctC/ssr/97593a4b0c29791200dfa6960c2ee4ab7f409244","Sv70zJ_VefJwjnnkCDctC/ssr/61b0a452891da57cf0a1d3d57a6403b541fee6a9","Sv70zJ_VefJwjnnkCDctC/ssr/07960523deb017bcba627a471f768c45b2615df8","Sv70zJ_VefJwjnnkCDctC/ssr/5a778f2e05728c4197a459581e505ccfd0e66b63","Sv70zJ_VefJwjnnkCDctC/ssr/6c0f4d768eb2b72f31c5501d0d9949ff416749cf","Sv70zJ_VefJwjnnkCDctC/ssr/d3a3c3c417c647198b46a0446afe908c8c8a12f8","Sv70zJ_VefJwjnnkCDctC/ssr/6c2627bed0d9f3d6466f5cf7d93a9480070666a2","Sv70zJ_VefJwjnnkCDctC/ssr/0df2859dbcfefe36df96cb68086100a5f5f3173d","Sv70zJ_VefJwjnnkCDctC/ssr/4b14c1f05f2c000bcda2d6d55df6d4c60793f352","Sv70zJ_VefJwjnnkCDctC/ssr/c4c785a7045db28c6dc5a6d93c31a29d3acc7d49","Sv70zJ_VefJwjnnkCDctC/ssr/9ffd0b989dd8a29ace3a14f29b2b7377731d31c5","Sv70zJ_VefJwjnnkCDctC/ssr/9d168bbfe0c29204e18e87ee7310b84739092bf7","Sv70zJ_VefJwjnnkCDctC/ssr/0abc76be8721de4db9c4868dec4524112fdd6f2c","Sv70zJ_VefJwjnnkCDctC/ssr/b6d9b22942df0a899302e1176fcafeda9028fe58","Sv70zJ_VefJwjnnkCDctC/ssr/f63427ef5f12e90f9111e8d75fa086a4cafcdbcc","Sv70zJ_VefJwjnnkCDctC/ssr/65a33a521816e4b1753706b25893c0e55df91474","Sv70zJ_VefJwjnnkCDctC/ssr/0dd0e394c0fdb25342f9eeb0a01114eb60c18c05","Sv70zJ_VefJwjnnkCDctC/ssr/d0baae87822755c3fde223325472423fe2ad14a8","Sv70zJ_VefJwjnnkCDctC/ssr/31ba2897101251b752e107220be6812d30ec1fee","Sv70zJ_VefJwjnnkCDctC/ssr/aa9f8190066645f5fab5c1adb9f92eaa9f94a105","Sv70zJ_VefJwjnnkCDctC/ssr/c76e8b4622d44ba9927de762dc23f7a987ae73a4","Sv70zJ_VefJwjnnkCDctC/ssr/8e6c16f1af662610c46925f5d5a0deec5e973c8f","Sv70zJ_VefJwjnnkCDctC/ssr/ceb5820a1d2dafdc98850dade09986af4670a69b","Sv70zJ_VefJwjnnkCDctC/ssr/db3588308c77c65e3f4e2c770be9ec08f892d685","Sv70zJ_VefJwjnnkCDctC/ssr/f42575b871470f135d3176f40e29562c566f6297","Sv70zJ_VefJwjnnkCDctC/ssr/e8b4796bdbeb7695fff36b2e95351775b9fa77dd","Sv70zJ_VefJwjnnkCDctC/ssr/5263a98dfa8d2e289ea0627d11e79a45f28fa127","Sv70zJ_VefJwjnnkCDctC/ssr/91817ba2877b2bf6e7630a7cc5c9ad267b50fcaf","Sv70zJ_VefJwjnnkCDctC/ssr/defd665e660f1dabd067511368daf8aa031e40e6","Sv70zJ_VefJwjnnkCDctC/ssr/a71a679e59193ddda0d2f5001d6ce4ef4a6cd1de","Sv70zJ_VefJwjnnkCDctC/ssr/69f1cc96b5ebd7cd945774498548b28777b8fb1e","Sv70zJ_VefJwjnnkCDctC/ssr/5d0d60d47a110056b41ec6e7a1f6d8de52a9ff0f","Sv70zJ_VefJwjnnkCDctC/ssr/b43f91759e6747b4144274b34cebf8bf1ef0e206","Sv70zJ_VefJwjnnkCDctC/ssr/b40a7b9ac1be360370748b935875cecfa8ea2527","Sv70zJ_VefJwjnnkCDctC/ssr/9caf09b5b6871a20968284cfc0914a6dddf1be14","Sv70zJ_VefJwjnnkCDctC/ssr/313016b24e608f2f067bd0a115ec48875edc21b5","Sv70zJ_VefJwjnnkCDctC/ssr/b86a2ba4a89c2da15adc2e248de6e9cf85360c5c","Sv70zJ_VefJwjnnkCDctC/ssr/3b65ebe9fcd12fe24a76bb927137b4222fe8e2ed","Sv70zJ_VefJwjnnkCDctC/ssr/3680b99cae573437b28c31d14053ab82aa2ff218","Sv70zJ_VefJwjnnkCDctC/ssr/4d2ca6f32a599a77625c91e851b5ee47802c71ae","Sv70zJ_VefJwjnnkCDctC/ssr/b27687e52e8c53384196d545873e9acd3c891889","Sv70zJ_VefJwjnnkCDctC/ssr/3500fc73fee410ed58f71ea53b1752db38b4a992","Sv70zJ_VefJwjnnkCDctC/ssr/1fcf132f314576ab88083820a691bd09bdbb6c71","Sv70zJ_VefJwjnnkCDctC/ssr/723db991cfb55705a53a470f1fef4b48e7326112","Sv70zJ_VefJwjnnkCDctC/ssr/b52821238d6f764d22cb048b650da96205c329cb","Sv70zJ_VefJwjnnkCDctC/ssr/0e5f9bdc4a9c328e402fd5403ef75abee83ccee9","Sv70zJ_VefJwjnnkCDctC/ssr/e1f7c6ccd2bf53f3f9d947a8c4590bcc1629a888","Sv70zJ_VefJwjnnkCDctC/ssr/3303a07f8acc2f81074668777c6e25c22ed967e9","Sv70zJ_VefJwjnnkCDctC/ssr/cfa1c36be30ed00d7918b112312230f8007351f1","Sv70zJ_VefJwjnnkCDctC/ssr/b36cb2afcb35c2418f36d243608e4a7b9b2952d4","Sv70zJ_VefJwjnnkCDctC/ssr/bca292541ae1ce0542406457e11e248f37b9f1f8","Sv70zJ_VefJwjnnkCDctC/ssr/7e1ef2892230a0b7d2c84a8aaf71eb41817c6eb4","Sv70zJ_VefJwjnnkCDctC/ssr/965a4acdf761ac29a47e461cc6fcb1fec1f90dda","Sv70zJ_VefJwjnnkCDctC/ssr/ba5ef04ddcca9b567a04ee2e24b334cd994e815c","Sv70zJ_VefJwjnnkCDctC/ssr/ee1129b0e27d116acad909c42bff055183d521ae","Sv70zJ_VefJwjnnkCDctC/ssr/d5778fd1adc167ca6293dab0abdfa62264707917","tmp.iOU32nzqCx/store/agents.ndjson","tmp.iOU32nzqCx/store/runs.ndjson","tmp.iOU32nzqCx/store/run_events.ndjson","tmp.iOU32nzqCx/store/checkpoints.ndjson","tmp.vKYnC7f9uh/store/agents.ndjson","tmp.vKYnC7f9uh/store/runs.ndjson","tmp.vKYnC7f9uh/store/run_events.ndjson","tmp.vKYnC7f9uh/store/checkpoints.ndjson","tmp.qjI2uPTcHz/store/agents.ndjson","tmp.qjI2uPTcHz/store/runs.ndjson","tmp.qjI2uPTcHz/store/run_events.ndjson","tmp.qjI2uPTcHz/store/checkpoints.ndjson","react-doctor-4e289efd-db39-4bbb-afec-26444c70c8a9/diagnostics.json","react-doctor-4e289efd-db39-4bbb-afec-26444c70c8a9/react-doctor--prefer-module-scope-pure-function.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/diagnostics.json","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--prefer-useReducer.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--rerender-lazy-ref-init.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--no-render-in-render.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--exhaustive-deps.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--no-event-handler.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--server-sequential-independent-await.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--js-combine-iterations.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--async-await-in-loop.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--no-cascading-set-state.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--no-barrel-import.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-hooks-js--set-state-in-effect.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-hooks-js--preserve-manual-memoization.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--only-export-components.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--react-compiler-no-manual-memoization.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--rn-prefer-expo-image.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--no-giant-component.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--no-derived-state.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--no-chain-state-updates.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--rn-style-prefer-boxshadow.txt","react-doctor-79ce569f-546c-407a-affb-2f0d6641a3c1/react-doctor--rn-no-legacy-shadow-styles.txt","tmp.rnt6jWdd66/store/agents.ndjson","tmp.rnt6jWdd66/store/runs.ndjson","tmp.rnt6jWdd66/store/run_events.ndjson","tmp.rnt6jWdd66/store/checkpoints.ndjson","tmp.jPtDqRD123/store/agents.ndjson","tmp.jPtDqRD123/store/runs.ndjson","tmp.jPtDqRD123/store/run_events.ndjson","tmp.jPtDqRD123/store/checkpoints.ndjson","tmp.jPtDqRD123/workspace/fixture.txt","tmp.x8cNhhZUbp/store/agents.ndjson","tmp.x8cNhhZUbp/store/runs.ndjson","tmp.x8cNhhZUbp/store/run_events.ndjson","tmp.x8cNhhZUbp/store/checkpoints.ndjson","tmp.g5gLilKPNJ/store/agents.ndjson","tmp.g5gLilKPNJ/store/runs.ndjson","tmp.g5gLilKPNJ/store/run_events.ndjson","tmp.zMdRdRTzEA/store/agents.ndjson","tmp.zMdRdRTzEA/store/runs.ndjson","tmp.qpYdIp2c36/store/agents.ndjson","tmp.qpYdIp2c36/store/runs.ndjson","tmp.qpYdIp2c36/store/run_events.ndjson","screenstudio/polyrecorder-discover-logs/polyrecorder-discovery-1781651188960.log","julius-code-insiders-zsh/.zlogin","julius-code-insiders-zsh/.zshenv","julius-code-insiders-zsh/.zprofile","julius-code-insiders-zsh/.zshrc","gitkraken/gitlens/gitlens-ipc-server-39753-58539.json","gitkraken/gitlens/agents/gitlens-ipc-server-39753-58539.json","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/diagnostics.json","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--no-render-in-render.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--js-combine-iterations.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--rerender-lazy-ref-init.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--no-barrel-import.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--no-cascading-set-state.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--async-await-in-loop.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-hooks-js--set-state-in-effect.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-hooks-js--preserve-manual-memoization.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--exhaustive-deps.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--no-event-handler.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--server-sequential-independent-await.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--prefer-useReducer.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--only-export-components.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--react-compiler-no-manual-memoization.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--rn-style-prefer-boxshadow.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--rn-no-legacy-shadow-styles.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--no-derived-state.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--no-chain-state-updates.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--rn-prefer-expo-image.txt","react-doctor-c70aa040-70de-4f72-88fb-2459f7e6d242/react-doctor--no-giant-component.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/diagnostics.json","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--prefer-useReducer.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--async-await-in-loop.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--rerender-lazy-ref-init.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--no-cascading-set-state.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-hooks-js--preserve-manual-memoization.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--js-combine-iterations.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-hooks-js--set-state-in-effect.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--no-event-handler.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--exhaustive-deps.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--no-render-in-render.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--no-barrel-import.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--only-export-components.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--server-sequential-independent-await.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--react-compiler-no-manual-memoization.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--rn-prefer-expo-image.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--no-giant-component.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--rn-style-prefer-boxshadow.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--rn-no-legacy-shadow-styles.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--no-derived-state.txt","react-doctor-923097e1-1901-414f-b867-ad3ed750c42f/react-doctor--no-chain-state-updates.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/diagnostics.json","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-hooks-js--preserve-manual-memoization.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--no-barrel-import.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--no-render-in-render.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--prefer-useReducer.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--no-cascading-set-state.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--js-combine-iterations.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-hooks-js--set-state-in-effect.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--exhaustive-deps.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--no-event-handler.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--server-sequential-independent-await.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--async-await-in-loop.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--rerender-lazy-ref-init.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--only-export-components.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--react-compiler-no-manual-memoization.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--no-derived-state.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--no-chain-state-updates.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--rn-style-prefer-boxshadow.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--rn-no-legacy-shadow-styles.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--rn-prefer-expo-image.txt","react-doctor-c75d1898-35bf-4040-9291-143a461c0bc2/react-doctor--no-giant-component.txt",".tmp0v8kOo/tracing.js",".tmp0v8kOo/meriyah.umd.min.js",".tmp0v8kOo/privileged-node-repl-config.js",".tmp0v8kOo/privileged-node-repl.js",".tmp0v8kOo/diagnostics.js",".tmp0v8kOo/kernel.js","t3-orchestrator-v2-subagent-oacb4S/.git/logs/refs/heads/main","t3-orchestrator-v2-subagent-oacb4S/.git/logs/HEAD","t3-orchestrator-v2-subagent-oacb4S/.git/refs/heads/main","t3-orchestrator-v2-subagent-oacb4S/.git/COMMIT_EDITMSG","t3-orchestrator-v2-subagent-oacb4S/README.md","t3-orchestrator-v2-subagent-oacb4S/.git/config","t3-orchestrator-v2-subagent-oacb4S/.git/HEAD","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/push-to-checkout.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/update.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/pre-push.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/pre-applypatch.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/pre-merge-commit.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/post-update.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/prepare-commit-msg.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/pre-receive.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/fsmonitor-watchman.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/applypatch-msg.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/pre-commit.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/sendemail-validate.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/pre-rebase.sample","t3-orchestrator-v2-subagent-oacb4S/.git/hooks/commit-msg.sample","t3-orchestrator-v2-subagent-oacb4S/.git/description","t3-orchestrator-v2-subagent-oacb4S/.git/info/exclude","6I2aNB1hLUYb8xMmS8D-A/ssr/03512668e6e78af858678dc39f94b4ebfd9a1636","6I2aNB1hLUYb8xMmS8D-A/ssr/fdc533205cc4baba09e204fbdf8df01390315412","6I2aNB1hLUYb8xMmS8D-A/ssr/3f7629ddfb31c7d1882e4a63c41b471931502692","6I2aNB1hLUYb8xMmS8D-A/ssr/860676e14ee3d69d53c9738f0fd4bda9479d233e","6I2aNB1hLUYb8xMmS8D-A/ssr/34df0ef1330a29eb2af138803f25778ecc0119b1","6I2aNB1hLUYb8xMmS8D-A/ssr/c48f49331b8d47b61b7d1002f7dfec1898e5fe68","6I2aNB1hLUYb8xMmS8D-A/ssr/f0a68998883dce361b13abae7c910adef144df83","6I2aNB1hLUYb8xMmS8D-A/ssr/0551f4c98fc518c371aa306e3ddbad88c20e1d55","6I2aNB1hLUYb8xMmS8D-A/ssr/4d806a05ae6f765a5e5f45ce7dee4af787b87360","6I2aNB1hLUYb8xMmS8D-A/ssr/8e4388c50c710c042e22ec1af4390e279ebc6648","6I2aNB1hLUYb8xMmS8D-A/ssr/cd66063ed4fe0d4de42a2b32fb2c341813252b55","6I2aNB1hLUYb8xMmS8D-A/ssr/26de644d4995473009b7631a28db744864c4764b","6I2aNB1hLUYb8xMmS8D-A/ssr/0aecaf88ba7ae6f5fabb7470190c09818a245ed9","6I2aNB1hLUYb8xMmS8D-A/ssr/2bea1ef0ffeac0c9c93ae42e5db78db5e44b587e","6I2aNB1hLUYb8xMmS8D-A/ssr/838cb839340a07873d557412c5e49aa4e9119c29","6I2aNB1hLUYb8xMmS8D-A/ssr/58fb45c9eb05a961625c5c6e41583c0732184cbf","6I2aNB1hLUYb8xMmS8D-A/ssr/ba8a90b824592ceda0530a2cd3102e59ba6f91fe","6I2aNB1hLUYb8xMmS8D-A/ssr/755216c401bc86e57923d13d970b3add9e765a4f","6I2aNB1hLUYb8xMmS8D-A/ssr/a69643eaa40e41bd8aa4603f3a1d1cbd4e158dce","6I2aNB1hLUYb8xMmS8D-A/ssr/c603b8fb065f1627cadccb40958ee7f81114ef4d","6I2aNB1hLUYb8xMmS8D-A/ssr/71ebda753b30298ee8df65705da598dacc90e576","6I2aNB1hLUYb8xMmS8D-A/ssr/db9374acb79c55a348d1e0cb91596bd6ceb10a46","6I2aNB1hLUYb8xMmS8D-A/ssr/04c31cd9d29e65042eb819a92c00db419b6374ec","6I2aNB1hLUYb8xMmS8D-A/ssr/2433d9b37ebfd55b5a22d8165c058d4640718980","6I2aNB1hLUYb8xMmS8D-A/ssr/610ef813701155348a6ae348d77ff02d8d90625b","6I2aNB1hLUYb8xMmS8D-A/ssr/424c11e49966d5f0e28e568d2fd38071338cf583","6I2aNB1hLUYb8xMmS8D-A/ssr/ef4b1551065fa9fbc8a4f3960477438a4124ebfc","6I2aNB1hLUYb8xMmS8D-A/ssr/209ef97a57202af3c590245a2a6459b05ed2bc76","6I2aNB1hLUYb8xMmS8D-A/ssr/f676ab4f721dad1ae78872ff557ad634494fc40a","6I2aNB1hLUYb8xMmS8D-A/ssr/5b64af36c9c42eb30d430c166b2774ac74b8b363","6I2aNB1hLUYb8xMmS8D-A/ssr/93ed55c3ae31357f7e80c7f369c8c59d9f0112cb","6I2aNB1hLUYb8xMmS8D-A/ssr/83cbd66c88465d3b2679eaaab4407cf80721d7bc","6I2aNB1hLUYb8xMmS8D-A/ssr/8b32ccb8244da40b454a2ac27bda2e4aee749fbf","6I2aNB1hLUYb8xMmS8D-A/ssr/38f5bfa52834c928215381c9f9329e447c0d06cc","6I2aNB1hLUYb8xMmS8D-A/ssr/dae2d57161ff35b6af32fb5f6ebc40ae6fcf37ad","6I2aNB1hLUYb8xMmS8D-A/ssr/605c73e2c8230b73996cb7980cce2b0095f5be1c","6I2aNB1hLUYb8xMmS8D-A/ssr/6698fc6c9bf9b4d90409b243a6f828d919fac87e","6I2aNB1hLUYb8xMmS8D-A/ssr/ae21b8913f01a4ed7ffcd98ebc3182c1b01bbf70","6I2aNB1hLUYb8xMmS8D-A/ssr/43bca728b1f279e9d412b1fcd2030e315b971722","6I2aNB1hLUYb8xMmS8D-A/ssr/b6020c6f92e349604cdfdd2d6d579fcfcd5c31f8","6I2aNB1hLUYb8xMmS8D-A/ssr/a617e5838c576056d39d4230104d06393f35bb6e","6I2aNB1hLUYb8xMmS8D-A/ssr/3986e4877b58d07219946b2108421ce55ad6f959","6I2aNB1hLUYb8xMmS8D-A/ssr/1feae06d3c90ed2e3875a1ce044dc373a3252ccf","6I2aNB1hLUYb8xMmS8D-A/ssr/146991546b310b2af6bb6de755ad7d0b1cf38f92","6I2aNB1hLUYb8xMmS8D-A/ssr/98ce595f163e64b5099e998d96a1c020d890587d","6I2aNB1hLUYb8xMmS8D-A/ssr/b26f46f7885db47c3eba2bffc6d45f6e0ce7a9b5","6I2aNB1hLUYb8xMmS8D-A/ssr/b63bf13d0ef6e1dd1842ec1cbf15a3f65532b66b","6I2aNB1hLUYb8xMmS8D-A/ssr/b1d331711b6511dc7892829a21ca4e2cd9c80975","6I2aNB1hLUYb8xMmS8D-A/ssr/e0aaf6150b6ea14c8d4e6dce5954c0e9d8998144","6I2aNB1hLUYb8xMmS8D-A/ssr/f680c1cee894d5bab7fd249a549e3ab7f57571d9","6I2aNB1hLUYb8xMmS8D-A/ssr/69acf08728a2467c4d8e6de2b923c5c0182bba75","6I2aNB1hLUYb8xMmS8D-A/ssr/356c1bc00d454ec31a7e8deba884081cf00e912f","6I2aNB1hLUYb8xMmS8D-A/ssr/43846eb0b29320bffbddea62d57d06a060ec6756","6I2aNB1hLUYb8xMmS8D-A/ssr/8d0cafa0b66ed744e46194ecc9b0d80b4942450c","6I2aNB1hLUYb8xMmS8D-A/ssr/08fa404ad5186677dedfa098a02f9825aea9e620","6I2aNB1hLUYb8xMmS8D-A/ssr/dea35a265fa2fceacc8c4e3154f1603e4ff93673","6I2aNB1hLUYb8xMmS8D-A/ssr/4911a8faffda0641ee2f30ac5ae327dae2396f06","6I2aNB1hLUYb8xMmS8D-A/ssr/170882a72561807b8a346067e0a2da57b8b1000c","6I2aNB1hLUYb8xMmS8D-A/ssr/5c9ee3f6838f0c667f00e43abda84d96b022be14","6I2aNB1hLUYb8xMmS8D-A/ssr/da516fa23a5d2553514372961d880948663398e1","6I2aNB1hLUYb8xMmS8D-A/ssr/76de13f05026829e30b5fdd5914c38f5c5d6e2f8","6I2aNB1hLUYb8xMmS8D-A/ssr/162d415198735858141d31bd17c4adf7d6750e85","6I2aNB1hLUYb8xMmS8D-A/ssr/bf652a35d42ca6df89bb1ed8469160412fd1158d","6I2aNB1hLUYb8xMmS8D-A/ssr/ce7a1fa56c53811127835c9a49de1c7d1a660323","6I2aNB1hLUYb8xMmS8D-A/ssr/f63427ef5f12e90f9111e8d75fa086a4cafcdbcc","6I2aNB1hLUYb8xMmS8D-A/ssr/bbb5513fd48ec448415cc832ed07aa99ce3dbfb1","6I2aNB1hLUYb8xMmS8D-A/ssr/894a63336f8fa24c61e7b7bfe49e62b41fc1fec4","6I2aNB1hLUYb8xMmS8D-A/ssr/35b7dba0d675903d97318f7c832e763fa2c07cd3","6I2aNB1hLUYb8xMmS8D-A/ssr/08a42571fb0c8804b9d6cffab51e7e8b8d15ef34","6I2aNB1hLUYb8xMmS8D-A/ssr/9cdb646b02792688dec5c37dca66f3f932bb88f1","6I2aNB1hLUYb8xMmS8D-A/ssr/36c35a7831ac6a12406b0071bbc01c85a64c2f09","6I2aNB1hLUYb8xMmS8D-A/ssr/9ba96505ccde7eeae7de2d9a13c2bf51ed82b6e3","6I2aNB1hLUYb8xMmS8D-A/ssr/90eb3168c927e9ddcf0ab4cd169bb31daa5d0e7f","6I2aNB1hLUYb8xMmS8D-A/ssr/e6d547638bc3fd9992a8897714b844680da6f7ae","6I2aNB1hLUYb8xMmS8D-A/ssr/2e90945296775108efa9b01fa739ad47e8aba4dd","6I2aNB1hLUYb8xMmS8D-A/ssr/426b264e71b295f2e89d6660c982549d998eb126","6I2aNB1hLUYb8xMmS8D-A/ssr/20b76bfc8ad0e67d220d0e9323a5b97fcfd765a4","6I2aNB1hLUYb8xMmS8D-A/ssr/89f1e0b4eae28003f174b7c5a5881d82a067e2c8","6I2aNB1hLUYb8xMmS8D-A/ssr/dda0cfe11159b11a071c1d68f2f188eb03e78b85","6I2aNB1hLUYb8xMmS8D-A/ssr/056577ee33fdf896349ea9d45f23b32c7411a9da","6I2aNB1hLUYb8xMmS8D-A/ssr/f56228a1c8e89972e0b9de88c268c4b1052e5208","6I2aNB1hLUYb8xMmS8D-A/ssr/9160809084ea0683dabd81e5664e5c4f92622373","6I2aNB1hLUYb8xMmS8D-A/ssr/9dc8fe9293a9583ebeb59c79d690e29244e2c6a1","6I2aNB1hLUYb8xMmS8D-A/ssr/3213db423cf0ce233e240a56922a0e08bfe0a3f4","6I2aNB1hLUYb8xMmS8D-A/ssr/e3f31f14749821719da975c89201b98d50d38e82","6I2aNB1hLUYb8xMmS8D-A/ssr/1e735c16bf919f0511b4872fb5b82fa216195e3e","6I2aNB1hLUYb8xMmS8D-A/ssr/03c44824143d55d72e5071a7a35b9d33b10b3e88","6I2aNB1hLUYb8xMmS8D-A/ssr/0c2684970f300361612969cc3f81f864f666fba0","6I2aNB1hLUYb8xMmS8D-A/ssr/ead5980546cb65d218d5c030272067a55b501021","6I2aNB1hLUYb8xMmS8D-A/ssr/9f339af2b6527f1a40317774dbc3c74b83486905","6I2aNB1hLUYb8xMmS8D-A/ssr/0bb63e10030b4ef29f811aa74fc442e258c76fb5","6I2aNB1hLUYb8xMmS8D-A/ssr/3132f86817109cc8acea83c7902d0fea0e96f9d7","6I2aNB1hLUYb8xMmS8D-A/ssr/66aee5cb71bbe76e949705d41738631ef02fc371","6I2aNB1hLUYb8xMmS8D-A/ssr/d21f9cb11d2cc7cc3972e960170f1abd15eb08f8","6I2aNB1hLUYb8xMmS8D-A/ssr/e8ef6410eaa3710dc899a3f22a0d4acf7ae889fb","6I2aNB1hLUYb8xMmS8D-A/ssr/176a4bd4eb0a3a539bcb1fa845821af9d2af41c3","6I2aNB1hLUYb8xMmS8D-A/ssr/bef06ba48d24d7081310d9b876431216182d06d6","6I2aNB1hLUYb8xMmS8D-A/ssr/7e0e0c3611fcc458858c9110971959c697bb313b","6I2aNB1hLUYb8xMmS8D-A/ssr/7b6792cb0e1a0594b40cae540b52bbb19763ac97","6I2aNB1hLUYb8xMmS8D-A/ssr/f263c4b6594e239dd59a42bed0b03cc1b0441dbc","6I2aNB1hLUYb8xMmS8D-A/ssr/86a32f9b4ff4370c6b514272b9b5975cba550c33","6I2aNB1hLUYb8xMmS8D-A/ssr/6a7963df7c19582578651c6705c8b0245061a93f","6I2aNB1hLUYb8xMmS8D-A/ssr/503ad5cdcbcc242806fdd170c760202c331a6b6d","6I2aNB1hLUYb8xMmS8D-A/ssr/daa354e850993db37f736a8f5b489436c8124327","6I2aNB1hLUYb8xMmS8D-A/ssr/5313d6298be90479e5793d25364fa4f4b2bad69d","6I2aNB1hLUYb8xMmS8D-A/ssr/4d15985c155136629f688c0f56440f4f01bd95d5","6I2aNB1hLUYb8xMmS8D-A/ssr/f26ac95ac397f898c6b0960740dc851c1a0dae44","6I2aNB1hLUYb8xMmS8D-A/ssr/9af13b165c80573c915bac4e4cd0cec12093bf62","6I2aNB1hLUYb8xMmS8D-A/ssr/dcf3d546a8b96be2d34171110b5cfd48b292e98f","6I2aNB1hLUYb8xMmS8D-A/ssr/b4a117709df63869dfd68c06c97766fea7f96d28","6I2aNB1hLUYb8xMmS8D-A/ssr/4a895b30a440125f726e11b292ce21434637204b","6I2aNB1hLUYb8xMmS8D-A/ssr/24bd728815ed2dfad045837f3fe0911543d0fd3f","6I2aNB1hLUYb8xMmS8D-A/ssr/5cdc491ca6c95b15118a439a30d1b45831e42cdd","6I2aNB1hLUYb8xMmS8D-A/ssr/461fcf4297afc4698b57f8b411a96513463487fe","6I2aNB1hLUYb8xMmS8D-A/ssr/8f9b7afad1e0ae0ff0bc855f44bb1a571f887cd3","6I2aNB1hLUYb8xMmS8D-A/ssr/0f40501169dcabe1bcf43b0ec600f87b02f7d15b","6I2aNB1hLUYb8xMmS8D-A/ssr/cab083fba948e156c272f1312af21b691378eca8","6I2aNB1hLUYb8xMmS8D-A/ssr/012418f8afa5d932d199eff26d9444fc3649d91d","6I2aNB1hLUYb8xMmS8D-A/ssr/81bb3a7ec5d30a472d5eee10d93843aa1ff5e217","6I2aNB1hLUYb8xMmS8D-A/ssr/f5dadf272ae5748ae224c18caf9967eb5c846649","6I2aNB1hLUYb8xMmS8D-A/ssr/98e6dfc34bf155f998f3757d2fe6503997ee7347","6I2aNB1hLUYb8xMmS8D-A/ssr/f5f9c8fe76a9bbc470c8d3279ed29c6336a6befe","6I2aNB1hLUYb8xMmS8D-A/ssr/218eb0759133e8caea9a14f5bd1dd28e19ec5e7c","6I2aNB1hLUYb8xMmS8D-A/ssr/9b0c800ee134a8f74ca2505ac3f0da43111847cc","6I2aNB1hLUYb8xMmS8D-A/ssr/255aedfeeadf2a4cd733177484b0910cfb900cf3","6I2aNB1hLUYb8xMmS8D-A/ssr/82224c0f6fa407944c819118a2b9b9ccd54b98d5","6I2aNB1hLUYb8xMmS8D-A/ssr/4230cbf5f9e3b638ce57eaa7d8820ed43588cfde","6I2aNB1hLUYb8xMmS8D-A/ssr/d478d4f1ebfbb5f487b14f9c68fa6445f969e1df","6I2aNB1hLUYb8xMmS8D-A/ssr/b5535b07bba259da83bb912423eec19163876994","6I2aNB1hLUYb8xMmS8D-A/ssr/86316717a491a8f273f5182d5ccbaee6623de4da","6I2aNB1hLUYb8xMmS8D-A/ssr/534b7c1fe05e6733a2b0723050177a5b3a94dd20","6I2aNB1hLUYb8xMmS8D-A/ssr/c6338f5cbcb4476872c42c16e4a951e2b00fbc37","6I2aNB1hLUYb8xMmS8D-A/ssr/93e2bb9dd21dae3cbb43930bb27c4229af9a7470","6I2aNB1hLUYb8xMmS8D-A/ssr/25257d03950239399a7ceafdd780caafe6129a7c","6I2aNB1hLUYb8xMmS8D-A/ssr/546009b089dbf57256bd33aafe17f2a3a6092f30","6I2aNB1hLUYb8xMmS8D-A/ssr/5cd925dd4034fc0a614c10d410631072f255f173","6I2aNB1hLUYb8xMmS8D-A/ssr/9ffd0b989dd8a29ace3a14f29b2b7377731d31c5","6I2aNB1hLUYb8xMmS8D-A/ssr/5b008a3507cfa6ade71132b321cb39233ab8533e","6I2aNB1hLUYb8xMmS8D-A/ssr/d884027d3b10fb57307eeedb3e51b3acc5e0fc2d","6I2aNB1hLUYb8xMmS8D-A/ssr/c636629172b3ea1502146817f54a5da25ebcf337","6I2aNB1hLUYb8xMmS8D-A/ssr/7e64cff11ee61529273a28f16e66545e3c10ed7c","6I2aNB1hLUYb8xMmS8D-A/ssr/e6abf249b2708845ac6f6c15e407822dae517223","6I2aNB1hLUYb8xMmS8D-A/ssr/fdfdcc1f950624fdf41d263586b85d083a43aa52","6I2aNB1hLUYb8xMmS8D-A/ssr/9a741f3dd6fd5809166de2f0c022f98f2a19b743","6I2aNB1hLUYb8xMmS8D-A/ssr/b1f82d09bec7ab9fc5e5625ac1f89de7c9a78282","6I2aNB1hLUYb8xMmS8D-A/ssr/e7a92a185a34b19bda41eba0ad256e370ea10c59","6I2aNB1hLUYb8xMmS8D-A/ssr/ac7d67a28e34fbf74670b344822ec819705c72c8","6I2aNB1hLUYb8xMmS8D-A/ssr/6c1b9463133f2ed96e6e7c9848606cf4e98f5349","6I2aNB1hLUYb8xMmS8D-A/ssr/9572c9465bbad942e5917c5b9ee6018efc4305fa","6I2aNB1hLUYb8xMmS8D-A/ssr/a6cbefb7a412277b882366e9e236697e619f328f","6I2aNB1hLUYb8xMmS8D-A/ssr/e515a77eaf480b778ad89ec036f7e437ae5e9609","6I2aNB1hLUYb8xMmS8D-A/ssr/9a3bf53517d2a4c3addc236ff717f1304caee9bf","6I2aNB1hLUYb8xMmS8D-A/ssr/74ed6f2492ce5b3974aa3531ecdd7969d5802f5c","6I2aNB1hLUYb8xMmS8D-A/ssr/34624b1d1de27fcd5081defc4f42bcc5a107cad3","6I2aNB1hLUYb8xMmS8D-A/ssr/52ecc183b63d00e54e83752ca85d8627290f46e5","6I2aNB1hLUYb8xMmS8D-A/ssr/0df2859dbcfefe36df96cb68086100a5f5f3173d","6I2aNB1hLUYb8xMmS8D-A/ssr/4b14c1f05f2c000bcda2d6d55df6d4c60793f352","6I2aNB1hLUYb8xMmS8D-A/ssr/c4c785a7045db28c6dc5a6d93c31a29d3acc7d49","6I2aNB1hLUYb8xMmS8D-A/ssr/b6d9b22942df0a899302e1176fcafeda9028fe58","6I2aNB1hLUYb8xMmS8D-A/ssr/5100ff82f79b4abb91fb4234e9b12c774804ca5f","6I2aNB1hLUYb8xMmS8D-A/ssr/15c04ddc4b71b3171a9968af32a09a3b1b9cbc1d","6I2aNB1hLUYb8xMmS8D-A/ssr/2c0885e21bf350e47164e280362c073f8451f471","6I2aNB1hLUYb8xMmS8D-A/ssr/d0baae87822755c3fde223325472423fe2ad14a8","6I2aNB1hLUYb8xMmS8D-A/ssr/31ba2897101251b752e107220be6812d30ec1fee","6I2aNB1hLUYb8xMmS8D-A/ssr/2d4d2626746953deac0b15dbf8b698fbb21ca373","6I2aNB1hLUYb8xMmS8D-A/ssr/58f558f67ce1473475dee602195610641bba1a62","6I2aNB1hLUYb8xMmS8D-A/ssr/db3588308c77c65e3f4e2c770be9ec08f892d685","6I2aNB1hLUYb8xMmS8D-A/ssr/f42575b871470f135d3176f40e29562c566f6297","6I2aNB1hLUYb8xMmS8D-A/ssr/e8b4796bdbeb7695fff36b2e95351775b9fa77dd","6I2aNB1hLUYb8xMmS8D-A/ssr/5263a98dfa8d2e289ea0627d11e79a45f28fa127","6I2aNB1hLUYb8xMmS8D-A/ssr/91817ba2877b2bf6e7630a7cc5c9ad267b50fcaf","6I2aNB1hLUYb8xMmS8D-A/ssr/defd665e660f1dabd067511368daf8aa031e40e6","6I2aNB1hLUYb8xMmS8D-A/ssr/a71a679e59193ddda0d2f5001d6ce4ef4a6cd1de","6I2aNB1hLUYb8xMmS8D-A/ssr/69f1cc96b5ebd7cd945774498548b28777b8fb1e","6I2aNB1hLUYb8xMmS8D-A/ssr/5d0d60d47a110056b41ec6e7a1f6d8de52a9ff0f","6I2aNB1hLUYb8xMmS8D-A/ssr/b43f91759e6747b4144274b34cebf8bf1ef0e206","6I2aNB1hLUYb8xMmS8D-A/ssr/b40a7b9ac1be360370748b935875cecfa8ea2527","6I2aNB1hLUYb8xMmS8D-A/ssr/9caf09b5b6871a20968284cfc0914a6dddf1be14","6I2aNB1hLUYb8xMmS8D-A/ssr/313016b24e608f2f067bd0a115ec48875edc21b5","6I2aNB1hLUYb8xMmS8D-A/ssr/b86a2ba4a89c2da15adc2e248de6e9cf85360c5c","6I2aNB1hLUYb8xMmS8D-A/ssr/3b65ebe9fcd12fe24a76bb927137b4222fe8e2ed","6I2aNB1hLUYb8xMmS8D-A/ssr/3680b99cae573437b28c31d14053ab82aa2ff218","6I2aNB1hLUYb8xMmS8D-A/ssr/4d2ca6f32a599a77625c91e851b5ee47802c71ae","6I2aNB1hLUYb8xMmS8D-A/ssr/b27687e52e8c53384196d545873e9acd3c891889","6I2aNB1hLUYb8xMmS8D-A/ssr/3500fc73fee410ed58f71ea53b1752db38b4a992","6I2aNB1hLUYb8xMmS8D-A/ssr/d1c7193ffe5d30f1dd8d5a3cd30f1ab729aed5ca","6I2aNB1hLUYb8xMmS8D-A/ssr/1fcf132f314576ab88083820a691bd09bdbb6c71","6I2aNB1hLUYb8xMmS8D-A/ssr/723db991cfb55705a53a470f1fef4b48e7326112","6I2aNB1hLUYb8xMmS8D-A/ssr/b52821238d6f764d22cb048b650da96205c329cb","6I2aNB1hLUYb8xMmS8D-A/ssr/0e5f9bdc4a9c328e402fd5403ef75abee83ccee9","6I2aNB1hLUYb8xMmS8D-A/ssr/e1f7c6ccd2bf53f3f9d947a8c4590bcc1629a888","6I2aNB1hLUYb8xMmS8D-A/ssr/3303a07f8acc2f81074668777c6e25c22ed967e9","6I2aNB1hLUYb8xMmS8D-A/ssr/cfa1c36be30ed00d7918b112312230f8007351f1","6I2aNB1hLUYb8xMmS8D-A/ssr/b36cb2afcb35c2418f36d243608e4a7b9b2952d4","6I2aNB1hLUYb8xMmS8D-A/ssr/bca292541ae1ce0542406457e11e248f37b9f1f8","6I2aNB1hLUYb8xMmS8D-A/ssr/7e1ef2892230a0b7d2c84a8aaf71eb41817c6eb4","6I2aNB1hLUYb8xMmS8D-A/ssr/965a4acdf761ac29a47e461cc6fcb1fec1f90dda","6I2aNB1hLUYb8xMmS8D-A/ssr/ba5ef04ddcca9b567a04ee2e24b334cd994e815c","6I2aNB1hLUYb8xMmS8D-A/ssr/ee1129b0e27d116acad909c42bff055183d521ae","6I2aNB1hLUYb8xMmS8D-A/ssr/c3e9dc1c0287989eb1fe1aeffb495bd43fb90d3c","6I2aNB1hLUYb8xMmS8D-A/ssr/3c99065d603c15874997449f8f1dec863d5d6ec9","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/diagnostics.json","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--js-combine-iterations.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--no-render-in-render.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--async-await-in-loop.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--no-barrel-import.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--no-cascading-set-state.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--rerender-lazy-ref-init.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--prefer-useReducer.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--exhaustive-deps.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--no-event-handler.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--server-sequential-independent-await.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--react-compiler-no-manual-memoization.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--only-export-components.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-hooks-js--set-state-in-effect.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-hooks-js--preserve-manual-memoization.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--rn-style-prefer-boxshadow.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--rn-no-legacy-shadow-styles.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--no-giant-component.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--rn-prefer-expo-image.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--no-derived-state.txt","react-doctor-7689037e-8abb-4ead-9a92-4f9c01ee2fec/react-doctor--no-chain-state-updates.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/diagnostics.json","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--no-cascading-set-state.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--no-barrel-import.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--prefer-useReducer.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--js-combine-iterations.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--exhaustive-deps.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--no-event-handler.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--server-sequential-independent-await.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--rerender-lazy-ref-init.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--only-export-components.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-hooks-js--set-state-in-effect.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-hooks-js--preserve-manual-memoization.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--async-await-in-loop.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--no-render-in-render.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--react-compiler-no-manual-memoization.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--rn-style-prefer-boxshadow.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--rn-no-legacy-shadow-styles.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--no-giant-component.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--rn-prefer-expo-image.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--no-derived-state.txt","react-doctor-dea5677f-6356-45da-ae48-29898add4f3d/react-doctor--no-chain-state-updates.txt","metro-cache/55/ae891e4bf5c3a1284aa9e426881c445707c9d590e0f832f818c6de850ef379dc1d6b67","metro-cache/71/b90bcbc6d99cddca9322d8a2e10368f024f76b12bed3d84647b984ac3e2ee934592280","metro-cache/e5/8d2690194be45c0f213f789b5e10ade9f5f5d40e05f97b59a912f472932219db59de56","metro-cache/3e/cf23617e4d3134b6cabad48cba46e83573ae42ffecf0ca7f74ce6e4445313c047a6293","metro-cache/ae/5911c4f429b2b971f819d76155e2e3a80124a579cbef75c3f92e527f05cde59a8b85c8","metro-cache/03/c75a7f6df5ac74e4e1616c19305267acf14cac245ed985bd264dc1c6857966326f601e","metro-cache/a4/6c70033b1eb5dc4ab1b3680ed2d998c80a9f100cc4130d48b59ab69177702d6e5e1967","metro-cache/88/a4f80e39aa8c930a4f5b0b0540a2b51e12815a99b322585dcd6706c7755bfb65740e95","metro-cache/d7/7eadb4813ea68d3ad698da87b59091b89134217fe238870106345de7392d1c744de657","metro-cache/d4/2c2f88a9e3fc4ab94d1ccb9aeb0927712fe76652c54b16073639b692157b72aca82365","metro-cache/8f/5da3f48b191aacf69d81b3bdb721606a690ae46349de8416a47618ee7515aa1a94f691","metro-cache/78/6d484fd8824184b4b1e86e5782c3ed1a9f6c7c00e701808f7fde4d793678cd3ff78698","metro-cache/34/be05c88776a1b2dcace166b56de28abb39bd39f3cd118e9bb1c9e64943342078279acd","metro-cache/52/0048c17e50ae25f310ad1ee4ca1b325bf9b2f26ac8c7e9322d78f24654f740004c4bec","metro-cache/1d/e17dac7f14b03d6c8dcb7b9f21bda51f250fdb6266784d7ddeb820ddd2aa9cb3f77f14","metro-cache/56/c4a16f36f2332fbddd1b1d6a6fb9cb2af1ba1179f234fdc5f63f35673e1aea50ea12da","metro-cache/ea/209ff0cd4956ff1cf202a3330b26bcc6dda4aae86a1f8cf891348b5aa9366433bd2d16","metro-cache/72/b87307702723d60484b86ee1754417b62708d4c6d57aea76ff87c570f5adf2541f78b0","metro-cache/86/2f1b4ac1f880a4c65f002165dffc45069889c43a8bfa5b6c58c9fcd2d6bc8c140ed3a1","metro-cache/a8/e7fca8fa717df92babc2a0568a2476d473a0789f6b9c420e8f83066c3b963b3c255a46","metro-cache/65/b265e5d9757622350bc9a64f4ad2421649e7f4b1a6a2663321f53e1f7e519c9181419d","metro-cache/4a/5765a4c1038692e987ba0c6daede38247ed197d4eb104e3a683137ceaadd7870aa1643","metro-cache/d7/ddb8fdbc433083fe81fa4353cb5df305a7c240489f860d19ab12b40e2d7b0ae14e7ca3","metro-cache/d4/144d83290270806fd0e066ae7f85cfac7cb26fb90c73e22b21a13a0f9b1e176bcec8c3","metro-cache/87/108211f3b8df8c853f201705fbf1e837f6c722970f2d75770af1a253b0208b2281e785","metro-cache/76/5d4c430a98cb2b7a4629d7c13885e4518861b6f01151da7c723cf0cb7b4251ab94f10c","metro-cache/da/a383663ecd0ccb21adcd374ffddb2a8713bee9a99d13df38b7d361f85c4835cb736395","metro-cache/15/d2f9550d8b0aca0d6ae6c309303523ebbffb7d7ea5362a22bfa1bab0bfdeb1617cd610","metro-cache/7a/179c7698f67424d1f0190b325dc4f4fbbb442c138a43c4f8063ba8e7738ea5c56b6e40","metro-cache/af/03996feff6199cfd615bb81494e0f43a9391b51f78e36447ec8272750a76cde6f98a3f","metro-cache/b7/f8eddec8fcf67976fcf3bef53138a4b247d31878019770ffab182f3abaf6e6bf67b3f7","metro-cache/61/a7da34911e9dd131226adb6b5b557e1ba5f29e239a2f1ea910a6ce5ffc0d0ebcae0dff","metro-cache/f5/5a1799c6433130284983658b0770649a171049a9ba63e445f8beb5450b04cc28335ab5","metro-cache/bd/718a02af093d64d21f24aa5a484c78b4cc5542e8dc759eef46581b3ef2bc93c0c41e33","metro-cache/0c/b16789981f00584286fbaaceffd368e75693be686632d510e3f87437e8da311e69f1a9","metro-cache/58/2ea5bbf91e0d167c2284f2223fe70c15e4444e2a60dbcd90632696dc9437ce82a4893e","metro-cache/0d/90058dd4221aa6814c1ef19adbdbc3fca640925d9dbf6518ded4b3b67979657c40a213","metro-cache/9c/2d85bbc9d4d6d1895c990b45d191be282ec2659a9e22f2db4935d4b661303bc2385dad","metro-cache/35/63f1bdbf2ae78d0bcdbd3fb8a1ecbccf0c732a404205a6fec8dedf90d8b7689406f2c8","metro-cache/f0/111fbb4ea4a19f40d0e9d873fc82b6b3370ab3ad06eb4d40cb26bc25f2058347b3b657","metro-cache/dd/7941684dd71e219a906005b8075245d312090485ff42b9d51700f89d5a6bfe046708d3","metro-cache/91/1aa1ab4c7bb54a47449cc0adccb8a42d839eedc64f881337e1197b89f84c16ac9f8c57","metro-cache/c4/e75ac58e6ee6c6bc0b5a9265413cf526b3ce80381d0759035e57453f4af277721104f4","metro-cache/c0/21db256633aeaafece57d62b4edf52ef45331ecc610913e7c49b92441e6bd0996efc19","metro-cache/a3/95d289e5d264d59da94691d3e0c4da7c06b978f881db1d05aeb16168d4468a69812cb2","metro-cache/76/5afb2f83e41f334b3c21b3a45605fc342289bd48a9b031b856ccca15c102b1240bc397","metro-cache/82/75e8db25ea68467d946786fb161f2964b88f1fe85d4448e403fbd70618ff4255c727e5","metro-cache/fa/5370f536426bce1b7021917b906f14aab0e4ad6cbef1d5d99846ef65ec5d402596ceb3","metro-cache/59/548871396103e8bcbae4f0a0056d49e7aec5afedc37faa43e782de82b9f897a63bdd46","metro-cache/4b/0c365d4050518538202eddbf99451aff104092ea95f93204cd2db9cd56076f2dffd63c","metro-cache/db/5e07b9c0496e38b12589f08b55cab5e9e7baf1c749c5a507ccc03f18f49f6e8ae85057","metro-cache/6f/22d983e1ffd04a49a7cdd3d13a7618e0d81e73d78b8260396c6fd119237a75247f7aea","metro-cache/e2/6b0cffa91184b9adcc589e89bcf24f762a84b00d1e5b0ee9a3a751f5c7b51602ee2901","metro-cache/7a/7b26b4e80b8d4260cd4cd14e4320822650aff1137bb94b951d27bf70b067b56bcc757e","metro-cache/e6/eae46b41d2387e1515b20377180896119f69c076882072ce138671c26f887ce245eaf1","metro-cache/92/e93462daa2fd7e3acff2d4fd37f0e1d8cfd20360f7fea399f59f90698d148185d7a698","metro-cache/5e/408c44923d2e1c3c7b9b0fd51c7a0728918f046cd525c7e98ef26be572f740ae807b32","metro-cache/58/49a365428a3eb87f8f8fddf9e2e9ece05c4b31aca8fbb79f5b30c3c83c13f3fec04539","metro-cache/b2/b03ff5e671b9a8e5e7d9e4512a7f8b2f5b0c5a70d1f114e37a4314252048210f24e5c5","metro-cache/b5/c8eab7e802a14ed3957a22cbc1598d122c03e88e00d8e63c14bee0fdf3152382a43013","metro-cache/13/92137f3d5ceef1a4066e6ebc50c379df365463d77f6c9ad572ba3bb3f064a7181f59d6","metro-cache/5c/2ef44455dea2019b838966a8a42453b878f55d43f619a18fed2bca450e40ddfe689ed8","metro-cache/1a/17d5502ce4a24ba7fb8416d01fb64caafc862e0cff951a6cb49f8865696d8fc629323e","metro-cache/d7/88e5e82a9b2e908c02b2d6b9ba127b4cab9a942ed28316fa3d72458fdea45183761919","metro-cache/6b/c644387a99328ce8f1888662b55b2cf1f963a3d32f0153ea3f617be662bff48056fa2e","metro-cache/1e/9c1961e38a0e6a13ed0501e79b55a69a713ab28892a259ced48e1b5eac242915295a9d","metro-cache/59/3912395227e623c11ece36e4b28fab0fff4043961cf0b2af8cabf233b353cc836cb8f9","metro-cache/03/c5df93f7c254369ff07c1751c0fd972ee803fcd0e34fe91423bf921dfcc9e1ec75a891","metro-cache/52/bf48081655201c96fe2bb0a48a58a9806f6df747c5e755d793843d0ddd2be0158d889c","metro-cache/b0/5d9dc9caa7b6cfe62465eb02275fec987c79e16dee569be53e6f3a984bc07642e3b355","metro-cache/0b/06a9ccb2ee19f8712652fb482e9add77901c6a0256ba334b2148f278cfc58e65d23c9d","metro-cache/73/865b2502fe14254e73ae43f5a67299ec5b4ba015ed158a7e52c2d366d35813e12bd726","metro-cache/27/9e003b5f70b6636cf81ebabd8a4058573b1be49a9ae690f02ebc95369c01c99799a29d","metro-cache/bb/617fc12dba5978d591a9fcc1c79cbcb47ed17946947771e1e4409766ff2865eb381f28","metro-cache/d2/035dece083349a131460cb4e57d00ab65ac9df80b6b59b752e178d960a87550cc1d8f9","metro-cache/02/cdcccec74d8989693cc0a8849acb37478b5a1d48f4a998b0226e1863837dbb282a5912","metro-cache/c8/51a3ce40a6d352cd69fae7cc010ba5df792feea90bfa00cf20fca936722de61d275ae2","metro-cache/3b/bf6c2bcbcf4fe17b9353067c270dd46e3aba528884e16f093c8fc08dc72d87b7380f85","metro-cache/01/3453a84b52d9c706cfdec416917c4ceb75635a6770e1f672f48af7dfb30af4cef67257","metro-cache/11/6b700f5dcf01a3493a7cb60d742cde5f02c0ee5d943c87db70503b449331109468a070","metro-cache/81/74587b35777277711b191eeffc6672007f97e34c69fb444dda8ce6c935516eca0ac38b","metro-cache/d5/7bf6ddc09bf3f1c1acc76399565dd56d6b70b54964431aefd2f163f6f85568fdbb2e8a","metro-cache/7a/bc6919df59b4c89a380c5cfb6c388d88ae3a69fa5d54e964e9c7bd9c3bce94f3ec1f5a","metro-cache/93/a65c8cdcc338e431148458b93129ca4dbbb78f0ff3ba425eab2e9c44b2abcb135cb5fa","metro-cache/34/3c210b0b7c8330aae93bd795acdfa6af6c91997f70ad4b900a50863a72c7d2e30e182c","metro-cache/de/1bd126ebab29b2d3e06d83c41cd3970073c92e49db2420d5a96a01b8dc2c0c3d23ee29","metro-cache/cd/7b8852008cabe73ad0aa4f87315991c66fe058484c8f9ade7740fe5cf1f314d5e3e736","metro-cache/85/619815d8b1e8d9d946973df1f27c53fda36387d976578d59b6b1742ff8578151d3ded5","metro-cache/a6/5907ac156e77a27122ca806a8db39293ab8f248b30c5076bae33d6530e6dfe3eca97e4","metro-cache/0d/dfcbd0f5e8fbacd79fa1f88a5e244ef25239db610218d0b3c17258a838acd250e8580e","metro-cache/48/2bb9b148736074ab402dd97be4dfd53e518a8d2bdfe2f7a6f6f1a57c2500574c8ef03c","metro-cache/11/f8621f4883799213ce67e0f107aa66d6223644174d5e121765fcb9f68546610e06bd0e","metro-cache/b6/c8dce4b7568b47936ce1a384fcbfae8aaa744cbbf4ac9d897d4ff1948c7812b4861f20","metro-cache/34/05a6f13c1ab9a138c8f25a95d825107d22c7aca1a3512c4576ca2e8efa76099505e30d","metro-cache/e9/01396787ccb3e63059f69f117ce5319983050e7a9362a1726519f611118eda66e9a58f","metro-cache/3f/f32e83895279b3e3b1b37c17474b8cb1a7021f0fce85b7cf3ace34803023099fb0979e","metro-cache/79/1e1704e15aeedc4e366d6ff34aef21d7b22afcb1f79198baa9f81fbe5d1e2d0ad6fbee","metro-cache/73/cc64a151663c7dc1564ed5447ae384813ef3c50570f208f95d0d3d5cb467df33324f4e","metro-cache/99/bc202578f2aec25edd46055437f8c5bdae81d63b86987532783062e9f0e52cd510337e","metro-cache/e5/ad68e8e596c4dad88c0e9e293baf3e6f89f0a0a7ce597288a7a6d4996687cbe8868fcb","metro-cache/3c/fa46286f7124807bba62bcd5fa2c51796af6597de85e5bd441222fe327ff1e182c805f","metro-cache/f6/468dc25d7680c73defe9778f743de821166423b991b3b2ac7ecc153fd7f766723431cc","metro-cache/82/205a9b61c88c1394389f205627a60385f4a727c53a3a941f13e5c1d4a5b5621837018d","metro-cache/c3/0a5e48e3c5a9b0a5a9b6d65727e81654122fce1116f43a4179acc71c67dfb7cc3b1244","metro-cache/e5/006a41d01e15c93b1853dc5931d8bdb8d9bfbf3de18a0d8d3732113e2a03cdba50b878","metro-cache/0f/e4570a447a892908cf2f8132000549c60988a46e7c6d88d60c36d9f8a37c87d31fa0ee","metro-cache/c0/23c366fc364954b69e11d75307d1532ec3506a357603ccaa7af39b9773f40a0cfc9845","metro-cache/e2/cdbfc1fecbed542f7ae7867c47ac667a444693ba27f2f07348961ad309d6285ba5b06c","metro-cache/4e/b7910147dd4274d718a328e61d4c3983f1244b88c620799ebb6302909434bdd31790cf","metro-cache/03/5d732c87804b0099421691b7ec7f82a7751758a1a2c4a6748435819e9748e42366cfa1","metro-cache/d8/e7e7344a2ebcb72e33ea7cce4d94737251473b1afcf9e24e5bdb75f17451914e3e8f32","metro-cache/66/1d676f2f89a370b8aa76a4cf182f5bb6b52bed7a0fabd01ceb776c0888153b4f6692c1","metro-cache/b9/d07aa9443dedfe8968706764d0076313b4f75ed3abbcfa3eb99daabddab07f0c81638b","metro-cache/42/c8d9001ac7c019b8927ade7b2fa35c8f0a98aaca1dbec87ed1be85b3c998501f89499d","metro-cache/de/cf625c0fc58d102e476caa3075217856aa928ca7d3b73fc583cd7a79073b5b09a53c55","metro-cache/bd/6ef3869139ff7d71e173f00067eb4f49a551419880e4be1b1a412de9882ba700628faa","metro-cache/d8/2824fa75f2897398aa9875963ddb3f936d8aa4a41bd45eee9f80d44de41ce973abe126","metro-cache/1e/2dd630afa1e7cd0c342470288f5f27373b6d92adebc6d2a0e22055f529b68c5d3b3e12","metro-cache/cd/185621f2aae046a8bc38039e4cfb19ca42618e8499f827e0e1a9e94edf692c04389e33","metro-cache/12/a97ed6cb09c1df74f87d7fe1a5f8019924133330792cf741214234c0a0c61781d654cb","metro-cache/c4/0330ef754a0f16ec0919b993bb5a0b7fb22fa4fe4be8c1f2f6d1ef6ab0ad2a4142dffb","metro-cache/f7/0330c462f3888f3c33b97a5820f8dc999d4c50a7e18c476f2c9039d029b526efc48ce4","metro-cache/8f/3cf30e29f25fb814da3fb3e065dbe277be619889c6618eb3718e684f72c6923ebac8e7","metro-cache/31/b7215609a31a186249cdd4c6d1ce393fb88d8d9c4bebe8607592ebc42aa6e525f3f9d9","metro-cache/3b/27cd3bdbfc5310c16106cd737611bf29d3d73bb6721d213428ce01afa43feff4b46740","metro-cache/73/aff9c18275ff815bee8f94aa9cfa0cd56186942c15afb5129ceae965284f34d71b9529","metro-cache/8b/faec3954712eb43f0fe51d15baf07a635c5ca7ed927ddc641c34908ec0afad51831635","metro-cache/f0/9bde564509b8b42fbb83d2ccd866ba017369c0ec9026b22b832f9fe545403d2e91850c","metro-cache/74/9c36da5cae8977ac4c6f8760efda2783aad00428736b1f34690d7e2290a6d8c791ad69","metro-cache/72/c4023d95bd076171edf4ca4252a283a82b157d29daa04f0fca078943b15657f225e112","metro-cache/18/be58c31ac62690c690dbf44d3267bd114968b158b993f59738df254154d66ac4685c10","metro-cache/3f/c7e547abb1652be90b7dd2ab51f5842069342183702206986a78d8349a4cee35e3d7a6","metro-cache/61/da3e92f1a47a937b931be9c5cb77eff6d0d7c0ee5ce97f3ef308b0f3ecd34b39419fcb","metro-cache/8f/c81d745636e29658211f23a88a32232179a7f92cb5dcc5e2c2ae103fc60a83bd75c794","metro-cache/f7/a7b1ea4488d43f5b75447ab1de3c402d7ce0ee55cca453cd96116e1ef3605a4b337b91","metro-cache/94/832a849d178289df4d52c4c0915d303694be15a782252bae9482a48f5acf6d140a6607","metro-cache/82/d8db5baa32fe9d914b037856cae1ab36c03d84b256d770f998934695381d16c223440e","metro-cache/e0/26c7584df4c302cedabf5824114bd27cc2245b9f478a9525455b59e263279fc7bf3ef1","metro-cache/aa/00f1f91559db642a124064cce15ec40183245229ea5d2b0ae239060d0be95a7ceaafaf","metro-cache/f0/2e987f40129786b229e670e1e167630dccc3b8420bbb8c2176ecd065cd6ba63193e301","metro-cache/15/78049477e0cd336b1bb26190e086e7bce2fd093a35bb22db772e551945679472dc1475","metro-cache/ec/9955eac293f90f4ba24a6198242419930890a517f1438dbdbee0686f85a649e8e998fa","metro-cache/4d/0d17e377e297989e1d5369b8add92e97dfd22ddd89c572448f2ff46d67b448d9104bf2","metro-cache/24/7b976c28c92977028a1f06d83410b9758b4ec082436fa3034910906be9980e2c68526a","metro-cache/cf/c6e46d244b966aafd0ef6a677fdebc2183f6e705776434eca3f41ccc1d897f208c6023","metro-cache/a8/894310263b41062039d6647fb51c55be12de0fc7cc6de53bf8705bda40df31b31ad7ff","metro-cache/a8/218ac0597a3ccdb36ff90794bd4768d0c14a92755b1969b8586207f4b1c9f58dd37026","metro-cache/e8/d1c3b9bdd27ef91827e6a6e955275c7a41142dbc97785473d7f05233cbde5e37e3726a","metro-cache/07/98049e21f43b432e79d277f333a8ec85082fd904bf6c7ac9886a3d9ea977dd9b30776b","metro-cache/d0/2e6bc6c1a1f6db69bc33f761f256c1de7cbd7c2ecba7515323293a1771afb627fb1d22","metro-cache/20/ac77241c58509fd96c40b127646feac07210e9d82761ad449a3cf680e9384b4505f83d","metro-cache/9f/9d2f4972ea277952696bc57f0b6cd9f8f92a7c06be652d2ab85fea3788113ebf3ab825","metro-cache/13/8bc9e8416ba19f761627e782355f6497bc07263a39dd3b1f62a6fc18326cc3bfc13e99","metro-cache/c7/8602939ad787725c2527a377a84504d62b104681a25708d46ea3845ced9286b3a8d1fd","metro-cache/57/069bfcadeb56c8dad9f4c99ed092b43c870ec8af8f75eba32fbffb23aaf51ebb0813b2","metro-cache/73/58f2975ddddc016d0a340b0584f2c1bfae71c9eee9ef60d32261ba6e99dbf11ed667c2","metro-cache/e2/5c5f6570938011c359dfb2f9528f959be3ff43b53eabeb6cd1c2512fbc2fe3c0afd610","metro-cache/a3/67537995910fe91f36b107e26a2cd95a4f50702c5df82771cd8b141e3cd10c7ec82d44","metro-cache/27/cb24f0e455b8efe296a18558896f7d65e0c706e5443d06322dda710015a2276e07ec31","metro-cache/2e/1b78ce8ccf5e55d9ad025f03a0585e433f55498ceca52d104d994fbc18dcfdca12d59a","metro-cache/10/e8e2e81ecd15ab85317d06ace50343f4b20fda373be61d0d0a931747cacd4db6c340de","metro-cache/71/6fa3eb2a716f1c334a62e8687be3aa6daa062b72c32cadb9b6a462fb00339a866748d0","metro-cache/a7/9effa014da65cf283ef035249534e41f1c7eab8b1852c21fb76cff5fe6d6ed73fe7262","metro-cache/b3/564c951212bb77fa44cfe70a8b3959e4705af170f7962e665e064b5a944f343812c167","metro-cache/86/5a9533e418ac1f8915aee35ab94a1f16f40c7ed3bc23c98584dad61da3663dd49bb4e9","metro-cache/2b/90ddb3c1bba650436ab64d6919a96791aef271ae771be40c5e6f3776d931b445f00b32","metro-cache/0c/3d1ed6ea10e8db10e82a79f3ee1076fbe728dccd6875db85c21a0cfbc6ea9803706d8a","metro-cache/84/6874ddc679a784eedc8dd6de264b11ac638cb5fc71d1e87f1f331ed139a0d69507a05a","metro-cache/8e/73b091ebae29edc6038b0f499d29fdbdf1041b6d2fd00a47a281adc882c6ec3ec400c7","metro-cache/51/976408b0b7aced73828292bc076d07e22e6ec5a95799a6781f44ca27dae4809fad9730","metro-cache/0e/0eb0e490703730d8070db9b879603de87a23a19db11f49091c557c3a17fab09adb79ac","metro-cache/8f/a487fa04571b02e6e89c99ce13f4540b8a49f80adcc8f1ae5615ad23f158114f5dd0c5","metro-cache/94/c93f25f5f86bcdfa5dbd68da3900bd873e27ee9bc9639dafe54395269d4e2c3c334a33","metro-cache/7a/11a6cbc397a035a9a878c1a65a254736f1614e45818df767cd80914978a14682da8d1c","metro-cache/f3/20e0af4707340f60e5179dc1df21a3df351095eba7d70f1314550fa29c26fc32c33d7a","metro-cache/b5/5f031b7fd3467199d8fb4334dc02e06beb72343aba284212d6c8785eea07f900a4a497","metro-cache/84/56733130a840c56b0fa509d9640d9fd61ad829abb78d5cbc55f7f5440be6fc22775285","metro-cache/65/6de9ad6639a6f8635f723bfe429820f7941e635273fdaadee183074c8c10bda4d722d1","metro-cache/80/c71dfe60b6b5d302a901eb508ae33dcaa830861b5f5bc3d43386db6847c270fffcbd86","metro-cache/98/449a7c2379ed3e721aa55b8dc6230572e0a8e72c9e3c916f6f91e1ea6acf39896ddce4","metro-cache/af/1dd55a01b3a31273f59c6d4e6157b073506d0afa0b9aafb4c23146474268ebe88d24be","metro-cache/61/0497d94c9e6d661bb9318d432e569fa39e9d2914ff3eddc8423a98333943601e7067ac","metro-cache/fa/8d7ce9e3f315ea25c4041326efaa714b8ad4bc2607cbcd25997d27603d2909f93779ec","metro-cache/cf/6e775327e10396fee9310d882828fd36c6fa32eff0c9d2e51158c057983cf63b05be10","metro-cache/95/3bc8cf342f0046379f2862299df1b7e12bad8ec84a1f7bbafcc707fc7fd8fa5fd2e543","metro-cache/32/d2d9a5e642e00c64efa63aa8894345a423e916c36581a3a6bffac1fbdf85552408bb84","metro-cache/8d/dcea02b4595b9f26eec3297e7e1c1c1ab948d47f4790d9ca9e4785f38a2fcd0ef5455c","metro-cache/e4/71278802de78a28da9ad08e3a8de171861169d65412ead375b2091b5ab4c3128e7b78f","metro-cache/f4/c713b77df0322b441b14516cea2aaa9c7ec361f53e53b0ec0f3900453aa701c29a3757","metro-cache/4b/0246ab0f0c67453f9c0c30d1d9a92345cafb794ef0fb26ecb825cdd619d2f5ccfa9f76","metro-cache/43/6522f5fb734898802a78015508e7ea6681b712baa6c0c2651fac5791097677c310d5ec","metro-cache/cc/1af54d719c9a1ab0c3b8b748a779cee1eeb88b1450edef4841169bc4083b86bee235b7","metro-cache/20/7f3eecafe7e67ae85ef570ff4df8ddd5681ffbb2b1631e94e8319f6d36655d410acb9a","metro-cache/3b/2a4442506f70b97cf5166904aed07e04ebbca03d0da20af72497cbff4f641339921d9f","metro-cache/90/1863829453c9bcdde8536146def3ed85d321b6e2711a065e65d9035531824fc1ad3b91","metro-cache/63/cb575d4bf8accec99afbf4ec2e0d09010f6162aa1143ede3f26d228a5cd0841c7bcc04","metro-cache/94/46b05e486da960fb35ceeb44c9c358ff2c32b5580f8b237ef68a76816d896835350da6","metro-cache/72/3c60f8ae78bd79a8fc8d9651c8af4a1f3a30ccfc0cb31377630ed0a64fa65bb5d9a912","metro-cache/38/d96cb38102a12d08bf3b6b8a1606eef5548c11f158d9cf1a4eac0e23525b926a1641b3","metro-cache/5b/3b3bffc224d91cce3bf66eb76a6e416c6120186b77d7c3174d6da19b415e317c10e68f","metro-cache/d1/9b5d021f80d67e690133965fa71d47dd8cc3c3a16192d522811d32d0b4f9dc1e99191a","metro-cache/ab/575a2394becf4123286b1bd829927230ab5186932ad36dd40e6877f4cb94bd4c2c3d9e","metro-cache/13/42214c623b2acc1b31f4b1f10571bd0aa99f269c717645c28adb5ef73d7eab177abd0f","metro-cache/66/1b14ec0bbc0b3dedb0e2122170766814c2beadfce9537a74ea0a60bffcab2059e723a5","metro-cache/f3/29e4201749fc1fc67be08258833a1ec137370cb447bb5bc56ff2430c2ab7b0cd6dabaf","metro-cache/eb/5bb04fe0f566e797a06758fe3a66f535cf2c6bde4ee484907a5c5da0dcd06c409501e1","metro-cache/a1/82310a16d24589c701f743a0b5899d923aacf6f2193185a9a1be459808f07de93b4015","metro-cache/1a/3cd556753c076b70d4ef88c4684070e981b1a5721b7676e3994a77f5b84e6b82311ee4","metro-cache/e6/10455278033369cc18311250faa551a05aaba7fa837594b8d939b6c47b5cef148a1773","metro-cache/db/a2917a26537ea26183248fa2bb269759e372108e8c988cc6f86c5a388eb45589cd1e9b","metro-cache/cb/8a01b6dcf544fc6b2b844b83fb91e8fb75abffb826f3a25d448f7db29878fc45dae58f","metro-cache/1d/b9255ecdc28d590d9b759404b50f005931ab9fb33b0b3e80b53404fcc92f96f0ccafa7","metro-cache/e9/3da63cf8567725a822b36318c140b32ca95c28ce58b472356d87072bac9555c6fa8d18","metro-cache/50/cebabd75dd1a3856296f0ba1a181220b06b32a307cdace45e3558233d80ba80c31f8d8","metro-cache/b7/a608444860da60388f0c1d473653bf5f97ad6caab7f3ba3ad599b9541e9570a9f1b2f6","metro-cache/96/1237063d6077edc5e0824cd5f76c577d0c7de3fa268c50b43e063e0f43c3d03cec7035","metro-cache/96/4d1d9afbd2b9813e75c21f14e7a983624b5607f58a1a84f47ad6d5d5e710217beeb23e","metro-cache/8b/74bd7e972e57e0f0fce56e3146e7b5aa03424ca430683debc283846cea2de2a2130ab1","metro-cache/2d/c4e707d808fe31775f38c0c181dd6bf595e677b47467358bd102975ba07adf3692f6e2","metro-cache/6b/2ea0ac032f738351a84de370a6f9aeada016ac5c6de753b8e071e65de3b772d947f326","metro-cache/32/bbb85523fff3626f1fac16b1084dd11e6a01cc6fd8e63809150af1008cd0c16bfa1fc5","metro-cache/75/18c79c7fc7566866970957e0c772c60cb972b8ab6d9702c1b855abac4ceb1b29ba300c","metro-cache/18/2ea770d8784d69747e6906f3e5040a16250322262885f40e4870e7c1b91cde633acd99","metro-cache/01/017a9291976ca35fd6555f46f614279cced3145a5cc3da5b69acbf54c476c59d7a7614","metro-cache/cc/2a8c6127551c44e7b7d743303a9abb99892d257ac6a4eb62c001ccdbc4d9adeeba0d6c","metro-cache/8b/97f7ef1e59609a991e0a7efb5a8560ac48478fb856a42103e178353f3bec8478d19632","metro-cache/80/1510bbc7ddcc0f95f46662a6147b8d38cd316286bd2cc8eddeda05fc619c6d55135c71","metro-cache/7c/9c1448e5dd363fb4f1290e1dd14d4952b6a78ef1d020346810fcc12d4f9b949aa53612","metro-cache/a3/71bca7c1c459a0471c203b025e573dcb5c432905b3a2b4d38c0c56143cbf311fec3e3b","metro-cache/b2/a8c4334dd8311dddf59ca53c47eaa1c9b0cc70c586ab3300be5722fccef224af5cfaff","metro-cache/58/e51599f428663dbe6401a792cf016c84c8475596a73be599ad81fa4c87171116ed626c","metro-cache/fc/2c011ae0efa7cffa77579a45d1359457984ed897edb2419fc7111072c7da472fb8d61f","metro-cache/e8/c0f724ddcab4facf4effebdb035b27a41d9041a832c5c180eee33411a3cbf6a523d28e","metro-cache/ce/2bb21823864c91c452f9edddc8970a79e980c0c622c34a409a8e58b18377b2dbcb3ec1","metro-cache/7f/0548a211dde20baf3f0af999e101a93e3ae8154624de76f2ab40059b33fb8b89f1af30","metro-cache/9d/d932dfce282262fbf511ea6e8f5ff11c735eb15f9c359ade385a19035a71ed3fa1cc0d","metro-cache/6f/444e7cf9e666ad7ed5815189b16314d3b5690b391a632d6949f5a1b12b8492bc2cbd58","metro-cache/ad/86f039c45edce486f7c2e680bd1675b9c858c990a5b4d4021233d3dccb448e19496279","metro-cache/94/b21421462e72e2f039ae93df3c5cacfd245a9ce091634c7683b4d5f8dc8ffe2006ab70","metro-cache/06/38ea39ce25371de33fd712d826bf10f31cac43cabfbcc1b8a43c36d2c8b4716a2e12f2","metro-cache/46/a6784217641b5056b3703e34f016fd1c9111c853bdfb5ac91ea98a498f24528c7857f4","metro-cache/ac/48adc902ddbbd04a6d48f6749a9261c70aa438ab667bce2403137a9926cf4d1455be83","metro-cache/3b/179eac5fe86f579c792907dd5045f94649aa57e9b8691e3b7e5d04c3f072f7b57ee720","metro-cache/ec/6c0b0f10722e94a599f0f694c3345fb48bcb9c032c3f29c0940cb953036b3ce17436dc","metro-cache/58/6a8c597adc2d3e4d1b13a772090b195d498f6d309840702770f388d7c8e6857045eb75","metro-cache/2c/36d05aa1412db7614c61b9c37b6488217b4566649cc609059cac43f20dc6b16fbe463e","metro-cache/59/abf6e1392c5f3a5fb0863b14ac0a0897a77d9b24d2a4dc73054244131a28fc0c199307","metro-cache/f3/5e041b93140e49714c44829842c14757e19b79c4d27b020376f4f1c2cf231c272bc6d2","metro-cache/d8/e136ed9b6fa4816eb7033af920762f928caa05531ab7ca69828bd42d1db813ee14c789","metro-cache/b7/96379e7cd155aaee8120358906820644f568d8522a9c49467713f05c327ab22512a170","metro-cache/0e/86269df5499722696e6cc0ab08fbca53a60a35d3d26e41a9dc5cd7bd12edb1e73e8fbd","metro-cache/3e/afe126d26e26fbafa7754b27f39f01a45491583ac404fa6b56b1dc65d1cb16ac41981a","metro-cache/9d/eade21170f461e2fa4dc60abfc00fdaba24cd354198406a6df70f284fdfc3cf43d4aaf","metro-cache/fb/588f91c5e96a2757d51f02c3cb516049db8e30e21b1499acfae826171d4992de43c0ca","metro-cache/d8/1de43dc6c67d4ec4cbcccdb3e09047a14ea153eca771e966db15c7efb905038985eec1","metro-cache/ea/ff9392d37f5bcba3161249dae3709aa23625358f737e4ede8befe6f413ba3b133da300","metro-cache/6f/bb945a95de4f005e0b291d6249a3dc40a1bcc83d64ec96ec94bef386a9af6e126d12b4","metro-cache/86/52fda23c8d76b776f49c84d575d00b5f062987a1173abda485ca7d20ff7512362eae77","metro-cache/2b/07be3249f9d0c09add23eb07b55ec32503a635759cdfd2c7de1f64e457030abda501a1","metro-cache/22/bf471bac903b355ad0ae2b3e08d6712a1bb0a208c4f094fe5184d09c35e7e68898971d","metro-cache/da/c5e5149ecad95e4e3f96b4168e71081d16fd0666e9e1cd7473ea8ac33c2680030386e2","metro-cache/aa/0a9740dca73006903cdb3ceb80243f3662f74890ecffb667cb41207b1ad9bc8d6c8352","metro-cache/28/643b776f4fdb268b7dbdd42a330d9e310d8e36b5f2495bd743fa49e3e2097418b88ec8","metro-cache/f5/fda65eb8d765c3de2181766f77a7b61099a4d42520a454d8dbd648ca9f829acd871789","metro-cache/47/6f9a36d423905dc58d0f1d2def951adb20105dc898fa8aa6706492502431c680c0dc94","metro-cache/3c/7bfec6398f36dc2b7508fbc501b17f6af240c1bc8acb76ce34b5aec83c7638082540f4","metro-cache/28/c753f5753aa00f7dac0d51710fab2e01005d1f2b540c4df7e02fb7ac06c60ba3bae371","metro-cache/73/71a0a27e531ab1eece04dbb1ed8e4d71f1cbc572efc7d939433d593f0ce11ed8cc1b7f","metro-cache/39/9c24e1a05ab10b3305a3d415fe526dc6faec724ef47d02ff303772e373c40c9f7f10fc","metro-cache/16/1e8751df11296ef715058f6bdb09fc3e2a7209b065f05777b03502bf2f7336fdb04867","metro-cache/36/0eb2bf650d249827dbaf2c3bd2819203c48b9812f560fd746e06d2245838655e9b0b46","metro-cache/a4/73a95e4d3aee2aaf39bd709a02708982de718a0fa27b93ffd661a58f8b57afd9766f78","metro-cache/33/eaa5bf68cc4f33ef2465e480b684041154567c1ad173a482174e8966f3a54ed7d8f0bd","metro-cache/30/0bfa566ec701fd2268a4356cc1f9c87669e8b84b6c0c87b4944194361b204565ff78cc","metro-cache/f4/ce129f3c2321c27df8cfeed726d1ae9e43b50620d590b1ea1d9e4ab80b30e76ffbb735","metro-cache/a7/32888ae84ae8d27231f6d0951af4d48138b507e70a990e3406a37b1397d706bc9f9f2e","metro-cache/25/1a6feff485b3a4582d807c8905a865e6081ae418c86b9a7c298db80af512a8462fb034","metro-cache/6d/9fc7d3e47695da96561a050d2aea8ebe12f6b17e29a65b2f8835d1326a7524ff194314","metro-cache/56/2bfd54e6abc8d50b8d6e0efe625c0add853347880b9db0997d648fd540252e1f8ccbc6","metro-cache/c5/9289b711ce6c423b18dcc35c6be8c6da242f209bc7d04f85064865a7b6ef1e3e378804","metro-cache/88/d3ee443e0f002720719ff1de495865dc93c48ff2a4546ef3df4d535a3ace35a66a9d51","metro-cache/1d/1e2fa2d4105e3c2992f5a090c146529b0045ff2c599913457121d988c0251211b905c3","metro-cache/84/df73ef8a5e0ebe7fe09e879ccca3d98fed13025b5f6e2cd40fe23badee7665177af59e","metro-cache/44/d6df74a88d0e0041657397137dbf90522bf3d51586b9ee7315d95017664d706fab22ad","metro-cache/b3/8827c8e91cadd50150f2ecf2f7f85e638f154744678907a9bd9c6fea52e702d23627e9","metro-cache/32/fe3e6afba5cd6a484deabac68e691d94713a985cdf03b1474e3b9f7162190a83022050","metro-cache/1c/639588c85cc598eccd7e86680021bdff76d166355052d633d555090e080ba3712e314a","metro-cache/7d/3f086f1c1889a136733ba39e01961785ddaa9fe55bef85f8970f0daac8c3098f468693","metro-cache/6e/4ca8e04ceb84f56896f456605108b67f80126916ca93ecab6896c4d4384fd2b0db2198","metro-cache/26/74a5deff7479f5703198d52bfe463750687b79a52defdbcde400e8755aeb760add4e88","metro-cache/c1/500441528f38c4b86837978b278eed47feda930790ca35148cd94d90cbf67f83dc57c9","metro-cache/98/3f7f761d88d737352b5d78044fe44d924357144ebb96855ec0cec5a9c4827b8668d259","metro-cache/6e/af8b85e97c11d6f5d2c78ec64d7629c0f5c46193377db830d25eac3333baf911f67870","metro-cache/a8/8dc92212ffa01550d29ec58abcce5fe6dc66f2ec504a4ee6bb16316ed31d1743a52838","metro-cache/eb/4e407d9fba7f2ccee2145d22f383d36694287e3f2e3bd86078aee00f31f5612f884088","metro-cache/b9/ef4b9ff65320611ff4a75781624c591b4074d6b8ed9821c9286bfcdf912b584aaeacae","metro-cache/01/1367653468a68e96da4e3b7c08435ff3d373fcef29341c63332fde70d091f95f7dc0d4","metro-cache/ed/e746705b9b51300e2203922937f4479c6364eddaf022891973f38676b1d9ac719c56bd","metro-cache/3b/0809331920bd4e9710a348fa4d1c02ef71d14b2f2c1e7669c1e2c5049c8dbbed7d4577","metro-cache/e4/f3d717b99e862b124502aba9b816c1ada42cd557e6702201e99d5e16da04480b371f0c","metro-cache/3f/785e131e8f590b20f9e5451f814919da62c3d018cb84f84d12441c816c930b3d88b51f","metro-cache/ee/db5eae006d5b2638a23c53a6e9f6c219b90a89cb84cf0a45677e2488636d00f5d5d2d5","metro-cache/b9/f0859543f5bfbf2b72c316da8833de1c46d16cf19816eef5cc9d64c7e0f84f0066f520","metro-cache/6f/78663e49f19d64b174ff86237f5507efc3756a16a4a7781db09e43be776e72259d06e1","metro-cache/db/9929084ccc6924fb1e626a4eefa3035b0cfee1a4b8fcf9bf089136695b686c64281914","metro-cache/0e/7f3a5c4c292a54dca23290baf26acfda3c0db4f2c0d582fbf1e9f0c07ae1e1619f30d0","metro-cache/c3/4b77ce6c5ead9dfabf43e532dea90df71146e2a8111eb5a7d66efe6ac5a2ad6711cbab","metro-cache/74/47881493f533ffe306815f4159b7f27466708d22fbe0a64c7e7b2e2465e84e9b7669f3","metro-cache/b7/3cab097f1a279af71f80e134986b5b4784e631e73b651615d7a40c21f28f4dac9a13ea","metro-cache/92/fe865002c5e283d9877af5d19d70364b5cdf7e761af9618053ee6b65a43d7874a8113a","metro-cache/7d/92cb7791e0777558796e4424c3332e1e877816d921329822a415ea7e1ae2bf76774d04","metro-cache/40/98509a07d0201d2e798483c47d83ca62465e56c468204fbca25e791a5ae8d58c9d5672","metro-cache/19/962b051d8b1b79d4e3efbe9191c44d0c9a3ab79811692b29b2448da8e92b828239f07c","metro-cache/32/a8a4dfdb9546555ecedfb5111a64e97e2f9c147d4e0dc25e5a8ca81cb83eb5653c4d35","metro-cache/73/bad0f0c052d5585d4fff279949793415fd49e52ec2bc900c9410fe703a1494d19f4f19","metro-cache/b0/e128f7e02bc98ca721c5a9f74c2e3963b354f3dc93ce32844834589de9c8ac510f3858","metro-cache/4c/3c45fee8c8264ab4e5f9d4ffd530e9c74196529541236fc08a54879ef96107c04cb2ef","metro-cache/c4/47819f3afc1fb5257240e7d98d795a07aaa7705d099a508d5c978bf837889d0f9e29b9","metro-cache/7c/0d273bba3268e9cacaa336abfd1b0c7e626363bb2acf593c61d5bbe76e443f58f3593e","metro-cache/70/cae48e6e1bd356119cff8b81c7c0586697c1db52c3a12d9da651161d0b58156bbba859","metro-cache/7e/7e9d971ce9a2edb7221a39254f4d63b7f00ed99d8aa81c5b33e8c1ef039c70a4d20234","metro-cache/0d/4d4382631b081ddd184e3eefb58a6addd260d91066697d34262077e2511689958163bf","metro-cache/73/eeadbc6e804e20ae419f567511c44dd91c99d60bbb89d85ee36bf22e40be8283ff8b95","metro-cache/04/9a2b5dbcf1a55c589293232d03a23ee0202e473bac8448c31984d0ead8083005bc46a4","metro-cache/da/bace2a471d8824ff7f39cfe7ac0ef4aa5f836de4c4f74fa1b06cf029da5fd368a1b52b","metro-cache/a1/dc08ed271f46c0ab26925faf77f97b5ec7c402c0c827a0d0d6912866becb91a7f20f08","metro-cache/70/d327ef3bd675a1908bde1524b140dcfa25808231bb391ea64a98a6ee30ef93fc651be3","metro-cache/ac/61d88c0fa6a0761571456dc82b63d6a5a2d8ad395ec9dbd9e21679badc501a6cbf89d2","metro-cache/e3/879a49148a823546427557a66dbec69254c382fbb46c67d53dffae70238cac70637350","metro-cache/03/498a5eeafd0ad3894d4220060340256867b2572ca8b36b716842df4ec933bfb6c7e910","metro-cache/11/e0b6dc55a2172e10e9655b1bec3b28e029531cb66f1a6afd3d77d94a55da8205560f76","metro-cache/ac/bc65e855160b9f8ef00302f50c78be878607dd34c0cf7dfd29ab83666a604096671073","metro-cache/7e/4521527d7bbf0d45757c7cb17ffa74115fd076dd6165f1dfc2d509508be6a7c5d0f9b6","metro-cache/38/3cea595353278c23f29ef8acb0cc498b6bd2d1093a111a1ac6ab91cfae798f9c62d40f","metro-cache/4b/1842ceceb5b537422d2c5a29c3184939624ad0770ecc208f6e891ebbb1903e440a7029","metro-cache/d1/16a95bc4f7e9763169f00d10d3fedbf3be96f13f5a608f645e755839dc8e0f774544d6","metro-cache/d7/1352bb15c040f3500f76c2062909fedba1c968c2a16022ea094387ce339703a14130ab","metro-cache/78/ca9af7cdce9ee5eb3290ebae6b35ddf145573a35268dc10646051aaf96b8fd1d89eedf","metro-cache/57/794ad38f04d0c2f609adc9ab56e39b6bf352455512c3cdc13e2da2fa5c83fd35b6dac2","metro-cache/ea/301e88606c7e980d0da9fddfe31e6333ab4db0ae7e64d751cc19e5870f930da753d4e1","metro-cache/70/1872bc4cc174e78ec14f08b3cd99f725279e5d7b3a59a23b0726a2c2e390eaa6145f54","metro-cache/9f/0490614da874398f3d553caa6bd8331da66973e773004b77d476770db1be05970f5c6c","metro-cache/ad/042be0a14d4d0f86da8fb0bc46ccaa3d50bfecaa1c9d9742ebdacb0f6979a535a77594","metro-cache/8f/744747a6c22afb2c2e8f294a212bf1886b6e364a156fbc3b11c09faf748946114bfcc4","metro-cache/52/cd05b10d4fc6ae35991f39de8fd55a770d4850114d08e47626847c5f17ed8b303750b1","metro-cache/a0/0e9c4f7a268131d928406758077152e3b6f983ff9ed1e4f2a76437965d501295e08057","metro-cache/cd/5fd1672d1cdc63a4b49a1b17216cd7431e1c4a20095f71c1e71d5ee92d8c6cdfa44311","metro-cache/b2/fb1bf5eb63e912f2f23c812dc166dcdffa733a378f1d04193bf1770c591463ec5e9c1f","metro-cache/79/51866c39996cd9c30de5f788d8bbdd8f31c5c1b3f5bed4721113735dabf4ce8caaf95b","metro-cache/c1/e88583908640e38f6023772d2ae32c08cfddb198966d8b6e6ca3e7dc98abcaaaa02be9","metro-cache/37/2835fe70f8cd1d2d68d672ecc95cc174059e050a74d16a38a8a46dd5a34121253dbf06","metro-cache/12/43eb4b6daa5a810eda732c10dff149aabb02936546ad78dcce7925be18e724c7d2e6ae","metro-cache/f8/dba22bb48455cc9bb7733ace88e47ccad0bb993bb27cdab79898d40e1427d1d6f5553b","metro-cache/a1/548ed51651d07218e1f2d060ab26a292607738e83d33295b4bda3f825886cc619645a6","metro-cache/af/87001c1c0ba39cba6fa128a56d73a44874e2c0ba57664ce10625ce624a411a6a5e4a7c","metro-cache/ca/2c96de175464d65c168804b62f9b6b1a695b4eedb0abe748db60d89b07e57589f3e5e1","metro-cache/a2/c7d77c9ca84cf88c16860b16e44fcdc9352be370722bd749f45b5995be744e1adc5b6c","metro-cache/88/950eef1b3c5a0ec7e80e1a3e4448bf504399e8a7a81ab317a222fb808cc4069d7e6ab2","metro-cache/13/96265468e0f4929d1ce860572b4952db79f111fca8791b02eaaa480611291aa4f1b17b","metro-cache/8e/5b10a285687964be66f3766688b34a4c272a78cfc6d9705a0255af553a58993d1696c4","metro-cache/fc/58dbaf6a2e5ea8117a1fbfeafb6e901c260b4cb5fe4b5a8b696b5178b0eddb0a7abb61","metro-cache/90/c749a3a93d3159ac4e76859e6df4aeaf471f367e1f1a943ca05ab98950c223617c312f","metro-cache/dd/0c56ce0f0b2ee7b69913cf7e887ce79386c8bebc506491b4ee7b098c26f06b1dbc5d26","metro-cache/a5/5f9fb2b7824ca4900fe58cbfffd48986e346e18aa62ddc0a90b99148a23e9151952334","metro-cache/4f/237c3fc3d330b39446a996c9a3c77c1f86eb97f0de86915326b565fa50c2fb5eb91c4c","metro-cache/68/c3f3c26f9c7d17128b03a56d16c740385224e2cc37cdd11c77ebc874a88e4f7c8b6b4f","metro-cache/d0/021d3e2d07c5226b4b7a4e91d9367929f7b67d6f00a851ec21f6ea6c06dede92703bc1","metro-cache/f6/a1283b64b2af3651df0611398bc8eaeec9127cbfc8223463339589a075efad53332aa6","metro-cache/92/2bcf574858c3f2dd15c6fcb22ae2e12a7b6860b63d618e46a578c368146e8b8c94ec45","metro-cache/88/cb500392a130f1e7e380f16a4b91f674718810957edcfd47a87f9d9f754c79e53ab500","metro-cache/b5/3628f27bcd634411190a6e8cb1c4bbd1fea14ab0229525e7438269f4be2bd8042feb44","metro-cache/e9/15bcb922dd84f13e54b0ae82265f98ff32848a40614b057ab8b055dcfb09a7daabd6f1","metro-cache/34/3056ff1cbabd99afa9f4bf4f3315b241ebd6f249556f9bdf3553e13393d663614f27f2","metro-cache/a2/1230c3823fb43b6cd3d8dabae04d92d04af1049fbc0926c94d94213fed2e7330679c53","metro-cache/79/99a1d94db78dfee080f7ee0525a920ca71d59effae6426b13dc02ec154ac266a37c9a8","metro-cache/a5/c274ecbc77fd12a785ea260796816b19446976d892fa7124b0c3574dbc5173a2c08231","metro-cache/65/8fb1b43a13454087d560eca1b9f66e51a16f1fc617a17d2546dbdad2b8670c61ea655c","metro-cache/17/52c50c046195d0e7c1bd39d4a86c40fb9ae4a280173c3d5554976cd2f7beb749d26dc5","metro-cache/de/565da312c70cc968ff8da771f548e86e9f78983b602521ff552b116daef075f6edda21","metro-cache/a3/4aa29bfb043b3f980c0fe9f87dc78642e0e197645ac6f7aac2f191def800b52feb0c9d","metro-cache/1b/fe076b93c0fbca3bafcf4c1ccaa26cb5d6647f74479f74414d7f8308c76c4c78dcf44f","metro-cache/40/59413defd9a33c20c1ceb822e9bd0ef29f87ea548c062e4e7b17df11f8cb8a9e93e350","metro-cache/ae/a5e56db11e0fcaae1af1e4f6b037c0080e396bdff3f6f009cac54e4e85231326bd7098","metro-cache/f9/23444fc00012b83dbf5fcc0ba1df0e408ac4b5012283266cfb78519db2d1f6553082dc","metro-cache/70/7733c363d53d6db74e0377893d7056fee117c6e29096618f4bb64b6c501ede4d6322b5","metro-cache/79/f25cc3a9416e72ec0eba2dcf1e7d1267ffb8c3aaec16baeafcf829546cf16e014eef64","metro-cache/bf/632a735b6f3782799be9f88b677c097e42709d12a30e2b407bbd73e125d8568bc0da8c","metro-cache/de/439fdd1bbe480dd78e825875553a6e39bd2bef758d36739b805360871478d96845a724","metro-cache/b7/d53106e45ca32a71e28f9377a5d79e79c3691018838a05069eee7d9503553048528b69","metro-cache/b6/e426fc7734f6662353519544e286532cae483e09388b77d566e6f74ac41265aa0c5d77","metro-cache/3f/5c1374f7de167ae851ee62f0c64791455dabedeac8d2e500759e8e60ac8d85ceb63ce7","metro-cache/12/6c0abadb2c54c473090eb6d54c753beb1dabc3a9974dfcabf71736640d2f0e5305a3da","metro-cache/67/14de1908e413e52a2bc1887c636c3b0660bad6704f1ec15ece242a7a15a7721ef727be","metro-cache/cc/ff34d7bb19482b11124507299aec3d82256fa61412b6d2af6cddf3c93fbb4bfa905543","metro-cache/6c/aca69bd7c9c0ab850fb42201f1878922cfdaf82ce64eb10295e9ccabf9247be4d8d47e","metro-cache/72/9638db719dbe3eca83bcec80aabc168a4769f97d7af98b2c8181c29fc738ecaa51544b","metro-cache/61/6cc7db07bcfcba6b146269bd000edd364f4d4b756ce5712be0b3b62316263ba3741ad7","metro-cache/b6/72270289c54891a908b765e2b7ea305529805ef9b0f40e3ed0d9b123c8669b725a3b02","metro-cache/a4/699606e14df1a4e2b4be24fc9549cf282532bcbd7a1a69480e247afb22f24d489d6ba7","metro-cache/f1/c87212ca8eb0da7f3a2daa7ef3fd9c3a14273c4bc030d3906d56aa43aa22e7ba6c6d7c","metro-cache/a4/8521efaeab2c12d65b9a942b7fbe00d1cbea1df65b7e296fc1a6c5d0ce516b9be7b30a","metro-cache/f1/5893319d73038f5fbeb1fdc9623bcec89d1e1fa50b942d073363e96a2eca2f9c8f00f3","metro-cache/53/738d822fdcfaed2d209423fb810785e9831ba9c8c6806de5e4c07ff31b9567daa19eb2","metro-cache/c2/c978f9ff0dcf03d7ac03eb89b62e4542a8427aece7c954f08a8677563f4fff68ad5ec2","metro-cache/9b/c980f065a4e4ff3c31e8170d1bc6bc8d4a251b5b785c0f83587aab0a68b920cccd272f","metro-cache/b0/cbe1c7c7c9febb054d3e8805b0421bcb2dff549dce6b872bf997709e25300e5bafe8ab","metro-cache/9a/5d386f33ac880c80066fb695cbc3678fe1929729619da339a1c259ba6862d6d7ada95d","metro-cache/6b/574f5eae88201d9ca19b04e0d5e9c849dcbd36cf95209331b5366df15c4aa4349f391f","metro-cache/a9/73bb4c2edbd3a3c989b6406804d7642318df9f018858302732a0d573bf4c43b73a2980","metro-cache/a2/bd5976d6f966be6ff63a5c00ef9c39ace5db6e20f9ca58701d6eb46a1b7e130963155c","metro-cache/75/913fbe3d84b681ba5c1b6220fde506101c16691ff3bfde140781722d3f8a28ef046baf","metro-cache/a9/810a6dd35abce4073495c081ec5ecc9436dd900be0b451003984d6c4f94f1fe1eaccde","metro-cache/c6/c1f5c60e1391ab154ea4f09df730bf2fee5fd5cae0ebb19f8e652b1636e9b8b5bee64f","metro-cache/4f/aeb0cd01df1100806da08519b3fe7296d3c8e8a6da3618a0b1a8da41d7bc6ededdc4bb","metro-cache/94/b7538def41654771b71712be1f36df249a437adad0d0665e06ae6343aecc7c851bf9b2","metro-cache/77/1358040a13bcb1de7d68a1366d23bbfab7f372b030aa799625d88a2f1bb7dbbcbd809b","metro-cache/73/02572229efb0278068a31f58a579de398eaa9ec2236878490ef1b463a5a324edc9bb6f","metro-cache/a8/dccaf825373c52b6d6211fffbf799d966852238bc069420a6402c27c4eee4fbba8571d","metro-cache/f9/393921f3d49e81820a7dc7c4c4d98cacda22b3f526ef71553db94b94009f30421f2657","metro-cache/02/aec10c0836fd0c764adbdd9188f5f0f0aedf0023dc9feac49f191f24da70534a31fb71","metro-cache/d2/eef9b899cb1292872fea44c860c0292b0dd63d32395893da707eb33d38fe40cb7eac06","metro-cache/23/29d9f755c668c286d92152ec83b3b51008b9da2e422063521ecb65d8c90f00991a1056","metro-cache/b7/eded85a9316c523e287066245816f90a009b9e7005b38ac40b1bf5653365f0a7ae21eb","metro-cache/7f/9db8e20ba38098772fb67e4d441dfe1c1297c032a2eefbaf9c6094508c3ca9bbcdf978","metro-cache/78/6450e4a359c1892e43d65567db3074df9a0441529d40a1a553a36647091e1e49bbf5f0","metro-cache/5c/a192356060066965b91ec4d1bd91ff8dfc25df75a5bf9f89314659ede6201a2ad12874","metro-cache/06/f3a6ec8a5c12de34873d90f312f0a6c025a73e037d012f746965c31addb73272de37d6","metro-cache/61/ef95ed6da75805da3af3f697fa66036f7208762ac05e87fed991b4ab24a35de42d2f4a","metro-cache/5b/d58d58301981725f7570bf4516e72eac61ee1b4a8e1763730e04ddd1003d4ee994add8","metro-cache/1e/6db9af91d981e3ea5a8eef1e4bca335e5d4693636eb4b2c4ce3e307f0573bd561709fc","metro-cache/ba/0ebc49e32c0ae08d3659b34181772616ac16f01ef9606ed5b714fd64299118b5afe1e0","metro-cache/0c/0105c5cecd121a475ccecc254031b12b813f77095eaa5ba54de2f1444b4dd8125fdb40","metro-cache/26/9e59aa9ab4a30b3f11adcf8b249a6f51674a8187d339507ae700149d4c3f2014082c43","metro-cache/54/7e25ec048dc74f336e30a0e04e8803e6fb8581e5bf05f66a175b4ae26c96630e9b0450","metro-cache/69/75fa48355effb29bede75189fae986006c1a5b2d7470dc0109ef4abec1eca21248584b","metro-cache/89/6858d1bc63b1aa101305777396f4c9c934b17426d91d5819f5c0f76ca0eb216d976fd5","metro-cache/c6/489fe66ae38f3e7c247541b6188bb378525553d6f0f40e1ced2708cba326d28e6b79e0","metro-cache/6d/aeda73ed0ac9fa163e4ad78f5aabc17efba3fd190110f9ee9bdbcbe8ae7aa1effcb345","metro-cache/97/dc6e87f5ae278c5e9a86b3923ea36cf07a582974ce5d9659af5aebb4ac8e58d2ef765a","metro-cache/6d/698c7a882bf5d77e85cfa6de936733a8902c9d85ca9bd0c529b9b9e1f8928cd9c4ec57","metro-cache/c9/8c0712944a1e5af9d71a50242dcc18a4a4ac71cc153f9c3a7a30bcc268f3d19d06d63d","metro-cache/e6/b8ecfbb0ebf7e9c028a194c7ac89ac2dc8c3343b0deb548f08d6680145876512eea3be","metro-cache/9e/3fdd3f6c0bdf8e0b8307b80ed16e1fbacd8f498850a7e816a40998644b88d030fb6778","metro-cache/4a/bdaa0d2baed3dee22cfc1d1294b66bbc549251486d4c35386fa782a58b059c5e8b0020","metro-cache/2a/49739c024a8b2bb23268173569b14ee3f2b013ac223c12c3d22f00b622cdfe14929b4f","metro-cache/74/96ba64fe70cb3f7ad39c70e89853c6f955d1c00ef5af0ceaf3183a79bbedd9df70a98d","metro-cache/1f/60d6dadd93bfeaf97bdfda944b6eca6c1870fa7265484430a5a1ef5f7e23d611061f85","metro-cache/cf/ed13b7c3bd5cd3fee6e03e789956d7633cc43203ebc98747b95e28c533d3aa90f857cc","metro-cache/12/eb4594a61549c9828c0349ffcaa66feca397c095bb9cff005cdec4a31f30f5bcaad922","metro-cache/ff/1657525ef1d32b8f2c5690f2fff6a7de05c054f29b1fa098d286ab14cdc2843ad60a06","metro-cache/92/a6bfe524258fad700445bb1f66396a9277fdd5fcbe287676aec4cdeb5a612cb846b379","metro-cache/5f/0037bd0e31abdecac526a86de21e3f07edcde3b1550d9aa440f1449d026e44cac44ef0","metro-cache/13/313b662e00c357d20256ed52a00e3f17059b8345631dcc4a7c36fff4cb090f88eb7413","metro-cache/f1/03c2ba0684aa30cd15b1866d93c9cadfb554e7422293e0ff46aed39e56b0567f58e4b5","metro-cache/25/e6c7361d0088dde13ad1a36d67f5881181aa61f00c0b32f8db347d887f329cbad5358d","metro-cache/51/f9503ddd99a693eb88b7807cba6b565eb3c8f78717bfa7adc94d97e36051248e5c1eec","metro-cache/5c/eb42de37b3faf6260b245a312b8c11a07ed7a78921d8160e4f5d834660aa95825c610a","metro-cache/a6/30a73fe1d9e93828d15e044b37586fad1669405577a2aa7fe742e34e7ccf2a5ba4cae0","metro-cache/9e/be1025f84e90892acbcff73e8d15b5f1e4a998f276163e09d8736508cf8731a78c8b1b","metro-cache/5a/7874861ce8f5f69437de9b33df345502b06c6b45e242c56d29c1d5c75536561bbe16bb","metro-cache/69/0d25e36b9413fe7e0813497cd51652d1989cceecd07b19fdfc51dc173479986dd3c9c4","metro-cache/36/01a1d5e833330b1fdabf260c6030c64096f7ab3aa503ee4fc9199b4a1302c109c3454f","metro-cache/cc/f330e5021abf4711c7b89998d0c599f01c9912c9dc00b470b2e8f41ed95ab70185d57b","metro-cache/e1/eb33913ef4d0d3343754b2b3a0cf2967ddf36f727a1d8a99c1324f02b8dd6d897e0a30","metro-cache/c7/983a24fd3a90fdfec5513fb1cc1301bd017569bbcca1e695225b1ffbad3943a687c38d","metro-cache/8e/fe14ecdeab07114469472bfccacd1b031cb78385bb27043fca42491a60aa8a6d1e9cd4","metro-cache/a8/f716f4dea27316b020f584bdafc69ba8456e1d4207c4fef2f33f17c6b0b6d19ed90ea3","metro-cache/3c/c4837fd0154c9b95005ef299223970bd017569bbcca1e695225b1ffbad3943a687c38d","metro-cache/e2/cd8a83fb0109c2530d530d930ebcc15a136d55c4d0bd813264d3feff3c26fbc605272c","metro-cache/13/fe5726b3ea4a133452d10ace740852a1a8f48a4e56e8e61807646016b8dfb6f17963cb","metro-cache/5c/164efe87c220028ff36a6faeba0db4dd16a139566efab77a88bec5e6a8be1153a0302a","metro-cache/a6/cab78680427101c854b2c36f5dc039f6d155c84bf33cfdd109884cf9202508ed197f76","metro-cache/a8/22cd64808814e4f5dba78bb6608347f14e8d990561530baa8d72bab876d7da1713515c","metro-cache/bf/fa1c66859d3e47e08952c86e1b1008bc6aea33196a3cd30be353281864950da07941f2","metro-cache/7f/a7177fbd228a36bec3f0858401ae08a903d7a044b2fc6b9cf58573f8fad39e40a426d8","metro-cache/a8/a977b6f348add0e87adb367a04ed5306a20647259844b7cc7914bb58538f3b39fa38b6","metro-cache/ff/c0fabbf395d0cab31d70d6c35e98e0f261fc5969827959170bd316c012a39bdeb03362","metro-cache/e3/64d602274801231f4346d7f7d9f76840faf1f36cd3b93f8bec0809d562a2543ecf0074","metro-cache/3f/8002cb7fc7f43b3010737e07e99b7f97e941a7f40f77c6fddbc74621dbb07f7dd77130","metro-cache/86/39b043b90e70e83a2d5831533bd43509ee2caa587bed44f6b5253c56e436e2bda271e9","metro-cache/ce/eb0b9813f1dda1a9d4c582530ff6bcd2179e3f400debd5377b58efa5c981ea236a1f32","metro-cache/f3/9b8a4619076edc043ef835997072f1114fa2b2fb0846e55004c1b0045d3b8593b9c083","metro-cache/96/9dd676f8e29bcb14eb3e0351154831ca76d6bf4a31003fdd4a977d1f5a5713c5b760ea","metro-cache/96/bb0792e0313fd1ac94f06aeb3ffc97e882288002cdc9dabf4a1517ce6d4fcef9710a8d","metro-cache/fd/a79e1bcb963c4f7157c8a1b077acb2be039f5cf18e094455588157e4fd4c216f7b37b5","metro-cache/49/b83419cf71a088377a31b7ad8ff061bd9f4d9f2ca128a7fc45d754281c77e40e793a2f","metro-cache/f3/233b412bac2a6b184ccc5229c56ccb9fc3dee7dac635965994c4d112be5dfdd8f7d894","metro-cache/cc/05ae9619a2fb32461de768bcf410fce4fed3812b741ad5c1b724646a069cf200d64319","metro-cache/39/98e45713a7998219939721ae15eef8b7c3c019e4e4bc615f7f73c3fb6872943f28a311","metro-cache/53/483554cbb7c26477d65eb4fdbd877e93640289391cb6bd69d4b192c31ad5d9b79f6919","metro-cache/70/649d6f24a3a0c36152a0b6d4249ee7686d3d8b5e806137ff4ae202788b7d7e4166bfa6","metro-cache/dc/0d2222e7008ad4a0fbe222d22be6eb6c84857cfb7c203e5627ed619654fa30d3ad4a45","metro-cache/3b/56568fd57d016cd7e732f9eda6d6c19d4545d85af6536c45d3dbff8fddc942fc057828","metro-cache/8c/1c46d5b624b2d9f181ea54899f032e1f22c44c855736323bfc1bf3ddc7925c91e9f3e3","metro-cache/30/7e484e29196b9ddb2845114b947a665215c46ff7e4acb5fb140887f062dfb66126676c","metro-cache/ea/9ba5b0c34f69a189c0f0fcce0c124d8f63d424524e772b994ec35921036ce8850cc007","metro-cache/8b/e30914f2a4a164738e988a1f57c11019e0ec83c766927b1c96d961ebfcb485dfa3ebb0","metro-cache/d7/9bccd7939015c66e4fdf2791519b15c7b78d4c705c9bb51b451fa9cea4367675144502","metro-cache/c1/412d40d9b0ef9bcdd5d16ecf2eeb1436f8525b5a8c5a95274ed49581859434d691c0e0","metro-cache/26/6eabfb436bbdf1bc51df7469e9e6854cbe6bce963ee356c1017440b908b3d3682d2061","metro-cache/b0/d5aca858d4f589999e123f7730b64383288d924078f2a0aea40f31ee04d4b306ffdd98","metro-cache/e8/3f1d1f67d66d422602ee76a3c2590722ad4a3f3530c448a2752aa7158d66819e5b8609","metro-cache/dc/e0f29458c981584d0e3c6a42f272280eafec6244f1db6336aa5b37986984216e69166b","metro-cache/d2/c677cfdf471f323665e3c9a41f3132a73ab16faf0dcd0a82995bd2abcefb957bf61232","metro-cache/37/37527fffa3b967a6f524e90d732c2ad27334a759dc4e083cd86dda798231c5ab2c297b","metro-cache/8f/baf624c2aa4a38f69c2b0468376d9e772ef0b23e93e07360d14aac6322db6dbea2ba3c","metro-cache/22/474d5ef3dd2157bc838ad33811bd1194b4dfd640d95543974fd08ee6cc907de432a6cf","metro-cache/a8/0c8657912d0648080c5a4b33f7c38bcc9c3b4ee728e1d19ac7ab89ad5de81505571fdc","metro-cache/80/66eb2e92df9531ef961acb4339b03ad51fb50e0fd799a59b20c3a9a44ef06a73288e81","metro-cache/80/ef857edee160fdf2df199cbde7a4ce2eaf4dd084b8b605c2c1e378b657e14f3e9babda","metro-cache/59/07977b07a04164bff21925c5fa2e3ee4891a86571ee88b0ae9e32abf3d3df064b69020","metro-cache/25/dba0b1ab20a1bf55f161210d25deac46bb8c978a8b0c0f0a1b9f8b82196d8bdbe8e905","metro-cache/8a/d43d014a886d1d361a885e4e285d6ad95b2bdbd831cbd00cf9fc6700385081d6e8223f","metro-cache/19/d7ee75d8dd56573f30e70148d932affe1fa8709e923c50172fc82459fbd1908ad6e54a","metro-cache/50/3e49a26576bbc09d62357255ebf9f903f5398e921bc6773a619cb8747d4683c7ea5c7c","metro-cache/b2/16ec97a5841329a978bafe0ff97a4ee7de7c9355bfc9e35bc27ae308c761bbff238ff2","metro-cache/e2/014701700518dc5f6e40562820be14aba0e50ef0b74c34efbc0ebd9923f26619cf64ab","metro-cache/58/5f813af1062df8f8845f1617623af33d623c713cab8302e87e06e5d424c92c60cf4455","metro-cache/89/f535caecb6004f623f679fad443514b02e7e7011f219f03f4f6482cf6e260ff624a41c","metro-cache/b3/dca124d3ce6cc3f6fcf42975105ce4ee3a807dfb536fe99b8568eaa326ad5f64d90b8f","metro-cache/05/1963909aeacdc1bbb0beff3842ba08136ebcb8d049cf4ae83acc1ea4a5125fdb3cf21b","metro-cache/59/fe29a395b8c9544b31a4577c0471f0944493b3efc3a67039637d9d2851cbab4088cfb0","metro-cache/5c/5ab7eaa686996d038ec6f94a1c65641b7566818f0efa9d847e06e80550df0a3917c8a5","metro-cache/f8/ad161f953b8f633bcd1d503bc3d64802719add2a02e1fd26fee7af49a81a13f7b3d6e8","metro-cache/ba/43d5768f4ba1d662a672b5bc8ccf119fedab17af54e6360be4a53dd17b7145ab822150","metro-cache/70/4d64fc6f50dc62299c2155b7b3e5cb8a37583fc44cd3c9804cb0bee11362871b854969","metro-cache/23/3982787c4c36419aeac549a1e7b9b349f06a7e758613a71faeb3dc0c0f62d85e7a9bfd","metro-cache/ce/3908ce32438fe1bd8c23097e6ce41ab079735415b65c817d219783930768f214570529","metro-cache/db/6b60612b6263dbf6539bc95cf1d8c8cf06cedda36144ae608f36653c74ef7cb46d431d","metro-cache/75/970cbb07c053a564ec455afabc6227c0c7a5b07271f7d19a6f4bfde9b04b45c4cc4068","metro-cache/3e/e3f856e6c40d7ab284df77dbe81c6b59353115a8ae488915463b4425f39b70315f59b1","metro-cache/4f/5311c9a30ead7ea769fa618368cc2cf99cec9eb8997fbae9913e2d1e15d1e64fac9a29","metro-cache/a4/55fde5e6ef8440d844dc1e5b49352bcd0cc5872b88d4d4574e9b11c506bf4659ff15a0","metro-cache/16/ce45371201fe21356943d639c1a6b56fe01c397898c76f12ac8daf183b4cadce501f7a","metro-cache/57/9180aa54e91ff15a51912db09d39bba187911e069ca72aa98ef1bf03481bb4d926117a","metro-cache/b9/cd6b98ea449fb9f777647d9af19ab933fc9d051f0edde9a40e59107d0af886e563c2b2","metro-cache/7f/10c367dabe178704a812b8c3ebaf3bea0774fff8159f16dea8bee3249fcb10a3d32511","metro-cache/31/7e2b4817ab7aa7917916b1124ce238a114f82dd8267046eaef88c69bc9449c6f7eb62f","metro-cache/45/0dc4053943aee78162e631768c37954085eab4628f6927f1c1d0d387855232b1825867","metro-cache/3d/55da36fb3ee637b9213065d93f92b502c51f0493784d0e40a4e4c166399e3d8e512981","metro-cache/d0/fa986e65e6a176c2348a3219bb7a00bbdc9047d2a940bf7439622540ee110ac50fbfc2","metro-cache/7e/aed9ace01f49c31c9eab5d581eaad2f5624957af74c1370c7ef2b0cd4b7c45fce25930","metro-cache/3e/8c9814be9b304b55006b5718822b60bcdf13510f94467e86f6c808b221ed4a93bc0049","metro-cache/32/bd481edc04a86232ee4186975dc5ef286b789c1384ce7a3965f771e09bfcbbda2cdc68","metro-cache/39/54b778967f2d897139be7964bac61f264006ae0e0923de228fca4251e6776969357c87","metro-cache/7b/bce91701fd53a11432c4bd6ae49c93f886d1b250ae2213be88ada07181ac1c5573ae21","metro-cache/63/e592396adf72047ccf271180bc6bca9bb9a2876f7917338c439c9d1da32690fbae8835","metro-cache/ba/c3f19702b7d21cdb9301fdf82045e12118765385dbc4944f2e44ae421c0fd34bec6538","metro-cache/66/15980b2aec0b2a3145bc0e9f7297bc2cdf1135cc2b8fa5d218130888fa5f369207593a","metro-cache/51/a5c26cbfbac11cddb62404ea62f65dd1184e881247a8c925ed88e3faa36c230a1f0335","metro-cache/39/d6c2a76d7510cd1ef5c4c3ac6910cdb66611b27a91c0af5b2178fe555781ced4471424","metro-cache/35/ce51788b97cdc998fb3e05dca57b32c1b76a45a87e5db3e9b45dddb7a1c800b2037101","metro-cache/d1/b12680d126bc3226e1b65f70439455adadf0fa9f32cf6a7538f3fbdb43182bc5ab7b1e","metro-cache/35/f8a3ddb217e030ea12acfb68165ca8c10b2f470ab63de30019c35155c0d05254bb1388","metro-cache/ef/12578335a7b3cb8df2f44a5a94dd990fa10a2036d09032a94a18f5a7f4f79cbacd7e1f","metro-cache/44/5c5383e7e8472885e5596e4f1a14572800bb7879fa0bdc2de6f34d95b74da6b0ad783b","metro-cache/13/4733250d13ba1f604d7ef752d78eca6a41ee904b2a7c5fec4502618f32b6974e290759","metro-cache/06/cfe70002179f4439965eaf9ef0d365ef2c66cdd0e07a7340d15eb796f7851ccb8821ad","metro-cache/69/cd1c67689a201fccf683928429ec8416d85fdf213df664f20ac9f46aa66e52f8db021c","metro-cache/36/0d0b8da92d32b7e37eef0803764cc68e97ce4830ac41fd29bacedfe559ba02f04eb2e3","metro-cache/93/336a6639c865c739e49c841d83cfa7e3306ee5aad5702d7fe24f87e263c0007f3659b2","metro-cache/de/8268c771c4c7a38a75289064a350d94bff734b90ddeb11297794ffc5e6baf554c4fde4","metro-cache/fc/ff5c79f92e3a9a8d6161f1fa98c1282e324ec6c2d91846b8559c60f4eaf01d529dd65c","metro-cache/7e/7ae6f92dd5c823c40a60f2cf74acba181e883607402fcb239591befec9456c0eba67a8","metro-cache/10/38a655761c866ca2ec3f88f13a53afb6e7832d87cf1c4749f7f76511c3e37ca7c7abe2","metro-cache/e3/30f04cfbeb67ab18070ca9010200a0d3159daed83a76c792b18863c864227ac5831d1a","metro-cache/2b/0bbe1a4b63e4e49aafb4d1a2235eb92751473ba7f506f88dce227ee1ab8b8e5522dd42","metro-cache/53/9520c9f3bc1265a6544dac2888b31cd6f8de36095337c8625e817dc8698da51afce19c","metro-cache/04/d03eda2ad4d24438a614f06dec70a15024650b55339d578a9e143dc40d238a38b6151a","metro-cache/42/47a1b6e2dce1f1ababaccae5c64e77a5890812939d4f2ab3c488126bb425492aa701a1","metro-cache/c4/23f5878325c84c13dc7d4497053ba5088bf9c2a0e586eb193d8b7e560e4179c176a8a6","metro-cache/ee/db1c8c193ae5b2920df3296a28365b22dd86ea81f489c762e2e542abe74db42ff7bb4e","metro-cache/2b/65598131dca20de82d86ffbac061970c9bd79111dc779d691af6ac81f18002fe2821aa","metro-cache/1f/068e77f0c9a6db748af65784f8159219910ec7e34707b54473072c502a9967fce2a6de","metro-cache/f0/c5077122244c2e921a0df34ccf66e8f86efbf29a65d33a160d12a7be6c7180f67f4a71","metro-cache/49/13ed036566bb378c884c8b8dc6650aefaefba90062f5b173f2241ae91a964874cdd0e2","metro-cache/7d/352ef163884de676de27bbb8a926c36232bbf561e3ea77c8d463784d87c6fc2575cd0f","metro-cache/99/45deac5d9b804f786b776487d687812ad80386de80536ed276a8417274b859aa0f2c62","metro-cache/7d/84d6606e6e55ac91170d78368ecf3af5a3feeacecbe13da659c7f596affaa8daaef1dd","metro-cache/3f/c32a7475263e10664803c210614a431b0eea8f4eff0e3d5324991fae782f4d4d9f812f","metro-cache/a8/dfc2c05ce871f0258911dad99429122cb3fc117fd17877f0eaa37f357d9769df610d10","metro-cache/7a/524dbc6f8092eaa7ec9ca164f6510a83554f61d40ffb7fb55e628eceef6c908bd505d2","metro-cache/a7/c2b20012359ae302afbf535fc37accb00e0223154d1df01481d7707d8a8ef1040867bb","metro-cache/4c/3b6e4aa48f599680e17cf870a00acf580f8d407f9f4b8c3d6fb723e91de216174df5e6","metro-cache/a5/4904548be69a3d133e803ae451a796c3122d1a428587bfc6e37caa2b6b5be76376cbb8","metro-cache/b0/f220c966fe4958e23b90bc55dd69abde6f4761937f4692723ffd92250160412dde4950","metro-cache/e5/7b7a76dfa1791c9487597a23756d0d3cbda9dbaf515b5a789742966c8f65b8800a0104","metro-cache/f6/41624fcf7f20fb5f37c470191cf08aeb68c5379d0c17d9bf7f33a2ffc9299b8ca5547e","metro-cache/fa/f8e9f30fb2103b431f75470c0724c5cebe3cb22f8fe09325f5f3754720f9e130b8f2ec","metro-cache/80/1376a4cc093dcbc07c3200231f9a1ee923a4a5e209940e2639afc972c016477d8d8d1c","metro-cache/ff/c6daa4e32f8312bcf295b9dc4f30ce0083351a734fccf8547696adf6cf5fae9069cf90","metro-cache/05/1ab62287ef1b12d0d3952f56f1523a08405595c0703f7b910ce5ace252d76ea9fc79f7","metro-cache/91/c432cb35c65945949d7eecd1807d121bf6928219fd319c079f03f3ca01eece4e851878","metro-cache/13/9350e2e85749c22d0176847b05a4f8699e9f3f36392a9cc435c724e407ac90c950f19c","metro-cache/85/24648f529f837119904e78be300294d0eae72dfc79097861eff7c0187d8b4210545f1b","metro-cache/0a/98e75bc84fbaa3754ecc96dfe7e7e5ed165eeb4387e28369a266f3fc8d48cf71554749","metro-cache/85/50af35fb6fbb3bbea5b6ac46290e04165a18c7ea74e45b63613191b196928556d44b44","metro-cache/4a/7809b2467654487e09719ba68fd195a2a38608cc1e3be03f9f4d827d60693974ca54f2","metro-cache/69/36c52b9476fb6f94a3532bef3842ba1b51cbeb1ea790e6af206b075097a2e7c109983d","metro-cache/a9/27a11a91dac6909686abaff09f41b6b82edbdd10dadf2aa3c831c6e35c52c637526587","metro-cache/2a/b3c5995c887fbeaab0d7228411ed7d10af9c4a6581fe1f6cf3b011488702f8170349fa","metro-cache/4c/fbf50d5d3457d04494db707b960c9bc8a99c2afe89a087f8b3fa3e1b3a2c0a5c718d1b","metro-cache/17/09573aac011884960fffd0ad5165a4c829244a8a627d9ef13ce45757dd87adf8566848","metro-cache/34/d552525a8ce3daddda1b5ac8c5dc0d3101694b3c964fbc336973571a5cd837516c2dfa","metro-cache/a7/868b8985a4b4b0e67de3cc6c2f0306f3005cd6762bf76a161e119624ce5135c43a1a9f","metro-cache/9f/f655837180a15145eaad204ee4987b3828fffb20f2a5cfbf84a593d2dfe16ffbc8a4bc","metro-cache/b9/164d01f720cfb9da0ee95ef9c6dee473bc66df954dab6cee0cda026fd29aad7f59ad9c","metro-cache/63/8cb500b2bd0462ad82d2c6c91a9b894c54bb10af2e9781a2cd76e1a82fbc9f2c65b259","metro-cache/b4/e18307e5d370374fbada9ca2d7e8a60f44f363379bb12f29b7fd9e49f0cb1daa76195e","metro-cache/64/840bd46b2349ce808ca4a57f82a09384a6f17d8bd293cc6cc41209094e3d45e938893b","metro-cache/06/51782855b7dcf86a16a4a22fa98d00c47cf489a5628721aef42b7385d109dd1d7888d4","metro-cache/11/1d0186032d553ef4da6bfeb0469a4e5070ab64cc765280af4df697ca55e1dc6480c90e","metro-cache/fa/da565c9d33aa63260eca2ab706c23eb12183260b005dbc46925e05f273831b29b35527","metro-cache/9c/55ee71600d3e542778f0a01d3f4b08a0d87c2dab07f5c07c3d919a93d4aa8068dea2ad","metro-cache/40/be491d1166e79f186dadf8ae9ef538b4efa6d46a6684d6e887033334e86c50a05308f9","metro-cache/aa/7e8e8e67a5ad551137ebaedccd4073db299ba942f6a1758f562010b41a13fe118a06dd","metro-cache/32/e4c8d51cb9a45d25b71c993a0303bd221ddb3cd27025d27aaf4c827e4dfc4c05bba85f","metro-cache/c3/ab6225bfcd68fb09b1eb62ba603712f812afbc2e8be71b79b8defc33921086470b4d69","metro-cache/69/04598cf535da24c37024f297913db654eb37480fae703a90415cf52a01e8ac2118bb59","metro-cache/a4/e1e1be337b0add8d2ce5d377466241255fc057557d90a58360e379db9628728d3ab787","metro-cache/9b/ac7409707ea0b5f48d37567d0df50bfa5bad04776843481c55fd88c572718134304374","metro-cache/b5/afc07444f1792092a3e151c71b2300e3ce593d38ca74ebe98180c5ae31352e5c3445ca","metro-cache/1c/cbecc73cd90b3a597ee0c9ef908aa0740dfda6afba0c915c41eb4ad84464d43035b58d","metro-cache/bc/09e4105db02506eab7b0dee6220f1b243cddb2bf3ea24f1b0947791e8cffa80294c10b","metro-cache/85/2df9ca5a624dea40cd3f2ff3f42ee268774f6a507a5b264a1aae19dd4eb295d5685880","metro-cache/3a/dc95846a2beb773dd36aade8ba99acbecb3886422e59fee8a8806ee9195f3b6b27defd","metro-cache/7e/be1026d9045c547b41a51a35f8219e0f98a94cf4cd075aec6451065029201f4dde4519","metro-cache/e5/7c80970d0f484f0c9b4435bceec85e81271c5e9d8e276a40aa9034d26591c619e7cefb","metro-cache/5d/886c90adc50003dc177f25e291f50fc643bd898f028a09719d450bd89722bc8c10332b","metro-cache/1b/7d1956e76bf679b2d23ef61a544352d3a5d4bfbae67200daeb9417d61a4d0b8677041a","metro-cache/60/abc57f04f0ad710140c1eaaef689a702720ff1c5172ed8e19522e87d85334b7444f749","metro-cache/27/cc8a77e29916f22ad095688f16adc536399ccf4ac852b38505be379f69a30ae9b66d9b","metro-cache/d0/1209c34066d6b7ac7415a4ee31401d75a9a193648d5ed53042528db08f289b1535de46","metro-cache/9d/4ce039c91e5d3964fb0bc7fa4f1b1abc439039967ada128b5c7cfc4eb3d14a4ec136ba","metro-cache/db/e4a52e2dc5e79d5fb399fb109dac50b52862c8f466e2b572a69b6caf0003d5eb27f64c","metro-cache/ea/d7becd973a162ff25fb7f52fcd2a243a8ccd91e4aa939a07786b29565f84a7ad000d61","metro-cache/c6/7c4954c355a22af6d50884caf80d1c1f3a9311637c8b5a942c27ace3d6aa75350f3249","metro-cache/72/a4df689da093154e80cad8c253dc7864d2efd8e5c1d8f7eb9f12c52b20f17b22c01d61","metro-cache/8f/8997d9182d300187a7b9752131c7fcc5e43341932829d8ae52a3ddfe05d680f48098f5","metro-cache/8c/85aebabfecf3833da0180418b260a6bb2196be0ca6209ce238cc38cc511971397561d2","metro-cache/c3/9e7bd2bea57d767661742760645d65ff9bf7af612ae6b929dde5a7a8f529f772fb654e","metro-cache/3f/dafd3958aab41f056a5d564368db1add4b73c302a7f6bfd34675cb2b2f0a599e78e132","metro-cache/be/396fb9d3c53f6a79787dad65bcc282a9c580ed52cd9d6fd031d3eab19d23f617bc3d9d","metro-cache/6b/f838d9eb5a1a367742463927545c2730d5c04b22013f8313cef4c8d850e5050f334d21","metro-cache/db/46baa216b3a05bc63648f971a68819a0e7ea46e25c7a1eeab14644cba8963a71db6e33","metro-cache/cb/483d4283f499950bd6143e38fcca06677d3a751c95ec23b21de787e44f1cb457df2f17","metro-cache/da/65332bd7f9870816c84dc52b882330c8783c1696a8009d4fc2805f4315fa090b7ab1d7","metro-cache/cf/938f46cd1c5b219547f432f2bd8d55d9b886175e2fb78d93f00b85856eb6ce0c1ace23","metro-cache/ac/35623f59afda7fa09d30b4d8fb31822025f7c92de7a60804f800739ca1887db5a22ab5","metro-cache/57/d4a0a527731ed59c01dad3b6e9a3c0c87b68e70546467fe1cd50db44be0c4cbada0591","metro-cache/08/7c003ac9956236e4a580f2765ed4f0a83a56c9d15c7670e0b13c17813a62ba050fd698","metro-cache/40/def0d149ca1a6a1818104a7920bef2456d93307acc56836955b7b965613f1b7089b5b6","metro-cache/fe/aeb81ada998f128472d4edf53092777adac681ced3f4fa01e2d55ac6f7b6dd28be52a5","metro-cache/32/228acad92eeee3e38ea346ea5d1f16904084ac9067a13e0bf60a1b061084f2dc62ee61","metro-cache/02/920653eca394105438238002befb962b0673ff4e8700d5f7b2f33e9b4ed5b6cc071e96","metro-cache/a6/46c14e9bfe7c1ea38b8f2c5b8b3b62c53899c06bde304f16867a2e9baaafeb66a59a82","metro-cache/63/79cbcd0f99ff0ee82cee785387aacd457bc43a23c8f86ea780c0a6dd3724e0a556b4d2","metro-cache/82/465af39ee6fa19ccc252c2973bc0e7ff6869a23b20ffc398165e7f03f8c7641387682a","metro-cache/a0/07ef7ccf5ef39f1ba6086997fe4887e06a530c944dc6c693c1c1d8278e68ade5a4fe70","metro-cache/5b/2244f09d3fee8f3ebf634542274b13637d473ae1454f54433dc8abda758483a97b4aa7","metro-cache/a2/fa1dc62ecaa1a7e6a1178c72d80ca6040078b3503543081030a7f44c9ca3d4107943f1","metro-cache/16/cfda696badb146b544603e6578796bc376d06b21a3b4bc9902fe9c9e241b67f8b269c8","metro-cache/01/5a957c7e1cf249f8dec1e730df599afa4295ad3ee0300de9925ef2d9944f789becb27a","metro-cache/3b/16bf997552e4a983ff0d9e0b1df7d808396e7ed4dd42afbdd6fc10023258377c1c73a0","metro-cache/23/72c1204754c0619d2392c5138773e5e987c126a0f77d5d4772aebc7c2728b8bf765b5f","metro-cache/b4/55e645f623291290abe7b15a107006851487c51cad4e0c36e89923c918fbb35723ed3b","metro-cache/be/3c0f9a9310d6d1fb5ae2346d96f782521b7339d8877d38e33c051815f995a933d8a83e","metro-cache/6e/cab366ad99272bd5173fcdd7529ec472ff4ecd9cf55b2809c6e2d57f307c89f8ebe677","metro-cache/7b/37ca30af8b8a409c943bd8719999d1d0043dc9e27b5ded651b539e8ec10e84edca0f17","metro-cache/e4/e5d8e41b506e289b2d4ad8854eba2e3c4961516891d9546f3d211190c03ecd88554487","metro-cache/f6/70ce768c68969cd2778e00a13fc14b62593f2760f9abe554b54a3449db69a5e61d44e1","metro-cache/bc/521faebe2c4b740c13c05cc226b5f453e752a43b6c5d548e61ceb3d6e2ee6d6c7646c9","metro-cache/1c/22e06363aaf34f70f542d29d6a347d2bba9e73d1c975ae85f427d1b1c2c96ddb452d55","metro-cache/f9/6a832adc7ddcc97d251c63f9f6ce1f95c8e58b0114707288dce6a7185f9c212c829799","metro-cache/07/d84fc32a5bb4fa78057f43de28c12a647c76854762a4f2b106e0db436c0d6f8a9b1708","metro-cache/6e/47c5158a35b98e0968c2520b22ea152816924371fc637ee62ef973565d73c79c2d2273","metro-cache/c3/ef6b63130dd0b1d4341f1653111b6b09b51df6ca5dde1733176d26a218a5feae6ca78c","metro-cache/59/b911bcaa9179d94c37b79670ecf0781a210fc0cecd8196659de041ced584d6ea778b92","metro-cache/62/ece867394787cd27243bc93dc6269180383f329cc19765b1e9f9d44311642303c99bf5","metro-cache/2a/8084d938ac5fcc44be6fed0f2f95beaf4a520d2023005bacf37ca9e1e6bbce11a5053b","metro-cache/7b/8d18790e42b0b50b3725fbb0c556a47f40fefd6b93381e0cb166a5313d6db830a9dda9","metro-cache/60/f06cc7da424a3270876d6b9c2514befdbd2b068e5cd2f51ba029430f615b2f170daeef","metro-cache/29/192c61364f28ce9cdeefcb20ff294be60f029e2c8b51dae5c2cdde9038ab8de949f1fc","metro-cache/ee/84b72c23879d45fb44c897b81a1cbc8b5cb159865419e095a0665f8b42ff15c0ddd126","metro-cache/64/b58c3b9735fa456d8c6ce9ab1d783804cf1af4d8b933e86372b473eb47f57915afeef6","metro-cache/e5/30e0e02b1c3e2bf7542eb65caf77700b318899efee1d85dac8e749b60e9adcff68b125","metro-cache/53/c6590316a2513c27bbf3e64ae074378e92f030c21b83374176d19e15f2cea7089af580","metro-cache/6d/ee45ebdb30d15cc0ed8a5abd61af7bbaf998ebde5e548bb145b71a4c8ccdfae891d891","metro-cache/7f/9e15a23cfedf7373235462b144fcf5a050a254010022eac29d3e93213726ae30092139","metro-cache/7f/dae1da3128173cd749d313db6edb39003f2584befd83de4bf8bc1d2d7fd6c675760137","metro-cache/30/c869fb6bcec56dd22bff747545431208373f51db1e3269c929fc0050abb0c0e81a1b39","metro-cache/b3/fc529a34c76bce8669e35c846179963d741592497e0445ce0cd5c1368caf6b4f3eda00","metro-cache/73/88325170e99e8d04419d91ec842c2c5ecb829b1baa55399fe01081c14392ab0aa87ff3","metro-cache/0c/80c7f2ab0c20629798fa0142d13f191324ee147599b1026491bda70c44ce7f61fa0c0f","metro-cache/74/2390f0c2220d2f043e9b65ac92331342c0ba6e2f56c4e16264c8439ebc112ed9b2191a","metro-cache/64/4c1880fae5cc49ade711b87ccf095582ce371f29f2c704dc357db4065abef63a8d13da","metro-cache/0d/cb07ec100044fda3c55b26df2b11598fd5989ca7f7ee278e8e0f42a4fcdae4988e9d54","metro-cache/a8/41b100b85c60f29ba309f1be57dd57f959338ed364fbf601aab566b69abc337493cf47","metro-cache/56/22dcf139595780a3b1be01a48ef61590e0d49cc41f97ffdaaf57b22f9c9b8fe1fa9b97","metro-cache/e9/108adbfaa7f8fcff215e6e1990d5e56f0236a37a42a9f695c5342d2bd359b9e52d3e0d","metro-cache/6b/013254ee3e238fd96837b5eea7aa5af6ddc8819ae1f31047644d62ea89b0faab593086","metro-cache/83/610add0f0a487eec7241bb576545b873df16557e86deaedb9b4c8fbce8fd254ebabbc4","metro-cache/fa/37b39f1b6a72ea604b79ee9d533383d0227a336a033a5253b9b142fb118b332e46f2cc","metro-cache/00/45bcdaa7900c82f07d6258a42148f5b629471c256f57f2cf67e013ef79fd68a16b60bd","metro-cache/8f/c2b5a49e5cc8f04115bed030f0a5afb7fe715b874f4415540f5698d41dc6a737b87612","metro-cache/3f/5a56a1a52ea72267aced56addb9373c0ec087ae92d3e0a586e850341268f4be26cb03f","metro-cache/09/8905cd2df3a34209abb5a2802d45e0cf890c9ba9d3984889eef2ca68f9f4488922a08d","metro-cache/e2/e1b1c8a5a5bbf9cc7bed55e6b2c2703a669c5b30a729d3474494a7321623d7812900ce","metro-cache/be/cfbb88dad6f0e83666961175bd988f6a4e8f783d5d1664e67109c0984ee56f508a9afc","metro-cache/3b/976e212883cd4b23b69d23b0d96568a2e75b2b35819034e19ddeacdbf1fa6dac15dd2e","metro-cache/a3/b636ed6095e23566a855261b3ab44d920a864c7350ab1e85c857f497197ab38caf385e","metro-cache/96/bd4aa48c71622600867a3bf8e8c147c29b420b82488fad4248c0f58ad354d98032e17f","metro-cache/a8/0c2b295b48d4810f7ad34470a76cfb1f833dd1e6167e1d9f40a7462624e3f45db149f6","metro-cache/4d/bdedc5ac8d3cd33d09a639d3596a8331a5fdc5b1db9744efa15d2218b1f1dea85df0ff","metro-cache/0a/07b0ee9834ef802f76bcec25340dbb4214959766a637fac767a9109206453513c8d506","metro-cache/79/37a7c60d184efb2f13802669cbe2edfbfcd3d327acd3f4b2ab271c79639cf6ca4a4a84","metro-cache/86/81d797230278b81bf6ba35ead588326f5d30acabe2b24fa1a44fcb3746bcd6e0066f86","metro-cache/4e/a025b3e500b8dcee9d470ec6e10eea528794e303b683475957d038f422fad3e74f6f80","metro-cache/b6/b2168db8989303acd5b7d523c512054c700593145b19716c159bba200699a1801ec7b0","metro-cache/bb/6724c8b80351e558d24d49e4a8c34ac8659235d73e0915099d6db7072a9e515d1d4ad3","metro-cache/3e/29829ce68b233e76cfb990df353f4ec7cd4e8aad63afd5ece442a7d1cc5ae30671739d","metro-cache/71/28e49f9b2f238ce30edfcfccd176c3726bf078f3fa8f3bdfbc4bf8ba72d17cd8be8cc2","metro-cache/85/6f62dddc76240638fcf1b4865f7ac5034dc5a67164c407cdfa3112968ffa3ba8c761bc","metro-cache/86/b8e362e5f602f14cbae963f165b2bb0cebf401ca7c3ed4faccf50882948b69fcfd1c86","metro-cache/e8/740a4ace2e034a6e2ceccbfd2311a03185a75d951b5985f33e0420b0773e045abf6101","metro-cache/dd/42cd428713f66662f43ae675e8778f33a0077b599052c909d232cfa5fc4071f914324b","metro-cache/e9/c2bb4373a1a2bbe2f3e3a56ee66193b5025e0ce6350702f35d6e3343d9b605a94be3fe","metro-cache/4b/bc3043ad443b954c9bd2c9608ffe88688b2059b7f6dedac01168e461305b3558a981e1","metro-cache/43/259e0c1722db9e0b84f54532c1a6f19349d1dc2824667abb4128a7dd6b754169a6d2c4","metro-cache/eb/84b9d525379ee943bb0f1fdf10c8e1d130e0f21e65b6d029d0b166436ef1a49acf6f7b","metro-cache/b1/c9627c52ef49131bdf37e1e02339598c9f4d2c4c9bf460579346abb9b53edb901cdd91","metro-cache/f2/d5507f4753ed2b54486e32555e72ed283684dde22acf1cc7d1c133a670627c311fc5ac","metro-cache/69/71f7e3454d2a465eb1f0de59d624841474dcaead8abfeb715b2c18bf4c846eca082a65","metro-cache/c4/10ee30055d48b88676193b28bbdb4168d487c0ef88b7a8a682091fff6c2d32ff541095","metro-cache/60/5bb232a2026e2f6fc3281fe13372935626c98572bd4856ac847e6f959b1b2d46ee6b7d","metro-cache/a2/da42e6af16f859e31568bdfb5a3dfd85ddf62d7db05e2e552c32a877a79bf6acaf8c93","metro-cache/0d/cc27118309cb3abc52633a4426c89a68e1387673d6e9da65fdc7f69ae97122354f0a83","metro-cache/f1/300a839fc273034b252649591d589028b3aafc5860ceed08bf1f9b84399d17f032f01f","metro-cache/5a/64c24f21d8b462e49a4ea60e9c380aa11418adbae80ef2ab1907a545cc0ac1ce73a1a6","metro-cache/1a/14a3e4f47a7e36e7c40da264059fb2e9c6dc2d49f0ba99706af32442e4574c01d20534","metro-cache/28/590bd648bde7f24b8d725371b6b98fd9f6604f6aa50f754b0aefed83d3062970bcb4db","metro-cache/c2/d400dc8510496fccf6105f1afef85dc542beb89a61a047b8ec7c3ebfef3ac7181bde50","metro-cache/60/270f6b2d3bb9f3a69058586d947a709f7581b0aa72d7dc051179a5b02780611158c6c5","metro-cache/b8/67c08845c9bea4ae8e02115f4c147031f160300402d258af59880fbc030584a887e74d","metro-cache/26/81d6ef6f7a6b7de70af3060a1e6df0c70caf0229b308431d47bf5f27f81c37e14aeb4c","metro-cache/fc/f4dc86ea5c9afe42cb73ffcb4cbec2867957c642cae73ce3f3489b38f0318abaa41835","metro-cache/43/b2a88a57af3c07de9d48230cba77051e79ec99c7411f1eb3391e45d9ffe392227f771b","metro-cache/a6/6b3f67441b13a15d72a2d33905256e6c6785d6dcfd904f510938b4df1742f31538f6f9","metro-cache/77/a48c6eeee96809da267f0ccdf81066f08ba2e6b953680a6a5898a04858ff387c12529f","metro-cache/aa/0470cbf7409a2ef97eb145497f61d3ddac09ad31aeefd83a62ea3afb9d68e7ed5d4056","metro-cache/31/e175f510725637e5c93a5f5ccd7a096f9ac192fba966e50135f5a2873a739e942f9451","metro-cache/95/811c215ef2e702680eff4df87c8617347df4e3246fe231aaac70d6281326c9c400b1f0","metro-cache/b6/7994dee338a007cac1579586cbe1d6815449048328c95f015e5db64e4232013847b38d","metro-cache/27/30223810c824aafba40dafd90b4968a68c316cac9130405839b241b9cd25a6f7993764","metro-cache/3a/ade616160ae1f3ae7c8495fb512d4142cd6f1c21de3cebd11ded40b10cc6a151ece9cb","metro-cache/61/965f7eff007d639b3c44ffdd316096a160b4fa06de5e1725ea538e601a03064d8295d0","metro-cache/4d/8609357d36f4df7b52007541fc648b31e8acc207d8123c670afcb0ed3b36da05929bcc","metro-cache/3f/3a5e0e3183bf26cb8e669cc59e1147c0e3c77bd8d57016343edd9ef5f5b11bea3e2800","metro-cache/a2/8be4f125d27f22666c19df4fbe4a261f0fcfd0513552c30ab5a2a6aca19d0606faa508","metro-cache/15/7472b25f64a8170b94f89152da0d5b0c83206845c3c354863eb6051f0fb7b4a5fd9409","metro-cache/24/9d5645380ad1a4af63902137e3d3d4ebf14af8d19c48f52a4397212124f4245664cab7","metro-cache/1f/97202822b4de0b25fa5ca79b3146f3f5709b48727558bea4e952ad811dc7a5a7060be4","metro-cache/a4/b654368ade29eac437b10665cddb05226cc7ecfd3cd6351090275e26642965504453eb","metro-cache/2d/e908138d8d3c73abe93f2a9f0e29abd95327556d4a811cf5da499dd8ef6e37ffcd9e63","metro-cache/e4/ea945a7a49b1f68f76b5dc5b6d74902f2ef54edb3ea87bae1247dad2e799ce91e8b220","metro-cache/9a/c1cdedeb0687082cebc907e48dd21b31d4596cf2287fdaa07d9f34f4638be065c58b38","metro-cache/d1/aa9e0aa07d7b310ff8c1e0edc713af5c956a47078927e3fc44bf0c62540f74fe1e3a09","metro-cache/ba/bf1c808c7ee9e5c44c0343ccf5772afacc0140cda5abcc4c60afccdca477db988a83d0","metro-cache/c4/b5b0fa6a278f32de08a76c410c1730c8eecb401fb98444e1aff71857997bfb926acff2","metro-cache/c8/43876ab3ac2aeaceb8aebdd61bf3f8c6b0f17e711f16dd128a547a6467efaca01b7671","metro-cache/64/c8b6573981e7b7ce1a9dc47909aa564bb5d2d2475adfa4f07af51a53ba504bf0739b25","metro-cache/ce/0fdfc20c5810bfdc68480c2ba514da7319a2366791a14d246f81535aa77ccb380610fb","metro-cache/42/d0d67fd6021c641bb17de25a8db7e7d3c6874ad1bede835f57970aaaf571d90952d250","metro-cache/cf/f78ed5b8de31c6c6bb784315595513360481c523c477ad193cc6b2cee16453c6562a52","metro-cache/1b/d36765d5c78cca96dc0337150688702c740f065f5aadaf9e10ab11490e673c823e450b","metro-cache/d6/b9906da4eaf7573174de65021c0aac7c6eec3c4ffbb2d9e731db77dd6b7a2494b3574d","metro-cache/08/429e6f32bf5e77ae41d0764a5d0158aa10342fb84afce46682cb47c4c45c4a28a50331","metro-cache/64/29c557909a3893ee9b9837695667e1cea4c0856f8ace59430f5d16e6e993a515123226","metro-cache/22/d4cd2067bd31069f238ec3ef334cdd83bc73ea3ed3ae99bb3369441b0a0944549585fb","metro-cache/26/98fc694ad7f40ef1ab79f73a4ff7596422ccbbc13cc76d4687dd07e3c120c237602298","metro-cache/ba/0bdf4872c26a5a4d14b8da329ed44541f5e0632a41eeea47e984ff9c03c2fe5146ded5","metro-cache/ae/6c0f04a79c4d934426faeb45ca306f7c545b70c84d5c27a1555c9f24d52e41ca12bd82","metro-cache/64/574bb65fa7e96746ae54401f843028a2d65efcbff7bf216c712a533e3fbef24d1e68b8","metro-cache/8b/d11912989c4fb6227d79bce1ac46a31d6ad0fcbb8dbbf6d551e64718faf746b840be79","metro-cache/a0/2eb4ced7e9628c2ce10b35beff94b972a7427e9cf077c9e5f28027bdcea5748b81e4d1","metro-cache/9c/3ee9031a063ece8b1341d563051c4ad26de03b5a1dcd038679ff465ddbd50d410e7b06","metro-cache/1d/31a2ee09bb8a09b4645f2163bbcca180f0a71bb6ffa9a9166eb2e86c46993a303709e8","metro-cache/a2/efec0c06166b912991fdc4c17d88ed4675541293a2139836b94c17e11b44be3c26c22a","metro-cache/23/fd07d87f203a82ff46250c01a37a42c066258b988c40e7b30a5f885679f1ce32692209","metro-cache/88/b27fee12404a93531793d220b84038b558975948fdf6b2a33959f90b5923920e0ad8d3","metro-cache/6d/d961158b0d14c7cad88215fc58fedf8efe4ef33fe9559f3dc61b417faa6a4f7ac1a0a2","metro-cache/df/a12f14073032b35efd1dbef80075ae49f0915d130526827dab9767c03cd7d29424993a","metro-cache/40/defe18dd131ae10c28779b4833b6622fac98efa2edaf9f209344880314df692214a241","metro-cache/b5/45e30ba9450bfe09ba228bd54bfd659fe3e80fab6f82d019af0cfc4cc520a835e09999","metro-cache/da/449ec3bf022987132927da9bb3bf832a3c55d5566bcc4ea5c6b9bded3bd863d8334a07","metro-cache/4b/fd75c5536c898415e596caeb0e8082fc46cb664e316f82f24d35711c3ae76779681248","metro-cache/0a/bea750608a1eb7e32cb792ee86e02380bd6aa323ebf3d35d7ed333f7e209c7e554fda4","metro-cache/f8/106c13d1664ae60b00ea82f83e52760505c33fa4a84204b93b9333ce530b45bd683524","metro-cache/ff/c4bb7a36baaf57375bd0bdce5ae7f9975e6796e04cd77eee858664d3fd7585d5fae657","metro-cache/bf/e13c0e0297a4f8172daa4103aa35038c071da10e8fbfdd9f8322946973b9e286b260f7","metro-cache/62/c3b872bc57fd8aba3ac178d89b006693a9e6be7d94658fafe0dbf644fa914f520426ae","metro-cache/88/a3863a562f0b7dbe847a67474e906df4128b5465cfe6016e71b8d8223e20008940f530","metro-cache/a5/d996cb0037b676b7829e3d1c03596193a9e6be7d94658fafe0dbf644fa914f520426ae","metro-cache/97/cd0b99011585a13c79a1a4c1827af7ca6203a185de9d9c2ac8737e99bc18e826a6ee33","metro-cache/b3/72dea7229ee996cbcbd2bce22509fac044a8b67e401bb41c4a29430f8d5ccb4e427a7e","metro-cache/f8/448e539144e6db5e1257a726f0fae0a1237632034b9d03c35a8e3553d326777cfbb69b","metro-cache/31/bdac4d815c2d14fffe0dc7b5c7a05eb9bae776f5b2b7f4b125a1884685293dff5d2e60","metro-cache/6f/f07f8f002737a2b3a20b4820155f2c90ff2bcc366a4959b32d989ec2855d1770e98995","metro-cache/fc/28fa3a919ee5875d91736184654a94cddbd1c69b5441db994a1ca068d4667be9b6199e","metro-cache/04/7942ea6dc5cf4a6be42e13c2981772d791b697d813d104562825837af75c995c75058c","metro-cache/54/5180ec9e6ab606ab9b262512c85928752c2b53ba1f31f966b53e3b63e20ee39749f61d","metro-cache/56/06cda8d522369569ffbbb36b9cc39ea22f14264994a4f2c7d7150c92c35f4518f96779","metro-cache/28/aa5212de457274e037a71ddbef99b44134931e5dd2b90a2e845fe221f2ab517d6d7631","metro-cache/ee/4a8c69d3534a5af0b007c8d9994dda127264a08fc61f2e6c3d9b62a28f82541b178aee","metro-cache/92/be48d3414e1f56cd77d8ccde2c951c9f791a29cab924d311d2163e2c99a825cf2931c5","metro-cache/eb/3fd080053449dccd53b61e168dd66e13e854b021150d048b9f8bc137807adeae6f0242","metro-cache/2c/9bd1d6b73efc1e0fd6463a42e4a8ecb3da9b31e7fb1f7c4325491b19ff4f4d87249068","metro-cache/b5/2efafa76a8c12765df937a6b47fcf2646b3e1285181ba456407f3e2983e0322d5fb5ce","metro-cache/57/1f02a8e5b86208b0ebff8e800d4c1ea9b23b1a983dfb3e77faea291f6773967b5fc19e","metro-cache/39/a18e8e647677da01c6a8f687ca35f02b1a9c5327c4c1e03e74546bb95a6b350ab49933","metro-cache/2c/b745ef776ac99e656babd9a04f38a2fa1c4e5bc81affed2f6d218896b89b6b83390214","metro-cache/83/d91975fac56693f9beea48cba4975e951609c263421fc5807d7fe6b15e07c29526e7ba","metro-cache/c9/188270e20ebc9110e9fbd69bb306444f7fb6c640790311b4d0f3b3f7f8c5de6326c2ab","metro-cache/ac/aee484d2ae9d7a0d622456d8c8dd1a5bc2943e51eaa43748f0a662c664d783100c171c","metro-cache/e3/0a3253d5eb990f438446d9646fa8f2816e603c5bae95b1ff069dad4d5f2658b8a1395b","metro-cache/bb/f650fefab96bbb23c0afa7df2019e40d43cb7089175bcb6764a2e56d717730103685d5","metro-cache/74/bc7cabf174a241bc16f95f3255a536057136e2544fd3105e5812e8b2fd0ff697bda664","metro-cache/76/fcd8d815ea2fcf81ecbf7e51678ebef57e8ec707e93735dae8fa5b53ebbe99b02a23d5","metro-cache/81/d5251a29a9fac6abeb5d2cf7d56a4df69e8a5aa74ad16aa803f1195b031ab326809d6b","metro-cache/c9/758add3d9abdc52b1d77dacad9be64960114096b7fb1966c02f856d72a5026671e8ef3","metro-cache/65/f294b16d9fa45864be005aea0cfa1e462c82e8f7a68c5b9c98aab9531f7030f283bd52","metro-cache/6e/62dce4bf4f472763338092b9abf6c0a60363147c7e92c992dbe80dd87e44a69a22b3a9","metro-cache/62/64fd561514fbeca4f8e5d9155fc5752e11a0e239a2607477da4696b3f8cf23c8e289c6","metro-cache/c6/bbc49a326ffdce102b4820a098000f75946d772baeb622633b17ba1ac36ae6a29242d5","metro-cache/bf/a3699b748f7df3cd6fe3ddc5c6924cc32a039448cef8639cc0b7cc676ad9016e89e3c1","metro-cache/a6/8afc098d0c59cd9d9ef1a642f57aa51add1083d55d20be4518b7a15feec7b196ea4a5d","metro-cache/ab/057b5c111708921d48bee194b6e2fe2be6e38e91b7a7905e047c9eab77068068111631","metro-cache/5f/7345f5ec2d3b8a54e47023095110748c64880a401ee75f43030c29ab84269243a55220","metro-cache/f7/8d7e7fdf001ab0ac6f8d436041b55d2bb05a10bc06181adfd499063ff910e3ea0d30ee","metro-cache/ac/dce5bf4303a2f009112e8cc23fcf9fcf3d45da95e9650df68bd214e0bc96b795b89a0e","metro-cache/45/5d20e8d0284673b83e6517545b2b036fb34d5d9551dc383d284080cbe1f952c880c582","metro-cache/87/2b7830f3b0b27f4d125e3b9ee826f28f4a2471569fcd215a60bfe92b3f9d7d8f5f7a90","metro-cache/59/082f4c8bd4daefcea322ca143f932bb35b054afdc1f95b01445050b25b4e1c9d7db0c0","metro-cache/ec/c5cae4b6b7d3215a35cfd36db275ac0533765efaf7feedd75a41724ffcfa7b605ab806","metro-cache/f9/8bc1281642058b6f59fb226f992738985031bea33846609ac9b809a378d96a3522028e","metro-cache/25/3defdedf665b0a83972b7acae7fc5b1de6a4bdd133ee36a193df6d6abab2e775506c24","metro-cache/6a/e4330fe172518bc7703a641229ea60d6d39f178aa147438c813447096cdd6d9d32e79f","metro-cache/c1/31b5dd8bce168a8a904e5861bec5aa67b4ce303694d04cc7348aa74bd6f168f8d76c12","metro-cache/47/f9da122d43e6855346e3072e1071ea36bc035d46b9f74858b47d36f41c2ee0354067f0","metro-cache/1e/15062892b1f592576a58e853b4d7010f7c18c8073557a43a0fbc077fa2e33a746030af","metro-cache/6a/dcde868663ef613cdd231cf16bd1904d7d60c55e866f095904cd78babf2909c36e5a96","metro-cache/c5/085c79388c332b3cbe1866da743b1f77bea4c09e91381c11d0097e9e585b703ff77864","metro-cache/39/ee61c415e5c32cb9ac5caf7e65419fdfb8734796f0d83e72eafa832d8ad381bca691c8","metro-cache/c8/d2493284539c14970427dbd13ec1e2b337a203fc06af1e1c434e9266209dd6b54e270b","metro-cache/59/82c4133864b7ca55e81bd53c5b5e373ada5afad7ab8ba91956a01ff4099b967e781415","metro-cache/fb/aef87b7f2387a6b18572c6a213b3c11be07e4e0e7c7765a45a278b36582cb0e53cc98f","metro-cache/f3/cdaf9f25ef49a789cf513d86048734f47bd88e781803a3565c7e4f8565e3a576899501","metro-cache/1e/fa37d275d1d9f50d307009d5cd8a5c23e73cc6ca9cda339774d24aa27d2d91e133a45b","metro-cache/9e/2c6a5a9348898e8d99e77c582bd1103e5fa8d3a7b555e3cc965169d0e25fd07f1038a0","metro-cache/19/1ac60e3e3d3e9c25d94f637607243e3ae95d263725f81ab50eab9efa69941b597701a1","metro-cache/be/d56655f9dd0d4ed873c9925849cd4d577b87044c60b580e7e0698e6d850b6adf6af7b4","metro-cache/93/42a41b7a99e41be545fdc2f2dc485ca52dd342a549d58a989c5e6b6fabece49bfafff9","metro-cache/a2/63697ee463ae8657fb02a1f16c3ca33e5f21814fb5570947a1a602a6856fdefb0352b2","metro-cache/4f/a11ad79b8b92e3683cda6a58136b5d57ee4a807ed9de1d73ee6f4af29e8ade2bd95658","metro-cache/89/b4e95f97009586c8d3d115089c443f0e2dbc42473871652b96a067138ae060bc3f9b74","metro-cache/9d/8ebff7626246bcd8ec210d59e5eccd71872838611c8d1b416d2a75028e46f84b2ae2ef","metro-cache/a5/32588e9624f9c83e2420306eeeef34eea3dc6d53e480ca732a8aad08b485898fc0a8c7","metro-cache/98/75f3380337d61c29dd2ccf69563bff51f0c1fc422758e4b18e15d2563a71d5d24e9f63","metro-cache/db/90d6c40fcc7f1de3adf73e6399b78614b03b442c343e235d534ee05857e3c904bea88e","metro-cache/c8/16d243f49f56cd49f2b93cde194af431ff9047091ec22a2d36b653b4ae5cbe40e87092","metro-cache/0e/62a0e5a1ee324e6d4cae103f29c9c3a345cd4485305cc21a5261b078abc1e90848d879","metro-cache/8b/7828fcadaddf87658adcafeb391d8ec2ab3d8597f657d3e7f8e18f7e32edfa96c9f31f","metro-cache/db/53df330b6a3d028f4974d015fa41ebc9a539e32697fdeab1b5411b527c5068bbe3c17b","metro-cache/29/6985f9698073abd12daf2bf7766f5950ea5d5f717d8a66fda4d92cb980a11d1cc95ad9","metro-cache/34/8f66ad583ad310fc7500549e683f8ad187fc8f42ce2e83f1cd27115e85eb02cc102669","metro-cache/2f/9274187bf4e44ac54a194498428bee53ef622b5f883fd15d78720e3c6aaabd971a5b32","metro-cache/fc/3b8205c8aeef08bfcbd508136083628bab11ee58b7e4495e2f816688f957b7125ba622","metro-cache/47/a976cf20ca4640022b04281bbf426ab92f48000ef25256fe8a76d9898500d4d818949a","metro-cache/8c/2bd4a3595d2ae79653271873b8640d631d905c18f8ec90c23dda0719743ddc16f52335","metro-cache/06/ef5b6674a2395cfbc7de0b87b2383efed57f345864e1301fac35764a655737f1829a72","metro-cache/12/4931bbd26b5b0341c9c576e830c59ab2c2fc3938799319e186522625b14e8dcb2e5198","metro-cache/21/74a7aa45b07881e6d39429f80b01e92868c8ad0ee4e736104a74a2f7ed372687369b09","metro-cache/9d/f4a67b1ca08052cbc204ab4b04adf8e5cfff0b63c530bd2d240d7669dde9aafde0b313","metro-cache/31/49d1237fb0f7598ac8b747047bafd312f0e3d172dce13768c8c3a52079c1d4a19e6580","metro-cache/ed/41c6bf29642e26d5cee73a2de79505e46922fd5166a572d288badf80c7c6912d16eb50","metro-cache/f6/ebea86c97c1f1c9ddc1b49a3da0f630e7570f93ce111a041ceb7317fa6911d8a31ac6d","metro-cache/91/ded6e53ef4240f7d9b3f9725575927edd5d0aeb006abeeb0ad65939d419bc7a5c91231","metro-cache/b4/d626c3f37ca72cc9c6d56eef55aef2b3530e603f6a08c096eeadc7c9444395fc4fadf5","metro-cache/3b/34db96d175c20e2fe0a8eb0f1d7b50777ad5855cafc4df70c832fc1d262403faff0e7f","metro-cache/52/9cad2402b9023ee8ab0c171f7f43b3933ff2a5909ca5c0188c3ffb22aee19a5d437b74","metro-cache/45/5db6636c33a5efd42277794b537898b27f76f735a3f4141b0aad38829430b3b1493a7d","metro-cache/c0/b3500d499fe21ef61e0c824a5385da19a68ac451cb57b89bec41715caa4e1df239a121","metro-cache/18/4cfcc366bce6f848564a6656013b4cde00b3dedfef8198feeeb9fbe51a81b879fbcd7a","metro-cache/f0/daebc9b262ed68769b5add7089a20e46145e3e04ce7f94c30307d93312b8f28f86ab4e","metro-cache/3b/a330ebacac9daf4cef320a592cd5fc2d34a0e22ebc74e80b389ed868a0e2a846ad99c9","metro-cache/8a/aa7a62ffbc6a3f25483617123786df7bcbc71da9c04c9aa6efe881dfaf2b9efa4dc991","metro-cache/85/8b8f46f9009f6187655139531e427879c2c3d4a0c007fc5be27df61eadea0ca80c064d","metro-cache/5a/5fead52a6844a7b87ac0f3a268165ae09d1c7363354b46be39d8d48ec0b5d78a8331fa","metro-cache/6c/1a8f47c5577c060e2827db628d8df3629c006f2e6a6271990b1667894e994174538f2d","metro-cache/0f/c7eb3ba8fd744c25a40c551d2f06a8c62018d16d9100e2cc2cdb25b3426e2ef8677578","metro-cache/c9/2a220883b5812d1a918f691c081f84d301543ff615ffb5c47a5997ac8d2687de9617ba","metro-cache/ce/f4027c09c43865cbccb85264f7edecff12082d6b7cfc77ae8685909430c14486561e70","metro-cache/85/969283187d4c085c92f3d8d9cf5705b775d25bfdb59c599829c02ac17b4fe82cd8d028","metro-cache/65/9cfcbe3f6ca7d5019d93b8a7e57ff5badd89799db7d567f981842c841160a45f00e687","metro-cache/22/e3ab76945a675c9e5887aaac5864a71c2509083f83f04ef8ae25a8498bb373bc1ee4fb","metro-cache/d5/4daa6197045f6d5a0b5d2853cd999e0941fa091c39bf106e13f8544c6812e61d6fd9dc","metro-cache/a0/276ec6a8f92b68d17751c766d1f542a4643db72d0578384427c6e8d35fe88bb131a056","metro-cache/e9/55a0c77a270017e4e76138405c4617f61d528b872e90ccfc815654e4fa202f6624c778","metro-cache/1e/9ba1b3873bef11aa6ab3f9339673ada5d2e2a920d8d0af8077224a15e43d31162b6bf4","metro-cache/1d/c88ddda1d0a8e049837c5696030c8c9aa44d0e6ca9012641f2ca717a4d27309563990a","metro-cache/cf/104ae6c1359cb32f9da333671b6b0b4f8e5d6f8a4470da2dcf008eab61d63873f12a66","metro-cache/21/85e214f0c18836a6ecc6253f1ab78caafdde0957672c4ca10c79b0b6ad67bc3c79b94c","metro-cache/4a/ff909c6502b92548247005d2547e4d9cde3179c0ea5b9b99cdc1555a9853642cb78855","metro-cache/cb/0d2574033b8b20f78ab47e06ec9477c5ee38229ab265cee9553a130a77717f9a72113b","metro-cache/38/dbcdba77edcc5408e2f0f5fc9769433ae88ede10622eb599566d9fdb37722e41738361","metro-cache/f6/2b31b3b844b32e494f15cac35250769fda832c06e05674ce66fc13dbf0f22cef3929f1","metro-cache/fc/d5339eb6b46db6bbba6ac01dbb63f6b8d59106c2d6a98c8ad822a881bbcacf09c9b976","metro-cache/e0/a42b79daa56c9a9054f046c07195cb7a13784fdef2d95da8db7f0b6c1d2752e3b8dd4e","metro-cache/77/cdf5a3f71568e4de1958508282d0a334bcaf94e81689d84feb2e1d4e7a6328a32aceff","metro-cache/77/6f846eacd79914811c82e1d19701bd9a2e6b499c70a752d2fd05fbdc9fa6c892e70268","metro-cache/01/d45b1b537f7cfabaa1126c30e464b8037f4a906a12e79b98cd6635dffdb02fce885cb9","metro-cache/9f/98d965f94aed8557b6cb1deb5e2e2adff30a8a4eb0bd08515e7c4e20e87831b350c121","metro-cache/62/76a9ec035450d3b04f92ca579164c5862997dfae4701bd0dfb559da6354a54471d422b","metro-cache/ab/006cd75c3c6757b34d8ad7ecb5c6e75145ecf45b1688b04d6fd11a25e298296a5ba9ed","metro-cache/09/7554894179ce929c91619c7a1d2ae66c56758790ab0f1502713accfa79bbba73f0d471","metro-cache/95/6bfc7e4f64fd7f408ffb709b5d1eae754b7d0b1092e46cffe89a218e22681bd7619c1f","metro-cache/52/399fbd6cc4566d47c3dcbe0cf625ee88d591ab71df7c37b02910be1d42c9c69f3bbfe1","metro-cache/96/8c80025d2f6c3b53e94e42b48e6051b90eed435a687542c2e4e6328fdd7330dacf2f2f","metro-cache/1a/69b3169b8bda93dfbb8620409a1b0612e81ea8fc7dede1d654a0921c1ec87381df158e","metro-cache/b3/5efd241364af2827fa069b455c307e5b63bdbcd18e6401d16f3ef6fb594c602d3d2be8","metro-cache/1f/b928c2b8d63ada6f1886e441d993e61902e9a78e8491a55c8e71427dbb274c8e6ddc33","metro-cache/f8/81fc11fdcd023c014c8faae71fe2bdea873bf24934a2740b47f0bbade1592b45feae9d","metro-cache/27/d32d3adc66e5c70e80b483d1dd2d193119f1637c27dcdd66fc32887a5547d41be5d847","metro-cache/d5/d6d371f3123513d4967b776c281eb8feb41e1aec0db582a7ace106809b8cb1b6159fe6","metro-cache/8a/fed5a3b0c41aa825f322420571552ed8acff1b102989f2e30aa3f233515c082ccdc7c3","metro-cache/af/5ecdd98a43134bded2175d6cb624ace15d671f339f6a629279fc1e5e1a2bf491598cae","metro-cache/21/f170e74ed114a33a95f87bfea74eed7193320888349ff47d318fb999cbc7895a747383","metro-cache/3b/0b134aa215ab785ec517e05870ecd24c283bbc1a827e6f0a6f73a1a6cd5ab1cd5a7cba","metro-cache/47/96851f43f5ae08f5cd2fd03564dfc99aafa4009270a000c35aaa9185461f8c3c91b1a3","metro-cache/ef/248b9e385bb7a7b250205b392614fe7f0675d3bc835d89856550c64ae4ac5b6ac381db","metro-cache/41/abce194b012269cd8699b8043a899f07c4f8af0b24489df0aa4c2e8c7274921ff4f5dd","metro-cache/14/b2470bb47aa6bf29d0040578ea1a6c0619cb6a33260bfb91ff15fc00e2f9afc213536e","metro-cache/0b/d2d6dfd4772a76afe82f99f8d60beb4107d17715e75492a3e47408be9afdcd66ea2cd6","metro-cache/d5/77cebb0925fe9c21ef7993826e588d38f0580cde45ed3d945f792b1ed6fcf36a35ef69","metro-cache/c7/1e11a697c969173de80d90949af47897645f59ac9c34914d254087f9a40dc0d70269a4","metro-cache/21/3fe682ffe544753dcbdcfdbf3aaaea11d4333cb46ad1e3f668cc580ed1df641fbe9e2a","metro-cache/02/935e6e8c5191a1a24ff39659b52c318445010f6d31c732564b9d0a8cc4c8f405044e36","metro-cache/40/e5e90afa5457f10d101203e2ed10a636e97a965d7f2a656ef8c53a72ca4f165dc1c675","metro-cache/4a/8c9b46b4792f74a2ea8670be074a286bf606832fbba4e725407e92934cda85b3906a2e","metro-cache/bc/e7b5adb4d7fd28d41172da463376ae05ec174181bc2b3468965cbb0fd7b820d9d6df00","metro-cache/72/b45a096c59021823b2df91f429e63e1e9e711e6685af312e3450beeaa3206b90c5f9ac","metro-cache/d4/0fb0468a80c23189dc08b3ec20d4fe52d0dd99c4af78b9ea200d215b467a62c2a85f44","metro-cache/2c/0c1a6da25923f79e63a075e1ef9cec5ae0b911cfa33122c18339fc15252463aa90017f","metro-cache/15/6550424a1db344091dbfd1abf50e489878290b5fe5c9b41dba704d57df8bf5d9d2b268","metro-cache/0b/fabd2fde6fdeb6de17471d583728317eabdb5f4b6bf4b56752ddf06dcbd56fbc731e85","metro-cache/30/094847b138e2af6b210b212a20b9f697693aed2e8bdd7d02685a2bbb176153a376d9fd","metro-cache/27/52de1ce0ca0feb9783e3365bdda28f112ee456c38a65b16daa5db25a1058cde1a45eed","metro-cache/cc/331f1d48231d5d1a3b847b9bbc1d868873ce5dc5a5b632e947fcbc0fce33de0f83f8f3","metro-cache/84/64401cd57eb33b47f02fb1073486e2cb5717ae855438c41fae3b87c3aaf8519cf6e5a6","metro-cache/d5/b4bbc89d88c4846d7fe96f3f9ced37fbb0d6fb62b75e0740801123b4e4318e3a08b22e","metro-cache/74/b245d28e6c88560ad47957b5cff52ffc18a2f3cebffa053eea13b83463218f862682f0","metro-cache/a3/f5f4dab786a3100bccc201954096e3980e96398db6f99962de65fbed4a7aeb772954de","metro-cache/38/fdf071113b470b4a1235f99f238812ef9047127dc032cecf9d6e91915de845bc7913a4","metro-cache/1f/49627a47eaa540ecfe68c469fe668229b437485c8ad3e8882d7809ae9ef795b558b573","metro-cache/b8/4ada01bd1c981ce7548c7729a5782f26edbdeba893595d0d5aa404008f85de1a7516a1","metro-cache/4a/aefd4ac1587cd1490c834ff9149c97b61becd38b2608ec297f203b6bf62a6e2c517745","metro-cache/30/fa6ca3b072a40fef39700ae57591a82d25fc71030cfa602b936bab1927266b16b59926","metro-cache/3a/a04f8bde36a7e20fc1361d8b50618318bf2be115d3ec8adc84169363968156d5e1ea47","metro-cache/7f/65f4f2a8be70810eed311e11bfc88613326f5202aeec6fef6d50a7a51d4472a550f3fa","metro-cache/ad/b13f8c0b1c0ac308f356914fe70efe736e9dae15748e9c22746e16c14d1473d7612112","metro-cache/47/35c15c6b57d9a2cc808ff647aa35f44846bf21504ee0383811bb63156f06b1ec9ba147","metro-cache/e5/6689a7859ab1203bf30ad3c4e31241f904e0d3a28259ef7206485b484b03f58ea0bdaf","metro-cache/be/f9bb6fbfd60c7366093926de0fae70a777399544954c0cfdccbcfab7b2ed5b2e884315","metro-cache/e0/c93b51909c292de5bbce5292b0f6d4f17484a5e2d93dd176d68803cea4e0de74ceab3c","metro-cache/39/f33b377b7fee88d4060bd428bdfe1ac58314f77290ba5eaf221ada9f25c2cdf7ee9fe1","metro-cache/1f/5e942cf892432532a17e2185b00b2e1de7c5264b7cdb2da6be49e7afac68ad574cb441","metro-cache/fb/54a9adb21e73947c89b8b7460373c9057bb5318d02333712d9a6f1ca3f727687d2759b","metro-cache/89/09105800c59cd05a5a2dc0ab37f363762c1b188543d8f5e32b31f33db88695e3509530","metro-cache/d7/8cc7ad9088df1cae4b50ed019facb222de44749ad9ba676ff68a55aee5aeeb1cf86f67","metro-cache/70/88a367d66e0a47dabbc0effaca013e4aa61ec235b609f46d12f01d6e24db3d2662d889","metro-cache/3a/104c18e114bf26568315735c4349a28f6e737a7944ca1abf61bd52d00351b0b308a6a2","metro-cache/9b/d6f7b2f364f163a62161bc0d0ce742ea911a6453389fefa662249eb125114a79e29df9","metro-cache/79/55811552e7918d68c739ce9ce8c46a79476a8917636f1591c8aa04a7da233169dd7d6b","metro-cache/09/e9f0781dc05d17a0ee1dacd471537cc355781c0f3fa4a32e511007deac2b94e59c78cc","metro-cache/c8/22ded40ef930c1084b61aabf0d7649a332ada0af3ad10a3d085caa0ac8712196046b80","metro-cache/90/69daab80ab81b382b68a3f98604896b73f0a86556e8d8d3d98f206c231dea6d80de944","metro-cache/4b/49878f7c21ce325e7e179fa5ff9f66e03814311da1801fb4fa626e0320fb463a469322","metro-cache/6b/800d3b3ae437ad30c2887b7803a3a3ca2c2f050da0faaf03509668f8ff32a5f5bb0386","metro-cache/f8/1fae29c0dfc43e4c675729d20ef82a4871e458ebe38763093e8db4fd280037622565c7","metro-cache/33/d4d678347935e205f4bd71be3ddd25ee744e0c6b26016e2ce758d3534f4d000f04261a","metro-cache/a1/68c52923014d223332c1be28f703d83af0b673d49efe9e1f950124b3a6600384e53adc","metro-cache/70/8429ddb2ea1c01677f50aa81226bd68ed76ccbc9078f7c5cc50ed87164fcba34f5a60d","metro-cache/0c/b57572c9f13fd6a3311bdf1904070ae54aaec15c02f01bdce2fd9fdaa3561e97f51f5d","metro-cache/f5/ae655df841a5e302a765690daaf8cebd8173a54567b55504dacab8a16d055c49ec9e0a","metro-cache/cb/512a2a2df45eab35b9f06eb613a17803ead64606dfc8eb26bc9f6f0850c2bad6f5e018","metro-cache/29/69fdd096ce0ff053f4b2ea6829a62d0d30ebb2302e5864bdbd2d0ce8cb024f900d9c58","metro-cache/7d/f5d19258e509d3899876310776dfe5653a0f0a16f416fc19e954d6e232be0f674b71a6","metro-cache/f0/974fb725e18245d4dab54841eebd864810b52989fbd1f484b9f62f3980a46f4db0f696","metro-cache/fd/d2d47653cd8bf8657ddd4b7f057069904e37e7ce6e451222752410a4f0ad2aba027162","metro-cache/be/6e1e223fa2c729e30c246106788b5a513f5d0e10f2a89e61362a7accd6e1145f1c221d","metro-cache/0b/def74a40464c1990770d766248834bbb2b0f8fbc66c256b141a254e7fe33adfc3cf8a8","metro-cache/17/57301b66e820129b1ad1f9075bc3a3adfa83640a63e78340d232cdc7bb95343dee1158","metro-cache/30/e5dddbd2eeb6632303cf9b1ec85feaaab037dc4909bc9f317e34f8bab9e56d72b5d350","metro-cache/8d/8f84c0d09b8b8db678df274d70360f2499b08d7f90cd66a14ce30703fc068a1fba826a","metro-cache/ab/69558004250c75a8cd71579c79488f36107750da3d03bd80aee2fa1d51ceed75f4a55a","metro-cache/49/210e9034504da515d270971812650a91d34b837e49145614f8405adc3d6516d5246f1a","metro-cache/d3/acebc0a06f94ff9ed93fd993c9e84e9beb61a12a7948f555938d8c31cf0a9e9e6a462c","metro-cache/c9/4f45707f2656d746615461d57c120cc8ee7e1a3c332c3195ef0cb1a4d3d8697b921f01","metro-cache/94/6a2c607c8032bc620e27967dbc9480bf86be183a05a66e3895655452a6f46c11b16a39","metro-cache/f2/a4538c685aeeeb8f6ec7ed34dfc8a1eab3b8609a358960507c6ad17134b5b67355b9a6","metro-cache/4d/faaf6948d2fa30cb4946d8ea4c292d1275384c63cd37bfa4732cb9fd5857dd164a64e4","metro-cache/42/a608ce43cbc78826c72c7d6fd7923085bf27c176e3e2290fbb0d58b90ea03a1fd7976e","metro-cache/c1/38942dbd3ef5c38934ebf54e7cf68e9ef017b6614ab3ea8b570aa464ba184b5dbf540d","metro-cache/74/6832ca95fd85f3f3085808f7b57119e9d14d0b75325bcfad5566e48417700cfacc40b1","metro-cache/e8/6e345b7eb25f183bae939bb1136e638d4545038994e288af78b3816842110bec84bbd2","metro-cache/81/65b284ff31f6b25d1cf1c3db85c18acf42715632f387e822ae582617d570bf14aedc4b","metro-cache/85/43c57416d28b78c21af5f3c3e28743e8c0ea448917d7afd8a417c0622c6efa2397db7f","metro-cache/61/7032f2ec5884b9431fb5597265f5b74085054a6d4cead98016e664ee285ee7f63b8224","metro-cache/32/271ade9a63cc76dd6582c4b22834ff3cc8737dae63601259eba3ffcc1f5c20ed790675","metro-cache/89/f4c8318b878cce09dd69ad4e9e3cf89d83496ad413acf2f2139f7385767cfaf637599b","metro-cache/20/ba7ad3b2b5a53b30d7c6fc48c0cd4efa894a1d31670c1bf009599d698a16e81fa3bae9","metro-cache/53/2ac444196401836fbdd757730cb395b5b0e78da53f5d0d22db52528a60f134e605a8c0","metro-cache/75/ea7d4b992eeeb28e8bce05459e23c9eef4d0b01ee26adea59ca7865822232d16aae872","metro-cache/b6/570095369c792127807236d4e40351b41e83b4f7f188976e4c342d27622c99b802c3d1","metro-cache/46/2221012a58b0a45d166453e9fe553ad9f5b11b14e3a04e8c29b074bd1bb36b15683d91","metro-cache/69/421f95c48a51435bce183293fca15c5f47959204196e544bc516ff5e8f6c6f10a13bcd","metro-cache/a0/c2632398bfe5cbfc7d73d60e7f4353bdb95abdaa00966d037b98fd24a83672e0341463","metro-cache/95/b4e32f0d70254b19c68fecb1766c6608ad4adaf494084f2f1ee45a3ca66ccce80cf459","metro-cache/71/c61816d38beb7fdd6719aec7d93ff459bdaaf6222ed85898a4db06f3961b7534853160","metro-cache/05/d489774ef72efbd6ae415c55e0689b1550a667531868fb3df1cc7c20318545999a8904","metro-cache/24/ff3d402defd58ff4ad5021cc2efbdb0f13841e587414c9fafbc220988f7543eb4b7b27","metro-cache/3c/d281acac8b275382993e228fab16083df9af1a379ab1f2be634d36588102012ca6365e","metro-cache/7f/8b09f6b68661a72d183fac2b3fa8ce7ca2627a10323a3bb851a062bf3527b0ec214bea","metro-cache/af/4562c098ba5e4629daab2f22c60c9ac6124dadbb0826b133424648c1e1262e54aa45f8","metro-cache/e1/98ebb517ac9a7c5a2102b480954833249fa227273ae801599c0c2abf230816916adb5d","metro-cache/f0/079c3cef2db7ba167bd2b4ed40567ed0874410227caebf69f641d5430e597a00702534","metro-cache/e3/23f4bed29ccec301ebac55143c985233a001ee46269984eb7a2b85046d23b8591f57c1","metro-cache/19/1e06a57ac58ea0eb8942be3d4155a292b30008774796e75fb51c729e981b05dad1dd6d","metro-cache/08/92b6458774a227d9ab7b50f2c79cb37b611f10699059c068b2bf26d0698bbd19e837c5","metro-cache/16/441d1005ff53d8f4e611821d0a3a45faceeba3a31bd56979c58282f191beb7f1699889","metro-cache/3d/e52c1b6a652a1d6f282cac3f8daa764ca5659a4a9433ce3a1240034a458e9b8aa2200b","metro-cache/80/104af27d0ee7b18720982d40e7e9eb840c09820bdd2d1d8ecbeb3fbbf4f73de37096aa","metro-cache/d6/f74aa6e6c13ec0618cb8b432b8b28644c5390f8a6c615cfcd8994805eb95fbe1c2d312","metro-cache/3d/c1e507f6c0c85062696c4eb5e3c3950960a46e1457c76dca59c9222f376f1e591d6a78","metro-cache/79/5398f746e2908a4ed1b2dfbcfbe28810eeffdf73150ac3ddfff3635cc55c2d064110f3","metro-cache/b5/3b13664aa37567052e7456432f9284748c634dd35b4867f128b662fbf3a2288848c928","metro-cache/76/e5bd999091be28402db08e157c45b94c70ba5fecde6d08efdea4f3c0a0970977e448a8","metro-cache/aa/d149bc7df6d09adce348e867876a51c7184f918daef36d39c9ea9c975cc32074eec395","metro-cache/57/f8ae11bf6573b2db34dd6776280b9280066ef791ab57c6f7b54d527a71a52872487ced","metro-cache/1a/0ce1d456291361eb45e9433c5f5673ab3a88bd0b5799101318b4c8d02ea92190b0770a","metro-cache/da/428a9a092419569694c73137661559a9749ce3643ca09768690930965048b27b8808c5","metro-cache/51/a618318bb7c9b6857388917ef0c2a218c6068d81c2962e5aba89d8b06705174ff1ae2f","metro-cache/0a/a59fd73e511c3f7f16b518f7aeb1692bc89129358f514b1e2adbc2c614e755a45b1bd5","metro-cache/be/7a85ec45460a9701099d1ce982e0a326a20099fbc5237bbe8481244a7f6028465dd27f","metro-cache/7d/dfdab63a03e090e2559609a1bf238a119fbfe9744986a5aa731211abc66b90fe1c1db2","metro-cache/0d/2542f0f2d31facbdac5aa5535a4337cbd1d1ec87af382f8fe9df47ec935124df3619d2","metro-cache/38/822f0652daf1b9e6b3124726556d04423804c851b0c98eb123f41edadefa88d10bc050","metro-cache/4f/eb655b3071cc9eaa71368839b48489a85024cf3b754156ca8fffb5df321e9d614e3ba1","metro-cache/9c/1eb30a70daed9a10353f3538ce4b707e6d4979d06e2bd1574f35f410dce47d38b2a700","metro-cache/53/ad2448ac9a3dc6c53f6b9fb52e9a9c5e8ff6071934b02b779850f8d06abe5874861fe4","metro-cache/74/505bde7c2e79813fafef05b045cf208bfcc84455fd01feccd463e726c9eff695e53512","metro-cache/0e/7a0d4343f2abd121c4f0e7c56a01d91b8213f84e71116a1bb07bde95bc4f45ab6ff232","metro-cache/54/33fcb507f0ab0ca1fe578531ba1cd30c40c2008bab81c9d3f4a7478c0428e30a6904de","metro-cache/82/975a0b6395058172bca7be1f58601caf93a8e94e0ca66fc7754e7e4d415357006eda72","metro-cache/65/cee7b84dccda00a746b7915f12a61a02f3cedf9a0bd0e778ed7ef1586edd50e9cdf380","metro-cache/77/f9e32d3afc7b3c05b631ca207c9d580c7477da76c4af38d04575a703ada74061a57ddf","metro-cache/70/fa0c34c969d49bd220ef2a599fc579e93ad09a808cc37992ad42a30b0771296ddcf578","metro-cache/5a/01aceaf140b92dadb9610e0d9ff1ba7b2e7262cebbef9cf33057e1fc79b8e1d91f53a0","metro-cache/82/5ae93b223fea7f8520c72765c53b5aacfe61026ac60a3433742a86cfd879a7637aac6a","metro-cache/cc/ed4f4835906cf24e4ac5878db1d46d1687bd0ee26dc4138271d1099c7251d5fced898a","metro-cache/90/4fee654c7839f992e4ca9917122cd80e4288b5eacdbae6c533099b1ddea543d751dfd2","metro-cache/f3/2c695ee043890af41c756d548db5903fbe14f57f537560b5d759d90c2ee09a03344f25","metro-cache/a7/d44b683adb64942a630640d597d7d19b2d63f56991febf18149ec2012ca1495b1d17bd","metro-cache/a2/fb735578eccaf698a400595a3bb6f219adff1058da7c568eb91db08ac016fd5dd55bed","metro-cache/53/448c12973b275fa77369ef0dab0c822d72b9c702badd6fa80dc3405ad7100b19ba049a","metro-cache/65/e43332110292f967b9773d6200bc51481505efd7b6c6a1aa160ba18e400e52bea3e1de","metro-cache/d9/dd51e99fd0a0a2b36d9913550afbfc311a2c8eaec6926cb91176a76a2d8cb003056a1b","metro-cache/d2/edc97fd1f7996a053139ca9c0e15c9018057b5632a6acab6db0cc0242db413487dce62","metro-cache/8f/a55e5195830c30f0b1e4469b3f513b9e70be8d8c5c4a0200f64bd9b9a766aa6f44ad8e","metro-cache/cb/2f05df36629ad2437078cafb016316cde92ed787f8596c5d8d7cab3cf8119435c041b1","metro-cache/ea/1c03fe900a06f694215afecf6acf41e5b8e3b2aadbd3d55187abc86974c30852311106","metro-cache/95/3c91c1a183fa79472d2369dce1e97f9ccd18faeff765b426a36a2b59ad19b007bc170b","metro-cache/f2/2a33727844c910249e68e935d69d4414a22b3eb563a3f2c5bb145c99a34540fdde01d7","metro-cache/8a/c5f3728a35ee2f3e69607172ab5c02b725243ab910d4940036254a34be6fe116322aa7","metro-cache/98/8e5a9eaa9535aa4eedae73106a635fdc68c1ec1e239b1fe9d83a70607f4729b3e0b4e4","metro-cache/88/9222a5d195dfeb3036fa12849c79286549b1523e2e83bbfbe665f218f863161864b6ce","metro-cache/a5/a5b73e76cb26e2ee8a92bba63f1691d63bf2d6e3c4cb9bffa0a3dc29c44e9aef87d517","metro-cache/7c/3d15c2f004c31375f3f06aaf1b4f87cbbf6db4f4a586b39285d94b0479db5aaa2d9308","metro-cache/9d/d9f1a156f0500a0899ce87e8390eab334ec56e622ebe99ef9566782888dc93b081803e","metro-cache/92/d7eb491da86de8d1d41cb23d7f84deedbe5b7f733081cbd57ac5070b1e9e166049706a","metro-cache/ed/4b51df989317141b76cf476e49826b68d26cb5e937e1998eea868ef4addc901282e193","metro-cache/9d/4c5289ef7baf9df79131eaf1d5a248933b75756d4cf0083ab9b573b28bd429385dfdf4","metro-cache/e4/85cbd0a564abf78e14a18b94f3f877520fdb56c230c977ef34ccff8ab03ab75fe26655","metro-cache/22/ec1a311a39065a045c1d8940be99a5ef970226f01b6b050571f7bdb5882e8c7508bf4f","metro-cache/45/2f60c95b3404b0bd413beef61291f96c0323229f4e85d9022927f0705baca22a6bfe89","metro-cache/14/073b16c4484fbf640ca59f3fcbf15d45ebd2e08a82f759d576b97aa8675bc1346f8392","metro-cache/e2/6ee5e2eebde5200f8183a826b056beb9a5aee05119d8b14885f772add34c98efd31266","metro-cache/c8/4ef0d64509b8794bc3888c9a23d31d498c548a9640cc9ccb19fd110e05670b05c5934d","metro-cache/58/4ce4e6095a3c7b636c3ace1161c58dba611da4200e5c26e3285fe28742bf9c407ecadf","metro-cache/b4/8ea74de4016b0cbea9d4e212c8904970af110ffc9e100f1d4275a66c5a2ea01151de36","metro-cache/a8/063ff5e5c13e3556474c811a02a8bf54a081b408572dec598853e3a45d797033d6fc0a","metro-cache/00/b8d3c58b71151c3cdfb28ddc42c4d5459bd4da93aface4c94f746944cd614e73c7718d","metro-cache/d6/1cf536a0ca6bf9f1eb037dcaffabdb5a89ae1a1ab5ba7955fd773ac234436e2839fc03","metro-cache/c3/c53bf3e08e76cef1a9bff2baa6621b78407b97ae5c3eb42c7dd2f8d0acfee10152ed98","metro-cache/36/f4ca0a1156cdf179fa4b589b24affef3c86de447d39fe4e41c4f0f873d4321a0fd8845","metro-cache/93/7dfacbce285bcf1e13f075bde09a64ba7af42b5d7de1adc23f72a5fe2e666f8db3be64","metro-cache/d5/2093330a6566a38106539337b44109250b27efef3cb57cd6ece95c77591951b0c6ebef","metro-cache/fb/ba239f66ae8c98ece0a1c45f1b339b75af78c5eb867660bf96704d5548899e787f4fa1"],"totalFiles":10000,"clientTruncated":true,"ripgrepTruncated":true}}},"toolCallId":"tool_19d8434f-d852-4aa1-ac74-ddfc2182ea7"}},{"toolCall":{"readToolCall":{"result":{"error":{"errorMessage":"Binary files without an extension are not supported by the read executor"}}},"toolCallId":"tool_5ba36719-f64a-4ee4-b1ef-09522d3044e"}},{"toolCall":{"globToolCall":{"result":{"error":{"error":"rg: ../../../../../../Users/julius/.cursor/projects/var-folders-b3-b51-pdxj7dl0t981zpqkxxhr0000gn-T-t3-orchestrator-v2-cursor-agent-sdk-record-proposed-plan-ggwFWW: IO error for operation on ../../../../../../Users/julius/.cursor/projects/var-folders-b3-b51-pdxj7dl0t981zpqkxxhr0000gn-T-t3-orchestrator-v2-cursor-agent-sdk-record-proposed-plan-ggwFWW: No such file or directory (os error 2)\n"}}},"toolCallId":"tool_1d4f6ac5-3ffc-406a-abfc-962a7ecdd49"}},{"toolCall":{"globToolCall":{"result":{"error":{"error":"rg: ../com.apple.replayd/TemporaryItems: IO error for operation on ../com.apple.replayd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.securityuploadd/TemporaryItems: IO error for operation on ../com.apple.securityuploadd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.syncdefaultsd/TemporaryItems: IO error for operation on ../com.apple.syncdefaultsd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.transparencyd/TemporaryItems: IO error for operation on ../com.apple.transparencyd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../scoped_dirILNojf/SingletonCookie: IO error for operation on ../scoped_dirILNojf/SingletonCookie: No such file or directory (os error 2)\nrg: ../com.apple.iconservicesagent/TemporaryItems: IO error for operation on ../com.apple.iconservicesagent/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.triald/TemporaryItems: IO error for operation on ../com.apple.triald/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.ap.promotedcontentd/TemporaryItems: IO error for operation on ../com.apple.ap.promotedcontentd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.replicatord/TemporaryItems: IO error for operation on ../com.apple.replicatord/TemporaryItems: Operation not permitted (os error 1)\nrg: ../net.imput.helium.yM2qKz/SingletonCookie: IO error for operation on ../net.imput.helium.yM2qKz/SingletonCookie: No such file or directory (os error 2)\nrg: ../scoped_dirRPFOYo/SingletonCookie: IO error for operation on ../scoped_dirRPFOYo/SingletonCookie: No such file or directory (os error 2)\nrg: ../scoped_dir6d02yz/SingletonCookie: IO error for operation on ../scoped_dir6d02yz/SingletonCookie: No such file or directory (os error 2)\nrg: ../com.apple.appstoreagent/TemporaryItems: IO error for operation on ../com.apple.appstoreagent/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.imtransferservices.IMTransferAgent/TemporaryItems: IO error for operation on ../com.apple.imtransferservices.IMTransferAgent/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.openai.codex.Kkes0i/SingletonCookie: IO error for operation on ../com.openai.codex.Kkes0i/SingletonCookie: No such file or directory (os error 2)\nrg: ../TemporaryItems: IO error for operation on ../TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.parsec-fbf/TemporaryItems: IO error for operation on ../com.apple.parsec-fbf/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.tipsd/TemporaryItems: IO error for operation on ../com.apple.tipsd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.fileproviderd/TemporaryItems: IO error for operation on ../com.apple.fileproviderd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.imtranscoding.IMTranscoderAgent/TemporaryItems: IO error for operation on ../com.apple.imtranscoding.IMTranscoderAgent/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.chrono/TemporaryItems: IO error for operation on ../com.apple.chrono/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.calaccessd/TemporaryItems: IO error for operation on ../com.apple.calaccessd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.remindd/TemporaryItems: IO error for operation on ../com.apple.remindd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../duetexpertd/TemporaryItems: IO error for operation on ../duetexpertd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.studentd/TemporaryItems: IO error for operation on ../com.apple.studentd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.parsecd/TemporaryItems: IO error for operation on ../com.apple.parsecd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.identityservicesd/TemporaryItems: IO error for operation on ../com.apple.identityservicesd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.sharingd/TemporaryItems: IO error for operation on ../com.apple.sharingd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.bluetoothuserd/TemporaryItems: IO error for operation on ../com.apple.bluetoothuserd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.bird/TemporaryItems: IO error for operation on ../com.apple.bird/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.spotify.client.TtbUSJ/SingletonCookie: IO error for operation on ../com.spotify.client.TtbUSJ/SingletonCookie: No such file or directory (os error 2)\nrg: ../com.openai.codex.OpU7gz/SingletonCookie: IO error for operation on ../com.openai.codex.OpU7gz/SingletonCookie: No such file or directory (os error 2)\nrg: ../homed/TemporaryItems: IO error for operation on ../homed/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.openai.codex.ARhBC6/SingletonCookie: IO error for operation on ../com.openai.codex.ARhBC6/SingletonCookie: No such file or directory (os error 2)\nrg: ../com.apple.usernotificationsd/TemporaryItems: IO error for operation on ../com.apple.usernotificationsd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.quicklook.ThumbnailsAgent/TemporaryItems: IO error for operation on ../com.apple.quicklook.ThumbnailsAgent/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.pluginkit/TemporaryItems: IO error for operation on ../com.apple.pluginkit/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.donotdisturbd/TemporaryItems: IO error for operation on ../com.apple.donotdisturbd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../scoped_dirfKTmVG/SingletonCookie: IO error for operation on ../scoped_dirfKTmVG/SingletonCookie: No such file or directory (os error 2)\nrg: ../com.apple.imagent/TemporaryItems: IO error for operation on ../com.apple.imagent/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.weatherd/TemporaryItems: IO error for operation on ../com.apple.weatherd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.imdpersistence.IMDPersistenceAgent/TemporaryItems: IO error for operation on ../com.apple.imdpersistence.IMDPersistenceAgent/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.amsengagementd/TemporaryItems: IO error for operation on ../com.apple.amsengagementd/TemporaryItems: Operation not permitted (os error 1)\nrg: ../com.apple.icloud.searchpartyuseragent/TemporaryItems: IO error for operation on ../com.apple.icloud.searchpartyuseragent/TemporaryItems: Operation not permitted (os error 1)\n"}}},"toolCallId":"tool_77d9e32a-f036-4ecc-9704-e6e484e158a"}},{"toolCall":{"globToolCall":{"result":{"error":{"error":"rg: ../../../../../../Users/julius/.cursor/projects: IO error for operation on ../../../../../../Users/julius/.cursor/projects: No such file or directory (os error 2)\n"}}},"toolCallId":"tool_5b5b3e42-39a6-4f5a-9a36-6dcf0296872"}},{"toolCall":{"readToolCall":{"args":{"path":"/tmp/cursor-replay-proposed_plan/.git/config"},"result":{"success":{"content":"[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n\tignorecase = true\n\tprecomposeunicode = true\n[user]\n\tname = T3 Code Test\n\temail = t3code-test@example.com\n","totalLines":11,"fileSize":198,"path":"/tmp/cursor-replay-proposed_plan/.git/config","readRange":{"startLine":1,"endLine":11}}}},"toolCallId":"tool_90ab80d0-945f-44d3-88e9-500bb6cab35"}},{"toolCall":{"grepToolCall":{"args":{"pattern":".","path":"/tmp/cursor-replay-proposed_plan","glob":"*","outputMode":"files_with_matches","caseInsensitive":false,"multiline":false,"toolCallId":"tool_af1a9a15-a402-49c4-b5a6-395ff6a285e","offset":0},"result":{"success":{"pattern":".","path":"/tmp/cursor-replay-proposed_plan","outputMode":"files_with_matches","workspaceResults":{"/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW":{"files":{"files":["./.git/logs/refs/heads/main","./.git/logs/HEAD","./.git/refs/heads/main","./.git/objects/fc/daa658b54f15c125d37542fbc5c39063cc8ae5","./.git/COMMIT_EDITMSG","./README.md","./.git/config","./.git/HEAD","./.git/hooks/push-to-checkout.sample","./.git/hooks/update.sample","./.git/hooks/pre-push.sample","./.git/hooks/pre-applypatch.sample","./.git/hooks/pre-merge-commit.sample","./.git/hooks/post-update.sample","./.git/hooks/prepare-commit-msg.sample","./.git/hooks/pre-receive.sample","./.git/hooks/fsmonitor-watchman.sample","./.git/hooks/applypatch-msg.sample","./.git/hooks/pre-commit.sample","./.git/hooks/sendemail-validate.sample","./.git/hooks/pre-rebase.sample","./.git/hooks/commit-msg.sample","./.git/description","./.git/info/exclude"],"totalFiles":24}}}}}},"toolCallId":"tool_af1a9a15-a402-49c4-b5a6-395ff6a285e"}},{"thinkingMessage":{"text":"The workspace contains only a README with a title. No source files, tests, or infrastructure exist yet.","durationMs":558}},{"toolCall":{"grepToolCall":{"result":{"error":{"error":"Glob pattern \"**/*\" matches every file and is not allowed. Use a more specific glob or no glob."}}},"toolCallId":"tool_ac6f70b1-60ad-4482-afdc-7563df54fee"}},{"toolCall":{"globToolCall":{"args":{"targetDirectory":"/tmp/cursor-replay-proposed_plan","globPattern":"**/.*"},"result":{"success":{"path":"/tmp/cursor-replay-proposed_plan"}}},"toolCallId":"tool_13b532d9-49c8-4d2d-a01b-99b1f3a4fcb"}},{"thinkingMessage":{"text":"The workspace contains only a README.md and git metadata. No source files or test infrastructure were found.","durationMs":681}},{"toolCall":{"grepToolCall":{"result":{"error":{"error":"Glob pattern \"**/*\" matches every file and is not allowed. Use a more specific glob or no glob."}}},"toolCallId":"tool_0a725569-43c1-4226-8b51-48fef92f25d"}},{"toolCall":{"webSearchToolCall":{"args":{"searchTerm":"t3-orchestrator cursor agent sdk proposed_plan record replay","toolCallId":"tool_854579ea-3e01-40e1-ab7a-304bf71c37c"},"result":{"success":{"references":[{"title":"Web search results","chunk":"Links:\n1. [Cursor Python SDK | Cursor Documentation](https://cursor.com/docs/sdk/python)\n2. [GustavoWinter/cursor-agent-orchestrator-mcp](https://github.com/GustavoWinter/cursor-agent-orchestrator-mcp)\n3. [Build programmatic agents with the Cursor SDK · Cursor](https://cursor.com/blog/typescript-sdk)\n4. [@cursor/february](https://registry.npmjs.org/@cursor/february)\n5. [docs/development/cursor-backend.md at main · ralphkrauss/agent-orchestrator](https://github.com/ralphkrauss/agent-orchestrator/blob/main/docs/development/cursor-backend.md)\n\nSynthesis:\nTo record and replay plans using the Cursor SDK, you utilize `mode=\"plan\"` during agent creation or updates [1]. \n\nThe Cursor SDK itself does not include a native `proposed_plan` record/replay mechanism; such functionality is typically implemented via an orchestrator layer [2]. Using the `@cursor/sdk` TypeScript or Python packages, you control this flow as follows [3][1]:\n\n- Plan creation: Set `mode=\"plan\"` in `Agent.create` or `agent.send` to trigger plan-oriented output [1].\n- Record: You must manually record these events via the SDK's streaming interface (e.g., `run.stream()` in TypeScript or `on_delta`/`on_step` callbacks in Python) [4][1].\n- Replay: Replaying requires consuming these stored event logs and re-initiating the agent state via `Agent.resume(agentId)` [5][4][1].\n\nThird-party MCP orchestrators provide ready-made tools like `propose_plan` and `confirm_plan` to automate this [2]. These tools handle the persistence of plan states and approvals in an external JSON file, allowing you to gate execution until the plan is confirmed [2].\n\nHighlights:\n\nCursor Python SDK | Cursor Documentation\nhttps://cursor.com/docs/sdk/python\nThe async client mirrors the\n[...]\nsurface and is recommended for servers, bots, and\n[...]\ncursor_sdk\n[...]\n### Conversation mode\n[...]\nPass `mode=\"plan\"` or `mode=\"agent\"` to control whether a run explores and plans first or implements changes directly. See Plan mode for what plan mode does in the product.\n[...]\nSet `mode` on `Agent.create()` to seed the first run. On follow-up `agent.send()` calls, omit `mode` to keep the conversation's current mode, or pass `mode` to switch for that run only.\n[...]\n) as agent:\n[...]\n| Property | Type | Description |\n| --- | --- | --- |\n| `model` | `str | ModelSelection | Mapping[str, Any]` | Per-send model override. If omitted, uses `agent.model`. Sticky after a successful send. |\n| `mode` | `\"agent\" | \"plan\"` | Per-send conversation mode override. If omitted on follow-ups, keeps the conversation's current mode. |\n| `mcp_servers` | `Mapping[str, McpServerConfig]` | Inline MCP server definitions. Fully replaces creation-time servers for this run. |\n| `local.force` | `bool` | Local agents only. Defaults to `False`. Expire a stuck active run before starting this message. Cloud returns `409 agent_busy` server-side, so no equivalent is needed. |\n| `idempotency_key` | `str` | Optional client-generated idempotency key for the send. |\n| `on_step` | `Callable[[ConversationStep], Any]` | Callback after each\n[...]\nconversation step (text, thinking, or tool batch). |\n| `on_delta` | `Callable[[InteractionUpdate], Any]` | Callback per raw `InteractionUpdate`. |\n[...]\nUse `Agent.resume()` or `client.agents.resume()` to reattach to an existing agent by ID. Common flows: reconnecting to a long-running cloud agent that was kicked off earlier, or continuing a conversation after the local process restarted. Runtime is auto-detected from the ID prefix (`bc-` is cloud, anything else is local).\n[...]\n| `mode\n[...]\n| `\"agent\" | \"plan\n[...]\n`\"agent\"` | Initial conversation mode\n[...]\nConversation mode. |\n\n\n\nGustavoWinter/cursor-agent-orchestrator-mcp\nhttps://github.com/GustavoWinter/cursor-agent-orchestrator-mcp\nAn MCP server that runs many `@cursor/sdk` agents in parallel from one Cursor chat — locally or on Cursor Cloud — with a **propose → confirm → execute** flow and one merged event stream.\n[...]\n> **TL;DR**: Add the MCP to `.cursor/mcp.json` and set `CURSOR_API_KEY`. Your agent gets tools like `propose_plan`, `confirm_plan`, `execute_plan`, and `stream_plan`. `CURSOR_ORCH_PROPOSE_PLAN_MODE` controls when `propose_plan` may run (`auto` asks yes/no first; `manual` only after you ask for orchestration). `confirm_plan` gates `execute_plan` unless `CURSOR_ORCH_SKIP_CONFIRMATION=true` is set before the MCP starts.\n[...]\n_plan`** — the planner reads your repos\n[...]\nruns a single SDK agent\n[...]\ndiagram. It picks the right\n[...]\n1. **`confirm_plan`** — records your explicit approval. `execute_plan` is blocked until this is called (server-side, not just instructions).\n2. **`execute_plan`** — spawns N independent SDK agents in parallel (locally or on Cursor Cloud). Each subagent gets the repo context, project conventions (`.cursor/rules/`, skills), and its own prompt slice.\n3. **`stream_plan`** — merges all subagent event streams into one labeled feed so you see everything in one place.\n[...]\n| Tool | Purpose |\n| --- | --- |\n| `propose_plan` | Turn a goal into a JSON plan; does not run agents |\n| `confirm_plan` | Record your approval of a plan |\n| `execute_plan` | Spawn subagents and return a `planRunId` |\n| `stream_plan` | Pull merged labeled events until `done` |\n| `get_plan_status` | Snapshot run state + PR links |\n| `get_subagent_result` | Per-label result text and PR URL |\n| `cancel_plan` / `cancel_subagent` | Stop a run or one subagent |\n| `prompt_one_shot` | Single SDK agent without the planner |\n| `list_plans` | Recent plans and runs |\n| `prune_runs` | Remove old finished runs |\n| `attach_plan` | Re-attach to persisted run after restart (cancel only) |\n| `resume_plan` | Fully resume a persisted plan run after MCP restart |\n\n\n\nBuild programmatic agents with the Cursor SDK · Cursor\nhttps://cursor.com/blog/typescript-sdk\nThe Cursor SDK\n[...]\n. Run`npm\n[...]\n/sdk`\n[...]\n`/sdk` skill\n[...]\nyou build.\n[...]\nThe SDK uses our updated Cloud Agents API, which allows cloud agent runs to show up in Cursor's Agents Window and web app. You can start a task programmatically and then jump into Cursor to inspect progress or take over the work.\n[...]\njs example that creates\n[...]\nprompt, and streams the response.\n[...]\n- Prototyping tool: A web app for spinning up agents to scaffold\n[...]\n- Kanban board: An agent-\n[...]\nWe're introducing the Cursor SDK so you can build agents with the same runtime, harness, and models that power Cursor.\n[...]\n. The Cursor SDK lets you deploy\n[...]\nThe Cursor SDK is now available in public beta for all users. Run`npm install @cursor/sdk` to get started, then use Cursor's native`/sdk` skill for guidance as you build.\n[...]\nfrom the SDK run on the same optimized runtime we use\n[...]\nCloud Agents.\n[...]\nstrong sandboxing\n[...]\nAgents keep going when your laptop sleeps or network drops. You can stream the conversation and reconnect later. When the agent finishes, it can\n[...]\na PR, push a branch, or attach demos and screenshots.\n[...]\nThe SDK uses our updated Cloud Agents API, which allows cloud agent runs to show up in Cursor's Agents Window and web app. You can start a task programmatically and then jump into Cursor to inspect progress or take over the work.\n[...]\nusing the Cursor SDK to ship\n[...]\n. For example, programmatic agents that are kicked off\n[...]\nfrom CI/CD to summarize changes\n[...]\nidentify root causes for CI failures, and update PRs with fixes. Others are building\n[...]\nlet GTM\n[...]\nquery product data\n\n\n\n@cursor/february\nhttps://registry.npmjs.org/@cursor/february\nTypeScript SDK for Cursor agents (private alpha — codenamed).\n[...]\nThe primary API is the static `Agent` namespace. It creates durable agents, starts runs, streams normalized run events, waits for completion, resumes existing agents, and inspects persisted agent state.\n[...]\n- `run.stream()`: Async generator of normalized `SDKMessage` events.\n- `run.wait()`: Resolves to a terminal `RunResult`.\n- `run.cancel()`: Cancels a supported running run.\n- `run.conversation()`: Returns accumulated `ConversationTurn[]` for this run.\n- `run.supports(operation)`: Checks whether `stream`, `wait`, `cancel`, or `conversation` is supported.\n- `run.unsupportedReason(operation)`: Explains why an operation is unavailable.\n- `run.onDidChangeStatus(listener)`: Subscribes to run status changes.\n[...]\n`run.stream()` emits `SDKMessage` events. This is the stable public stream surface shared by local and cloud runs.\n[...]\nLive local runs can accumulate tool steps from raw executor deltas. Persisted replay reconstructs conversation from `SDKMessage` events, so replayed conversations can include tool-call steps when the persisted stream contains tool payload data.\n[...]\n`Agent.messages.list(...)` reads persisted conversation/checkpoint messages. It is complementary to `run.conversation()`, which is scoped to one run.\n[...]\n- Persisted conversation replay can only reconstruct information present in the public `SDKMessage` stream.\n- Inline MCP server definitions are not persisted across `Agent.resume(...)`.\n- Artifact download support is not implemented for local SDK agents yet.\n\n\n\ndocs/development/cursor-backend.md at main · ralphkrauss/agent-orchestrator\nhttps://github.com/ralphkrauss/agent-orchestrator/blob/main/docs/development/cursor-backend.md\n```md\n# Cursor Backend (Local SDK Runtime)\n[...]\nThe `cursor` backend runs Cursor agents in-process through the\n[`@cursor/sdk`](https://www.npmjs.com/package/@cursor/sdk) TypeScript SDK.\n[...]\nUnlike the `codex` and `claude` backends, the cursor backend does not spawn a\nCLI subprocess: it loads the SDK in the daemon Node process and streams typed\nevents.\n[...]\nThis release ships the **local runtime only** (`local: { cwd }`). The agent\noperates against files on disk in the run's `cwd`. Cloud and self-hosted\nruntimes (`cloud: { repos, ... }`) are out of scope and tracked as follow-ups.\n[...]\n- subagents (`AgentDefinition`)\n[...]\nserver forwarding from the\n[...]\n| Setting | Cursor behavior |\n|---|---|\n| `model` | Required. Pass a Cursor-side model id (e.g. `composer-2`). Resume calls thread per-run model overrides through `SendOptions.model`. |\n| `reasoning_effort` | Rejected with `INVALID_INPUT`. The SDK exposes per-model parameter discovery (`Cursor.models.list()`) which is deferred. |\n| `service_tier` | Rejected with `INVALID_INPUT`. |\n| `worker_posture` | Optional; defaults to `'trusted'`. Under `'trusted'`, `Agent.create` and `Agent.resume` receive `local.settingSources: ['all']` so the SDK loads every ambient settings layer (project, user, team, MDM, plugins). Under `'restricted'`, the shim omits `settingSources` and the SDK falls back to its pre-#58 in-process defaults. |\n[...]\n## Sessions and resume\n[...]\nThe runtime persists the durable Cursor `agentId` as the run's `session_id`.\nFollow-ups call `Agent.resume(agentId, { apiKey, local: { cwd } })` and pass\nthe per-call `SendOptions.model` only when the follow-up changes the model.\n[...]\nA `Run.status === \"cancelled\"` is recorded with `runStatusOverride =\n\"cancelled\"` so the run finalizes as cancelled rather than completed.\n"}]}}},"toolCallId":"tool_854579ea-3e01-40e1-ab7a-304bf71c37c"}},{"thinkingMessage":{"text":"The workspace appears empty. No files are present to explore.","durationMs":868}},{"toolCall":{"readToolCall":{"args":{"path":"/tmp/cursor-replay-proposed_plan/.git/info/exclude"},"result":{"success":{"content":"# git ls-files --others --exclude-from=.git/info/exclude\n# Lines that start with '#' are comments.\n# For a project mostly in C, the following would be a good set of\n# exclude patterns (uncomment them if you want to use them):\n# *.[oa]\n# *~\n","totalLines":7,"fileSize":240,"path":"/tmp/cursor-replay-proposed_plan/.git/info/exclude","readRange":{"startLine":1,"endLine":7}}}},"toolCallId":"tool_cb4fafcf-08df-449a-8ee9-a1a0f78410d"}},{"toolCall":{"readToolCall":{"args":{"path":"/tmp/cursor-replay-proposed_plan/.git/refs/heads/main"},"result":{"success":{"content":"fcdaa658b54f15c125d37542fbc5c39063cc8ae5\n","totalLines":2,"fileSize":41,"path":"/tmp/cursor-replay-proposed_plan/.git/refs/heads/main","readRange":{"startLine":1,"endLine":2}}}},"toolCallId":"tool_a4044dd9-25ad-43ba-89e7-2315b0165c7"}},{"toolCall":{"globToolCall":{"args":{"targetDirectory":"/tmp/cursor-replay-proposed_plan","globPattern":"**/*.{ts,tsx,js,py,json,yml,yaml,md}"},"result":{"success":{"path":"/tmp/cursor-replay-proposed_plan","files":["README.md"],"totalFiles":1}}},"toolCallId":"tool_c7276bc4-70a6-4065-8ecc-d5002bd2aec"}},{"thinkingMessage":{"text":"The workspace is a greenfield scaffold containing only a README.","durationMs":979}},{"toolCall":{"globToolCall":{"result":{"error":{"error":"rg: ../../../../../../Users/julius/.cursor/projects: IO error for operation on ../../../../../../Users/julius/.cursor/projects: No such file or directory (os error 2)\n"}}},"toolCallId":"tool_1bbd05f9-8821-42a4-aaa1-748344ca8f2"}},{"toolCall":{"grepToolCall":{"args":{"pattern":"t3-orchestrator|proposed_plan|cursor-agent-sdk","glob":"*.jsonl","caseInsensitive":false,"headLimit":30,"multiline":false,"toolCallId":"tool_726cab56-6097-4b7c-a4d3-493856329fb","offset":0},"result":{"success":{"pattern":"t3-orchestrator|proposed_plan|cursor-agent-sdk","outputMode":"content","workspaceResults":{"/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW":{"content":{"headLimitApplied":30}}}}}},"toolCallId":"tool_726cab56-6097-4b7c-a4d3-493856329fb"}},{"assistantMessage":{"text":"## Executive summary\n\nThis workspace is a **greenfield scaffold**, not an existing orchestrator or SDK codebase. After a full file search and keyword scan, the only tracked source file is `README.md` (one line). There is **no** replay, recording, fixture, test, or mock infrastructure to analyze yet.\n\n---\n\n## 1. What is this project?\n\n### What exists in the repo\n\n```\nt3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW/\n├── README.md # sole source file\n└── .git/ # single initial commit (\"initial\")\n```\n\nThe entire README:\n\n```1:1:README.md\n# cursor-agent-sdk-record-proposed_plan\n```\n\nGit metadata suggests this was created by an automated test harness:\n\n- Author: `T3 Code Test `\n- One commit: `fcdaa658` — message `\"initial\"`\n- No remote, no `package.json`, no source tree, no tests\n\n### Inferred intent (from naming only)\n\nThe directory name `t3-orchestrator-v2-cursor-agent-sdk-record-proposed_plan-ggwFWW` strongly suggests a **planned feature task**, not an implemented repo:\n\n| Name segment | Likely meaning |\n|---|---|\n| `t3-orchestrator-v2` | Second version of a T3 test orchestrator |\n| `cursor-agent-sdk` | Integration with Cursor’s programmatic Agent SDK |\n| `record-proposed_plan` | Record `proposed_plan` events/states for deterministic replay |\n| `ggwFWW` | Ephemeral sandbox ID |\n\nThere is **no orchestrator code, no SDK wrapper, and no `proposed_plan` type** in this workspace.\n\n---\n\n## 2. Replay / recording / fixture infrastructure\n\n### Keyword search results (entire repo)\n\n| Keyword | Matches |\n|---|---|\n| `proposed_plan` | `README.md` title only |\n| `record` | Substring of README title only |\n| `replay` | **0** |\n| `fixture` | **0** |\n| `deterministic` | **0** |\n| `snapshot` | **0** |\n| `mock` | **0** |\n| `orchestrator` | **0** (only in path name) |\n| `cursor-agent-sdk` | **0** (only in path/README title) |\n\n**Conclusion:** No recording, replay, fixture, or persistence layer exists.\n\n---\n\n## 3. Test patterns, mocks, deterministic testing\n\n**None present.** Specifically absent:\n\n- No test runner config (`package.json`, `vitest.config.*`, `jest.config.*`, `pytest.ini`, etc.)\n- No `__tests__/`, `test/`, or `*.test.*` / `*.spec.*` files\n- No mock/stub utilities\n- No snapshot or golden-file tests\n- No VCR/cassette-style HTTP recording\n- No seed/time/randomness controls for determinism\n\n---\n\n## 4. Project structure overview\n\n| Path | Role |\n|---|---|\n| `README.md` | Project title placeholder |\n| `.git/` | Initial commit with README only |\n| *(everything else)* | **Does not exist** |\n\nExpected structure for the task implied by the name (not yet present):\n\n```\nsrc/\n orchestrator/ # plan lifecycle, subagent coordination\n cursor-sdk/ # Agent.create, run.stream(), Agent.resume()\n record/ # event capture during propose_plan\n replay/ # fixture playback for tests\nfixtures/\n proposed_plan/ # recorded plan JSON + SDK event streams\ntests/\n replay/ # deterministic replay tests\n```\n\n---\n\n## 5. Current gaps for deterministic replay fixtures\n\nBecause there is no code, **all** of the following are gaps:\n\n1. **Event schema** — No type for `proposed_plan` or recorded SDK events (`SDKMessage`, tool calls, deltas).\n2. **Recorder** — No hook on agent streaming (`run.stream()`, `on_delta`, `on_step`) to persist events.\n3. **Fixture format** — No JSON/YAML convention for plans + event sequences + metadata (model, mode, cwd).\n4. **Replayer** — No substitute transport that feeds recorded events instead of live SDK calls.\n5. **Determinism controls** — No fixed clocks, idempotency keys, or stripped nondeterministic fields (timestamps, IDs).\n6. **Test harness** — No assertions comparing replay output to expected snapshots.\n7. **Orchestrator integration** — No `propose_plan` → `confirm_plan` → `execute_plan` flow to record.\n\n---\n\n## 6. Types/interfaces that would need extension\n\n**None exist in this repo.** Based on the task name and typical Cursor SDK + orchestrator patterns, you would likely need:\n\n```typescript\n// Hypothetical — NOT in codebase\n\ninterface ProposedPlan {\n id: string;\n goal: string;\n subagents: SubagentSpec[];\n // ...\n}\n\ninterface RecordedRun {\n mode: 'plan' | 'agent';\n agentId?: string;\n events: SDKMessage[]; // from run.stream()\n proposedPlan?: ProposedPlan;\n recordedAt: string;\n}\n\ninterface ReplayFixture {\n name: string;\n input: { prompt: string; mode: 'plan' };\n recorded: RecordedRun;\n expected?: { finalStatus: string; planHash?: string };\n}\n\ninterface RecordReplayTransport {\n record(run: AgentRun): AsyncIterable;\n replay(fixture: ReplayFixture): AsyncIterable;\n}\n```\n\nPublic Cursor SDK concepts relevant here (external, not in repo):\n\n- `run.stream()` → normalized `SDKMessage` events (stable replay surface)\n- `Agent.resume(agentId)` → reconnect to persisted agents\n- `mode: \"plan\"` → plan-mode runs\n- Persisted replay reconstructs conversation from `SDKMessage` streams\n\n---\n\n## 7. Suggested integration points (when code is added)\n\nSince there is no code to anchor to, these are **architectural integration points** aligned with the task name:\n\n### A. SDK stream boundary (primary record point)\n\nWrap `Agent.create()` / `run.stream()`:\n\n```\nLive: prompt → Agent.create → run.stream() → [RECORDER] → consumer\nReplay: fixture → [REPLAYER] → consumer (same interface as stream)\n```\n\nRecord every `SDKMessage`; normalize timestamps/IDs before writing fixtures.\n\n### B. Orchestrator plan lifecycle\n\nIf following a propose → confirm → execute model:\n\n| Phase | Record | Replay |\n|---|---|---|\n| `propose_plan` | Full plan JSON + planning stream | Feed recorded plan stream; skip LLM |\n| `confirm_plan` | Approval metadata | Inject fixed approval |\n| `execute_plan` | Per-subagent streams | Replay subagent fixtures in parallel |\n\n### C. Transport abstraction\n\n```typescript\ninterface AgentTransport {\n create(options: CreateOptions): Promise;\n stream(handle: AgentHandle): AsyncIterable;\n}\n\nclass LiveSdkTransport implements AgentTransport { /* @cursor/sdk */ }\nclass FixtureReplayTransport implements AgentTransport { /* read fixtures/ */ }\n```\n\nTests inject `FixtureReplayTransport`; production uses `LiveSdkTransport`.\n\n### D. Fixture layout (suggested)\n\n```\nfixtures/\n proposed_plan/\n simple-refactor/\n meta.json # mode, model, cwd\n plan.json # proposed plan artifact\n events.ndjson # one SDKMessage per line\n expected.json # optional assertions\n```\n\n### E. Determinism checklist\n\n- Strip or normalize: `timestamp`, `runId`, `agentId`, token counts\n- Use stable ordering for parallel subagent events (e.g. sort by `label` + `sequence`)\n- Fixed `idempotency_key` on sends\n- Mock filesystem/git side effects in replay mode\n\n---\n\n## 8. What you can do next\n\nThis workspace has nothing to extend yet. To proceed you would need either:\n\n1. **The parent `t3-orchestrator-v2` repo** cloned or copied into this workspace, or \n2. **Agent mode** to scaffold the recorder/replayer/fixture layout from scratch.\n\nIf you have the orchestrator source elsewhere, point me at that repo (or switch to Agent mode with it checked out) and I can map concrete files, existing types, and exact hook points instead of this inferred architecture."}}],"agentId":"95b8b4ba-cc91-4455-9389-9729e569f76b","isBackground":false,"durationMs":45981,"backgroundReason":"unspecified"}}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-0-yjwv"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1912}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":"The codebase is empty"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" aside from a README."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" I will draft a proposed"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":6}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" plan for deterministic"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":6}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" replay fixtures."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"thinking-completed","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-completed","thinkingDurationMs":562}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":"Checking agent"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" transcripts for"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" prior context on this"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" task"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":".\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":41}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-started","callId":"tool_2fcd3afc-c41d-42ca-9978-6944952eba2","toolCall":{"type":"glob","args":{"globPattern":"**/*","targetDirectory":"/Users/julius/.cursor/projects/var-folders-b3-b51-pdxj7dl0t981zpqkxxhr0000gn-T-t3-orchestrator-v2-cursor-agent-sdk-record-proposed-plan-ggwFWW/agent-transcripts"}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-1-e5et"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":31}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-started","callId":"tool_240db1e2-13d0-4012-9b2a-90ad32b0ec5","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-proposed_plan/README.md"}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-1-e5et"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":107}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-completed","callId":"tool_240db1e2-13d0-4012-9b2a-90ad32b0ec5","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-proposed_plan/README.md"},"result":{"status":"success","value":{"content":"# cursor-agent-sdk-record-proposed_plan\n","totalLines":2,"fileSize":40}}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-1-e5et"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":10}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":"The workspace is empty."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" I am drafting a short"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":6}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" implementation plan"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" for deterministic replay"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":6}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" fixtures based on typical"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":7}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" Cursor Agent SDK and"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-delta","text":" orchestrator patterns."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":6}}} +{"type":"emit_inbound","label":"thinking-completed","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"thinking-completed","thinkingDurationMs":1027}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":"The"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" repo is a"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" greenfield scaffold;"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" drafting"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" a concise"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" plan"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" from the project"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" name and standard"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" Cursor Agent SDK replay"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":6}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":" patterns"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"text-delta","text":".\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"partial-tool-call","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"partial-tool-call","callId":"tool_27b151a5-1a11-410a-a412-5325c4f7d2c","toolCall":{"type":"createPlan","args":{"plan":""}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-2-z9b0"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":8}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":49}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"partial-tool-call","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"partial-tool-call","callId":"tool_27b151a5-1a11-410a-a412-5325c4f7d2c","toolCall":{"type":"createPlan","args":{"plan":"# Deterministic Replay Fixtures for `proposed_plan`\n\n## Context\n\nThe workspace is a greenfield scaffold ([README.md](README.md)). This plan introduces the minimum structure to **record** plan-mode SDK streams and **replay** them as fixtures for deterministic tests.\n\n## Architecture\n\n```mermaid\nflowchart LR\n subgraph live [Live mode]\n Prompt --> LiveTransport\n LiveTransport --> SDK[\"Cursor Agent SDK\"]\n SDK --> Recorder\n Recorder --> Consumer\n Recorder --> FixtureFile\n end\n\n subgraph replay [Replay mode]\n FixtureFile2[Fixture file] --> ReplayTransport\n ReplayTransport --> Consumer2[Same consumer code]\n end\n```\n\n**Key idea:** Orchestrator code consumes `AsyncIterable` from a transport interface; production uses live SDK, tests use fixture replay.\n\n## 1. Define core types\n\nAdd [`src/replay/types.ts`](src/replay/types.ts):\n\n- `ProposedPlanFixture` — `{ name, input: { prompt, mode: \"plan\" }, events: SDKMessage[], plan?: unknown, meta: { model?, cwd? } }`\n- `AgentTransport` — `{ create(opts), stream(handle): AsyncIterable }`\n- `RecordOptions` / `ReplayOptions` — fixture path, normalize flag\n\nReuse SDK message shapes from `@cursor/agent-sdk` (or a thin local mirror) so fixtures stay aligned with `run.stream()` output.\n\n## 2. Fixture format and layout\n\n```\nfixtures/proposed_plan/\n /\n meta.json # input prompt, mode, model, cwd\n events.ndjson # one SDKMessage per line (post-normalization)\n plan.json # extracted proposed plan artifact (optional convenience)\n expected.json # optional assertions (status, plan hash, key fields)\n```\n\n- NDJSON keeps diffs readable and supports streaming replay.\n- Commit normalized fixtures only (no secrets, no absolute paths).\n\n## 3. Normalization for determinism\n\nAdd [`src/replay/normalize.ts`](src/replay/normalize.ts) to strip or replace volatile fields before write/compare:\n\n- Timestamps, run/agent IDs, token counts, latency fields\n- Sort parallel sub-events by stable key (`type` + `sequence` or `label`)\n- Replace cwd/home with placeholders (e.g. `$WORKSPACE`)\n\nApply normalization in both **recorder** (on write) and **test assertions** (on compare).\n\n## 4. Implement recorder\n\nAdd [`src/replay/recorder.ts`](src/replay/recorder.ts):\n\n- Wrap live `AgentTransport.stream()` and tee events to consumer + buffer\n- On stream end, extract `proposed_plan` payload from terminal plan message(s)\n- Write fixture directory via `writeFixture(name, data)`\n\nCLI entrypoint [`src/cli/record-fixture.ts`](src/cli/record-fixture.ts):\n\n```bash\npnpm record-fixture --name simple-refactor --prompt \"...\" --mode plan\n```\n\n## 5. Implement replay transport\n\nAdd [`src/replay/replay-transport.ts`](src/replay/replay-transport.ts):\n\n- Load fixture by name/path\n- Yield `events.ndjson` lines as `SDKMessage` with configurable tick delay (default 0)\n- Implement `AgentTransport` so orchestrator plan flow is unchanged\n\nWire via env or factory in [`src/transport/index.ts`](src/transport/index.ts):\n\n```typescript\nexport function createTransport(): AgentTransport {\n return process.env.REPLAY_FIXTURE\n ? new ReplayTransport(process.env.REPLAY_FIXTURE)\n : new LiveSdkTransport();\n}\n```\n\n## 6. Orchestrator hook: `proposed_plan`\n\nAdd [`src/orchestrator/propose-plan.ts`](src/orchestrator/propose-plan.ts):\n\n1. `createTransport()` → `create({ mode: \"plan\", prompt })`\n2. Consume stream until plan-complete message\n3. Return parsed plan + raw events (for optional inline recording in dev)\n\nThis is the single integration point both live and replay modes share.\n\n## 7. Deterministic tests\n\nAdd [`tests/replay/proposed-plan.test.ts`](tests/replay/proposed-plan.test.ts):\n\n- Set `REPLAY_FIXTURE=fixtures/proposed_plan/simple-refactor`\n- Run `proposePlan(prompt)` and assert:\n - Stable plan hash or snapshot of normalized `plan.json`\n - Expected status from `expected.json`\n- Add one golden fixture recorded from a real run, then lock behavior\n\nUse Vitest (or project-standard runner) with `fixtures/` as test data.\n\n## 8. Deliverables (minimal scope)\n\n| File | Purpose |\n|------|---------|\n| `src/replay/types.ts` | Shared fixture + transport types |\n| `src/replay/normalize.ts` | Deterministic field stripping |\n| `src/replay/recorder.ts` | Live capture → fixture |\n| `src/replay/replay-transport.ts` | Fixture → stream |\n| `src/transport/live-sdk-transport.ts` | Thin SDK wrapper |\n| `src/orchestrator/propose-plan.ts` | Plan-mode orchestration |\n| `src/cli/record-fixture.ts` | One-shot recording CLI |\n| `fixtures/proposed_plan/simple-refactor/` | Seed fixture |\n| `tests/replay/proposed-plan.test.ts` | Replay regression test |\n\n## Success criteria\n\n- Same orchestrator code path works in live and replay modes.\n- Re-running replay tests produces identical normalized plan output across machines.\n- New fixtures can be recorded with one CLI command and committed as NDJSON + JSON.\n"}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-2-z9b0"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":1261}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":155}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-started","callId":"tool_27b151a5-1a11-410a-a412-5325c4f7d2c","toolCall":{"type":"createPlan","args":{"plan":"# Deterministic Replay Fixtures for `proposed_plan`\n\n## Context\n\nThe workspace is a greenfield scaffold ([README.md](README.md)). This plan introduces the minimum structure to **record** plan-mode SDK streams and **replay** them as fixtures for deterministic tests.\n\n## Architecture\n\n```mermaid\nflowchart LR\n subgraph live [Live mode]\n Prompt --> LiveTransport\n LiveTransport --> SDK[\"Cursor Agent SDK\"]\n SDK --> Recorder\n Recorder --> Consumer\n Recorder --> FixtureFile\n end\n\n subgraph replay [Replay mode]\n FixtureFile2[Fixture file] --> ReplayTransport\n ReplayTransport --> Consumer2[Same consumer code]\n end\n```\n\n**Key idea:** Orchestrator code consumes `AsyncIterable` from a transport interface; production uses live SDK, tests use fixture replay.\n\n## 1. Define core types\n\nAdd [`src/replay/types.ts`](src/replay/types.ts):\n\n- `ProposedPlanFixture` — `{ name, input: { prompt, mode: \"plan\" }, events: SDKMessage[], plan?: unknown, meta: { model?, cwd? } }`\n- `AgentTransport` — `{ create(opts), stream(handle): AsyncIterable }`\n- `RecordOptions` / `ReplayOptions` — fixture path, normalize flag\n\nReuse SDK message shapes from `@cursor/agent-sdk` (or a thin local mirror) so fixtures stay aligned with `run.stream()` output.\n\n## 2. Fixture format and layout\n\n```\nfixtures/proposed_plan/\n /\n meta.json # input prompt, mode, model, cwd\n events.ndjson # one SDKMessage per line (post-normalization)\n plan.json # extracted proposed plan artifact (optional convenience)\n expected.json # optional assertions (status, plan hash, key fields)\n```\n\n- NDJSON keeps diffs readable and supports streaming replay.\n- Commit normalized fixtures only (no secrets, no absolute paths).\n\n## 3. Normalization for determinism\n\nAdd [`src/replay/normalize.ts`](src/replay/normalize.ts) to strip or replace volatile fields before write/compare:\n\n- Timestamps, run/agent IDs, token counts, latency fields\n- Sort parallel sub-events by stable key (`type` + `sequence` or `label`)\n- Replace cwd/home with placeholders (e.g. `$WORKSPACE`)\n\nApply normalization in both **recorder** (on write) and **test assertions** (on compare).\n\n## 4. Implement recorder\n\nAdd [`src/replay/recorder.ts`](src/replay/recorder.ts):\n\n- Wrap live `AgentTransport.stream()` and tee events to consumer + buffer\n- On stream end, extract `proposed_plan` payload from terminal plan message(s)\n- Write fixture directory via `writeFixture(name, data)`\n\nCLI entrypoint [`src/cli/record-fixture.ts`](src/cli/record-fixture.ts):\n\n```bash\npnpm record-fixture --name simple-refactor --prompt \"...\" --mode plan\n```\n\n## 5. Implement replay transport\n\nAdd [`src/replay/replay-transport.ts`](src/replay/replay-transport.ts):\n\n- Load fixture by name/path\n- Yield `events.ndjson` lines as `SDKMessage` with configurable tick delay (default 0)\n- Implement `AgentTransport` so orchestrator plan flow is unchanged\n\nWire via env or factory in [`src/transport/index.ts`](src/transport/index.ts):\n\n```typescript\nexport function createTransport(): AgentTransport {\n return process.env.REPLAY_FIXTURE\n ? new ReplayTransport(process.env.REPLAY_FIXTURE)\n : new LiveSdkTransport();\n}\n```\n\n## 6. Orchestrator hook: `proposed_plan`\n\nAdd [`src/orchestrator/propose-plan.ts`](src/orchestrator/propose-plan.ts):\n\n1. `createTransport()` → `create({ mode: \"plan\", prompt })`\n2. Consume stream until plan-complete message\n3. Return parsed plan + raw events (for optional inline recording in dev)\n\nThis is the single integration point both live and replay modes share.\n\n## 7. Deterministic tests\n\nAdd [`tests/replay/proposed-plan.test.ts`](tests/replay/proposed-plan.test.ts):\n\n- Set `REPLAY_FIXTURE=fixtures/proposed_plan/simple-refactor`\n- Run `proposePlan(prompt)` and assert:\n - Stable plan hash or snapshot of normalized `plan.json`\n - Expected status from `expected.json`\n- Add one golden fixture recorded from a real run, then lock behavior\n\nUse Vitest (or project-standard runner) with `fixtures/` as test data.\n\n## 8. Deliverables (minimal scope)\n\n| File | Purpose |\n|------|---------|\n| `src/replay/types.ts` | Shared fixture + transport types |\n| `src/replay/normalize.ts` | Deterministic field stripping |\n| `src/replay/recorder.ts` | Live capture → fixture |\n| `src/replay/replay-transport.ts` | Fixture → stream |\n| `src/transport/live-sdk-transport.ts` | Thin SDK wrapper |\n| `src/orchestrator/propose-plan.ts` | Plan-mode orchestration |\n| `src/cli/record-fixture.ts` | One-shot recording CLI |\n| `fixtures/proposed_plan/simple-refactor/` | Seed fixture |\n| `tests/replay/proposed-plan.test.ts` | Replay regression test |\n\n## Success criteria\n\n- Same orchestrator code path works in live and replay modes.\n- Re-running replay tests produces identical normalized plan output across machines.\n- New fixtures can be recorded with one CLI command and committed as NDJSON + JSON.\n"}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-2-z9b0"}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"tool-call-completed","callId":"tool_27b151a5-1a11-410a-a412-5325c4f7d2c","toolCall":{"type":"createPlan","args":{"plan":"# Deterministic Replay Fixtures for `proposed_plan`\n\n## Context\n\nThe workspace is a greenfield scaffold ([README.md](README.md)). This plan introduces the minimum structure to **record** plan-mode SDK streams and **replay** them as fixtures for deterministic tests.\n\n## Architecture\n\n```mermaid\nflowchart LR\n subgraph live [Live mode]\n Prompt --> LiveTransport\n LiveTransport --> SDK[\"Cursor Agent SDK\"]\n SDK --> Recorder\n Recorder --> Consumer\n Recorder --> FixtureFile\n end\n\n subgraph replay [Replay mode]\n FixtureFile2[Fixture file] --> ReplayTransport\n ReplayTransport --> Consumer2[Same consumer code]\n end\n```\n\n**Key idea:** Orchestrator code consumes `AsyncIterable` from a transport interface; production uses live SDK, tests use fixture replay.\n\n## 1. Define core types\n\nAdd [`src/replay/types.ts`](src/replay/types.ts):\n\n- `ProposedPlanFixture` — `{ name, input: { prompt, mode: \"plan\" }, events: SDKMessage[], plan?: unknown, meta: { model?, cwd? } }`\n- `AgentTransport` — `{ create(opts), stream(handle): AsyncIterable }`\n- `RecordOptions` / `ReplayOptions` — fixture path, normalize flag\n\nReuse SDK message shapes from `@cursor/agent-sdk` (or a thin local mirror) so fixtures stay aligned with `run.stream()` output.\n\n## 2. Fixture format and layout\n\n```\nfixtures/proposed_plan/\n /\n meta.json # input prompt, mode, model, cwd\n events.ndjson # one SDKMessage per line (post-normalization)\n plan.json # extracted proposed plan artifact (optional convenience)\n expected.json # optional assertions (status, plan hash, key fields)\n```\n\n- NDJSON keeps diffs readable and supports streaming replay.\n- Commit normalized fixtures only (no secrets, no absolute paths).\n\n## 3. Normalization for determinism\n\nAdd [`src/replay/normalize.ts`](src/replay/normalize.ts) to strip or replace volatile fields before write/compare:\n\n- Timestamps, run/agent IDs, token counts, latency fields\n- Sort parallel sub-events by stable key (`type` + `sequence` or `label`)\n- Replace cwd/home with placeholders (e.g. `$WORKSPACE`)\n\nApply normalization in both **recorder** (on write) and **test assertions** (on compare).\n\n## 4. Implement recorder\n\nAdd [`src/replay/recorder.ts`](src/replay/recorder.ts):\n\n- Wrap live `AgentTransport.stream()` and tee events to consumer + buffer\n- On stream end, extract `proposed_plan` payload from terminal plan message(s)\n- Write fixture directory via `writeFixture(name, data)`\n\nCLI entrypoint [`src/cli/record-fixture.ts`](src/cli/record-fixture.ts):\n\n```bash\npnpm record-fixture --name simple-refactor --prompt \"...\" --mode plan\n```\n\n## 5. Implement replay transport\n\nAdd [`src/replay/replay-transport.ts`](src/replay/replay-transport.ts):\n\n- Load fixture by name/path\n- Yield `events.ndjson` lines as `SDKMessage` with configurable tick delay (default 0)\n- Implement `AgentTransport` so orchestrator plan flow is unchanged\n\nWire via env or factory in [`src/transport/index.ts`](src/transport/index.ts):\n\n```typescript\nexport function createTransport(): AgentTransport {\n return process.env.REPLAY_FIXTURE\n ? new ReplayTransport(process.env.REPLAY_FIXTURE)\n : new LiveSdkTransport();\n}\n```\n\n## 6. Orchestrator hook: `proposed_plan`\n\nAdd [`src/orchestrator/propose-plan.ts`](src/orchestrator/propose-plan.ts):\n\n1. `createTransport()` → `create({ mode: \"plan\", prompt })`\n2. Consume stream until plan-complete message\n3. Return parsed plan + raw events (for optional inline recording in dev)\n\nThis is the single integration point both live and replay modes share.\n\n## 7. Deterministic tests\n\nAdd [`tests/replay/proposed-plan.test.ts`](tests/replay/proposed-plan.test.ts):\n\n- Set `REPLAY_FIXTURE=fixtures/proposed_plan/simple-refactor`\n- Run `proposePlan(prompt)` and assert:\n - Stable plan hash or snapshot of normalized `plan.json`\n - Expected status from `expected.json`\n- Add one golden fixture recorded from a real run, then lock behavior\n\nUse Vitest (or project-standard runner) with `fixtures/` as test data.\n\n## 8. Deliverables (minimal scope)\n\n| File | Purpose |\n|------|---------|\n| `src/replay/types.ts` | Shared fixture + transport types |\n| `src/replay/normalize.ts` | Deterministic field stripping |\n| `src/replay/recorder.ts` | Live capture → fixture |\n| `src/replay/replay-transport.ts` | Fixture → stream |\n| `src/transport/live-sdk-transport.ts` | Thin SDK wrapper |\n| `src/orchestrator/propose-plan.ts` | Plan-mode orchestration |\n| `src/cli/record-fixture.ts` | One-shot recording CLI |\n| `fixtures/proposed_plan/simple-refactor/` | Seed fixture |\n| `tests/replay/proposed-plan.test.ts` | Replay regression test |\n\n## Success criteria\n\n- Same orchestrator code path works in live and replay modes.\n- Re-running replay tests produces identical normalized plan output across machines.\n- New fixtures can be recorded with one CLI command and committed as NDJSON + JSON.\n"},"result":{"status":"success","value":{}}},"modelCallId":"12792358-9978-4430-839d-d1aa84ab7471-2-z9b0"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"token-delta","tokens":7}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"step-completed","stepId":11,"stepDurationMs":54858}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","update":{"type":"turn-ended","usage":{"inputTokens":41151,"outputTokens":2171,"cacheReadTokens":30336,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-6fc6c9f5-f2b7-4f6e-8922-d479f68e7710","requestId":"12792358-9978-4430-839d-d1aa84ab7471","status":"finished","result":"The repo is a greenfield scaffold; drafting a concise plan from the project name and standard Cursor Agent SDK replay patterns.\n","model":{"id":"composer-2.5"},"durationMs":57537}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-bc5d59e0-bc8b-441a-9292-b49dfff962d3"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/input.ts new file mode 100644 index 00000000000..6d880c2f5c3 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/proposed_plan/input.ts @@ -0,0 +1,8 @@ +import { PROPOSED_PLAN_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function proposedPlanInput(): OrchestratorFixtureInput { + return { + interactionMode: "plan", + steps: [{ type: "message", text: PROPOSED_PLAN_PROMPT }], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/codex_transcript.ndjson new file mode 100644 index 00000000000..c8d19f6d076 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/codex_transcript.ndjson @@ -0,0 +1,78 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"provider_thread_resume","metadata":{"source":"codex-app-server-probe","fileName":"provider_thread_resume.ndjson","description":"One provider-native thread is started, completed, then resumed by thread id in a fresh app-server session."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019db117-b50d-7a01-a6b6-65e47d749c3e","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776792614,"updatedAt":1776792614,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T10-30-14-019db117-b50d-7a01-a6b6-65e47d749c3e.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.3-codex","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Respond with exactly: provider thread resume fixture first turn complete","type":"text"}],"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019db117-b50d-7a01-a6b6-65e47d749c3e","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776792614,"updatedAt":1776792614,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T10-30-14-019db117-b50d-7a01-a6b6-65e47d749c3e.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019db117-b519-7fa3-85c8-7762b26478a7","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turn":{"id":"019db117-b519-7fa3-85c8-7762b26478a7","items":[],"status":"inProgress","error":null,"startedAt":1776792614,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"ae32c5a2-6bec-4610-99f2-7827af1bb75b","content":[{"type":"text","text":"Respond with exactly: provider thread resume fixture first turn complete","text_elements":[]}]},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"ae32c5a2-6bec-4610-99f2-7827af1bb75b","content":[{"type":"text","text":"Respond with exactly: provider thread resume fixture first turn complete","text_elements":[]}]},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":8,"windowDurationMins":300,"resetsAt":1776799966},"secondary":{"usedPercent":2,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_06068603eaa6e1e70169e7b42dd9fc819696b725f447365db0","summary":[],"content":[]},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_06068603eaa6e1e70169e7b42dd9fc819696b725f447365db0","summary":[],"content":[]},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_06068603eaa6e1e70169e7b42e9cac819687b02433006e736b","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7","itemId":"msg_06068603eaa6e1e70169e7b42e9cac819687b02433006e736b","delta":"provider"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7","itemId":"msg_06068603eaa6e1e70169e7b42e9cac819687b02433006e736b","delta":" thread"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7","itemId":"msg_06068603eaa6e1e70169e7b42e9cac819687b02433006e736b","delta":" resume"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7","itemId":"msg_06068603eaa6e1e70169e7b42e9cac819687b02433006e736b","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7","itemId":"msg_06068603eaa6e1e70169e7b42e9cac819687b02433006e736b","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7","itemId":"msg_06068603eaa6e1e70169e7b42e9cac819687b02433006e736b","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7","itemId":"msg_06068603eaa6e1e70169e7b42e9cac819687b02433006e736b","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_06068603eaa6e1e70169e7b42e9cac819687b02433006e736b","text":"provider thread resume fixture first turn complete","phase":"final_answer","memoryCitation":null},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-b519-7fa3-85c8-7762b26478a7","tokenUsage":{"total":{"totalTokens":25863,"inputTokens":25810,"cachedInputTokens":4480,"outputTokens":53,"reasoningOutputTokens":40},"last":{"totalTokens":25863,"inputTokens":25810,"cachedInputTokens":4480,"outputTokens":53,"reasoningOutputTokens":40},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":8,"windowDurationMins":300,"resetsAt":1776799966},"secondary":{"usedPercent":2,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turn":{"id":"019db117-b519-7fa3-85c8-7762b26478a7","items":[],"status":"completed","error":null,"startedAt":1776792614,"completedAt":1776792622,"durationMs":8794}}}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/resume","frame":{"id":2,"method":"thread/resume","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e"}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"thread/resume","frame":{"id":2,"result":{"thread":{"id":"019db117-b50d-7a01-a6b6-65e47d749c3e","forkedFromId":null,"preview":"Respond with exactly: provider thread resume fixture first turn complete","ephemeral":false,"modelProvider":"openai","createdAt":1776792614,"updatedAt":1776792622,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T10-30-14-019db117-b50d-7a01-a6b6-65e47d749c3e.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":{"sha":"7eefb2eb1d98f6b814c9fd7f43652727059c3326","branch":"t3code/codex-turn-mapping","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019db117-b519-7fa3-85c8-7762b26478a7","items":[{"type":"userMessage","id":"item-1","content":[{"type":"text","text":"Respond with exactly: provider thread resume fixture first turn complete","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"provider thread resume fixture first turn complete","phase":"final_answer","memoryCitation":null}],"status":"completed","error":null,"startedAt":1776792614,"completedAt":1776792622,"durationMs":8794}]},"model":"gpt-5.3-codex","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Using the conversation history available in this resumed thread, first repeat the exact final answer you gave in the previous turn. Then on a new line write exactly: provider thread resume fixture second turn complete","type":"text"}],"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019db117-d7ac-7462-90cf-8e244a290f78","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turn":{"id":"019db117-d7ac-7462-90cf-8e244a290f78","items":[],"status":"inProgress","error":null,"startedAt":1776792623,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"bbc9d0c6-a0cf-420f-8376-64b7b6f2671f","content":[{"type":"text","text":"Using the conversation history available in this resumed thread, first repeat the exact final answer you gave in the previous turn. Then on a new line write exactly: provider thread resume fixture second turn complete","text_elements":[]}]},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"bbc9d0c6-a0cf-420f-8376-64b7b6f2671f","content":[{"type":"text","text":"Using the conversation history available in this resumed thread, first repeat the exact final answer you gave in the previous turn. Then on a new line write exactly: provider thread resume fixture second turn complete","text_elements":[]}]},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","tokenUsage":{"total":{"totalTokens":25863,"inputTokens":25810,"cachedInputTokens":4480,"outputTokens":53,"reasoningOutputTokens":40},"last":{"totalTokens":25863,"inputTokens":25810,"cachedInputTokens":4480,"outputTokens":53,"reasoningOutputTokens":40},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":8,"windowDurationMins":300,"resetsAt":1776799966},"secondary":{"usedPercent":2,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_026c2c40daf929d70169e7b43983148195b9f744d3c959d778","summary":[],"content":[]},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_026c2c40daf929d70169e7b43983148195b9f744d3c959d778","summary":[],"content":[]},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":"provider"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" thread"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" resume"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" complete"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":"provider"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" thread"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" resume"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" second"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","itemId":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_026c2c40daf929d70169e7b43a9ae88195b509f693bda7ff89","text":"provider thread resume fixture first turn complete\nprovider thread resume fixture second turn complete","phase":"final_answer","memoryCitation":null},"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turnId":"019db117-d7ac-7462-90cf-8e244a290f78","tokenUsage":{"total":{"totalTokens":51845,"inputTokens":51717,"cachedInputTokens":30336,"outputTokens":128,"reasoningOutputTokens":94},"last":{"totalTokens":25982,"inputTokens":25907,"cachedInputTokens":25856,"outputTokens":75,"reasoningOutputTokens":54},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":8,"windowDurationMins":300,"resetsAt":1776799966},"secondary":{"usedPercent":2,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019db117-b50d-7a01-a6b6-65e47d749c3e","turn":{"id":"019db117-d7ac-7462-90cf-8e244a290f78","items":[],"status":"completed","error":null,"startedAt":1776792623,"completedAt":1776792634,"durationMs":11956}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/cursor_transcript.ndjson new file mode 100644 index 00000000000..5adf0d528c1 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/cursor_transcript.ndjson @@ -0,0 +1,60 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"provider_thread_resume","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-06c9d7f0-c6a5-4194-b204-8aa8ddb04d10"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":false,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-06c9d7f0-c6a5-4194-b204-8aa8ddb04d10"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Respond with exactly: provider thread resume fixture first turn complete","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","agentId":"agent-06c9d7f0-c6a5-4194-b204-8aa8ddb04d10"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"text-delta","text":"provider"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"text-delta","text":" thread"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"text-delta","text":" resume"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"text-delta","text":" fixture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"text-delta","text":" first"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"text-delta","text":" turn"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"text-delta","text":" complete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"step-completed","stepId":1,"stepDurationMs":766}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","update":{"type":"turn-ended","usage":{"inputTokens":10274,"outputTokens":45,"cacheReadTokens":1856,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-2aa4fc7d-3102-4d95-b449-2fb5e8703b5b","requestId":"ff15a3d5-de92-4d4c-ae3d-2b627b3d4839","status":"finished","result":"provider thread resume fixture first turn complete","model":{"id":"composer-2.5"},"durationMs":5298}}} +{"type":"expect_outbound","label":"agent.close:before-prompt-2","frame":{"type":"agent.close","agentId":"agent-06c9d7f0-c6a5-4194-b204-8aa8ddb04d10"}} +{"type":"expect_outbound","label":"agent.resume:before-prompt-2","frame":{"type":"agent.open","operation":"resume","agentId":"agent-06c9d7f0-c6a5-4194-b204-8aa8ddb04d10","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":false,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.resumed:before-prompt-2","frame":{"type":"agent.opened","agentId":"agent-06c9d7f0-c6a5-4194-b204-8aa8ddb04d10"}} +{"type":"expect_outbound","label":"run.start:2","frame":{"type":"run.start","message":"Using the conversation history available in this resumed thread, first repeat the exact final answer you gave in the previous turn. Then on a new line write exactly: provider thread resume fixture second turn complete","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:2","frame":{"type":"run.started","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","agentId":"agent-06c9d7f0-c6a5-4194-b204-8aa8ddb04d10"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":"provider"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" thread"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" resume"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" fixture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" first"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" turn"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" complete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":"\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":"provider"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" thread"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" resume"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" fixture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" second"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" turn"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"text-delta","text":" complete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"step-completed","stepId":1,"stepDurationMs":773}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","update":{"type":"turn-ended","usage":{"inputTokens":10369,"outputTokens":101,"cacheReadTokens":10304,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:2","frame":{"type":"run.completed","result":{"id":"run-57e0a96b-f734-4919-b59a-8fbdfa9a8880","requestId":"3822e867-4c17-44fb-81b9-e0f3d76b0c87","status":"finished","result":"provider thread resume fixture first turn complete\nprovider thread resume fixture second turn complete","model":{"id":"composer-2.5"},"durationMs":4461}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-06c9d7f0-c6a5-4194-b204-8aa8ddb04d10"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/claude_transcript.ndjson new file mode 100644 index 00000000000..ed3dd64c83e --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/claude_transcript.ndjson @@ -0,0 +1,14 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"queued_turn","metadata":{"prompts":["Respond with exactly: first fixture turn complete","Respond with exactly: second fixture turn complete"],"model":"claude-sonnet-4-6","nativeSessionId":"df83c769-3523-4961-95ad-9739bfdf71dd","queryMode":"streaming","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"df83c769-3523-4961-95ad-9739bfdf71dd"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: first fixture turn complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"cbcb8e66-b64a-49c2-a192-5928cf12f605","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"e1ba5cbc-e83f-452d-a12e-2decc801d1df","session_id":"df83c769-3523-4961-95ad-9739bfdf71dd"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"cbcb8e66-b64a-49c2-a192-5928cf12f605","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"9a8d10e1-b662-44f5-873a-cddcb1f95cd0","session_id":"df83c769-3523-4961-95ad-9739bfdf71dd"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-queued_turn","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"86533c51-7a5a-4034-b94f-12b1b6dd6472","session_id":"df83c769-3523-4961-95ad-9739bfdf71dd"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01NQEE1Hdycdhefjehrr9zcz","type":"message","role":"assistant","content":[{"type":"text","text":"first fixture turn complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2337,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2337},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"df83c769-3523-4961-95ad-9739bfdf71dd","uuid":"284c2284-4cd0-4f23-8e0d-32ad924e9227"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"ab9548dc-42c4-4c53-bfb3-765384bd3c8c","session_id":"df83c769-3523-4961-95ad-9739bfdf71dd"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":1429,"duration_api_ms":1065,"num_turns":1,"result":"first fixture turn complete","stop_reason":"end_turn","session_id":"df83c769-3523-4961-95ad-9739bfdf71dd","total_cost_usd":0.01171425,"usage":{"input_tokens":3,"cache_creation_input_tokens":2337,"cache_read_input_tokens":9455,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2337,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":9455,"cache_creation_input_tokens":2337,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2337},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":7,"cacheReadInputTokens":9455,"cacheCreationInputTokens":2337,"webSearchRequests":0,"costUSD":0.01171425,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"bd5d5c48-b577-45f8-a0ef-d45fce2ab84d"}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: second fixture turn complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-queued_turn","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"9e812fd7-f51a-49f6-878e-b95360f3da63","session_id":"df83c769-3523-4961-95ad-9739bfdf71dd"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01TLFS8vZYtEwwSUSbBztEFJ","type":"message","role":"assistant","content":[{"type":"text","text":"second fixture turn complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":19,"cache_read_input_tokens":11792,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":19},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"df83c769-3523-4961-95ad-9739bfdf71dd","uuid":"39376d80-6475-4fbc-a95c-b005f83dcfee"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":1866,"duration_api_ms":2133,"num_turns":1,"result":"second fixture turn complete","stop_reason":"end_turn","session_id":"df83c769-3523-4961-95ad-9739bfdf71dd","total_cost_usd":0.0154371,"usage":{"input_tokens":3,"cache_creation_input_tokens":19,"cache_read_input_tokens":11792,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":19,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":11792,"cache_creation_input_tokens":19,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":19},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":6,"outputTokens":14,"cacheReadInputTokens":21247,"cacheCreationInputTokens":2356,"webSearchRequests":0,"costUSD":0.0154371,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"5e21b6c3-b1e0-4d8c-a106-d34a14e941ee"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/codex_output.ts new file mode 100644 index 00000000000..5eac40193b5 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/codex_output.ts @@ -0,0 +1,49 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertConversationMessageRoles, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessageInputIntents, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + MULTI_TURN_FIRST_PROMPT, + MULTI_TURN_SECOND_PROMPT, + projectionFor, +} from "../shared.ts"; + +export function assertQueuedTurnOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ + result, + transcript, + runCount: 2, + runStatuses: ["completed", "completed"], + }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1, 2]); + assertConversationMessageRoles(projection, ["user", "assistant", "user", "assistant"]); + assertTurnItemTypes(projection, ["user_message", "assistant_message"]); + assertUserMessagesInclude(projection, [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT]); + assertUserMessageInputIntents(projection, ["turn_start", "queued_turn"]); + assert.equal(projection.turnItems.filter((item) => item.type === "user_message").length, 2); + assertAssistantTextIncludes(projection, "first fixture turn complete"); + assertAssistantTextIncludes(projection, "second fixture turn complete"); + + const run2Events = result.domainEvents + .filter((event) => event.type === "run.created" || event.type === "run.updated") + .filter((event) => event.runId === projection.runs[1]?.id); + assert.equal(run2Events[0]?.type, "run.created"); + assert.equal(run2Events[0]?.payload.status, "queued"); + assert.isTrue(run2Events.some((event) => event.payload.status === "running")); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/codex_transcript.ndjson new file mode 100644 index 00000000000..d8fd90960d5 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/codex_transcript.ndjson @@ -0,0 +1,52 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"queued_turn","metadata":{"source":"codex-app-server-probe","fileName":"queued_turn.ndjson","description":"One thread with a queued second user turn."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019dadec-c14b-7b13-b96c-92c98ccfc342","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739467,"updatedAt":1776739467,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-44-27-019dadec-c14b-7b13-b96c-92c98ccfc342.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Respond with exactly: first fixture turn complete","type":"text"}],"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019dadec-c14b-7b13-b96c-92c98ccfc342","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739467,"updatedAt":1776739467,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-44-27-019dadec-c14b-7b13-b96c-92c98ccfc342.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019dadec-c155-74a2-8acc-f9f4ff86c15b","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turn":{"id":"019dadec-c155-74a2-8acc-f9f4ff86c15b","items":[],"status":"inProgress","error":null,"startedAt":1776739467,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"2562fa0b-0d7c-4030-9be5-ece4962d27bd","content":[{"type":"text","text":"Respond with exactly: first fixture turn complete","text_elements":[]}]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"2562fa0b-0d7c-4030-9be5-ece4962d27bd","content":[{"type":"text","text":"Respond with exactly: first fixture turn complete","text_elements":[]}]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0110066f30127def0169e6e4920934819996efbb3a16f7bded","summary":[],"content":[]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0110066f30127def0169e6e4920934819996efbb3a16f7bded","summary":[],"content":[]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","itemId":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","delta":"first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","itemId":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","itemId":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","itemId":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0110066f30127def0169e6e49249948199913f38f859d73ca9","text":"first fixture turn complete","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-c155-74a2-8acc-f9f4ff86c15b","tokenUsage":{"total":{"totalTokens":28248,"inputTokens":28222,"cachedInputTokens":28032,"outputTokens":26,"reasoningOutputTokens":16},"last":{"totalTokens":28248,"inputTokens":28222,"cachedInputTokens":28032,"outputTokens":26,"reasoningOutputTokens":16},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turn":{"id":"019dadec-c155-74a2-8acc-f9f4ff86c15b","items":[],"status":"completed","error":null,"startedAt":1776739467,"completedAt":1776739474,"durationMs":6986}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":4,"method":"turn/start","params":{"input":[{"text":"Respond with exactly: second fixture turn complete","type":"text"}],"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":4,"result":{"turn":{"id":"019dadec-dca0-7df0-9bc1-5a44135d8735","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turn":{"id":"019dadec-dca0-7df0-9bc1-5a44135d8735","items":[],"status":"inProgress","error":null,"startedAt":1776739474,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"0c4bed3e-fdd2-422c-bc28-4d688a5a9289","content":[{"type":"text","text":"Respond with exactly: second fixture turn complete","text_elements":[]}]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"0c4bed3e-fdd2-422c-bc28-4d688a5a9289","content":[{"type":"text","text":"Respond with exactly: second fixture turn complete","text_elements":[]}]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","tokenUsage":{"total":{"totalTokens":28248,"inputTokens":28222,"cachedInputTokens":28032,"outputTokens":26,"reasoningOutputTokens":16},"last":{"totalTokens":28248,"inputTokens":28222,"cachedInputTokens":28032,"outputTokens":26,"reasoningOutputTokens":16},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748633},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335433},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0110066f30127def0169e6e495c82481998e6c7e0925c86cfa","summary":[],"content":[]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0110066f30127def0169e6e495c82481998e6c7e0925c86cfa","summary":[],"content":[]},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","itemId":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","delta":"second"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","itemId":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","itemId":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","itemId":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0110066f30127def0169e6e495f6088199b4c7aad5e1b7d0aa","text":"second fixture turn complete","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turnId":"019dadec-dca0-7df0-9bc1-5a44135d8735","tokenUsage":{"total":{"totalTokens":56514,"inputTokens":56466,"cachedInputTokens":56064,"outputTokens":48,"reasoningOutputTokens":28},"last":{"totalTokens":28266,"inputTokens":28244,"cachedInputTokens":28032,"outputTokens":22,"reasoningOutputTokens":12},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748633},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335433},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadec-c14b-7b13-b96c-92c98ccfc342","turn":{"id":"019dadec-dca0-7df0-9bc1-5a44135d8735","items":[],"status":"completed","error":null,"startedAt":1776739474,"completedAt":1776739478,"durationMs":3700}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/cursor_transcript.ndjson new file mode 100644 index 00000000000..86448aaa89f --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/cursor_transcript.ndjson @@ -0,0 +1,30 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"queued_turn","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-2c1578fc-9bfb-4db9-91c2-8d6744a8cb4c"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":false,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-2c1578fc-9bfb-4db9-91c2-8d6744a8cb4c"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Respond with exactly: first fixture turn complete","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","agentId":"agent-2c1578fc-9bfb-4db9-91c2-8d6744a8cb4c"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"text-delta","text":"first"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"text-delta","text":" fixture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"text-delta","text":" turn"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"text-delta","text":" complete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"step-completed","stepId":1,"stepDurationMs":884}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","update":{"type":"turn-ended","usage":{"inputTokens":10397,"outputTokens":41,"cacheReadTokens":448,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-e8e94512-2578-458e-8fcd-3d2c232ed00a","requestId":"b0ca1b65-cdfd-4f50-8698-1924b0521b15","status":"finished","result":"first fixture turn complete","model":{"id":"composer-2.5"},"durationMs":3685}}} +{"type":"expect_outbound","label":"run.start:2","frame":{"type":"run.start","message":"Respond with exactly: second fixture turn complete","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:2","frame":{"type":"run.started","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","agentId":"agent-2c1578fc-9bfb-4db9-91c2-8d6744a8cb4c"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"text-delta","text":"second"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"text-delta","text":" fixture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"text-delta","text":" turn"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"text-delta","text":" complete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"step-completed","stepId":1,"stepDurationMs":830}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","update":{"type":"turn-ended","usage":{"inputTokens":10458,"outputTokens":29,"cacheReadTokens":10432,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:2","frame":{"type":"run.completed","result":{"id":"run-28ee8e5c-e5b0-4da3-a793-6ac182da167f","requestId":"9e7ec735-db7c-4bdc-b75d-dce82a288678","status":"finished","result":"second fixture turn complete","model":{"id":"composer-2.5"},"durationMs":2467}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-2c1578fc-9bfb-4db9-91c2-8d6744a8cb4c"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/grok_transcript.ndjson new file mode 100644 index 00000000000..a1bfc238bc3 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/grok_transcript.ndjson @@ -0,0 +1,12 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"queued_turn","metadata":{"generatedBy":"protocol-semantic-fixture","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"grok-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"}]}}}} +{"type":"expect_outbound","label":"first.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Respond with exactly: first fixture turn complete"}]}}} +{"type":"emit_inbound","label":"first.assistant","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"first fixture turn complete"}}}}} +{"type":"emit_inbound","label":"first.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"expect_outbound","label":"queued.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Respond with exactly: second fixture turn complete"}]}}} +{"type":"emit_inbound","label":"queued.assistant","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"second fixture turn complete"}}}}} +{"type":"emit_inbound","label":"queued.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/input.ts new file mode 100644 index 00000000000..01337a0b223 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/input.ts @@ -0,0 +1,14 @@ +import { + MULTI_TURN_FIRST_PROMPT, + MULTI_TURN_SECOND_PROMPT, + type OrchestratorFixtureInput, +} from "../shared.ts"; + +export function queuedTurnInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: MULTI_TURN_FIRST_PROMPT }, + { type: "queue_message", text: MULTI_TURN_SECOND_PROMPT }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/shared.ts b/apps/server/src/orchestration-v2/testkit/fixtures/shared.ts new file mode 100644 index 00000000000..3a81ef01bd7 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/shared.ts @@ -0,0 +1,1058 @@ +import { assert } from "@effect/vitest"; +import { + type ChatAttachment, + CommandId, + MessageId, + ProjectId, + ThreadId, + type ModelSelection, + type OrchestrationV2Command, + type OrchestrationV2ExecutionNode, + type OrchestrationV2RunStatus, + type OrchestrationV2ThreadProjection, + type OrchestrationV2TurnItem, + type OrchestrationV2UserMessageInputIntent, + ProviderInstanceId, + type ProviderInteractionMode, + type ProviderDriverKind, + type ProviderReplayTranscript, + type ProviderUserInputAnswers, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; + +import type { + OrchestratorV2ScenarioResult, + OrchestratorV2ScenarioStep, +} from "../OrchestratorScenario.ts"; +import { IdAllocatorV2, type IdAllocatorV2Error } from "../../IdAllocator.ts"; +import type { RuntimePolicyV2Override } from "../../RuntimePolicy.ts"; + +export const SIMPLE_PROMPT = "Respond with the following text: fixture simple ok"; +export const MULTI_TURN_FIRST_PROMPT = "Respond with exactly: first fixture turn complete"; +export const MULTI_TURN_SECOND_PROMPT = "Respond with exactly: second fixture turn complete"; +export const PROVIDER_THREAD_RESUME_FIRST_PROMPT = + "Respond with exactly: provider thread resume fixture first turn complete"; +export const PROVIDER_THREAD_RESUME_SECOND_PROMPT = + "Using the conversation history available in this resumed thread, first repeat the exact final answer you gave in the previous turn. Then on a new line write exactly: provider thread resume fixture second turn complete"; +export const TOOL_CALL_READ_ONLY_WORKSPACE_ROOT = "/tmp/claude-replay-tool_call_read_only"; +export const TOOL_CALL_READ_ONLY_PROMPT = `Read ${TOOL_CALL_READ_ONLY_WORKSPACE_ROOT}/package.json and ${TOOL_CALL_READ_ONLY_WORKSPACE_ROOT}/tsconfig.json, then answer exactly: read only tool fixture complete`; +export const TOOL_CALL_WRITE_PROMPT = + "Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP."; +export const MESSAGE_STEERING_INITIAL_PROMPT = + "Respond with exactly: steering fixture initial response"; +export const SUBAGENT_PROMPT = + "Spawn 2 subagents, one to read package.json and one to read tsconfig.json"; +export const OPENCODE_SUBAGENT_PROMPT = + "Use the task tool exactly once. Delegate to the general subagent with this prompt: Respond exactly CHILD_OK. After the task completes, respond exactly PARENT_OK."; +export const SUBAGENT_CONTINUE_PROMPT = + "Spawn one subagent and have it reply exactly: initial subagent response"; +export const SUBAGENT_CONTINUE_PARENT_PROMPT = + "@hooke have the same subagent reply exactly: continued subagent response"; +export const SUBAGENT_CONTINUE_CHILD_PROMPT = "Reply exactly: continued subagent response"; +export const TURN_INTERRUPT_PROMPT = + "Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally."; +export const TURN_INTERRUPT_MID_TOOL_PROMPT = + "Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally."; +export const TURN_INTERRUPT_RECOVERY_PROMPT = + "Respond with exactly: interrupt recovery fixture complete"; +export const MESSAGE_STEERING_STEER_PROMPT = + "Actually, respond with exactly: steering fixture observed"; +export const THREAD_ROLLBACK_FIRST_PROMPT = + "Respond with exactly: rollback fixture first turn complete"; +export const THREAD_ROLLBACK_SECOND_PROMPT = + "Respond with exactly: rollback fixture second turn complete"; +export const THREAD_ROLLBACK_AFTER_PROMPT = "Repeat the conversation verbatim."; +export const THREAD_FORK_NATIVE_SOURCE_PROMPT = + "Respond with the following text: source fork seed ok"; +export const THREAD_FORK_NATIVE_TARGET_PROMPT = "Respond with the following text: fork native ok"; +export const THREAD_FORK_NATIVE_CONTINUE_SOURCE_MARKER = "source-marker-7Q9V"; +export const THREAD_FORK_NATIVE_CONTINUE_FORK_MARKER = "fork-marker-2K4M"; +export const THREAD_FORK_NATIVE_CONTINUE_RECALL = `${THREAD_FORK_NATIVE_CONTINUE_SOURCE_MARKER}|${THREAD_FORK_NATIVE_CONTINUE_FORK_MARKER}`; +export const THREAD_FORK_NATIVE_CONTINUE_SOURCE_PROMPT = `Remember the opaque marker ${THREAD_FORK_NATIVE_CONTINUE_SOURCE_MARKER} for later in this conversation. Respond with exactly: source marker stored`; +export const THREAD_FORK_NATIVE_CONTINUE_FIRST_PROMPT = `Remember the second opaque marker ${THREAD_FORK_NATIVE_CONTINUE_FORK_MARKER} for later in this conversation. Respond with exactly: fork marker stored`; +export const THREAD_FORK_NATIVE_CONTINUE_SECOND_PROMPT = + "Return the two opaque markers previously provided in chronological order, separated by a single | character. Respond with only the markers and separator."; +export const THREAD_FORK_NATIVE_SIBLINGS_SOURCE_MARKER = "sibling-source-8R3D"; +export const THREAD_FORK_NATIVE_SIBLINGS_FIRST_MARKER = "sibling-first-5L2P"; +export const THREAD_FORK_NATIVE_SIBLINGS_SECOND_MARKER = "sibling-second-9N6C"; +export const THREAD_FORK_NATIVE_SIBLINGS_SOURCE_PROMPT = `Remember the opaque marker ${THREAD_FORK_NATIVE_SIBLINGS_SOURCE_MARKER} for later in this conversation. Respond with exactly: sibling source stored`; +export const THREAD_FORK_NATIVE_SIBLINGS_FIRST_PROMPT = `Remember the fork-local marker ${THREAD_FORK_NATIVE_SIBLINGS_FIRST_MARKER}. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator.`; +export const THREAD_FORK_NATIVE_SIBLINGS_SECOND_PROMPT = `Remember the fork-local marker ${THREAD_FORK_NATIVE_SIBLINGS_SECOND_MARKER}. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator.`; +export const THREAD_MERGE_BACK_SOURCE_MARKER = "merge-source-4H8Q"; +export const THREAD_MERGE_BACK_FORK_MARKER = "merge-fork-7T2W"; +export const THREAD_MERGE_BACK_SOURCE_PROMPT = `Remember the opaque marker ${THREAD_MERGE_BACK_SOURCE_MARKER} for later in this conversation. Respond with exactly: merge source stored`; +export const THREAD_MERGE_BACK_FORK_PROMPT = `Remember the fork-local marker ${THREAD_MERGE_BACK_FORK_MARKER}. Respond with exactly: merge fork stored`; +export const THREAD_MERGE_BACK_HANDOFF_PROMPT = [ + "Context handoff (merge_back / fork_delta_summary):", + "Merge-back context from forked conversation.", + "", + "Fork delta:", + `- User introduced opaque marker ${THREAD_MERGE_BACK_FORK_MARKER}.`, + "- Assistant confirmed: merge fork stored", + "", + "User message:", + "Retain the transferred fork marker for later. Respond with exactly: merge delta stored", +].join("\n"); +export const THREAD_MERGE_BACK_RECALL = `${THREAD_MERGE_BACK_SOURCE_MARKER}|${THREAD_MERGE_BACK_FORK_MARKER}`; +export const THREAD_MERGE_BACK_RECALL_PROMPT = + "Return the source marker followed by the transferred fork marker, separated by a single | character. Respond with only the markers and separator."; +export const THREAD_MERGE_BACK_SIBLINGS_SOURCE_MARKER = "merge-sibling-source-3C7K"; +export const THREAD_MERGE_BACK_SIBLINGS_FIRST_MARKER = "merge-sibling-first-6V2J"; +export const THREAD_MERGE_BACK_SIBLINGS_SECOND_MARKER = "merge-sibling-second-9X5B"; +export const THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT = `Remember the opaque marker ${THREAD_MERGE_BACK_SIBLINGS_SOURCE_MARKER} for later in this conversation. Respond with exactly: merge sibling source stored`; +export const THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT = `Remember the fork-local marker ${THREAD_MERGE_BACK_SIBLINGS_FIRST_MARKER}. Respond with exactly: first merge sibling stored`; +export const THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT = `Remember the fork-local marker ${THREAD_MERGE_BACK_SIBLINGS_SECOND_MARKER}. Respond with exactly: second merge sibling stored`; +export const THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT = [ + "Context handoff (merge_back / fork_delta_summary):", + "Merge-back context from first forked conversation.", + "", + "Fork delta:", + `- User introduced opaque marker ${THREAD_MERGE_BACK_SIBLINGS_FIRST_MARKER}.`, + "- Assistant confirmed: first merge sibling stored", + "", + "User message:", + "Retain the first transferred marker for later. Respond with exactly: first merge delta stored", +].join("\n"); +export const THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT = [ + "Context handoff (merge_back / fork_delta_summary):", + "Merge-back context from second forked conversation.", + "", + "Fork delta:", + `- User introduced opaque marker ${THREAD_MERGE_BACK_SIBLINGS_SECOND_MARKER}.`, + "- Assistant confirmed: second merge sibling stored", + "", + "User message:", + "Retain the second transferred marker for later. Respond with exactly: second merge delta stored", +].join("\n"); +export const THREAD_MERGE_BACK_SIBLINGS_RECALL = [ + THREAD_MERGE_BACK_SIBLINGS_SOURCE_MARKER, + THREAD_MERGE_BACK_SIBLINGS_FIRST_MARKER, + THREAD_MERGE_BACK_SIBLINGS_SECOND_MARKER, +].join("|"); +export const THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT = + "Return the source marker followed by both transferred fork markers in merge order, separated by single | characters. Respond with only the markers and separators."; +export const THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT = + "For this fork-boundary fixture, respond with exactly: fork boundary alpha"; +export const THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT = + "For this fork-boundary fixture, respond with exactly: fork boundary beta"; +export const THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT = + "Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content."; +export const TODO_LIST_PROMPT = + "Use the update_plan tool to track exactly three steps: inspect package.json, inspect tsconfig.json, report completion. Then read package.json and tsconfig.json, and answer exactly: todo list fixture complete"; +export const PLAN_QUESTIONS_PROMPT = + "Use request_user_input to ask one multiple-choice clarifying question about whether this fixture should prefer strict schemas or UI flexibility. After receiving the answer, respond exactly: plan questions fixture complete"; +export const PROPOSED_PLAN_PROMPT = + "Create a short implementation plan for adding deterministic replay fixtures. Do not ask questions. Present the final plan in a proposed plan block."; +export const WEB_SEARCH_PROMPT = + "Search the web for FIFA World Cup ticket pricing, then answer exactly: web search fixture complete"; + +export type OrchestratorFixtureInputStep = + | { + readonly type: "message"; + readonly text: string; + readonly attachments?: ReadonlyArray; + } + | { + readonly type: "queue_message"; + readonly text: string; + readonly attachments?: ReadonlyArray; + } + | { + readonly type: "steer"; + readonly text: string; + readonly attachments?: ReadonlyArray; + readonly targetRunIndex: number; + } + | { + readonly type: "restart"; + readonly text: string; + readonly attachments?: ReadonlyArray; + readonly targetRunIndex: number; + } + | { + readonly type: "interrupt"; + readonly targetRunIndex: number; + readonly waitForTurnItemType?: OrchestrationV2TurnItem["type"]; + } + | { + readonly type: "approve_next_runtime_request"; + readonly decision?: Extract< + OrchestrationV2Command, + { readonly type: "runtime-request.respond" } + >["decision"]; + } + | { + readonly type: "answer_next_user_input_request"; + readonly answers: ProviderUserInputAnswers; + } + | { + readonly type: "rollback"; + readonly checkpointScopeSuffix: string; + readonly checkpointSuffix: string; + }; + +export interface OrchestratorFixtureInput { + readonly interactionMode?: ProviderInteractionMode; + readonly steps: ReadonlyArray; +} + +export interface ProviderOrchestratorReplayVariant { + readonly driver: ProviderDriverKind; + readonly transcriptFile: URL; + readonly modelSelection: ModelSelection; + readonly runtimePolicyOverride?: RuntimePolicyV2Override; + readonly assertOutput: ( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, + ) => void; +} + +export interface OrchestratorReplayFixture { + readonly name: string; + readonly buildInput: () => OrchestratorFixtureInput; + readonly providers: ReadonlyArray; +} + +export interface MaterializedOrchestratorFixtureInput { + readonly commands: ReadonlyArray; + readonly steps: ReadonlyArray; + readonly projectionThreadIds: ReadonlyArray; +} + +export interface FixtureIds { + readonly threadId: ThreadId; + readonly projectId: ProjectId; +} + +export const CODEX_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", +} satisfies ModelSelection; + +export const CLAUDE_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-sonnet-4-6", +} satisfies ModelSelection; + +export const CURSOR_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("cursor"), + model: "composer-2.5", +} satisfies ModelSelection; + +export const GROK_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("grok"), + model: "grok-build", +} satisfies ModelSelection; + +export const OPENCODE_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("opencode"), + model: "openai/gpt-5.4-mini", + options: [{ id: "agent", value: "build" }], +} satisfies ModelSelection; + +export const ACP_REGISTRY_MODEL_SELECTION = { + instanceId: ProviderInstanceId.make("acpRegistry"), + model: "default", +} satisfies ModelSelection; + +export const READ_ONLY_ON_REQUEST_POLICY = { + approvalPolicy: "on-request", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, +} as const satisfies RuntimePolicyV2Override; + +export const READ_ONLY_NEVER_POLICY = { + approvalPolicy: "never", + sandboxPolicy: { + type: "readOnly", + access: { type: "fullAccess" }, + networkAccess: false, + }, +} as const satisfies RuntimePolicyV2Override; + +export const WORKSPACE_NEVER_POLICY = { + approvalPolicy: "never", + sandboxPolicy: { + type: "workspaceWrite", + writableRoots: [], + readOnlyAccess: { type: "fullAccess" }, + networkAccess: false, + }, +} as const satisfies RuntimePolicyV2Override; + +export const RESTRICTED_GRANULAR_POLICY = { + approvalPolicy: { + granular: { + mcp_elicitations: true, + request_permissions: true, + rules: true, + sandbox_approval: true, + skill_approval: true, + }, + }, + sandboxPolicy: { + type: "readOnly", + access: { + type: "restricted", + includePlatformDefaults: false, + readableRoots: [], + }, + networkAccess: false, + }, +} as const satisfies RuntimePolicyV2Override; + +export function createThreadCommand(input: { + readonly commandId: CommandId; + readonly ids: FixtureIds; + readonly scenario: string; + readonly modelSelection: ModelSelection; + readonly interactionMode?: ProviderInteractionMode; +}): OrchestrationV2Command { + return { + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: input.commandId, + threadId: input.ids.threadId, + projectId: input.ids.projectId, + title: `Replay fixture: ${input.scenario}`, + modelSelection: input.modelSelection, + runtimeMode: "full-access", + interactionMode: input.interactionMode ?? "default", + branch: null, + worktreePath: null, + }; +} + +export function dispatchMessageCommand(input: { + readonly commandId: CommandId; + readonly ids: FixtureIds; + readonly modelSelection: ModelSelection; + readonly messageId: MessageId; + readonly text: string; + readonly attachments?: ReadonlyArray; + readonly dispatchMode?: Extract< + OrchestrationV2Command, + { readonly type: "message.dispatch" } + >["dispatchMode"]; +}): OrchestrationV2Command { + return { + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: input.commandId, + threadId: input.ids.threadId, + messageId: input.messageId, + text: input.text, + attachments: [...(input.attachments ?? [])], + modelSelection: input.modelSelection, + dispatchMode: input.dispatchMode ?? { type: "start_immediately" }, + }; +} + +export function materializeFixtureInput(input: { + readonly scenario: string; + readonly fixtureInput: OrchestratorFixtureInput; + readonly driver: ProviderDriverKind; + readonly modelSelection: ModelSelection; +}): Effect.Effect { + return Effect.gen(function* () { + const idAllocator = yield* IdAllocatorV2; + const projectId = yield* idAllocator.allocate.project({ fixtureName: input.scenario }); + const threadId = yield* idAllocator.allocate.thread({ + fixtureName: input.scenario, + projectId, + }); + const ids = { threadId, projectId } satisfies FixtureIds; + const commands: Array = []; + const steps: Array = []; + let messageIndex = 0; + const activeRunDispatchKeys = new Set(); + + const runIdFor = (runOrdinal: number) => + idAllocator.derive.run({ threadId: ids.threadId, ordinal: runOrdinal }); + + const pushDispatch = ( + command: OrchestrationV2Command, + options: { + readonly await?: boolean; + readonly key?: string; + readonly advanceClockAfter?: boolean; + } = {}, + ) => { + commands.push(command); + steps.push({ + type: "dispatch", + command, + await: options.await ?? true, + ...(options.key === undefined ? {} : { key: options.key }), + }); + if (options.advanceClockAfter ?? true) { + steps.push({ type: "advance_clock", duration: "1 millis" }); + } + }; + + pushDispatch( + createThreadCommand({ + commandId: yield* idAllocator.allocate.command({ + fixtureName: input.scenario, + commandName: "thread-create", + }), + ids, + scenario: input.scenario, + modelSelection: input.modelSelection, + ...(input.fixtureInput.interactionMode === undefined + ? {} + : { interactionMode: input.fixtureInput.interactionMode }), + }), + ); + + for (const [stepIndex, step] of input.fixtureInput.steps.entries()) { + switch (step.type) { + case "message": + messageIndex += 1; + { + const nextStep = input.fixtureInput.steps[stepIndex + 1]; + const shouldRunInBackground = + (nextStep !== undefined && + ((nextStep.type === "interrupt" && nextStep.targetRunIndex === messageIndex) || + nextStep.type === "queue_message" || + (nextStep.type === "restart" && nextStep.targetRunIndex === messageIndex))) || + nextStep?.type === "approve_next_runtime_request" || + nextStep?.type === "answer_next_user_input_request"; + const key = `run:${messageIndex}`; + pushDispatch( + dispatchMessageCommand({ + commandId: yield* idAllocator.allocate.command({ + fixtureName: input.scenario, + commandName: `message-${messageIndex}`, + }), + ids, + modelSelection: input.modelSelection, + messageId: yield* idAllocator.allocate.message({ + threadId: ids.threadId, + ordinal: messageIndex, + }), + text: step.text, + ...(step.attachments === undefined ? {} : { attachments: step.attachments }), + }), + shouldRunInBackground ? { await: false, key } : undefined, + ); + if (shouldRunInBackground) { + activeRunDispatchKeys.add(key); + } else if ( + !( + nextStep !== undefined && + (nextStep.type === "steer" || nextStep.type === "restart") && + nextStep.targetRunIndex === messageIndex + ) + ) { + steps.push({ type: "await_thread_idle", threadId: ids.threadId }); + } + } + break; + case "queue_message": + messageIndex += 1; + pushDispatch( + dispatchMessageCommand({ + commandId: yield* idAllocator.allocate.command({ + fixtureName: input.scenario, + commandName: `queue-message-${messageIndex}`, + }), + ids, + modelSelection: input.modelSelection, + messageId: yield* idAllocator.allocate.message({ + threadId: ids.threadId, + ordinal: messageIndex, + }), + text: step.text, + ...(step.attachments === undefined ? {} : { attachments: step.attachments }), + }), + ); + steps.push({ type: "await", key: `run:${messageIndex - 1}` }); + steps.push({ type: "await_thread_idle", threadId: ids.threadId }); + break; + case "answer_next_user_input_request": + pushDispatch( + { + type: "runtime-request.respond", + commandId: yield* idAllocator.allocate.command({ + fixtureName: input.scenario, + commandName: `answer-user-input-request-${messageIndex}`, + }), + threadId: ids.threadId, + requestId: yield* idAllocator.allocate.runtimeRequest({ + driver: input.driver, + nativeRequestId: `fixture-placeholder-${messageIndex}`, + }), + answers: step.answers, + }, + { advanceClockAfter: false }, + ); + steps[steps.length - 1] = { + type: "respond_to_next_runtime_request", + threadId: ids.threadId, + commandId: commands.at(-1)!.commandId, + answers: step.answers, + }; + steps.push({ type: "advance_clock", duration: "1 millis" }); + steps.push({ type: "await_thread_idle", threadId: ids.threadId }); + break; + case "approve_next_runtime_request": + pushDispatch( + { + type: "runtime-request.respond", + commandId: yield* idAllocator.allocate.command({ + fixtureName: input.scenario, + commandName: `approve-runtime-request-${messageIndex}`, + }), + threadId: ids.threadId, + requestId: yield* idAllocator.allocate.runtimeRequest({ + driver: input.driver, + nativeRequestId: `fixture-placeholder-${messageIndex}`, + }), + decision: step.decision ?? "accept", + }, + { advanceClockAfter: false }, + ); + steps[steps.length - 1] = { + type: "respond_to_next_runtime_request", + threadId: ids.threadId, + commandId: commands.at(-1)!.commandId, + decision: step.decision ?? "accept", + }; + steps.push({ type: "advance_clock", duration: "1 millis" }); + steps.push({ type: "await_thread_idle", threadId: ids.threadId }); + break; + case "steer": + messageIndex += 1; + steps.push({ + type: "await_run_steerable", + threadId: ids.threadId, + runId: runIdFor(step.targetRunIndex), + }); + pushDispatch( + dispatchMessageCommand({ + commandId: yield* idAllocator.allocate.command({ + fixtureName: input.scenario, + commandName: `steer-${messageIndex}`, + }), + ids, + modelSelection: input.modelSelection, + messageId: yield* idAllocator.allocate.message({ + threadId: ids.threadId, + ordinal: messageIndex, + }), + text: step.text, + ...(step.attachments === undefined ? {} : { attachments: step.attachments }), + dispatchMode: { + type: "steer_active", + targetRunId: runIdFor(step.targetRunIndex), + }, + }), + ); + if (input.fixtureInput.steps[stepIndex + 1]?.type !== "approve_next_runtime_request") { + if (activeRunDispatchKeys.delete(`run:${step.targetRunIndex}`)) { + steps.push({ type: "await", key: `run:${step.targetRunIndex}` }); + } + steps.push({ type: "await_thread_idle", threadId: ids.threadId }); + } + break; + case "restart": + messageIndex += 1; + steps.push({ + type: "await_run_steerable", + threadId: ids.threadId, + runId: runIdFor(step.targetRunIndex), + }); + pushDispatch( + dispatchMessageCommand({ + commandId: yield* idAllocator.allocate.command({ + fixtureName: input.scenario, + commandName: `restart-${messageIndex}`, + }), + ids, + modelSelection: input.modelSelection, + messageId: yield* idAllocator.allocate.message({ + threadId: ids.threadId, + ordinal: messageIndex, + }), + text: step.text, + ...(step.attachments === undefined ? {} : { attachments: step.attachments }), + dispatchMode: { + type: "restart_active", + targetRunId: runIdFor(step.targetRunIndex), + }, + }), + ); + if (input.fixtureInput.steps[stepIndex + 1]?.type !== "approve_next_runtime_request") { + if (activeRunDispatchKeys.delete(`run:${step.targetRunIndex}`)) { + steps.push({ type: "await", key: `run:${step.targetRunIndex}` }); + } + steps.push({ type: "await_thread_idle", threadId: ids.threadId }); + } + break; + case "interrupt": + if (step.waitForTurnItemType !== undefined) { + steps.push({ + type: "await_run_turn_item", + threadId: ids.threadId, + runId: runIdFor(step.targetRunIndex), + itemType: step.waitForTurnItemType, + }); + } + pushDispatch( + { + type: "run.interrupt", + commandId: yield* idAllocator.allocate.command({ + fixtureName: input.scenario, + commandName: `interrupt-${step.targetRunIndex}`, + }), + threadId: ids.threadId, + runId: runIdFor(step.targetRunIndex), + }, + { advanceClockAfter: false }, + ); + if (activeRunDispatchKeys.delete(`run:${step.targetRunIndex}`)) { + steps.push({ type: "await", key: `run:${step.targetRunIndex}` }); + } + steps.push({ type: "advance_clock", duration: "1 millis" }); + steps.push({ type: "await_thread_idle", threadId: ids.threadId }); + break; + case "rollback": + { + const scopeId = yield* idAllocator.allocate.checkpointScope({ + threadId: ids.threadId, + name: step.checkpointScopeSuffix, + }); + pushDispatch({ + type: "checkpoint.rollback", + commandId: yield* idAllocator.allocate.command({ + fixtureName: input.scenario, + commandName: `rollback-${step.checkpointSuffix}`, + }), + threadId: ids.threadId, + scopeId, + checkpointId: yield* idAllocator.allocate.checkpoint({ + checkpointScopeId: scopeId, + name: step.checkpointSuffix, + }), + }); + } + break; + } + } + + if (activeRunDispatchKeys.size > 0) { + steps.push({ type: "await_all" }); + steps.push({ type: "await_thread_idle", threadId: ids.threadId }); + } + + return { + commands, + steps, + projectionThreadIds: [ids.threadId], + }; + }); +} + +export function projectionFor( + result: OrchestratorV2ScenarioResult, + scenario: string, +): OrchestrationV2ThreadProjection { + const projections = [...result.projections.values()].filter( + (projection) => projection.thread.lineage.parentThreadId === null, + ); + + assert.equal(projections.length, 1, `expected one root projection for ${scenario}`); + const projection = projections[0]; + assert.isDefined(projection, `missing projection for ${scenario}`); + return projection; +} + +export function assertBaseProjection(input: { + readonly result: OrchestratorV2ScenarioResult; + readonly transcript: ProviderReplayTranscript; + readonly runCount: number; + readonly providerTurnCountAtLeast?: number; + readonly runStatuses?: ReadonlyArray; +}) { + const projection = projectionFor(input.result, input.transcript.scenario); + + assert.equal( + projection.thread.providerInstanceId, + ProviderInstanceId.make(input.transcript.provider), + ); + assert.lengthOf(projection.runs, input.runCount); + assert.isAtLeast(projection.providerThreads.length, 1); + assert.isAtLeast( + projection.providerTurns.length, + input.providerTurnCountAtLeast ?? input.runCount, + ); + assert.isAtLeast(input.result.domainEvents.length, 1); + assert.deepEqual( + input.result.storedEvents.map((stored) => stored.sequence), + input.result.storedEvents.map((_, index) => index + 1), + ); + assert.deepEqual( + input.result.storedEvents.map((stored) => stored.event.id), + input.result.domainEvents.map((event) => event.id), + ); + + if (input.runStatuses) { + assert.deepEqual( + projection.runs.map((run) => run.status), + input.runStatuses, + ); + } +} + +export function assertRunOrdinals( + projection: OrchestrationV2ThreadProjection, + expectedOrdinals: ReadonlyArray, +) { + assert.deepEqual( + projection.runs.map((run) => run.ordinal), + expectedOrdinals, + ); +} + +export function assertRunsHaveRootNodes(projection: OrchestrationV2ThreadProjection) { + for (const run of projection.runs) { + assert.isNotNull(run.rootNodeId, `run ${run.id} must have a root node`); + assert.isTrue( + projection.nodes.some((node) => node.id === run.rootNodeId && node.kind === "root_turn"), + `run ${run.id} root node must exist`, + ); + } +} + +export function assertRootNodesCountForRuns(projection: OrchestrationV2ThreadProjection) { + const rootNodes = projection.nodes.filter((node) => node.kind === "root_turn"); + assert.isAtLeast(rootNodes.length, projection.runs.length); + for (const node of rootNodes) { + assert.equal(node.countsForRun, true, `root node ${node.id} must count for its app run`); + } +} + +export function assertProviderTurnsReferenceNodes(projection: OrchestrationV2ThreadProjection) { + for (const providerTurn of projection.providerTurns) { + assert.isTrue( + projection.nodes.some((node) => node.id === providerTurn.nodeId), + `provider turn ${providerTurn.id} must reference an execution node`, + ); + assert.isTrue( + projection.providerThreads.some((thread) => thread.id === providerTurn.providerThreadId), + `provider turn ${providerTurn.id} must reference a provider thread`, + ); + } +} + +export function assertTurnItemsAreOrdered(projection: OrchestrationV2ThreadProjection) { + const ordinals = projection.turnItems.map((item) => item.ordinal); + assert.deepEqual( + ordinals, + [...ordinals].toSorted((left, right) => left - right), + ); +} + +export function assertTurnItemsReferenceProjection(projection: OrchestrationV2ThreadProjection) { + for (const item of projection.turnItems) { + if (item.runId !== null) { + assert.isTrue( + projection.runs.some((run) => run.id === item.runId), + `turn item ${item.id} must reference an existing run`, + ); + } + if (item.nodeId !== null) { + assert.isTrue( + projection.nodes.some((node) => node.id === item.nodeId), + `turn item ${item.id} must reference an existing node`, + ); + } + if (item.providerTurnId !== null) { + assert.isTrue( + projection.providerTurns.some((turn) => turn.id === item.providerTurnId), + `turn item ${item.id} must reference an existing provider turn`, + ); + } + } +} + +export function assertVisibleTurnItemsMirrorLocalTurnItems( + projection: OrchestrationV2ThreadProjection, +) { + assert.lengthOf( + projection.visibleTurnItems, + projection.turnItems.length, + "non-fork visible turn items must mirror local canonical turn items", + ); + + for (const [index, item] of projection.turnItems.entries()) { + const visibleItem = projection.visibleTurnItems[index]; + assert.isDefined(visibleItem, `missing visible turn item at position ${index}`); + assert.equal(visibleItem.position, index); + assert.equal(visibleItem.visibility, "local"); + assert.equal(visibleItem.sourceThreadId, item.threadId); + assert.equal(visibleItem.sourceItemId, item.id); + assert.deepEqual(visibleItem.item, item); + } +} + +export function assertMessagesReferenceProjection(projection: OrchestrationV2ThreadProjection) { + for (const message of projection.messages) { + if (message.runId !== null) { + assert.isTrue( + projection.runs.some((run) => run.id === message.runId), + `message ${message.id} must reference an existing run`, + ); + } + if (message.nodeId !== null) { + assert.isTrue( + projection.nodes.some((node) => node.id === message.nodeId), + `message ${message.id} must reference an existing node`, + ); + } + } +} + +export function assertRuntimeRequestsReferenceProjection( + projection: OrchestrationV2ThreadProjection, +) { + for (const request of projection.runtimeRequests) { + const requestNode = projection.nodes.find((node) => node.id === request.nodeId); + assert.isTrue( + requestNode !== undefined, + `runtime request ${request.id} must reference an existing node`, + ); + if ( + requestNode !== undefined && + (request.kind === "command" || request.kind === "file-read" || request.kind === "file-change") + ) { + assert.equal( + requestNode.kind, + "approval_request", + `runtime request ${request.id} must reference an approval request node`, + ); + } + if (request.providerTurnId !== null) { + assert.isTrue( + projection.providerTurns.some((turn) => turn.id === request.providerTurnId), + `runtime request ${request.id} must reference an existing provider turn`, + ); + } + } +} + +export function assertSemanticProjectionIntegrity(projection: OrchestrationV2ThreadProjection) { + assertRunsHaveRootNodes(projection); + assertRootNodesCountForRuns(projection); + assertProviderTurnsReferenceNodes(projection); + assertTurnItemsAreOrdered(projection); + assertTurnItemsReferenceProjection(projection); + assertMessagesReferenceProjection(projection); + assertRuntimeRequestsReferenceProjection(projection); +} + +export function assertRunProviderTurnCardinality(input: { + readonly projection: OrchestrationV2ThreadProjection; + readonly rootRunCount: number; + readonly providerTurnCountAtLeast?: number; +}) { + assert.lengthOf(input.projection.runs, input.rootRunCount); + assert.isAtLeast( + input.projection.providerTurns.length, + input.providerTurnCountAtLeast ?? input.rootRunCount, + ); +} + +export function assertNoExtraAppRunsForProviderChildren(input: { + readonly projection: OrchestrationV2ThreadProjection; + readonly expectedAppRuns: number; +}) { + assert.lengthOf( + input.projection.runs, + input.expectedAppRuns, + "provider child activity must not create additional app runs", + ); +} + +export function assertExecutionNodeKinds( + projection: OrchestrationV2ThreadProjection, + expectedKinds: ReadonlyArray, +) { + const kinds = projection.nodes.map((node) => node.kind); + for (const expectedKind of expectedKinds) { + assert.include(kinds, expectedKind); + } +} + +export function assertTurnItemTypes( + projection: OrchestrationV2ThreadProjection, + expectedTypes: ReadonlyArray, +) { + const actualTypes = projection.turnItems.map((item) => item.type); + for (const expectedType of expectedTypes) { + assert.include(actualTypes, expectedType); + } +} + +export function assertTurnItemTypeSequence( + projection: OrchestrationV2ThreadProjection, + expectedTypes: ReadonlyArray, +) { + assert.deepEqual( + projection.turnItems.map((item) => item.type), + expectedTypes, + ); +} + +export function assertVisibleTurnItemTypeSequence( + projection: OrchestrationV2ThreadProjection, + expectedTypes: ReadonlyArray, +) { + assert.deepEqual( + projection.visibleTurnItems.map((row) => row.item.type), + expectedTypes, + ); +} + +export function assertAssistantTextIncludes( + projection: OrchestrationV2ThreadProjection, + expectedText: string, +) { + assert.isTrue( + projection.turnItems.some( + (item) => item.type === "assistant_message" && item.text.includes(expectedText), + ), + `expected assistant output to include ${JSON.stringify(expectedText)}`, + ); +} + +export function assertRuntimeRequestCounts( + projection: OrchestrationV2ThreadProjection, + expected: { readonly total: number; readonly resolved?: number }, +) { + assert.lengthOf(projection.runtimeRequests, expected.total); + if (expected.resolved !== undefined) { + assert.equal( + projection.runtimeRequests.filter((request) => request.status === "resolved").length, + expected.resolved, + ); + } +} + +export function countReplayLabelsWithPrefix( + transcript: ProviderReplayTranscript, + prefix: string, +): number { + return transcript.entries.filter( + (entry) => entry.type !== "runtime_exit" && (entry.label?.startsWith(prefix) ?? false), + ).length; +} + +export function assertReplayLabelPrefixCount( + transcript: ProviderReplayTranscript, + prefix: string, + expected: number, +) { + assert.equal(countReplayLabelsWithPrefix(transcript, prefix), expected); +} + +export function assertRuntimeRequestKinds( + projection: OrchestrationV2ThreadProjection, + expectedKinds: ReadonlyArray, +) { + assert.deepEqual( + projection.runtimeRequests.map((request) => request.kind), + expectedKinds, + ); +} + +export function assertAllRuntimeRequestsResolved(projection: OrchestrationV2ThreadProjection) { + assert.deepEqual( + projection.runtimeRequests.map((request) => request.status), + projection.runtimeRequests.map(() => "resolved"), + ); +} + +export function assertConversationMessageRoles( + projection: OrchestrationV2ThreadProjection, + expectedRoles: ReadonlyArray, +) { + assert.deepEqual( + projection.messages.map((message) => message.role), + expectedRoles, + ); +} + +export function assertUserMessagesInclude( + projection: OrchestrationV2ThreadProjection, + expectedTexts: ReadonlyArray, +) { + for (const expectedText of expectedTexts) { + assert.isTrue( + projection.turnItems.some( + (item) => item.type === "user_message" && item.text.includes(expectedText), + ), + `expected user input to include ${JSON.stringify(expectedText)}`, + ); + } +} + +export function assertUserMessagesExclude( + projection: OrchestrationV2ThreadProjection, + rejectedTexts: ReadonlyArray, +) { + for (const rejectedText of rejectedTexts) { + assert.isFalse( + projection.turnItems.some( + (item) => item.type === "user_message" && item.text.includes(rejectedText), + ), + `expected user input to exclude ${JSON.stringify(rejectedText)}`, + ); + } +} + +export function assertVisibleUserMessagesInclude( + projection: OrchestrationV2ThreadProjection, + expectedTexts: ReadonlyArray, +) { + for (const expectedText of expectedTexts) { + assert.isTrue( + projection.visibleTurnItems.some( + (row) => row.item.type === "user_message" && row.item.text.includes(expectedText), + ), + `expected visible user input to include ${JSON.stringify(expectedText)}`, + ); + } +} + +export function assertVisibleUserMessagesExclude( + projection: OrchestrationV2ThreadProjection, + rejectedTexts: ReadonlyArray, +) { + for (const rejectedText of rejectedTexts) { + assert.isFalse( + projection.visibleTurnItems.some( + (row) => row.item.type === "user_message" && row.item.text.includes(rejectedText), + ), + `expected visible user input to exclude ${JSON.stringify(rejectedText)}`, + ); + } +} + +export function assertUserMessageInputIntents( + projection: OrchestrationV2ThreadProjection, + expectedIntents: ReadonlyArray, +) { + assert.deepEqual( + projection.turnItems + .filter((item) => item.type === "user_message") + .map((item) => item.inputIntent), + expectedIntents, + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/simple/claude_output.ts new file mode 100644 index 00000000000..ee6aa72f2f9 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/claude_output.ts @@ -0,0 +1,33 @@ +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertConversationMessageRoles, + assertExecutionNodeKinds, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + SIMPLE_PROMPT, +} from "../shared.ts"; + +export function assertSimpleClaudeOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1]); + assertExecutionNodeKinds(projection, ["root_turn", "assistant_message"]); + assertConversationMessageRoles(projection, ["user", "assistant"]); + assertTurnItemTypes(projection, ["user_message", "assistant_message"]); + assertUserMessagesInclude(projection, [SIMPLE_PROMPT]); + assertAssistantTextIncludes(projection, "fixture simple ok"); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/claude_transcript.ndjson new file mode 100644 index 00000000000..633984e8874 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/claude_transcript.ndjson @@ -0,0 +1,10 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"simple","metadata":{"prompts":["Respond with the following text: fixture simple ok"],"model":"claude-sonnet-4-6","nativeSessionId":"77171d01-fb4f-4dff-a961-d9dd334be93d","queryMode":"streaming","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"77171d01-fb4f-4dff-a961-d9dd334be93d"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with the following text: fixture simple ok"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"6402f3ef-506b-4859-b69c-173458c244b3","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"5daa14d4-b571-4451-986a-a9a07d3c277e","session_id":"77171d01-fb4f-4dff-a961-d9dd334be93d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"6402f3ef-506b-4859-b69c-173458c244b3","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"adfe8057-6178-47bc-945d-b178f5ca578a","session_id":"77171d01-fb4f-4dff-a961-d9dd334be93d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-simple","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"df4adde0-72a9-49e1-9149-195fe0f629ee","session_id":"77171d01-fb4f-4dff-a961-d9dd334be93d"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01CM449j71BXKjsRZZpW2z64","type":"message","role":"assistant","content":[{"type":"text","text":"fixture simple ok"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2338,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2338},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"77171d01-fb4f-4dff-a961-d9dd334be93d","uuid":"502cdf40-1af1-428c-87cb-a07cfc99dafd"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"ea562a84-375e-4e8f-ba30-8b98d56122a9","session_id":"77171d01-fb4f-4dff-a961-d9dd334be93d"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":1291,"duration_api_ms":1039,"num_turns":1,"result":"fixture simple ok","stop_reason":"end_turn","session_id":"77171d01-fb4f-4dff-a961-d9dd334be93d","total_cost_usd":0.011703,"usage":{"input_tokens":3,"cache_creation_input_tokens":2338,"cache_read_input_tokens":9455,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2338,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":9455,"cache_creation_input_tokens":2338,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2338},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":9455,"cacheCreationInputTokens":2338,"webSearchRequests":0,"costUSD":0.011703,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"6b6fcbd9-d2dd-4b4f-9345-d6c7a7d7eb4e"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/simple/codex_output.ts new file mode 100644 index 00000000000..b8a30ba9f3b --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/codex_output.ts @@ -0,0 +1,33 @@ +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertConversationMessageRoles, + assertExecutionNodeKinds, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + SIMPLE_PROMPT, +} from "../shared.ts"; + +export function assertSimpleOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1]); + assertExecutionNodeKinds(projection, ["root_turn", "assistant_message"]); + assertConversationMessageRoles(projection, ["user", "assistant"]); + assertTurnItemTypes(projection, ["user_message", "assistant_message"]); + assertUserMessagesInclude(projection, [SIMPLE_PROMPT]); + assertAssistantTextIncludes(projection, "fixture simple ok"); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/codex_transcript.ndjson new file mode 100644 index 00000000000..adf4fc6c040 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/codex_transcript.ndjson @@ -0,0 +1,31 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"simple","metadata":{"source":"codex-app-server-probe","fileName":"simple.ndjson","description":"One thread and one turn with a deterministic text-only response."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019dadea-f49b-7012-aa03-534f1bfc3181","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739349,"updatedAt":1776739349,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-42-29-019dadea-f49b-7012-aa03-534f1bfc3181.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Respond with the following text: fixture simple ok","type":"text"}],"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019dadea-f49b-7012-aa03-534f1bfc3181","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739349,"updatedAt":1776739349,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-42-29-019dadea-f49b-7012-aa03-534f1bfc3181.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019dadea-f4a8-75f0-9e95-07b357d41cfe","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turn":{"id":"019dadea-f4a8-75f0-9e95-07b357d41cfe","items":[],"status":"inProgress","error":null,"startedAt":1776739349,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"c0d2203d-1eb7-48ef-91c8-e99e72dfa899","content":[{"type":"text","text":"Respond with the following text: fixture simple ok","text_elements":[]}]},"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"c0d2203d-1eb7-48ef-91c8-e99e72dfa899","content":[{"type":"text","text":"Respond with the following text: fixture simple ok","text_elements":[]}]},"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0dae408e3c0b132b0169e6e41ba700819a9d9801b062c4c022","summary":[],"content":[]},"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0dae408e3c0b132b0169e6e41ba700819a9d9801b062c4c022","summary":[],"content":[]},"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0dae408e3c0b132b0169e6e41bf13c819a885a64988b80a02d","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe","itemId":"msg_0dae408e3c0b132b0169e6e41bf13c819a885a64988b80a02d","delta":"fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe","itemId":"msg_0dae408e3c0b132b0169e6e41bf13c819a885a64988b80a02d","delta":" simple"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe","itemId":"msg_0dae408e3c0b132b0169e6e41bf13c819a885a64988b80a02d","delta":" ok"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0dae408e3c0b132b0169e6e41bf13c819a885a64988b80a02d","text":"fixture simple ok","phase":"final_answer","memoryCitation":null},"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turnId":"019dadea-f4a8-75f0-9e95-07b357d41cfe","tokenUsage":{"total":{"totalTokens":28260,"inputTokens":28223,"cachedInputTokens":3456,"outputTokens":37,"reasoningOutputTokens":28},"last":{"totalTokens":28260,"inputTokens":28223,"cachedInputTokens":3456,"outputTokens":37,"reasoningOutputTokens":28},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadea-f49b-7012-aa03-534f1bfc3181","turn":{"id":"019dadea-f4a8-75f0-9e95-07b357d41cfe","items":[],"status":"completed","error":null,"startedAt":1776739349,"completedAt":1776739356,"durationMs":6434}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/cursor_transcript.ndjson new file mode 100644 index 00000000000..76b8b5278c4 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/cursor_transcript.ndjson @@ -0,0 +1,11 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"simple","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-78cfad4d-9b71-4534-b49c-fbcfab939a0f"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":false,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-78cfad4d-9b71-4534-b49c-fbcfab939a0f"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Respond with the following text: fixture simple ok","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-74d16078-96e5-44f9-ac08-9d854eeb3bab","agentId":"agent-78cfad4d-9b71-4534-b49c-fbcfab939a0f"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-74d16078-96e5-44f9-ac08-9d854eeb3bab","update":{"type":"text-delta","text":"fixture simple ok"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-74d16078-96e5-44f9-ac08-9d854eeb3bab","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-74d16078-96e5-44f9-ac08-9d854eeb3bab","update":{"type":"step-completed","stepId":1,"stepDurationMs":1246}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-74d16078-96e5-44f9-ac08-9d854eeb3bab","update":{"type":"turn-ended","usage":{"inputTokens":10393,"outputTokens":38,"cacheReadTokens":1856,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-74d16078-96e5-44f9-ac08-9d854eeb3bab","requestId":"a35bc166-9742-4bd7-bcc4-22620560c2d2","status":"finished","result":"fixture simple ok","model":{"id":"composer-2.5"},"durationMs":3983}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-78cfad4d-9b71-4534-b49c-fbcfab939a0f"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/grok_transcript.ndjson new file mode 100644 index 00000000000..15a35708bca --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/grok_transcript.ndjson @@ -0,0 +1,12 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"simple","metadata":{"generatedBy":"live-grok-shape-probe","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"grok-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"},{"modelId":"grok-composer-2.5-fast","name":"Grok Composer 2.5 Fast"}]}}}} +{"type":"expect_outbound","label":"session.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Respond with the following text: fixture simple ok"}]}}} +{"type":"emit_inbound","label":"reasoning.1","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"I should "}}}}} +{"type":"emit_inbound","label":"reasoning.2","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"answer exactly."}}}}} +{"type":"emit_inbound","label":"assistant.1","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"fixture simple "}}}}} +{"type":"emit_inbound","label":"assistant.2","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}} +{"type":"emit_inbound","label":"prompt.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/simple/input.ts new file mode 100644 index 00000000000..2d011aace21 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/input.ts @@ -0,0 +1,7 @@ +import { SIMPLE_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function simpleInput(): OrchestratorFixtureInput { + return { + steps: [{ type: "message", text: SIMPLE_PROMPT }], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/opencode_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/opencode_transcript.ndjson new file mode 100644 index 00000000000..b38d99658b1 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/opencode_transcript.ndjson @@ -0,0 +1,19 @@ +{"type":"transcript_start","provider":"opencode","protocol":"opencode-sdk.sse","version":"1.14.39","scenario":"simple","metadata":{"source":"authenticated-opencode-sdk-probe","capturedAt":"2026-06-18","nativeSessionId":"ses_1236c522dffeunHBidt20OIzWc","model":"openai/gpt-5.4-mini","description":"One real OpenCode session and text-only turn, captured at the SDK request/SSE boundary."}} +{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}} +{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"title":"","permission":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_1236c522dffeunHBidt20OIzWc","slug":"shiny-tiger","version":"1.14.39","projectID":"global","directory":"/private/tmp/t3-opencode-probe","path":"private/tmp/t3-opencode-probe","title":"T3 OpenCode v2 probe","time":{"created":1781817126354,"updated":1781817126354}}}} +{"type":"expect_outbound","label":"session.promptAsync","frame":{"type":"session.promptAsync","input":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","model":{"providerID":"openai","modelID":"gpt-5.4-mini"},"agent":"build","parts":[{"type":"text","text":"Respond with the following text: fixture simple ok"}]}}} +{"type":"emit_inbound","label":"session.promptAsync.response","frame":{"type":"sdk.response","operation":"session.promptAsync","data":null}} +{"type":"emit_inbound","label":"message.updated.user","frame":{"type":"sdk.event","event":{"id":"evt_edc93ade5002giWB7gMKgKTh1N","type":"message.updated","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","info":{"role":"user","time":{"created":1781817126365},"agent":"build","model":{"providerID":"openai","modelID":"gpt-5.4-mini"},"id":"msg_edc93addd001jzzrE6lotn7BJj","sessionID":"ses_1236c522dffeunHBidt20OIzWc","summary":{"diffs":[]}}}}}} +{"type":"emit_inbound","label":"session.status.busy","frame":{"type":"sdk.event","event":{"id":"evt_edc93ade80015pjdCBdTty67tQ","type":"session.status","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","status":{"type":"busy"}}}}} +{"type":"emit_inbound","label":"step-start","frame":{"type":"sdk.event","event":{"id":"evt_edc93b29c002bOE0jF6Ngtp9WQ","type":"message.part.updated","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","part":{"id":"prt_edc93b29c001e2RRrSU0wC3EBK","messageID":"msg_edc93ade1001nkGj5Jt1JAd8nq","sessionID":"ses_1236c522dffeunHBidt20OIzWc","type":"step-start"},"time":1781817127580}}}} +{"type":"emit_inbound","label":"reasoning.started","frame":{"type":"sdk.event","event":{"id":"evt_edc93b3ac002IjQHVrdbEr0grF","type":"message.part.updated","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","part":{"id":"prt_edc93b3ac0018TDuhzb5SgaTNW","messageID":"msg_edc93ade1001nkGj5Jt1JAd8nq","sessionID":"ses_1236c522dffeunHBidt20OIzWc","type":"reasoning","text":"","time":{"start":1781817127852},"metadata":{"openai":{"itemId":"rs_0cebf1703d45601c016a345f27cd448193bf5ca8d64792912b","reasoningEncryptedContent":""}}},"time":1781817127852}}}} +{"type":"emit_inbound","label":"reasoning.completed","frame":{"type":"sdk.event","event":{"id":"evt_edc93b5b7001F8JgWVWz3axRi4","type":"message.part.updated","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","part":{"id":"prt_edc93b3ac0018TDuhzb5SgaTNW","messageID":"msg_edc93ade1001nkGj5Jt1JAd8nq","sessionID":"ses_1236c522dffeunHBidt20OIzWc","type":"reasoning","text":"","time":{"start":1781817127852,"end":1781817128375},"metadata":{"openai":{"itemId":"rs_0cebf1703d45601c016a345f27cd448193bf5ca8d64792912b","reasoningEncryptedContent":""}}},"time":1781817128375}}}} +{"type":"emit_inbound","label":"assistant.started","frame":{"type":"sdk.event","event":{"id":"evt_edc93b5b8002b8hqk8j4ycOQDv","type":"message.part.updated","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","part":{"id":"prt_edc93b5b8001T13mKZn2DRRWdH","messageID":"msg_edc93ade1001nkGj5Jt1JAd8nq","sessionID":"ses_1236c522dffeunHBidt20OIzWc","type":"text","text":"","time":{"start":1781817128376},"metadata":{"openai":{"itemId":"msg_0cebf1703d45601c016a345f284b7c819398d57b692541e5a1","phase":"final_answer"}}},"time":1781817128376}}}} +{"type":"emit_inbound","label":"assistant.completed","frame":{"type":"sdk.event","event":{"id":"evt_edc93b5b90010jWiRMyH8CF4tS","type":"message.part.updated","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","part":{"id":"prt_edc93b5b8001T13mKZn2DRRWdH","messageID":"msg_edc93ade1001nkGj5Jt1JAd8nq","sessionID":"ses_1236c522dffeunHBidt20OIzWc","type":"text","text":"fixture simple ok","time":{"start":1781817128376,"end":1781817128377},"metadata":{"openai":{"itemId":"msg_0cebf1703d45601c016a345f284b7c819398d57b692541e5a1","phase":"final_answer"}}},"time":1781817128377}}}} +{"type":"emit_inbound","label":"step-finish","frame":{"type":"sdk.event","event":{"id":"evt_edc93b5d500236244jGWEFb6ac","type":"message.part.updated","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","part":{"id":"prt_edc93b5d5001CPBPBe0mxI1DbU","reason":"stop","messageID":"msg_edc93ade1001nkGj5Jt1JAd8nq","sessionID":"ses_1236c522dffeunHBidt20OIzWc","type":"step-finish","tokens":{"total":10837,"input":571,"output":9,"reasoning":17,"cache":{"write":0,"read":10240}},"cost":0},"time":1781817128405}}}} +{"type":"emit_inbound","label":"message.updated.assistant","frame":{"type":"sdk.event","event":{"id":"evt_edc93b5d6001bMOQP8gbJZ0od6","type":"message.updated","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","info":{"id":"msg_edc93ade1001nkGj5Jt1JAd8nq","parentID":"msg_edc93addd001jzzrE6lotn7BJj","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"total":10837,"input":571,"output":9,"reasoning":17,"cache":{"write":0,"read":10240}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781817126369},"sessionID":"ses_1236c522dffeunHBidt20OIzWc","finish":"stop"}}}}} +{"type":"emit_inbound","label":"message.updated.assistant.completed","frame":{"type":"sdk.event","event":{"id":"evt_edc93b5d7001XXJei8Ixk5qMHN","type":"message.updated","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","info":{"id":"msg_edc93ade1001nkGj5Jt1JAd8nq","parentID":"msg_edc93addd001jzzrE6lotn7BJj","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"total":10837,"input":571,"output":9,"reasoning":17,"cache":{"write":0,"read":10240}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781817126369,"completed":1781817128407},"sessionID":"ses_1236c522dffeunHBidt20OIzWc","finish":"stop"}}}}} +{"type":"emit_inbound","label":"session.status.busy.finalizing","frame":{"type":"sdk.event","event":{"id":"evt_edc93b5d7002NYE00v0LorwLmf","type":"session.status","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","status":{"type":"busy"}}}}} +{"type":"emit_inbound","label":"session.status.idle","frame":{"type":"sdk.event","event":{"id":"evt_edc93b5d8001wkyQQVJNv3M9vP","type":"session.status","properties":{"sessionID":"ses_1236c522dffeunHBidt20OIzWc","status":{"type":"idle"}}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_output.ts new file mode 100644 index 00000000000..d2db4ac5c97 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_output.ts @@ -0,0 +1,103 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertExecutionNodeKinds, + assertNoExtraAppRunsForProviderChildren, + assertRunProviderTurnCardinality, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + projectionFor, + SUBAGENT_PROMPT, +} from "../shared.ts"; + +export function assertClaudeSubagentOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ + result, + transcript, + runCount: 1, + runStatuses: ["completed"], + }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertExecutionNodeKinds(projection, ["root_turn", "subagent"]); + assertTurnItemTypes(projection, ["user_message", "subagent", "assistant_message"]); + assertRunProviderTurnCardinality({ projection, rootRunCount: 1 }); + assertNoExtraAppRunsForProviderChildren({ projection, expectedAppRuns: 1 }); + assertUserMessagesInclude(projection, [SUBAGENT_PROMPT]); + assertAssistantTextIncludes(projection, "claude-read-only-fixture"); + assertAssistantTextIncludes(projection, "ES2022"); + assert.lengthOf( + projection.turnItems.filter((item) => item.type === "dynamic_tool"), + 0, + "subagent tools must not be projected into the parent thread", + ); + + const subagentNodes = projection.nodes.filter((node) => node.kind === "subagent"); + assert.lengthOf(subagentNodes, 2); + assert.deepEqual( + subagentNodes.map((node) => node.status), + ["completed", "completed"], + ); + + assert.lengthOf(projection.subagents, 2); + assert.lengthOf(result.shellSnapshot.threads, 3); + assert.isTrue( + projection.subagents.some( + (subagent) => + subagent.prompt === + "Read the file `package.json` in the current working directory and return its full contents." && + subagent.result?.includes("claude-read-only-fixture"), + ), + ); + assert.isTrue( + projection.subagents.some( + (subagent) => + subagent.prompt === + "Read the file `tsconfig.json` in the current working directory and return its full contents." && + subagent.result?.includes("ES2022"), + ), + ); + for (const subagent of projection.subagents) { + assert.equal(subagent.origin, "provider_native"); + assert.equal(subagent.createdBy, "agent"); + assert.equal(subagent.driver, "claudeAgent"); + assert.equal(subagent.status, "completed"); + assert.isNull(subagent.providerThreadId); + assert.isNotNull(subagent.childThreadId); + assert.isNotNull(subagent.nativeTaskRef); + assert.isNotNull(subagent.completedAt); + if (subagent.childThreadId === null) { + throw new Error(`Subagent ${subagent.id} is missing its child thread`); + } + + const childProjection = result.projections.get(subagent.childThreadId); + assert.isDefined(childProjection); + assert.equal(childProjection.thread.lineage.parentThreadId, projection.thread.id); + assert.equal(childProjection.thread.lineage.relationshipToParent, "subagent"); + assert.isNull(childProjection.thread.activeProviderThreadId); + assert.lengthOf(childProjection.runs, 0); + assert.lengthOf(childProjection.providerThreads, 0); + assert.lengthOf(childProjection.providerTurns, 0); + assertExecutionNodeKinds(childProjection, ["root_turn", "tool_call"]); + assertTurnItemTypes(childProjection, ["user_message", "dynamic_tool", "assistant_message"]); + assertUserMessagesInclude(childProjection, [subagent.prompt]); + assert.isTrue( + childProjection.turnItems.some( + (item) => + item.type === "assistant_message" && + subagent.result !== null && + item.text.includes(subagent.result.slice(0, 40)), + ), + `child thread ${subagent.childThreadId} must contain the subagent response`, + ); + } +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_transcript.ndjson new file mode 100644 index 00000000000..26773f4b22e --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_transcript.ndjson @@ -0,0 +1,28 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"subagent","metadata":{"prompts":["Spawn 2 subagents, one to read package.json and one to read tsconfig.json"],"model":"claude-sonnet-4-6","nativeSessionId":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","queryMode":"streaming","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Spawn 2 subagents, one to read package.json and one to read tsconfig.json"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"4fde9c84-285e-4a68-9070-3704c94168f6","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"caf0439c-1455-4bd5-bcbf-14be3b13db5c","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"4fde9c84-285e-4a68-9070-3704c94168f6","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"5af52be5-9839-49fb-aebe-da4068d0ba2a","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-subagent","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"38e13364-a3ab-43cf-865f-cb790e39532b","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_014tG9SY8VfiqS7bCEAJ1oKt","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to spawn 2 subagents in parallel - one to read package.json and one to read tsconfig.json.","signature":"ErMCCmUIDhgCKkBRbKaYlD+XeRoQv536GegpaFwi9jwMXXtuPsLxongxP3e2xG7qRV+sWykxHk4twYSGIzmfHtgg9TYwH4IBe1GuMhFjbGF1ZGUtc29ubmV0LTQtNjgAQgh0aGlua2luZxIMasLti+z60m9mTQ7iGgwfWbzjzMVaZS/99/0iMBzqZIXucbe83OrH5rh5RJUMDW06UPJkxEczZtLVG/CEhmOLiPnJ23mzX4QZkbxHBip8IqOdwO4Zse+34IUUKau4H/PkUq20GEDeRENlujYtF+YkMI8wk9Oh3X/xKjUy+/kj0udJORGyQp7W0oTRRBojJNT6uYQ53rx+5d8BNwIjJMs2GUAlyBDCJz14UrfXDY3qQTqmtBowAlVKaKx6w8KZLNdGVFdH0nVzqugdMBgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2351,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2351},"output_tokens":0,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"682886b5-991a-42be-9f6f-0b92cd685c3d"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_014tG9SY8VfiqS7bCEAJ1oKt","type":"message","role":"assistant","content":[{"type":"text","text":"I'll spawn both subagents simultaneously right now!"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2351,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2351},"output_tokens":0,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"79a75f39-f73f-428d-9009-986d85044454"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_014tG9SY8VfiqS7bCEAJ1oKt","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FfHTrUeymLi4LfqJVmTZjz","name":"Agent","input":{"description":"Read package.json","prompt":"Read the file `package.json` in the current working directory and return its full contents."},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2351,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2351},"output_tokens":0,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"3688ac34-266c-43f3-bd97-863da3485701"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"task_started","task_id":"a35c9b7d354c20172","tool_use_id":"toolu_01FfHTrUeymLi4LfqJVmTZjz","description":"Read package.json","task_type":"local_agent","prompt":"Read the file `package.json` in the current working directory and return its full contents.","uuid":"5de9156d-9c9c-414e-9a5c-4f88878f7f27","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Read the file `package.json` in the current working directory and return its full contents."}]},"parent_tool_use_id":"toolu_01FfHTrUeymLi4LfqJVmTZjz","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"4e7570c6-2512-4307-99ea-37f4cb98ba74","timestamp":"2026-05-27T00:24:44.805Z"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_014tG9SY8VfiqS7bCEAJ1oKt","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0113VSWTCamvp1adXDZrK5NG","name":"Agent","input":{"description":"Read tsconfig.json","prompt":"Read the file `tsconfig.json` in the current working directory and return its full contents."},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2351,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2351},"output_tokens":0,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"caf23ae6-07b0-4934-a5bf-a4c7e95d112e"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"task_started","task_id":"a3d3213ffad1bd057","tool_use_id":"toolu_0113VSWTCamvp1adXDZrK5NG","description":"Read tsconfig.json","task_type":"local_agent","prompt":"Read the file `tsconfig.json` in the current working directory and return its full contents.","uuid":"e907d290-9015-42f1-a80b-f7d0835b9c96","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Read the file `tsconfig.json` in the current working directory and return its full contents."}]},"parent_tool_use_id":"toolu_0113VSWTCamvp1adXDZrK5NG","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"01811bb7-eb17-4da4-b511-0bbee0ae887f","timestamp":"2026-05-27T00:24:44.900Z"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"d61e6565-fa4c-4fd6-9174-17c93b91614b","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"task_progress","task_id":"a35c9b7d354c20172","tool_use_id":"toolu_01FfHTrUeymLi4LfqJVmTZjz","description":"Reading package.json","usage":{"total_tokens":8707,"tool_uses":1,"duration_ms":2053},"last_tool_name":"Read","uuid":"4ed5c537-4bc1-424f-9462-8efb9d5dd0ab","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01DqozNMytHgJ2P5Buwf5f6A","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01EV8j88CPEmhoWF3iuvaFSC","name":"Read","input":{"file_path":"/private/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-claude-agent-sdk-record-subagent-jAoxxI/package.json"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":1139,"cache_read_input_tokens":7492,"cache_creation":{"ephemeral_5m_input_tokens":1139,"ephemeral_1h_input_tokens":0},"output_tokens":73,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_01FfHTrUeymLi4LfqJVmTZjz","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"debf5765-e302-4724-8df7-c64d11108860"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"task_progress","task_id":"a3d3213ffad1bd057","tool_use_id":"toolu_0113VSWTCamvp1adXDZrK5NG","description":"Reading tsconfig.json","usage":{"total_tokens":8708,"tool_uses":1,"duration_ms":2093},"last_tool_name":"Read","uuid":"e31a6683-35ab-49f0-8913-1e8a8d559343","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_019MHopVQzAkpKERfKvRB1cj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01P6KQcUPy8t2bCbFjv8K4Jz","name":"Read","input":{"file_path":"/private/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-claude-agent-sdk-record-subagent-jAoxxI/tsconfig.json"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":1140,"cache_read_input_tokens":7492,"cache_creation":{"ephemeral_5m_input_tokens":1140,"ephemeral_1h_input_tokens":0},"output_tokens":73,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":"toolu_0113VSWTCamvp1adXDZrK5NG","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"a5d6d0ed-bc30-43fb-9295-f338fedbf020"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01P6KQcUPy8t2bCbFjv8K4Jz","type":"tool_result","content":"1\t{\"compilerOptions\":{\"module\":\"ESNext\",\"strict\":true,\"target\":\"ES2022\"}}"}]},"parent_tool_use_id":"toolu_0113VSWTCamvp1adXDZrK5NG","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"6814c55d-46df-4aeb-b594-f8819f353925","timestamp":"2026-05-27T00:24:46.995Z"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01EV8j88CPEmhoWF3iuvaFSC","type":"tool_result","content":"1\t{\"name\":\"claude-read-only-fixture\",\"private\":true,\"scripts\":{\"typecheck\":\"tsc --noEmit\"}}"}]},"parent_tool_use_id":"toolu_01FfHTrUeymLi4LfqJVmTZjz","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"cd552b77-8f0a-4984-b2e1-8eeea3e63148","timestamp":"2026-05-27T00:24:46.875Z"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"task_notification","task_id":"a3d3213ffad1bd057","tool_use_id":"toolu_0113VSWTCamvp1adXDZrK5NG","status":"completed","output_file":"","summary":"Read tsconfig.json","usage":{"total_tokens":10750,"tool_uses":1,"duration_ms":5183},"uuid":"71cc121b-cc9f-42ad-aaea-40730ef81125","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0113VSWTCamvp1adXDZrK5NG","type":"tool_result","content":[{"type":"text","text":"Here are the full contents of `tsconfig.json`:\n\n```json\n{\"compilerOptions\":{\"module\":\"ESNext\",\"strict\":true,\"target\":\"ES2022\"}}\n```\n\nIt is a minimal TypeScript configuration with three compiler options set:\n- `module`: `ESNext` — uses ES module syntax\n- `strict`: `true` — enables all strict type-checking options\n- `target`: `ES2022` — compiles down to ES2022 JavaScript"},{"type":"text","text":"agentId: a3d3213ffad1bd057 (use SendMessage with to: 'a3d3213ffad1bd057' to continue this agent)\ntotal_tokens: 10783\ntool_uses: 1\nduration_ms: 5186"}]}]},"parent_tool_use_id":null,"session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"526e0d3c-7688-4b39-8b54-4aa438b93f07","timestamp":"2026-05-27T00:24:50.087Z","tool_use_result":{"status":"completed","prompt":"Read the file `tsconfig.json` in the current working directory and return its full contents.","agentId":"a3d3213ffad1bd057","agentType":"general-purpose","content":[{"type":"text","text":"Here are the full contents of `tsconfig.json`:\n\n```json\n{\"compilerOptions\":{\"module\":\"ESNext\",\"strict\":true,\"target\":\"ES2022\"}}\n```\n\nIt is a minimal TypeScript configuration with three compiler options set:\n- `module`: `ESNext` — uses ES module syntax\n- `strict`: `true` — enables all strict type-checking options\n- `target`: `ES2022` — compiles down to ES2022 JavaScript"}],"totalDurationMs":5186,"totalTokens":10783,"totalToolUseCount":1,"usage":{"input_tokens":1,"cache_creation_input_tokens":2043,"cache_read_input_tokens":8632,"output_tokens":107,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":2043},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":107,"cache_read_input_tokens":8632,"cache_creation_input_tokens":2043,"cache_creation":{"ephemeral_5m_input_tokens":2043,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"toolStats":{"readCount":1,"searchCount":0,"bashCount":0,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"task_notification","task_id":"a35c9b7d354c20172","tool_use_id":"toolu_01FfHTrUeymLi4LfqJVmTZjz","status":"completed","output_file":"","summary":"Read package.json","usage":{"total_tokens":10755,"tool_uses":1,"duration_ms":6211},"uuid":"5fd5a6f9-6045-4b50-99d9-a0bbe8991047","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FfHTrUeymLi4LfqJVmTZjz","type":"tool_result","content":[{"type":"text","text":"Here are the full contents of `package.json`:\n\n```json\n{\"name\":\"claude-read-only-fixture\",\"private\":true,\"scripts\":{\"typecheck\":\"tsc --noEmit\"}}\n```\n\nFormatted for readability:\n\n```json\n{\n \"name\": \"claude-read-only-fixture\",\n \"private\": true,\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\"\n }\n}\n```\n\nThe file is minimal, containing just the package name (`claude-read-only-fixture`), a `private: true` flag, and a single script (`typecheck`) that runs the TypeScript compiler in no-emit mode."},{"type":"text","text":"agentId: a35c9b7d354c20172 (use SendMessage with to: 'a35c9b7d354c20172' to continue this agent)\ntotal_tokens: 10839\ntool_uses: 1\nduration_ms: 6213"}]}]},"parent_tool_use_id":null,"session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"3dac736f-a3d6-4f43-b01d-92c17f34b020","timestamp":"2026-05-27T00:24:51.018Z","tool_use_result":{"status":"completed","prompt":"Read the file `package.json` in the current working directory and return its full contents.","agentId":"a35c9b7d354c20172","agentType":"general-purpose","content":[{"type":"text","text":"Here are the full contents of `package.json`:\n\n```json\n{\"name\":\"claude-read-only-fixture\",\"private\":true,\"scripts\":{\"typecheck\":\"tsc --noEmit\"}}\n```\n\nFormatted for readability:\n\n```json\n{\n \"name\": \"claude-read-only-fixture\",\n \"private\": true,\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\"\n }\n}\n```\n\nThe file is minimal, containing just the package name (`claude-read-only-fixture`), a `private: true` flag, and a single script (`typecheck`) that runs the TypeScript compiler in no-emit mode."}],"totalDurationMs":6213,"totalTokens":10839,"totalToolUseCount":1,"usage":{"input_tokens":1,"cache_creation_input_tokens":2049,"cache_read_input_tokens":8631,"output_tokens":158,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":2049},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":158,"cache_read_input_tokens":8631,"cache_creation_input_tokens":2049,"cache_creation":{"ephemeral_5m_input_tokens":2049,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"toolStats":{"readCount":1,"searchCount":0,"bashCount":0,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}}}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_0149tB2WYRkkPLcXTkYZknLb","type":"message","role":"assistant","content":[{"type":"text","text":"Both subagents completed in parallel! Here's a summary of what they found:\n\n---\n\n### 📦 `package.json`\n```json\n{\n \"name\": \"claude-read-only-fixture\",\n \"private\": true,\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\"\n }\n}\n```\nA minimal package config — private package named `claude-read-only-fixture` with a single `typecheck` script.\n\n---\n\n### 🔧 `tsconfig.json`\n```json\n{\n \"compilerOptions\": {\n \"module\": \"ESNext\",\n \"strict\": true,\n \"target\": \"ES2022\"\n }\n}\n```\nA lean TypeScript config targeting **ES2022**, using **ESNext modules**, and enabling **strict** type-checking."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":209,"cache_creation_input_tokens":481,"cache_read_input_tokens":11806,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":481},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","uuid":"dbd66caf-dabe-4f13-956a-3a4e276c1e80"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":16909,"duration_api_ms":18827,"num_turns":3,"result":"Both subagents completed in parallel! Here's a summary of what they found:\n\n---\n\n### 📦 `package.json`\n```json\n{\n \"name\": \"claude-read-only-fixture\",\n \"private\": true,\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\"\n }\n}\n```\nA minimal package config — private package named `claude-read-only-fixture` with a single `typecheck` script.\n\n---\n\n### 🔧 `tsconfig.json`\n```json\n{\n \"compilerOptions\": {\n \"module\": \"ESNext\",\n \"strict\": true,\n \"target\": \"ES2022\"\n }\n}\n```\nA lean TypeScript config targeting **ES2022**, using **ESNext modules**, and enabling **strict** type-checking.","stop_reason":"end_turn","session_id":"edf38df1-8d2f-4781-b9e3-f9e72ba7b76d","total_cost_usd":0.06497865000000001,"usage":{"input_tokens":212,"cache_creation_input_tokens":2832,"cache_read_input_tokens":21261,"output_tokens":419,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2832,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":209,"output_tokens":201,"cache_read_input_tokens":11806,"cache_creation_input_tokens":481,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":481},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":220,"outputTokens":917,"cacheReadInputTokens":53508,"cacheCreationInputTokens":9203,"webSearchRequests":0,"costUSD":0.06497865000000001,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"aa48d52b-3473-44a8-b291-3844d61a4105"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_output.ts new file mode 100644 index 00000000000..143b9bd9002 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_output.ts @@ -0,0 +1,113 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertExecutionNodeKinds, + assertNoExtraAppRunsForProviderChildren, + assertRunProviderTurnCardinality, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + projectionFor, + SUBAGENT_PROMPT, +} from "../shared.ts"; + +export function assertSubagentOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ + result, + transcript, + runCount: 1, + runStatuses: ["completed"], + }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertTurnItemTypes(projection, ["user_message", "subagent", "assistant_message"]); + assertExecutionNodeKinds(projection, ["root_turn", "subagent"]); + assertRunProviderTurnCardinality({ projection, rootRunCount: 1 }); + assertNoExtraAppRunsForProviderChildren({ projection, expectedAppRuns: 1 }); + assertUserMessagesInclude(projection, [SUBAGENT_PROMPT]); + assert.equal(projection.runs.length, 1, "subagent provider turns must not become app runs"); + assert.lengthOf( + projection.turnItems.filter((item) => item.type === "command_execution"), + 0, + "subagent commands must not be projected into the parent thread", + ); + + assert.lengthOf(projection.subagents, 2); + assert.lengthOf(result.shellSnapshot.threads, 3); + assert.deepEqual( + projection.subagents.map((subagent) => subagent.status), + ["completed", "completed"], + ); + assert.isTrue( + projection.subagents.some( + (subagent) => + subagent.prompt.includes("Read package.json only") && + subagent.result?.includes("Package name: `effect-codex-app-server`"), + ), + ); + assert.isTrue( + projection.subagents.some( + (subagent) => + subagent.prompt.includes("Read tsconfig.json only") && + subagent.result?.includes("`extends`: `../../tsconfig.base.json`"), + ), + ); + + for (const subagent of projection.subagents) { + assert.equal(subagent.origin, "provider_native"); + assert.equal(subagent.createdBy, "agent"); + assert.equal(subagent.driver, "codex"); + assert.isNotNull(subagent.childThreadId); + assert.isNotNull(subagent.providerThreadId); + assert.isNotNull(subagent.nativeTaskRef); + assert.isNotNull(subagent.completedAt); + if (subagent.childThreadId === null) { + throw new Error(`Subagent ${subagent.id} is missing its child thread`); + } + + const providerThread = projection.providerThreads.find( + (thread) => thread.id === subagent.providerThreadId, + ); + assert.isDefined(providerThread); + assert.equal(providerThread.appThreadId, subagent.childThreadId); + assert.isNull(providerThread.ownerNodeId); + + const childProjection = result.projections.get(subagent.childThreadId); + assert.isDefined(childProjection); + assert.equal(childProjection.thread.lineage.parentThreadId, projection.thread.id); + assert.equal(childProjection.thread.lineage.relationshipToParent, "subagent"); + assert.equal(childProjection.thread.activeProviderThreadId, providerThread.id); + assert.lengthOf(childProjection.runs, 0); + assert.lengthOf(childProjection.providerThreads, 1); + assert.lengthOf(childProjection.providerTurns, 1); + assertTurnItemTypes(childProjection, [ + "user_message", + "command_execution", + "assistant_message", + ]); + assertUserMessagesInclude(childProjection, [subagent.prompt]); + assert.isTrue( + childProjection.turnItems.some( + (item) => + item.type === "assistant_message" && + subagent.result !== null && + item.text.includes(subagent.result.slice(0, 40)), + ), + `child thread ${subagent.childThreadId} must contain the subagent response`, + ); + } + + const rootProviderThread = projection.providerThreads.find( + (thread) => thread.ownerNodeId === null && thread.appThreadId === projection.thread.id, + ); + assert.isDefined(rootProviderThread); + assert.equal(rootProviderThread.appThreadId, projection.thread.id); + assert.equal(projection.thread.activeProviderThreadId, rootProviderThread.id); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_transcript.ndjson new file mode 100644 index 00000000000..f8c8355aa67 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_transcript.ndjson @@ -0,0 +1,577 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"subagent","metadata":{"source":"codex-app-server-probe","fileName":"subagent.ndjson","description":"One root turn that asks Codex to spawn two collab agents."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739437,"updatedAt":1776739437,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-43-57-019dadec-4bed-7991-a1b7-1ed4fc1038b8.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"approvalPolicy":"on-request","input":[{"text":"Spawn 2 subagents, one to read package.json and one to read tsconfig.json","type":"text"}],"sandboxPolicy":{"networkAccess":false,"type":"readOnly"},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739437,"updatedAt":1776739437,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-43-57-019dadec-4bed-7991-a1b7-1ed4fc1038b8.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019dadec-4bfa-73e1-868c-9251fd3f65c3","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turn":{"id":"019dadec-4bfa-73e1-868c-9251fd3f65c3","items":[],"status":"inProgress","error":null,"startedAt":1776739437,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"b582fa32-d45c-4963-b7e1-1ed3c8296742","content":[{"type":"text","text":"Spawn 2 subagents, one to read package.json and one to read tsconfig.json","text_elements":[]}]},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"b582fa32-d45c-4963-b7e1-1ed3c8296742","content":[{"type":"text","text":"Spawn 2 subagents, one to read package.json and one to read tsconfig.json","text_elements":[]}]},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_04b4cd274d53a3dd0169e6e47486c4819a9dbce004b2ccde45","summary":[],"content":[]},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_04b4cd274d53a3dd0169e6e47486c4819a9dbce004b2ccde45","summary":[],"content":[]},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","text":"","phase":"commentary","memoryCitation":null},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":"Sp"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":"awning"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" two"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" explorer"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" agents"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" now"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" one"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" scoped"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":"package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" one"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" scoped"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":"ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":"config"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":"`."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" They"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" will"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" read"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" only"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" report"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":" back"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_04b4cd274d53a3dd0169e6e47b6d84819a81f30d787a641f45","text":"Spawning two explorer agents now: one scoped to `package.json`, one scoped to `tsconfig.json`. They will read only and report back.","phase":"commentary","memoryCitation":null},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"collabAgentToolCall","id":"call_PEuMv7rWiiPuZwUv4DOiM9jl","tool":"spawnAgent","status":"inProgress","senderThreadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","receiverThreadIds":[],"prompt":"You are in /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server. Read package.json only. Do not edit files. Report the package name, scripts, dependencies/devDependencies summary, and any notable package metadata. Keep it concise.","model":"","reasoningEffort":"low","agentsStates":{}},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","tokenUsage":{"total":{"totalTokens":28790,"inputTokens":28131,"cachedInputTokens":28032,"outputTokens":659,"reasoningOutputTokens":516},"last":{"totalTokens":28790,"inputTokens":28131,"cachedInputTokens":28032,"outputTokens":659,"reasoningOutputTokens":516},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turn":{"id":"019dadec-86a3-73d1-82b8-c97820fc845e","items":[],"status":"inProgress","error":null,"startedAt":1776739452,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"collabAgentToolCall","id":"call_PEuMv7rWiiPuZwUv4DOiM9jl","tool":"spawnAgent","status":"completed","senderThreadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","receiverThreadIds":["019dadec-8671-7861-98e8-f458a0fa9ed6"],"prompt":"You are in /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server. Read package.json only. Do not edit files. Report the package name, scripts, dependencies/devDependencies summary, and any notable package metadata. Keep it concise.","model":"gpt-5.4","reasoningEffort":"low","agentsStates":{"019dadec-8671-7861-98e8-f458a0fa9ed6":{"status":"pendingInit","message":null}}},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"526a55d9-9300-4ab0-a4c2-e2e56d541d74","content":[{"type":"text","text":"You are in /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server. Read package.json only. Do not edit files. Report the package name, scripts, dependencies/devDependencies summary, and any notable package metadata. Keep it concise.","text_elements":[]}]},"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"526a55d9-9300-4ab0-a4c2-e2e56d541d74","content":[{"type":"text","text":"You are in /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server. Read package.json only. Do not edit files. Report the package name, scripts, dependencies/devDependencies summary, and any notable package metadata. Keep it concise.","text_elements":[]}]},"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"collabAgentToolCall","id":"call_iJmO2XRSBlxqdsWgYJbWopX8","tool":"spawnAgent","status":"inProgress","senderThreadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","receiverThreadIds":[],"prompt":"You are in /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server. Read tsconfig.json only. Do not edit files. Report the extends/compilerOptions/include/exclude or equivalent structure, plus any notable TypeScript configuration choices. Keep it concise.","model":"","reasoningEffort":"low","agentsStates":{}},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","tokenUsage":{"total":{"totalTokens":57729,"inputTokens":56963,"cachedInputTokens":56064,"outputTokens":766,"reasoningOutputTokens":516},"last":{"totalTokens":28939,"inputTokens":28832,"cachedInputTokens":28032,"outputTokens":107,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"collabAgentToolCall","id":"call_iJmO2XRSBlxqdsWgYJbWopX8","tool":"spawnAgent","status":"completed","senderThreadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","receiverThreadIds":["019dadec-92b5-78c0-b9f1-f729dd16b784"],"prompt":"You are in /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server. Read tsconfig.json only. Do not edit files. Report the extends/compilerOptions/include/exclude or equivalent structure, plus any notable TypeScript configuration choices. Keep it concise.","model":"gpt-5.4","reasoningEffort":"low","agentsStates":{"019dadec-92b5-78c0-b9f1-f729dd16b784":{"status":"pendingInit","message":null}}},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turn":{"id":"019dadec-92de-7873-a0c8-cfc82a9a0801","items":[],"status":"inProgress","error":null,"startedAt":1776739455,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","text":"","phase":"commentary","memoryCitation":null},"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":"Reading"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":"package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":" only"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":" leaving"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":" workspace"}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":" unchanged"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0cac0024e4c3e1140169e6e47fe524819ba6db378fcb930527","text":"Reading `package.json` only and leaving the workspace unchanged.","phase":"commentary","memoryCitation":null},"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e"}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","tokenUsage":{"total":{"totalTokens":25637,"inputTokens":25538,"cachedInputTokens":2432,"outputTokens":99,"reasoningOutputTokens":0},"last":{"totalTokens":25637,"inputTokens":25538,"cachedInputTokens":2432,"outputTokens":99,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_IACZrSa5ZyzSOlbGjsIfruAx","command":"/bin/zsh -lc \"sed -n '1,220p' package.json\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"85606","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"read","command":"sed -n '1,220p' package.json","name":"package.json","path":"package.json"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_IACZrSa5ZyzSOlbGjsIfruAx","command":"/bin/zsh -lc \"sed -n '1,220p' package.json\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"85606","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"read","command":"sed -n '1,220p' package.json","name":"package.json","path":"package.json"}],"aggregatedOutput":"{\n \"name\": \"effect-codex-app-server\",\n \"private\": true,\n \"type\": \"module\",\n \"exports\": {\n \"./client\": {\n \"types\": \"./src/client.ts\",\n \"import\": \"./src/client.ts\"\n },\n \"./schema\": {\n \"types\": \"./src/schema.ts\",\n \"import\": \"./src/schema.ts\"\n },\n \"./rpc\": {\n \"types\": \"./src/rpc.ts\",\n \"import\": \"./src/rpc.ts\"\n },\n \"./protocol\": {\n \"types\": \"./src/protocol.ts\",\n \"import\": \"./src/protocol.ts\"\n },\n \"./replay\": {\n \"types\": \"./src/replay.ts\",\n \"import\": \"./src/replay.ts\"\n },\n \"./errors\": {\n \"types\": \"./src/errors.ts\",\n \"import\": \"./src/errors.ts\"\n }\n },\n \"scripts\": {\n \"dev\": \"tsdown src/client.ts src/rpc.ts src/protocol.ts src/schema.ts --format esm,cjs --dts --watch --clean\",\n \"build\": \"tsdown src/client.ts src/rpc.ts src/protocol.ts src/schema.ts --format esm,cjs --dts --clean\",\n \"prepare\": \"effect-language-service patch\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"generate\": \"bun run scripts/generate.ts\",\n \"probe\": \"bun run test/examples/codex-app-server-probe.ts\"\n },\n \"dependencies\": {\n \"effect\": \"catalog:\"\n },\n \"devDependencies\": {\n \"@effect/language-service\": \"catalog:\",\n \"@effect/openapi-generator\": \"catalog:\",\n \"@effect/platform-node\": \"catalog:\",\n \"@effect/vitest\": \"catalog:\",\n \"tsdown\": \"catalog:\",\n \"typescript\": \"catalog:\",\n \"vitest\": \"catalog:\"\n }\n}\n","exitCode":0,"durationMs":0},"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","text":"","phase":"commentary","memoryCitation":null},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":"Both"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" agents"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" are"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" running"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" I"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":"’ll"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" wait"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" for"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" their"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" summaries"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" relay"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" useful"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":" parts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_04b4cd274d53a3dd0169e6e481adac819a97b08e12770d8abb","text":"Both agents are running. I’ll wait for their summaries and relay the useful parts.","phase":"commentary","memoryCitation":null},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" only"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" name"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"effect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"od"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-app"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Metadata"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"private"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" true"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"type"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" module"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"ports"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"./"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"client"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"./"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"schema"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"./"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"rpc"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"./"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"protocol"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"./"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"re"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"play"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"./"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"errors"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" all"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" pointing"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Type"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"Script"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" files"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" under"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"src"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"/"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Scripts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"dev"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"down"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" watch"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" build"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" for"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" selected"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" entry"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"points"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"build"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"collabAgentToolCall","id":"call_VHAnY7OGDl0gPQyCvmDHfQAR","tool":"wait","status":"inProgress","senderThreadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","receiverThreadIds":["019dadec-8671-7861-98e8-f458a0fa9ed6","019dadec-92b5-78c0-b9f1-f729dd16b784"],"prompt":null,"model":null,"reasoningEffort":null,"agentsStates":{}},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"down"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" E"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"SM"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"/C"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"JS"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"/D"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","tokenUsage":{"total":{"totalTokens":86801,"inputTokens":85944,"cachedInputTokens":84608,"outputTokens":857,"reasoningOutputTokens":516},"last":{"totalTokens":29072,"inputTokens":28981,"cachedInputTokens":28544,"outputTokens":91,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"TS"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" build"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"prepare"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"effect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-language"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-service"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" patch"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"type"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"check"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" --"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"no"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"Emit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"test"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"vit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"est"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" run"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"generate"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" runs"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"scripts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"/g"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"enerate"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":".ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"probe"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" runs"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" app"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" probe"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" example"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Dependencies"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" only"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"effect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Dev"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" dependencies"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Effect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" tooling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Open"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"API"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" generator"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" platform"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-node"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Vit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"est"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" integration"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"down"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"typescript"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"vit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"est"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" Version"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"ing"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" uses"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" workspace"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" catalog"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" protocol"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":"catalog"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","itemId":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","delta":":`"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0cac0024e4c3e1140169e6e4823318819b813207b3fd805826","text":"`package.json` only:\n\n- Package name: `effect-codex-app-server`\n- Metadata: `private: true`, `type: module`\n- Exports: `./client`, `./schema`, `./rpc`, `./protocol`, `./replay`, `./errors`, all pointing to TypeScript source files under `src/`\n- Scripts:\n - `dev`: tsdown watch build for selected entrypoints\n - `build`: tsdown ESM/CJS/DTS build\n - `prepare`: `effect-language-service patch`\n - `typecheck`: `tsc --noEmit`\n - `test`: `vitest run`\n - `generate`: runs `scripts/generate.ts`\n - `probe`: runs Codex app-server probe example\n- Dependencies: only `effect`\n- Dev dependencies: Effect tooling, OpenAPI generator, platform-node, Vitest integration, `tsdown`, `typescript`, `vitest`\n- Versioning uses workspace catalog protocol: `catalog:`","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turnId":"019dadec-86a3-73d1-82b8-c97820fc845e","tokenUsage":{"total":{"totalTokens":51942,"inputTokens":51629,"cachedInputTokens":27904,"outputTokens":313,"reasoningOutputTokens":0},"last":{"totalTokens":26305,"inputTokens":26091,"cachedInputTokens":25472,"outputTokens":214,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadec-8671-7861-98e8-f458a0fa9ed6","turn":{"id":"019dadec-86a3-73d1-82b8-c97820fc845e","items":[],"status":"completed","error":null,"startedAt":1776739452,"completedAt":1776739459,"durationMs":7390}}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"collabAgentToolCall","id":"call_VHAnY7OGDl0gPQyCvmDHfQAR","tool":"wait","status":"completed","senderThreadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","receiverThreadIds":["019dadec-8671-7861-98e8-f458a0fa9ed6"],"prompt":null,"model":null,"reasoningEffort":null,"agentsStates":{"019dadec-8671-7861-98e8-f458a0fa9ed6":{"status":"completed","message":"`package.json` only:\n\n- Package name: `effect-codex-app-server`\n- Metadata: `private: true`, `type: module`\n- Exports: `./client`, `./schema`, `./rpc`, `./protocol`, `./replay`, `./errors`, all pointing to TypeScript source files under `src/`\n- Scripts:\n - `dev`: tsdown watch build for selected entrypoints\n - `build`: tsdown ESM/CJS/DTS build\n - `prepare`: `effect-language-service patch`\n - `typecheck`: `tsc --noEmit`\n - `test`: `vitest run`\n - `generate`: runs `scripts/generate.ts`\n - `probe`: runs Codex app-server probe example\n- Dependencies: only `effect`\n- Dev dependencies: Effect tooling, OpenAPI generator, platform-node, Vitest integration, `tsdown`, `typescript`, `vitest`\n- Versioning uses workspace catalog protocol: `catalog:`"}}},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","tokenUsage":{"total":{"totalTokens":86801,"inputTokens":85944,"cachedInputTokens":84608,"outputTokens":857,"reasoningOutputTokens":516},"last":{"totalTokens":29072,"inputTokens":28981,"cachedInputTokens":28544,"outputTokens":91,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"42af2ce8-54d7-44c7-b8d1-b06d07252994","content":[{"type":"text","text":"You are in /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server. Read tsconfig.json only. Do not edit files. Report the extends/compilerOptions/include/exclude or equivalent structure, plus any notable TypeScript configuration choices. Keep it concise.","text_elements":[]}]},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"42af2ce8-54d7-44c7-b8d1-b06d07252994","content":[{"type":"text","text":"You are in /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server. Read tsconfig.json only. Do not edit files. Report the extends/compilerOptions/include/exclude or equivalent structure, plus any notable TypeScript configuration choices. Keep it concise.","text_elements":[]}]},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","tokenUsage":{"total":{"totalTokens":28790,"inputTokens":28131,"cachedInputTokens":28032,"outputTokens":659,"reasoningOutputTokens":516},"last":{"totalTokens":28790,"inputTokens":28131,"cachedInputTokens":28032,"outputTokens":659,"reasoningOutputTokens":516},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748633},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335433},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_073f47c9f548c99b0169e6e485cba4819ab336315c13c3d0b0","summary":[],"content":[]},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_04b4cd274d53a3dd0169e6e485fa44819aa4aeb85ff58480fc","summary":[],"content":[]},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_073f47c9f548c99b0169e6e485cba4819ab336315c13c3d0b0","summary":[],"content":[]},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","text":"","phase":"commentary","memoryCitation":null},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":"Reading"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":"ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":"config"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":" only"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":" will"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":" report"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":" requested"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":" structure"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_073f47c9f548c99b0169e6e4861f34819aa52c1968a4557e3f","text":"Reading `tsconfig.json` only and will report the requested structure.","phase":"commentary","memoryCitation":null},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_04b4cd274d53a3dd0169e6e485fa44819aa4aeb85ff58480fc","summary":[],"content":[]},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","tokenUsage":{"total":{"totalTokens":54450,"inputTokens":53672,"cachedInputTokens":53504,"outputTokens":778,"reasoningOutputTokens":534},"last":{"totalTokens":25660,"inputTokens":25541,"cachedInputTokens":25472,"outputTokens":119,"reasoningOutputTokens":18},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748633},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335433},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_SDdfBoFgywZ8TrT74OddggA3","command":"/bin/zsh -lc \"sed -n '1,220p' tsconfig.json\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"53307","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"read","command":"sed -n '1,220p' tsconfig.json","name":"tsconfig.json","path":"tsconfig.json"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_SDdfBoFgywZ8TrT74OddggA3","command":"/bin/zsh -lc \"sed -n '1,220p' tsconfig.json\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"53307","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"read","command":"sed -n '1,220p' tsconfig.json","name":"tsconfig.json","path":"tsconfig.json"}],"aggregatedOutput":"{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"plugins\": [\n {\n \"name\": \"@effect/language-service\",\n \"namespaceImportPackages\": [\"@effect/platform-node\"],\n \"diagnosticSeverity\": {\n \"importFromBarrel\": \"error\",\n \"anyUnknownInErrorContext\": \"warning\",\n \"instanceOfSchema\": \"warning\",\n \"deterministicKeys\": \"warning\"\n }\n }\n ]\n },\n \"include\": [\"src\", \"scripts\", \"test\"]\n}\n","exitCode":0,"durationMs":0},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"collabAgentToolCall","id":"call_Y4ENPpPPMs0p1N9KdBG8rRaL","tool":"wait","status":"inProgress","senderThreadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","receiverThreadIds":["019dadec-92b5-78c0-b9f1-f729dd16b784"],"prompt":null,"model":null,"reasoningEffort":null,"agentsStates":{}},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","tokenUsage":{"total":{"totalTokens":116527,"inputTokens":115552,"cachedInputTokens":113152,"outputTokens":975,"reasoningOutputTokens":587},"last":{"totalTokens":29726,"inputTokens":29608,"cachedInputTokens":28544,"outputTokens":118,"reasoningOutputTokens":71},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"config"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" structure"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":":\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"extends"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"../../"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"config"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":".base"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"compiler"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Options"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":".plugins"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" uses"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"@"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"effect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"/l"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"anguage"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"-service"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"namespace"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Import"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Packages"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"[\"@"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"effect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"/platform"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"-node"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"\"]"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" diagnostics"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":":\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"import"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"From"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Bar"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"rel"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"error"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"any"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Unknown"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"In"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Error"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Context"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"warning"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"instance"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Of"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Schema"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"warning"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" -"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"det"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"ermin"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"istic"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Keys"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"warning"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"include"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"[\""}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"src"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"\","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" \""}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"scripts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"\","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" \""}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"test"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"\"]"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"exclude"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" not"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" specified"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"Not"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"able"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" choice"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" this"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" relies"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" mostly"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" on"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" shared"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" base"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" config"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" with"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"-specific"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" Effect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" language"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" service"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" diagnostics"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" layered"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" on"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":" top"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","itemId":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_073f47c9f548c99b0169e6e4888d58819a8aeb946008ad6167","text":"`tsconfig.json` structure:\n\n- `extends`: `../../tsconfig.base.json`\n- `compilerOptions.plugins`: uses `@effect/language-service`\n - `namespaceImportPackages`: `[\"@effect/platform-node\"]`\n - diagnostics:\n - `importFromBarrel`: `error`\n - `anyUnknownInErrorContext`: `warning`\n - `instanceOfSchema`: `warning`\n - `deterministicKeys`: `warning`\n- `include`: `[\"src\", \"scripts\", \"test\"]`\n- `exclude`: not specified\n\nNotable choice: this package relies mostly on the shared base config, with package-specific Effect language service diagnostics layered on top.","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turnId":"019dadec-92de-7873-a0c8-cfc82a9a0801","tokenUsage":{"total":{"totalTokens":80412,"inputTokens":79489,"cachedInputTokens":78976,"outputTokens":923,"reasoningOutputTokens":534},"last":{"totalTokens":25962,"inputTokens":25817,"cachedInputTokens":25472,"outputTokens":145,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748633},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335433},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadec-92b5-78c0-b9f1-f729dd16b784","turn":{"id":"019dadec-92de-7873-a0c8-cfc82a9a0801","items":[],"status":"completed","error":null,"startedAt":1776739455,"completedAt":1776739465,"durationMs":9914}}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"collabAgentToolCall","id":"call_Y4ENPpPPMs0p1N9KdBG8rRaL","tool":"wait","status":"completed","senderThreadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","receiverThreadIds":["019dadec-92b5-78c0-b9f1-f729dd16b784"],"prompt":null,"model":null,"reasoningEffort":null,"agentsStates":{"019dadec-92b5-78c0-b9f1-f729dd16b784":{"status":"completed","message":"`tsconfig.json` structure:\n\n- `extends`: `../../tsconfig.base.json`\n- `compilerOptions.plugins`: uses `@effect/language-service`\n - `namespaceImportPackages`: `[\"@effect/platform-node\"]`\n - diagnostics:\n - `importFromBarrel`: `error`\n - `anyUnknownInErrorContext`: `warning`\n - `instanceOfSchema`: `warning`\n - `deterministicKeys`: `warning`\n- `include`: `[\"src\", \"scripts\", \"test\"]`\n- `exclude`: not specified\n\nNotable choice: this package relies mostly on the shared base config, with package-specific Effect language service diagnostics layered on top."}}},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","tokenUsage":{"total":{"totalTokens":116527,"inputTokens":115552,"cachedInputTokens":113152,"outputTokens":975,"reasoningOutputTokens":587},"last":{"totalTokens":29726,"inputTokens":29608,"cachedInputTokens":28544,"outputTokens":118,"reasoningOutputTokens":71},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"Done"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" I"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" spawned"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" two"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" explorer"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" sub"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"agents"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":":\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" reported"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" metadata"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" exports"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" scripts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" dependencies"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":".\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"config"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" reported"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" base"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" config"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" inheritance"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" Effect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" language"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" service"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" plugin"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" diagnostics"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" includes"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" lack"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" of"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" explicit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":" excludes"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","itemId":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_04b4cd274d53a3dd0169e6e48ab3c0819aa622fac79e1dcc77","text":"Done. I spawned two explorer subagents:\n\n- `package.json`: reported package metadata, exports, scripts, and dependencies.\n- `tsconfig.json`: reported base config inheritance, Effect language service plugin diagnostics, includes, and lack of explicit excludes.","phase":"final_answer","memoryCitation":null},"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turnId":"019dadec-4bfa-73e1-868c-9251fd3f65c3","tokenUsage":{"total":{"totalTokens":146706,"inputTokens":145677,"cachedInputTokens":142720,"outputTokens":1029,"reasoningOutputTokens":587},"last":{"totalTokens":30179,"inputTokens":30125,"cachedInputTokens":29568,"outputTokens":54,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadec-4bed-7991-a1b7-1ed4fc1038b8","turn":{"id":"019dadec-4bfa-73e1-868c-9251fd3f65c3","items":[],"status":"completed","error":null,"startedAt":1776739437,"completedAt":1776739467,"durationMs":29849}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/cursor_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/cursor_output.ts new file mode 100644 index 00000000000..d152df55e8e --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/cursor_output.ts @@ -0,0 +1,96 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertExecutionNodeKinds, + assertNoExtraAppRunsForProviderChildren, + assertRunProviderTurnCardinality, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + projectionFor, + SUBAGENT_PROMPT, +} from "../shared.ts"; + +export function assertCursorSubagentOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ + result, + transcript, + runCount: 1, + runStatuses: ["completed"], + }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertExecutionNodeKinds(projection, ["root_turn", "subagent"]); + assertTurnItemTypes(projection, ["user_message", "subagent", "assistant_message"]); + assertRunProviderTurnCardinality({ projection, rootRunCount: 1 }); + assertNoExtraAppRunsForProviderChildren({ projection, expectedAppRuns: 1 }); + assertUserMessagesInclude(projection, [SUBAGENT_PROMPT]); + assertAssistantTextIncludes(projection, "cursor-read-only-fixture"); + assertAssistantTextIncludes(projection, "ES2022"); + + const lifecycleItems = projection.turnItems.filter( + (item) => + item.type === "reasoning" || item.type === "assistant_message" || item.type === "subagent", + ); + assert.deepEqual( + lifecycleItems.map((item) => item.type), + ["reasoning", "assistant_message", "subagent", "subagent", "assistant_message"], + "Cursor progress, subagents, and the final response must retain provider order", + ); + const assistantMessages = lifecycleItems.filter((item) => item.type === "assistant_message"); + assert.lengthOf(assistantMessages, 2); + assert.equal( + assistantMessages[0]?.text, + "Spawning two subagents in parallel to read `package.json` and `tsconfig.json`.\n", + ); + assert.include(assistantMessages[1]?.text ?? "", "Both subagents finished."); + + assert.lengthOf(projection.subagents, 2); + assert.lengthOf(result.shellSnapshot.threads, 3); + assert.deepEqual( + projection.subagents.map((subagent) => subagent.status), + ["completed", "completed"], + ); + + for (const subagent of projection.subagents) { + assert.equal(subagent.origin, "provider_native"); + assert.equal(subagent.createdBy, "agent"); + assert.equal(subagent.driver, "cursor"); + assert.isNull(subagent.providerThreadId); + assert.isNotNull(subagent.childThreadId); + assert.isNotNull(subagent.nativeTaskRef); + assert.isNotNull(subagent.completedAt); + assert.isNotNull(subagent.result); + if (subagent.childThreadId === null) { + throw new Error(`Subagent ${subagent.id} is missing its child thread`); + } + + const childProjection = result.projections.get(subagent.childThreadId); + assert.isDefined(childProjection); + assert.equal(childProjection.thread.lineage.parentThreadId, projection.thread.id); + assert.equal(childProjection.thread.lineage.relationshipToParent, "subagent"); + assert.isNull(childProjection.thread.activeProviderThreadId); + assert.lengthOf(childProjection.runs, 0); + assert.lengthOf(childProjection.providerThreads, 0); + assert.lengthOf(childProjection.providerTurns, 0); + assertExecutionNodeKinds(childProjection, ["root_turn", "tool_call"]); + assertTurnItemTypes(childProjection, ["user_message", "file_search", "assistant_message"]); + assertUserMessagesInclude(childProjection, [subagent.prompt]); + assert.isTrue( + childProjection.turnItems.some( + (item) => + item.type === "assistant_message" && + subagent.result !== null && + item.text.includes(subagent.result.slice(0, 40)), + ), + ); + } +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/cursor_transcript.ndjson new file mode 100644 index 00000000000..f1e2187642b --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/cursor_transcript.ndjson @@ -0,0 +1,474 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"subagent","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-1eee5e51-da10-4b21-af9a-ef9aca8c482d"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":true,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-1eee5e51-da10-4b21-af9a-ef9aca8c482d"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Spawn 2 subagents, one to read package.json and one to read tsconfig.json","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","agentId":"agent-1eee5e51-da10-4b21-af9a-ef9aca8c482d"}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"thinking-delta","text":"Spawning two subagents"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"thinking-delta","text":" to read package.json"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"thinking-delta","text":" and tsconfig.json."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-completed","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"thinking-completed","thinkingDurationMs":130}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Sp"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"awning"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" two"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" sub"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"agents"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" in"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" parallel"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" to"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" read"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"package"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":".json"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" and"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"ts"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"config"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":".json"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`.\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":72}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"partial-tool-call","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"partial-tool-call","callId":"tool_0c313ecc-9783-4a21-b0d5-74150331312","toolCall":{"type":"task","args":{"description":"Read package.json","prompt":"Read the file package.json in the workspace root at /tmp/cursor-replay-subagent/package.json and return its full contents along with a brief summary of what the project is (name, dependencies, scripts, etc.).","subagentType":{"kind":"unspecified"},"model":"composer-2.5-fast","agentId":"414d4889-6f0e-4866-9379-cd8a57673251","mode":"unspecified"}},"modelCallId":"393d6cf5-3363-488b-80aa-ca0498451dca-0-zcvb"}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"tool-call-started","callId":"tool_0c313ecc-9783-4a21-b0d5-74150331312","toolCall":{"type":"task","args":{"description":"Read package.json","prompt":"Read the file package.json in the workspace root at /tmp/cursor-replay-subagent/package.json and return its full contents along with a brief summary of what the project is (name, dependencies, scripts, etc.).","subagentType":{"kind":"unspecified"},"model":"composer-2.5-fast","agentId":"414d4889-6f0e-4866-9379-cd8a57673251","mode":"unspecified"}},"modelCallId":"393d6cf5-3363-488b-80aa-ca0498451dca-0-zcvb"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":78}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"partial-tool-call","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"partial-tool-call","callId":"tool_1d92aa7d-e115-4918-9cf6-d384c92098a","toolCall":{"type":"task","args":{"description":"Read tsconfig.json","prompt":"Read the file tsconfig.json in the workspace root at /tmp/cursor-replay-subagent/tsconfig.json and return its full contents along with a brief summary of the TypeScript configuration (compiler options, target, module settings, etc.).","subagentType":{"kind":"unspecified"},"model":"composer-2.5-fast","agentId":"4eab0559-c807-4f08-a101-c8d99d895c82","mode":"unspecified"}},"modelCallId":"393d6cf5-3363-488b-80aa-ca0498451dca-0-zcvb"}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"tool-call-started","callId":"tool_1d92aa7d-e115-4918-9cf6-d384c92098a","toolCall":{"type":"task","args":{"description":"Read tsconfig.json","prompt":"Read the file tsconfig.json in the workspace root at /tmp/cursor-replay-subagent/tsconfig.json and return its full contents along with a brief summary of the TypeScript configuration (compiler options, target, module settings, etc.).","subagentType":{"kind":"unspecified"},"model":"composer-2.5-fast","agentId":"4eab0559-c807-4f08-a101-c8d99d895c82","mode":"unspecified"}},"modelCallId":"393d6cf5-3363-488b-80aa-ca0498451dca-0-zcvb"}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"tool-call-completed","callId":"tool_0c313ecc-9783-4a21-b0d5-74150331312","toolCall":{"type":"task","args":{"description":"Read package.json","prompt":"Read the file package.json in the workspace root at /tmp/cursor-replay-subagent/package.json and return its full contents along with a brief summary of what the project is (name, dependencies, scripts, etc.).","subagentType":{"kind":"unspecified"},"model":"composer-2.5-fast","agentId":"414d4889-6f0e-4866-9379-cd8a57673251","mode":"unspecified"},"result":{"status":"success","value":{"conversationSteps":[{"toolCall":{"readToolCall":{"args":{"path":"/tmp/cursor-replay-subagent/package.json"},"result":{"success":{"content":"{\"name\":\"cursor-read-only-fixture\",\"private\":true,\"scripts\":{\"typecheck\":\"tsc --noEmit\"}}","totalLines":1,"fileSize":89,"path":"/tmp/cursor-replay-subagent/package.json","readRange":{"startLine":1,"endLine":1}}}},"toolCallId":"tool_45b696cd-7ad1-4fb6-bdd5-32452d02ea5"}},{"assistantMessage":{"text":"## Full contents\n\n```json\n{\n \"name\": \"cursor-read-only-fixture\",\n \"private\": true,\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\"\n }\n}\n```\n\n## Summary\n\n| Field | Value |\n|--------|--------|\n| **Name** | `cursor-read-only-fixture` |\n| **Private** | `true` (not intended for npm publish) |\n| **Scripts** | `typecheck` — runs TypeScript compiler in check-only mode (`tsc --noEmit`) |\n| **Dependencies** | None listed |\n| **Dev dependencies** | None listed |\n\nThis is a minimal TypeScript fixture project. It has no runtime or build dependencies in `package.json`; the only script is type-checking via `tsc`. The name suggests it’s a read-only test or demo setup for Cursor/orchestrator tooling rather than a full application."}}],"agentId":"414d4889-6f0e-4866-9379-cd8a57673251","isBackground":false,"durationMs":3570,"backgroundReason":"unspecified"}}},"modelCallId":"393d6cf5-3363-488b-80aa-ca0498451dca-0-zcvb"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":258}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"tool-call-completed","callId":"tool_1d92aa7d-e115-4918-9cf6-d384c92098a","toolCall":{"type":"task","args":{"description":"Read tsconfig.json","prompt":"Read the file tsconfig.json in the workspace root at /tmp/cursor-replay-subagent/tsconfig.json and return its full contents along with a brief summary of the TypeScript configuration (compiler options, target, module settings, etc.).","subagentType":{"kind":"unspecified"},"model":"composer-2.5-fast","agentId":"4eab0559-c807-4f08-a101-c8d99d895c82","mode":"unspecified"},"result":{"status":"success","value":{"conversationSteps":[{"assistantMessage":{"text":"Reading `tsconfig.json` and summarizing its TypeScript settings.\n"}},{"toolCall":{"readToolCall":{"args":{"path":"/tmp/cursor-replay-subagent/tsconfig.json"},"result":{"success":{"content":"{\"compilerOptions\":{\"module\":\"ESNext\",\"strict\":true,\"target\":\"ES2022\"}}","totalLines":1,"fileSize":71,"path":"/tmp/cursor-replay-subagent/tsconfig.json","readRange":{"startLine":1,"endLine":1}}}},"toolCallId":"tool_af98da3d-ddfc-4f07-a9a2-1c1ac8efcd6"}},{"assistantMessage":{"text":"## Full contents\n\n```json\n{\n \"compilerOptions\": {\n \"module\": \"ESNext\",\n \"strict\": true,\n \"target\": \"ES2022\"\n }\n}\n```\n\n## Summary\n\nThis is a minimal TypeScript config with three compiler options:\n\n| Option | Value | Meaning |\n|--------|--------|---------|\n| **target** | `ES2022` | Emit JavaScript for ES2022 (modern syntax and runtime features). |\n| **module** | `ESNext` | Use the latest ECMAScript module format for imports/exports. |\n| **strict** | `true` | Enable all strict type-checking options (e.g. `strictNullChecks`, `noImplicitAny`). |\n\n**Not specified (defaults apply):**\n- No `include` / `exclude` — TypeScript will include all `.ts` files in the project directory.\n- No `outDir` / `rootDir` — output location and root are inferred or use defaults.\n- No `moduleResolution`, `esModuleInterop`, `skipLibCheck`, or path aliases — standard defaults are used.\n\nOverall, it targets modern ES modules with strict checking and leaves most other settings at TypeScript defaults."}}],"agentId":"4eab0559-c807-4f08-a101-c8d99d895c82","isBackground":false,"durationMs":3614,"backgroundReason":"unspecified"}}},"modelCallId":"393d6cf5-3363-488b-80aa-ca0498451dca-0-zcvb"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":325}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Both"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" sub"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"agents"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" finished"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" Here"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"’s"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" what"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" they"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" reported"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":":\n\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"##"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" package"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":".json"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\n\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"```"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"json"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"{\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"name"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\":"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"cursor"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-read"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-only"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-fix"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"ture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\",\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"private"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\":"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" true"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":",\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"scripts"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\":"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" {\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"type"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"check"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\":"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"t"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"sc"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" --"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"no"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Emit"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\"\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" }\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"}\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"```\n\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" **"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Name"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":":**"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"cursor"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-read"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-only"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-fix"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"ture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" **"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Private"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":":**"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"true"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" ("}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"not"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" for"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" npm"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" publish"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":")\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" **"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Scripts"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":":**"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"type"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"check"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" runs"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"t"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"sc"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" --"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"no"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Emit"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" **"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Dependencies"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":":**"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" none"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" listed"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\n\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"This"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" looks"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" like"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" a"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" minimal"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" Type"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Script"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" fixture"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" for"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" Cursor"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"/or"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"che"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"strator"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" tooling"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":".\n\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"##"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" ts"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"config"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":".json"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\n\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"```"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"json"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"{\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"compiler"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Options"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\":"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" {\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"module"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\":"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"ES"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Next"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\",\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"strict"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\":"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" true"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":",\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"target"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\":"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" \""}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"ES"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"202"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"2"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\"\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" "}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" }\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"}\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"```\n\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" **"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"target"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":":**"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"ES"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"202"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"2"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" **"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"module"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":":**"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"ES"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Next"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" **"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"strict"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":":**"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"true"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" ("}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"full"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" strict"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" type"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-check"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"ing"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":")\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" No"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"include"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"/`"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"exclude"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`,"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" `"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"out"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Dir"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"`,"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" or"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" path"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" aliases"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" —"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" defaults"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" apply"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"\n\n"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Together"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":","}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" this"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" is"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" a"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" small"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" read"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"-only"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" Type"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"Script"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" project"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" with"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" strict"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" checking"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" and"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" a"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" single"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" type"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"check"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":" script"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"text-delta","text":"."}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"step-completed","stepId":5,"stepDurationMs":6831}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","update":{"type":"turn-ended","usage":{"inputTokens":21878,"outputTokens":661,"cacheReadTokens":17788,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-4f9fd970-8f15-44ae-8973-09aa4d42daaf","requestId":"393d6cf5-3363-488b-80aa-ca0498451dca","status":"finished","result":"Both subagents finished. Here’s what they reported:\n\n## package.json\n\n```json\n{\n \"name\": \"cursor-read-only-fixture\",\n \"private\": true,\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\"\n }\n}\n```\n\n- **Name:** `cursor-read-only-fixture`\n- **Private:** `true` (not for npm publish)\n- **Scripts:** `typecheck` runs `tsc --noEmit`\n- **Dependencies:** none listed\n\nThis looks like a minimal TypeScript fixture for Cursor/orchestrator tooling.\n\n## tsconfig.json\n\n```json\n{\n \"compilerOptions\": {\n \"module\": \"ESNext\",\n \"strict\": true,\n \"target\": \"ES2022\"\n }\n}\n```\n\n- **target:** `ES2022`\n- **module:** `ESNext`\n- **strict:** `true` (full strict type-checking)\n- No `include`/`exclude`, `outDir`, or path aliases — defaults apply\n\nTogether, this is a small read-only TypeScript project with strict checking and a single typecheck script.","model":{"id":"composer-2.5"},"durationMs":9585}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-1eee5e51-da10-4b21-af9a-ef9aca8c482d"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/input.ts new file mode 100644 index 00000000000..d0f885c57dc --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/input.ts @@ -0,0 +1,7 @@ +import { SUBAGENT_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function subagentInput(): OrchestratorFixtureInput { + return { + steps: [{ type: "message", text: SUBAGENT_PROMPT }], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/README.md b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/README.md new file mode 100644 index 00000000000..ed6c33a8a5d --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/README.md @@ -0,0 +1,28 @@ +# Continued Native Subagent + +This fixture records Codex continuing a native subagent from a later parent turn. + +```text +Parent app thread / Codex thread A + Turn 1: spawn one subagent + | + +-- spawnAgent + | + v + Child app thread / Codex thread B + Turn 1: initial prompt + Assistant: initial subagent response + + Turn 2: @hooke continue the same subagent + | + +-- resumeAgent + sendInput + | + v + Child app thread / Codex thread B + Turn 2: continuation prompt + Assistant: continued subagent response +``` + +The original parent `subagent` item represents only the spawn lifecycle. It must +remain completed with the first result while the child thread accumulates later +conversation turns. diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/codex_output.ts new file mode 100644 index 00000000000..b631cbf565f --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/codex_output.ts @@ -0,0 +1,59 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertNoExtraAppRunsForProviderChildren, + assertSemanticProjectionIntegrity, + assertUserMessagesInclude, + projectionFor, + SUBAGENT_CONTINUE_CHILD_PROMPT, + SUBAGENT_CONTINUE_PARENT_PROMPT, + SUBAGENT_CONTINUE_PROMPT, +} from "../shared.ts"; + +export function assertSubagentContinueOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ + result, + transcript, + runCount: 2, + runStatuses: ["completed", "completed"], + }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertNoExtraAppRunsForProviderChildren({ projection, expectedAppRuns: 2 }); + assertUserMessagesInclude(projection, [ + SUBAGENT_CONTINUE_PROMPT, + SUBAGENT_CONTINUE_PARENT_PROMPT, + ]); + assert.lengthOf(projection.subagents, 1); + + const subagent = projection.subagents[0]!; + assert.equal(subagent.status, "completed"); + assert.equal(subagent.result, "initial subagent response"); + assert.isNotNull(subagent.childThreadId); + if (subagent.childThreadId === null) { + throw new Error("Continued subagent is missing its child thread"); + } + + const childProjection = result.projections.get(subagent.childThreadId); + assert.isDefined(childProjection); + assert.lengthOf(childProjection.runs, 0); + assert.lengthOf(childProjection.providerTurns, 2); + assertUserMessagesInclude(childProjection, [subagent.prompt, SUBAGENT_CONTINUE_CHILD_PROMPT]); + assert.isTrue( + childProjection.turnItems.some( + (item) => item.type === "assistant_message" && item.text === "initial subagent response", + ), + ); + assert.isTrue( + childProjection.turnItems.some( + (item) => item.type === "assistant_message" && item.text === "continued subagent response", + ), + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/codex_transcript.ndjson new file mode 100644 index 00000000000..f6ba3c2d4cd --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/codex_transcript.ndjson @@ -0,0 +1,33 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.137.0","scenario":"subagent_continue","metadata":{"source":"record-codex-app-server-replay-fixture","fileName":"subagent_continue.ndjson","description":"A root turn spawns one native Codex subagent, then a later root turn resumes and messages the same child thread."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.137.0","codexHome":"/tmp/codex-home","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"native-root-thread","sessionId":"native-root-thread","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1781051850,"updatedAt":1781051850,"status":{"type":"idle"},"path":"/tmp/root-thread.jsonl","cwd":"/tmp","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":null,"cwd":"/tmp","instructionSources":[],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false},"reasoningEffort":"high"}}} +{"type":"expect_outbound","label":"turn/start/spawn","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Spawn one subagent and have it reply exactly: initial subagent response","type":"text"}],"threadId":"native-root-thread"}}} +{"type":"emit_inbound","label":"turn/start/spawn","frame":{"id":3,"result":{"turn":{"id":"native-root-turn-1","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"turn/started/root-1","frame":{"method":"turn/started","params":{"threadId":"native-root-thread","turn":{"id":"native-root-turn-1","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1781051851,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/completed/root-user-1","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"root-user-1","content":[{"type":"text","text":"Spawn one subagent and have it reply exactly: initial subagent response","text_elements":[]}]},"threadId":"native-root-thread","turnId":"native-root-turn-1","completedAtMs":1781051851000}}} +{"type":"emit_inbound","label":"item/started/spawnAgent","frame":{"method":"item/started","params":{"item":{"type":"collabAgentToolCall","id":"spawn-call-1","tool":"spawnAgent","status":"inProgress","senderThreadId":"native-root-thread","receiverThreadIds":[],"prompt":"Reply exactly: initial subagent response","model":"","reasoningEffort":"medium","agentsStates":{}},"threadId":"native-root-thread","turnId":"native-root-turn-1","startedAtMs":1781051852000}}} +{"type":"emit_inbound","label":"item/completed/spawnAgent","frame":{"method":"item/completed","params":{"item":{"type":"collabAgentToolCall","id":"spawn-call-1","tool":"spawnAgent","status":"completed","senderThreadId":"native-root-thread","receiverThreadIds":["native-child-thread"],"prompt":"Reply exactly: initial subagent response","model":"kindle-alpha","reasoningEffort":"high","agentsStates":{"native-child-thread":{"status":"pendingInit","message":null}}},"threadId":"native-root-thread","turnId":"native-root-turn-1","completedAtMs":1781051852100}}} +{"type":"emit_inbound","label":"turn/started/child-1","frame":{"method":"turn/started","params":{"threadId":"native-child-thread","turn":{"id":"native-child-turn-1","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1781051852,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/completed/child-user-1","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"child-user-1","content":[{"type":"text","text":"Reply exactly: initial subagent response","text_elements":[]}]},"threadId":"native-child-thread","turnId":"native-child-turn-1","completedAtMs":1781051852200}}} +{"type":"emit_inbound","label":"item/completed/child-answer-1","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"child-answer-1","text":"initial subagent response","phase":"final_answer","memoryCitation":null},"threadId":"native-child-thread","turnId":"native-child-turn-1","completedAtMs":1781051853000}}} +{"type":"emit_inbound","label":"turn/completed/child-1","frame":{"method":"turn/completed","params":{"threadId":"native-child-thread","turn":{"id":"native-child-turn-1","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1781051852,"completedAt":1781051853,"durationMs":1000}}}} +{"type":"emit_inbound","label":"item/completed/wait-1","frame":{"method":"item/completed","params":{"item":{"type":"collabAgentToolCall","id":"wait-call-1","tool":"wait","status":"completed","senderThreadId":"native-root-thread","receiverThreadIds":["native-child-thread"],"prompt":null,"model":null,"reasoningEffort":null,"agentsStates":{"native-child-thread":{"status":"completed","message":"initial subagent response"}}},"threadId":"native-root-thread","turnId":"native-root-turn-1","completedAtMs":1781051853100}}} +{"type":"emit_inbound","label":"item/completed/root-answer-1","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"root-answer-1","text":"Hooke says: initial subagent response","phase":"final_answer","memoryCitation":null},"threadId":"native-root-thread","turnId":"native-root-turn-1","completedAtMs":1781051854000}}} +{"type":"emit_inbound","label":"turn/completed/root-1","frame":{"method":"turn/completed","params":{"threadId":"native-root-thread","turn":{"id":"native-root-turn-1","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1781051851,"completedAt":1781051854,"durationMs":3000}}}} +{"type":"expect_outbound","label":"turn/start/continue","frame":{"id":4,"method":"turn/start","params":{"input":[{"text":"@hooke have the same subagent reply exactly: continued subagent response","type":"text"}],"threadId":"native-root-thread"}}} +{"type":"emit_inbound","label":"turn/start/continue","frame":{"id":4,"result":{"turn":{"id":"native-root-turn-2","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"turn/started/root-2","frame":{"method":"turn/started","params":{"threadId":"native-root-thread","turn":{"id":"native-root-turn-2","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1781051860,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/completed/root-user-2","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"root-user-2","content":[{"type":"text","text":"@hooke have the same subagent reply exactly: continued subagent response","text_elements":[]}]},"threadId":"native-root-thread","turnId":"native-root-turn-2","completedAtMs":1781051860000}}} +{"type":"emit_inbound","label":"item/completed/resumeAgent","frame":{"method":"item/completed","params":{"item":{"type":"collabAgentToolCall","id":"resume-call-1","tool":"resumeAgent","status":"completed","senderThreadId":"native-root-thread","receiverThreadIds":["native-child-thread"],"prompt":null,"model":null,"reasoningEffort":null,"agentsStates":{"native-child-thread":{"status":"completed","message":"initial subagent response"}}},"threadId":"native-root-thread","turnId":"native-root-turn-2","completedAtMs":1781051860100}}} +{"type":"emit_inbound","label":"item/completed/sendInput","frame":{"method":"item/completed","params":{"item":{"type":"collabAgentToolCall","id":"send-input-call-1","tool":"sendInput","status":"completed","senderThreadId":"native-root-thread","receiverThreadIds":["native-child-thread"],"prompt":"Reply exactly: continued subagent response","model":null,"reasoningEffort":null,"agentsStates":{"native-child-thread":{"status":"completed","message":"initial subagent response"}}},"threadId":"native-root-thread","turnId":"native-root-turn-2","completedAtMs":1781051860200}}} +{"type":"emit_inbound","label":"turn/started/child-2","frame":{"method":"turn/started","params":{"threadId":"native-child-thread","turn":{"id":"native-child-turn-2","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1781051861,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/completed/child-user-2","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"child-user-2","content":[{"type":"text","text":"Reply exactly: continued subagent response","text_elements":[]}]},"threadId":"native-child-thread","turnId":"native-child-turn-2","completedAtMs":1781051861100}}} +{"type":"emit_inbound","label":"item/completed/child-answer-2","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"child-answer-2","text":"continued subagent response","phase":"final_answer","memoryCitation":null},"threadId":"native-child-thread","turnId":"native-child-turn-2","completedAtMs":1781051862000}}} +{"type":"emit_inbound","label":"turn/completed/child-2","frame":{"method":"turn/completed","params":{"threadId":"native-child-thread","turn":{"id":"native-child-turn-2","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1781051861,"completedAt":1781051862,"durationMs":1000}}}} +{"type":"emit_inbound","label":"item/completed/wait-2","frame":{"method":"item/completed","params":{"item":{"type":"collabAgentToolCall","id":"wait-call-2","tool":"wait","status":"completed","senderThreadId":"native-root-thread","receiverThreadIds":["native-child-thread"],"prompt":null,"model":null,"reasoningEffort":null,"agentsStates":{"native-child-thread":{"status":"completed","message":"continued subagent response"}}},"threadId":"native-root-thread","turnId":"native-root-turn-2","completedAtMs":1781051862100}}} +{"type":"emit_inbound","label":"item/completed/root-answer-2","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"root-answer-2","text":"Hooke says: continued subagent response","phase":"final_answer","memoryCitation":null},"threadId":"native-root-thread","turnId":"native-root-turn-2","completedAtMs":1781051863000}}} +{"type":"emit_inbound","label":"turn/completed/root-2","frame":{"method":"turn/completed","params":{"threadId":"native-root-thread","turn":{"id":"native-root-turn-2","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1781051860,"completedAt":1781051863,"durationMs":3000}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/input.ts new file mode 100644 index 00000000000..136ffa1dab9 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_continue/input.ts @@ -0,0 +1,14 @@ +import { + SUBAGENT_CONTINUE_PARENT_PROMPT, + SUBAGENT_CONTINUE_PROMPT, + type OrchestratorFixtureInput, +} from "../shared.ts"; + +export function subagentContinueInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: SUBAGENT_CONTINUE_PROMPT }, + { type: "message", text: SUBAGENT_CONTINUE_PARENT_PROMPT }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native/claude_transcript.ndjson new file mode 100644 index 00000000000..f1cb48cd8b0 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native/claude_transcript.ndjson @@ -0,0 +1,21 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"thread_fork_native","metadata":{"prompts":["Respond with the following text: source fork seed ok","Respond with the following text: fork native ok"],"model":"claude-sonnet-4-6","nativeSessionId":"8d0dafab-45d3-45eb-abef-41b9e55d5ff9","queryMode":"fork_session","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript","sourceAssistantMessageUuids":["68baa075-b7d4-4689-866b-96bd51e570c3"],"forkUpToMessageId":"68baa075-b7d4-4689-866b-96bd51e570c3","forkedNativeSessionId":"7e7ad6ec-db4a-4510-85ff-59df35cf7fad"}} +{"type":"expect_outbound","label":"query.open:source","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"8d0dafab-45d3-45eb-abef-41b9e55d5ff9"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with the following text: source fork seed ok"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"72dd9de7-b5a9-46bf-9c57-9bca11344e73","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"4edf7621-d696-4b60-9821-187bcac04c7d","session_id":"8d0dafab-45d3-45eb-abef-41b9e55d5ff9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"72dd9de7-b5a9-46bf-9c57-9bca11344e73","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"6959d784-05f0-48b4-97de-d3f4a11b4b1a","session_id":"8d0dafab-45d3-45eb-abef-41b9e55d5ff9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-thread_fork_native","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"fd90233b-ff4e-429c-a3b2-088d9b3bafc8","session_id":"8d0dafab-45d3-45eb-abef-41b9e55d5ff9"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01RzC6FfR9RGxxVcrpBK6duZ","type":"message","role":"assistant","content":[{"type":"text","text":"source fork seed ok"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2339,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2339},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"8d0dafab-45d3-45eb-abef-41b9e55d5ff9","uuid":"68baa075-b7d4-4689-866b-96bd51e570c3"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"84451d3f-e7ba-4d80-aea3-4fbc442f4b3b","session_id":"8d0dafab-45d3-45eb-abef-41b9e55d5ff9"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2066,"duration_api_ms":1815,"num_turns":1,"result":"source fork seed ok","stop_reason":"end_turn","session_id":"8d0dafab-45d3-45eb-abef-41b9e55d5ff9","total_cost_usd":0.01172175,"usage":{"input_tokens":3,"cache_creation_input_tokens":2339,"cache_read_input_tokens":9455,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2339,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":9455,"cache_creation_input_tokens":2339,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2339},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":7,"cacheReadInputTokens":9455,"cacheCreationInputTokens":2339,"webSearchRequests":0,"costUSD":0.01172175,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"892cdc1f-e2d5-440d-ae57-ca27d32f63f1"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"session.fork","frame":{"type":"session.fork","sessionId":"8d0dafab-45d3-45eb-abef-41b9e55d5ff9","options":{"dir":"/tmp/claude-replay-thread_fork_native","upToMessageId":"68baa075-b7d4-4689-866b-96bd51e570c3"}}} +{"type":"emit_inbound","label":"session.forked","frame":{"type":"session.forked","sessionId":"7e7ad6ec-db4a-4510-85ff-59df35cf7fad"}} +{"type":"expect_outbound","label":"query.open:fork","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"7e7ad6ec-db4a-4510-85ff-59df35cf7fad"}}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with the following text: fork native ok"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"53ad62fa-7988-4d2e-9efd-e923756eb7e5","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"c2160988-c159-4ace-abd3-1eb6bbaa76ad","session_id":"d2c41d57-6ad4-4c9e-9444-f93bc492aa53"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"53ad62fa-7988-4d2e-9efd-e923756eb7e5","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"fa30d315-9265-40ec-afbe-c76704c6d66c","session_id":"d2c41d57-6ad4-4c9e-9444-f93bc492aa53"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-thread_fork_native","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"41edb892-5f02-4bdd-bef3-5945d3414fc8","session_id":"7e7ad6ec-db4a-4510-85ff-59df35cf7fad"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01MvGdK8D9o71fU1raKAbM69","type":"message","role":"assistant","content":[{"type":"text","text":"fork native ok"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":20,"cache_read_input_tokens":11794,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":20},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"7e7ad6ec-db4a-4510-85ff-59df35cf7fad","uuid":"b800a4dd-77a5-4e68-acac-1c651a50a02e"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"91026341-c039-4110-928f-4ab67cd13375","session_id":"7e7ad6ec-db4a-4510-85ff-59df35cf7fad"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":4452,"duration_api_ms":3680,"num_turns":1,"result":"fork native ok","stop_reason":"end_turn","session_id":"7e7ad6ec-db4a-4510-85ff-59df35cf7fad","total_cost_usd":0.0037122,"usage":{"input_tokens":3,"cache_creation_input_tokens":20,"cache_read_input_tokens":11794,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":20,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":11794,"cache_creation_input_tokens":20,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":20},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":11794,"cacheCreationInputTokens":20,"webSearchRequests":0,"costUSD":0.0037122,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"cc267266-37bf-4cac-a602-ecfabd1e8170"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native/codex_transcript.ndjson new file mode 100644 index 00000000000..7076046378f --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native/codex_transcript.ndjson @@ -0,0 +1,21 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"thread_fork_native","metadata":{"source":"hand-authored-replay","description":"One completed source turn, a native thread/fork, and the first target fork turn."}} +{"type":"expect_outbound","label":"initialize/source","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize/source","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0","codexHome":"/tmp/codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized/source","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start/source","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start/source","frame":{"id":2,"result":{"thread":{"id":"native-source-thread","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739349,"updatedAt":1776739349,"status":{"type":"idle"},"path":"/tmp/source.jsonl","cwd":"/tmp/project","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/tmp/project","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start/source","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Respond with the following text: source fork seed ok","type":"text"}],"threadId":"native-source-thread"}}} +{"type":"emit_inbound","label":"turn/start/source","frame":{"id":3,"result":{"turn":{"id":"native-source-turn","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"turn/started/source","frame":{"method":"turn/started","params":{"threadId":"native-source-thread","turn":{"id":"native-source-turn","items":[],"status":"inProgress","error":null,"startedAt":1776739349,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/completed/source-user","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"native-source-user-item","content":[{"type":"text","text":"Respond with the following text: source fork seed ok","text_elements":[]}]},"threadId":"native-source-thread","turnId":"native-source-turn"}}} +{"type":"emit_inbound","label":"item/completed/source-agent","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"native-source-agent-item","text":"source fork seed ok","phase":"final_answer","memoryCitation":null},"threadId":"native-source-thread","turnId":"native-source-turn"}}} +{"type":"emit_inbound","label":"turn/completed/source","frame":{"method":"turn/completed","params":{"threadId":"native-source-thread","turn":{"id":"native-source-turn","items":[],"status":"completed","error":null,"startedAt":1776739349,"completedAt":1776739356,"durationMs":6434}}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":4,"method":"thread/fork","params":{"threadId":"native-source-thread"}}} +{"type":"emit_inbound","label":"thread/fork","frame":{"id":4,"result":{"thread":{"id":"native-fork-thread","forkedFromId":"native-source-thread","preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739360,"updatedAt":1776739360,"status":{"type":"idle"},"path":"/tmp/fork.jsonl","cwd":"/tmp/project","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/tmp/project","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start/fork","frame":{"id":5,"method":"turn/start","params":{"input":[{"text":"Respond with the following text: fork native ok","type":"text"}],"threadId":"native-fork-thread"}}} +{"type":"emit_inbound","label":"turn/start/fork","frame":{"id":5,"result":{"turn":{"id":"native-fork-turn","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"turn/started/fork","frame":{"method":"turn/started","params":{"threadId":"native-fork-thread","turn":{"id":"native-fork-turn","items":[],"status":"inProgress","error":null,"startedAt":1776739361,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/completed/fork-user","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"native-fork-user-item","content":[{"type":"text","text":"Respond with the following text: fork native ok","text_elements":[]}]},"threadId":"native-fork-thread","turnId":"native-fork-turn"}}} +{"type":"emit_inbound","label":"item/completed/fork-agent","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"native-fork-agent-item","text":"fork native ok","phase":"final_answer","memoryCitation":null},"threadId":"native-fork-thread","turnId":"native-fork-turn"}}} +{"type":"emit_inbound","label":"turn/completed/fork","frame":{"method":"turn/completed","params":{"threadId":"native-fork-thread","turn":{"id":"native-fork-turn","items":[],"status":"completed","error":null,"startedAt":1776739361,"completedAt":1776739363,"durationMs":2000}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/README.md b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/README.md new file mode 100644 index 00000000000..0bb565f1e8b --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/README.md @@ -0,0 +1,49 @@ +# Native Fork Continuation Fixture + +This fixture records a provider-native fork followed by two turns on the fork. +It verifies that: + +1. The fork inherits conversation context from its source. +2. Context introduced on the fork remains available on later fork turns. +3. The final recall prompt does not contain either marker, so the response must + come from provider conversation state. + +Both `codex_transcript.ndjson` and `claude_transcript.ndjson` record the same +logical scenario using each provider's native thread or session mechanism. + +## Conversation Graph + +```text +Source provider thread +| ++-- Turn 1 +| User: remember source-marker-7Q9V +| Assistant: source marker stored +| +`-- native fork after Turn 1 + | + `-- Forked provider thread + | + +-- Turn 2 + | User: remember fork-marker-2K4M + | Assistant: fork marker stored + | + `-- Turn 3 + User: recall both markers without restating them + Assistant: source-marker-7Q9V|fork-marker-2K4M +``` + +## Assertions + +- The fork has a native provider thread or session ID distinct from its source. +- Turn 3's user prompt contains neither opaque marker. +- Turn 3 returns the source marker first and the fork-local marker second. + +The providers may add whitespace around `|`; replay assertions normalize that +formatting before comparing the semantic result. + +## Scope + +This is a provider-capability fixture. It proves native fork inheritance and +continued fork context. It does not record or assert app-level merge-back +behavior. diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/claude_transcript.ndjson new file mode 100644 index 00000000000..cd638783fe4 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/claude_transcript.ndjson @@ -0,0 +1,25 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"thread_fork_native_continue","metadata":{"prompts":["Remember the opaque marker source-marker-7Q9V for later in this conversation. Respond with exactly: source marker stored","Remember the second opaque marker fork-marker-2K4M for later in this conversation. Respond with exactly: fork marker stored","Return the two opaque markers previously provided in chronological order, separated by a single | character. Respond with only the markers and separator."],"model":"claude-sonnet-4-6","nativeSessionId":"ec97110d-6e25-4737-9071-38680f41f305","queryMode":"fork_session_continue","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript","sourceAssistantMessageUuids":["a82bbfaa-01e1-42c0-8cfd-8e513b0814ee"],"forkUpToMessageId":"a82bbfaa-01e1-42c0-8cfd-8e513b0814ee","forkedNativeSessionId":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac"}} +{"type":"expect_outbound","label":"query.open:source","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"ec97110d-6e25-4737-9071-38680f41f305"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the opaque marker source-marker-7Q9V for later in this conversation. Respond with exactly: source marker stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"e69e5e70-7070-47a3-8ed6-684c6508e5e0","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"b84c01b3-4af9-419c-9b7c-8fb430603a24","session_id":"ec97110d-6e25-4737-9071-38680f41f305"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"e69e5e70-7070-47a3-8ed6-684c6508e5e0","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"d076f999-5546-47f9-9b99-bb06dbf7fadb","session_id":"ec97110d-6e25-4737-9071-38680f41f305"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_fork_native_continue","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"a5a55a44-292a-41de-8030-36e3af337e0a","session_id":"ec97110d-6e25-4737-9071-38680f41f305"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_015wswEVVDWXMk7ALAtkQ8X6","type":"message","role":"assistant","content":[{"type":"text","text":"source marker stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2783,"cache_read_input_tokens":16194,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2783},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"ec97110d-6e25-4737-9071-38680f41f305","uuid":"a82bbfaa-01e1-42c0-8cfd-8e513b0814ee","request_id":"req_011Cbrdy9BoEWCy9YaxrFqcz"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"3785a246-1146-4db3-9153-7f6c91e62554","session_id":"ec97110d-6e25-4737-9071-38680f41f305"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2978,"duration_api_ms":1928,"ttft_ms":1910,"num_turns":1,"result":"source marker stored","stop_reason":"end_turn","session_id":"ec97110d-6e25-4737-9071-38680f41f305","total_cost_usd":0.01539345,"usage":{"input_tokens":3,"cache_creation_input_tokens":2783,"cache_read_input_tokens":16194,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2783,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":16194,"cache_creation_input_tokens":2783,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2783},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":16194,"cacheCreationInputTokens":2783,"webSearchRequests":0,"costUSD":0.01539345,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"1aefbda2-db0f-40bc-ba5e-dac873fa0f4d"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"session.fork","frame":{"type":"session.fork","sessionId":"ec97110d-6e25-4737-9071-38680f41f305","options":{"dir":"/tmp/claude-replay-thread_fork_native_continue","upToMessageId":"a82bbfaa-01e1-42c0-8cfd-8e513b0814ee"}}} +{"type":"emit_inbound","label":"session.forked","frame":{"type":"session.forked","sessionId":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac"}} +{"type":"expect_outbound","label":"query.open:fork","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac"}}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the second opaque marker fork-marker-2K4M for later in this conversation. Respond with exactly: fork marker stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"e8cc877f-a07a-410c-80e7-6d2c8309fe8c","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"0ab192fd-6114-41b7-baa2-683a93bb3d48","session_id":"d7fc5a69-f503-492f-ad0e-791e464d0f75"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"e8cc877f-a07a-410c-80e7-6d2c8309fe8c","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"1ccbe90e-1eb7-45d9-ace2-7fc5f05ed933","session_id":"d7fc5a69-f503-492f-ad0e-791e464d0f75"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_fork_native_continue","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"81aea19a-ca67-4743-889d-8ee8c2ececdf","session_id":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01M35CGHxMAeVJ2bwPMcKsjj","type":"message","role":"assistant","content":[{"type":"text","text":"fork marker stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":136,"cache_read_input_tokens":18977,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":136},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac","uuid":"33970774-b18e-4bf9-bb78-b08d2e1085eb","request_id":"req_011CbrdyUNDeFvnau5cyxM8q"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"8fd2f971-b39f-465b-94e8-e33e2c698ace","session_id":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2711,"duration_api_ms":1768,"ttft_ms":2232,"num_turns":1,"result":"fork marker stored","stop_reason":"end_turn","session_id":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac","total_cost_usd":0.006302100000000001,"usage":{"input_tokens":3,"cache_creation_input_tokens":136,"cache_read_input_tokens":18977,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":136,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":18977,"cache_creation_input_tokens":136,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":136},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":18977,"cacheCreationInputTokens":136,"webSearchRequests":0,"costUSD":0.006302100000000001,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"f0b7f4b3-446d-465a-9518-98a4bd85190e"}} +{"type":"expect_outbound","label":"prompt.offer:3","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Return the two opaque markers previously provided in chronological order, separated by a single | character. Respond with only the markers and separator."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_fork_native_continue","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"c66e8e65-fc24-42c8-a6c4-96085ae1b682","session_id":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01LvikUbyD2Jid97f8q8MUnP","type":"message","role":"assistant","content":[{"type":"text","text":"source-marker-7Q9V | fork-marker-2K4M"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":38,"cache_read_input_tokens":19113,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":38},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac","uuid":"4d63fe38-f2b9-4112-8c35-347bee5ffe81","request_id":"req_011CbrdyfTDMCoDxxqAckUGN"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2717,"duration_api_ms":3802,"ttft_ms":2303,"num_turns":1,"result":"source-marker-7Q9V | fork-marker-2K4M","stop_reason":"end_turn","session_id":"07ab8628-bbc1-41c3-9e14-0ed8af3651ac","total_cost_usd":0.012487500000000002,"usage":{"input_tokens":3,"cache_creation_input_tokens":38,"cache_read_input_tokens":19113,"output_tokens":20,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":38,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":20,"cache_read_input_tokens":19113,"cache_creation_input_tokens":38,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":38},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":6,"outputTokens":26,"cacheReadInputTokens":38090,"cacheCreationInputTokens":174,"webSearchRequests":0,"costUSD":0.012487500000000002,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"76b4538f-0ec8-49b8-bd04-ac000bc6dfcd"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/codex_transcript.ndjson new file mode 100644 index 00000000000..864898bd8c3 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/codex_transcript.ndjson @@ -0,0 +1,96 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.137.0","scenario":"thread_fork_native_continue","metadata":{"source":"record-codex-app-server-replay-fixture","fileName":"thread_fork_native_continue.ndjson","description":"One source marker turn, a native thread fork, a fork-local marker turn, and a recall turn that requires both contexts."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.137.0 (Mac OS 26.5.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"remoteControl/status/changed","frame":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"Juliuss-MacBook-Pro.local","installationId":"c067a38f-79bf-4874-ba7b-c00e86012954","environmentId":null}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019ea981-51d9-7ba2-a200-6937187e3928","sessionId":"019ea981-51d9-7ba2-a200-6937187e3928","forkedFromId":null,"parentThreadId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1780960285,"updatedAt":1780960285,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-11-25-019ea981-51d9-7ba2-a200-6937187e3928.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Remember the opaque marker source-marker-7Q9V for later in this conversation. Respond with exactly: source marker stored","type":"text"}],"threadId":"019ea981-51d9-7ba2-a200-6937187e3928"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea981-51d9-7ba2-a200-6937187e3928","sessionId":"019ea981-51d9-7ba2-a200-6937187e3928","forkedFromId":null,"parentThreadId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1780960285,"updatedAt":1780960285,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-11-25-019ea981-51d9-7ba2-a200-6937187e3928.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019ea981-539b-7650-a091-e03174d8e009","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turn":{"id":"019ea981-539b-7650-a091-e03174d8e009","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780960285,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"024dc3fe-b529-40cb-b948-ae403129811d","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker source-marker-7Q9V for later in this conversation. Respond with exactly: source marker stored","text_elements":[]}]},"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turnId":"019ea981-539b-7650-a091-e03174d8e009","startedAtMs":1780960288755}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"024dc3fe-b529-40cb-b948-ae403129811d","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker source-marker-7Q9V for later in this conversation. Respond with exactly: source marker stored","text_elements":[]}]},"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turnId":"019ea981-539b-7650-a091-e03174d8e009","completedAtMs":1780960288755}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_033096791f6ecb8a016a274c21e078819b8210e40617e54a06","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turnId":"019ea981-539b-7650-a091-e03174d8e009","startedAtMs":1780960289901}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turnId":"019ea981-539b-7650-a091-e03174d8e009","itemId":"msg_033096791f6ecb8a016a274c21e078819b8210e40617e54a06","delta":"source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turnId":"019ea981-539b-7650-a091-e03174d8e009","itemId":"msg_033096791f6ecb8a016a274c21e078819b8210e40617e54a06","delta":" marker"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turnId":"019ea981-539b-7650-a091-e03174d8e009","itemId":"msg_033096791f6ecb8a016a274c21e078819b8210e40617e54a06","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_033096791f6ecb8a016a274c21e078819b8210e40617e54a06","text":"source marker stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turnId":"019ea981-539b-7650-a091-e03174d8e009","completedAtMs":1780960289969}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turnId":"019ea981-539b-7650-a091-e03174d8e009","tokenUsage":{"total":{"totalTokens":18446,"inputTokens":18439,"cachedInputTokens":9088,"outputTokens":7,"reasoningOutputTokens":0},"last":{"totalTokens":18446,"inputTokens":18439,"cachedInputTokens":9088,"outputTokens":7,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea981-51d9-7ba2-a200-6937187e3928","turn":{"id":"019ea981-539b-7650-a091-e03174d8e009","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780960285,"completedAt":1780960289,"durationMs":4325}}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":4,"method":"thread/fork","params":{"threadId":"019ea981-51d9-7ba2-a200-6937187e3928"}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/fork","frame":{"id":4,"result":{"thread":{"id":"019ea981-64c4-76b1-9f90-866490b7dd60","sessionId":"019ea981-64c4-76b1-9f90-866490b7dd60","forkedFromId":"019ea981-51d9-7ba2-a200-6937187e3928","parentThreadId":null,"preview":"Remember the opaque marker source-marker-7Q9V for later in this conversation. Respond with exactly: source marker stored","ephemeral":false,"modelProvider":"openai","createdAt":1780960289,"updatedAt":1780960290,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-11-30-019ea981-64c4-76b1-9f90-866490b7dd60.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019ea981-539b-7650-a091-e03174d8e009","items":[{"type":"userMessage","id":"item-1","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker source-marker-7Q9V for later in this conversation. Respond with exactly: source marker stored","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"source marker stored","phase":"final_answer","memoryCitation":null}],"itemsView":"full","status":"completed","error":null,"startedAt":1780960285,"completedAt":1780960289,"durationMs":4325}]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":5,"method":"turn/start","params":{"input":[{"text":"Remember the second opaque marker fork-marker-2K4M for later in this conversation. Respond with exactly: fork marker stored","type":"text"}],"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-539b-7650-a091-e03174d8e009","tokenUsage":{"total":{"totalTokens":18446,"inputTokens":18439,"cachedInputTokens":9088,"outputTokens":7,"reasoningOutputTokens":0},"last":{"totalTokens":18446,"inputTokens":18439,"cachedInputTokens":9088,"outputTokens":7,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea981-64c4-76b1-9f90-866490b7dd60","sessionId":"019ea981-64c4-76b1-9f90-866490b7dd60","forkedFromId":"019ea981-51d9-7ba2-a200-6937187e3928","parentThreadId":null,"preview":"Remember the opaque marker source-marker-7Q9V for later in this conversation. Respond with exactly: source marker stored","ephemeral":false,"modelProvider":"openai","createdAt":1780960289,"updatedAt":1780960290,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-11-30-019ea981-64c4-76b1-9f90-866490b7dd60.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":5,"result":{"turn":{"id":"019ea981-662f-70c3-b2e4-9357b2beb122","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turn":{"id":"019ea981-662f-70c3-b2e4-9357b2beb122","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780960290,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"13a3ec40-b839-4190-a15f-263b54ae12e9","clientId":null,"content":[{"type":"text","text":"Remember the second opaque marker fork-marker-2K4M for later in this conversation. Respond with exactly: fork marker stored","text_elements":[]}]},"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-662f-70c3-b2e4-9357b2beb122","startedAtMs":1780960293090}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"13a3ec40-b839-4190-a15f-263b54ae12e9","clientId":null,"content":[{"type":"text","text":"Remember the second opaque marker fork-marker-2K4M for later in this conversation. Respond with exactly: fork marker stored","text_elements":[]}]},"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-662f-70c3-b2e4-9357b2beb122","completedAtMs":1780960293090}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_09d1bdad36e20dad016a274c262d4881999249df611ae5658c","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-662f-70c3-b2e4-9357b2beb122","startedAtMs":1780960294269}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-662f-70c3-b2e4-9357b2beb122","itemId":"msg_09d1bdad36e20dad016a274c262d4881999249df611ae5658c","delta":"fork"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-662f-70c3-b2e4-9357b2beb122","itemId":"msg_09d1bdad36e20dad016a274c262d4881999249df611ae5658c","delta":" marker"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-662f-70c3-b2e4-9357b2beb122","itemId":"msg_09d1bdad36e20dad016a274c262d4881999249df611ae5658c","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_09d1bdad36e20dad016a274c262d4881999249df611ae5658c","text":"fork marker stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-662f-70c3-b2e4-9357b2beb122","completedAtMs":1780960294269}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-662f-70c3-b2e4-9357b2beb122","tokenUsage":{"total":{"totalTokens":36930,"inputTokens":36916,"cachedInputTokens":27392,"outputTokens":14,"reasoningOutputTokens":0},"last":{"totalTokens":18484,"inputTokens":18477,"cachedInputTokens":18304,"outputTokens":7,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turn":{"id":"019ea981-662f-70c3-b2e4-9357b2beb122","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780960290,"completedAt":1780960294,"durationMs":3942}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":6,"method":"turn/start","params":{"input":[{"text":"Return the two opaque markers previously provided in chronological order, separated by a single | character. Respond with only the markers and separator.","type":"text"}],"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":6,"result":{"turn":{"id":"019ea981-759a-7e61-90b2-29c4a08825db","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turn":{"id":"019ea981-759a-7e61-90b2-29c4a08825db","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780960294,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"438a8ce6-36e7-42e4-b37d-0689934995b5","clientId":null,"content":[{"type":"text","text":"Return the two opaque markers previously provided in chronological order, separated by a single | character. Respond with only the markers and separator.","text_elements":[]}]},"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","startedAtMs":1780960294306}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"438a8ce6-36e7-42e4-b37d-0689934995b5","clientId":null,"content":[{"type":"text","text":"Return the two opaque markers previously provided in chronological order, separated by a single | character. Respond with only the markers and separator.","text_elements":[]}]},"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","completedAtMs":1780960294306}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","startedAtMs":1780960297297}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"-marker"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"7"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"Q"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"9"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"V"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"|"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"fork"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"-marker"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"2"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"K"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"4"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","itemId":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","delta":"M"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_09d1bdad36e20dad016a274c2945788199af67a52221987a8f","text":"source-marker-7Q9V|fork-marker-2K4M","phase":"final_answer","memoryCitation":null},"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","completedAtMs":1780960297433}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turnId":"019ea981-759a-7e61-90b2-29c4a08825db","tokenUsage":{"total":{"totalTokens":55465,"inputTokens":55432,"cachedInputTokens":45696,"outputTokens":33,"reasoningOutputTokens":0},"last":{"totalTokens":18535,"inputTokens":18516,"cachedInputTokens":18304,"outputTokens":19,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea981-64c4-76b1-9f90-866490b7dd60","turn":{"id":"019ea981-759a-7e61-90b2-29c4a08825db","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780960294,"completedAt":1780960297,"durationMs":3150}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_fork_local_rollback/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_fork_local_rollback/claude_transcript.ndjson new file mode 100644 index 00000000000..c1efbdb65ab --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_fork_local_rollback/claude_transcript.ndjson @@ -0,0 +1,35 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"thread_fork_native_fork_local_rollback","metadata":{"prompts":["For this fork-local rollback fixture, respond with exactly: fork local source alpha","For this fork-local rollback fixture, respond with exactly: fork local first","For this fork-local rollback fixture, respond with exactly: fork local second","Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content."],"model":"claude-sonnet-4-6","nativeSessionId":"273fe17b-65fb-451f-ad99-ff19d323852d","queryMode":"fork_session_resume_at_fork_cursor","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"probeClaudeForkLocalRollbackReplay","sourceAssistantMessageUuids":["7659a146-b729-47b6-aac2-8fc0c0981e92"],"forkUpToMessageId":"7659a146-b729-47b6-aac2-8fc0c0981e92","forkedNativeSessionId":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","forkAssistantMessageUuids":["14310fd5-a1aa-448b-8880-de52f86541b1","b9bb4e45-d52d-4fb3-adf7-4b5e4d9d0dd6"],"resumeSessionAt":"14310fd5-a1aa-448b-8880-de52f86541b1"}} +{"type":"expect_outbound","label":"query.open:source","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"273fe17b-65fb-451f-ad99-ff19d323852d"}}} +{"type":"expect_outbound","label":"prompt.offer:source","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"For this fork-local rollback fixture, respond with exactly: fork local source alpha"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"36e94137-12f0-4e71-828b-bd4c76467204","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"f8884f88-0672-4602-bb93-a35473be10f8","session_id":"273fe17b-65fb-451f-ad99-ff19d323852d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"36e94137-12f0-4e71-828b-bd4c76467204","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"9b44ee7b-9a43-41a6-993d-6cb5945a8210","session_id":"273fe17b-65fb-451f-ad99-ff19d323852d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","cwd":"/private/tmp/claude-replay-thread_fork_native_fork_local_rollback","session_id":"273fe17b-65fb-451f-ad99-ff19d323852d","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","ListMcpResourcesTool","Monitor","NotebookEdit","PushNotification","Read","ReadMcpResourceTool","RemoteTrigger","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","ToolSearch","WebFetch","WebSearch","Write","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication","mcp__uidotsh__uidotsh_fetch"],"mcp_servers":[{"name":"uidotsh","status":"connected"},{"name":"claude.ai Google Drive","status":"needs-auth"}],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":["update-config","debug","simplify","batch","less-permission-prompts","loop","schedule","claude-api","expo-dev-client","ui","agent-browser","native-data-fetching","expo-deployment","btca-cli","uniwind","web-design-guidelines","react-doctor","migrate-nativewind-to-uniwind","building-native-ui","frontend-design","turborepo","vercel-composition-patterns","expo-cicd-workflows","expo-tailwind-setup","coderabbit:review","coderabbit:coderabbit-review","coderabbit:code-review","agent-browser:agent-browser","frontend-design:frontend-design","coderabbit:autofix","compact","context","cost","heapdump","init","review","security-review","extra-usage","insights","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.111","output_style":"default","agents":["coderabbit:code-reviewer","Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","less-permission-prompts","loop","schedule","claude-api","expo-dev-client","ui","agent-browser","native-data-fetching","expo-deployment","btca-cli","uniwind","web-design-guidelines","react-doctor","migrate-nativewind-to-uniwind","building-native-ui","frontend-design","turborepo","vercel-composition-patterns","expo-cicd-workflows","expo-tailwind-setup","coderabbit:code-review","agent-browser:agent-browser","frontend-design:frontend-design","coderabbit:autofix","coderabbit:code-review"],"plugins":[{"name":"coderabbit","path":"/home/replay-user/.claude/plugins/marketplaces/coderabbit/","source":"coderabbit@coderabbit"},{"name":"agent-browser","path":"/home/replay-user/.claude/plugins/cache/agent-browser/agent-browser/ea17db856473","source":"agent-browser@agent-browser"},{"name":"frontend-design","path":"/home/replay-user/.claude/plugins/cache/claude-plugins-official/frontend-design/unknown","source":"frontend-design@claude-plugins-official"},{"name":"coderabbit","path":"/home/replay-user/.claude/plugins/cache/claude-plugins-official/coderabbit/1.1.1","source":"coderabbit@claude-plugins-official"}],"uuid":"1e7d6f8f-2295-42cb-a413-50274ffe1bdd","memory_paths":{"auto":"/home/replay-user/.claude/projects/-private-var-folders-b3-b51-pdxj7dl0t981zpqkxxhr0000gn-T-t3-orchestrator-v2-claude-agent-sdk-record-thread-fork-native-fork-local-rollback-6isYFV/memory/"},"fast_mode_state":"off"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01GymzntxN7tobzwVerJJDMw","type":"message","role":"assistant","content":[{"type":"text","text":"fork local source alpha"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11800,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"273fe17b-65fb-451f-ad99-ff19d323852d","uuid":"7659a146-b729-47b6-aac2-8fc0c0981e92"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1779846000,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"94e56646-7610-4a7a-8245-9301469861ae","session_id":"273fe17b-65fb-451f-ad99-ff19d323852d"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2928,"duration_api_ms":1977,"num_turns":1,"result":"fork local source alpha","stop_reason":"end_turn","session_id":"273fe17b-65fb-451f-ad99-ff19d323852d","total_cost_usd":0.0036539999999999997,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11800,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":11800,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":7,"cacheReadInputTokens":11800,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0036539999999999997,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"684990a5-0d00-4928-a332-fdc2c66769c6"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"session.fork","frame":{"type":"session.fork","sessionId":"273fe17b-65fb-451f-ad99-ff19d323852d","options":{"dir":"/tmp/claude-replay-thread_fork_native_fork_local_rollback","upToMessageId":"7659a146-b729-47b6-aac2-8fc0c0981e92"}}} +{"type":"emit_inbound","label":"session.forked","frame":{"type":"session.forked","sessionId":"c9784d72-7e4f-4994-8c21-2e7e670ff91c"}} +{"type":"expect_outbound","label":"query.open:fork","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"c9784d72-7e4f-4994-8c21-2e7e670ff91c"}}} +{"type":"expect_outbound","label":"prompt.offer:fork-first","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"For this fork-local rollback fixture, respond with exactly: fork local first"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"b8ac144f-11aa-4f98-96f3-9c9caa8c93ef","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"7e53533c-0f7a-4c67-bed4-0124e8b57438","session_id":"1eb8c977-ac69-4559-8501-92edb5a32f2f"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"b8ac144f-11aa-4f98-96f3-9c9caa8c93ef","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"c176d46f-750b-4bab-9d7a-ab863903a141","session_id":"1eb8c977-ac69-4559-8501-92edb5a32f2f"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","cwd":"/private/tmp/claude-replay-thread_fork_native_fork_local_rollback","session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","ListMcpResourcesTool","Monitor","NotebookEdit","PushNotification","Read","ReadMcpResourceTool","RemoteTrigger","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","ToolSearch","WebFetch","WebSearch","Write","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication","mcp__uidotsh__uidotsh_fetch"],"mcp_servers":[{"name":"uidotsh","status":"connected"},{"name":"claude.ai Google Drive","status":"needs-auth"}],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":["update-config","debug","simplify","batch","less-permission-prompts","loop","schedule","claude-api","expo-dev-client","ui","agent-browser","native-data-fetching","expo-deployment","btca-cli","uniwind","web-design-guidelines","react-doctor","migrate-nativewind-to-uniwind","building-native-ui","frontend-design","turborepo","vercel-composition-patterns","expo-cicd-workflows","expo-tailwind-setup","coderabbit:review","coderabbit:coderabbit-review","coderabbit:code-review","agent-browser:agent-browser","frontend-design:frontend-design","coderabbit:autofix","compact","context","cost","heapdump","init","review","security-review","extra-usage","insights","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.111","output_style":"default","agents":["coderabbit:code-reviewer","Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","less-permission-prompts","loop","schedule","claude-api","expo-dev-client","ui","agent-browser","native-data-fetching","expo-deployment","btca-cli","uniwind","web-design-guidelines","react-doctor","migrate-nativewind-to-uniwind","building-native-ui","frontend-design","turborepo","vercel-composition-patterns","expo-cicd-workflows","expo-tailwind-setup","coderabbit:code-review","agent-browser:agent-browser","frontend-design:frontend-design","coderabbit:autofix","coderabbit:code-review"],"plugins":[{"name":"coderabbit","path":"/home/replay-user/.claude/plugins/cache/coderabbit/coderabbit/1.0.0","source":"coderabbit@coderabbit"},{"name":"agent-browser","path":"/home/replay-user/.claude/plugins/cache/agent-browser/agent-browser/ea17db856473","source":"agent-browser@agent-browser"},{"name":"frontend-design","path":"/home/replay-user/.claude/plugins/cache/claude-plugins-official/frontend-design/unknown","source":"frontend-design@claude-plugins-official"},{"name":"coderabbit","path":"/home/replay-user/.claude/plugins/cache/claude-plugins-official/coderabbit/1.1.1","source":"coderabbit@claude-plugins-official"}],"uuid":"741656e7-2341-497c-83bc-89e946bd631f","memory_paths":{"auto":"/home/replay-user/.claude/projects/-private-var-folders-b3-b51-pdxj7dl0t981zpqkxxhr0000gn-T-t3-orchestrator-v2-claude-agent-sdk-record-thread-fork-native-fork-local-rollback-6isYFV/memory/"},"fast_mode_state":"off"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01Fum9PErU1oqWY16swx8VpU","type":"message","role":"assistant","content":[{"type":"text","text":"fork local first"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11826,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","uuid":"14310fd5-a1aa-448b-8880-de52f86541b1"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1779846000,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"830763b3-7ff2-4b0f-8dd9-35b141cca7a4","session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2891,"duration_api_ms":2011,"num_turns":1,"result":"fork local first","stop_reason":"end_turn","session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","total_cost_usd":0.0036468,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11826,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":11826,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":11826,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0036468,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"bac4638c-4c4e-4e8d-9361-1c32f538297e"}} +{"type":"expect_outbound","label":"prompt.offer:fork-second","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"For this fork-local rollback fixture, respond with exactly: fork local second"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","cwd":"/private/tmp/claude-replay-thread_fork_native_fork_local_rollback","session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","ListMcpResourcesTool","Monitor","NotebookEdit","PushNotification","Read","ReadMcpResourceTool","RemoteTrigger","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","ToolSearch","WebFetch","WebSearch","Write","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication","mcp__uidotsh__uidotsh_fetch"],"mcp_servers":[{"name":"uidotsh","status":"connected"},{"name":"claude.ai Google Drive","status":"needs-auth"}],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":["update-config","debug","simplify","batch","less-permission-prompts","loop","schedule","claude-api","expo-dev-client","ui","agent-browser","native-data-fetching","expo-deployment","btca-cli","uniwind","web-design-guidelines","react-doctor","migrate-nativewind-to-uniwind","building-native-ui","frontend-design","turborepo","vercel-composition-patterns","expo-cicd-workflows","expo-tailwind-setup","coderabbit:review","coderabbit:coderabbit-review","coderabbit:code-review","agent-browser:agent-browser","frontend-design:frontend-design","coderabbit:autofix","compact","context","cost","heapdump","init","review","security-review","extra-usage","insights","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.111","output_style":"default","agents":["coderabbit:code-reviewer","Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","less-permission-prompts","loop","schedule","claude-api","expo-dev-client","ui","agent-browser","native-data-fetching","expo-deployment","btca-cli","uniwind","web-design-guidelines","react-doctor","migrate-nativewind-to-uniwind","building-native-ui","frontend-design","turborepo","vercel-composition-patterns","expo-cicd-workflows","expo-tailwind-setup","coderabbit:code-review","agent-browser:agent-browser","frontend-design:frontend-design","coderabbit:autofix","coderabbit:code-review"],"plugins":[{"name":"coderabbit","path":"/home/replay-user/.claude/plugins/cache/coderabbit/coderabbit/1.0.0","source":"coderabbit@coderabbit"},{"name":"agent-browser","path":"/home/replay-user/.claude/plugins/cache/agent-browser/agent-browser/ea17db856473","source":"agent-browser@agent-browser"},{"name":"frontend-design","path":"/home/replay-user/.claude/plugins/cache/claude-plugins-official/frontend-design/unknown","source":"frontend-design@claude-plugins-official"},{"name":"coderabbit","path":"/home/replay-user/.claude/plugins/cache/claude-plugins-official/coderabbit/1.1.1","source":"coderabbit@claude-plugins-official"}],"uuid":"d53475a1-0a9f-4c9e-b247-b3b9d347229f","memory_paths":{"auto":"/home/replay-user/.claude/projects/-private-var-folders-b3-b51-pdxj7dl0t981zpqkxxhr0000gn-T-t3-orchestrator-v2-claude-agent-sdk-record-thread-fork-native-fork-local-rollback-6isYFV/memory/"},"fast_mode_state":"off"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_016vLjeBrWCvu6QqJMFd8jDp","type":"message","role":"assistant","content":[{"type":"text","text":"fork local second"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11851,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","uuid":"b9bb4e45-d52d-4fb3-adf7-4b5e4d9d0dd6"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2787,"duration_api_ms":3998,"num_turns":1,"result":"fork local second","stop_reason":"end_turn","session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","total_cost_usd":0.0073011,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11851,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":11851,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":6,"outputTokens":12,"cacheReadInputTokens":23677,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0073011,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"af3731ff-720f-4919-87f6-c0b27b3ee21e"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"query.open:fork-resume-at-cursor","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resumeSessionAt":"14310fd5-a1aa-448b-8880-de52f86541b1","resume":"c9784d72-7e4f-4994-8c21-2e7e670ff91c"}}} +{"type":"expect_outbound","label":"prompt.offer:fork-after-rollback","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"9deedf90-8751-4ba3-8602-4499dba72f9c","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"c2908d02-3d69-4d36-bc32-50aaa32e4e92","session_id":"05a5ea05-d17a-4841-98e3-7cf6957f3f0a"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"9deedf90-8751-4ba3-8602-4499dba72f9c","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"f438cfbd-c7e3-4c83-a047-205862b253e6","session_id":"05a5ea05-d17a-4841-98e3-7cf6957f3f0a"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","cwd":"/private/tmp/claude-replay-thread_fork_native_fork_local_rollback","session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","ListMcpResourcesTool","Monitor","NotebookEdit","PushNotification","Read","ReadMcpResourceTool","RemoteTrigger","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","ToolSearch","WebFetch","WebSearch","Write","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication","mcp__uidotsh__uidotsh_fetch"],"mcp_servers":[{"name":"uidotsh","status":"connected"},{"name":"claude.ai Google Drive","status":"needs-auth"}],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":["update-config","debug","simplify","batch","less-permission-prompts","loop","schedule","claude-api","expo-dev-client","ui","agent-browser","native-data-fetching","expo-deployment","btca-cli","uniwind","web-design-guidelines","react-doctor","migrate-nativewind-to-uniwind","building-native-ui","frontend-design","turborepo","vercel-composition-patterns","expo-cicd-workflows","expo-tailwind-setup","coderabbit:review","coderabbit:coderabbit-review","coderabbit:code-review","agent-browser:agent-browser","frontend-design:frontend-design","coderabbit:autofix","compact","context","cost","heapdump","init","review","security-review","extra-usage","insights","team-onboarding"],"apiKeySource":"none","claude_code_version":"2.1.111","output_style":"default","agents":["coderabbit:code-reviewer","Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","less-permission-prompts","loop","schedule","claude-api","expo-dev-client","ui","agent-browser","native-data-fetching","expo-deployment","btca-cli","uniwind","web-design-guidelines","react-doctor","migrate-nativewind-to-uniwind","building-native-ui","frontend-design","turborepo","vercel-composition-patterns","expo-cicd-workflows","expo-tailwind-setup","coderabbit:code-review","agent-browser:agent-browser","frontend-design:frontend-design","coderabbit:code-review","coderabbit:autofix"],"plugins":[{"name":"coderabbit","path":"/home/replay-user/.claude/plugins/marketplaces/coderabbit/","source":"coderabbit@coderabbit"},{"name":"agent-browser","path":"/home/replay-user/.claude/plugins/cache/agent-browser/agent-browser/ea17db856473","source":"agent-browser@agent-browser"},{"name":"frontend-design","path":"/home/replay-user/.claude/plugins/cache/claude-plugins-official/frontend-design/unknown","source":"frontend-design@claude-plugins-official"},{"name":"coderabbit","path":"/home/replay-user/.claude/plugins/cache/claude-plugins-official/coderabbit/1.1.1","source":"coderabbit@claude-plugins-official"}],"uuid":"54c3aef7-de37-41de-81e9-b79cf044464b","memory_paths":{"auto":"/home/replay-user/.claude/projects/-private-var-folders-b3-b51-pdxj7dl0t981zpqkxxhr0000gn-T-t3-orchestrator-v2-claude-agent-sdk-record-thread-fork-native-fork-local-rollback-6isYFV/memory/"},"fast_mode_state":"off"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01SMtNaYRbU1BonxpaTu5bxG","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to repeat the user-visible conversation so far verbatim, including only user and assistant messages.","signature":"Er4CCmUIDhgCKkAHbcjlRRK3rHsyqzZn5jI5+wWIl32x7k4E5feJpl1bBZVM6FJVJDBBV5ZgPlHGZMMALoCcDsULDBqzwNbRQ8o0MhFjbGF1ZGUtc29ubmV0LTQtNjgAQgh0aGlua2luZxIMoLzq1u1TRvWwekqBGgwtjzuuwl6xVaBbaIwiMFElE3wNpvOZkdvQJt0d5Iy2NLdr4Wfwrm+7xTXMyx8a6uiOS9qM9NCvBFlNCOV80SqGARBc5k/ENzjBSzsiEbFPeRQi7CV0sm3Csm4WAbC92/ulSlHrBa/+2flxuX4BC24CtUORK7vwEaBjXCmClZibaHWKTmk2cq1SXGzQGifcNR94R5O/B7/oYR/VBEaZb7ctHHXaO7SEifznsRhPibvj9wG4BnxU99aoUgnk1riwYsyEXWsg1rFNGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11864,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","uuid":"a259ccd5-2f6b-4e3c-9c1a-ce55d21c5ba5"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01SMtNaYRbU1BonxpaTu5bxG","type":"message","role":"assistant","content":[{"type":"text","text":"**User:** For this fork-local rollback fixture, respond with exactly: fork local source alpha\n\n**Assistant:** fork local source alpha\n\n**User:** For this fork-local rollback fixture, respond with exactly: fork local first\n\n**Assistant:** fork local first\n\n**User:** Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11864,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","uuid":"1ef6575a-75b1-4568-84cb-0eadc9e224e6"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1779846000,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"a39eccdb-a0f9-4b39-bbd1-6931138dc5d0","session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":4060,"duration_api_ms":3133,"num_turns":1,"result":"**User:** For this fork-local rollback fixture, respond with exactly: fork local source alpha\n\n**Assistant:** fork local source alpha\n\n**User:** For this fork-local rollback fixture, respond with exactly: fork local first\n\n**Assistant:** fork local first\n\n**User:** Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content.","stop_reason":"end_turn","session_id":"c9784d72-7e4f-4994-8c21-2e7e670ff91c","total_cost_usd":0.005518199999999999,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":11864,"output_tokens":130,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":130,"cache_read_input_tokens":11864,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":130,"cacheReadInputTokens":11864,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.005518199999999999,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"7741f0e1-8180-48d8-8073-0295e00a19f8"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/claude_transcript.ndjson new file mode 100644 index 00000000000..5f8abf8cdaf --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/claude_transcript.ndjson @@ -0,0 +1,26 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"thread_fork_native_prior_turn","metadata":{"prompts":["For this fork-boundary fixture, respond with exactly: fork boundary alpha","For this fork-boundary fixture, respond with exactly: fork boundary beta","Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content."],"model":"claude-sonnet-4-6","nativeSessionId":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99","queryMode":"fork_session_prior_turn","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript","sourceAssistantMessageUuids":["be0e3276-8c27-44bf-8ed0-0b4424e9f902","c517b74b-9537-4260-afcb-186c4056c0a4"],"forkUpToMessageId":"be0e3276-8c27-44bf-8ed0-0b4424e9f902","forkedNativeSessionId":"f5cc58dd-0d74-4950-9fb5-2a9fded47ddd"}} +{"type":"expect_outbound","label":"query.open:source","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"For this fork-boundary fixture, respond with exactly: fork boundary alpha"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"25b085e7-a6e9-4f30-9708-b189c961d06f","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"42c40147-86c8-4294-bdb4-f1815f48808e","session_id":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"25b085e7-a6e9-4f30-9708-b189c961d06f","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"9804deb1-9abf-4e58-9d04-6e163dc7b0af","session_id":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-thread_fork_native_prior_turn","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"b9412e24-eefc-4838-9ede-bb6e00951d15","session_id":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_014hdmSJdwbpJ5NkFgqbhfEe","type":"message","role":"assistant","content":[{"type":"text","text":"fork boundary alpha"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2342,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2342},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99","uuid":"be0e3276-8c27-44bf-8ed0-0b4424e9f902"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"0c649726-b7a2-4e89-a62a-a9ae534905c7","session_id":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":1174,"duration_api_ms":897,"num_turns":1,"result":"fork boundary alpha","stop_reason":"end_turn","session_id":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99","total_cost_usd":0.011718000000000001,"usage":{"input_tokens":3,"cache_creation_input_tokens":2342,"cache_read_input_tokens":9455,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2342,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":9455,"cache_creation_input_tokens":2342,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2342},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":9455,"cacheCreationInputTokens":2342,"webSearchRequests":0,"costUSD":0.011718000000000001,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"d384185c-9b38-44a4-8917-53d7a8765a5a"}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"For this fork-boundary fixture, respond with exactly: fork boundary beta"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-thread_fork_native_prior_turn","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"5fd1f879-e742-4911-95e3-3dfb0141dc52","session_id":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_0181K4CdzyVanM6Px26CQP7L","type":"message","role":"assistant","content":[{"type":"text","text":"fork boundary beta"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":23,"cache_read_input_tokens":11797,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":23},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99","uuid":"c517b74b-9537-4260-afcb-186c4056c0a4"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2545,"duration_api_ms":3001,"num_turns":1,"result":"fork boundary beta","stop_reason":"end_turn","session_id":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99","total_cost_usd":0.01544235,"usage":{"input_tokens":3,"cache_creation_input_tokens":23,"cache_read_input_tokens":11797,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":23,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":11797,"cache_creation_input_tokens":23,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":23},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":6,"outputTokens":12,"cacheReadInputTokens":21252,"cacheCreationInputTokens":2365,"webSearchRequests":0,"costUSD":0.01544235,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"e4f26af4-3a60-45f3-8d5c-c0f8b968c400"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"session.fork","frame":{"type":"session.fork","sessionId":"a91391e1-0de3-4107-a7c9-db7b1bbd8b99","options":{"dir":"/tmp/claude-replay-thread_fork_native_prior_turn","upToMessageId":"be0e3276-8c27-44bf-8ed0-0b4424e9f902"}}} +{"type":"emit_inbound","label":"session.forked","frame":{"type":"session.forked","sessionId":"f5cc58dd-0d74-4950-9fb5-2a9fded47ddd"}} +{"type":"expect_outbound","label":"query.open:fork","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"f5cc58dd-0d74-4950-9fb5-2a9fded47ddd"}}} +{"type":"expect_outbound","label":"prompt.offer:3","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"6ce9327c-4534-484c-96e5-b47fa1a3143f","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"d4280b57-8334-4021-971d-93bba0e38b4c","session_id":"1dc8dab2-9e1b-4475-bd46-f40e2ebca07d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"6ce9327c-4534-484c-96e5-b47fa1a3143f","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"ac1dee1e-63c5-4db8-81b7-e1f68ecadf01","session_id":"1dc8dab2-9e1b-4475-bd46-f40e2ebca07d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-thread_fork_native_prior_turn","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"65e2ff1d-ded8-42d2-a491-70fed78b2368","session_id":"f5cc58dd-0d74-4950-9fb5-2a9fded47ddd"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01NP2pmrWJHY1AZ8HWqgsCQS","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to repeat the user-visible conversation so far verbatim, including only user and assistant messages.","signature":"ErQCClsIDRgCKkAHbcjlRRK3rHsyqzZn5jI5+wWIl32x7k4E5feJpl1bBZVM6FJVJDBBV5ZgPlHGZMMALoCcDsULDBqzwNbRQ8o0MhFjbGF1ZGUtc29ubmV0LTQtNjgAEgzxdU+J4dE9HPjPJBwaDFLfMOU1oTIY4zJlVSIwkqZMdRU0W/uaPIztqHfQSORVwpEKdR6RAOJb0t5E+IOBu+tFmopmwfCbQjON3NoEKoYBs8E4pkuYLNz2Q+Qp7XeZOdCvvXUTYVIBJ90r2tl9NtEyDgCKce2VtEShQxnPZH9OhcyuEuKrL8c+Fsc8rTj6vYelMB2UJfj9KI1bREqSwoBf1oD7uwiuOz+S40+1Tx7LprJUcmw+JzkTO73YmVr8EHYGCHbQlLSeUHm7NgypEkTcOAaETiIYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":38,"cache_read_input_tokens":11797,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":38},"output_tokens":7,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f5cc58dd-0d74-4950-9fb5-2a9fded47ddd","uuid":"3a81c9d0-22dd-4bbd-8f27-103c75e2878c"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01NP2pmrWJHY1AZ8HWqgsCQS","type":"message","role":"assistant","content":[{"type":"text","text":"**User:** For this fork-boundary fixture, respond with exactly: fork boundary alpha\n\n**Assistant:** fork boundary alpha\n\n**User:** Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":38,"cache_read_input_tokens":11797,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":38},"output_tokens":7,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f5cc58dd-0d74-4950-9fb5-2a9fded47ddd","uuid":"90f4c83b-8fff-454a-a95d-aa0cf87ff967"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"74ba363a-dc66-4335-b18c-b335f75d4c9b","session_id":"f5cc58dd-0d74-4950-9fb5-2a9fded47ddd"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2557,"duration_api_ms":2194,"num_turns":1,"result":"**User:** For this fork-boundary fixture, respond with exactly: fork boundary alpha\n\n**Assistant:** fork boundary alpha\n\n**User:** Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content.","stop_reason":"end_turn","session_id":"f5cc58dd-0d74-4950-9fb5-2a9fded47ddd","total_cost_usd":0.0051756,"usage":{"input_tokens":3,"cache_creation_input_tokens":38,"cache_read_input_tokens":11797,"output_tokens":99,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":38,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":99,"cache_read_input_tokens":11797,"cache_creation_input_tokens":38,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":38},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":99,"cacheReadInputTokens":11797,"cacheCreationInputTokens":38,"webSearchRequests":0,"costUSD":0.0051756,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"6fe96da3-b212-41e9-be7b-12d5aee4f191"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/codex_transcript.ndjson new file mode 100644 index 00000000000..3d37d5d21bb --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_prior_turn/codex_transcript.ndjson @@ -0,0 +1,933 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.124.0-alpha.3","scenario":"thread_fork_native_prior_turn","metadata":{"source":"codex-app-server-live-probe-normalized-for-orchestrator-replay","description":"Two completed source turns, native thread/fork, rollback of the fork to the first source turn, and a fork turn that only sees the first turn.","sourceThreadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","forkThreadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","firstTurnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","secondTurnId":"019dd6ba-416d-7902-95cf-d4179c3f4738","repeatTurnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","normalization":"Native frames are live. Initialize frames are normalized to the T3 adapter client info, and a fork-session initialize was inserted to match orchestrator provider-session lifecycle."}} +{"type":"expect_outbound","label":"initialize/source","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize/source","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.124.0-alpha.3","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized/source","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1777424017,"updatedAt":1777424017,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/28/rollout-2026-04-28T17-53-37-019dd6ba-2681-7bf0-b051-141b0cbcbb27.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.124.0-alpha.3","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.5","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"permissionProfile":{"network":{"enabled":false},"fileSystem":{"entries":[{"path":{"type":"special","value":{"kind":"root"}},"access":"read"},{"path":{"type":"special","value":{"kind":"current_working_directory"}},"access":"write"},{"path":{"type":"special","value":{"kind":"slash_tmp"}},"access":"write"},{"path":{"type":"special","value":{"kind":"tmpdir"}},"access":"write"},{"path":{"type":"path","path":"/Users/julius/.codex/memories"},"access":"write"},{"path":{"type":"path","path":"/Users/julius/Development/Work/codething-mvp/.git/worktrees/t3code-c1e5e1d1"},"access":"read"},{"path":{"type":"path","path":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/.git"},"access":"read"},{"path":{"type":"path","path":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/.codex"},"access":"read"}]}},"reasoningEffort":"medium"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1777424017,"updatedAt":1777424017,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/28/rollout-2026-04-28T17-53-37-019dd6ba-2681-7bf0-b051-141b0cbcbb27.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.124.0-alpha.3","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","input":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary alpha"}],"approvalPolicy":"never","sandboxPolicy":{"type":"readOnly","networkAccess":false}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"warning","frame":{"method":"warning","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","message":"Under-development features enabled: apply_patch_freeform. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in /Users/julius/.codex/config.toml."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turn":{"id":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","items":[],"status":"inProgress","error":null,"startedAt":1777424017,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"844ba324-a7e8-42c8-9008-70c68163b7e8","content":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary alpha","text_elements":[]}]},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"844ba324-a7e8-42c8-9008-70c68163b7e8","content":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary alpha","text_elements":[]}]},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":61,"windowDurationMins":300,"resetsAt":1777429830},"secondary":{"usedPercent":23,"windowDurationMins":10080,"resetsAt":1777959590},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0e55e81d6295a5050169f156979d24819b80cbd167a76cda9d","summary":[],"content":[]},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0e55e81d6295a5050169f156979d24819b80cbd167a76cda9d","summary":[],"content":[]},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0e55e81d6295a5050169f15697bfc0819bb19113933c189c9a","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","itemId":"msg_0e55e81d6295a5050169f15697bfc0819bb19113933c189c9a","delta":"fork"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","itemId":"msg_0e55e81d6295a5050169f15697bfc0819bb19113933c189c9a","delta":" boundary"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","itemId":"msg_0e55e81d6295a5050169f15697bfc0819bb19113933c189c9a","delta":" alpha"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0e55e81d6295a5050169f15697bfc0819bb19113933c189c9a","text":"fork boundary alpha","phase":"final_answer","memoryCitation":null},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","tokenUsage":{"total":{"totalTokens":26918,"inputTokens":26900,"cachedInputTokens":3456,"outputTokens":18,"reasoningOutputTokens":9},"last":{"totalTokens":26918,"inputTokens":26900,"cachedInputTokens":3456,"outputTokens":18,"reasoningOutputTokens":9},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":61,"windowDurationMins":300,"resetsAt":1777429830},"secondary":{"usedPercent":23,"windowDurationMins":10080,"resetsAt":1777959590},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turn":{"id":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","items":[],"status":"completed","error":null,"startedAt":1777424017,"completedAt":1777424023,"durationMs":6880}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":4,"method":"turn/start","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","input":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary beta"}],"approvalPolicy":"never","sandboxPolicy":{"type":"readOnly","networkAccess":false}}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":4,"result":{"turn":{"id":"019dd6ba-416d-7902-95cf-d4179c3f4738","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turn":{"id":"019dd6ba-416d-7902-95cf-d4179c3f4738","items":[],"status":"inProgress","error":null,"startedAt":1777424023,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"e8550ffa-7e59-48b3-8fd2-7bbca932ab95","content":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary beta","text_elements":[]}]},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"e8550ffa-7e59-48b3-8fd2-7bbca932ab95","content":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary beta","text_elements":[]}]},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738","tokenUsage":{"total":{"totalTokens":26918,"inputTokens":26900,"cachedInputTokens":3456,"outputTokens":18,"reasoningOutputTokens":9},"last":{"totalTokens":26918,"inputTokens":26900,"cachedInputTokens":3456,"outputTokens":18,"reasoningOutputTokens":9},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":61,"windowDurationMins":300,"resetsAt":1777429830},"secondary":{"usedPercent":23,"windowDurationMins":10080,"resetsAt":1777959590},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0e55e81d6295a5050169f156990ae4819b9bc7f93dfdd76a91","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738","itemId":"msg_0e55e81d6295a5050169f156990ae4819b9bc7f93dfdd76a91","delta":"fork"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738","itemId":"msg_0e55e81d6295a5050169f156990ae4819b9bc7f93dfdd76a91","delta":" boundary"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738","itemId":"msg_0e55e81d6295a5050169f156990ae4819b9bc7f93dfdd76a91","delta":" beta"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0e55e81d6295a5050169f156990ae4819b9bc7f93dfdd76a91","text":"fork boundary beta","phase":"final_answer","memoryCitation":null},"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738","tokenUsage":{"total":{"totalTokens":53852,"inputTokens":53827,"cachedInputTokens":29952,"outputTokens":25,"reasoningOutputTokens":9},"last":{"totalTokens":26934,"inputTokens":26927,"cachedInputTokens":26496,"outputTokens":7,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":61,"windowDurationMins":300,"resetsAt":1777429830},"secondary":{"usedPercent":23,"windowDurationMins":10080,"resetsAt":1777959590},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","turn":{"id":"019dd6ba-416d-7902-95cf-d4179c3f4738","items":[],"status":"completed","error":null,"startedAt":1777424023,"completedAt":1777424025,"durationMs":1596}}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":5,"method":"thread/fork","params":{"threadId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27"}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"warning","frame":{"method":"warning","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","message":"Under-development features enabled: apply_patch_freeform. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in /Users/julius/.codex/config.toml."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/fork","frame":{"id":5,"result":{"thread":{"id":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","forkedFromId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","preview":"For this fork-boundary fixture, respond with exactly: fork boundary alpha","ephemeral":false,"modelProvider":"openai","createdAt":1777424025,"updatedAt":1777424025,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/28/rollout-2026-04-28T17-53-45-019dd6ba-47b7-7092-8688-9cf7fe5f6498.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.124.0-alpha.3","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":{"sha":"969e8ea4fac1f2066ed50dd4f82b5fd0014d8a4d","branch":"t3code/codex-turn-mapping","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","items":[{"type":"userMessage","id":"item-1","content":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary alpha","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"fork boundary alpha","phase":"final_answer","memoryCitation":null}],"status":"completed","error":null,"startedAt":1777424017,"completedAt":1777424023,"durationMs":6880},{"id":"019dd6ba-416d-7902-95cf-d4179c3f4738","items":[{"type":"userMessage","id":"item-3","content":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary beta","text_elements":[]}]},{"type":"agentMessage","id":"item-4","text":"fork boundary beta","phase":"final_answer","memoryCitation":null}],"status":"completed","error":null,"startedAt":1777424023,"completedAt":1777424025,"durationMs":1596}]},"model":"gpt-5.5","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"permissionProfile":{"network":{"enabled":false},"fileSystem":{"entries":[{"path":{"type":"special","value":{"kind":"root"}},"access":"read"},{"path":{"type":"special","value":{"kind":"current_working_directory"}},"access":"write"},{"path":{"type":"special","value":{"kind":"slash_tmp"}},"access":"write"},{"path":{"type":"special","value":{"kind":"tmpdir"}},"access":"write"},{"path":{"type":"path","path":"/Users/julius/.codex/memories"},"access":"write"},{"path":{"type":"path","path":"/Users/julius/Development/Work/codething-mvp/.git/worktrees/t3code-c1e5e1d1"},"access":"read"},{"path":{"type":"path","path":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/.git"},"access":"read"},{"path":{"type":"path","path":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/.codex"},"access":"read"}]}},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"thread/rollback","frame":{"id":6,"method":"thread/rollback","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","numTurns":1}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-416d-7902-95cf-d4179c3f4738","tokenUsage":{"total":{"totalTokens":53852,"inputTokens":53827,"cachedInputTokens":29952,"outputTokens":25,"reasoningOutputTokens":9},"last":{"totalTokens":26934,"inputTokens":26927,"cachedInputTokens":26496,"outputTokens":7,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","forkedFromId":"019dd6ba-2681-7bf0-b051-141b0cbcbb27","preview":"For this fork-boundary fixture, respond with exactly: fork boundary alpha","ephemeral":false,"modelProvider":"openai","createdAt":1777424025,"updatedAt":1777424025,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/28/rollout-2026-04-28T17-53-45-019dd6ba-47b7-7092-8688-9cf7fe5f6498.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.124.0-alpha.3","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":{"sha":"969e8ea4fac1f2066ed50dd4f82b5fd0014d8a4d","branch":"t3code/codex-turn-mapping","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","items":[{"type":"userMessage","id":"item-1","content":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary alpha","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"fork boundary alpha","phase":"final_answer","memoryCitation":null}],"status":"completed","error":null,"startedAt":1777424017,"completedAt":1777424023,"durationMs":6880},{"id":"019dd6ba-416d-7902-95cf-d4179c3f4738","items":[{"type":"userMessage","id":"item-3","content":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary beta","text_elements":[]}]},{"type":"agentMessage","id":"item-4","text":"fork boundary beta","phase":"final_answer","memoryCitation":null}],"status":"completed","error":null,"startedAt":1777424023,"completedAt":1777424025,"durationMs":1596}]}}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47e9-7ed2-ba44-374e8b74cf27","tokenUsage":{"total":{"totalTokens":53852,"inputTokens":53827,"cachedInputTokens":29952,"outputTokens":25,"reasoningOutputTokens":9},"last":{"totalTokens":10877,"inputTokens":0,"cachedInputTokens":0,"outputTokens":0,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"thread/rollback","frame":{"id":6,"result":{"thread":{"id":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","forkedFromId":null,"preview":"For this fork-boundary fixture, respond with exactly: fork boundary alpha","ephemeral":false,"modelProvider":"openai","createdAt":1777424025,"updatedAt":1777424025,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/28/rollout-2026-04-28T17-53-45-019dd6ba-47b7-7092-8688-9cf7fe5f6498.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.124.0-alpha.3","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":{"sha":"969e8ea4fac1f2066ed50dd4f82b5fd0014d8a4d","branch":"t3code/codex-turn-mapping","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019dd6ba-268b-7f82-8f4c-9fde0edcfd55","items":[{"type":"userMessage","id":"item-1","content":[{"type":"text","text":"For this fork-boundary fixture, respond with exactly: fork boundary alpha","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"fork boundary alpha","phase":"final_answer","memoryCitation":null}],"status":"completed","error":null,"startedAt":1777424017,"completedAt":1777424023,"durationMs":6880}]}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":7,"method":"turn/start","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","input":[{"type":"text","text":"Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content."}],"approvalPolicy":"never","sandboxPolicy":{"type":"readOnly","networkAccess":false}}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":7,"result":{"turn":{"id":"019dd6ba-47eb-7041-ad45-5abe752c28c9","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turn":{"id":"019dd6ba-47eb-7041-ad45-5abe752c28c9","items":[],"status":"inProgress","error":null,"startedAt":1777424025,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"855c150b-9611-4385-9064-92f6995caeb9","content":[{"type":"text","text":"Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content.","text_elements":[]}]},"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"855c150b-9611-4385-9064-92f6995caeb9","content":[{"type":"text","text":"Repeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content.","text_elements":[]}]},"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","tokenUsage":{"total":{"totalTokens":53852,"inputTokens":53827,"cachedInputTokens":29952,"outputTokens":25,"reasoningOutputTokens":9},"last":{"totalTokens":10877,"inputTokens":0,"cachedInputTokens":0,"outputTokens":0,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":61,"windowDurationMins":300,"resetsAt":1777429830},"secondary":{"usedPercent":23,"windowDurationMins":10080,"resetsAt":1777959590},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0b7536498b07e3900169f1569c4fcc81998db1e60f01250900","summary":[],"content":[]},"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0b7536498b07e3900169f1569c4fcc81998db1e60f01250900","summary":[],"content":[]},"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"User"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"#"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" AG"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ENTS"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".md"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" instructions"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" for"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" /"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Users"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/j"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"uli"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"us"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"t"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/work"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"trees"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"od"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ething"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-m"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"vp"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/t"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"code"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"1"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"e"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"5"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"e"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"1"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"d"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"1"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" include"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Co"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-auth"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ored"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-by"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" in"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" commit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" messages"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" always"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" run"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" project"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" lint"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" scripts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" before"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" considering"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" any"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" task"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" be"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" done"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" always"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" implement"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" run"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" tests"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" for"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" backend"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" changes"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" add"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" test"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" implementations"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" ("}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"layers"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":")"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" for"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" external"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" services"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" only"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" don't"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" mock"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" core"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" business"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" logic"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" out"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"---"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" project"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-doc"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" --"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"#"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" AG"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ENTS"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".md"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"##"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Task"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Completion"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Requirements"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" All"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" of"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"bun"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" fmt"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"bun"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" lint"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`,"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"bun"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" type"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"check"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" must"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" pass"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" before"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" considering"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" tasks"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" completed"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" NEVER"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" run"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"bun"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" test"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Always"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" use"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"bun"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" run"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" test"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" ("}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"runs"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Vit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"est"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":").\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"##"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Project"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Snapshot"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"T"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Code"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" is"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" minimal"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" GUI"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" for"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" using"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" coding"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" agents"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" like"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Claude"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"This"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" repository"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" is"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" VERY"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" EAR"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"LY"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" W"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"IP"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Pro"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"posing"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" sweeping"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" changes"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" that"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" improve"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" long"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-term"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" maintain"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ability"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" is"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" encouraged"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"##"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Core"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Prior"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ities"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"1"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Performance"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"2"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Reliability"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Keep"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" behavior"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" predictable"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" under"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" load"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" during"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" failures"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" ("}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"session"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" rest"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"arts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" reconnect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" partial"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" streams"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":").\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"If"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" trade"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"off"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" is"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" required"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" choose"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" correctness"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" robustness"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" over"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" short"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-term"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" convenience"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"##"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Maintain"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ability"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Long"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" term"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" maintain"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ability"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" is"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" core"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" priority"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" If"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" you"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" add"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" new"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" functionality"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" check"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" if"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" there"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" is"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" shared"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" logic"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" that"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" can"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" be"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" extracted"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" separate"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" module"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Duplicate"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" logic"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" across"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" multiple"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" files"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" is"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" code"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" smell"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" should"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" be"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" avoided"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Don't"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" be"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" afraid"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" change"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" existing"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" code"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Don't"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" take"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" shortcuts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" by"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" just"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" adding"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" local"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" logic"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" solve"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" problem"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"##"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Roles"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"apps"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Node"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".js"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Socket"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Wrap"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" app"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" ("}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"JSON"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-R"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"PC"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" over"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" std"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"io"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"),"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" serves"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" React"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" app"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" manages"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" provider"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" sessions"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"apps"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" React"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/V"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ite"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" UI"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Own"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" session"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" UX"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" conversation"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/event"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" rendering"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" client"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-side"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" state"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Connect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" via"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Socket"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"packages"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/contracts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Shared"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" effect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Schema"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" schemas"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Type"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Script"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" contracts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" for"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" provider"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" events"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Socket"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" protocol"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" model"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/session"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" types"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Keep"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" this"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" schema"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-only"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" —"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" no"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" runtime"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" logic"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"packages"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/shared"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`:"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Shared"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" runtime"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" utilities"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" consumed"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" by"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" both"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Uses"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" explicit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" sub"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"path"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" exports"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" ("}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"e"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".g"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"@"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"t"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"tools"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/shared"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/git"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`)"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" —"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" no"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" barrel"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" index"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"##"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" App"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" ("}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Important"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":")\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"T"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Code"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" is"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" currently"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" The"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" starts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" app"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" ("}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"JSON"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-R"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"PC"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" over"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" std"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"io"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":")"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" per"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" provider"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" session"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" then"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" streams"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" structured"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" events"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" browser"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" through"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Socket"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" push"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" messages"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"How"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" we"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" use"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" it"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" in"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" this"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" code"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"base"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Session"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" startup"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/res"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ume"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" lifecycle"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" are"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" broker"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ed"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" in"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"apps"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/src"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"od"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"App"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Manager"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`.\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Provider"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" dispatch"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" thread"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" event"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" logging"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" are"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" coordinated"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" in"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"apps"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/src"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/provider"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Manager"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`.\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Socket"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" routes"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Native"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Api"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" methods"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" in"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"apps"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/src"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/ws"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`.\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" app"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" consumes"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" orches"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"tration"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" domain"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" events"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" via"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Socket"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" push"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" on"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" channel"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"or"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ches"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"tration"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".domain"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Event"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" ("}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"provider"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" runtime"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" activity"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" is"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" projected"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" into"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" orches"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"tration"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" events"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-side"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":").\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Docs"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" App"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" docs"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" https"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"://"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"developers"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".open"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ai"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".com"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"od"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/sdk"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/#"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"app"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"##"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Reference"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Re"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"pos"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Open"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" repo"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" https"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"://"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"github"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".com"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/open"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ai"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"od"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-M"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"onitor"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" ("}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"T"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"auri"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" feature"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-com"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"plete"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" strong"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" reference"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" implementation"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"):"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" https"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"://"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"github"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".com"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Dim"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"illian"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/C"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"od"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Monitor"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Use"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" these"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" as"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" implementation"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" references"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" when"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" designing"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" protocol"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" handling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" UX"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" flows"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" operational"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" safeguards"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":".\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":""}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Users"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/j"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"uli"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"us"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"t"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/work"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"trees"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"od"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ething"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-m"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"vp"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/t"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"code"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"1"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"e"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"5"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"e"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"1"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"d"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"1"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":""}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"z"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"sh"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":""}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"202"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"6"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"04"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"28"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" "}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":""}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"America"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Los"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"_A"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ngeles"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"User"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"For"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" this"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" fork"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-bound"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"ary"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" respond"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" with"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" exactly"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" fork"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" boundary"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" alpha"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Assistant"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"fork"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" boundary"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" alpha"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"User"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":":\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"Repeat"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" user"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"-visible"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" conversation"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" so"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" far"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" verb"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"atim"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Include"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" only"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" user"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" assistant"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" messages"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" Do"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" not"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" include"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" hidden"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" system"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"/de"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"veloper"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":" content"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","itemId":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0b7536498b07e3900169f1569db3a48199a7192e9f3b34cc8d","text":"User:\n# AGENTS.md instructions for /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1\n\n\n- include `Co-authored-by: codex ` in commit messages\n- always run project lint scripts before considering any task to be done\n- always implement and run tests for backend changes. add test implementations (layers) for external services only. don't mock core business logic out.\n\n--- project-doc ---\n\n# AGENTS.md\n\n## Task Completion Requirements\n\n- All of `bun fmt`, `bun lint`, and `bun typecheck` must pass before considering tasks completed.\n- NEVER run `bun test`. Always use `bun run test` (runs Vitest).\n\n## Project Snapshot\n\nT3 Code is a minimal web GUI for using coding agents like Codex and Claude.\n\nThis repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged.\n\n## Core Priorities\n\n1. Performance first.\n2. Reliability first.\n3. Keep behavior predictable under load and during failures (session restarts, reconnects, partial streams).\n\nIf a tradeoff is required, choose correctness and robustness over short-term convenience.\n\n## Maintainability\n\nLong term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.\n\n## Package Roles\n\n- `apps/server`: Node.js WebSocket server. Wraps Codex app-server (JSON-RPC over stdio), serves the React web app, and manages provider sessions.\n- `apps/web`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. Connects to the server via WebSocket.\n- `packages/contracts`: Shared effect/Schema schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. Keep this package schema-only — no runtime logic.\n- `packages/shared`: Shared runtime utilities consumed by both server and web. Uses explicit subpath exports (e.g. `@t3tools/shared/git`) — no barrel index.\n\n## Codex App Server (Important)\n\nT3 Code is currently Codex-first. The server starts `codex app-server` (JSON-RPC over stdio) per provider session, then streams structured events to the browser through WebSocket push messages.\n\nHow we use it in this codebase:\n\n- Session startup/resume and turn lifecycle are brokered in `apps/server/src/codexAppServerManager.ts`.\n- Provider dispatch and thread event logging are coordinated in `apps/server/src/providerManager.ts`.\n- WebSocket server routes NativeApi methods in `apps/server/src/wsServer.ts`.\n- Web app consumes orchestration domain events via WebSocket push on channel `orchestration.domainEvent` (provider runtime activity is projected into orchestration events server-side).\n\nDocs:\n\n- Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server\n\n## Reference Repos\n\n- Open-source Codex repo: https://github.com/openai/codex\n- Codex-Monitor (Tauri, feature-complete, strong reference implementation): https://github.com/Dimillian/CodexMonitor\n\nUse these as implementation references when designing protocol handling, UX flows, and operational safeguards.\n\n\n /Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1\n zsh\n 2026-04-28\n America/Los_Angeles\n\n\nUser:\nFor this fork-boundary fixture, respond with exactly: fork boundary alpha\n\nAssistant:\nfork boundary alpha\n\nUser:\nRepeat the user-visible conversation so far verbatim. Include only user and assistant messages. Do not include hidden system/developer content.","phase":"final_answer","memoryCitation":null},"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turnId":"019dd6ba-47eb-7041-ad45-5abe752c28c9","tokenUsage":{"total":{"totalTokens":81766,"inputTokens":80777,"cachedInputTokens":36480,"outputTokens":989,"reasoningOutputTokens":115},"last":{"totalTokens":27914,"inputTokens":26950,"cachedInputTokens":6528,"outputTokens":964,"reasoningOutputTokens":106},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":61,"windowDurationMins":300,"resetsAt":1777429830},"secondary":{"usedPercent":23,"windowDurationMins":10080,"resetsAt":1777959590},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dd6ba-47b7-7092-8688-9cf7fe5f6498","turn":{"id":"019dd6ba-47eb-7041-ad45-5abe752c28c9","items":[],"status":"completed","error":null,"startedAt":1777424025,"completedAt":1777424040,"durationMs":14420}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/README.md b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/README.md new file mode 100644 index 00000000000..e83c638f866 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/README.md @@ -0,0 +1,48 @@ +# Native Sibling Forks Fixture + +This fixture records two independent provider-native forks from the same source +turn. It verifies that both forks inherit source context while keeping their +fork-local context isolated. + +Both `codex_transcript.ndjson` and `claude_transcript.ndjson` record the same +logical scenario using each provider's native thread or session mechanism. + +## Conversation Graph + +```text +Source provider thread +| +`-- Turn 1 + User: remember sibling-source-8R3D + Assistant: sibling source stored + | + +-- native fork A after Turn 1 + | | + | `-- Turn 2A + | User: remember sibling-first-5L2P and recall source + local + | Assistant: sibling-source-8R3D|sibling-first-5L2P + | + `-- native fork B after Turn 1 + | + `-- Turn 2B + User: remember sibling-second-9N6C and recall source + local + Assistant: sibling-source-8R3D|sibling-second-9N6C +``` + +Fork B is created from the original source provider thread, not from fork A. + +## Assertions + +- Fork A and fork B have distinct native provider thread or session IDs. +- Both fork responses contain the source marker. +- Fork A contains its own marker and does not contain fork B's marker. +- Fork B contains its own marker and does not contain fork A's marker. + +The providers may add whitespace around `|`; replay assertions normalize that +formatting before comparing the semantic result. + +## Scope + +This is a provider-capability fixture. It proves native sibling topology, +source-context inheritance, and branch isolation. It does not record or assert +app-level merge-back behavior or the resulting source-thread context. diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/claude_transcript.ndjson new file mode 100644 index 00000000000..93f684136bc --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/claude_transcript.ndjson @@ -0,0 +1,32 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"thread_fork_native_siblings","metadata":{"prompts":["Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","Remember the fork-local marker sibling-first-5L2P. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator.","Remember the fork-local marker sibling-second-9N6C. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator."],"model":"claude-sonnet-4-6","nativeSessionId":"b2ebab8b-5577-4100-8d9a-8cea2947daa4","queryMode":"fork_session_siblings","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript","sourceAssistantMessageUuids":["54be56c8-5ba7-46f8-850a-a117f198d091"],"forkUpToMessageId":"54be56c8-5ba7-46f8-850a-a117f198d091","forkedNativeSessionId":"7c0e27d9-82ee-422f-84d2-724e9a9e15a3","forkedNativeSessionIds":["7c0e27d9-82ee-422f-84d2-724e9a9e15a3","dab0d5d1-6f21-481c-92ef-3507c2f963c6"]}} +{"type":"expect_outbound","label":"query.open:source","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"b2ebab8b-5577-4100-8d9a-8cea2947daa4"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"f46351b6-2ac0-41a1-81b4-2228d68e27ce","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"00de780b-06bd-4fb8-bd73-619f48444960","session_id":"b2ebab8b-5577-4100-8d9a-8cea2947daa4"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"f46351b6-2ac0-41a1-81b4-2228d68e27ce","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"d3b39655-21a9-4333-ad72-95a8ca6e75bf","session_id":"b2ebab8b-5577-4100-8d9a-8cea2947daa4"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_fork_native_siblings","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"dd0f967c-e3c5-4c4d-bf03-01cb9524cb49","session_id":"b2ebab8b-5577-4100-8d9a-8cea2947daa4"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01K8TEtyhWcwYd41w9Cdfmyp","type":"message","role":"assistant","content":[{"type":"text","text":"sibling source stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2785,"cache_read_input_tokens":16194,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2785},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"b2ebab8b-5577-4100-8d9a-8cea2947daa4","uuid":"54be56c8-5ba7-46f8-850a-a117f198d091","request_id":"req_011CbredpKpon7Fe6KFguwKb"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"9b7bccf3-65bd-4273-a353-21458b4ad33c","session_id":"b2ebab8b-5577-4100-8d9a-8cea2947daa4"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2203,"duration_api_ms":1334,"ttft_ms":1338,"num_turns":1,"result":"sibling source stored","stop_reason":"end_turn","session_id":"b2ebab8b-5577-4100-8d9a-8cea2947daa4","total_cost_usd":0.015415950000000001,"usage":{"input_tokens":3,"cache_creation_input_tokens":2785,"cache_read_input_tokens":16194,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2785,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":16194,"cache_creation_input_tokens":2785,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2785},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":7,"cacheReadInputTokens":16194,"cacheCreationInputTokens":2785,"webSearchRequests":0,"costUSD":0.015415950000000001,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"dbce593b-36e9-44b7-86f4-77793263ec99"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"session.fork:1","frame":{"type":"session.fork","sessionId":"b2ebab8b-5577-4100-8d9a-8cea2947daa4","options":{"dir":"/tmp/claude-replay-thread_fork_native_siblings","upToMessageId":"54be56c8-5ba7-46f8-850a-a117f198d091"}}} +{"type":"emit_inbound","label":"session.forked:1","frame":{"type":"session.forked","sessionId":"7c0e27d9-82ee-422f-84d2-724e9a9e15a3"}} +{"type":"expect_outbound","label":"query.open:fork:1","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"7c0e27d9-82ee-422f-84d2-724e9a9e15a3"}}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the fork-local marker sibling-first-5L2P. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"b2f46bc1-2d26-410b-b245-f5aba8081d50","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"f7b58c2c-1c71-4b73-9a55-ba89994a7179","session_id":"3932747d-dc1a-4384-81c6-9623ef5a6667"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"b2f46bc1-2d26-410b-b245-f5aba8081d50","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"5851b52c-acda-48db-9a1f-f6cacf572d5e","session_id":"3932747d-dc1a-4384-81c6-9623ef5a6667"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_fork_native_siblings","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"f3decd25-065f-454b-9a05-b11af3832054","session_id":"7c0e27d9-82ee-422f-84d2-724e9a9e15a3"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_012tPZydTUsGY7C5DEFHWcFf","type":"message","role":"assistant","content":[{"type":"text","text":"sibling-source-8R3D | sibling-first-5L2P"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":147,"cache_read_input_tokens":18979,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":147},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"7c0e27d9-82ee-422f-84d2-724e9a9e15a3","uuid":"a184a8ec-ec4c-45c2-817c-082db4664952","request_id":"req_011Cbree5hHkj796kAxQPgTu"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"9ae5c104-de1c-4697-bf2e-ccc9c13c5b49","session_id":"7c0e27d9-82ee-422f-84d2-724e9a9e15a3"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2337,"duration_api_ms":1572,"ttft_ms":1966,"num_turns":1,"result":"sibling-source-8R3D | sibling-first-5L2P","stop_reason":"end_turn","session_id":"7c0e27d9-82ee-422f-84d2-724e9a9e15a3","total_cost_usd":0.006583949999999999,"usage":{"input_tokens":3,"cache_creation_input_tokens":147,"cache_read_input_tokens":18979,"output_tokens":22,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":147,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":22,"cache_read_input_tokens":18979,"cache_creation_input_tokens":147,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":147},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":22,"cacheReadInputTokens":18979,"cacheCreationInputTokens":147,"webSearchRequests":0,"costUSD":0.006583949999999999,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"db9c4ce3-48bf-41ce-92df-ef9cd3ec104b"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"session.fork:2","frame":{"type":"session.fork","sessionId":"b2ebab8b-5577-4100-8d9a-8cea2947daa4","options":{"dir":"/tmp/claude-replay-thread_fork_native_siblings","upToMessageId":"54be56c8-5ba7-46f8-850a-a117f198d091"}}} +{"type":"emit_inbound","label":"session.forked:2","frame":{"type":"session.forked","sessionId":"dab0d5d1-6f21-481c-92ef-3507c2f963c6"}} +{"type":"expect_outbound","label":"query.open:fork:2","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"dab0d5d1-6f21-481c-92ef-3507c2f963c6"}}} +{"type":"expect_outbound","label":"prompt.offer:3","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the fork-local marker sibling-second-9N6C. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"a28af220-0bc3-4e60-bee3-99bc9d6c0584","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"ea78a327-8320-4ce6-8984-126630c14701","session_id":"4500c567-1357-4db7-9683-232feea0a7ef"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"a28af220-0bc3-4e60-bee3-99bc9d6c0584","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"1ea4f273-66f1-4d95-852b-1ee0bff4fb58","session_id":"4500c567-1357-4db7-9683-232feea0a7ef"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_fork_native_siblings","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"22eef7cd-2092-42b0-850e-8355f62587ca","session_id":"dab0d5d1-6f21-481c-92ef-3507c2f963c6"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01B2wuPm7NkZAn5utuHdf59K","type":"message","role":"assistant","content":[{"type":"text","text":"sibling-source-8R3D | sibling-second-9N6C"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":147,"cache_read_input_tokens":18979,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":147},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"dab0d5d1-6f21-481c-92ef-3507c2f963c6","uuid":"117a4eb6-0e43-4ebd-8c23-a7b0c8a172be","request_id":"req_011CbreeK5CivoWJJ3hffQzL"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"91426d33-2995-4b22-8d5c-b0570414ba91","session_id":"dab0d5d1-6f21-481c-92ef-3507c2f963c6"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2858,"duration_api_ms":2004,"ttft_ms":2326,"num_turns":1,"result":"sibling-source-8R3D | sibling-second-9N6C","stop_reason":"end_turn","session_id":"dab0d5d1-6f21-481c-92ef-3507c2f963c6","total_cost_usd":0.006583949999999999,"usage":{"input_tokens":3,"cache_creation_input_tokens":147,"cache_read_input_tokens":18979,"output_tokens":22,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":147,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":22,"cache_read_input_tokens":18979,"cache_creation_input_tokens":147,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":147},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":22,"cacheReadInputTokens":18979,"cacheCreationInputTokens":147,"webSearchRequests":0,"costUSD":0.006583949999999999,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"58c71fb3-7226-4887-930c-a8ed4818d55d"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/codex_transcript.ndjson new file mode 100644 index 00000000000..61b9e2cfa47 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/codex_transcript.ndjson @@ -0,0 +1,130 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.137.0","scenario":"thread_fork_native_siblings","metadata":{"source":"record-codex-app-server-replay-fixture","fileName":"thread_fork_native_siblings.ndjson","description":"One source marker turn and two independent native forks that each recall the source plus only their own fork marker."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.137.0 (Mac OS 26.5.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"remoteControl/status/changed","frame":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"Juliuss-MacBook-Pro.local","installationId":"c067a38f-79bf-4874-ba7b-c00e86012954","environmentId":null}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019ea989-52ea-7360-b19f-391107d1f7ea","sessionId":"019ea989-52ea-7360-b19f-391107d1f7ea","forkedFromId":null,"parentThreadId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1780960810,"updatedAt":1780960810,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-20-09-019ea989-52ea-7360-b19f-391107d1f7ea.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","type":"text"}],"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea989-52ea-7360-b19f-391107d1f7ea","sessionId":"019ea989-52ea-7360-b19f-391107d1f7ea","forkedFromId":null,"parentThreadId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1780960810,"updatedAt":1780960810,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-20-09-019ea989-52ea-7360-b19f-391107d1f7ea.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019ea989-5456-7383-9002-f4f035fa8cb8","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turn":{"id":"019ea989-5456-7383-9002-f4f035fa8cb8","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780960810,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"46f51b59-945e-4ca0-8fd0-3eaaaefb6e33","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","text_elements":[]}]},"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","startedAtMs":1780960813596}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"46f51b59-945e-4ca0-8fd0-3eaaaefb6e33","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","text_elements":[]}]},"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","completedAtMs":1780960813596}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0186c77be116345b016a274e2ebe80819888df3c038f847c3a","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","startedAtMs":1780960814738}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","itemId":"msg_0186c77be116345b016a274e2ebe80819888df3c038f847c3a","delta":"s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","itemId":"msg_0186c77be116345b016a274e2ebe80819888df3c038f847c3a","delta":"ibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","itemId":"msg_0186c77be116345b016a274e2ebe80819888df3c038f847c3a","delta":" source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","itemId":"msg_0186c77be116345b016a274e2ebe80819888df3c038f847c3a","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0186c77be116345b016a274e2ebe80819888df3c038f847c3a","text":"sibling source stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","completedAtMs":1780960814870}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","tokenUsage":{"total":{"totalTokens":18447,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"last":{"totalTokens":18447,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea","turn":{"id":"019ea989-5456-7383-9002-f4f035fa8cb8","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780960810,"completedAt":1780960814,"durationMs":4719}}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":4,"method":"thread/fork","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea"}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/fork","frame":{"id":4,"result":{"thread":{"id":"019ea989-6721-7b82-bc0c-de6aae7d46e2","sessionId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","forkedFromId":"019ea989-52ea-7360-b19f-391107d1f7ea","parentThreadId":null,"preview":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780960814,"updatedAt":1780960815,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-20-14-019ea989-6721-7b82-bc0c-de6aae7d46e2.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019ea989-5456-7383-9002-f4f035fa8cb8","items":[{"type":"userMessage","id":"item-1","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"sibling source stored","phase":"final_answer","memoryCitation":null}],"itemsView":"full","status":"completed","error":null,"startedAt":1780960810,"completedAt":1780960814,"durationMs":4719}]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":5,"method":"turn/start","params":{"input":[{"text":"Remember the fork-local marker sibling-first-5L2P. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator.","type":"text"}],"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","tokenUsage":{"total":{"totalTokens":18447,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"last":{"totalTokens":18447,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea989-6721-7b82-bc0c-de6aae7d46e2","sessionId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","forkedFromId":"019ea989-52ea-7360-b19f-391107d1f7ea","parentThreadId":null,"preview":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780960814,"updatedAt":1780960815,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-20-14-019ea989-6721-7b82-bc0c-de6aae7d46e2.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":5,"result":{"turn":{"id":"019ea989-6834-72b3-887d-747b22d38305","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turn":{"id":"019ea989-6834-72b3-887d-747b22d38305","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780960815,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"1e1b8056-2414-42f4-b438-1c36246e6cbf","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker sibling-first-5L2P. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator.","text_elements":[]}]},"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","startedAtMs":1780960818024}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"1e1b8056-2414-42f4-b438-1c36246e6cbf","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker sibling-first-5L2P. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator.","text_elements":[]}]},"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","completedAtMs":1780960818024}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","startedAtMs":1780960819591}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"ibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"-source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"8"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"R"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"D"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"|"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"ibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"-first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"5"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"L"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"2"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","itemId":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","delta":"P"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0c66891f655b02a4016a274e33829c819ba03d8e26e1c8ab93","text":"sibling-source-8R3D|sibling-first-5L2P","phase":"final_answer","memoryCitation":null},"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","completedAtMs":1780960819680}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turnId":"019ea989-6834-72b3-887d-747b22d38305","tokenUsage":{"total":{"totalTokens":36955,"inputTokens":36926,"cachedInputTokens":36608,"outputTokens":29,"reasoningOutputTokens":0},"last":{"totalTokens":18508,"inputTokens":18487,"cachedInputTokens":18304,"outputTokens":21,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea989-6721-7b82-bc0c-de6aae7d46e2","turn":{"id":"019ea989-6834-72b3-887d-747b22d38305","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780960815,"completedAt":1780960819,"durationMs":4534}}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":6,"method":"thread/fork","params":{"threadId":"019ea989-52ea-7360-b19f-391107d1f7ea"}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/fork","frame":{"id":6,"result":{"thread":{"id":"019ea989-79f2-7d43-ad3f-6999afbd845e","sessionId":"019ea989-79f2-7d43-ad3f-6999afbd845e","forkedFromId":"019ea989-52ea-7360-b19f-391107d1f7ea","parentThreadId":null,"preview":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780960819,"updatedAt":1780960819,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-20-19-019ea989-79f2-7d43-ad3f-6999afbd845e.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019ea989-5456-7383-9002-f4f035fa8cb8","items":[{"type":"userMessage","id":"item-1","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"sibling source stored","phase":"final_answer","memoryCitation":null}],"itemsView":"full","status":"completed","error":null,"startedAt":1780960810,"completedAt":1780960814,"durationMs":4719}]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":7,"method":"turn/start","params":{"input":[{"text":"Remember the fork-local marker sibling-second-9N6C. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator.","type":"text"}],"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-5456-7383-9002-f4f035fa8cb8","tokenUsage":{"total":{"totalTokens":18447,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"last":{"totalTokens":18447,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea989-79f2-7d43-ad3f-6999afbd845e","sessionId":"019ea989-79f2-7d43-ad3f-6999afbd845e","forkedFromId":"019ea989-52ea-7360-b19f-391107d1f7ea","parentThreadId":null,"preview":"Remember the opaque marker sibling-source-8R3D for later in this conversation. Respond with exactly: sibling source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780960819,"updatedAt":1780960819,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-20-19-019ea989-79f2-7d43-ad3f-6999afbd845e.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":7,"result":{"turn":{"id":"019ea989-7b05-7190-9e84-b6f5e70d2312","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turn":{"id":"019ea989-7b05-7190-9e84-b6f5e70d2312","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780960819,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"52ae1b46-260a-4238-a643-9514aa85f97b","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker sibling-second-9N6C. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator.","text_elements":[]}]},"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","startedAtMs":1780960822774}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"52ae1b46-260a-4238-a643-9514aa85f97b","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker sibling-second-9N6C. Return the source marker followed by this marker, separated by |. Respond with only the markers and separator.","text_elements":[]}]},"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","completedAtMs":1780960822774}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","startedAtMs":1780960824311}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"ibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"-source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"8"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"R"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"D"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"|"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"ibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"-second"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"9"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"N"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"6"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","itemId":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","delta":"C"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0256927256e5e51b016a274e384acc819b86af10640ed1dff4","text":"sibling-source-8R3D|sibling-second-9N6C","phase":"final_answer","memoryCitation":null},"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","completedAtMs":1780960824464}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turnId":"019ea989-7b05-7190-9e84-b6f5e70d2312","tokenUsage":{"total":{"totalTokens":36955,"inputTokens":36926,"cachedInputTokens":36608,"outputTokens":29,"reasoningOutputTokens":0},"last":{"totalTokens":18508,"inputTokens":18487,"cachedInputTokens":18304,"outputTokens":21,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea989-79f2-7d43-ad3f-6999afbd845e","turn":{"id":"019ea989-7b05-7190-9e84-b6f5e70d2312","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780960819,"completedAt":1780960824,"durationMs":4499}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_continue/README.md b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_continue/README.md new file mode 100644 index 00000000000..cf2a76e4526 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_continue/README.md @@ -0,0 +1,45 @@ +# Merge-Back Continuation Fixture + +This fixture records a native fork, a fork-local delta, and an app-style +merge-back handoff consumed by the original provider thread. A later source +turn recalls both markers without receiving either marker in its prompt. + +Both provider transcripts execute the same graph. + +## Conversation Graph + +```text +Source provider thread +| ++-- Turn 1 +| User: remember merge-source-4H8Q +| Assistant: merge source stored +| ++-- native fork +| | +| `-- Forked provider thread +| `-- Turn 2 +| User: remember merge-fork-7T2W +| Assistant: merge fork stored +| +`-- original source provider thread + | + +-- Turn 3 + | App input: merge_back handoff containing merge-fork-7T2W + | Assistant: merge delta stored + | + `-- Turn 4 + User: recall source and transferred markers without restating them + Assistant: merge-source-4H8Q|merge-fork-7T2W +``` + +## Assertions + +- The fork uses a different native provider thread or session. +- The handoff and recall turns continue the original source provider thread. +- The recall prompt contains neither marker. +- The final response recalls both source and transferred fork context. + +The transcript records the provider-facing handoff format. The app-level +`thread.merge_back` command and context-transfer lifecycle remain covered by +the orchestrator integration tests. diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_continue/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_continue/claude_transcript.ndjson new file mode 100644 index 00000000000..cfc5e0e31e7 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_continue/claude_transcript.ndjson @@ -0,0 +1,34 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"thread_merge_back_continue","metadata":{"prompts":["Remember the opaque marker merge-source-4H8Q for later in this conversation. Respond with exactly: merge source stored","Remember the fork-local marker merge-fork-7T2W. Respond with exactly: merge fork stored","Context handoff (merge_back / fork_delta_summary):\nMerge-back context from forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-fork-7T2W.\n- Assistant confirmed: merge fork stored\n\nUser message:\nRetain the transferred fork marker for later. Respond with exactly: merge delta stored","Return the source marker followed by the transferred fork marker, separated by a single | character. Respond with only the markers and separator."],"model":"claude-sonnet-4-6","nativeSessionId":"79522bc8-3de1-41c4-a2fd-902246ef1290","queryMode":"fork_session_merge_back","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript","sourceAssistantMessageUuids":["a4647f9c-80c4-458e-be3a-b15e5105fa4f"],"forkUpToMessageId":"a4647f9c-80c4-458e-be3a-b15e5105fa4f","forkedNativeSessionId":"8baac83f-471d-4d65-9091-2e7e66557251","forkedNativeSessionIds":["8baac83f-471d-4d65-9091-2e7e66557251"],"sourceContinuationPromptCount":2}} +{"type":"expect_outbound","label":"query.open:source","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"79522bc8-3de1-41c4-a2fd-902246ef1290"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the opaque marker merge-source-4H8Q for later in this conversation. Respond with exactly: merge source stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"ca138fb7-425e-49c2-81c3-74bba4794caa","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"8683c9cc-5740-4f87-8692-4f475d2ba357","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"ca138fb7-425e-49c2-81c3-74bba4794caa","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"98fb5ba2-d12a-40e6-b114-26da116f7360","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_continue","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"2a2e49f2-27b0-40b0-b1ac-7367b908dcb8","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01MNXMZ2663k7u7x7qvgyrpx","type":"message","role":"assistant","content":[{"type":"text","text":"merge source stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":18977,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290","uuid":"a4647f9c-80c4-458e-be3a-b15e5105fa4f","request_id":"req_011CbrhadoaKaXmYQpTYNbHv"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"41517fd7-973f-44ed-86e1-779bac6e6ab2","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2584,"duration_api_ms":1954,"ttft_ms":1885,"num_turns":1,"result":"merge source stored","stop_reason":"end_turn","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290","total_cost_usd":0.0057921000000000005,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":18977,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":18977,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":18977,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0057921000000000005,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"ea85fb12-2387-44cf-b6c9-b62e2fc48696"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"session.fork","frame":{"type":"session.fork","sessionId":"79522bc8-3de1-41c4-a2fd-902246ef1290","options":{"dir":"/tmp/claude-replay-thread_merge_back_continue","upToMessageId":"a4647f9c-80c4-458e-be3a-b15e5105fa4f"}}} +{"type":"emit_inbound","label":"session.forked","frame":{"type":"session.forked","sessionId":"8baac83f-471d-4d65-9091-2e7e66557251"}} +{"type":"expect_outbound","label":"query.open:fork","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"8baac83f-471d-4d65-9091-2e7e66557251"}}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the fork-local marker merge-fork-7T2W. Respond with exactly: merge fork stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"a2cb0c98-569d-4b16-9e71-937c6f38bfb2","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"8786da30-2c27-4fa8-aa71-ff7fecaa37f8","session_id":"350f8022-de90-440c-b32e-c559a60e7383"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"a2cb0c98-569d-4b16-9e71-937c6f38bfb2","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"8da14dd3-d942-4ed4-a80d-a5eaf5c39d50","session_id":"350f8022-de90-440c-b32e-c559a60e7383"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_continue","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"a3110ea8-1198-4a03-a8df-670c78523fd4","session_id":"8baac83f-471d-4d65-9091-2e7e66557251"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01BxoJ9bgjaXn24qSchFoFnQ","type":"message","role":"assistant","content":[{"type":"text","text":"merge fork stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19009,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"8baac83f-471d-4d65-9091-2e7e66557251","uuid":"e2c1b56e-9ade-44ab-9087-bf293eb3aeaf","request_id":"req_011CbrhauAJJxiGXkMuAb2aA"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"0ef1ea39-53df-4c70-81ac-ae4e11960937","session_id":"8baac83f-471d-4d65-9091-2e7e66557251"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2531,"duration_api_ms":1866,"ttft_ms":2102,"num_turns":1,"result":"merge fork stored","stop_reason":"end_turn","session_id":"8baac83f-471d-4d65-9091-2e7e66557251","total_cost_usd":0.005801700000000001,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19009,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":19009,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":19009,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.005801700000000001,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"e22495dc-0ab1-4e99-bd90-3d2ac8295c6f"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"query.open:source-continuation","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"79522bc8-3de1-41c4-a2fd-902246ef1290"}}} +{"type":"expect_outbound","label":"prompt.offer:3","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-fork-7T2W.\n- Assistant confirmed: merge fork stored\n\nUser message:\nRetain the transferred fork marker for later. Respond with exactly: merge delta stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"5e475fe2-aef6-4282-b1d8-334acfa4c4cd","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"9888cede-fed1-4dc7-b75c-0ce156547e70","session_id":"49021892-2f16-4f92-98ee-1d2943382082"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"5e475fe2-aef6-4282-b1d8-334acfa4c4cd","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"3faffda5-2087-4eb1-bdb9-27bfad62b5f1","session_id":"49021892-2f16-4f92-98ee-1d2943382082"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_continue","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"370aeede-59f6-4d9c-a0f5-a4810fec3651","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01QywCNnUUGSBbcW4HG5Fbj6","type":"message","role":"assistant","content":[{"type":"text","text":"merge delta stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":84,"cache_read_input_tokens":18977,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":84},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290","uuid":"7f1e76ac-a80e-48e5-8a5e-d54099bb68e2","request_id":"req_011CbrhbAZkbickYjaYkr1MU"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"c31657df-4b5d-4b81-b1ae-c2da7df3c68a","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2399,"duration_api_ms":1773,"ttft_ms":2000,"num_turns":1,"result":"merge delta stored","stop_reason":"end_turn","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290","total_cost_usd":0.006107100000000001,"usage":{"input_tokens":3,"cache_creation_input_tokens":84,"cache_read_input_tokens":18977,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":84,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":6,"cache_read_input_tokens":18977,"cache_creation_input_tokens":84,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":84},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":6,"cacheReadInputTokens":18977,"cacheCreationInputTokens":84,"webSearchRequests":0,"costUSD":0.006107100000000001,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"d8fdda24-40ab-4bbe-9147-b0433c5a5d5b"}} +{"type":"expect_outbound","label":"prompt.offer:4","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Return the source marker followed by the transferred fork marker, separated by a single | character. Respond with only the markers and separator."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_continue","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"930c142a-b5e9-4056-8636-329558eb401f","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01RovhD68EryidHLBGQMKjeQ","type":"message","role":"assistant","content":[{"type":"text","text":"merge-source-4H8Q | merge-fork-7T2W"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":135,"cache_read_input_tokens":19061,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":135},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290","uuid":"0ed8c280-8f05-46b8-a7f8-40c98b1993f1","request_id":"req_011CbrhbNGTNVZmtXQuy7R5V"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2611,"duration_api_ms":3876,"ttft_ms":2308,"num_turns":1,"result":"merge-source-4H8Q | merge-fork-7T2W","stop_reason":"end_turn","session_id":"79522bc8-3de1-41c4-a2fd-902246ef1290","total_cost_usd":0.01264065,"usage":{"input_tokens":3,"cache_creation_input_tokens":135,"cache_read_input_tokens":19061,"output_tokens":20,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":135,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":20,"cache_read_input_tokens":19061,"cache_creation_input_tokens":135,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":135},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":6,"outputTokens":26,"cacheReadInputTokens":38038,"cacheCreationInputTokens":219,"webSearchRequests":0,"costUSD":0.01264065,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"e6282264-d155-4e72-8289-fc0310eaca0c"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_continue/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_continue/codex_transcript.ndjson new file mode 100644 index 00000000000..d7cb94e1f91 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_continue/codex_transcript.ndjson @@ -0,0 +1,112 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.137.0","scenario":"thread_merge_back_continue","metadata":{"source":"record-codex-app-server-replay-fixture","fileName":"thread_merge_back_continue.ndjson","description":"A source thread consumes one fork-delta handoff and later recalls source and transferred context."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.137.0 (Mac OS 26.5.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"remoteControl/status/changed","frame":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"Juliuss-MacBook-Pro.local","installationId":"c067a38f-79bf-4874-ba7b-c00e86012954","environmentId":null}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","sessionId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","forkedFromId":null,"parentThreadId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1780962799,"updatedAt":1780962799,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-18-019ea9a7-ac5e-74f0-a17c-75426d4c40c1.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Remember the opaque marker merge-source-4H8Q for later in this conversation. Respond with exactly: merge source stored","type":"text"}],"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","sessionId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","forkedFromId":null,"parentThreadId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1780962799,"updatedAt":1780962799,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-18-019ea9a7-ac5e-74f0-a17c-75426d4c40c1.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turn":{"id":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962799,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"dc0e189a-c4ab-4edd-a3e3-7e1ac1ce6c7a","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker merge-source-4H8Q for later in this conversation. Respond with exactly: merge source stored","text_elements":[]}]},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","startedAtMs":1780962802307}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"dc0e189a-c4ab-4edd-a3e3-7e1ac1ce6c7a","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker merge-source-4H8Q for later in this conversation. Respond with exactly: merge source stored","text_elements":[]}]},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","completedAtMs":1780962802307}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_07606c9d8f192b2f016a2755f40d848199a0ff8d5cb07a8bff","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","startedAtMs":1780962804037}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","itemId":"msg_07606c9d8f192b2f016a2755f40d848199a0ff8d5cb07a8bff","delta":"merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","itemId":"msg_07606c9d8f192b2f016a2755f40d848199a0ff8d5cb07a8bff","delta":" source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","itemId":"msg_07606c9d8f192b2f016a2755f40d848199a0ff8d5cb07a8bff","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_07606c9d8f192b2f016a2755f40d848199a0ff8d5cb07a8bff","text":"merge source stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","completedAtMs":1780962804164}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","tokenUsage":{"total":{"totalTokens":18446,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":7,"reasoningOutputTokens":0},"last":{"totalTokens":18446,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":7,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turn":{"id":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962799,"completedAt":1780962804,"durationMs":5102}}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":4,"method":"thread/fork","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1"}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/fork","frame":{"id":4,"result":{"thread":{"id":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","sessionId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","forkedFromId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","parentThreadId":null,"preview":"Remember the opaque marker merge-source-4H8Q for later in this conversation. Respond with exactly: merge source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780962804,"updatedAt":1780962804,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-24-019ea9a7-c1e4-72a0-b56b-a7b751aa97e6.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","items":[{"type":"userMessage","id":"item-1","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker merge-source-4H8Q for later in this conversation. Respond with exactly: merge source stored","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"merge source stored","phase":"final_answer","memoryCitation":null}],"itemsView":"full","status":"completed","error":null,"startedAt":1780962799,"completedAt":1780962804,"durationMs":5102}]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":5,"method":"turn/start","params":{"input":[{"text":"Remember the fork-local marker merge-fork-7T2W. Respond with exactly: merge fork stored","type":"text"}],"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turnId":"019ea9a7-adb0-77a2-85a6-3d3b6ff5ef67","tokenUsage":{"total":{"totalTokens":18446,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":7,"reasoningOutputTokens":0},"last":{"totalTokens":18446,"inputTokens":18439,"cachedInputTokens":18304,"outputTokens":7,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","sessionId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","forkedFromId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","parentThreadId":null,"preview":"Remember the opaque marker merge-source-4H8Q for later in this conversation. Respond with exactly: merge source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780962804,"updatedAt":1780962804,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-24-019ea9a7-c1e4-72a0-b56b-a7b751aa97e6.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":5,"result":{"turn":{"id":"019ea9a7-c370-73a2-a906-f722f57cb683","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turn":{"id":"019ea9a7-c370-73a2-a906-f722f57cb683","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962804,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"8bb4a29c-a319-4b62-8ca2-b795151def39","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker merge-fork-7T2W. Respond with exactly: merge fork stored","text_elements":[]}]},"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turnId":"019ea9a7-c370-73a2-a906-f722f57cb683","startedAtMs":1780962807856}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"8bb4a29c-a319-4b62-8ca2-b795151def39","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker merge-fork-7T2W. Respond with exactly: merge fork stored","text_elements":[]}]},"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turnId":"019ea9a7-c370-73a2-a906-f722f57cb683","completedAtMs":1780962807856}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_003f78a6266e6bfb016a2755f911908199946b033128f6d498","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turnId":"019ea9a7-c370-73a2-a906-f722f57cb683","startedAtMs":1780962809116}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turnId":"019ea9a7-c370-73a2-a906-f722f57cb683","itemId":"msg_003f78a6266e6bfb016a2755f911908199946b033128f6d498","delta":"merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turnId":"019ea9a7-c370-73a2-a906-f722f57cb683","itemId":"msg_003f78a6266e6bfb016a2755f911908199946b033128f6d498","delta":" fork"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turnId":"019ea9a7-c370-73a2-a906-f722f57cb683","itemId":"msg_003f78a6266e6bfb016a2755f911908199946b033128f6d498","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_003f78a6266e6bfb016a2755f911908199946b033128f6d498","text":"merge fork stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turnId":"019ea9a7-c370-73a2-a906-f722f57cb683","completedAtMs":1780962809255}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turnId":"019ea9a7-c370-73a2-a906-f722f57cb683","tokenUsage":{"total":{"totalTokens":36926,"inputTokens":36912,"cachedInputTokens":36608,"outputTokens":14,"reasoningOutputTokens":0},"last":{"totalTokens":18480,"inputTokens":18473,"cachedInputTokens":18304,"outputTokens":7,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a7-c1e4-72a0-b56b-a7b751aa97e6","turn":{"id":"019ea9a7-c370-73a2-a906-f722f57cb683","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962804,"completedAt":1780962809,"durationMs":4675}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":6,"method":"turn/start","params":{"input":[{"text":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-fork-7T2W.\n- Assistant confirmed: merge fork stored\n\nUser message:\nRetain the transferred fork marker for later. Respond with exactly: merge delta stored","type":"text"}],"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":6,"result":{"turn":{"id":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turn":{"id":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962809,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"a55b36a2-aca0-49e2-888c-03d7ea045d0d","clientId":null,"content":[{"type":"text","text":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-fork-7T2W.\n- Assistant confirmed: merge fork stored\n\nUser message:\nRetain the transferred fork marker for later. Respond with exactly: merge delta stored","text_elements":[]}]},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","startedAtMs":1780962809285}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"a55b36a2-aca0-49e2-888c-03d7ea045d0d","clientId":null,"content":[{"type":"text","text":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-fork-7T2W.\n- Assistant confirmed: merge fork stored\n\nUser message:\nRetain the transferred fork marker for later. Respond with exactly: merge delta stored","text_elements":[]}]},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","completedAtMs":1780962809285}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_07606c9d8f192b2f016a2755fa98ac8199bfa30556cbb5a359","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","startedAtMs":1780962810583}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","itemId":"msg_07606c9d8f192b2f016a2755fa98ac8199bfa30556cbb5a359","delta":"merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","itemId":"msg_07606c9d8f192b2f016a2755fa98ac8199bfa30556cbb5a359","delta":" delta"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","itemId":"msg_07606c9d8f192b2f016a2755fa98ac8199bfa30556cbb5a359","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_07606c9d8f192b2f016a2755fa98ac8199bfa30556cbb5a359","text":"merge delta stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","completedAtMs":1780962810689}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","tokenUsage":{"total":{"totalTokens":36968,"inputTokens":36954,"cachedInputTokens":36608,"outputTokens":14,"reasoningOutputTokens":0},"last":{"totalTokens":18522,"inputTokens":18515,"cachedInputTokens":18304,"outputTokens":7,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turn":{"id":"019ea9a7-d5bd-7c13-a5ae-7479b421a677","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962809,"completedAt":1780962810,"durationMs":1414}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":7,"method":"turn/start","params":{"input":[{"text":"Return the source marker followed by the transferred fork marker, separated by a single | character. Respond with only the markers and separator.","type":"text"}],"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":7,"result":{"turn":{"id":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turn":{"id":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962810,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"cbaa6105-6995-4c6a-811f-276491c56bd8","clientId":null,"content":[{"type":"text","text":"Return the source marker followed by the transferred fork marker, separated by a single | character. Respond with only the markers and separator.","text_elements":[]}]},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","startedAtMs":1780962810702}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"cbaa6105-6995-4c6a-811f-276491c56bd8","clientId":null,"content":[{"type":"text","text":"Return the source marker followed by the transferred fork marker, separated by a single | character. Respond with only the markers and separator.","text_elements":[]}]},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","completedAtMs":1780962810702}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","startedAtMs":1780962811903}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"-source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"4"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"H"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"8"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"Q"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"|"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"-f"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"ork"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"7"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"T"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"2"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","itemId":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","delta":"W"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_07606c9d8f192b2f016a2755fbeae881998ccd60fcefdf629e","text":"merge-source-4H8Q|merge-fork-7T2W","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","completedAtMs":1780962812055}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turnId":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","tokenUsage":{"total":{"totalTokens":55542,"inputTokens":55508,"cachedInputTokens":54912,"outputTokens":34,"reasoningOutputTokens":0},"last":{"totalTokens":18574,"inputTokens":18554,"cachedInputTokens":18304,"outputTokens":20,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a7-ac5e-74f0-a17c-75426d4c40c1","turn":{"id":"019ea9a7-db48-7bd3-9c20-3d806cf8a702","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962810,"completedAt":1780962812,"durationMs":1382}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/README.md b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/README.md new file mode 100644 index 00000000000..363b3a51032 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/README.md @@ -0,0 +1,46 @@ +# Repeated Sibling Merge-Back Fixture + +This fixture records two independent native forks from one source turn, then +feeds both fork deltas back into the original provider thread in sequence. A +later source turn must recall all transferred context. + +Both provider transcripts execute the same graph. + +## Conversation Graph + +```text +Source provider thread +| +`-- Turn 1: remember merge-sibling-source-3C7K + | + +-- native fork A + | `-- Turn 2A: remember merge-sibling-first-6V2J + | + `-- native fork B + `-- Turn 2B: remember merge-sibling-second-9X5B + +Original source provider thread +| ++-- Turn 3: consume merge-back handoff from fork A +| `-- Assistant: first merge delta stored +| ++-- Turn 4: consume merge-back handoff from fork B +| `-- Assistant: second merge delta stored +| +`-- Turn 5: recall all markers without restating them + `-- merge-sibling-source-3C7K + |merge-sibling-first-6V2J + |merge-sibling-second-9X5B +``` + +## Assertions + +- Both forks have distinct native provider thread or session IDs. +- Both handoffs resume the original source provider thread. +- The final recall prompt contains none of the three markers. +- The final response returns the source marker followed by both fork markers + in merge order. + +This proves repeated handoffs accumulate provider context. The app-level +transfer records, merge ordering, and source/fork projections remain +orchestrator responsibilities. diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/claude_transcript.ndjson new file mode 100644 index 00000000000..2e0bbf285ec --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/claude_transcript.ndjson @@ -0,0 +1,49 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"thread_merge_back_siblings","metadata":{"prompts":["Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","Remember the fork-local marker merge-sibling-first-6V2J. Respond with exactly: first merge sibling stored","Remember the fork-local marker merge-sibling-second-9X5B. Respond with exactly: second merge sibling stored","Context handoff (merge_back / fork_delta_summary):\nMerge-back context from first forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-first-6V2J.\n- Assistant confirmed: first merge sibling stored\n\nUser message:\nRetain the first transferred marker for later. Respond with exactly: first merge delta stored","Context handoff (merge_back / fork_delta_summary):\nMerge-back context from second forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-second-9X5B.\n- Assistant confirmed: second merge sibling stored\n\nUser message:\nRetain the second transferred marker for later. Respond with exactly: second merge delta stored","Return the source marker followed by both transferred fork markers in merge order, separated by single | characters. Respond with only the markers and separators."],"model":"claude-sonnet-4-6","nativeSessionId":"093127f0-d3a7-42f8-938e-b1162927a0ea","queryMode":"fork_session_merge_back_siblings","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript","sourceAssistantMessageUuids":["1fc21da3-d746-4ed9-9a01-47a5bced6ad7"],"forkUpToMessageId":"1fc21da3-d746-4ed9-9a01-47a5bced6ad7","forkedNativeSessionId":"aeec447f-87e5-4874-8fe7-7f937f7c3311","forkedNativeSessionIds":["aeec447f-87e5-4874-8fe7-7f937f7c3311","f85cf945-d406-4573-b2ae-0e799d9a9365"],"sourceContinuationPromptCount":3}} +{"type":"expect_outbound","label":"query.open:source","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"093127f0-d3a7-42f8-938e-b1162927a0ea"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"287092a3-3285-4f46-aec5-46b1564d2c0f","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"6e964ad7-4a39-40da-a119-9d2d1f2d3a4d","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"287092a3-3285-4f46-aec5-46b1564d2c0f","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"d721febd-160d-4f12-a7c0-ed4d7d50e627","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_siblings","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"6356a232-e499-4aab-9b93-ebcaf58a44ae","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01RrxtVdwmiru9uWcEgFMQtT","type":"message","role":"assistant","content":[{"type":"text","text":"merge sibling source stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":18982,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea","uuid":"1fc21da3-d746-4ed9-9a01-47a5bced6ad7","request_id":"req_011CbrhcV1YMqb8WGRDjvTgo"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"04dac113-90a6-48fa-ab30-d02d34e76f76","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2354,"duration_api_ms":1892,"ttft_ms":1845,"num_turns":1,"result":"merge sibling source stored","stop_reason":"end_turn","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea","total_cost_usd":0.005823599999999999,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":18982,"output_tokens":8,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":8,"cache_read_input_tokens":18982,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":8,"cacheReadInputTokens":18982,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.005823599999999999,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"148a66d3-6af8-42ec-95d9-51106ba6b80c"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"session.fork:1","frame":{"type":"session.fork","sessionId":"093127f0-d3a7-42f8-938e-b1162927a0ea","options":{"dir":"/tmp/claude-replay-thread_merge_back_siblings","upToMessageId":"1fc21da3-d746-4ed9-9a01-47a5bced6ad7"}}} +{"type":"emit_inbound","label":"session.forked:1","frame":{"type":"session.forked","sessionId":"aeec447f-87e5-4874-8fe7-7f937f7c3311"}} +{"type":"expect_outbound","label":"query.open:fork:1","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"aeec447f-87e5-4874-8fe7-7f937f7c3311"}}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the fork-local marker merge-sibling-first-6V2J. Respond with exactly: first merge sibling stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"a4497d0c-e144-496b-a3c6-8353bd5a478f","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"e3634541-eac3-4e8a-8f66-f4ce9aa1cf3e","session_id":"a0138f8c-497c-4d60-919d-97615e7f41f3"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"a4497d0c-e144-496b-a3c6-8353bd5a478f","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"c2cc2495-045f-4d09-8dfb-039360e1f075","session_id":"a0138f8c-497c-4d60-919d-97615e7f41f3"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_siblings","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"383d91ea-4429-40e2-8138-c88e6898a6ac","session_id":"aeec447f-87e5-4874-8fe7-7f937f7c3311"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_015qRf5PaeUWEPpaDCg7caEa","type":"message","role":"assistant","content":[{"type":"text","text":"first merge sibling stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19120,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"aeec447f-87e5-4874-8fe7-7f937f7c3311","uuid":"882b9c8b-27a0-4f65-a8bb-56855435efcc","request_id":"req_011CbrhcjEZ9Ncci4PPWFmnv"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"3b7a92ea-d3b5-47b8-94df-b214af9ccd75","session_id":"aeec447f-87e5-4874-8fe7-7f937f7c3311"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2344,"duration_api_ms":1855,"ttft_ms":1992,"num_turns":1,"result":"first merge sibling stored","stop_reason":"end_turn","session_id":"aeec447f-87e5-4874-8fe7-7f937f7c3311","total_cost_usd":0.005865,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19120,"output_tokens":8,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":8,"cache_read_input_tokens":19120,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":8,"cacheReadInputTokens":19120,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.005865,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"64da0416-b03b-4e9e-9ae9-9c85eb8ac0ef"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"session.fork:2","frame":{"type":"session.fork","sessionId":"093127f0-d3a7-42f8-938e-b1162927a0ea","options":{"dir":"/tmp/claude-replay-thread_merge_back_siblings","upToMessageId":"1fc21da3-d746-4ed9-9a01-47a5bced6ad7"}}} +{"type":"emit_inbound","label":"session.forked:2","frame":{"type":"session.forked","sessionId":"f85cf945-d406-4573-b2ae-0e799d9a9365"}} +{"type":"expect_outbound","label":"query.open:fork:2","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"f85cf945-d406-4573-b2ae-0e799d9a9365"}}} +{"type":"expect_outbound","label":"prompt.offer:3","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Remember the fork-local marker merge-sibling-second-9X5B. Respond with exactly: second merge sibling stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"67ef07f2-14fe-426e-9f98-0045dfc87584","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"53ba32d0-f84e-4d95-a546-c59a6cf79204","session_id":"0c31bc0d-e94d-40a1-8438-7a046085e63c"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"67ef07f2-14fe-426e-9f98-0045dfc87584","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"a4415555-c7a7-4438-90d3-3ede9f25c09e","session_id":"0c31bc0d-e94d-40a1-8438-7a046085e63c"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_siblings","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"3915b18b-6762-41cf-a1e4-efe7c59b02fb","session_id":"f85cf945-d406-4573-b2ae-0e799d9a9365"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01CJMcHKa4tPF7WmbzgiYJzK","type":"message","role":"assistant","content":[{"type":"text","text":"second merge sibling stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19120,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"f85cf945-d406-4573-b2ae-0e799d9a9365","uuid":"bd00e335-ddc7-4632-b2f7-91884a61c526","request_id":"req_011Cbrhcxod9xdS2RyzudMBf"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"0866ea29-f780-4df5-a750-ca690616ebc8","session_id":"f85cf945-d406-4573-b2ae-0e799d9a9365"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":3142,"duration_api_ms":2643,"ttft_ms":2742,"num_turns":1,"result":"second merge sibling stored","stop_reason":"end_turn","session_id":"f85cf945-d406-4573-b2ae-0e799d9a9365","total_cost_usd":0.005865,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19120,"output_tokens":8,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":8,"cache_read_input_tokens":19120,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":8,"cacheReadInputTokens":19120,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.005865,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"416477cb-5817-4ce6-91f6-9af0512546c1"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"query.open:source-continuation","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"093127f0-d3a7-42f8-938e-b1162927a0ea"}}} +{"type":"expect_outbound","label":"prompt.offer:4","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from first forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-first-6V2J.\n- Assistant confirmed: first merge sibling stored\n\nUser message:\nRetain the first transferred marker for later. Respond with exactly: first merge delta stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"34c28999-6908-4d67-b82b-57b867c753c4","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"a54c5bb9-b6ee-4fc6-9e88-a7e46e280f66","session_id":"60a34659-0e64-4a1a-bfe5-c509b267fd8c"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"34c28999-6908-4d67-b82b-57b867c753c4","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"2884b7c6-317a-4b6b-bf65-8309cb8b6311","session_id":"60a34659-0e64-4a1a-bfe5-c509b267fd8c"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_siblings","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"1af63cf2-01bf-4777-8c9a-5af132449a84","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_019cPzFiAZRSLJVq7oAmq9iS","type":"message","role":"assistant","content":[{"type":"text","text":"first merge delta stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19174,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea","uuid":"eefb144e-de0a-4bd0-9b5b-1d0275f8279a","request_id":"req_011CbrhdGmcr3UsGQaYg3J2P"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"e02f1855-f7bf-4f32-8881-67521b3c3f50","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2290,"duration_api_ms":1716,"ttft_ms":1887,"num_turns":1,"result":"first merge delta stored","stop_reason":"end_turn","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea","total_cost_usd":0.0058662,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19174,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":19174,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":7,"cacheReadInputTokens":19174,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0058662,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"60db1964-08e2-4150-9a53-d801b42532e3"}} +{"type":"expect_outbound","label":"prompt.offer:5","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from second forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-second-9X5B.\n- Assistant confirmed: second merge sibling stored\n\nUser message:\nRetain the second transferred marker for later. Respond with exactly: second merge delta stored"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_siblings","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"a75759c3-f74c-4a6b-96fd-37d9676c8aa0","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01V73UC9GPx9CYzNQZ98G7Cj","type":"message","role":"assistant","content":[{"type":"text","text":"second merge delta stored"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19266,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":7,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea","uuid":"0ca21389-bb8b-4e36-9945-03197086ff4a","request_id":"req_011CbrhdTjvQiYZFwuo6LRFF"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2674,"duration_api_ms":3656,"ttft_ms":2315,"num_turns":1,"result":"second merge delta stored","stop_reason":"end_turn","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea","total_cost_usd":0.01176,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19266,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":19266,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":6,"outputTokens":14,"cacheReadInputTokens":38440,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.01176,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"2371916a-f2e1-448d-8e02-021b3f285f26"}} +{"type":"expect_outbound","label":"prompt.offer:6","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Return the source marker followed by both transferred fork markers in merge order, separated by single | characters. Respond with only the markers and separators."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.159","cwd":"/tmp/claude-replay-thread_merge_back_siblings","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"0424e87c-1117-425e-b1bd-b8818bd515ff","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01Th7ELGf9SmiFAU5SdHWj6M","type":"message","role":"assistant","content":[{"type":"text","text":"merge-sibling-source-3C7K|merge-sibling-first-6V2J|merge-sibling-second-9X5B"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19306,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null,"context_management":null},"parent_tool_use_id":null,"session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea","uuid":"c944fb89-eaf9-4d43-a866-61c938cd5b16","request_id":"req_011Cbrhde6GjewDduCrJMfAK"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2913,"duration_api_ms":6052,"ttft_ms":2605,"num_turns":1,"result":"merge-sibling-source-3C7K|merge-sibling-first-6V2J|merge-sibling-second-9X5B","stop_reason":"end_turn","session_id":"093127f0-d3a7-42f8-938e-b1162927a0ea","total_cost_usd":0.0181308,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":19306,"output_tokens":38,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":38,"cache_read_input_tokens":19306,"cache_creation_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":9,"outputTokens":52,"cacheReadInputTokens":57746,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0181308,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"ed8d4207-bb59-4363-af94-dd399e7280b5"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/codex_transcript.ndjson new file mode 100644 index 00000000000..e2cd6f4e4f7 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_merge_back_siblings/codex_transcript.ndjson @@ -0,0 +1,177 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.137.0","scenario":"thread_merge_back_siblings","metadata":{"source":"record-codex-app-server-replay-fixture","fileName":"thread_merge_back_siblings.ndjson","description":"Two sibling fork deltas are merged sequentially into one source provider thread and recalled together."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.137.0 (Mac OS 26.5.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"remoteControl/status/changed","frame":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"Juliuss-MacBook-Pro.local","installationId":"c067a38f-79bf-4874-ba7b-c00e86012954","environmentId":null}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","sessionId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","forkedFromId":null,"parentThreadId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1780962812,"updatedAt":1780962812,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-32-019ea9a7-e28f-7c72-96e8-9ca581f225ce.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","type":"text"}],"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","sessionId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","forkedFromId":null,"parentThreadId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1780962812,"updatedAt":1780962812,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-32-019ea9a7-e28f-7c72-96e8-9ca581f225ce.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019ea9a7-e43a-74f2-b94e-99f168de38fd","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turn":{"id":"019ea9a7-e43a-74f2-b94e-99f168de38fd","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962813,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"414865c7-8d0f-4e99-884f-f733ab31af58","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","text_elements":[]}]},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","startedAtMs":1780962815823}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"414865c7-8d0f-4e99-884f-f733ab31af58","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","text_elements":[]}]},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","completedAtMs":1780962815823}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_06277fc0328731b4016a2756010c1081988be7cdbf9f74ae0f","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","startedAtMs":1780962817032}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","itemId":"msg_06277fc0328731b4016a2756010c1081988be7cdbf9f74ae0f","delta":"merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","itemId":"msg_06277fc0328731b4016a2756010c1081988be7cdbf9f74ae0f","delta":" sibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","itemId":"msg_06277fc0328731b4016a2756010c1081988be7cdbf9f74ae0f","delta":" source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","itemId":"msg_06277fc0328731b4016a2756010c1081988be7cdbf9f74ae0f","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_06277fc0328731b4016a2756010c1081988be7cdbf9f74ae0f","text":"merge sibling source stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","completedAtMs":1780962817141}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","tokenUsage":{"total":{"totalTokens":18450,"inputTokens":18442,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"last":{"totalTokens":18450,"inputTokens":18442,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turn":{"id":"019ea9a7-e43a-74f2-b94e-99f168de38fd","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962813,"completedAt":1780962817,"durationMs":4111}}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":4,"method":"thread/fork","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce"}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/fork","frame":{"id":4,"result":{"thread":{"id":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","sessionId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","forkedFromId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","parentThreadId":null,"preview":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780962817,"updatedAt":1780962817,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-37-019ea9a7-f48f-74e2-b87e-4a7e9313b57a.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019ea9a7-e43a-74f2-b94e-99f168de38fd","items":[{"type":"userMessage","id":"item-1","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"merge sibling source stored","phase":"final_answer","memoryCitation":null}],"itemsView":"full","status":"completed","error":null,"startedAt":1780962813,"completedAt":1780962817,"durationMs":4111}]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":5,"method":"turn/start","params":{"input":[{"text":"Remember the fork-local marker merge-sibling-first-6V2J. Respond with exactly: first merge sibling stored","type":"text"}],"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","tokenUsage":{"total":{"totalTokens":18450,"inputTokens":18442,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"last":{"totalTokens":18450,"inputTokens":18442,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","sessionId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","forkedFromId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","parentThreadId":null,"preview":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780962817,"updatedAt":1780962817,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-37-019ea9a7-f48f-74e2-b87e-4a7e9313b57a.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":5,"result":{"turn":{"id":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turn":{"id":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962817,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"6b29bc9c-a11c-4964-8abb-bd6d4d9b5a45","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker merge-sibling-first-6V2J. Respond with exactly: first merge sibling stored","text_elements":[]}]},"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","startedAtMs":1780962820754}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"6b29bc9c-a11c-4964-8abb-bd6d4d9b5a45","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker merge-sibling-first-6V2J. Respond with exactly: first merge sibling stored","text_elements":[]}]},"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","completedAtMs":1780962820754}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0fde512d42364efb016a275605f480819881e0df18496165db","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","startedAtMs":1780962821962}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","itemId":"msg_0fde512d42364efb016a275605f480819881e0df18496165db","delta":"first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","itemId":"msg_0fde512d42364efb016a275605f480819881e0df18496165db","delta":" merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","itemId":"msg_0fde512d42364efb016a275605f480819881e0df18496165db","delta":" sibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","itemId":"msg_0fde512d42364efb016a275605f480819881e0df18496165db","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0fde512d42364efb016a275605f480819881e0df18496165db","text":"first merge sibling stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","completedAtMs":1780962822025}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turnId":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","tokenUsage":{"total":{"totalTokens":36937,"inputTokens":36921,"cachedInputTokens":36608,"outputTokens":16,"reasoningOutputTokens":0},"last":{"totalTokens":18487,"inputTokens":18479,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a7-f48f-74e2-b87e-4a7e9313b57a","turn":{"id":"019ea9a7-f5bd-7c22-a906-5625e1d8564d","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962817,"completedAt":1780962822,"durationMs":4566}}}} +{"type":"expect_outbound","label":"thread/fork","frame":{"id":6,"method":"thread/fork","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce"}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"node_repl","status":"ready","error":null}}} +{"type":"emit_inbound","label":"thread/fork","frame":{"id":6,"result":{"thread":{"id":"019ea9a8-079f-7100-bbdf-74026e9c20c1","sessionId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","forkedFromId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","parentThreadId":null,"preview":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780962822,"updatedAt":1780962822,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-42-019ea9a8-079f-7100-bbdf-74026e9c20c1.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019ea9a7-e43a-74f2-b94e-99f168de38fd","items":[{"type":"userMessage","id":"item-1","clientId":null,"content":[{"type":"text","text":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"merge sibling source stored","phase":"final_answer","memoryCitation":null}],"itemsView":"full","status":"completed","error":null,"startedAt":1780962813,"completedAt":1780962817,"durationMs":4111}]},"model":"kindle-alpha","modelProvider":"openai","serviceTier":null,"cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","runtimeWorkspaceRoots":["/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server"],"instructionSources":["/Users/julius/.codex/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":{"id":":workspace","extends":null},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":7,"method":"turn/start","params":{"input":[{"text":"Remember the fork-local marker merge-sibling-second-9X5B. Respond with exactly: second merge sibling stored","type":"text"}],"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a7-e43a-74f2-b94e-99f168de38fd","tokenUsage":{"total":{"totalTokens":18450,"inputTokens":18442,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"last":{"totalTokens":18450,"inputTokens":18442,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019ea9a8-079f-7100-bbdf-74026e9c20c1","sessionId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","forkedFromId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","parentThreadId":null,"preview":"Remember the opaque marker merge-sibling-source-3C7K for later in this conversation. Respond with exactly: merge sibling source stored","ephemeral":false,"modelProvider":"openai","createdAt":1780962822,"updatedAt":1780962822,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/06/08/rollout-2026-06-08T16-53-42-019ea9a8-079f-7100-bbdf-74026e9c20c1.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-17b43ed6/apps/server","cliVersion":"0.137.0","source":"vscode","threadSource":null,"agentNickname":null,"agentRole":null,"gitInfo":{"sha":"5115887f33051a4e25d2b427622a54656eeb9ab0","branch":"t3code/17b43ed6","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":7,"result":{"turn":{"id":"019ea9a8-093e-7f11-8cd1-a38c032d4642","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turn":{"id":"019ea9a8-093e-7f11-8cd1-a38c032d4642","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962822,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"xcodebuildmcp","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"9b987e5e-f881-42f5-af00-5933edcaa621","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker merge-sibling-second-9X5B. Respond with exactly: second merge sibling stored","text_elements":[]}]},"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a8-093e-7f11-8cd1-a38c032d4642","startedAtMs":1780962825378}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"9b987e5e-f881-42f5-af00-5933edcaa621","clientId":null,"content":[{"type":"text","text":"Remember the fork-local marker merge-sibling-second-9X5B. Respond with exactly: second merge sibling stored","text_elements":[]}]},"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a8-093e-7f11-8cd1-a38c032d4642","completedAtMs":1780962825378}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0d7a2fc55871887b016a27560a7d448199be437b0c7def0a16","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a8-093e-7f11-8cd1-a38c032d4642","startedAtMs":1780962826472}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a8-093e-7f11-8cd1-a38c032d4642","itemId":"msg_0d7a2fc55871887b016a27560a7d448199be437b0c7def0a16","delta":"second"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a8-093e-7f11-8cd1-a38c032d4642","itemId":"msg_0d7a2fc55871887b016a27560a7d448199be437b0c7def0a16","delta":" merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a8-093e-7f11-8cd1-a38c032d4642","itemId":"msg_0d7a2fc55871887b016a27560a7d448199be437b0c7def0a16","delta":" sibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a8-093e-7f11-8cd1-a38c032d4642","itemId":"msg_0d7a2fc55871887b016a27560a7d448199be437b0c7def0a16","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0d7a2fc55871887b016a27560a7d448199be437b0c7def0a16","text":"second merge sibling stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a8-093e-7f11-8cd1-a38c032d4642","completedAtMs":1780962826567}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turnId":"019ea9a8-093e-7f11-8cd1-a38c032d4642","tokenUsage":{"total":{"totalTokens":36937,"inputTokens":36921,"cachedInputTokens":36608,"outputTokens":16,"reasoningOutputTokens":0},"last":{"totalTokens":18487,"inputTokens":18479,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a8-079f-7100-bbdf-74026e9c20c1","turn":{"id":"019ea9a8-093e-7f11-8cd1-a38c032d4642","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962822,"completedAt":1780962826,"durationMs":4114}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":8,"method":"turn/start","params":{"input":[{"text":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from first forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-first-6V2J.\n- Assistant confirmed: first merge sibling stored\n\nUser message:\nRetain the first transferred marker for later. Respond with exactly: first merge delta stored","type":"text"}],"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":8,"result":{"turn":{"id":"019ea9a8-1956-7183-acaa-f43d53da7a22","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turn":{"id":"019ea9a8-1956-7183-acaa-f43d53da7a22","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962826,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"7eabb7de-b7f9-4c1d-b82d-cfbbe71c4ea4","clientId":null,"content":[{"type":"text","text":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from first forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-first-6V2J.\n- Assistant confirmed: first merge sibling stored\n\nUser message:\nRetain the first transferred marker for later. Respond with exactly: first merge delta stored","text_elements":[]}]},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-1956-7183-acaa-f43d53da7a22","startedAtMs":1780962826590}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"7eabb7de-b7f9-4c1d-b82d-cfbbe71c4ea4","clientId":null,"content":[{"type":"text","text":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from first forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-first-6V2J.\n- Assistant confirmed: first merge sibling stored\n\nUser message:\nRetain the first transferred marker for later. Respond with exactly: first merge delta stored","text_elements":[]}]},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-1956-7183-acaa-f43d53da7a22","completedAtMs":1780962826590}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_06277fc0328731b4016a27560c205881988d5b078c30e6526b","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-1956-7183-acaa-f43d53da7a22","startedAtMs":1780962828113}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-1956-7183-acaa-f43d53da7a22","itemId":"msg_06277fc0328731b4016a27560c205881988d5b078c30e6526b","delta":"first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-1956-7183-acaa-f43d53da7a22","itemId":"msg_06277fc0328731b4016a27560c205881988d5b078c30e6526b","delta":" merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-1956-7183-acaa-f43d53da7a22","itemId":"msg_06277fc0328731b4016a27560c205881988d5b078c30e6526b","delta":" delta"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-1956-7183-acaa-f43d53da7a22","itemId":"msg_06277fc0328731b4016a27560c205881988d5b078c30e6526b","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_06277fc0328731b4016a27560c205881988d5b078c30e6526b","text":"first merge delta stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-1956-7183-acaa-f43d53da7a22","completedAtMs":1780962828289}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-1956-7183-acaa-f43d53da7a22","tokenUsage":{"total":{"totalTokens":36981,"inputTokens":36965,"cachedInputTokens":36608,"outputTokens":16,"reasoningOutputTokens":0},"last":{"totalTokens":18531,"inputTokens":18523,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turn":{"id":"019ea9a8-1956-7183-acaa-f43d53da7a22","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962826,"completedAt":1780962828,"durationMs":1738}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":9,"method":"turn/start","params":{"input":[{"text":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from second forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-second-9X5B.\n- Assistant confirmed: second merge sibling stored\n\nUser message:\nRetain the second transferred marker for later. Respond with exactly: second merge delta stored","type":"text"}],"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":9,"result":{"turn":{"id":"019ea9a8-2025-7de2-bb4b-ecd86a568956","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turn":{"id":"019ea9a8-2025-7de2-bb4b-ecd86a568956","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962828,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"fa82b56f-507e-488a-b784-899434c3d8cc","clientId":null,"content":[{"type":"text","text":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from second forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-second-9X5B.\n- Assistant confirmed: second merge sibling stored\n\nUser message:\nRetain the second transferred marker for later. Respond with exactly: second merge delta stored","text_elements":[]}]},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-2025-7de2-bb4b-ecd86a568956","startedAtMs":1780962828332}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"fa82b56f-507e-488a-b784-899434c3d8cc","clientId":null,"content":[{"type":"text","text":"Context handoff (merge_back / fork_delta_summary):\nMerge-back context from second forked conversation.\n\nFork delta:\n- User introduced opaque marker merge-sibling-second-9X5B.\n- Assistant confirmed: second merge sibling stored\n\nUser message:\nRetain the second transferred marker for later. Respond with exactly: second merge delta stored","text_elements":[]}]},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-2025-7de2-bb4b-ecd86a568956","completedAtMs":1780962828332}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_06277fc0328731b4016a27560d83d48198b3495fcdb9df105b","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-2025-7de2-bb4b-ecd86a568956","startedAtMs":1780962829564}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-2025-7de2-bb4b-ecd86a568956","itemId":"msg_06277fc0328731b4016a27560d83d48198b3495fcdb9df105b","delta":"second"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-2025-7de2-bb4b-ecd86a568956","itemId":"msg_06277fc0328731b4016a27560d83d48198b3495fcdb9df105b","delta":" merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-2025-7de2-bb4b-ecd86a568956","itemId":"msg_06277fc0328731b4016a27560d83d48198b3495fcdb9df105b","delta":" delta"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-2025-7de2-bb4b-ecd86a568956","itemId":"msg_06277fc0328731b4016a27560d83d48198b3495fcdb9df105b","delta":" stored"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_06277fc0328731b4016a27560d83d48198b3495fcdb9df105b","text":"second merge delta stored","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-2025-7de2-bb4b-ecd86a568956","completedAtMs":1780962829632}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-2025-7de2-bb4b-ecd86a568956","tokenUsage":{"total":{"totalTokens":55593,"inputTokens":55569,"cachedInputTokens":54912,"outputTokens":24,"reasoningOutputTokens":0},"last":{"totalTokens":18612,"inputTokens":18604,"cachedInputTokens":18304,"outputTokens":8,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turn":{"id":"019ea9a8-2025-7de2-bb4b-ecd86a568956","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962828,"completedAt":1780962829,"durationMs":1313}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":10,"method":"turn/start","params":{"input":[{"text":"Return the source marker followed by both transferred fork markers in merge order, separated by single | characters. Respond with only the markers and separators.","type":"text"}],"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":10,"result":{"turn":{"id":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turn":{"id":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","items":[],"itemsView":"notLoaded","status":"inProgress","error":null,"startedAt":1780962829,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"f92dacf3-9750-47d3-937b-8af94a859b87","clientId":null,"content":[{"type":"text","text":"Return the source marker followed by both transferred fork markers in merge order, separated by single | characters. Respond with only the markers and separators.","text_elements":[]}]},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","startedAtMs":1780962829651}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"f92dacf3-9750-47d3-937b-8af94a859b87","clientId":null,"content":[{"type":"text","text":"Return the source marker followed by both transferred fork markers in merge order, separated by single | characters. Respond with only the markers and separators.","text_elements":[]}]},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","completedAtMs":1780962829651}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","startedAtMs":1780962830796}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"-s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"ibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"-source"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"3"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"C"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"7"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"K"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"|"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"-s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"ibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"-first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"6"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"V"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"2"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"J"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"|"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"merge"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"-s"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"ibling"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"-second"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"-"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"9"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"X"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"5"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","itemId":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","delta":"B"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_06277fc0328731b4016a27560ecf108198af3cbd23416442ff","text":"merge-sibling-source-3C7K|merge-sibling-first-6V2J|merge-sibling-second-9X5B","phase":"final_answer","memoryCitation":null},"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","completedAtMs":1780962831144}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turnId":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","tokenUsage":{"total":{"totalTokens":74272,"inputTokens":74215,"cachedInputTokens":73216,"outputTokens":57,"reasoningOutputTokens":0},"last":{"totalTokens":18679,"inputTokens":18646,"cachedInputTokens":18304,"outputTokens":33,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1780968999},"secondary":{"usedPercent":3,"windowDurationMins":10080,"resetsAt":1781296757},"credits":null,"individualLimit":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019ea9a7-e28f-7c72-96e8-9ca581f225ce","turn":{"id":"019ea9a8-254b-7e53-af1e-1066f8bc37a4","items":[],"itemsView":"notLoaded","status":"completed","error":null,"startedAt":1780962829,"completedAt":1780962831,"durationMs":1502}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/claude_output.ts new file mode 100644 index 00000000000..1100fec5108 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/claude_output.ts @@ -0,0 +1,69 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypeSequence, + assertUserMessagesInclude, + assertVisibleTurnItemTypeSequence, + assertVisibleUserMessagesExclude, + assertVisibleUserMessagesInclude, + projectionFor, + THREAD_ROLLBACK_AFTER_PROMPT, + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_SECOND_PROMPT, +} from "../shared.ts"; + +export function assertClaudeThreadRollbackOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ + result, + transcript, + runCount: 3, + runStatuses: ["completed", "rolled_back", "completed"], + }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertRunOrdinals(projection, [1, 2, 3]); + assertTurnItemTypeSequence(projection, [ + "user_message", + "assistant_message", + "checkpoint", + "user_message", + "assistant_message", + "checkpoint", + "user_message", + "assistant_message", + "checkpoint", + ]); + assertVisibleTurnItemTypeSequence(projection, [ + "user_message", + "assistant_message", + "checkpoint", + "user_message", + "assistant_message", + "checkpoint", + ]); + assertUserMessagesInclude(projection, [ + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_SECOND_PROMPT, + THREAD_ROLLBACK_AFTER_PROMPT, + ]); + assertVisibleUserMessagesInclude(projection, [ + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_AFTER_PROMPT, + ]); + assertVisibleUserMessagesExclude(projection, [THREAD_ROLLBACK_SECOND_PROMPT]); + assert.isAtLeast(projection.checkpoints.length, 2); + assert.isTrue( + projection.runs.some((run) => run.status === "rolled_back"), + "rollback must be visible in run state", + ); + assert.equal(projection.providerThreads[0]?.nativeConversationHeadRef, null); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/claude_transcript.ndjson new file mode 100644 index 00000000000..cbf7760f096 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/claude_transcript.ndjson @@ -0,0 +1,24 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"thread_rollback","metadata":{"prompts":["Respond with exactly: rollback fixture first turn complete","Respond with exactly: rollback fixture second turn complete","Repeat the conversation verbatim."],"model":"claude-sonnet-4-6","nativeSessionId":"f9415703-bf61-49e3-8155-1d3fed83f935","queryMode":"resume_at_cursor","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript","resumeSessionAt":"3831efb6-619c-4c16-a38a-bf788040ac42","sourceAssistantMessageUuids":["3831efb6-619c-4c16-a38a-bf788040ac42","d03f5cd6-5e8b-4cf5-998f-7d96a923ecf8"]}} +{"type":"expect_outbound","label":"query.open:source","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"f9415703-bf61-49e3-8155-1d3fed83f935"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: rollback fixture first turn complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"10af24d0-396e-4d26-a07b-d6c66704b322","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"45ae7f96-b6f0-49fa-933d-ea449ac377ea","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"10af24d0-396e-4d26-a07b-d6c66704b322","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"b3e745a0-d47d-4bb8-b67f-5faf244eb50d","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-thread_rollback","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"218871ce-bb95-4526-ba59-0dcc71e47835","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01NUv7RH7XTuJnHd6WyVG1Vo","type":"message","role":"assistant","content":[{"type":"text","text":"rollback fixture first turn complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11794,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":11794},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f9415703-bf61-49e3-8155-1d3fed83f935","uuid":"3831efb6-619c-4c16-a38a-bf788040ac42"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"db72cb9f-000f-454a-8b05-cc231a6434ed","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":1813,"duration_api_ms":1317,"num_turns":1,"result":"rollback fixture first turn complete","stop_reason":"end_turn","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935","total_cost_usd":0.0443715,"usage":{"input_tokens":3,"cache_creation_input_tokens":11794,"cache_read_input_tokens":0,"output_tokens":9,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":11794,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":9,"cache_read_input_tokens":0,"cache_creation_input_tokens":11794,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":11794},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":9,"cacheReadInputTokens":0,"cacheCreationInputTokens":11794,"webSearchRequests":0,"costUSD":0.0443715,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"7b499f7a-fb88-462d-bdd0-aa4841528c96"}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: rollback fixture second turn complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-thread_rollback","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"b398ca88-f2c0-4606-9480-d4a37be9fcdc","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01U9BNPSbhQkJZsmNc4VpdBn","type":"message","role":"assistant","content":[{"type":"text","text":"rollback fixture second turn complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":23,"cache_read_input_tokens":11794,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":23},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f9415703-bf61-49e3-8155-1d3fed83f935","uuid":"d03f5cd6-5e8b-4cf5-998f-7d96a923ecf8"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":1563,"duration_api_ms":2262,"num_turns":1,"result":"rollback fixture second turn complete","stop_reason":"end_turn","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935","total_cost_usd":0.04813995,"usage":{"input_tokens":3,"cache_creation_input_tokens":23,"cache_read_input_tokens":11794,"output_tokens":9,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":23,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":9,"cache_read_input_tokens":11794,"cache_creation_input_tokens":23,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":23},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":6,"outputTokens":18,"cacheReadInputTokens":11794,"cacheCreationInputTokens":11817,"webSearchRequests":0,"costUSD":0.04813995,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"badfd2cf-d9be-486f-a0ad-fdd84c9b644f"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"query.open:resume_at_cursor","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resumeSessionAt":"3831efb6-619c-4c16-a38a-bf788040ac42","resume":"f9415703-bf61-49e3-8155-1d3fed83f935"}}} +{"type":"expect_outbound","label":"prompt.offer:3","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Repeat the conversation verbatim."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"c9f667e7-ca12-49e9-a8ba-bdbca3abbb52","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"8ca5d046-22be-4a78-b62d-017a82cacde5","session_id":"364d2478-ab5e-4d54-8f1a-ac5985780b94"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"c9f667e7-ca12-49e9-a8ba-bdbca3abbb52","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"6369a44f-6abb-4e6b-8302-603b2ae6f4af","session_id":"364d2478-ab5e-4d54-8f1a-ac5985780b94"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-thread_rollback","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"18ad1c2a-af69-41b0-b5c4-68368d89338e","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_018VXP9s6TV1dsMYKJDkM5cb","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to repeat the conversation verbatim. Let me recall the conversation so far.","signature":"EpoCClsIDRgCKkAWt3L1MZbqQnRJfaQpC/zTmnX+89ilokWcVeWkpDrrEY2oZntDTkRch9BHty3vZDGi5JqizC60fkS3UYDi9nvdMhFjbGF1ZGUtc29ubmV0LTQtNjgAEgy2iD+JrxDeNoidixsaDLnyeHyQiuJ87Mp2HSIwYxphRaf5TTSOq0kBF/cS2SYHvchyl7A857XzjZC6x1+n1WRVjeFu85/guUqGhkbpKm2zNZSjsFz9IVYeNWdsNuBbUftQ5RZuBipNrqaJfXtBuGl7NHeBArYZvFgcNZMScvsFrYjII7RWaEOAZYaiRnxSUzrZB+rEe0xnx0PJHKM3Vv5uVJl68/JXEyTfWV1hgKGa5hm6rUpoudwW7eujGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":20,"cache_read_input_tokens":11794,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":20},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f9415703-bf61-49e3-8155-1d3fed83f935","uuid":"2a455c39-eef8-45b9-83f5-11567ff0459b"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_018VXP9s6TV1dsMYKJDkM5cb","type":"message","role":"assistant","content":[{"type":"text","text":"Here is the conversation verbatim:\n\n---\n\n**User:** Respond with exactly: rollback fixture first turn complete\n\n**Assistant:** rollback fixture first turn complete\n\n---\n\n**User:** Repeat the conversation verbatim."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":20,"cache_read_input_tokens":11794,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":20},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f9415703-bf61-49e3-8155-1d3fed83f935","uuid":"572fafb1-2351-4f3a-848d-7b4703496bc3"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"f3a34752-3bb6-42d7-a5d2-230917f30a7c","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":2368,"duration_api_ms":1731,"num_turns":1,"result":"Here is the conversation verbatim:\n\n---\n\n**User:** Respond with exactly: rollback fixture first turn complete\n\n**Assistant:** rollback fixture first turn complete\n\n---\n\n**User:** Repeat the conversation verbatim.","stop_reason":"end_turn","session_id":"f9415703-bf61-49e3-8155-1d3fed83f935","total_cost_usd":0.0049272000000000005,"usage":{"input_tokens":3,"cache_creation_input_tokens":20,"cache_read_input_tokens":11794,"output_tokens":87,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":20,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":87,"cache_read_input_tokens":11794,"cache_creation_input_tokens":20,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":20},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":87,"cacheReadInputTokens":11794,"cacheCreationInputTokens":20,"webSearchRequests":0,"costUSD":0.0049272000000000005,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"980fa6e9-fcc5-4f52-92c7-ff95c180a46a"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/codex_output.ts new file mode 100644 index 00000000000..89c904cee9b --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/codex_output.ts @@ -0,0 +1,68 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypeSequence, + assertVisibleTurnItemTypeSequence, + assertVisibleUserMessagesExclude, + assertVisibleUserMessagesInclude, + assertUserMessagesInclude, + projectionFor, + THREAD_ROLLBACK_AFTER_PROMPT, + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_SECOND_PROMPT, +} from "../shared.ts"; + +export function assertThreadRollbackOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ + result, + transcript, + runCount: 3, + runStatuses: ["completed", "rolled_back", "completed"], + }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertRunOrdinals(projection, [1, 2, 3]); + assertTurnItemTypeSequence(projection, [ + "user_message", + "assistant_message", + "checkpoint", + "user_message", + "assistant_message", + "checkpoint", + "user_message", + "assistant_message", + "checkpoint", + ]); + assertVisibleTurnItemTypeSequence(projection, [ + "user_message", + "assistant_message", + "checkpoint", + "user_message", + "assistant_message", + "checkpoint", + ]); + assertUserMessagesInclude(projection, [ + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_SECOND_PROMPT, + THREAD_ROLLBACK_AFTER_PROMPT, + ]); + assertVisibleUserMessagesInclude(projection, [ + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_AFTER_PROMPT, + ]); + assertVisibleUserMessagesExclude(projection, [THREAD_ROLLBACK_SECOND_PROMPT]); + assert.isAtLeast(projection.checkpoints.length, 2); + assert.isTrue( + projection.runs.some((run) => run.status === "rolled_back"), + "rollback must be visible in run state", + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/codex_transcript.ndjson new file mode 100644 index 00000000000..0ec8b32174d --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/codex_transcript.ndjson @@ -0,0 +1,100 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"thread_rollback","metadata":{"source":"codex-app-server-probe","fileName":"thread_rollback.ndjson","description":"One thread completes two turns, rolls back the most recent turn, then starts another turn."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019daded-23e0-7100-87d7-12089c71339a","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739492,"updatedAt":1776739492,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-44-52-019daded-23e0-7100-87d7-12089c71339a.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Respond with exactly: rollback fixture first turn complete","type":"text"}],"threadId":"019daded-23e0-7100-87d7-12089c71339a"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019daded-23e0-7100-87d7-12089c71339a","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739492,"updatedAt":1776739492,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-44-52-019daded-23e0-7100-87d7-12089c71339a.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019daded-23e9-7100-8f16-28223024be18","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turn":{"id":"019daded-23e9-7100-8f16-28223024be18","items":[],"status":"inProgress","error":null,"startedAt":1776739492,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"99075ce0-b5fd-4d44-b765-874c3d5e98d7","content":[{"type":"text","text":"Respond with exactly: rollback fixture first turn complete","text_elements":[]}]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"99075ce0-b5fd-4d44-b765-874c3d5e98d7","content":[{"type":"text","text":"Respond with exactly: rollback fixture first turn complete","text_elements":[]}]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_06f406cdc76c42010169e6e4ac9c3c819a8f45370ee32a9e95","summary":[],"content":[]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_06f406cdc76c42010169e6e4ac9c3c819a8f45370ee32a9e95","summary":[],"content":[]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_06f406cdc76c42010169e6e4ace378819a8bac065db540df6c","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18","itemId":"msg_06f406cdc76c42010169e6e4ace378819a8bac065db540df6c","delta":"rollback"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18","itemId":"msg_06f406cdc76c42010169e6e4ace378819a8bac065db540df6c","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18","itemId":"msg_06f406cdc76c42010169e6e4ace378819a8bac065db540df6c","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18","itemId":"msg_06f406cdc76c42010169e6e4ace378819a8bac065db540df6c","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18","itemId":"msg_06f406cdc76c42010169e6e4ace378819a8bac065db540df6c","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_06f406cdc76c42010169e6e4ace378819a8bac065db540df6c","text":"rollback fixture first turn complete","phase":"final_answer","memoryCitation":null},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-23e9-7100-8f16-28223024be18","tokenUsage":{"total":{"totalTokens":28247,"inputTokens":28223,"cachedInputTokens":28032,"outputTokens":24,"reasoningOutputTokens":13},"last":{"totalTokens":28247,"inputTokens":28223,"cachedInputTokens":28032,"outputTokens":24,"reasoningOutputTokens":13},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turn":{"id":"019daded-23e9-7100-8f16-28223024be18","items":[],"status":"completed","error":null,"startedAt":1776739492,"completedAt":1776739501,"durationMs":8544}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":4,"method":"turn/start","params":{"input":[{"text":"Respond with exactly: rollback fixture second turn complete","type":"text"}],"threadId":"019daded-23e0-7100-87d7-12089c71339a"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":4,"result":{"turn":{"id":"019daded-454a-70e2-acce-09a54f6192d0","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turn":{"id":"019daded-454a-70e2-acce-09a54f6192d0","items":[],"status":"inProgress","error":null,"startedAt":1776739501,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"9a8286af-6e78-4b7b-9122-1b477229a4e8","content":[{"type":"text","text":"Respond with exactly: rollback fixture second turn complete","text_elements":[]}]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"9a8286af-6e78-4b7b-9122-1b477229a4e8","content":[{"type":"text","text":"Respond with exactly: rollback fixture second turn complete","text_elements":[]}]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0","tokenUsage":{"total":{"totalTokens":28247,"inputTokens":28223,"cachedInputTokens":28032,"outputTokens":24,"reasoningOutputTokens":13},"last":{"totalTokens":28247,"inputTokens":28223,"cachedInputTokens":28032,"outputTokens":24,"reasoningOutputTokens":13},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_06f406cdc76c42010169e6e4b03dbc819abd1ab64b725345b6","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0","itemId":"msg_06f406cdc76c42010169e6e4b03dbc819abd1ab64b725345b6","delta":"rollback"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0","itemId":"msg_06f406cdc76c42010169e6e4b03dbc819abd1ab64b725345b6","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0","itemId":"msg_06f406cdc76c42010169e6e4b03dbc819abd1ab64b725345b6","delta":" second"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0","itemId":"msg_06f406cdc76c42010169e6e4b03dbc819abd1ab64b725345b6","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0","itemId":"msg_06f406cdc76c42010169e6e4b03dbc819abd1ab64b725345b6","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_06f406cdc76c42010169e6e4b03dbc819abd1ab64b725345b6","text":"rollback fixture second turn complete","phase":"final_answer","memoryCitation":null},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-454a-70e2-acce-09a54f6192d0","tokenUsage":{"total":{"totalTokens":56503,"inputTokens":56470,"cachedInputTokens":56064,"outputTokens":33,"reasoningOutputTokens":13},"last":{"totalTokens":28256,"inputTokens":28247,"cachedInputTokens":28032,"outputTokens":9,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turn":{"id":"019daded-454a-70e2-acce-09a54f6192d0","items":[],"status":"completed","error":null,"startedAt":1776739501,"completedAt":1776739504,"durationMs":3091}}}} +{"type":"expect_outbound","label":"thread/rollback","frame":{"id":5,"method":"thread/rollback","params":{"numTurns":1,"threadId":"019daded-23e0-7100-87d7-12089c71339a"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-515f-7642-b0db-bd62c3f7a473","tokenUsage":{"total":{"totalTokens":56503,"inputTokens":56470,"cachedInputTokens":56064,"outputTokens":33,"reasoningOutputTokens":13},"last":{"totalTokens":12317,"inputTokens":0,"cachedInputTokens":0,"outputTokens":0,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/rollback","frame":{"id":5,"result":{"thread":{"id":"019daded-23e0-7100-87d7-12089c71339a","forkedFromId":null,"preview":"Respond with exactly: rollback fixture first turn complete","ephemeral":false,"modelProvider":"openai","createdAt":1776739492,"updatedAt":1776739504,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-44-52-019daded-23e0-7100-87d7-12089c71339a.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":{"sha":"7eefb2eb1d98f6b814c9fd7f43652727059c3326","branch":"t3code/codex-turn-mapping","originUrl":"git@github.com:pingdotgg/t3code.git"},"name":null,"turns":[{"id":"019daded-23e9-7100-8f16-28223024be18","items":[{"type":"userMessage","id":"item-1","content":[{"type":"text","text":"Respond with exactly: rollback fixture first turn complete","text_elements":[]}]},{"type":"agentMessage","id":"item-2","text":"rollback fixture first turn complete","phase":"final_answer","memoryCitation":null}],"status":"completed","error":null,"startedAt":1776739492,"completedAt":1776739501,"durationMs":8544}]}}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":6,"method":"turn/start","params":{"input":[{"text":"Repeat the conversation verbatim.","type":"text"}],"threadId":"019daded-23e0-7100-87d7-12089c71339a"}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":6,"result":{"turn":{"id":"019daded-5165-7f73-a6bc-3cf4386b542c","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turn":{"id":"019daded-5165-7f73-a6bc-3cf4386b542c","items":[],"status":"inProgress","error":null,"startedAt":1776739504,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"18c178ce-88cd-436b-8a5b-11f9e081167d","content":[{"type":"text","text":"Repeat the conversation verbatim.","text_elements":[]}]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"18c178ce-88cd-436b-8a5b-11f9e081167d","content":[{"type":"text","text":"Repeat the conversation verbatim.","text_elements":[]}]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","tokenUsage":{"total":{"totalTokens":56503,"inputTokens":56470,"cachedInputTokens":56064,"outputTokens":33,"reasoningOutputTokens":13},"last":{"totalTokens":12317,"inputTokens":0,"cachedInputTokens":0,"outputTokens":0,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0aad8fe64aad9e920169e6e4b1c9bc819896d64445e9585404","summary":[],"content":[]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0aad8fe64aad9e920169e6e4b1c9bc819896d64445e9585404","summary":[],"content":[]},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"User"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":":\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"Respond"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" with"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" exactly"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":":"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" rollback"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" complete"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"Assistant"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":":\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"rollback"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" turn"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" complete"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"User"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":":\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"Repeat"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" conversation"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":" verb"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"atim"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","itemId":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0aad8fe64aad9e920169e6e4b7d6b08198b4e2c09b80bbed9b","text":"User:\nRespond with exactly: rollback fixture first turn complete\n\nAssistant:\nrollback fixture first turn complete\n\nUser:\nRepeat the conversation verbatim.","phase":"final_answer","memoryCitation":null},"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turnId":"019daded-5165-7f73-a6bc-3cf4386b542c","tokenUsage":{"total":{"totalTokens":85297,"inputTokens":84714,"cachedInputTokens":84096,"outputTokens":583,"reasoningOutputTokens":529},"last":{"totalTokens":28794,"inputTokens":28244,"cachedInputTokens":28032,"outputTokens":550,"reasoningOutputTokens":516},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019daded-23e0-7100-87d7-12089c71339a","turn":{"id":"019daded-5165-7f73-a6bc-3cf4386b542c","items":[],"status":"completed","error":null,"startedAt":1776739504,"completedAt":1776739512,"durationMs":7806}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/input.ts new file mode 100644 index 00000000000..47371f6be76 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/input.ts @@ -0,0 +1,21 @@ +import { + THREAD_ROLLBACK_AFTER_PROMPT, + THREAD_ROLLBACK_FIRST_PROMPT, + THREAD_ROLLBACK_SECOND_PROMPT, + type OrchestratorFixtureInput, +} from "../shared.ts"; + +export function threadRollbackInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: THREAD_ROLLBACK_FIRST_PROMPT }, + { type: "message", text: THREAD_ROLLBACK_SECOND_PROMPT }, + { + type: "rollback", + checkpointScopeSuffix: "root", + checkpointSuffix: "1", + }, + { type: "message", text: THREAD_ROLLBACK_AFTER_PROMPT }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/codex_output.ts new file mode 100644 index 00000000000..3b1ff8379f7 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/codex_output.ts @@ -0,0 +1,39 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertExecutionNodeKinds, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TODO_LIST_PROMPT, +} from "../shared.ts"; + +export function assertTodoListOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1]); + assertExecutionNodeKinds(projection, ["root_turn", "todo_list", "tool_call"]); + assertTurnItemTypes(projection, ["user_message", "todo_list", "command_execution"]); + assertUserMessagesInclude(projection, [TODO_LIST_PROMPT]); + assertAssistantTextIncludes(projection, "todo list fixture complete"); + + const todoLists = projection.plans.filter((plan) => plan.kind === "todo_list"); + assert.isAtLeast(todoLists.length, 1); + assert.deepEqual( + todoLists.at(-1)?.steps.map((step) => step.status), + ["completed", "completed", "completed"], + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/codex_transcript.ndjson new file mode 100644 index 00000000000..02af6f8c1b8 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/codex_transcript.ndjson @@ -0,0 +1,105 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"todo_list","metadata":{"source":"codex-app-server-probe","fileName":"todo_list.ndjson","description":"One turn that asks Codex to emit progress through update_plan."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019db229-ac7a-7923-ab72-67bfefd79a8b","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776810568,"updatedAt":1776810568,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T15-29-28-019db229-ac7a-7923-ab72-67bfefd79a8b.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.3-codex","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"approvalPolicy":"never","input":[{"text":"Use the update_plan tool to track exactly three steps: inspect package.json, inspect tsconfig.json, report completion. Then read package.json and tsconfig.json, and answer exactly: todo list fixture complete","type":"text"}],"sandboxPolicy":{"networkAccess":false,"type":"readOnly"},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019db229-ac7a-7923-ab72-67bfefd79a8b","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776810568,"updatedAt":1776810568,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T15-29-28-019db229-ac7a-7923-ab72-67bfefd79a8b.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turn":{"id":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","items":[],"status":"inProgress","error":null,"startedAt":1776810568,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"a37f5d89-8a7a-4799-8398-2d6c409acb98","content":[{"type":"text","text":"Use the update_plan tool to track exactly three steps: inspect package.json, inspect tsconfig.json, report completion. Then read package.json and tsconfig.json, and answer exactly: todo list fixture complete","text_elements":[]}]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"a37f5d89-8a7a-4799-8398-2d6c409acb98","content":[{"type":"text","text":"Use the update_plan tool to track exactly three steps: inspect package.json, inspect tsconfig.json, report completion. Then read package.json and tsconfig.json, and answer exactly: todo list fixture complete","text_elements":[]}]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_075f3159e45307370169e7fa4dd1448198bca55c29410eafb7","summary":[],"content":[]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_075f3159e45307370169e7fa4dd1448198bca55c29410eafb7","summary":[],"content":[]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","text":"","phase":"commentary","memoryCitation":null},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":"Tracking"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" three"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" requested"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" plan"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" steps"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" first"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" then"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" I"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":"’ll"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" read"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":"package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" `"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":"ts"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":"config"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":".json"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" and"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" finish"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" with"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" exact"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" response"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":" text"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_075f3159e45307370169e7fa5f305c81989b4dcab9d3036440","text":"Tracking the three requested plan steps first, then I’ll read `package.json` and `tsconfig.json` and finish with the exact response text.","phase":"commentary","memoryCitation":null},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"turn/plan/updated","frame":{"method":"turn/plan/updated","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","explanation":null,"plan":[{"step":"inspect package.json","status":"inProgress"},{"step":"inspect tsconfig.json","status":"pending"},{"step":"report completion","status":"pending"}]}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","tokenUsage":{"total":{"totalTokens":25386,"inputTokens":24336,"cachedInputTokens":3456,"outputTokens":1050,"reasoningOutputTokens":962},"last":{"totalTokens":25386,"inputTokens":24336,"cachedInputTokens":3456,"outputTokens":1050,"reasoningOutputTokens":962},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_075f3159e45307370169e7fa61751c8198bcb1d63a6e31608c","summary":[],"content":[]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_075f3159e45307370169e7fa61751c8198bcb1d63a6e31608c","summary":[],"content":[]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","text":"","phase":"commentary","memoryCitation":null},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":"Reading"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" both"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" files"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" now"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" complete"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" inspect"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" steps"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" then"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" I"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":"’ll"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" mark"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" plan"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":" complete"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_075f3159e45307370169e7fa6276f881988716e668d1364f8b","text":"Reading both files now to complete the inspect steps, then I’ll mark the plan complete.","phase":"commentary","memoryCitation":null},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","tokenUsage":{"total":{"totalTokens":50994,"inputTokens":49735,"cachedInputTokens":28416,"outputTokens":1259,"reasoningOutputTokens":1015},"last":{"totalTokens":25608,"inputTokens":25399,"cachedInputTokens":24960,"outputTokens":209,"reasoningOutputTokens":53},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_ZCMqKDv3xX8eeFDlEsjdf9cz","command":"/bin/zsh -lc 'cat package.json'","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"57218","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"read","command":"cat package.json","name":"package.json","path":"package.json"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_ZCMqKDv3xX8eeFDlEsjdf9cz","command":"/bin/zsh -lc 'cat package.json'","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"57218","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"read","command":"cat package.json","name":"package.json","path":"package.json"}],"aggregatedOutput":"{\n \"name\": \"effect-codex-app-server\",\n \"private\": true,\n \"type\": \"module\",\n \"exports\": {\n \"./client\": {\n \"types\": \"./src/client.ts\",\n \"import\": \"./src/client.ts\"\n },\n \"./schema\": {\n \"types\": \"./src/schema.ts\",\n \"import\": \"./src/schema.ts\"\n },\n \"./rpc\": {\n \"types\": \"./src/rpc.ts\",\n \"import\": \"./src/rpc.ts\"\n },\n \"./protocol\": {\n \"types\": \"./src/protocol.ts\",\n \"import\": \"./src/protocol.ts\"\n },\n \"./replay\": {\n \"types\": \"./src/replay.ts\",\n \"import\": \"./src/replay.ts\"\n },\n \"./errors\": {\n \"types\": \"./src/errors.ts\",\n \"import\": \"./src/errors.ts\"\n }\n },\n \"scripts\": {\n \"dev\": \"tsdown src/client.ts src/rpc.ts src/protocol.ts src/schema.ts --format esm,cjs --dts --watch --clean\",\n \"build\": \"tsdown src/client.ts src/rpc.ts src/protocol.ts src/schema.ts --format esm,cjs --dts --clean\",\n \"prepare\": \"effect-language-service patch\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"generate\": \"bun run scripts/generate.ts\",\n \"probe\": \"bun run test/examples/codex-app-server-probe.ts\"\n },\n \"dependencies\": {\n \"effect\": \"catalog:\"\n },\n \"devDependencies\": {\n \"@effect/language-service\": \"catalog:\",\n \"@effect/openapi-generator\": \"catalog:\",\n \"@effect/platform-node\": \"catalog:\",\n \"@effect/vitest\": \"catalog:\",\n \"tsdown\": \"catalog:\",\n \"typescript\": \"catalog:\",\n \"vitest\": \"catalog:\"\n }\n}\n","exitCode":0,"durationMs":0},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_ZXkLHFX09x8LZ8WEfEq0V85a","command":"/bin/zsh -lc 'cat tsconfig.json'","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"71881","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"read","command":"cat tsconfig.json","name":"tsconfig.json","path":"tsconfig.json"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_ZXkLHFX09x8LZ8WEfEq0V85a","command":"/bin/zsh -lc 'cat tsconfig.json'","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"71881","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"read","command":"cat tsconfig.json","name":"tsconfig.json","path":"tsconfig.json"}],"aggregatedOutput":"{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"plugins\": [\n {\n \"name\": \"@effect/language-service\",\n \"namespaceImportPackages\": [\"@effect/platform-node\"],\n \"diagnosticSeverity\": {\n \"importFromBarrel\": \"error\",\n \"anyUnknownInErrorContext\": \"warning\",\n \"instanceOfSchema\": \"warning\",\n \"deterministicKeys\": \"warning\"\n }\n }\n ]\n },\n \"include\": [\"src\", \"scripts\", \"test\"]\n}\n","exitCode":0,"durationMs":0},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_075f3159e45307370169e7fa665fc881988fc7df3876aaea10","summary":[],"content":[]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_075f3159e45307370169e7fa665fc881988fc7df3876aaea10","summary":[],"content":[]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"turn/plan/updated","frame":{"method":"turn/plan/updated","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","explanation":null,"plan":[{"step":"inspect package.json","status":"completed"},{"step":"inspect tsconfig.json","status":"completed"},{"step":"report completion","status":"inProgress"}]}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","tokenUsage":{"total":{"totalTokens":77451,"inputTokens":75954,"cachedInputTokens":54016,"outputTokens":1497,"reasoningOutputTokens":1202},"last":{"totalTokens":26457,"inputTokens":26219,"cachedInputTokens":25600,"outputTokens":238,"reasoningOutputTokens":187},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"turn/plan/updated","frame":{"method":"turn/plan/updated","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","explanation":null,"plan":[{"step":"inspect package.json","status":"completed"},{"step":"inspect tsconfig.json","status":"completed"},{"step":"report completion","status":"completed"}]}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","tokenUsage":{"total":{"totalTokens":103969,"inputTokens":102424,"cachedInputTokens":80384,"outputTokens":1545,"reasoningOutputTokens":1202},"last":{"totalTokens":26518,"inputTokens":26470,"cachedInputTokens":26368,"outputTokens":48,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_075f3159e45307370169e7fa6f98608198aa4521e637e1bc62","summary":[],"content":[]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_075f3159e45307370169e7fa6f98608198aa4521e637e1bc62","summary":[],"content":[]},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_075f3159e45307370169e7fa6fde708198b09a8c5ac7c02317","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6fde708198b09a8c5ac7c02317","delta":"todo"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6fde708198b09a8c5ac7c02317","delta":" list"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6fde708198b09a8c5ac7c02317","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","itemId":"msg_075f3159e45307370169e7fa6fde708198b09a8c5ac7c02317","delta":" complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_075f3159e45307370169e7fa6fde708198b09a8c5ac7c02317","text":"todo list fixture complete","phase":"final_answer","memoryCitation":null},"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turnId":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","tokenUsage":{"total":{"totalTokens":130523,"inputTokens":128955,"cachedInputTokens":106880,"outputTokens":1568,"reasoningOutputTokens":1215},"last":{"totalTokens":26554,"inputTokens":26531,"cachedInputTokens":26496,"outputTokens":23,"reasoningOutputTokens":13},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":1,"windowDurationMins":300,"resetsAt":1776821038},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777407838},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019db229-ac7a-7923-ab72-67bfefd79a8b","turn":{"id":"019db229-ac8a-7bb2-8e03-9dcd9f2328e0","items":[],"status":"completed","error":null,"startedAt":1776810568,"completedAt":1776810608,"durationMs":39233}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/cursor_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/cursor_output.ts new file mode 100644 index 00000000000..219f51f3adc --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/cursor_output.ts @@ -0,0 +1,62 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertExecutionNodeKinds, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TODO_LIST_PROMPT, +} from "../shared.ts"; + +export function assertTodoListCursorOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1]); + assertExecutionNodeKinds(projection, ["root_turn", "todo_list", "tool_call"]); + assertTurnItemTypes(projection, [ + "user_message", + "file_search", + "todo_list", + "assistant_message", + ]); + assertUserMessagesInclude(projection, [TODO_LIST_PROMPT]); + assertAssistantTextIncludes(projection, "todo list fixture complete"); + + const conversationalItems = projection.turnItems.filter( + (item) => item.type === "assistant_message" || item.type === "reasoning", + ); + assert.deepEqual( + conversationalItems.map((item) => item.type), + ["assistant_message", "reasoning", "reasoning", "assistant_message", "assistant_message"], + "separate Cursor reasoning and assistant segments must not be concatenated", + ); + const assistantMessages = conversationalItems.filter((item) => item.type === "assistant_message"); + assert.lengthOf(assistantMessages, 3); + assert.include(assistantMessages[0]?.text ?? "", "I'll use the update_plan tool"); + assert.include(assistantMessages[1]?.text ?? "", "No `update_plan` tool is available"); + assert.equal(assistantMessages[2]?.text, "todo list fixture complete"); + assert.lengthOf( + conversationalItems.filter((item) => item.type === "reasoning"), + 2, + ); + + const todoLists = projection.plans.filter((plan) => plan.kind === "todo_list"); + assert.isAtLeast(todoLists.length, 1); + assert.deepEqual( + todoLists.at(-1)?.steps.map((step) => step.status), + ["completed", "completed", "completed"], + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/cursor_transcript.ndjson new file mode 100644 index 00000000000..915b3b3c284 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/cursor_transcript.ndjson @@ -0,0 +1,104 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"todo_list","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-dc85ea29-1f94-4803-8d28-0dc358300ffe"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":true,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-dc85ea29-1f94-4803-8d28-0dc358300ffe"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Use the update_plan tool to track exactly three steps: inspect package.json, inspect tsconfig.json, report completion. Then read package.json and tsconfig.json, and answer exactly: todo list fixture complete","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","agentId":"agent-dc85ea29-1f94-4803-8d28-0dc358300ffe"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":"I'll"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" use"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" the update_plan"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" tool for"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" the three steps"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":","}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" then read both"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" config"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" files"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":".\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":30}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-started","callId":"tool_1afa2c76-8304-41d9-a59d-139ff962eb0","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-todo_list/package.json"}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-0-wgfq"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":31}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-started","callId":"tool_fb1809e9-fffb-4f76-9b0f-10e4f0f1f88","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-todo_list/tsconfig.json"}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-0-wgfq"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":17}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-completed","callId":"tool_1afa2c76-8304-41d9-a59d-139ff962eb0","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-todo_list/package.json"},"result":{"status":"success","value":{"content":"{\"name\":\"cursor-read-only-fixture\",\"private\":true,\"scripts\":{\"typecheck\":\"tsc --noEmit\"}}","totalLines":1,"fileSize":89}}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-0-wgfq"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":22}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-completed","callId":"tool_fb1809e9-fffb-4f76-9b0f-10e4f0f1f88","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-todo_list/tsconfig.json"},"result":{"status":"success","value":{"content":"{\"compilerOptions\":{\"module\":\"ESNext\",\"strict\":true,\"target\":\"ES2022\"}}","totalLines":1,"fileSize":71}}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-0-wgfq"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":18}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-started","callId":"tool_b9573f13-5878-4f12-8a4a-1ac6539d2c6","toolCall":{"type":"grep","args":{"pattern":"update_plan","offset":0}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-1-kj0v"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-started","callId":"tool_85d050cc-5276-4dea-9ae0-69d7d0c5dcf","toolCall":{"type":"glob","args":{"globPattern":"**/*"}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-1-kj0v"}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-completed","callId":"tool_b9573f13-5878-4f12-8a4a-1ac6539d2c6","toolCall":{"type":"grep","args":{"pattern":"update_plan","offset":0},"result":{"status":"success","value":{"workspaceResults":{"/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-cursor-agent-sdk-record-todo_list-CIb23S":{"type":"content","output":{"matches":[],"totalMatches":0}}}}}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-1-kj0v"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":45}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-completed","callId":"tool_85d050cc-5276-4dea-9ae0-69d7d0c5dcf","toolCall":{"type":"glob","args":{"globPattern":"**/*"},"result":{"status":"success","value":{"files":["./tsconfig.json","./package.json","./.git/logs/refs/heads/main","./.git/logs/HEAD","./.git/refs/heads/main","./.git/objects/ca/af47d623ededb9003c98e14a66bcfd037f0182","./.git/COMMIT_EDITMSG","./README.md","./.git/config","./.git/HEAD","./.git/hooks/push-to-checkout.sample","./.git/hooks/update.sample","./.git/hooks/pre-push.sample","./.git/hooks/pre-applypatch.sample","./.git/hooks/pre-merge-commit.sample","./.git/hooks/post-update.sample","./.git/hooks/prepare-commit-msg.sample","./.git/hooks/pre-receive.sample","./.git/hooks/fsmonitor-watchman.sample","./.git/hooks/applypatch-msg.sample","./.git/hooks/pre-commit.sample","./.git/hooks/sendemail-validate.sample","./.git/hooks/pre-rebase.sample","./.git/hooks/commit-msg.sample","./.git/description","./.git/info/exclude"],"totalFiles":26,"clientTruncated":false,"ripgrepTruncated":false}}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-1-kj0v"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":206}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-delta","text":"The update_plan tool"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-delta","text":" does not exist in the"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-delta","text":" MCP servers or codebase."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":6}}} +{"type":"emit_inbound","label":"thinking-completed","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-completed","thinkingDurationMs":608}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":30}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-started","callId":"tool_196feaa1-1381-49c6-8bf5-e716ff05eef","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-todo_list/README.md"}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-2-zbbf"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":10}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-completed","callId":"tool_196feaa1-1381-49c6-8bf5-e716ff05eef","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-todo_list/README.md"},"result":{"status":"success","value":{"content":"# cursor-agent-sdk-record-todo_list\n","totalLines":2,"fileSize":36}}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-2-zbbf"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":9}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-delta","text":"The update_plan tool"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-delta","text":" is unavailable."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-delta","text":"\n\nTodoWrite will replace"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":6}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-delta","text":" the unavailable update_plan"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":7}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-delta","text":" tool."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"thinking-completed","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"thinking-completed","thinkingDurationMs":984}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":"No `update_plan"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":"` tool is"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" available here; I'll"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" track"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" the three"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" steps with Todo"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":"Write instead"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":".\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"partial-tool-call","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"partial-tool-call","callId":"tool_64775a27-f6f1-4f85-9061-2038c1c6d57","toolCall":{"type":"updateTodos","args":{"todos":[{"content":"inspect package.json","status":"completed"}]}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-3-fvov"}}} +{"type":"emit_inbound","label":"partial-tool-call","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"partial-tool-call","callId":"tool_64775a27-f6f1-4f85-9061-2038c1c6d57","toolCall":{"type":"updateTodos","args":{"todos":[{"content":"inspect package.json","status":"completed"},{"content":"inspect tsconfig.json","status":"completed"}]}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-3-fvov"}}} +{"type":"emit_inbound","label":"partial-tool-call","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"partial-tool-call","callId":"tool_64775a27-f6f1-4f85-9061-2038c1c6d57","toolCall":{"type":"updateTodos","args":{"todos":[{"content":"inspect package.json","status":"completed"},{"content":"inspect tsconfig.json","status":"completed"},{"content":"report completion","status":"completed"}]}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-3-fvov"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":53}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-started","callId":"tool_64775a27-f6f1-4f85-9061-2038c1c6d57","toolCall":{"type":"updateTodos","args":{"todos":[{"content":"inspect package.json","status":"completed"},{"content":"inspect tsconfig.json","status":"completed"},{"content":"report completion","status":"completed"}]}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-3-fvov"}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"tool-call-completed","callId":"tool_64775a27-f6f1-4f85-9061-2038c1c6d57","toolCall":{"type":"updateTodos","args":{"todos":[{"content":"inspect package.json","status":"completed"},{"content":"inspect tsconfig.json","status":"completed"},{"content":"report completion","status":"completed"}]},"result":{"status":"success","value":{"todos":[{"content":"inspect package.json","status":"completed"},{"content":"inspect tsconfig.json","status":"completed"},{"content":"report completion","status":"completed"}],"totalCount":3}}},"modelCallId":"78b3b965-5601-4832-a723-a9259703c762-3-fvov"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":93}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":"todo"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"text-delta","text":" list fixture complete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"step-completed","stepId":13,"stepDurationMs":9044}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-50c35736-cb9d-4070-8299-8a4360c76af7","update":{"type":"turn-ended","usage":{"inputTokens":55956,"outputTokens":1036,"cacheReadTokens":40288,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-50c35736-cb9d-4070-8299-8a4360c76af7","requestId":"78b3b965-5601-4832-a723-a9259703c762","status":"finished","result":"todo list fixture complete","model":{"id":"composer-2.5"},"durationMs":11741}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-dc85ea29-1f94-4803-8d28-0dc358300ffe"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_output.ts new file mode 100644 index 00000000000..d9e778e28d5 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_output.ts @@ -0,0 +1,49 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertExecutionNodeKinds, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TODO_LIST_PROMPT, +} from "../shared.ts"; + +export function assertTodoListGrokOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertExecutionNodeKinds(projection, ["root_turn", "todo_list", "tool_call"]); + assertTurnItemTypes(projection, [ + "user_message", + "todo_list", + "file_search", + "assistant_message", + ]); + assertUserMessagesInclude(projection, [TODO_LIST_PROMPT]); + assertAssistantTextIncludes(projection, "todo list fixture complete"); + + const planEvents = result.domainEvents.filter((event) => event.type === "plan.updated"); + assert.lengthOf(planEvents, 2); + assert.lengthOf( + new Set(planEvents.map((event) => (event.type === "plan.updated" ? event.payload.id : null))), + 1, + "ACP plan updates must preserve one plan identity", + ); + const todoLists = projection.plans.filter((plan) => plan.kind === "todo_list"); + assert.isAtLeast(todoLists.length, 1); + assert.deepEqual( + todoLists.at(-1)?.steps.map((step) => step.status), + ["completed", "completed", "completed"], + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_transcript.ndjson new file mode 100644 index 00000000000..e726ac5b21a --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_transcript.ndjson @@ -0,0 +1,16 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"todo_list","metadata":{"generatedBy":"live-grok-shape-probe","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"grok-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"}]}}}} +{"type":"expect_outbound","label":"session.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Use the update_plan tool to track exactly three steps: inspect package.json, inspect tsconfig.json, report completion. Then read package.json and tsconfig.json, and answer exactly: todo list fixture complete"}]}}} +{"type":"emit_inbound","label":"assistant.progress","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"I'll track the three steps and inspect both files."}}}}} +{"type":"emit_inbound","label":"plan.started","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"plan","entries":[{"content":"Inspect package.json","priority":"high","status":"in_progress"},{"content":"Inspect tsconfig.json","priority":"high","status":"pending"},{"content":"Report completion","priority":"high","status":"pending"}]}}}} +{"type":"emit_inbound","label":"tool.package.started","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call","toolCallId":"read-package","title":"Read package.json","kind":"read","status":"pending","rawInput":{"path":"package.json"}}}}} +{"type":"emit_inbound","label":"tool.package.completed","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call_update","toolCallId":"read-package","status":"completed","rawOutput":{"content":"{\"name\":\"t3\"}"}}}}} +{"type":"emit_inbound","label":"tool.tsconfig.started","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call","toolCallId":"read-tsconfig","title":"Read tsconfig.json","kind":"read","status":"pending","rawInput":{"path":"tsconfig.json"}}}}} +{"type":"emit_inbound","label":"tool.tsconfig.completed","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call_update","toolCallId":"read-tsconfig","status":"completed","rawOutput":{"content":"{\"compilerOptions\":{}}"}}}}} +{"type":"emit_inbound","label":"plan.completed","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"plan","entries":[{"content":"Inspect package.json","priority":"high","status":"completed"},{"content":"Inspect tsconfig.json","priority":"high","status":"completed"},{"content":"Report completion","priority":"high","status":"completed"}]}}}} +{"type":"emit_inbound","label":"assistant.final","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"todo list fixture complete"}}}}} +{"type":"emit_inbound","label":"prompt.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/input.ts new file mode 100644 index 00000000000..8e41003bb6c --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/input.ts @@ -0,0 +1,7 @@ +import { TODO_LIST_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function todoListInput(): OrchestratorFixtureInput { + return { + steps: [{ type: "message", text: TODO_LIST_PROMPT }], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/claude_output.ts new file mode 100644 index 00000000000..41fced1a633 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/claude_output.ts @@ -0,0 +1,62 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertReplayLabelPrefixCount, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TOOL_CALL_READ_ONLY_PROMPT, +} from "../shared.ts"; + +export function assertToolCallReadOnlyClaudeOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, ["user_message", "dynamic_tool", "assistant_message"]); + assertUserMessagesInclude(projection, [TOOL_CALL_READ_ONLY_PROMPT]); + assertAssistantTextIncludes(projection, "read only tool fixture complete"); + assertRuntimeRequestCounts(projection, { total: 0 }); + assertReplayLabelPrefixCount(transcript, "permission.request:", 0); + assertReplayLabelPrefixCount(transcript, "permission.response:", 0); + assert.notInclude(JSON.stringify(transcript.entries), '"is_error":true'); + assert.notInclude(JSON.stringify(transcript.entries), "File does not exist"); + + const dynamicTools = projection.turnItems.filter((item) => item.type === "dynamic_tool"); + assert.lengthOf(dynamicTools, 2); + assert.lengthOf( + dynamicTools.filter((item) => item.toolName === "Glob"), + 0, + "Claude should not need discovery errors for this deterministic read-only fixture", + ); + assert.lengthOf( + dynamicTools.filter((item) => item.toolName === "Read"), + 2, + "Claude must project Read file tool calls", + ); + assert.isTrue( + dynamicTools.some((item) => + JSON.stringify(item.output ?? null).includes("claude-read-only-fixture"), + ), + "Claude must read package.json contents", + ); + assert.isTrue( + dynamicTools.some((item) => JSON.stringify(item.output ?? null).includes("ESNext")), + "Claude must read tsconfig.json contents", + ); + assert.lengthOf( + projection.turnItems.filter((item) => item.type === "approval_request"), + 0, + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/claude_transcript.ndjson new file mode 100644 index 00000000000..1278ba6b1e1 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/claude_transcript.ndjson @@ -0,0 +1,16 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"tool_call_read_only","metadata":{"prompts":["Read /tmp/claude-replay-tool_call_read_only/package.json and /tmp/claude-replay-tool_call_read_only/tsconfig.json, then answer exactly: read only tool fixture complete"],"model":"claude-sonnet-4-6","nativeSessionId":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9","queryMode":"streaming","tools":["Read","Glob","Grep"],"permissionMode":"dontAsk","allowedTools":["Read","Glob","Grep"],"generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":["Read","Glob","Grep"],"permissionMode":"dontAsk","allowedTools":["Read","Glob","Grep"],"sessionId":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Read /tmp/claude-replay-tool_call_read_only/package.json and /tmp/claude-replay-tool_call_read_only/tsconfig.json, then answer exactly: read only tool fixture complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"4d07cf82-fa99-4897-afc7-51775243eb93","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"1c40e207-bb7e-438e-b09e-8fe547a5fd7f","session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"4d07cf82-fa99-4897-afc7-51775243eb93","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"926cc2b6-6925-495e-95d4-15e722ed3e80","session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-tool_call_read_only","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"dontAsk","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"7d295979-f2e5-43cb-a9ee-b05f140f3ed8","session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01GA9LpBQUx46bYfJz1Dg3sx","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Let me read both files simultaneously.","signature":"EuMBClsIDRgCKkCgcxLVMMfd/q0XMe3G9eezJXNLWZHFeCeLQQe+ZV/7LgzunPPqJA4qIhBlaUd20MdLktQmupV8Pf3F0M48mCXvMhFjbGF1ZGUtc29ubmV0LTQtNjgAEgyXwqTEvhX6TK7UPN0aDFxE8OrDIanq7FuUWSIwylTxDMwjDluRqVJpzPtcyggC9mQWyDuXfygmPXaHmQC4tKfQm7u+n1Q+KXKkqQQQKjYALOZjd9uroAWLpWRN1lxeoD9ivHXr/I/DLToXbofDxYKZ7+AUJPsKWfIBAjUtu208DrI5/FsYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":3149,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9","uuid":"7afffaac-efb9-4e50-a30d-2fbab11e09ff"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01GA9LpBQUx46bYfJz1Dg3sx","type":"message","role":"assistant","content":[{"type":"text","text":"I'll read both files simultaneously!"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":3149,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9","uuid":"1431fb1c-7870-4f90-bb8b-f93f3e1c2c39"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01GA9LpBQUx46bYfJz1Dg3sx","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_017Q89TYYCDJPT2uLttrCn3A","name":"Read","input":{"file_path":"/tmp/claude-replay-tool_call_read_only/package.json"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":3149,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9","uuid":"b1f4ff95-1ab5-4b39-ad86-0b820abfba5a"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01GA9LpBQUx46bYfJz1Dg3sx","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CAafSU7YvuRYWdrFzpGCCW","name":"Read","input":{"file_path":"/tmp/claude-replay-tool_call_read_only/tsconfig.json"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":0,"cache_read_input_tokens":3149,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9","uuid":"4a184534-4099-48f8-8bd0-4b9aaab8868c"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"1d2f25d8-fb17-4671-bd83-c918afbabdff","session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_017Q89TYYCDJPT2uLttrCn3A","type":"tool_result","content":"1\t{\n2\t \"name\": \"claude-read-only-fixture\",\n3\t \"private\": true,\n4\t \"scripts\": {\n5\t \"typecheck\": \"tsc --noEmit\"\n6\t }\n7\t}"}]},"parent_tool_use_id":null,"session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9","uuid":"0d5bfbc0-e826-466a-bea8-54368db21675","timestamp":"2026-05-07T03:35:43.611Z","tool_use_result":{"type":"text","file":{"filePath":"/tmp/claude-replay-tool_call_read_only/package.json","content":"{\n \"name\": \"claude-read-only-fixture\",\n \"private\": true,\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\"\n }\n}","numLines":7,"startLine":1,"totalLines":7}}}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CAafSU7YvuRYWdrFzpGCCW","type":"tool_result","content":"1\t{\n2\t \"compilerOptions\": {\n3\t \"module\": \"ESNext\",\n4\t \"strict\": true,\n5\t \"target\": \"ES2022\"\n6\t }\n7\t}"}]},"parent_tool_use_id":null,"session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9","uuid":"259604ce-267c-4f3f-8a26-29e55f461d40","timestamp":"2026-05-07T03:35:43.652Z","tool_use_result":{"type":"text","file":{"filePath":"/tmp/claude-replay-tool_call_read_only/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"module\": \"ESNext\",\n \"strict\": true,\n \"target\": \"ES2022\"\n }\n}","numLines":7,"startLine":1,"totalLines":7}}}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_017vbtgCJT7W7ayU6dpcREpU","type":"message","role":"assistant","content":[{"type":"text","text":"read only tool fixture complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":328,"cache_read_input_tokens":3149,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":328},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9","uuid":"5f4a941e-08e7-4217-bc4c-a48cd3c237fa"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":4122,"duration_api_ms":3687,"num_turns":3,"result":"read only tool fixture complete","stop_reason":"end_turn","session_id":"473fb58f-85a3-4f21-b9d3-b1fdf5ef41f9","total_cost_usd":0.005561399999999999,"usage":{"input_tokens":4,"cache_creation_input_tokens":328,"cache_read_input_tokens":6298,"output_tokens":162,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":328,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":8,"cache_read_input_tokens":3149,"cache_creation_input_tokens":328,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":328},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":4,"outputTokens":162,"cacheReadInputTokens":6298,"cacheCreationInputTokens":328,"webSearchRequests":0,"costUSD":0.005561399999999999,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"6f61e05c-80c8-4b60-afd0-ce2d0a9eafc9"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/cursor_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/cursor_output.ts new file mode 100644 index 00000000000..f909d333603 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/cursor_output.ts @@ -0,0 +1,50 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TOOL_CALL_READ_ONLY_PROMPT, +} from "../shared.ts"; + +export function assertToolCallReadOnlyCursorOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, ["user_message", "file_search", "assistant_message"]); + assertUserMessagesInclude(projection, [TOOL_CALL_READ_ONLY_PROMPT]); + assertAssistantTextIncludes(projection, "read only tool fixture complete"); + assertRuntimeRequestCounts(projection, { total: 0 }); + + const assistantMessages = projection.turnItems.filter( + (item) => item.type === "assistant_message", + ); + assert.deepEqual( + assistantMessages.map((item) => item.text), + ["Reading both files now.\n", "read only tool fixture complete"], + "Cursor progress text and the final response must be separate messages", + ); + + const fileSearches = projection.turnItems.filter((item) => item.type === "file_search"); + assert.lengthOf(fileSearches, 2); + assert.isBelow(assistantMessages[0]?.ordinal ?? Infinity, fileSearches[0]?.ordinal ?? -Infinity); + assert.isBelow(fileSearches[1]?.ordinal ?? Infinity, assistantMessages[1]?.ordinal ?? -Infinity); + assert.isTrue( + fileSearches.some((item) => + JSON.stringify(item.results ?? []).includes("cursor-read-only-fixture"), + ), + ); + assert.isTrue(fileSearches.some((item) => JSON.stringify(item.results ?? []).includes("ES2022"))); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/cursor_transcript.ndjson new file mode 100644 index 00000000000..6238f8d6e67 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/cursor_transcript.ndjson @@ -0,0 +1,27 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"tool_call_read_only","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-3d017b33-d7fe-40aa-868b-0b7a68d0e15b"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":true,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-3d017b33-d7fe-40aa-868b-0b7a68d0e15b"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Read /tmp/claude-replay-tool_call_read_only/package.json and /tmp/claude-replay-tool_call_read_only/tsconfig.json, then answer exactly: read only tool fixture complete","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","agentId":"agent-3d017b33-d7fe-40aa-868b-0b7a68d0e15b"}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"text-delta","text":"Reading both files now"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"text-delta","text":".\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":13}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"tool-call-started","callId":"tool_56c7e2fb-8a39-4b1d-8b28-31f47bd717e","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-tool_call_read_only/package.json"}},"modelCallId":"fb59b086-fb0a-4263-ac27-2c53351e44ac-0-279u"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":13}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"tool-call-started","callId":"tool_a4c6ef64-f13c-4738-b527-1aad09b6185","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-tool_call_read_only/tsconfig.json"}},"modelCallId":"fb59b086-fb0a-4263-ac27-2c53351e44ac-0-279u"}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"tool-call-completed","callId":"tool_56c7e2fb-8a39-4b1d-8b28-31f47bd717e","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-tool_call_read_only/package.json"},"result":{"status":"success","value":{"content":"{\"name\":\"cursor-read-only-fixture\",\"private\":true,\"scripts\":{\"typecheck\":\"tsc --noEmit\"}}","totalLines":1,"fileSize":89}}},"modelCallId":"fb59b086-fb0a-4263-ac27-2c53351e44ac-0-279u"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":23}}} +{"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"tool-call-completed","callId":"tool_a4c6ef64-f13c-4738-b527-1aad09b6185","toolCall":{"type":"read","args":{"path":"/tmp/cursor-replay-tool_call_read_only/tsconfig.json"},"result":{"status":"success","value":{"content":"{\"compilerOptions\":{\"module\":\"ESNext\",\"strict\":true,\"target\":\"ES2022\"}}","totalLines":1,"fileSize":71}}},"modelCallId":"fb59b086-fb0a-4263-ac27-2c53351e44ac-0-279u"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":17}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"text-delta","text":"read"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"text-delta","text":" only tool fixture complete"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"token-delta","tokens":6}}} +{"type":"emit_inbound","label":"step-completed","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"step-completed","stepId":4,"stepDurationMs":2132}}} +{"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","update":{"type":"turn-ended","usage":{"inputTokens":20572,"outputTokens":118,"cacheReadTokens":14720,"cacheWriteTokens":0}}}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-db8a1002-593e-4baf-9ed1-d837c14643ed","requestId":"fb59b086-fb0a-4263-ac27-2c53351e44ac","status":"finished","result":"read only tool fixture complete","model":{"id":"composer-2.5"},"durationMs":4662}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-3d017b33-d7fe-40aa-868b-0b7a68d0e15b"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/grok_transcript.ndjson new file mode 100644 index 00000000000..e0742029b68 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/grok_transcript.ndjson @@ -0,0 +1,15 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"tool_call_read_only","metadata":{"generatedBy":"live-grok-shape-probe","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"grok-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"}]}}}} +{"type":"expect_outbound","label":"session.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Read /tmp/claude-replay-tool_call_read_only/package.json and /tmp/claude-replay-tool_call_read_only/tsconfig.json, then answer exactly: read only tool fixture complete"}]}}} +{"type":"emit_inbound","label":"assistant.progress","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"Reading both files now.\n"}}}}} +{"type":"emit_inbound","label":"tool.package.started","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call","toolCallId":"read-package","title":"Read package.json","kind":"read","status":"pending","rawInput":{"path":"/tmp/claude-replay-tool_call_read_only/package.json"}}}}} +{"type":"emit_inbound","label":"tool.package.running","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call_update","toolCallId":"read-package","status":"in_progress"}}}} +{"type":"emit_inbound","label":"tool.package.completed","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call_update","toolCallId":"read-package","status":"completed","rawOutput":{"content":"{\"name\":\"cursor-read-only-fixture\"}"}}}}} +{"type":"emit_inbound","label":"tool.tsconfig.started","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call","toolCallId":"read-tsconfig","title":"Read tsconfig.json","kind":"read","status":"pending","rawInput":{"path":"/tmp/claude-replay-tool_call_read_only/tsconfig.json"}}}}} +{"type":"emit_inbound","label":"tool.tsconfig.completed","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call_update","toolCallId":"read-tsconfig","status":"completed","rawOutput":{"content":"{\"compilerOptions\":{\"target\":\"ES2022\"}}"}}}}} +{"type":"emit_inbound","label":"assistant.final","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"read only tool fixture complete"}}}}} +{"type":"emit_inbound","label":"prompt.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/input.ts new file mode 100644 index 00000000000..8559e5239f2 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only/input.ts @@ -0,0 +1,7 @@ +import { TOOL_CALL_READ_ONLY_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function toolCallReadOnlyInput(): OrchestratorFixtureInput { + return { + steps: [{ type: "message", text: TOOL_CALL_READ_ONLY_PROMPT }], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/claude_output.ts new file mode 100644 index 00000000000..6bdf718ca02 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/claude_output.ts @@ -0,0 +1,50 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAllRuntimeRequestsResolved, + assertBaseProjection, + assertReplayLabelPrefixCount, + assertRuntimeRequestCounts, + assertRuntimeRequestKinds, + assertSemanticProjectionIntegrity, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TOOL_CALL_WRITE_PROMPT, +} from "../shared.ts"; + +export function assertToolCallReadOnlyOnRequestClaudeOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertUserMessagesInclude(projection, [TOOL_CALL_WRITE_PROMPT]); + assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 }); + assertRuntimeRequestKinds(projection, ["command"]); + assertAllRuntimeRequestsResolved(projection); + assertReplayLabelPrefixCount(transcript, "permission.request:", 1); + assertReplayLabelPrefixCount(transcript, "permission.response:", 1); + + assert.isAtLeast( + projection.turnItems.filter((i) => i.type === "command_execution" || i.type === "file_change") + .length, + 1, + "Claude must project at least one concrete tool item", + ); + assert.isAtLeast( + projection.turnItems.filter((item) => item.type === "approval_request").length, + 1, + "Claude must project a V2 approval request for the permission callback", + ); + assert.isAtLeast( + projection.turnItems.filter((item) => item.type === "assistant_message").length, + 1, + "Claude must project the final assistant message", + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/claude_transcript.ndjson new file mode 100644 index 00000000000..e9b0b981eec --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/claude_transcript.ndjson @@ -0,0 +1,15 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"tool_call_read_only_on_request","metadata":{"prompts":["Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP."],"model":"claude-sonnet-4-6","nativeSessionId":"2b73817d-f7ab-41a7-a533-31b13b698b0d","queryMode":"streaming","tools":"claude_code","permissionMode":"default","enablePermissionCallback":true,"generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"default","sessionId":"2b73817d-f7ab-41a7-a533-31b13b698b0d"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"ae3f0d68-becd-4b08-871b-f857a9ca1f90","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"1d299042-40fc-44af-8459-0a80baa7b7de","session_id":"2b73817d-f7ab-41a7-a533-31b13b698b0d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"ae3f0d68-becd-4b08-871b-f857a9ca1f90","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"5c410b64-6b03-49c4-9991-a8e624d0fe12","session_id":"2b73817d-f7ab-41a7-a533-31b13b698b0d"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-tool_call_read_only_on_request","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"default","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"c5dfaae5-ef85-4e12-8409-cc9351fd7210","session_id":"2b73817d-f7ab-41a7-a533-31b13b698b0d"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01RYtFxM6APgAoCYhiLedG4K","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to create or overwrite a file `.codex-probe-write-action.txt` with the exact text \"codex app-server approval fixture\". I'll use the Write tool to do this.","signature":"EuoCClsIDRgCKkClr6vi12d2VWDHyQI4PQ3CYaTuGSqke5KamWkoyt7GSszWGimltdaakn0h3D57BAMzZugqy2w6r5sxzVumSF+WMhFjbGF1ZGUtc29ubmV0LTQtNjgAEgx+g0cUWHR+Om1U0+0aDFf7COMwxRTyh5ltrCIwrLK4K+YAHJlq+V1ocE64ayjAFKrejgHNsqaAc1laYo6nwBSbGGTEL0A5qYF9yNl6KrwBBK2oZnFeIjkYKk31mNjrQ3ZdBKjHMrSa4PrqZWSTddS3rSDrFCAJPCewW5LUzy9lwI46DhHyK/b81RUao8cRd3YTpI3Bem4ze8hBoUJLGgm+BHTNC4wctLsZtacVMVJkpFozCCS/1slxj5WlzLKOarNfk299l86FD/VJpz7YxnQtYPUgaB+emkyWfjpk2Fy/RL1scue1gjBfuhQlaWZiy66XR/+6N221Xa6KtqAc6t+gSeeu3hYQ+7u+2PYYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11844,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":11844},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"2b73817d-f7ab-41a7-a533-31b13b698b0d","uuid":"b021382a-4ec3-4e73-906c-6449bc858477"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01RYtFxM6APgAoCYhiLedG4K","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CUcXE2dtk9uUvh3LpAWzhK","name":"Bash","input":{"command":"printf 'codex app-server approval fixture' > .codex-probe-write-action.txt","description":"Write exact text to .codex-probe-write-action.txt"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11844,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":11844},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"2b73817d-f7ab-41a7-a533-31b13b698b0d","uuid":"4887439e-b0cd-4d24-88d7-b60dfc5268b0"}} +{"type":"emit_inbound","label":"permission.request:Bash","frame":{"type":"permission.request","toolName":"Bash","input":{"command":"printf 'codex app-server approval fixture' > .codex-probe-write-action.txt","description":"Write exact text to .codex-probe-write-action.txt"},"options":{"suggestions":[{"type":"addDirectories","directories":["/private/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-claude-agent-sdk-record-tool_call_read_only_on_request-lW3JAE"],"destination":"session"}],"blockedPath":"/private/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-claude-agent-sdk-record-tool_call_read_only_on_request-lW3JAE/.codex-probe-write-action.txt","displayName":"Bash","toolUseID":"toolu_01CUcXE2dtk9uUvh3LpAWzhK"}}} +{"type":"expect_outbound","label":"permission.response:Bash","frame":{"type":"permission.response","result":{"behavior":"allow","updatedInput":{"command":"printf 'codex app-server approval fixture' > .codex-probe-write-action.txt","description":"Write exact text to .codex-probe-write-action.txt"},"toolUseID":"toolu_01CUcXE2dtk9uUvh3LpAWzhK","decisionClassification":"user_temporary"}}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"f35e140f-d07e-4155-8500-21dadb554186","session_id":"2b73817d-f7ab-41a7-a533-31b13b698b0d"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CUcXE2dtk9uUvh3LpAWzhK","type":"tool_result","content":"(Bash completed with no output)","is_error":false}]},"parent_tool_use_id":null,"session_id":"2b73817d-f7ab-41a7-a533-31b13b698b0d","uuid":"fc89ff85-b44d-4429-b85a-ccaf034bbcfc","timestamp":"2026-05-06T23:30:31.673Z","tool_use_result":{"stdout":"","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_011qF9hmJ6aS4YS2j41cqJhz","type":"message","role":"assistant","content":[{"type":"text","text":"Done. I used `printf` to write the exact text `codex app-server approval fixture` (no trailing newline) to `.codex-probe-write-action.txt` in the current working directory. The file was created (or overwritten if it already existed)."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":186,"cache_read_input_tokens":11844,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":186},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"2b73817d-f7ab-41a7-a533-31b13b698b0d","uuid":"af0006d0-bdda-4ac7-9724-cc877f7a7931"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":6918,"duration_api_ms":6218,"num_turns":2,"result":"Done. I used `printf` to write the exact text `codex app-server approval fixture` (no trailing newline) to `.codex-probe-write-action.txt` in the current working directory. The file was created (or overwritten if it already existed).","stop_reason":"end_turn","session_id":"2b73817d-f7ab-41a7-a533-31b13b698b0d","total_cost_usd":0.052097700000000004,"usage":{"input_tokens":4,"cache_creation_input_tokens":12030,"cache_read_input_tokens":11844,"output_tokens":228,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":12030,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":62,"cache_read_input_tokens":11844,"cache_creation_input_tokens":186,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":186},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":4,"outputTokens":228,"cacheReadInputTokens":11844,"cacheCreationInputTokens":12030,"webSearchRequests":0,"costUSD":0.052097700000000004,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"1b85e0c8-2cd3-44dc-b33a-4e084453cc9d"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/codex_output.ts new file mode 100644 index 00000000000..74948d6931e --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/codex_output.ts @@ -0,0 +1,36 @@ +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAllRuntimeRequestsResolved, + assertBaseProjection, + assertRuntimeRequestCounts, + assertRuntimeRequestKinds, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TOOL_CALL_WRITE_PROMPT, +} from "../shared.ts"; + +export function assertToolCallReadOnlyOnRequestOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, [ + "user_message", + "command_execution", + "approval_request", + "assistant_message", + ]); + assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 }); + assertRuntimeRequestKinds(projection, ["command"]); + assertAllRuntimeRequestsResolved(projection); + assertUserMessagesInclude(projection, [TOOL_CALL_WRITE_PROMPT]); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/codex_transcript.ndjson new file mode 100644 index 00000000000..17005a1b1c6 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/codex_transcript.ndjson @@ -0,0 +1,65 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"tool_call_read_only_on_request","metadata":{"source":"codex-app-server-probe","fileName":"tool_call_read_only_on_request.ndjson","description":"Write a small probe file with read-only full filesystem visibility and on-request approvals."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739376,"updatedAt":1776739376,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-42-56-019dadeb-5cd0-76e1-910c-3effc0cc5e67.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"approvalPolicy":"on-request","input":[{"text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP.","type":"text"}],"sandboxPolicy":{"networkAccess":false,"type":"readOnly"},"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739376,"updatedAt":1776739376,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-42-56-019dadeb-5cd0-76e1-910c-3effc0cc5e67.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turn":{"id":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","items":[],"status":"inProgress","error":null,"startedAt":1776739376,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"2e670fd4-fe2c-4186-b5ec-2adf65fa4423","content":[{"type":"text","text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP.","text_elements":[]}]},"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"2e670fd4-fe2c-4186-b5ec-2adf65fa4423","content":[{"type":"text","text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP.","text_elements":[]}]},"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_036b6500c9fbca5b0169e6e43488c481998992663af0a4d71f","summary":[],"content":[]},"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_036b6500c9fbca5b0169e6e43488c481998992663af0a4d71f","summary":[],"content":[]},"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd"}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","status":{"type":"active","activeFlags":["waitingOnApproval"]}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_amHJNIBwvjyYP8oLIY2jnZmZ","command":"/bin/zsh -lc \"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":null,"source":"agent","status":"inProgress","commandActions":[{"type":"unknown","command":"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd"}}} +{"type":"emit_inbound","label":"item/commandExecution/requestApproval","frame":{"method":"item/commandExecution/requestApproval","id":0,"params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"call_amHJNIBwvjyYP8oLIY2jnZmZ","reason":"Allow creating or overwriting .codex-probe-write-action.txt in the current workspace?","command":"/bin/zsh -lc \"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","commandActions":[{"type":"unknown","command":"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt"}],"proposedExecpolicyAmendment":["/bin/zsh","-lc","printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt"],"availableDecisions":["accept",{"acceptWithExecpolicyAmendment":{"execpolicy_amendment":["/bin/zsh","-lc","printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt"]}},"cancel"]}}} +{"type":"expect_outbound","label":"item/commandExecution/requestApproval","frame":{"id":0,"result":{"decision":"accept"}}} +{"type":"emit_inbound","label":"serverRequest/resolved","frame":{"method":"serverRequest/resolved","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","requestId":0}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","tokenUsage":{"total":{"totalTokens":28807,"inputTokens":28169,"cachedInputTokens":21376,"outputTokens":638,"reasoningOutputTokens":516},"last":{"totalTokens":28807,"inputTokens":28169,"cachedInputTokens":21376,"outputTokens":638,"reasoningOutputTokens":516},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_amHJNIBwvjyYP8oLIY2jnZmZ","command":"/bin/zsh -lc \"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"70053","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"unknown","command":"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt"}],"aggregatedOutput":null,"exitCode":0,"durationMs":0},"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"Created"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"/"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"over"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"w"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"rote"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":" `."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"-pro"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"be"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"-write"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"-action"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":".txt"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":" with"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":" exactly"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":":\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"```"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"text"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":" app"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"-server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":" approval"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","itemId":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","delta":"```"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_036b6500c9fbca5b0169e6e43f90448199920abc1d46f9f832","text":"Created/overwrote `.codex-probe-write-action.txt` with exactly:\n\n```text\ncodex app-server approval fixture\n```","phase":"final_answer","memoryCitation":null},"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turnId":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","tokenUsage":{"total":{"totalTokens":57691,"inputTokens":57021,"cachedInputTokens":49408,"outputTokens":670,"reasoningOutputTokens":516},"last":{"totalTokens":28884,"inputTokens":28852,"cachedInputTokens":28032,"outputTokens":32,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadeb-5cd0-76e1-910c-3effc0cc5e67","turn":{"id":"019dadeb-5cdc-7932-a5cc-c15b0a5abacd","items":[],"status":"completed","error":null,"startedAt":1776739376,"completedAt":1776739391,"durationMs":15502}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/grok_transcript.ndjson new file mode 100644 index 00000000000..b3d009f1281 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/grok_transcript.ndjson @@ -0,0 +1,13 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"tool_call_read_only_on_request","metadata":{"generatedBy":"live-grok-shape-probe","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"grok-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"}]}}}} +{"type":"expect_outbound","label":"session.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP."}]}}} +{"type":"emit_inbound","label":"tool.started","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call","toolCallId":"write-probe","title":"Write probe file","kind":"execute","status":"pending","rawInput":{"command":"printf fixture > .codex-probe-write-action.txt"}}}}} +{"type":"emit_inbound","label":"permission.request","frame":{"kind":"request","method":"session/request_permission","params":{"sessionId":"grok-replay-session-1","toolCall":{"toolCallId":"write-probe","title":"Write probe file","kind":"execute","status":"pending","content":[{"type":"content","content":{"type":"text","text":"Write .codex-probe-write-action.txt"}}]},"options":[{"optionId":"allow-once","name":"Allow once","kind":"allow_once"},{"optionId":"allow-always","name":"Allow for session","kind":"allow_always"},{"optionId":"reject-once","name":"Reject","kind":"reject_once"}]}}} +{"type":"expect_outbound","label":"permission.response","frame":{"kind":"response","method":"session/request_permission","result":{"outcome":{"outcome":"selected","optionId":"allow-once"}}}} +{"type":"emit_inbound","label":"tool.completed","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"tool_call_update","toolCallId":"write-probe","status":"completed","rawOutput":{"exitCode":0,"stdout":"","stderr":""}}}}} +{"type":"emit_inbound","label":"assistant.final","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"Created the requested probe file."}}}}} +{"type":"emit_inbound","label":"prompt.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/input.ts new file mode 100644 index 00000000000..5df27434a0a --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/input.ts @@ -0,0 +1,10 @@ +import { TOOL_CALL_WRITE_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function toolCallReadOnlyOnRequestInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: TOOL_CALL_WRITE_PROMPT }, + { type: "approve_next_runtime_request" }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/claude_output.ts new file mode 100644 index 00000000000..b29b8f78e41 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/claude_output.ts @@ -0,0 +1,41 @@ +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAllRuntimeRequestsResolved, + assertAssistantTextIncludes, + assertBaseProjection, + assertReplayLabelPrefixCount, + assertRuntimeRequestCounts, + assertRuntimeRequestKinds, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TOOL_CALL_WRITE_PROMPT, +} from "../shared.ts"; + +export function assertToolCallRestrictedGranularClaudeOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, [ + "user_message", + "command_execution", + "approval_request", + "assistant_message", + ]); + assertUserMessagesInclude(projection, [TOOL_CALL_WRITE_PROMPT]); + assertAssistantTextIncludes(projection, "codex app-server approval fixture"); + assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 }); + assertRuntimeRequestKinds(projection, ["command"]); + assertAllRuntimeRequestsResolved(projection); + assertReplayLabelPrefixCount(transcript, "permission.request:", 1); + assertReplayLabelPrefixCount(transcript, "permission.response:", 1); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/claude_transcript.ndjson new file mode 100644 index 00000000000..348a77c2bbd --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/claude_transcript.ndjson @@ -0,0 +1,15 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"tool_call_restricted_granular","metadata":{"prompts":["Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP."],"model":"claude-sonnet-4-6","nativeSessionId":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b","queryMode":"streaming","tools":"claude_code","permissionMode":"default","enablePermissionCallback":true,"generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"default","sessionId":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"ba333d12-6a9a-4a5e-9cb5-2d6ce18cd0f8","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"544c0cee-2195-49a9-ab30-088c3d3d5e83","session_id":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"ba333d12-6a9a-4a5e-9cb5-2d6ce18cd0f8","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"09419836-e234-4231-a15d-927acb1fbdb1","session_id":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-tool_call_restricted_granular","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"default","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"cc4bb931-a9d3-4819-8d00-58e00c2d81fe","session_id":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01DbHDhWivNDrjU961RBEG9p","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to create or overwrite a file called `.codex-probe-write-action.txt` with exactly the text \"codex app-server approval fixture\". I'll use the Write tool to do this.","signature":"EvMCClsIDRgCKkA0ya25b0YOVTPetE8tyofNk75/KAu2bOCsI5OuAA4QGPrPmQrwANVZrxU3sm28pCyXwwS/QRHWvKNw8o8ewFfBMhFjbGF1ZGUtc29ubmV0LTQtNjgAEgyBNStUvXa2J+3E/YgaDKD5vB/dvuXfFaB9QyIwVs4Dk5D21dBC5+DbUFpY60RQMafxcmSkL4Li3yVljtNzv4Xu6r3hwy4j8Vx+hKNPKsUBnZzPx9BrnInN42xNoQMW/Gmx6yn1jD02wnol96oqWWPuM1jaAtPD92I2Fq2h64yWC0SCX3+mYDjYSo3jN/K/++w5+d8YZ1ciglXFPCjJ/dbD8ZcA5MGjNuUEVerGqaO8MAmai7v7NzMgf2mGggZJBll7rLYDFo/d5tNBcEKsM5lqRBqwKaXxiNmNGWHv5e3x04cwLUVUD9VCxjhFoh9td5K737EuLDu43StHlqHLNsH1JJ3ipZR9wPrYQ7ZIfdTjt8cpb58YAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2389,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2389},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b","uuid":"5404910b-43e2-4261-a599-428eece3b4ed"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01DbHDhWivNDrjU961RBEG9p","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Ayxc73fyGSj2frmw8XLmJn","name":"Bash","input":{"command":"printf 'codex app-server approval fixture' > .codex-probe-write-action.txt","description":"Write exact text to .codex-probe-write-action.txt"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2389,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2389},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b","uuid":"bd8a3c7b-706b-4833-a859-2914fe94da80"}} +{"type":"emit_inbound","label":"permission.request:Bash","frame":{"type":"permission.request","toolName":"Bash","input":{"command":"printf 'codex app-server approval fixture' > .codex-probe-write-action.txt","description":"Write exact text to .codex-probe-write-action.txt"},"options":{"suggestions":[{"type":"addDirectories","directories":["/private/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-claude-agent-sdk-record-tool_call_restricted_granular-6FUxAp"],"destination":"session"}],"blockedPath":"/private/var/folders/b3/b51_pdxj7dl0t981zpqkxxhr0000gn/T/t3-orchestrator-v2-claude-agent-sdk-record-tool_call_restricted_granular-6FUxAp/.codex-probe-write-action.txt","displayName":"Bash","toolUseID":"toolu_01Ayxc73fyGSj2frmw8XLmJn"}}} +{"type":"expect_outbound","label":"permission.response:Bash","frame":{"type":"permission.response","result":{"behavior":"allow","updatedInput":{"command":"printf 'codex app-server approval fixture' > .codex-probe-write-action.txt","description":"Write exact text to .codex-probe-write-action.txt"},"toolUseID":"toolu_01Ayxc73fyGSj2frmw8XLmJn","decisionClassification":"user_temporary"}}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"a2b5e849-0d78-45f9-9b80-3c8e81949adb","session_id":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Ayxc73fyGSj2frmw8XLmJn","type":"tool_result","content":"(Bash completed with no output)","is_error":false}]},"parent_tool_use_id":null,"session_id":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b","uuid":"35e1a0cd-5946-4b13-b8c8-ae65f859c447","timestamp":"2026-05-07T01:41:04.458Z","tool_use_result":{"stdout":"","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_014BHCrtTfAbEdhQ3oBkZUAZ","type":"message","role":"assistant","content":[{"type":"text","text":"Done. The file `.codex-probe-write-action.txt` was created (or overwritten) in the current directory with the exact content `codex app-server approval fixture` — no trailing newline, no extra characters."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":187,"cache_read_input_tokens":11844,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":187},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b","uuid":"ea75d6df-7f51-4d9b-ae74-c6e0fbf1d3f6"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":6064,"duration_api_ms":5641,"num_turns":2,"result":"Done. The file `.codex-probe-write-action.txt` was created (or overwritten) in the current directory with the exact content `codex app-server approval fixture` — no trailing newline, no extra characters.","stop_reason":"end_turn","session_id":"ffd3df75-f3cf-433d-bb67-1dd1e9db316b","total_cost_usd":0.0193617,"usage":{"input_tokens":4,"cache_creation_input_tokens":2576,"cache_read_input_tokens":21299,"output_tokens":220,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2576,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":53,"cache_read_input_tokens":11844,"cache_creation_input_tokens":187,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":187},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":4,"outputTokens":220,"cacheReadInputTokens":21299,"cacheCreationInputTokens":2576,"webSearchRequests":0,"costUSD":0.0193617,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"11c4a92a-677b-4caf-b7bf-4d0f1d5f7c08"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/codex_output.ts new file mode 100644 index 00000000000..7217f4005c1 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/codex_output.ts @@ -0,0 +1,37 @@ +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAllRuntimeRequestsResolved, + assertBaseProjection, + assertRuntimeRequestCounts, + assertRuntimeRequestKinds, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TOOL_CALL_WRITE_PROMPT, +} from "../shared.ts"; + +export function assertToolCallRestrictedGranularOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, [ + "user_message", + "command_execution", + "file_change", + "approval_request", + "assistant_message", + ]); + assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 }); + assertRuntimeRequestKinds(projection, ["file-change"]); + assertAllRuntimeRequestsResolved(projection); + assertUserMessagesInclude(projection, [TOOL_CALL_WRITE_PROMPT]); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/codex_transcript.ndjson new file mode 100644 index 00000000000..4f99c9b4e06 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/codex_transcript.ndjson @@ -0,0 +1,137 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"tool_call_restricted_granular","metadata":{"source":"codex-app-server-probe","fileName":"tool_call_restricted_granular.ndjson","description":"Write a small probe file with restricted read access and granular approval flags enabled."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019dadeb-c0d3-7331-8353-a710f2912df3","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739401,"updatedAt":1776739401,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-43-21-019dadeb-c0d3-7331-8353-a710f2912df3.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"approvalPolicy":{"granular":{"mcp_elicitations":true,"request_permissions":true,"rules":true,"sandbox_approval":true,"skill_approval":true}},"input":[{"text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP.","type":"text"}],"sandboxPolicy":{"networkAccess":false,"type":"readOnly"},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019dadeb-c0d3-7331-8353-a710f2912df3","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739401,"updatedAt":1776739401,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-43-21-019dadeb-c0d3-7331-8353-a710f2912df3.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019dadeb-c0de-7803-b8fc-a791b566ced6","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turn":{"id":"019dadeb-c0de-7803-b8fc-a791b566ced6","items":[],"status":"inProgress","error":null,"startedAt":1776739401,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"e43e205e-f22b-4ab0-9b88-69b41f7fec16","content":[{"type":"text","text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP.","text_elements":[]}]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"e43e205e-f22b-4ab0-9b88-69b41f7fec16","content":[{"type":"text","text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP.","text_elements":[]}]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e44e641c819abe2c74de8be2df7c","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e44e641c819abe2c74de8be2df7c","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","text":"","phase":"commentary","memoryCitation":null},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":"Writing"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" file"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" now"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" with"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" single"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" local"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" shell"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":" command"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_068036a3123897970169e6e452cf78819a87cbdc7d3da1631f","text":"Writing the fixture file now with a single local shell command.","phase":"commentary","memoryCitation":null},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","tokenUsage":{"total":{"totalTokens":27988,"inputTokens":27471,"cachedInputTokens":21376,"outputTokens":517,"reasoningOutputTokens":400},"last":{"totalTokens":27988,"inputTokens":27471,"cachedInputTokens":21376,"outputTokens":517,"reasoningOutputTokens":400},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e4549b5c819aaf17f9850caeee02","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e4549b5c819aaf17f9850caeee02","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","tokenUsage":{"total":{"totalTokens":56381,"inputTokens":55562,"cachedInputTokens":48384,"outputTokens":819,"reasoningOutputTokens":648},"last":{"totalTokens":28393,"inputTokens":28091,"cachedInputTokens":27008,"outputTokens":302,"reasoningOutputTokens":248},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_Ldr19xmcSmrCDZPWOU281hio","command":"/bin/zsh -lc \"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"67342","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"unknown","command":"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_Ldr19xmcSmrCDZPWOU281hio","command":"/bin/zsh -lc \"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"67342","source":"unifiedExecStartup","status":"failed","commandActions":[{"type":"unknown","command":"printf '%s' 'codex app-server approval fixture' > .codex-probe-write-action.txt"}],"aggregatedOutput":null,"exitCode":-1,"durationMs":0},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e45a524c819a89b5d0e20d23b4b2","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e45a524c819a89b5d0e20d23b4b2","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","text":"","phase":"commentary","memoryCitation":null},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":"The"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" sandbox"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" rejected"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" shell"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" write"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" without"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" producing"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" diagnostic"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" I"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":"’m"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" switching"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" to"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" file"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":"-edit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" tool"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" for"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" same"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" single"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":"-file"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":" change"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_068036a3123897970169e6e45b9ea8819aa4ca6c7150b75e02","text":"The sandbox rejected the shell write without producing a diagnostic. I’m switching to the file-edit tool for the same single-file change.","phase":"commentary","memoryCitation":null},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"fileChange","id":"call_CjSAZQlgh7pnYe8dEL4Ri8Q5","changes":[{"path":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server/.codex-probe-write-action.txt","kind":{"type":"add"},"diff":"codex app-server approval fixture\n"}],"status":"inProgress"},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","status":{"type":"active","activeFlags":["waitingOnApproval"]}}}} +{"type":"emit_inbound","label":"item/fileChange/requestApproval","frame":{"method":"item/fileChange/requestApproval","id":0,"params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"call_CjSAZQlgh7pnYe8dEL4Ri8Q5","reason":null,"grantRoot":null}}} +{"type":"expect_outbound","label":"item/fileChange/requestApproval","frame":{"id":0,"result":{"decision":"accept"}}} +{"type":"emit_inbound","label":"serverRequest/resolved","frame":{"method":"serverRequest/resolved","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","requestId":0}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"item/fileChange/outputDelta","frame":{"method":"item/fileChange/outputDelta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"call_CjSAZQlgh7pnYe8dEL4Ri8Q5","delta":"sandbox-exec: execvp() of '/opt/homebrew/bin/codex' failed: Operation not permitted\n"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","tokenUsage":{"total":{"totalTokens":84983,"inputTokens":84000,"cachedInputTokens":76416,"outputTokens":983,"reasoningOutputTokens":737},"last":{"totalTokens":28602,"inputTokens":28438,"cachedInputTokens":28032,"outputTokens":164,"reasoningOutputTokens":89},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/fileChange/outputDelta","frame":{"method":"item/fileChange/outputDelta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"call_CjSAZQlgh7pnYe8dEL4Ri8Q5","delta":"Success. Updated the following files:\nA .codex-probe-write-action.txt\n"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"fileChange","id":"call_CjSAZQlgh7pnYe8dEL4Ri8Q5","changes":[{"path":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server/.codex-probe-write-action.txt","kind":{"type":"add"},"diff":"codex app-server approval fixture\n"}],"status":"completed"},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"turn/diff/updated","frame":{"method":"turn/diff/updated","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","diff":"diff --git a/packages/effect-codex-app-server/.codex-probe-write-action.txt b/packages/effect-codex-app-server/.codex-probe-write-action.txt\nindex e268d360687fcf05587151030b12bd2f96437cf7..6e495a5a1d7e41515ba0968b0f65b3675bfb0a01\n--- a/packages/effect-codex-app-server/.codex-probe-write-action.txt\n+++ b/packages/effect-codex-app-server/.codex-probe-write-action.txt\n@@ -1 +1 @@\n-codex app-server approval fixture\n\\ No newline at end of file\n+codex app-server approval fixture\n"}}} +{"type":"emit_inbound","label":"turn/diff/updated","frame":{"method":"turn/diff/updated","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","diff":"diff --git a/packages/effect-codex-app-server/.codex-probe-write-action.txt b/packages/effect-codex-app-server/.codex-probe-write-action.txt\nindex e268d360687fcf05587151030b12bd2f96437cf7..6e495a5a1d7e41515ba0968b0f65b3675bfb0a01\n--- a/packages/effect-codex-app-server/.codex-probe-write-action.txt\n+++ b/packages/effect-codex-app-server/.codex-probe-write-action.txt\n@@ -1 +1 @@\n-codex app-server approval fixture\n\\ No newline at end of file\n+codex app-server approval fixture\n"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e45df0f4819ab4d2890641352e6b","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e45df0f4819ab4d2890641352e6b","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","tokenUsage":{"total":{"totalTokens":114190,"inputTokens":112645,"cachedInputTokens":104448,"outputTokens":1545,"reasoningOutputTokens":1253},"last":{"totalTokens":29207,"inputTokens":28645,"cachedInputTokens":28032,"outputTokens":562,"reasoningOutputTokens":516},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_3CTnF0RRhAPV1UmQxnNl9F1T","command":"/bin/zsh -lc 'truncate -s -1 .codex-probe-write-action.txt'","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"81884","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"unknown","command":"truncate -s -1 .codex-probe-write-action.txt"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_3CTnF0RRhAPV1UmQxnNl9F1T","command":"/bin/zsh -lc 'truncate -s -1 .codex-probe-write-action.txt'","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"81884","source":"unifiedExecStartup","status":"failed","commandActions":[{"type":"unknown","command":"truncate -s -1 .codex-probe-write-action.txt"}],"aggregatedOutput":null,"exitCode":-1,"durationMs":0},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"turn/diff/updated","frame":{"method":"turn/diff/updated","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","diff":"diff --git a/packages/effect-codex-app-server/.codex-probe-write-action.txt b/packages/effect-codex-app-server/.codex-probe-write-action.txt\nindex e268d360687fcf05587151030b12bd2f96437cf7..6e495a5a1d7e41515ba0968b0f65b3675bfb0a01\n--- a/packages/effect-codex-app-server/.codex-probe-write-action.txt\n+++ b/packages/effect-codex-app-server/.codex-probe-write-action.txt\n@@ -1 +1 @@\n-codex app-server approval fixture\n\\ No newline at end of file\n+codex app-server approval fixture\n"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e465c344819aae78ff3d9f491306","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_068036a3123897970169e6e465c344819aae78ff3d9f491306","summary":[],"content":[]},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"Created"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" `."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"-pro"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"be"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"-write"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"-action"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":".txt"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" with"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" the"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" requested"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" text"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" using"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" a"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" local"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" file"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" edit"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" No"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" Git"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"Hub"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" web"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" MCP"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":","}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" or"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" package"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" metadata"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" access"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":" used"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","itemId":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","delta":"."}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_068036a3123897970169e6e46cc118819ab235e4613b1adcbb","text":"Created `.codex-probe-write-action.txt` with the requested text using a local file edit. No GitHub, web, MCP, or package metadata access used.","phase":"final_answer","memoryCitation":null},"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","tokenUsage":{"total":{"totalTokens":143998,"inputTokens":141897,"cachedInputTokens":132992,"outputTokens":2101,"reasoningOutputTokens":1769},"last":{"totalTokens":29808,"inputTokens":29252,"cachedInputTokens":28544,"outputTokens":556,"reasoningOutputTokens":516},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"turn/diff/updated","frame":{"method":"turn/diff/updated","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turnId":"019dadeb-c0de-7803-b8fc-a791b566ced6","diff":"diff --git a/packages/effect-codex-app-server/.codex-probe-write-action.txt b/packages/effect-codex-app-server/.codex-probe-write-action.txt\nindex e268d360687fcf05587151030b12bd2f96437cf7..6e495a5a1d7e41515ba0968b0f65b3675bfb0a01\n--- a/packages/effect-codex-app-server/.codex-probe-write-action.txt\n+++ b/packages/effect-codex-app-server/.codex-probe-write-action.txt\n@@ -1 +1 @@\n-codex app-server approval fixture\n\\ No newline at end of file\n+codex app-server approval fixture\n"}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadeb-c0d3-7331-8353-a710f2912df3","turn":{"id":"019dadeb-c0de-7803-b8fc-a791b566ced6","items":[],"status":"completed","error":null,"startedAt":1776739401,"completedAt":1776739437,"durationMs":35380}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/input.ts new file mode 100644 index 00000000000..f6c14cd94b4 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_restricted_granular/input.ts @@ -0,0 +1,10 @@ +import { TOOL_CALL_WRITE_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function toolCallRestrictedGranularInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: TOOL_CALL_WRITE_PROMPT }, + { type: "approve_next_runtime_request" }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/claude_output.ts new file mode 100644 index 00000000000..55cd22cec32 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/claude_output.ts @@ -0,0 +1,34 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertReplayLabelPrefixCount, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TOOL_CALL_WRITE_PROMPT, +} from "../shared.ts"; + +export function assertToolCallWorkspaceNeverClaudeOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, ["user_message", "command_execution", "assistant_message"]); + assertUserMessagesInclude(projection, [TOOL_CALL_WRITE_PROMPT]); + assertAssistantTextIncludes(projection, "codex app-server approval fixture"); + assertRuntimeRequestCounts(projection, { total: 0 }); + assert.equal(projection.turnItems.filter((item) => item.type === "approval_request").length, 0); + assertReplayLabelPrefixCount(transcript, "permission.request:", 0); + assertReplayLabelPrefixCount(transcript, "permission.response:", 0); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/claude_transcript.ndjson new file mode 100644 index 00000000000..9b7288fae65 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/claude_transcript.ndjson @@ -0,0 +1,13 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"tool_call_workspace_never","metadata":{"prompts":["Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP."],"model":"claude-sonnet-4-6","nativeSessionId":"d5a53b77-f5e5-4b4b-b387-65392391e9c9","queryMode":"streaming","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"d5a53b77-f5e5-4b4b-b387-65392391e9c9"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"7c9c2603-f09a-4200-93a0-3b5a2d9f7b7d","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"b8818ae6-b7d3-4d82-b22d-4c93a4ca297e","session_id":"d5a53b77-f5e5-4b4b-b387-65392391e9c9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"7c9c2603-f09a-4200-93a0-3b5a2d9f7b7d","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"cfb9d36b-2b7d-4e20-a097-a6435b424cb2","session_id":"d5a53b77-f5e5-4b4b-b387-65392391e9c9"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-tool_call_workspace_never","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"f27f371a-cb8f-4b65-a45c-b6140b83de93","session_id":"d5a53b77-f5e5-4b4b-b387-65392391e9c9"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01GQZgjX6VSrRs4s66TgykJA","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to create or overwrite a file called `.codex-probe-write-action.txt` with exactly the text \"codex app-server approval fixture\". I'll use the Write tool to do this.","signature":"EvMCClsIDRgCKkA0ya25b0YOVTPetE8tyofNk75/KAu2bOCsI5OuAA4QGPrPmQrwANVZrxU3sm28pCyXwwS/QRHWvKNw8o8ewFfBMhFjbGF1ZGUtc29ubmV0LTQtNjgAEgxmiLI12jaPlA5zzlEaDHaQPVLmpq9Et6BpmSIwpM8rayY3bPZo77J1XMmGDz5TVtFpYEUI30e7ErnsceW5jPAqqBsIibR3TpEOJmGeKsUBwuMobTb4l172BeZhif8O/HyWbW0hNPkVFrfzcsfx2gC3CiAzJiaTK7qPKYemFpSGzp8DwpzubxwVXuZdWlNxuhm21gTsx1H5BKPaIzPk9xD1gEMFnZgEh5Fs8jojN1Xn6XpQOuk3okL19/KT3muV/Z2C7TT0FTMEABI6Sk+RvzY0IuvmO+vMNFLS5xWIpUPyB5FFAmXn6yv/8z1eVG0S7Koyqmg7v7AO8OHxj8xX85kE4xB+FP4woAUUs3sF1f9VgcWoN+4YAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2389,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2389},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"d5a53b77-f5e5-4b4b-b387-65392391e9c9","uuid":"fb7a6901-22b2-40f6-9741-681310cd0920"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01GQZgjX6VSrRs4s66TgykJA","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FQqJNkP2G4VexujpTyb9QK","name":"Bash","input":{"command":"printf 'codex app-server approval fixture' > .codex-probe-write-action.txt","description":"Write exact text to .codex-probe-write-action.txt"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2389,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2389},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"d5a53b77-f5e5-4b4b-b387-65392391e9c9","uuid":"1c6c4d15-a807-4fc0-8061-08e73d3f5a64"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"b9ed927a-4036-4761-adad-903db0eeded4","session_id":"d5a53b77-f5e5-4b4b-b387-65392391e9c9"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FQqJNkP2G4VexujpTyb9QK","type":"tool_result","content":"(Bash completed with no output)","is_error":false}]},"parent_tool_use_id":null,"session_id":"d5a53b77-f5e5-4b4b-b387-65392391e9c9","uuid":"b4842b76-eaed-4e14-825c-3e65fa199b59","timestamp":"2026-05-07T01:40:47.865Z","tool_use_result":{"stdout":"","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01ALXv3UiL2KfbTV1dxPnhWt","type":"message","role":"assistant","content":[{"type":"text","text":"Done. I used `printf` to write exactly `codex app-server approval fixture` (no trailing newline) to `.codex-probe-write-action.txt` in the current working directory. The command completed successfully with no errors."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":187,"cache_read_input_tokens":11844,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":187},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"d5a53b77-f5e5-4b4b-b387-65392391e9c9","uuid":"da722e7b-e35c-47fc-b7a1-c01fed1dd46c"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":6172,"duration_api_ms":5548,"num_turns":2,"result":"Done. I used `printf` to write exactly `codex app-server approval fixture` (no trailing newline) to `.codex-probe-write-action.txt` in the current working directory. The command completed successfully with no errors.","stop_reason":"end_turn","session_id":"d5a53b77-f5e5-4b4b-b387-65392391e9c9","total_cost_usd":0.019391699999999998,"usage":{"input_tokens":4,"cache_creation_input_tokens":2576,"cache_read_input_tokens":21299,"output_tokens":222,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":2576,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":55,"cache_read_input_tokens":11844,"cache_creation_input_tokens":187,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":187},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":4,"outputTokens":222,"cacheReadInputTokens":21299,"cacheCreationInputTokens":2576,"webSearchRequests":0,"costUSD":0.019391699999999998,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"f5a7d267-410b-47e6-8296-1a71551565bc"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/codex_output.ts new file mode 100644 index 00000000000..d6bdc414c75 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/codex_output.ts @@ -0,0 +1,29 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TOOL_CALL_WRITE_PROMPT, +} from "../shared.ts"; + +export function assertToolCallWorkspaceNeverOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, ["user_message", "command_execution", "assistant_message"]); + assertUserMessagesInclude(projection, [TOOL_CALL_WRITE_PROMPT]); + assertRuntimeRequestCounts(projection, { total: 0 }); + assert.equal(projection.turnItems.filter((item) => item.type === "approval_request").length, 0); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/codex_transcript.ndjson new file mode 100644 index 00000000000..e989da4bbd3 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/codex_transcript.ndjson @@ -0,0 +1,57 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"tool_call_workspace_never","metadata":{"source":"codex-app-server-probe","fileName":"tool_call_workspace_never.ndjson","description":"Write a small probe file with workspace-write sandbox policy and never approvals."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739392,"updatedAt":1776739392,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-43-12-019dadeb-9a1f-7d61-8e4b-92d487d180b4.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"approvalPolicy":"never","input":[{"text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP.","type":"text"}],"sandboxPolicy":{"networkAccess":false,"type":"workspaceWrite","writableRoots":[]},"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739392,"updatedAt":1776739392,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-43-12-019dadeb-9a1f-7d61-8e4b-92d487d180b4.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turn":{"id":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","items":[],"status":"inProgress","error":null,"startedAt":1776739392,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"fabe6753-e817-4530-af44-2981cc32d64b","content":[{"type":"text","text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP.","text_elements":[]}]},"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"fabe6753-e817-4530-af44-2981cc32d64b","content":[{"type":"text","text":"Create or overwrite .codex-probe-write-action.txt with exactly this text: codex app-server approval fixture. Use a local shell command or file edit only, then briefly report what happened. Do not read package metadata, use GitHub, use web, or use MCP.","text_elements":[]}]},"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"reasoning","id":"rs_0b179d008122e92d0169e6e444dbcc8199a1dfe44c0c295d8c","summary":[],"content":[]},"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"reasoning","id":"rs_0b179d008122e92d0169e6e444dbcc8199a1dfe44c0c295d8c","summary":[],"content":[]},"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","tokenUsage":{"total":{"totalTokens":27205,"inputTokens":26816,"cachedInputTokens":3456,"outputTokens":389,"reasoningOutputTokens":336},"last":{"totalTokens":27205,"inputTokens":26816,"cachedInputTokens":3456,"outputTokens":389,"reasoningOutputTokens":336},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_lLtECQhta9io8u1TEtgo5DJ0","command":"/bin/zsh -lc \"printf %s 'codex app-server approval fixture' > .codex-probe-write-action.txt\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"71918","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"unknown","command":"printf %s 'codex app-server approval fixture' > .codex-probe-write-action.txt"}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"commandExecution","id":"call_lLtECQhta9io8u1TEtgo5DJ0","command":"/bin/zsh -lc \"printf %s 'codex app-server approval fixture' > .codex-probe-write-action.txt\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","processId":"71918","source":"unifiedExecStartup","status":"completed","commandActions":[{"type":"unknown","command":"printf %s 'codex app-server approval fixture' > .codex-probe-write-action.txt"}],"aggregatedOutput":null,"exitCode":0,"durationMs":0},"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"Created"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"/"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"over"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"w"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"rote"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":" `."}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"-pro"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"be"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"-write"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"-action"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":".txt"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":" with"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":" exactly"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":":\n\n"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"`"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"cod"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"ex"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":" app"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"-server"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":" approval"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":" fixture"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","itemId":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","delta":"`"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0b179d008122e92d0169e6e44980f88199b7bfbd7f33eaff77","text":"Created/overwrote `.codex-probe-write-action.txt` with exactly:\n\n`codex app-server approval fixture`","phase":"final_answer","memoryCitation":null},"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f"}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turnId":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","tokenUsage":{"total":{"totalTokens":54484,"inputTokens":54066,"cachedInputTokens":29952,"outputTokens":418,"reasoningOutputTokens":336},"last":{"totalTokens":27279,"inputTokens":27250,"cachedInputTokens":26496,"outputTokens":29,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":0,"windowDurationMins":300,"resetsAt":1776748632},"secondary":{"usedPercent":0,"windowDurationMins":10080,"resetsAt":1777335432},"credits":null,"planType":"pro"}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019dadeb-9a1f-7d61-8e4b-92d487d180b4","turn":{"id":"019dadeb-9a2a-7e92-abef-82bf1c3d103f","items":[],"status":"completed","error":null,"startedAt":1776739392,"completedAt":1776739401,"durationMs":9704}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/input.ts new file mode 100644 index 00000000000..d45698ce5fc --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_workspace_never/input.ts @@ -0,0 +1,7 @@ +import { TOOL_CALL_WRITE_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function toolCallWorkspaceNeverInput(): OrchestratorFixtureInput { + return { + steps: [{ type: "message", text: TOOL_CALL_WRITE_PROMPT }], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/claude_output.ts new file mode 100644 index 00000000000..cbcf90cc962 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/claude_output.ts @@ -0,0 +1,51 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TURN_INTERRUPT_PROMPT, +} from "../shared.ts"; + +export function assertTurnInterruptClaudeOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assert.equal(transcript.provider, "claudeAgent"); + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["interrupted"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, [ + "user_message", + "run_interrupt_request", + "run_interrupt_result", + ]); + assertUserMessagesInclude(projection, [TURN_INTERRUPT_PROMPT]); + + const interruptRequest = projection.turnItems.find( + (item) => item.type === "run_interrupt_request", + ); + const interruptResult = projection.turnItems.find((item) => item.type === "run_interrupt_result"); + assert.isDefined(interruptRequest); + assert.isDefined(interruptResult); + assert.equal(interruptRequest.status, "completed"); + assert.equal(interruptResult.status, "interrupted"); + assert.equal(interruptResult.parentItemId, interruptRequest.id); + assert.deepEqual( + projection.attempts.map((attempt) => attempt.status), + ["interrupted"], + ); + assert.deepEqual( + projection.nodes.map((node) => node.status), + ["interrupted"], + ); + assert.equal(projection.providerThreads[0]?.status, "idle"); + assert.equal(projection.providerTurns[0]?.status, "interrupted"); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/claude_transcript.ndjson new file mode 100644 index 00000000000..a036d00cd01 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/claude_transcript.ndjson @@ -0,0 +1,7 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"turn_interrupt","metadata":{"prompts":["Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally."],"model":"claude-sonnet-4-6","nativeSessionId":"2da9a7f1-f9cd-4d45-abd7-25f5370301f3","queryMode":"interrupt","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"2da9a7f1-f9cd-4d45-abd7-25f5370301f3"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally."},"parent_tool_use_id":null}}} +{"type":"expect_outbound","label":"query.interrupt:1","frame":{"type":"query.interrupt"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"7dd3898f-86a3-4aac-ae8a-222fe62cb13f","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"34154a8b-b7bb-4204-8c14-d9feb1971e17","session_id":"2da9a7f1-f9cd-4d45-abd7-25f5370301f3"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"7dd3898f-86a3-4aac-ae8a-222fe62cb13f","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"3e1cab63-3de4-4a98-a36c-a237d01bbea6","session_id":"2da9a7f1-f9cd-4d45-abd7-25f5370301f3"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/codex_output.ts new file mode 100644 index 00000000000..a446c5abbba --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/codex_output.ts @@ -0,0 +1,49 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TURN_INTERRUPT_PROMPT, +} from "../shared.ts"; + +export function assertTurnInterruptOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["interrupted"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, [ + "user_message", + "run_interrupt_request", + "run_interrupt_result", + ]); + assertUserMessagesInclude(projection, [TURN_INTERRUPT_PROMPT]); + const interruptRequest = projection.turnItems.find( + (item) => item.type === "run_interrupt_request", + ); + const interruptResult = projection.turnItems.find((item) => item.type === "run_interrupt_result"); + assert.isDefined(interruptRequest); + assert.isDefined(interruptResult); + assert.equal(interruptRequest.status, "completed"); + assert.equal(interruptResult.status, "interrupted"); + assert.equal(interruptResult.parentItemId, interruptRequest.id); + assert.deepEqual( + projection.attempts.map((attempt) => attempt.status), + ["interrupted"], + ); + assert.deepEqual( + projection.nodes.map((node) => node.status), + ["interrupted"], + ); + assert.equal(projection.providerThreads[0]?.status, "idle"); + assert.include(["interrupted", "cancelled"], projection.providerTurns[0]?.status); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/codex_transcript.ndjson new file mode 100644 index 00000000000..ec87666a94f --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/codex_transcript.ndjson @@ -0,0 +1,21 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"turn_interrupt","metadata":{"source":"codex-app-server-probe","fileName":"turn_interrupt.ndjson","description":"One active turn is interrupted before it finishes naturally."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019daded-1d25-7df0-abf3-9ed9d31c346c","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739491,"updatedAt":1776739491,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-44-51-019daded-1d25-7df0-abf3-9ed9d31c346c.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"approvalPolicy":"never","input":[{"text":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","type":"text"}],"sandboxPolicy":{"networkAccess":false,"type":"workspaceWrite","writableRoots":[]},"threadId":"019daded-1d25-7df0-abf3-9ed9d31c346c"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019daded-1d25-7df0-abf3-9ed9d31c346c","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776739491,"updatedAt":1776739491,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/20/rollout-2026-04-20T19-44-51-019daded-1d25-7df0-abf3-9ed9d31c346c.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/packages/effect-codex-app-server","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019daded-1d2f-70f2-8abd-bf90f7dff165","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019daded-1d25-7df0-abf3-9ed9d31c346c","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019daded-1d25-7df0-abf3-9ed9d31c346c","turn":{"id":"019daded-1d2f-70f2-8abd-bf90f7dff165","items":[],"status":"inProgress","error":null,"startedAt":1776739491,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"expect_outbound","label":"turn/interrupt","frame":{"id":4,"method":"turn/interrupt","params":{"threadId":"019daded-1d25-7df0-abf3-9ed9d31c346c","turnId":"019daded-1d2f-70f2-8abd-bf90f7dff165"}}} +{"type":"emit_inbound","label":"turn/interrupt","frame":{"id":4,"result":{}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019daded-1d25-7df0-abf3-9ed9d31c346c","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019daded-1d25-7df0-abf3-9ed9d31c346c","turn":{"id":"019daded-1d2f-70f2-8abd-bf90f7dff165","items":[],"status":"interrupted","error":null,"startedAt":1776739491,"completedAt":1776739492,"durationMs":1539}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/grok_transcript.ndjson new file mode 100644 index 00000000000..d8496c0a557 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/grok_transcript.ndjson @@ -0,0 +1,9 @@ +{"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"turn_interrupt","metadata":{"generatedBy":"protocol-semantic-fixture","nativeSessionId":"grok-replay-session-1"}} +{"type":"expect_outbound","label":"initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"elicitation":{"form":{}}},"clientInfo":{"name":"t3-code","version":"0.0.0"}}}} +{"type":"emit_inbound","label":"initialized","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"audio":false,"embeddedContext":true,"image":false}},"authMethods":[{"id":"replay","name":"Replay"}],"agentInfo":{"name":"grok-replay","version":"1"}}}} +{"type":"expect_outbound","label":"session.new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"kind":"response","method":"session/new","result":{"sessionId":"grok-replay-session-1","models":{"currentModelId":"grok-build","availableModels":[{"modelId":"grok-build","name":"Grok Build"}]}}}} +{"type":"expect_outbound","label":"session.prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally."}]}}} +{"type":"expect_outbound","label":"session.cancel","frame":{"kind":"notification","method":"session/cancel","params":{"sessionId":"grok-replay-session-1"}}} +{"type":"emit_inbound","label":"prompt.cancelled","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"cancelled"}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/input.ts new file mode 100644 index 00000000000..341e7378f57 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/input.ts @@ -0,0 +1,10 @@ +import { TURN_INTERRUPT_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function turnInterruptInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: TURN_INTERRUPT_PROMPT }, + { type: "interrupt", targetRunIndex: 1 }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/opencode_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/opencode_transcript.ndjson new file mode 100644 index 00000000000..14ab96d6a95 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/opencode_transcript.ndjson @@ -0,0 +1,14 @@ +{"type":"transcript_start","provider":"opencode","protocol":"opencode-sdk.sse","version":"1.14.39","scenario":"turn_interrupt","metadata":{"source":"authenticated-opencode-sdk-probe","capturedAt":"2026-06-18","nativeSessionId":"ses_123570840ffenLEumkPQfegGCd","model":"openai/gpt-5.4-mini","filteredToRelevantEvents":true,"description":"A real immediate session abort where the authoritative idle notification arrived before the abort HTTP response."}} +{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}} +{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"title":"","permission":""}}} +{"type":"emit_inbound","label":"session.created","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_123570840ffenLEumkPQfegGCd","slug":"shiny-wolf","version":"1.14.39","projectID":"global","directory":"/private/tmp/t3-opencode-probe","path":"private/tmp/t3-opencode-probe","title":"T3 OpenCode interrupt probe","permission":[{"permission":"bash","pattern":"*","action":"deny"},{"permission":"edit","pattern":"*","action":"deny"},{"permission":"webfetch","pattern":"*","action":"deny"},{"permission":"websearch","pattern":"*","action":"deny"},{"permission":"codesearch","pattern":"*","action":"deny"},{"permission":"external_directory","pattern":"*","action":"deny"},{"permission":"question","pattern":"*","action":"allow"},{"permission":"read","pattern":"*","action":"allow"},{"permission":"glob","pattern":"*","action":"allow"},{"permission":"grep","pattern":"*","action":"allow"},{"permission":"lsp","pattern":"*","action":"allow"},{"permission":"todowrite","pattern":"*","action":"allow"},{"permission":"task","pattern":"*","action":"allow"},{"permission":"skill","pattern":"*","action":"allow"},{"permission":"doom_loop","pattern":"*","action":"allow"},{"permission":"edit","pattern":"*","action":"allow"}],"time":{"created":1781818521535,"updated":1781818521535}}}} +{"type":"expect_outbound","label":"session.promptAsync","frame":{"type":"session.promptAsync","input":{"sessionID":"ses_123570840ffenLEumkPQfegGCd","model":{"providerID":"openai","modelID":"gpt-5.4-mini"},"agent":"build","parts":[{"type":"text","text":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally."}]}}} +{"type":"emit_inbound","label":"session.promptAsync.response","frame":{"type":"sdk.response","operation":"session.promptAsync","data":null}} +{"type":"expect_outbound","label":"session.abort","frame":{"type":"session.abort","input":{"sessionID":"ses_123570840ffenLEumkPQfegGCd"}}} +{"type":"emit_inbound","label":"message.updated.user","frame":{"type":"sdk.event","event":{"id":"evt_edca8f7ca002yIVK4oYgGErvdA","type":"message.updated","properties":{"sessionID":"ses_123570840ffenLEumkPQfegGCd","info":{"id":"msg_edca8f7c9001w8R30p1SpzPuUJ","role":"user","sessionID":"ses_123570840ffenLEumkPQfegGCd","time":{"created":1781818521545},"agent":"build","model":{"providerID":"openai","modelID":"gpt-5.4-mini"}}}}}} +{"type":"emit_inbound","label":"message.part.updated.user","frame":{"type":"sdk.event","event":{"id":"evt_edca8f7ca00377fxNZjgW62lds","type":"message.part.updated","properties":{"sessionID":"ses_123570840ffenLEumkPQfegGCd","part":{"type":"text","text":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","messageID":"msg_edca8f7c9001w8R30p1SpzPuUJ","sessionID":"ses_123570840ffenLEumkPQfegGCd","id":"prt_edca8f7ca001ugmdiNWtC2964M"},"time":1781818521546}}}} +{"type":"emit_inbound","label":"session.status.busy","frame":{"type":"sdk.event","event":{"id":"evt_edca8f7cc001qWTqVj4Xrui4Sc","type":"session.status","properties":{"sessionID":"ses_123570840ffenLEumkPQfegGCd","status":{"type":"busy"}}}}} +{"type":"emit_inbound","label":"message.updated.assistant","frame":{"type":"sdk.event","event":{"id":"evt_edca8f7cc003brTfV2AHRR2C61","type":"message.updated","properties":{"sessionID":"ses_123570840ffenLEumkPQfegGCd","info":{"id":"msg_edca8f7cc002W56d1gpf5e17kX","parentID":"msg_edca8f7c9001w8R30p1SpzPuUJ","role":"assistant","mode":"build","agent":"build","path":{"cwd":"/private/tmp/t3-opencode-probe","root":"/"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"modelID":"gpt-5.4-mini","providerID":"openai","time":{"created":1781818521548},"sessionID":"ses_123570840ffenLEumkPQfegGCd"}}}}} +{"type":"emit_inbound","label":"session.status.idle","frame":{"type":"sdk.event","event":{"id":"evt_edca8f7d0001Gjy7U3InhsfliL","type":"session.status","properties":{"sessionID":"ses_123570840ffenLEumkPQfegGCd","status":{"type":"idle"}}}}} +{"type":"emit_inbound","label":"session.abort.response","frame":{"type":"sdk.response","operation":"session.abort","data":true}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/claude_output.ts new file mode 100644 index 00000000000..72884607068 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/claude_output.ts @@ -0,0 +1,95 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TURN_INTERRUPT_MID_TOOL_PROMPT, +} from "../shared.ts"; + +function frameType(frame: unknown): string | undefined { + return typeof frame === "object" && frame !== null && "type" in frame + ? (frame as { readonly type?: string }).type + : undefined; +} + +function assistantHasToolUse(frame: unknown): boolean { + if (frameType(frame) !== "assistant") { + return false; + } + const message = (frame as { readonly message?: unknown }).message; + const content = + typeof message === "object" && message !== null && "content" in message + ? (message as { readonly content?: unknown }).content + : undefined; + return ( + Array.isArray(content) && + content.some( + (part) => + typeof part === "object" && + part !== null && + "type" in part && + (part as { readonly type?: string }).type === "tool_use", + ) + ); +} + +function assertClaudeInterruptAfterToolUse(transcript: ProviderReplayTranscript) { + const toolUseIndex = transcript.entries.findIndex( + (entry) => entry.type === "emit_inbound" && assistantHasToolUse(entry.frame), + ); + const interruptIndex = transcript.entries.findIndex( + (entry) => entry.type === "expect_outbound" && frameType(entry.frame) === "query.interrupt", + ); + assert.isAtLeast(toolUseIndex, 0, "Claude interrupt fixture must record a started tool use"); + assert.isAbove( + interruptIndex, + toolUseIndex, + "Claude interrupt must be issued after the replayed tool use starts", + ); +} + +export function assertTurnInterruptMidToolClaudeOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assert.equal(transcript.provider, "claudeAgent"); + assertClaudeInterruptAfterToolUse(transcript); + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["interrupted"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, [ + "user_message", + "command_execution", + "run_interrupt_request", + "run_interrupt_result", + ]); + assertUserMessagesInclude(projection, [TURN_INTERRUPT_MID_TOOL_PROMPT]); + + const commandItem = projection.turnItems.find((item) => item.type === "command_execution"); + const interruptRequest = projection.turnItems.find( + (item) => item.type === "run_interrupt_request", + ); + const interruptResult = projection.turnItems.find((item) => item.type === "run_interrupt_result"); + assert.isDefined(commandItem); + assert.isDefined(interruptRequest); + assert.isDefined(interruptResult); + assert.equal(commandItem.status, "failed"); + assert.include(commandItem.input, "node -e"); + assert.equal(interruptRequest.status, "completed"); + assert.equal(interruptResult.status, "interrupted"); + assert.equal(interruptResult.parentItemId, interruptRequest.id); + assert.deepEqual( + projection.attempts.map((attempt) => attempt.status), + ["interrupted"], + ); + assert.equal(projection.providerThreads[0]?.status, "idle"); + assert.equal(projection.providerTurns[0]?.status, "interrupted"); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/claude_transcript.ndjson new file mode 100644 index 00000000000..ada528b3093 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/claude_transcript.ndjson @@ -0,0 +1,11 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"turn_interrupt_mid_tool","metadata":{"prompts":["Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally."],"model":"claude-sonnet-4-6","nativeSessionId":"7d29f78b-4ff3-4147-b35f-4d0f89c5d7f5","queryMode":"interrupt","tools":"claude_code","permissionMode":"bypassPermissions","interruptAfter":"tool_use","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"7d29f78b-4ff3-4147-b35f-4d0f89c5d7f5"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"8557ae70-649c-47ed-94c4-555567ddf93f","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"5f8a6db0-da77-4ac6-a34e-1502fdddcb2d","session_id":"7d29f78b-4ff3-4147-b35f-4d0f89c5d7f5"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"8557ae70-649c-47ed-94c4-555567ddf93f","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"93406e3b-e131-447a-8e21-066c7ac7da5a","session_id":"7d29f78b-4ff3-4147-b35f-4d0f89c5d7f5"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-turn_interrupt_mid_tool","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"24114010-ef49-4403-92e8-d76b3234d9eb","session_id":"7d29f78b-4ff3-4147-b35f-4d0f89c5d7f5"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_0134LoSqmeAtmiGfT8XBAnc4","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to run a specific command that will take 30 seconds to complete (due to the setTimeout). Let me run it.","signature":"ErcCClsIDRgCKkCX7DIBv4J2HjLNPifJq4HAANiIdEr2/GYMGyhdF9zkHFjE9AwbyKNoDI3MXdCly81YpJMMr6ky4//OnX7XrJcAMhFjbGF1ZGUtc29ubmV0LTQtNjgAEgzvtPfUQk3cHbNsLsEaDASyv+jHKV/wcM5HASIw415m94Xy244+UXxpWThWU7A8/9DEtOQ9sLzR9/duaPYc2SeKjvmhKOg1oQZi3nczKokB2YWThB+/yKnsTc2k+1xsUcUYXPcNk6jCZsnJtwkFDA0j7ar6Z5cY0ZaNli+aFbVGJrreiWvsGf8icWy0GedLUshQHGjvwXTbQ/F1kcR2NMBgBjot13Q3iN8/zcGMp6ViJu/R+roW7q6se2i0vbbRBg+1UtYrQ5rkbSBQ3TAlNmYDqe78XtexmHoYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2377,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2377},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"7d29f78b-4ff3-4147-b35f-4d0f89c5d7f5","uuid":"6574c500-9646-4151-ab05-a0d7065adc47"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_0134LoSqmeAtmiGfT8XBAnc4","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FAyzzWN34naNJKvPcD7B1D","name":"Bash","input":{"command":"node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"","timeout":60000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2377,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2377},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"7d29f78b-4ff3-4147-b35f-4d0f89c5d7f5","uuid":"ae2c90aa-4ded-4184-941b-ff32527cc9de"}} +{"type":"expect_outbound","label":"query.interrupt:1","frame":{"type":"query.interrupt"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"0f354af4-9fdf-463f-a3bc-75c31eeaa074","session_id":"7d29f78b-4ff3-4147-b35f-4d0f89c5d7f5"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/codex_output.ts new file mode 100644 index 00000000000..f3f98b53104 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/codex_output.ts @@ -0,0 +1,94 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TURN_INTERRUPT_MID_TOOL_PROMPT, +} from "../shared.ts"; + +function protocolMethod(frame: unknown): string | undefined { + return typeof frame === "object" && frame !== null && "method" in frame + ? (frame as { readonly method?: string }).method + : undefined; +} + +function isCommandExecutionStartedFrame(frame: unknown): boolean { + if (protocolMethod(frame) !== "item/started") { + return false; + } + const params = + typeof frame === "object" && frame !== null && "params" in frame + ? (frame as { readonly params?: unknown }).params + : undefined; + const item = + typeof params === "object" && params !== null && "item" in params + ? (params as { readonly item?: unknown }).item + : undefined; + return ( + typeof item === "object" && + item !== null && + "type" in item && + (item as { readonly type?: string }).type === "commandExecution" + ); +} + +function assertCodexInterruptAfterCommandExecution(transcript: ProviderReplayTranscript) { + const commandIndex = transcript.entries.findIndex( + (entry) => entry.type === "emit_inbound" && isCommandExecutionStartedFrame(entry.frame), + ); + const interruptIndex = transcript.entries.findIndex( + (entry) => entry.type === "expect_outbound" && protocolMethod(entry.frame) === "turn/interrupt", + ); + assert.isAtLeast(commandIndex, 0, "Codex interrupt fixture must record command execution start"); + assert.isAbove( + interruptIndex, + commandIndex, + "Codex interrupt must be issued after command execution starts in replay", + ); +} + +export function assertTurnInterruptMidToolCodexOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assert.equal(transcript.provider, "codex"); + assertCodexInterruptAfterCommandExecution(transcript); + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["interrupted"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, [ + "user_message", + "command_execution", + "run_interrupt_request", + "run_interrupt_result", + ]); + assertUserMessagesInclude(projection, [TURN_INTERRUPT_MID_TOOL_PROMPT]); + + const commandItem = projection.turnItems.find((item) => item.type === "command_execution"); + const interruptRequest = projection.turnItems.find( + (item) => item.type === "run_interrupt_request", + ); + const interruptResult = projection.turnItems.find((item) => item.type === "run_interrupt_result"); + assert.isDefined(commandItem); + assert.isDefined(interruptRequest); + assert.isDefined(interruptResult); + assert.include(["running", "completed", "failed"], commandItem.status); + assert.include(commandItem.input, "node -e"); + assert.equal(interruptRequest.status, "completed"); + assert.equal(interruptResult.status, "interrupted"); + assert.equal(interruptResult.parentItemId, interruptRequest.id); + assert.deepEqual( + projection.attempts.map((attempt) => attempt.status), + ["interrupted"], + ); + assert.equal(projection.providerThreads[0]?.status, "idle"); + assert.include(["interrupted", "cancelled"], projection.providerTurns[0]?.status); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/codex_transcript.ndjson new file mode 100644 index 00000000000..ab43808f8b7 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/codex_transcript.ndjson @@ -0,0 +1,33 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.128.0","scenario":"turn_interrupt_mid_tool","metadata":{"source":"record-codex-app-server-replay-fixture","fileName":"turn_interrupt_mid_tool.ndjson","description":"One active turn is interrupted after Codex has already executed a local command."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.128.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex_p","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"remoteControl/status/changed","frame":{"method":"remoteControl/status/changed","params":{"status":"disabled","environmentId":null}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1778178891,"updatedAt":1778178891,"status":{"type":"idle"},"path":"/Users/julius/.codex_p/sessions/2026/05/07/rollout-2026-05-07T11-34-51-019e03b8-9e5c-7b32-88ab-f742a29b75b8.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/apps/server","cliVersion":"0.128.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.5","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/apps/server","instructionSources":["/Users/julius/.codex_p/AGENTS.md","/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/AGENTS.md"],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex_p/memories"],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"permissionProfile":{"type":"managed","network":{"enabled":false},"fileSystem":{"type":"restricted","entries":[{"path":{"type":"special","value":{"kind":"root"}},"access":"read"},{"path":{"type":"special","value":{"kind":"project_roots","subpath":null}},"access":"write"},{"path":{"type":"special","value":{"kind":"slash_tmp"}},"access":"write"},{"path":{"type":"special","value":{"kind":"tmpdir"}},"access":"write"},{"path":{"type":"special","value":{"kind":"project_roots","subpath":".git"}},"access":"read"},{"path":{"type":"special","value":{"kind":"project_roots","subpath":".agents"}},"access":"read"},{"path":{"type":"special","value":{"kind":"project_roots","subpath":".codex"}},"access":"read"},{"path":{"type":"path","path":"/Users/julius/.codex_p/memories"},"access":"write"}]}},"activePermissionProfile":{"id":":workspace","extends":null,"modifications":[]},"reasoningEffort":"medium"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"approvalPolicy":"never","input":[{"text":"Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally.","type":"text"}],"sandboxPolicy":{"networkAccess":false,"type":"workspaceWrite","writableRoots":[]},"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1778178891,"updatedAt":1778178891,"status":{"type":"idle"},"path":"/Users/julius/.codex_p/sessions/2026/05/07/rollout-2026-05-07T11-34-51-019e03b8-9e5c-7b32-88ab-f742a29b75b8.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/apps/server","cliVersion":"0.128.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"deprecationNotice","frame":{"method":"deprecationNotice","params":{"summary":"`[features].collab` is deprecated. Use `[features].multi_agent` instead.","details":"Enable it with `--enable multi_agent` or `[features].multi_agent` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."}}} +{"type":"emit_inbound","label":"warning","frame":{"method":"warning","params":{"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","message":"Under-development features enabled: apply_patch_freeform. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in /Users/julius/.codex_p/config.toml."}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"starting","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"starting","error":null}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019e03b8-9f34-7bb1-8267-ad79b7e2e69a","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","turn":{"id":"019e03b8-9f34-7bb1-8267-ad79b7e2e69a","items":[],"status":"inProgress","error":null,"startedAt":1778178891,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"computer-use","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"openaiDeveloperDocs","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"codex_apps","status":"ready","error":null}}} +{"type":"emit_inbound","label":"mcpServer/startupStatus/updated","frame":{"method":"mcpServer/startupStatus/updated","params":{"name":"uidotsh","status":"ready","error":null}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"ed97ce32-6075-4da5-9c7e-7518427a7bb6","content":[{"type":"text","text":"Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally.","text_elements":[]}]},"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","turnId":"019e03b8-9f34-7bb1-8267-ad79b7e2e69a"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"ed97ce32-6075-4da5-9c7e-7518427a7bb6","content":[{"type":"text","text":"Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally.","text_elements":[]}]},"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","turnId":"019e03b8-9f34-7bb1-8267-ad79b7e2e69a"}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":6,"windowDurationMins":300,"resetsAt":1778193273},"secondary":{"usedPercent":32,"windowDurationMins":10080,"resetsAt":1778539136},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"thread/tokenUsage/updated","frame":{"method":"thread/tokenUsage/updated","params":{"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","turnId":"019e03b8-9f34-7bb1-8267-ad79b7e2e69a","tokenUsage":{"total":{"totalTokens":17497,"inputTokens":17444,"cachedInputTokens":7552,"outputTokens":53,"reasoningOutputTokens":0},"last":{"totalTokens":17497,"inputTokens":17444,"cachedInputTokens":7552,"outputTokens":53,"reasoningOutputTokens":0},"modelContextWindow":258400}}}} +{"type":"emit_inbound","label":"account/rateLimits/updated","frame":{"method":"account/rateLimits/updated","params":{"rateLimits":{"limitId":"codex","limitName":null,"primary":{"usedPercent":6,"windowDurationMins":300,"resetsAt":1778193273},"secondary":{"usedPercent":32,"windowDurationMins":10080,"resetsAt":1778539136},"credits":null,"planType":"pro","rateLimitReachedType":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"commandExecution","id":"call_m8JXk02QAzuzBMHNgh1RsGaw","command":"/bin/zsh -lc \"node -e \\\"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\\\"\"","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1/apps/server","processId":"4275","source":"unifiedExecStartup","status":"inProgress","commandActions":[{"type":"unknown","command":"node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\""}],"aggregatedOutput":null,"exitCode":null,"durationMs":null},"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","turnId":"019e03b8-9f34-7bb1-8267-ad79b7e2e69a"}}} +{"type":"expect_outbound","label":"turn/interrupt","frame":{"id":4,"method":"turn/interrupt","params":{"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","turnId":"019e03b8-9f34-7bb1-8267-ad79b7e2e69a"}}} +{"type":"emit_inbound","label":"turn/interrupt","frame":{"id":4,"result":{}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019e03b8-9e5c-7b32-88ab-f742a29b75b8","turn":{"id":"019e03b8-9f34-7bb1-8267-ad79b7e2e69a","items":[],"status":"interrupted","error":null,"startedAt":1778178891,"completedAt":1778178895,"durationMs":3792}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_output.ts new file mode 100644 index 00000000000..becc6006a97 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_output.ts @@ -0,0 +1,75 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertBaseProjection, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TURN_INTERRUPT_MID_TOOL_PROMPT, +} from "../shared.ts"; + +function frameType(frame: unknown): string | undefined { + return typeof frame === "object" && frame !== null + ? (Reflect.get(frame, "type") as string | undefined) + : undefined; +} + +function cursorUpdateType(frame: unknown): string | undefined { + if (frameType(frame) !== "interaction.update") { + return undefined; + } + const update = Reflect.get(frame as object, "update"); + return typeof update === "object" && update !== null + ? (Reflect.get(update, "type") as string | undefined) + : undefined; +} + +export function assertTurnInterruptMidToolCursorOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + const toolStartedIndex = transcript.entries.findIndex( + (entry) => + entry.type === "emit_inbound" && cursorUpdateType(entry.frame) === "tool-call-started", + ); + const cancelIndex = transcript.entries.findIndex( + (entry) => entry.type === "expect_outbound" && frameType(entry.frame) === "run.cancel", + ); + assert.isAtLeast(toolStartedIndex, 0); + assert.isAbove(cancelIndex, toolStartedIndex); + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["interrupted"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertTurnItemTypes(projection, [ + "user_message", + "command_execution", + "run_interrupt_request", + "run_interrupt_result", + ]); + assertUserMessagesInclude(projection, [TURN_INTERRUPT_MID_TOOL_PROMPT]); + + const commandItem = projection.turnItems.find((item) => item.type === "command_execution"); + const interruptRequest = projection.turnItems.find( + (item) => item.type === "run_interrupt_request", + ); + const interruptResult = projection.turnItems.find((item) => item.type === "run_interrupt_result"); + assert.isDefined(commandItem); + assert.isDefined(interruptRequest); + assert.isDefined(interruptResult); + assert.include(commandItem.input, "node -e"); + assert.equal(interruptRequest.status, "completed"); + assert.equal(interruptResult.status, "interrupted"); + assert.equal(interruptResult.parentItemId, interruptRequest.id); + assert.deepEqual( + projection.attempts.map((attempt) => attempt.status), + ["interrupted"], + ); + assert.equal(projection.providerThreads[0]?.status, "idle"); + assert.equal(projection.providerTurns[0]?.status, "interrupted"); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_transcript.ndjson new file mode 100644 index 00000000000..fbbcca5af5a --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/cursor_transcript.ndjson @@ -0,0 +1,43 @@ +{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"turn_interrupt_mid_tool","metadata":{"generatedBy":"recordCursorAgentSdkReplayTranscript","nativeAgentId":"agent-e4dcb030-b82a-4526-9b30-3db02142c1bb"}} +{"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"sandboxEnabled":true,"enableAgentRetries":true}}}} +{"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-e4dcb030-b82a-4526-9b30-3db02142c1bb"}} +{"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally.","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} +{"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","agentId":"agent-e4dcb030-b82a-4526-9b30-3db02142c1bb"}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"thinking-delta","text":"Running the Node.js "}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"thinking-delta","text":"command now. Waiting"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"thinking-delta","text":" for it to complete "}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"thinking-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"thinking-delta","text":"before responding."}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"thinking-completed","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"thinking-completed","thinkingDurationMs":697}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":"Running"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":" the"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":" command"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":" and"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":" waiting"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":" for"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":" it"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":" to"}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":" finish"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":2}}} +{"type":"emit_inbound","label":"text-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"text-delta","text":".\n"}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":3}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":22}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":4}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":9}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":5}}} +{"type":"emit_inbound","label":"token-delta","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"token-delta","tokens":1}}} +{"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","update":{"type":"tool-call-started","callId":"tool_6ecf0e02-3c15-448d-8994-2d041457475","toolCall":{"type":"shell","args":{"command":"node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"","timeout":35000}},"modelCallId":"d7f78da1-7ccd-45fc-a021-a0f7c333bd1d-0-kteu"}}} +{"type":"expect_outbound","label":"run.cancel:1","frame":{"type":"run.cancel","runId":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2"}} +{"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-3735f72d-3c3a-40b3-ae4a-caed784e6ea2","requestId":"d7f78da1-7ccd-45fc-a021-a0f7c333bd1d","status":"cancelled","model":{"id":"composer-2.5"},"durationMs":4496}}} +{"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-e4dcb030-b82a-4526-9b30-3db02142c1bb"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/input.ts new file mode 100644 index 00000000000..238c0f7a9ed --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/input.ts @@ -0,0 +1,10 @@ +import { TURN_INTERRUPT_MID_TOOL_PROMPT, type OrchestratorFixtureInput } from "../shared.ts"; + +export function turnInterruptMidToolInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: TURN_INTERRUPT_MID_TOOL_PROMPT }, + { type: "interrupt", targetRunIndex: 1, waitForTurnItemType: "command_execution" }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_restart/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_restart/claude_output.ts new file mode 100644 index 00000000000..1da224a8485 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_restart/claude_output.ts @@ -0,0 +1,129 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertConversationMessageRoles, + assertRunOrdinals, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + TURN_INTERRUPT_MID_TOOL_PROMPT, + TURN_INTERRUPT_RECOVERY_PROMPT, +} from "../shared.ts"; + +function isReplayFrameWithType( + frame: unknown, + type: string, +): frame is { readonly type: string; readonly options?: Record } { + return ( + typeof frame === "object" && + frame !== null && + "type" in frame && + (frame as { readonly type?: unknown }).type === type + ); +} + +function frameType(frame: unknown): string | undefined { + return typeof frame === "object" && frame !== null && "type" in frame + ? (frame as { readonly type?: string }).type + : undefined; +} + +function assistantHasToolUse(frame: unknown): boolean { + if (frameType(frame) !== "assistant") { + return false; + } + const message = (frame as { readonly message?: unknown }).message; + const content = + typeof message === "object" && message !== null && "content" in message + ? (message as { readonly content?: unknown }).content + : undefined; + return ( + Array.isArray(content) && + content.some( + (part) => + typeof part === "object" && + part !== null && + "type" in part && + (part as { readonly type?: string }).type === "tool_use", + ) + ); +} + +function assertClaudeInterruptAfterToolUse(transcript: ProviderReplayTranscript) { + const toolUseIndex = transcript.entries.findIndex( + (entry) => entry.type === "emit_inbound" && assistantHasToolUse(entry.frame), + ); + const interruptIndex = transcript.entries.findIndex( + (entry) => entry.type === "expect_outbound" && frameType(entry.frame) === "query.interrupt", + ); + assert.isAtLeast(toolUseIndex, 0, "Claude interrupt fixture must record a started tool use"); + assert.isAbove( + interruptIndex, + toolUseIndex, + "Claude interrupt must be issued after the replayed tool use starts", + ); +} + +export function assertTurnInterruptRestartClaudeOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assert.equal(transcript.provider, "claudeAgent"); + assertClaudeInterruptAfterToolUse(transcript); + assertBaseProjection({ + result, + transcript, + runCount: 2, + runStatuses: ["interrupted", "completed"], + }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertRunOrdinals(projection, [1, 2]); + assertConversationMessageRoles(projection, ["user", "user", "assistant"]); + assertTurnItemTypes(projection, [ + "user_message", + "command_execution", + "run_interrupt_request", + "run_interrupt_result", + "assistant_message", + ]); + assertUserMessagesInclude(projection, [ + TURN_INTERRUPT_MID_TOOL_PROMPT, + TURN_INTERRUPT_RECOVERY_PROMPT, + ]); + assertAssistantTextIncludes(projection, "interrupt recovery fixture complete"); + assert.deepEqual( + projection.attempts.map((attempt) => attempt.status), + ["interrupted", "completed"], + ); + assert.deepEqual( + projection.providerTurns.map((turn) => turn.status), + ["interrupted", "completed"], + ); + assert.equal(projection.providerThreads[0]?.status, "idle"); + const commandItem = projection.turnItems.find((item) => item.type === "command_execution"); + assert.isDefined(commandItem); + assert.equal(commandItem.status, "failed"); + assert.include(commandItem.input, "node -e"); + + const outboundFrames = transcript.entries.flatMap((entry) => + entry.type === "expect_outbound" ? [entry.frame] : [], + ); + const queryOpenFrames = outboundFrames.filter((frame) => + isReplayFrameWithType(frame, "query.open"), + ); + const interruptFrames = outboundFrames.filter((frame) => + isReplayFrameWithType(frame, "query.interrupt"), + ); + assert.lengthOf(queryOpenFrames, 2); + assert.isString(queryOpenFrames[1]?.options?.resume); + assert.lengthOf(interruptFrames, 1); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_restart/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_restart/claude_transcript.ndjson new file mode 100644 index 00000000000..80ecb6d2a98 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_restart/claude_transcript.ndjson @@ -0,0 +1,20 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"turn_interrupt_restart","metadata":{"prompts":["Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally.","Respond with exactly: interrupt recovery fixture complete"],"model":"claude-sonnet-4-6","nativeSessionId":"fb591f8f-073f-4981-bf67-9b5bffb537d5","queryMode":"interrupt_restart","tools":"claude_code","permissionMode":"bypassPermissions","interruptAfter":"tool_use","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open:1","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"fb591f8f-073f-4981-bf67-9b5bffb537d5"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally."},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"9e9d889c-c7cc-41c8-862c-691efcebe3a4","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"ac49901c-0458-4870-b5d7-9c8ed20ffa68","session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"9e9d889c-c7cc-41c8-862c-691efcebe3a4","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"1f69ab12-dcff-4bb3-9dc7-6284fc356fd4","session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-turn_interrupt_restart","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"a3d73993-da1f-4dd2-ae3f-6d9fbfde4e17","session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01Ri3g1NyAvmxateKU6RpTZt","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to run a specific command that will take 30 seconds to complete (due to the setTimeout). Let me run it.","signature":"ErcCClsIDRgCKkCX7DIBv4J2HjLNPifJq4HAANiIdEr2/GYMGyhdF9zkHFjE9AwbyKNoDI3MXdCly81YpJMMr6ky4//OnX7XrJcAMhFjbGF1ZGUtc29ubmV0LTQtNjgAEgyvpmUstPaO3mULIDoaDFDTBmO2AKTDNHheWCIwmltJgzr/Y82edEUCc2SZ/vmduVXkEGJFBpYfDgWg++UCyftnJ2jIuU7OjMrgnlGyKokBovHsKA1SGudP3mxdKaHpo9wyKvUQgjmjBUYndKN68q/l/LSN27pia+/x2C99K8iJTvWWykEjSvXn59qOQJI49MVav3hPKR0t0kQcrdM17ceB/ZYPcAxNN4u+EB2cxxeUAGXmcoJ5H69FBoNxtnDf1ECnTp3cdcd7af7W5oTUKX075fqAXS7rKJ4YAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2377,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2377},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5","uuid":"d7c45832-9ef6-4a07-81ef-2f3c5bfbbb60"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01Ri3g1NyAvmxateKU6RpTZt","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KHpBiM7muckukoYeKCJnSn","name":"Bash","input":{"command":"node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"","timeout":60000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":2377,"cache_read_input_tokens":9455,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2377},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5","uuid":"a4208b49-5191-48bf-ae90-9e25f49822de"}} +{"type":"expect_outbound","label":"query.interrupt:1","frame":{"type":"query.interrupt"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"72dd2e75-8bf4-4119-85b4-0176e138ca89","session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5"}} +{"type":"runtime_exit","status":"success"} +{"type":"expect_outbound","label":"query.open:2","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"resume":"fb591f8f-073f-4981-bf67-9b5bffb537d5"}}} +{"type":"expect_outbound","label":"prompt.offer:2","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Respond with exactly: interrupt recovery fixture complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"b6eca4f5-a876-4c87-b985-ac51dfdcd45a","hook_name":"SessionStart:resume","hook_event":"SessionStart","uuid":"333f23b1-5e16-416a-ad70-28fc820390b8","session_id":"be6cf26c-092d-4fe1-8a63-057bfd8b4723"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"b6eca4f5-a876-4c87-b985-ac51dfdcd45a","hook_name":"SessionStart:resume","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"29fb9baf-b0ed-4097-b8fc-be223aa6dd76","session_id":"be6cf26c-092d-4fe1-8a63-057bfd8b4723"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-turn_interrupt_restart","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"1b86dd32-214c-4c7e-81d1-2a73befc5da8","session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_015EmKr5Mwpb1EixMz3GZupx","type":"message","role":"assistant","content":[{"type":"text","text":"interrupt recovery fixture complete"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":243,"cache_read_input_tokens":11832,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":243},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5","uuid":"dd46fcd4-0fca-4418-bcd6-30e6df3e9479"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"0548e03f-e3ea-431d-b6d2-100d02680b4d","session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":9358,"duration_api_ms":8922,"num_turns":1,"result":"interrupt recovery fixture complete","stop_reason":"end_turn","session_id":"fb591f8f-073f-4981-bf67-9b5bffb537d5","total_cost_usd":0.00457485,"usage":{"input_tokens":3,"cache_creation_input_tokens":243,"cache_read_input_tokens":11832,"output_tokens":7,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":243,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":7,"cache_read_input_tokens":11832,"cache_creation_input_tokens":243,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":243},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":3,"outputTokens":7,"cacheReadInputTokens":11832,"cacheCreationInputTokens":243,"webSearchRequests":0,"costUSD":0.00457485,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"aaf4e093-6e70-4418-8507-99d62cece13e"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_restart/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_restart/input.ts new file mode 100644 index 00000000000..c6ecdf6302b --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_restart/input.ts @@ -0,0 +1,15 @@ +import { + TURN_INTERRUPT_MID_TOOL_PROMPT, + TURN_INTERRUPT_RECOVERY_PROMPT, + type OrchestratorFixtureInput, +} from "../shared.ts"; + +export function turnInterruptRestartInput(): OrchestratorFixtureInput { + return { + steps: [ + { type: "message", text: TURN_INTERRUPT_MID_TOOL_PROMPT }, + { type: "interrupt", targetRunIndex: 1, waitForTurnItemType: "command_execution" }, + { type: "message", text: TURN_INTERRUPT_RECOVERY_PROMPT }, + ], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/web_search/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/claude_output.ts new file mode 100644 index 00000000000..4bffd81e708 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/claude_output.ts @@ -0,0 +1,58 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertConversationMessageRoles, + assertExecutionNodeKinds, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + WEB_SEARCH_PROMPT, +} from "../shared.ts"; + +const CLAUDE_WEB_SEARCH_QUERY = "FIFA World Cup 2026 ticket pricing"; + +export function assertClaudeWebSearchOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertExecutionNodeKinds(projection, ["root_turn", "tool_call", "assistant_message"]); + assertConversationMessageRoles(projection, ["user", "assistant"]); + assertTurnItemTypes(projection, [ + "user_message", + "dynamic_tool", + "web_search", + "assistant_message", + ]); + assertRuntimeRequestCounts(projection, { total: 0 }); + assertUserMessagesInclude(projection, [WEB_SEARCH_PROMPT]); + assertAssistantTextIncludes(projection, "web search fixture complete"); + + const toolSearchItems = projection.turnItems.filter( + (item) => item.type === "dynamic_tool" && item.toolName === "ToolSearch", + ); + assert.lengthOf(toolSearchItems, 1); + + const webSearchItems = projection.turnItems.filter((item) => item.type === "web_search"); + assert.lengthOf(webSearchItems, 1); + const webSearch = webSearchItems[0]; + assert.isDefined(webSearch); + assert.equal(webSearch.status, "completed"); + assert.include(webSearch.patterns ?? [], CLAUDE_WEB_SEARCH_QUERY); + assert.isAtLeast(webSearch.results?.length ?? 0, 1); + assert.isTrue( + webSearch.results?.some((entry) => entry.url?.includes("fifa.com")) ?? false, + "Claude web search should project structured result URLs", + ); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/web_search/claude_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/claude_transcript.ndjson new file mode 100644 index 00000000000..c4a1a314fda --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/claude_transcript.ndjson @@ -0,0 +1,16 @@ +{"type":"transcript_start","provider":"claudeAgent","protocol":"claude-agent-sdk.query","version":"0.2.111","scenario":"web_search","metadata":{"prompts":["Search the web for FIFA World Cup ticket pricing, then answer exactly: web search fixture complete"],"model":"claude-sonnet-4-6","nativeSessionId":"f5ff7f51-4729-4331-93bd-416f3fdf2d19","queryMode":"streaming","tools":"claude_code","permissionMode":"bypassPermissions","generatedBy":"recordClaudeAgentSdkReplayTranscript"}} +{"type":"expect_outbound","label":"query.open","frame":{"type":"query.open","options":{"model":"claude-sonnet-4-6","tools":{"type":"preset","preset":"claude_code"},"permissionMode":"bypassPermissions","allowDangerouslySkipPermissions":true,"sessionId":"f5ff7f51-4729-4331-93bd-416f3fdf2d19"}}} +{"type":"expect_outbound","label":"prompt.offer:1","frame":{"type":"prompt.offer","message":{"type":"user","message":{"role":"user","content":"Search the web for FIFA World Cup ticket pricing, then answer exactly: web search fixture complete"},"parent_tool_use_id":null}}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_started","hook_id":"824ca161-8a1d-45f3-9fac-d5611119d73b","hook_name":"SessionStart:startup","hook_event":"SessionStart","uuid":"37c7a47b-8682-443c-a12c-61b2b3520213","session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"hook_response","hook_id":"824ca161-8a1d-45f3-9fac-d5611119d73b","hook_name":"SessionStart:startup","hook_event":"SessionStart","output":"","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"6c2fbe70-34ca-4fd7-be53-7ee413e9ea0c","session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19"}} +{"type":"emit_inbound","label":"system","frame":{"type":"system","subtype":"init","agents":[],"apiKeySource":"none","claude_code_version":"2.1.111","cwd":"/tmp/claude-replay-web_search","tools":[],"mcp_servers":[],"model":"claude-sonnet-4-6","permissionMode":"bypassPermissions","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"fast_mode_state":"off","uuid":"c0128e2f-6a1c-4e08-a6df-e0c9e770e520","session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_013ghDLNooL9JjT7LM8dfvKv","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to search the web for FIFA World Cup ticket pricing. Let me use the WebSearch tool for this.","signature":"EqsCClsIDRgCKkAVt/Rl62+yw6rhmmNVYRVjiFAgkjumuFJIsvwCZZL0vIbb2flNthojSzcHnxTJlkEKnkjDIrmmpounw3JVAbBYMhFjbGF1ZGUtc29ubmV0LTQtNjgAEgyfu3NkAC3ykykneaIaDJj7JQdfxEhIV0a5HiIw3XX+IVr+XsX569Exa2UvUqbgWIrbtbQZsePslTE4ej81ZtUtSrsymPeoD9pKnMogKn4w73zb3sYiICwUenD0Hh5XOT9gmutoI6b2xPSbFRTjwnI/B0cqqkF2BS9+Zt+W+j+0jWxVP4/+uhR/wg6zul8LOg0RZZKqHEpYeDIXNYkbENE4EwVNwwhlSUHJ6VAha+TBx9iO2xMz2RJuxQp3PnEhq1Ggp07qewWZQKyHk/AYAQ=="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11801,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":11801},"output_tokens":0,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19","uuid":"94477848-5d1a-43c5-9845-19ba3d150a6f"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_013ghDLNooL9JjT7LM8dfvKv","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KGz7z4A7CtpkSnPyMCS7EP","name":"ToolSearch","input":{"query":"select:WebSearch","max_results":1},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11801,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":11801},"output_tokens":0,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19","uuid":"a07dbc7b-03c1-47d7-86d1-653c65d9bbd8"}} +{"type":"emit_inbound","label":"rate_limit_event","frame":{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"uuid":"215ceb84-3aab-4ab5-a044-773fdc7a86f1","session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01KGz7z4A7CtpkSnPyMCS7EP","content":[{"type":"tool_reference","tool_name":"WebSearch"}]}]},"parent_tool_use_id":null,"session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19","uuid":"196d5d0d-d6e5-4130-a559-45ad7030542f","timestamp":"2026-05-07T01:18:48.005Z","tool_use_result":{"matches":["WebSearch"],"query":"select:WebSearch","total_deferred_tools":22}}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01YTXwWhu5v3exh54x4Y9mgo","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Now let me search the web for FIFA World Cup ticket pricing.","signature":"EvkBClsIDRgCKkA45QKRkX3BN5Cgyc8u4q/I7qrBKNidzbBl7AYPkXb013ySNW9pzbwWCWqKsAKp+a8oyT0z0aksj3mKlSf8pcU2MhFjbGF1ZGUtc29ubmV0LTQtNjgAEgzgDr6UfC7FnM/MLd4aDHzA23y4Ui176BzMNSIwSsB7fT9onFB7qo4QK/qJrv8KL7lnlKXtuvo4N+SRBkPNTkBwFbxDeR5ZZaIKQEtQKkxNTo4mSkAL4p+WGarjdfaUrLbEuVN6imJ6esefnk4BEhdH3+tonhzYKezTa2NnxE1zPlSoB7xm/vxGiBMF6kiefrO0XbxrR/FXneFpGAE="}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":644,"cache_read_input_tokens":11801,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":644},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19","uuid":"820d6a9a-f033-493b-ad0a-7554be7dafc1"}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01YTXwWhu5v3exh54x4Y9mgo","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011fKbh9ER69cDiCCpp93oRG","name":"WebSearch","input":{"query":"FIFA World Cup 2026 ticket pricing"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":644,"cache_read_input_tokens":11801,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":644},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19","uuid":"2b089653-2f18-4833-99d9-c2194073a327"}} +{"type":"emit_inbound","label":"user","frame":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_011fKbh9ER69cDiCCpp93oRG","type":"tool_result","content":"Web search results for query: \"FIFA World Cup 2026 ticket pricing\"\n\nLinks: [{\"title\":\"Tickets - FIFA World Cup 2026™\",\"url\":\"https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/tickets\"},{\"title\":\"FIFA chief Infantino defends World Cup ticket prices | World Cup 2026 News | Al Jazeera\",\"url\":\"https://www.aljazeera.com/sports/2026/5/6/fifa-chief-infantino-defends-world-cup-ticket-prices\"},{\"title\":\"World Cup 2026 tickets: Fans slam FIFA's new pricing strategy as tournament availability lingers | FOX 11 Los Angeles\",\"url\":\"https://www.foxla.com/news/world-cup-2026-ticket-prices-fifa-strategy\"},{\"title\":\"Tickets now on sale for FIFA World Cup 2026™ Play-Off ...\",\"url\":\"https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/articles/tickets-now-on-sale-for-play-off-tournament\"},{\"title\":\"New ticket pricing tier introduced for fans | FIFA World Cup\",\"url\":\"https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/articles/fifa-world-cup-2026-new-ticket-pricing-tier\"},{\"title\":\"How expensive is a 2026 FIFA World Cup ticket? | Britannica\",\"url\":\"https://www.britannica.com/question/How-expensive-is-a-2026-FIFA-World-Cup-ticket\"},{\"title\":\"Why are FIFA World Cup 2026 tickets so expensive? | World Cup 2026 News | Al Jazeera\",\"url\":\"https://www.aljazeera.com/sports/2026/4/23/why-are-fifa-world-cup-2026-tickets-so-expensive\"},{\"title\":\"2026 World Cup Ticket Prices by City | The World Cup Guide\",\"url\":\"https://theworldcupguide.com/2026-world-cup-ticket-prices/\"},{\"title\":\"How Much Do 2026 World Cup Tickets Cost? Complete Price Guide\",\"url\":\"https://www.jetpacglobal.com/blog/fifa-world-cup-2026-ticket-prices/\"},{\"title\":\"Expensive tickets for early World Cup games still on general sale | PBS News\",\"url\":\"https://www.pbs.org/newshour/world/expensive-tickets-for-early-world-cup-games-still-on-general-sale\"}]\n\nBased on the search results, here's what I found about FIFA World Cup 2026 ticket pricing:\n\n## Price Range\n\nTicket prices on the official direct sale platform vary from about $60 to $10,990 depending on factors including the fixture's popularity, the seat category, FIFA's dynamic pricing models, and the date of purchase.\n\n## Specific Examples\n\n- General public entry starts at USD 120.\n- The cheapest tickets currently are $380 for seven different games, including matches like Curacao vs. Ivory Coast in Philadelphia.\n- Tickets for USA vs Paraguay start at $1,120 and go as high as $4,105, with many tickets priced at about $2,000 for the June 12 match in Los Angeles.\n- The most expensive ticket for the final is about $11,000 at its original price.\n\n## Pricing Strategy\n\nOfficial FIFA World Cup 2026 ticket prices use dynamic pricing, meaning costs shift based on match demand, teams, city, and sales phase. Additionally, all prices are subject to a 15% FIFA service fee at checkout.\n\n## Context\n\nThe 2026 FIFA World Cup will be the most expensive in tournament history. Dynamic pricing, three host nations, 104 matches, and record ticket demand have created a pricing environment unlike anything seen at a previous World Cup. FIFA received in excess of 500 million ticket requests for 2026, compared with fewer than 50 million combined for the 2018 and 2022 World Cups.\n\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks."}]},"parent_tool_use_id":null,"session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19","uuid":"14e14d41-5638-4293-bc96-5b872dc25556","timestamp":"2026-05-07T01:18:56.738Z","tool_use_result":{"query":"FIFA World Cup 2026 ticket pricing","results":[{"tool_use_id":"srvtoolu_01PoQRt4QWnLhDhzXYQJqbsk","content":[{"title":"Tickets - FIFA World Cup 2026™","url":"https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/tickets"},{"title":"FIFA chief Infantino defends World Cup ticket prices | World Cup 2026 News | Al Jazeera","url":"https://www.aljazeera.com/sports/2026/5/6/fifa-chief-infantino-defends-world-cup-ticket-prices"},{"title":"World Cup 2026 tickets: Fans slam FIFA's new pricing strategy as tournament availability lingers | FOX 11 Los Angeles","url":"https://www.foxla.com/news/world-cup-2026-ticket-prices-fifa-strategy"},{"title":"Tickets now on sale for FIFA World Cup 2026™ Play-Off ...","url":"https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/articles/tickets-now-on-sale-for-play-off-tournament"},{"title":"New ticket pricing tier introduced for fans | FIFA World Cup","url":"https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/articles/fifa-world-cup-2026-new-ticket-pricing-tier"},{"title":"How expensive is a 2026 FIFA World Cup ticket? | Britannica","url":"https://www.britannica.com/question/How-expensive-is-a-2026-FIFA-World-Cup-ticket"},{"title":"Why are FIFA World Cup 2026 tickets so expensive? | World Cup 2026 News | Al Jazeera","url":"https://www.aljazeera.com/sports/2026/4/23/why-are-fifa-world-cup-2026-tickets-so-expensive"},{"title":"2026 World Cup Ticket Prices by City | The World Cup Guide","url":"https://theworldcupguide.com/2026-world-cup-ticket-prices/"},{"title":"How Much Do 2026 World Cup Tickets Cost? Complete Price Guide","url":"https://www.jetpacglobal.com/blog/fifa-world-cup-2026-ticket-prices/"},{"title":"Expensive tickets for early World Cup games still on general sale | PBS News","url":"https://www.pbs.org/newshour/world/expensive-tickets-for-early-world-cup-games-still-on-general-sale"}]},"Based on the search results, here's what I found about FIFA World Cup 2026 ticket pricing:\n\n## Price Range\n\nTicket prices on the official direct sale platform vary from about $60 to $10,990 depending on factors including the fixture's popularity, the seat category, FIFA's dynamic pricing models, and the date of purchase.\n\n## Specific Examples\n\n- General public entry starts at USD 120.\n- The cheapest tickets currently are $380 for seven different games, including matches like Curacao vs. Ivory Coast in Philadelphia.\n- Tickets for USA vs Paraguay start at $1,120 and go as high as $4,105, with many tickets priced at about $2,000 for the June 12 match in Los Angeles.\n- The most expensive ticket for the final is about $11,000 at its original price.\n\n## Pricing Strategy\n\nOfficial FIFA World Cup 2026 ticket prices use dynamic pricing, meaning costs shift based on match demand, teams, city, and sales phase. Additionally, all prices are subject to a 15% FIFA service fee at checkout.\n\n## Context\n\nThe 2026 FIFA World Cup will be the most expensive in tournament history. Dynamic pricing, three host nations, 104 matches, and record ticket demand have created a pricing environment unlike anything seen at a previous World Cup. FIFA received in excess of 500 million ticket requests for 2026, compared with fewer than 50 million combined for the 2018 and 2022 World Cups."],"durationSeconds":6.5914467089999995}}} +{"type":"emit_inbound","label":"assistant","frame":{"type":"assistant","message":{"model":"claude-sonnet-4-6","id":"msg_01Cu2HWhZV3Mp5ukA4H97kP7","type":"message","role":"assistant","content":[{"type":"text","text":"web search fixture complete\n\nSources:\n- [Tickets - FIFA World Cup 2026™](https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/tickets)\n- [FIFA chief Infantino defends World Cup ticket prices | Al Jazeera](https://www.aljazeera.com/sports/2026/5/6/fifa-chief-infantino-defends-world-cup-ticket-prices)\n- [Why are FIFA World Cup 2026 tickets so expensive? | Al Jazeera](https://www.aljazeera.com/sports/2026/4/23/why-are-fifa-world-cup-2026-tickets-so-expensive)\n- [2026 World Cup Ticket Prices by City | The World Cup Guide](https://theworldcupguide.com/2026-world-cup-ticket-prices/)\n- [How Much Do 2026 World Cup Tickets Cost? | Jetpac Global](https://www.jetpacglobal.com/blog/fifa-world-cup-2026-ticket-prices/)"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":1065,"cache_read_input_tokens":12445,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1065},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19","uuid":"84c57a60-1b00-4e0c-a34a-b6431a3b5590"}} +{"type":"emit_inbound","label":"result","frame":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":15297,"duration_api_ms":14227,"num_turns":3,"result":"web search fixture complete\n\nSources:\n- [Tickets - FIFA World Cup 2026™](https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/tickets)\n- [FIFA chief Infantino defends World Cup ticket prices | Al Jazeera](https://www.aljazeera.com/sports/2026/5/6/fifa-chief-infantino-defends-world-cup-ticket-prices)\n- [Why are FIFA World Cup 2026 tickets so expensive? | Al Jazeera](https://www.aljazeera.com/sports/2026/4/23/why-are-fifa-world-cup-2026-tickets-so-expensive)\n- [2026 World Cup Ticket Prices by City | The World Cup Guide](https://theworldcupguide.com/2026-world-cup-ticket-prices/)\n- [How Much Do 2026 World Cup Tickets Cost? | Jetpac Global](https://www.jetpacglobal.com/blog/fifa-world-cup-2026-ticket-prices/)","stop_reason":"end_turn","session_id":"f5ff7f51-4729-4331-93bd-416f3fdf2d19","total_cost_usd":0.09358280000000001,"usage":{"input_tokens":7,"cache_creation_input_tokens":13510,"cache_read_input_tokens":24246,"output_tokens":471,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":13510,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":268,"cache_read_input_tokens":12445,"cache_creation_input_tokens":1065,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1065},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-4-6":{"inputTokens":7,"outputTokens":471,"cacheReadInputTokens":24246,"cacheCreationInputTokens":13510,"webSearchRequests":0,"costUSD":0.0650223,"contextWindow":200000,"maxOutputTokens":32000},"claude-haiku-4-5-20251001":{"inputTokens":2363,"outputTokens":524,"cacheReadInputTokens":0,"cacheCreationInputTokens":10862,"webSearchRequests":1,"costUSD":0.028560500000000003,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"b0c24262-d526-4ef7-b0d6-92b91e51915c"}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/web_search/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/codex_output.ts new file mode 100644 index 00000000000..eeae90be444 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/codex_output.ts @@ -0,0 +1,41 @@ +import { assert } from "@effect/vitest"; +import type { ProviderReplayTranscript } from "@t3tools/contracts"; + +import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; +import { + assertAssistantTextIncludes, + assertBaseProjection, + assertConversationMessageRoles, + assertExecutionNodeKinds, + assertRuntimeRequestCounts, + assertSemanticProjectionIntegrity, + assertTurnItemTypes, + assertUserMessagesInclude, + assertVisibleTurnItemsMirrorLocalTurnItems, + projectionFor, + WEB_SEARCH_PROMPT, +} from "../shared.ts"; + +const WEB_SEARCH_QUERY = "FIFA World Cup 2026 ticket prices official"; + +export function assertWebSearchOutput( + result: OrchestratorV2ScenarioResult, + transcript: ProviderReplayTranscript, +) { + assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] }); + + const projection = projectionFor(result, transcript.scenario); + assertSemanticProjectionIntegrity(projection); + assertVisibleTurnItemsMirrorLocalTurnItems(projection); + assertExecutionNodeKinds(projection, ["root_turn", "tool_call", "assistant_message"]); + assertConversationMessageRoles(projection, ["user", "assistant"]); + assertTurnItemTypes(projection, ["user_message", "web_search", "assistant_message"]); + assertRuntimeRequestCounts(projection, { total: 0 }); + assertUserMessagesInclude(projection, [WEB_SEARCH_PROMPT]); + assertAssistantTextIncludes(projection, "web search fixture complete"); + + const webSearchItems = projection.turnItems.filter((item) => item.type === "web_search"); + assert.lengthOf(webSearchItems, 1); + assert.equal(webSearchItems[0]?.status, "completed"); + assert.include(webSearchItems[0]?.patterns ?? [], WEB_SEARCH_QUERY); +} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/web_search/codex_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/codex_transcript.ndjson new file mode 100644 index 00000000000..1305eaf121d --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/codex_transcript.ndjson @@ -0,0 +1,22 @@ +{"type":"transcript_start","provider":"codex","protocol":"codex.app-server","version":"0.120.0","scenario":"web_search","metadata":{"source":"codex-app-server-probe","fileName":"web_search.ndjson","description":"One turn with a native Codex webSearch item that should become normalized progress."}} +{"type":"expect_outbound","label":"initialize","frame":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"t3code_desktop","title":"T3 Code Desktop","version":"0.1.0"}}}} +{"type":"emit_inbound","label":"initialize","frame":{"id":1,"result":{"userAgent":"t3code_desktop/0.120.0 (Mac OS 26.4.1; arm64) dumb (t3code_desktop; 0.1.0)","codexHome":"/Users/julius/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"type":"expect_outbound","label":"initialized","frame":{"method":"initialized"}} +{"type":"expect_outbound","label":"thread/start","frame":{"id":2,"method":"thread/start","params":{}}} +{"type":"emit_inbound","label":"thread/start","frame":{"id":2,"result":{"thread":{"id":"019db20e-d982-70e9-a996-8e56a2f7968d","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776819600,"updatedAt":1776819600,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T18-00-00-019db20e-d982-70e9-a996-8e56a2f7968d.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]},"model":"gpt-5.4","modelProvider":"openai","serviceTier":"fast","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":["/Users/julius/.codex/memories"],"readOnlyAccess":{"type":"fullAccess"},"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"reasoningEffort":"xhigh"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"id":3,"method":"turn/start","params":{"input":[{"text":"Search the web for FIFA World Cup ticket pricing, then answer exactly: web search fixture complete","type":"text"}],"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d"}}} +{"type":"emit_inbound","label":"thread/started","frame":{"method":"thread/started","params":{"thread":{"id":"019db20e-d982-70e9-a996-8e56a2f7968d","forkedFromId":null,"preview":"","ephemeral":false,"modelProvider":"openai","createdAt":1776819600,"updatedAt":1776819600,"status":{"type":"idle"},"path":"/Users/julius/.codex/sessions/2026/04/21/rollout-2026-04-21T18-00-00-019db20e-d982-70e9-a996-8e56a2f7968d.jsonl","cwd":"/Users/julius/.t3/worktrees/codething-mvp/t3code-c1e5e1d1","cliVersion":"0.120.0","source":"vscode","agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,"turns":[]}}}} +{"type":"emit_inbound","label":"turn/start","frame":{"id":3,"result":{"turn":{"id":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf","items":[],"status":"inProgress","error":null,"startedAt":null,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","status":{"type":"active","activeFlags":[]}}}} +{"type":"emit_inbound","label":"turn/started","frame":{"method":"turn/started","params":{"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turn":{"id":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf","items":[],"status":"inProgress","error":null,"startedAt":1776819600,"completedAt":null,"durationMs":null}}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"userMessage","id":"a089d29b-7d1a-4b02-b628-5a5c4210a7ad","content":[{"type":"text","text":"Search the web for FIFA World Cup ticket pricing, then answer exactly: web search fixture complete","text_elements":[]}]},"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turnId":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"userMessage","id":"a089d29b-7d1a-4b02-b628-5a5c4210a7ad","content":[{"type":"text","text":"Search the web for FIFA World Cup ticket pricing, then answer exactly: web search fixture complete","text_elements":[]}]},"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turnId":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"webSearch","id":"ws_019db20f2b4d741ca59b7931c377143d","query":"","action":{"type":"other"}},"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turnId":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"webSearch","id":"ws_019db20f2b4d741ca59b7931c377143d","query":"FIFA World Cup 2026 ticket prices official","action":{"type":"search","query":"FIFA World Cup 2026 ticket prices official","queries":["FIFA World Cup 2026 ticket prices official"]}},"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turnId":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf"}}} +{"type":"emit_inbound","label":"item/started","frame":{"method":"item/started","params":{"item":{"type":"agentMessage","id":"msg_0b4f006af9ba4fd99d08163d5945120a","text":"","phase":"final_answer","memoryCitation":null},"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turnId":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turnId":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf","itemId":"msg_0b4f006af9ba4fd99d08163d5945120a","delta":"web search"}}} +{"type":"emit_inbound","label":"item/agentMessage/delta","frame":{"method":"item/agentMessage/delta","params":{"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turnId":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf","itemId":"msg_0b4f006af9ba4fd99d08163d5945120a","delta":" fixture complete"}}} +{"type":"emit_inbound","label":"item/completed","frame":{"method":"item/completed","params":{"item":{"type":"agentMessage","id":"msg_0b4f006af9ba4fd99d08163d5945120a","text":"web search fixture complete","phase":"final_answer","memoryCitation":null},"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turnId":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf"}}} +{"type":"emit_inbound","label":"thread/status/changed","frame":{"method":"thread/status/changed","params":{"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","status":{"type":"idle"}}}} +{"type":"emit_inbound","label":"turn/completed","frame":{"method":"turn/completed","params":{"threadId":"019db20e-d982-70e9-a996-8e56a2f7968d","turn":{"id":"019db20e-d98f-73c1-bb7e-e5d2dd9a77cf","items":[],"status":"completed","error":null,"startedAt":1776819600,"completedAt":1776819610,"durationMs":10000}}}} +{"type":"runtime_exit","status":"success"} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/web_search/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/input.ts new file mode 100644 index 00000000000..9de95fe069b --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/fixtures/web_search/input.ts @@ -0,0 +1,7 @@ +import { type OrchestratorFixtureInput, WEB_SEARCH_PROMPT } from "../shared.ts"; + +export function webSearchInput(): OrchestratorFixtureInput { + return { + steps: [{ type: "message", text: WEB_SEARCH_PROMPT }], + }; +} diff --git a/apps/server/src/orchestration-v2/testkit/index.ts b/apps/server/src/orchestration-v2/testkit/index.ts new file mode 100644 index 00000000000..17b95ad8d94 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/index.ts @@ -0,0 +1,4 @@ +export * from "./DeterministicRuntime.ts"; +export * from "./OrchestratorScenario.ts"; +export * from "./ProviderReplayHarness.ts"; +export * from "./ReplayTranscriptNdjson.ts"; diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts deleted file mode 100644 index 707c87c43c9..00000000000 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ /dev/null @@ -1,1147 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; -import * as NodeChildProcess from "node:child_process"; - -import { - ProviderDriverKind, - ProviderRuntimeEvent, - ProviderSession, - ProviderInstanceId, -} from "@t3tools/contracts"; -import { - CommandId, - DEFAULT_PROVIDER_INTERACTION_MODE, - EventId, - MessageId, - ProjectId, - ThreadId, - TurnId, -} from "@t3tools/contracts"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import * as Clock from "effect/Clock"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Layer from "effect/Layer"; -import * as ManagedRuntime from "effect/ManagedRuntime"; -import * as PubSub from "effect/PubSub"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import { afterEach, describe, expect, it, vi } from "vite-plus/test"; - -import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts"; -import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts"; -import * as VcsProcess from "../../vcs/VcsProcess.ts"; -import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts"; -import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; -import { CheckpointReactorLive } from "./CheckpointReactor.ts"; -import { OrchestrationEngineLive } from "./OrchestrationEngine.ts"; -import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts"; -import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts"; -import { RuntimeReceiptBusLive } from "./RuntimeReceiptBus.ts"; -import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts"; -import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts"; -import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; -import { - OrchestrationEngineService, - type OrchestrationEngineShape, -} from "../Services/OrchestrationEngine.ts"; -import { CheckpointReactor } from "../Services/CheckpointReactor.ts"; -import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; -import { - ProviderService, - type ProviderServiceShape, -} from "../../provider/Services/ProviderService.ts"; -import { checkpointRefForThreadTurn } from "../../checkpointing/Utils.ts"; -import { ServerConfig } from "../../config.ts"; -import * as WorkspaceEntries from "../../workspace/WorkspaceEntries.ts"; -import * as WorkspacePaths from "../../workspace/WorkspacePaths.ts"; - -const asProjectId = (value: string): ProjectId => ProjectId.make(value); -const asTurnId = (value: string): TurnId => TurnId.make(value); - -type LegacyProviderRuntimeEvent = { - readonly type: string; - readonly eventId: EventId; - readonly provider: ProviderDriverKind; - readonly createdAt: string; - readonly threadId: ThreadId; - readonly turnId?: string | undefined; - readonly itemId?: string | undefined; - readonly requestId?: string | undefined; - readonly payload?: unknown | undefined; - readonly [key: string]: unknown; -}; - -function createProviderServiceHarness( - cwd: string, - hasSession = true, - sessionCwd = cwd, - providerName: ProviderSession["provider"] = ProviderDriverKind.make("codex"), -) { - const now = "2026-01-01T00:00:00.000Z"; - const runtimeEventPubSub = Effect.runSync(PubSub.unbounded()); - const rollbackConversation = vi.fn( - (_input: { readonly threadId: ThreadId; readonly numTurns: number }) => Effect.void, - ); - - const unsupported = () => - Effect.die(new Error("Unsupported provider call in test")) as Effect.Effect; - const listSessions = () => - hasSession - ? Effect.succeed([ - { - provider: providerName, - status: "ready", - runtimeMode: "full-access", - threadId: ThreadId.make("thread-1"), - cwd: sessionCwd, - createdAt: now, - updatedAt: now, - }, - ] satisfies ReadonlyArray) - : Effect.succeed([] as ReadonlyArray); - const service: ProviderServiceShape = { - startSession: () => unsupported(), - sendTurn: () => unsupported(), - interruptTurn: () => unsupported(), - respondToRequest: () => unsupported(), - respondToUserInput: () => unsupported(), - stopSession: () => unsupported(), - listSessions, - getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }), - getInstanceInfo: (instanceId) => - Effect.succeed({ - instanceId, - driverKind: ProviderDriverKind.make(providerName), - displayName: undefined, - enabled: true, - continuationIdentity: { - driverKind: ProviderDriverKind.make(providerName), - continuationKey: `${providerName}:instance:${instanceId}`, - }, - }), - rollbackConversation, - get streamEvents() { - return Stream.fromPubSub(runtimeEventPubSub); - }, - }; - - const emit = (event: LegacyProviderRuntimeEvent): void => { - Effect.runSync(PubSub.publish(runtimeEventPubSub, event as unknown as ProviderRuntimeEvent)); - }; - - return { - service, - rollbackConversation, - emit, - }; -} - -async function waitForThread( - readModel: () => Promise<{ - readonly threads: ReadonlyArray<{ - readonly id: ThreadId; - readonly latestTurn: { readonly turnId: string } | null; - readonly checkpoints: ReadonlyArray<{ readonly checkpointTurnCount: number }>; - readonly activities: ReadonlyArray<{ readonly kind: string }>; - }>; - }>, - predicate: (thread: { - latestTurn: { turnId: string } | null; - checkpoints: ReadonlyArray<{ checkpointTurnCount: number }>; - activities: ReadonlyArray<{ kind: string }>; - }) => boolean, - timeoutMs = 15_000, -) { - const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs; - const poll = async (): Promise<{ - latestTurn: { turnId: string } | null; - checkpoints: ReadonlyArray<{ checkpointTurnCount: number }>; - activities: ReadonlyArray<{ kind: string }>; - }> => { - const snapshot = await readModel(); - const thread = snapshot.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - if (thread && predicate(thread)) { - return thread; - } - if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) { - throw new Error("Timed out waiting for thread state."); - } - await Effect.runPromise(Effect.sleep("10 millis")); - return poll(); - }; - return poll(); -} - -async function waitForEvent( - engine: OrchestrationEngineShape, - predicate: (event: { type: string }) => boolean, - timeoutMs = 15_000, -) { - const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs; - const poll = async () => { - const events = await Effect.runPromise( - Stream.runCollect(engine.readEvents(0)).pipe(Effect.map((chunk) => Array.from(chunk))), - ); - if (events.some(predicate)) { - return events; - } - if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) { - throw new Error("Timed out waiting for orchestration event."); - } - await Effect.runPromise(Effect.sleep("10 millis")); - return poll(); - }; - return poll(); -} - -function runGit(cwd: string, args: ReadonlyArray) { - return NodeChildProcess.execFileSync("git", args, { - cwd, - stdio: ["ignore", "pipe", "pipe"], - encoding: "utf8", - }); -} - -function createGitRepository() { - const cwd = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-checkpoint-handler-")); - runGit(cwd, ["init", "--initial-branch=main"]); - runGit(cwd, ["config", "user.email", "test@example.com"]); - runGit(cwd, ["config", "user.name", "Test User"]); - NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), "v1\n", "utf8"); - runGit(cwd, ["add", "."]); - runGit(cwd, ["commit", "-m", "Initial"]); - return cwd; -} - -function gitRefExists(cwd: string, ref: string): boolean { - try { - runGit(cwd, ["show-ref", "--verify", "--quiet", ref]); - return true; - } catch { - return false; - } -} - -function gitShowFileAtRef(cwd: string, ref: string, filePath: string): string { - return runGit(cwd, ["show", `${ref}:${filePath}`]); -} - -async function waitForGitRefExists(cwd: string, ref: string, timeoutMs = 15_000) { - const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs; - const poll = async (): Promise => { - if (gitRefExists(cwd, ref)) { - return; - } - if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) { - throw new Error(`Timed out waiting for git ref '${ref}'.`); - } - await Effect.runPromise(Effect.sleep("10 millis")); - return poll(); - }; - return poll(); -} - -describe("CheckpointReactor", () => { - let runtime: ManagedRuntime.ManagedRuntime< - | OrchestrationEngineService - | CheckpointReactor - | CheckpointStore.CheckpointStore - | ProjectionSnapshotQuery, - unknown - > | null = null; - let scope: Scope.Closeable | null = null; - const tempDirs: string[] = []; - - afterEach(async () => { - if (scope) { - await Effect.runPromise(Scope.close(scope, Exit.void)); - } - scope = null; - if (runtime) { - await runtime.dispose(); - } - runtime = null; - while (tempDirs.length > 0) { - const dir = tempDirs.pop(); - if (dir) { - NodeFS.rmSync(dir, { recursive: true, force: true }); - } - } - }); - - async function createHarness(options?: { - readonly hasSession?: boolean; - readonly seedFilesystemCheckpoints?: boolean; - readonly projectWorkspaceRoot?: string; - readonly threadWorktreePath?: string | null; - readonly providerSessionCwd?: string; - readonly providerName?: ProviderDriverKind; - readonly gitStatusRefreshCalls?: Array; - }) { - const cwd = createGitRepository(); - tempDirs.push(cwd); - const provider = createProviderServiceHarness( - cwd, - options?.hasSession ?? true, - options?.providerSessionCwd ?? cwd, - options?.providerName ?? ProviderDriverKind.make("codex"), - ); - const orchestrationLayer = OrchestrationEngineLive.pipe( - Layer.provide(OrchestrationProjectionSnapshotQueryLive), - Layer.provide(OrchestrationProjectionPipelineLive), - Layer.provide(OrchestrationEventStoreLive), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - ); - const projectionSnapshotLayer = OrchestrationProjectionSnapshotQueryLive.pipe( - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - ); - - const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { - prefix: "t3-checkpoint-reactor-test-", - }); - const vcsStatusBroadcasterLayer = Layer.succeed(VcsStatusBroadcaster, { - getStatus: () => Effect.die("getStatus should not be called in this test"), - refreshLocalStatus: (cwd: string) => - Effect.sync(() => { - options?.gitStatusRefreshCalls?.push(cwd); - }).pipe( - Effect.as({ - isRepo: true, - hasPrimaryRemote: false, - isDefaultRef: true, - refName: "main", - hasWorkingTreeChanges: false, - workingTree: { files: [], insertions: 0, deletions: 0 }, - }), - ), - refreshStatus: () => Effect.die("refreshStatus should not be called in this test"), - streamStatus: () => Stream.empty, - }); - - const layer = CheckpointReactorLive.pipe( - Layer.provideMerge(orchestrationLayer), - Layer.provideMerge(projectionSnapshotLayer), - Layer.provideMerge(RuntimeReceiptBusLive), - Layer.provideMerge(Layer.succeed(ProviderService, provider.service)), - Layer.provideMerge(vcsStatusBroadcasterLayer), - Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer))), - Layer.provideMerge( - WorkspaceEntries.layer.pipe( - Layer.provide(WorkspacePaths.layer), - Layer.provideMerge(VcsDriverRegistry.layer), - ), - ), - Layer.provideMerge(WorkspacePaths.layer), - Layer.provideMerge(VcsProcess.layer), - Layer.provideMerge(ServerConfigLayer), - Layer.provideMerge(NodeServices.layer), - ); - - runtime = ManagedRuntime.make(layer); - const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); - const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery)); - const reactor = await runtime.runPromise(Effect.service(CheckpointReactor)); - const checkpointStore = await runtime.runPromise( - Effect.service(CheckpointStore.CheckpointStore), - ); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reactor.start().pipe(Scope.provide(scope))); - const drain = () => Effect.runPromise(reactor.drain); - - const createdAt = "2026-01-01T00:00:00.000Z"; - await Effect.runPromise( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-create"), - projectId: asProjectId("project-1"), - title: "Test Project", - workspaceRoot: options?.projectWorkspaceRoot ?? cwd, - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - await Effect.runPromise( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create"), - threadId: ThreadId.make("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: options?.threadWorktreePath ?? cwd, - createdAt, - }), - ); - - if (options?.seedFilesystemCheckpoints ?? true) { - await runtime.runPromise( - checkpointStore.captureCheckpoint({ - cwd, - checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0), - }), - ); - NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), "v2\n", "utf8"); - await runtime.runPromise( - checkpointStore.captureCheckpoint({ - cwd, - checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1), - }), - ); - NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), "v3\n", "utf8"); - await runtime.runPromise( - checkpointStore.captureCheckpoint({ - cwd, - checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 2), - }), - ); - } - - return { - engine, - readModel: () => Effect.runPromise(snapshotQuery.getSnapshot()), - provider, - cwd, - drain, - }; - } - - it("captures pre-turn baseline on turn.started and post-turn checkpoint on turn.completed", async () => { - const harness = await createHarness({ seedFilesystemCheckpoints: false }); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-capture"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - harness.provider.emit({ - type: "turn.started", - eventId: EventId.make("evt-turn-started-1"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-1"), - }); - await waitForGitRefExists( - harness.cwd, - checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0), - ); - - NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "v2\n", "utf8"); - harness.provider.emit({ - type: "turn.completed", - eventId: EventId.make("evt-turn-completed-1"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-1"), - payload: { state: "completed" }, - }); - - await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); - const thread = await waitForThread( - harness.readModel, - (entry) => entry.latestTurn?.turnId === "turn-1" && entry.checkpoints.length === 1, - ); - expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); - expect( - gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0)), - ).toBe(true); - expect( - gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1)), - ).toBe(true); - expect( - gitShowFileAtRef( - harness.cwd, - checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0), - "README.md", - ), - ).toBe("v1\n"); - expect( - gitShowFileAtRef( - harness.cwd, - checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1), - "README.md", - ), - ).toBe("v2\n"); - }); - - it("refreshes local git status state on turn completion using the session cwd", async () => { - const gitStatusRefreshCalls: string[] = []; - const harness = await createHarness({ - seedFilesystemCheckpoints: false, - gitStatusRefreshCalls, - }); - - harness.provider.emit({ - type: "turn.completed", - eventId: EventId.make("evt-turn-completed-refresh-local-status"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-refresh-local-status"), - payload: { state: "completed" }, - }); - - await harness.drain(); - - expect(gitStatusRefreshCalls).toEqual([harness.cwd]); - }); - - it("ignores auxiliary thread turn completion while primary turn is active", async () => { - const harness = await createHarness({ seedFilesystemCheckpoints: false }); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-primary-running"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "running", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: asTurnId("turn-main"), - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - harness.provider.emit({ - type: "turn.started", - eventId: EventId.make("evt-turn-started-main"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-main"), - }); - await waitForGitRefExists( - harness.cwd, - checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0), - ); - - NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "v2\n", "utf8"); - - harness.provider.emit({ - type: "turn.completed", - eventId: EventId.make("evt-turn-completed-aux"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-aux"), - payload: { state: "completed" }, - }); - - await harness.drain(); - const midReadModel = await harness.readModel(); - const midThread = midReadModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(midThread?.checkpoints).toHaveLength(0); - - harness.provider.emit({ - type: "turn.completed", - eventId: EventId.make("evt-turn-completed-main"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-main"), - payload: { state: "completed" }, - }); - - const thread = await waitForThread( - harness.readModel, - (entry) => entry.latestTurn?.turnId === "turn-main" && entry.checkpoints.length === 1, - ); - expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); - }); - - it("captures pre-turn and completion checkpoints for claude runtime events", async () => { - const harness = await createHarness({ - seedFilesystemCheckpoints: false, - providerName: ProviderDriverKind.make("claudeAgent"), - }); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-capture-claude"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "claudeAgent", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - harness.provider.emit({ - type: "turn.started", - eventId: EventId.make("evt-turn-started-claude-1"), - provider: ProviderDriverKind.make("claudeAgent"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-claude-1"), - }); - await waitForGitRefExists( - harness.cwd, - checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0), - ); - - NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "v2\n", "utf8"); - harness.provider.emit({ - type: "turn.completed", - eventId: EventId.make("evt-turn-completed-claude-1"), - provider: ProviderDriverKind.make("claudeAgent"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-claude-1"), - payload: { state: "completed" }, - }); - - await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); - const thread = await waitForThread( - harness.readModel, - (entry) => entry.latestTurn?.turnId === "turn-claude-1" && entry.checkpoints.length === 1, - ); - - expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); - expect( - gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1)), - ).toBe(true); - }); - - it("appends capture failure activity when turn diff summary cannot be derived", async () => { - const harness = await createHarness({ seedFilesystemCheckpoints: false }); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-missing-baseline-diff"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - harness.provider.emit({ - type: "turn.completed", - eventId: EventId.make("evt-turn-completed-missing-baseline"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-missing-baseline"), - payload: { state: "completed" }, - }); - - await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); - const thread = await waitForThread( - harness.readModel, - (entry) => - entry.checkpoints.length === 1 && - entry.activities.some((activity) => activity.kind === "checkpoint.capture.failed"), - ); - - expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); - expect( - thread.activities.some((activity) => activity.kind === "checkpoint.capture.failed"), - ).toBe(true); - }); - - it("captures pre-turn baseline from project workspace root when thread worktree is unset", async () => { - const harness = await createHarness({ - hasSession: false, - seedFilesystemCheckpoints: false, - threadWorktreePath: null, - }); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-for-baseline"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: MessageId.make("message-user-1"), - role: "user", - text: "start turn", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: "2026-01-01T00:00:00.000Z", - }), - ); - - await waitForGitRefExists( - harness.cwd, - checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0), - ); - expect( - gitShowFileAtRef( - harness.cwd, - checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0), - "README.md", - ), - ).toBe("v1\n"); - }); - - it("captures turn completion checkpoint from project workspace root when provider session cwd is unavailable", async () => { - const harness = await createHarness({ - hasSession: false, - seedFilesystemCheckpoints: false, - threadWorktreePath: null, - }); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-missing-provider-cwd"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "running", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: asTurnId("turn-missing-cwd"), - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "v2\n", "utf8"); - harness.provider.emit({ - type: "turn.completed", - eventId: EventId.make("evt-turn-completed-missing-provider-cwd"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-missing-cwd"), - payload: { state: "completed" }, - }); - - await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); - expect( - gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1)), - ).toBe(true); - expect( - gitShowFileAtRef( - harness.cwd, - checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1), - "README.md", - ), - ).toBe("v2\n"); - }); - - it("ignores non-v2 checkpoint.captured runtime events", async () => { - const harness = await createHarness(); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-checkpoint-captured"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - harness.provider.emit({ - type: "checkpoint.captured", - eventId: EventId.make("evt-checkpoint-captured-3"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-3"), - turnCount: 3, - status: "completed", - }); - - await harness.drain(); - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.checkpoints.some((checkpoint) => checkpoint.checkpointTurnCount === 3)).toBe( - false, - ); - }); - - it("continues processing runtime events after a single checkpoint runtime failure", async () => { - const nonRepositorySessionCwd = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-checkpoint-runtime-non-repo-"), - ); - tempDirs.push(nonRepositorySessionCwd); - - const harness = await createHarness({ - seedFilesystemCheckpoints: false, - providerSessionCwd: nonRepositorySessionCwd, - }); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-non-repo-runtime"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - harness.provider.emit({ - type: "turn.completed", - eventId: EventId.make("evt-runtime-capture-failure"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-runtime-failure"), - payload: { state: "completed" }, - }); - - harness.provider.emit({ - type: "turn.started", - eventId: EventId.make("evt-turn-started-after-runtime-failure"), - provider: ProviderDriverKind.make("codex"), - - createdAt: "2026-01-01T00:00:00.000Z", - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-after-runtime-failure"), - }); - - await waitForGitRefExists( - harness.cwd, - checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0), - ); - expect( - gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.make("thread-1"), 0)), - ).toBe(true); - }); - - it("executes provider revert and emits thread.reverted for checkpoint revert requests", async () => { - const harness = await createHarness(); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.diff.complete", - commandId: CommandId.make("cmd-diff-1"), - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-1"), - completedAt: createdAt, - checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1), - status: "ready", - files: [], - checkpointTurnCount: 1, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.diff.complete", - commandId: CommandId.make("cmd-diff-2"), - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-2"), - completedAt: createdAt, - checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 2), - status: "ready", - files: [], - checkpointTurnCount: 2, - createdAt, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.checkpoint.revert", - commandId: CommandId.make("cmd-revert-request"), - threadId: ThreadId.make("thread-1"), - turnCount: 1, - createdAt, - }), - ); - - await waitForEvent(harness.engine, (event) => event.type === "thread.reverted"); - const thread = await waitForThread( - harness.readModel, - (entry) => entry.checkpoints.length === 1, - ); - - expect(thread.latestTurn?.turnId).toBe("turn-1"); - expect(thread.checkpoints).toHaveLength(1); - expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); - expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); - expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ - threadId: ThreadId.make("thread-1"), - numTurns: 1, - }); - expect(NodeFS.readFileSync(NodePath.join(harness.cwd, "README.md"), "utf8")).toBe("v2\n"); - expect( - gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.make("thread-1"), 2)), - ).toBe(false); - }); - - it("executes provider revert and emits thread.reverted for claude sessions", async () => { - const harness = await createHarness({ providerName: ProviderDriverKind.make("claudeAgent") }); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-claude"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "claudeAgent", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.diff.complete", - commandId: CommandId.make("cmd-diff-claude-1"), - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-claude-1"), - completedAt: createdAt, - checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1), - status: "ready", - files: [], - checkpointTurnCount: 1, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.diff.complete", - commandId: CommandId.make("cmd-diff-claude-2"), - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-claude-2"), - completedAt: createdAt, - checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 2), - status: "ready", - files: [], - checkpointTurnCount: 2, - createdAt, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.checkpoint.revert", - commandId: CommandId.make("cmd-revert-request-claude"), - threadId: ThreadId.make("thread-1"), - turnCount: 1, - createdAt, - }), - ); - - await waitForEvent(harness.engine, (event) => event.type === "thread.reverted"); - expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); - expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ - threadId: ThreadId.make("thread-1"), - numTurns: 1, - }); - }); - - it("processes consecutive revert requests with deterministic rollback sequencing", async () => { - const harness = await createHarness(); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-inline-revert"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: createdAt, - }, - createdAt, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.diff.complete", - commandId: CommandId.make("cmd-inline-revert-diff-1"), - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-1"), - completedAt: createdAt, - checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1), - status: "ready", - files: [], - checkpointTurnCount: 1, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.diff.complete", - commandId: CommandId.make("cmd-inline-revert-diff-2"), - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-2"), - completedAt: createdAt, - checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 2), - status: "ready", - files: [], - checkpointTurnCount: 2, - createdAt, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.checkpoint.revert", - commandId: CommandId.make("cmd-sequenced-revert-request-1"), - threadId: ThreadId.make("thread-1"), - turnCount: 1, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.checkpoint.revert", - commandId: CommandId.make("cmd-sequenced-revert-request-0"), - threadId: ThreadId.make("thread-1"), - turnCount: 0, - createdAt, - }), - ); - - await harness.drain(); - - expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(2); - expect(harness.provider.rollbackConversation.mock.calls[0]?.[0]).toEqual({ - threadId: ThreadId.make("thread-1"), - numTurns: 1, - }); - expect(harness.provider.rollbackConversation.mock.calls[1]?.[0]).toEqual({ - threadId: ThreadId.make("thread-1"), - numTurns: 1, - }); - }); - - it("appends an error activity when revert is requested without an active session", async () => { - const harness = await createHarness({ hasSession: false }); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.checkpoint.revert", - commandId: CommandId.make("cmd-revert-no-session"), - threadId: ThreadId.make("thread-1"), - turnCount: 1, - createdAt, - }), - ); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some((activity) => activity.kind === "checkpoint.revert.failed"), - ); - - expect(thread.activities.some((activity) => activity.kind === "checkpoint.revert.failed")).toBe( - true, - ); - expect(harness.provider.rollbackConversation).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.ts deleted file mode 100644 index 3ba244ddf2c..00000000000 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.ts +++ /dev/null @@ -1,866 +0,0 @@ -import { - CommandId, - type CheckpointRef, - EventId, - MessageId, - type ProjectId, - ThreadId, - TurnId, - type OrchestrationEvent, - type ProviderRuntimeEvent, -} from "@t3tools/contracts"; -import * as Cause from "effect/Cause"; -import * as Crypto from "effect/Crypto"; -import * as DateTime from "effect/DateTime"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import type * as PlatformError from "effect/PlatformError"; -import * as Stream from "effect/Stream"; -import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; - -import { parseTurnDiffFilesFromUnifiedDiff } from "../../checkpointing/Diffs.ts"; -import { - checkpointRefForThreadTurn, - resolveThreadWorkspaceCwd, -} from "../../checkpointing/Utils.ts"; -import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts"; -import { ProviderService } from "../../provider/Services/ProviderService.ts"; -import { CheckpointReactor, type CheckpointReactorShape } from "../Services/CheckpointReactor.ts"; -import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; -import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; -import { RuntimeReceiptBus } from "../Services/RuntimeReceiptBus.ts"; -import type { CheckpointStoreError } from "../../checkpointing/Errors.ts"; -import type { OrchestrationDispatchError } from "../Errors.ts"; -import { isGitRepository } from "../../git/Utils.ts"; -import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts"; -import * as WorkspaceEntries from "../../workspace/WorkspaceEntries.ts"; - -const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - -type ReactorInput = - | { - readonly source: "runtime"; - readonly event: ProviderRuntimeEvent; - } - | { - readonly source: "domain"; - readonly event: OrchestrationEvent; - }; - -function toTurnId(value: string | undefined): TurnId | null { - return value === undefined ? null : TurnId.make(String(value)); -} - -function sameId(left: string | null | undefined, right: string | null | undefined): boolean { - if (left === null || left === undefined || right === null || right === undefined) { - return false; - } - return left === right; -} - -function checkpointStatusFromRuntime(status: string | undefined): "ready" | "missing" | "error" { - switch (status) { - case "failed": - return "error"; - case "cancelled": - case "interrupted": - return "missing"; - case "completed": - default: - return "ready"; - } -} - -const make = Effect.gen(function* () { - const crypto = yield* Crypto.Crypto; - const randomUUID = crypto.randomUUIDv4; - const serverEventId = randomUUID.pipe(Effect.map(EventId.make)); - const serverCommandId = (tag: string) => - randomUUID.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`))); - const orchestrationEngine = yield* OrchestrationEngineService; - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; - const providerService = yield* ProviderService; - const checkpointStore = yield* CheckpointStore.CheckpointStore; - const receiptBus = yield* RuntimeReceiptBus; - const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; - const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; - - const appendRevertFailureActivity = (input: { - readonly threadId: ThreadId; - readonly turnCount: number; - readonly detail: string; - readonly createdAt: string; - }) => - Effect.all({ - commandId: serverCommandId("checkpoint-revert-failure"), - activityId: serverEventId, - }).pipe( - Effect.flatMap(({ commandId, activityId }) => - orchestrationEngine.dispatch({ - type: "thread.activity.append", - commandId, - threadId: input.threadId, - activity: { - id: activityId, - tone: "error", - kind: "checkpoint.revert.failed", - summary: "Checkpoint revert failed", - payload: { - turnCount: input.turnCount, - detail: input.detail, - }, - turnId: null, - createdAt: input.createdAt, - }, - createdAt: input.createdAt, - }), - ), - ); - - const appendCaptureFailureActivity = (input: { - readonly threadId: ThreadId; - readonly turnId: TurnId | null; - readonly detail: string; - readonly createdAt: string; - }) => - Effect.all({ - commandId: serverCommandId("checkpoint-capture-failure"), - activityId: serverEventId, - }).pipe( - Effect.flatMap(({ commandId, activityId }) => - orchestrationEngine.dispatch({ - type: "thread.activity.append", - commandId, - threadId: input.threadId, - activity: { - id: activityId, - tone: "error", - kind: "checkpoint.capture.failed", - summary: "Checkpoint capture failed", - payload: { - detail: input.detail, - }, - turnId: input.turnId, - createdAt: input.createdAt, - }, - createdAt: input.createdAt, - }), - ), - ); - - const resolveSessionRuntimeForThread = Effect.fn("resolveSessionRuntimeForThread")(function* ( - threadId: ThreadId, - ): Effect.fn.Return> { - const sessions = yield* providerService.listSessions(); - const session = sessions.find((entry) => entry.threadId === threadId); - return session?.cwd - ? Option.some({ threadId: session.threadId, cwd: session.cwd }) - : Option.none(); - }); - - const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) { - return yield* projectionSnapshotQuery - .getThreadDetailById(threadId) - .pipe(Effect.map(Option.getOrUndefined)); - }); - - const resolveThreadProjects = Effect.fn("resolveThreadProjects")(function* ( - projectId: ProjectId, - ) { - const project = yield* projectionSnapshotQuery - .getProjectShellById(projectId) - .pipe(Effect.map(Option.getOrUndefined)); - return project ? [project] : []; - }); - - const isGitWorkspace = (cwd: string) => isGitRepository(cwd); - - // Resolves the workspace CWD for checkpoint operations, preferring the - // active provider session CWD and falling back to the thread/project config. - // Returns undefined when no CWD can be determined or the workspace is not - // a git repository. - const resolveCheckpointCwd = Effect.fn("resolveCheckpointCwd")(function* (input: { - readonly threadId: ThreadId; - readonly thread: { readonly projectId: ProjectId; readonly worktreePath: string | null }; - readonly projects: ReadonlyArray<{ readonly id: ProjectId; readonly workspaceRoot: string }>; - readonly preferSessionRuntime: boolean; - }): Effect.fn.Return { - const fromSession = yield* resolveSessionRuntimeForThread(input.threadId); - const fromThread = resolveThreadWorkspaceCwd({ - thread: input.thread, - projects: input.projects, - }); - - const cwd = input.preferSessionRuntime - ? (Option.match(fromSession, { - onNone: () => undefined, - onSome: (runtime) => runtime.cwd, - }) ?? fromThread) - : (fromThread ?? - Option.match(fromSession, { - onNone: () => undefined, - onSome: (runtime) => runtime.cwd, - })); - - if (!cwd) { - return undefined; - } - if (!isGitWorkspace(cwd)) { - return undefined; - } - return cwd; - }); - - // Shared tail for both capture paths: creates the git checkpoint ref, diffs - // it against the previous turn, then dispatches the domain events to update - // the orchestration read model. - const captureAndDispatchCheckpoint = Effect.fn("captureAndDispatchCheckpoint")(function* (input: { - readonly threadId: ThreadId; - readonly turnId: TurnId; - readonly thread: { - readonly messages: ReadonlyArray<{ - readonly id: MessageId; - readonly role: string; - readonly turnId: TurnId | null; - }>; - }; - readonly cwd: string; - readonly turnCount: number; - readonly status: "ready" | "missing" | "error"; - readonly assistantMessageId: MessageId | undefined; - readonly createdAt: string; - }) { - const fromTurnCount = Math.max(0, input.turnCount - 1); - const fromCheckpointRef = checkpointRefForThreadTurn(input.threadId, fromTurnCount); - const targetCheckpointRef = checkpointRefForThreadTurn(input.threadId, input.turnCount); - - const fromCheckpointExists = yield* checkpointStore.hasCheckpointRef({ - cwd: input.cwd, - checkpointRef: fromCheckpointRef, - }); - if (!fromCheckpointExists) { - yield* Effect.logWarning("checkpoint capture missing pre-turn baseline", { - threadId: input.threadId, - turnId: input.turnId, - fromTurnCount, - }); - } - - yield* checkpointStore.captureCheckpoint({ - cwd: input.cwd, - checkpointRef: targetCheckpointRef, - }); - - // Refresh the workspace entry index so the @-mention file picker - // reflects files created or deleted during this turn. - yield* workspaceEntries.refresh(input.cwd); - - const files = yield* checkpointStore - .diffCheckpoints({ - cwd: input.cwd, - fromCheckpointRef, - toCheckpointRef: targetCheckpointRef, - fallbackFromToHead: false, - ignoreWhitespace: false, - }) - .pipe( - Effect.map((diff) => - parseTurnDiffFilesFromUnifiedDiff(diff).map((file) => ({ - path: file.path, - kind: "modified" as const, - additions: file.additions, - deletions: file.deletions, - })), - ), - Effect.tapError((error) => - appendCaptureFailureActivity({ - threadId: input.threadId, - turnId: input.turnId, - detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`, - createdAt: input.createdAt, - }), - ), - Effect.catch((error) => - Effect.logWarning("failed to derive checkpoint file summary", { - threadId: input.threadId, - turnId: input.turnId, - turnCount: input.turnCount, - detail: error.message, - }).pipe(Effect.as([])), - ), - ); - - const assistantMessageId = - input.assistantMessageId ?? - input.thread.messages - .toReversed() - .find((entry) => entry.role === "assistant" && entry.turnId === input.turnId)?.id ?? - MessageId.make(`assistant:${input.turnId}`); - - yield* orchestrationEngine.dispatch({ - type: "thread.turn.diff.complete", - commandId: yield* serverCommandId("checkpoint-turn-diff-complete"), - threadId: input.threadId, - turnId: input.turnId, - completedAt: input.createdAt, - checkpointRef: targetCheckpointRef, - status: input.status, - files, - assistantMessageId, - checkpointTurnCount: input.turnCount, - createdAt: input.createdAt, - }); - yield* receiptBus.publish({ - type: "checkpoint.diff.finalized", - threadId: input.threadId, - turnId: input.turnId, - checkpointTurnCount: input.turnCount, - checkpointRef: targetCheckpointRef, - status: input.status, - createdAt: input.createdAt, - }); - yield* receiptBus.publish({ - type: "turn.processing.quiesced", - threadId: input.threadId, - turnId: input.turnId, - checkpointTurnCount: input.turnCount, - createdAt: input.createdAt, - }); - - yield* orchestrationEngine.dispatch({ - type: "thread.activity.append", - commandId: yield* serverCommandId("checkpoint-captured-activity"), - threadId: input.threadId, - activity: { - id: EventId.make(yield* randomUUID), - tone: "info", - kind: "checkpoint.captured", - summary: "Checkpoint captured", - payload: { - turnCount: input.turnCount, - status: input.status, - }, - turnId: input.turnId, - createdAt: input.createdAt, - }, - createdAt: input.createdAt, - }); - }); - - // Captures a real git checkpoint when a turn completes via a runtime event. - const captureCheckpointFromTurnCompletion = Effect.fn("captureCheckpointFromTurnCompletion")( - function* (event: Extract) { - const turnId = toTurnId(event.turnId); - if (!turnId) { - return; - } - - const thread = yield* resolveThreadDetail(event.threadId); - if (!thread) { - return; - } - - // When a primary turn is active, only that turn may produce completion checkpoints. - if (thread.session?.activeTurnId && !sameId(thread.session.activeTurnId, turnId)) { - return; - } - - // Only skip if a real (non-placeholder) checkpoint already exists for this turn. - // ProviderRuntimeIngestion may insert placeholder entries with status "missing" - // before this reactor runs; those must not prevent real git capture. - if ( - thread.checkpoints.some( - (checkpoint) => checkpoint.turnId === turnId && checkpoint.status !== "missing", - ) - ) { - return; - } - - const projects = yield* resolveThreadProjects(thread.projectId); - const checkpointCwd = yield* resolveCheckpointCwd({ - threadId: thread.id, - thread, - projects, - preferSessionRuntime: true, - }); - if (!checkpointCwd) { - return; - } - - // If a placeholder checkpoint exists for this turn, reuse its turn count - // instead of incrementing past it. - const existingPlaceholder = thread.checkpoints.find( - (checkpoint) => checkpoint.turnId === turnId && checkpoint.status === "missing", - ); - const currentTurnCount = thread.checkpoints.reduce( - (maxTurnCount, checkpoint) => Math.max(maxTurnCount, checkpoint.checkpointTurnCount), - 0, - ); - const nextTurnCount = existingPlaceholder - ? existingPlaceholder.checkpointTurnCount - : currentTurnCount + 1; - - yield* captureAndDispatchCheckpoint({ - threadId: thread.id, - turnId, - thread, - cwd: checkpointCwd, - turnCount: nextTurnCount, - status: checkpointStatusFromRuntime(event.payload.state), - assistantMessageId: undefined, - createdAt: event.createdAt, - }); - }, - ); - - // Captures a real git checkpoint when a placeholder checkpoint (status "missing") - // is detected via a domain event. This replaces the placeholder with a real - // git-ref-based checkpoint. - // - // ProviderRuntimeIngestion creates placeholder checkpoints on turn.diff.updated - // events from the Codex runtime. This handler fires when the corresponding - // domain event arrives, allowing the reactor to capture the actual filesystem - // state into a git ref and dispatch a replacement checkpoint. - const captureCheckpointFromPlaceholder = Effect.fn("captureCheckpointFromPlaceholder")(function* ( - event: Extract, - ) { - const { threadId, turnId, checkpointTurnCount, status } = event.payload; - - // Only replace placeholders; skip events from our own real captures. - if (status !== "missing") { - return; - } - - const thread = yield* resolveThreadDetail(threadId); - if (!thread) { - yield* Effect.logWarning("checkpoint capture from placeholder skipped: thread not found", { - threadId, - }); - return; - } - - // If a real checkpoint already exists for this turn, skip. - if ( - thread.checkpoints.some( - (checkpoint) => checkpoint.turnId === turnId && checkpoint.status !== "missing", - ) - ) { - yield* Effect.logDebug( - "checkpoint capture from placeholder skipped: real checkpoint already exists", - { threadId, turnId }, - ); - return; - } - - const projects = yield* resolveThreadProjects(thread.projectId); - const checkpointCwd = yield* resolveCheckpointCwd({ - threadId, - thread, - projects, - preferSessionRuntime: true, - }); - if (!checkpointCwd) { - return; - } - - yield* captureAndDispatchCheckpoint({ - threadId, - turnId, - thread, - cwd: checkpointCwd, - turnCount: checkpointTurnCount, - status: "ready", - assistantMessageId: event.payload.assistantMessageId ?? undefined, - createdAt: event.payload.completedAt, - }); - }); - - const ensurePreTurnBaselineFromTurnStart = Effect.fn("ensurePreTurnBaselineFromTurnStart")( - function* (event: Extract) { - const turnId = toTurnId(event.turnId); - if (!turnId) { - return; - } - - const thread = yield* resolveThreadDetail(event.threadId); - if (!thread) { - return; - } - - const projects = yield* resolveThreadProjects(thread.projectId); - const checkpointCwd = yield* resolveCheckpointCwd({ - threadId: thread.id, - thread, - projects, - preferSessionRuntime: false, - }); - if (!checkpointCwd) { - return; - } - - const currentTurnCount = thread.checkpoints.reduce( - (maxTurnCount, checkpoint) => Math.max(maxTurnCount, checkpoint.checkpointTurnCount), - 0, - ); - const baselineCheckpointRef = checkpointRefForThreadTurn(thread.id, currentTurnCount); - const baselineExists = yield* checkpointStore.hasCheckpointRef({ - cwd: checkpointCwd, - checkpointRef: baselineCheckpointRef, - }); - if (baselineExists) { - return; - } - - yield* checkpointStore.captureCheckpoint({ - cwd: checkpointCwd, - checkpointRef: baselineCheckpointRef, - }); - yield* receiptBus.publish({ - type: "checkpoint.baseline.captured", - threadId: thread.id, - checkpointTurnCount: currentTurnCount, - checkpointRef: baselineCheckpointRef, - createdAt: event.createdAt, - }); - }, - ); - - const refreshLocalGitStatusFromTurnCompletion = Effect.fn( - "refreshLocalGitStatusFromTurnCompletion", - )(function* (event: Extract) { - const sessionRuntime = yield* resolveSessionRuntimeForThread(event.threadId); - if (Option.isNone(sessionRuntime)) { - return; - } - - yield* vcsStatusBroadcaster.refreshLocalStatus(sessionRuntime.value.cwd).pipe( - Effect.catch((error) => - Effect.logWarning("failed to refresh local git status after turn completion", { - threadId: event.threadId, - turnId: event.turnId ?? null, - cwd: sessionRuntime.value.cwd, - detail: error.message, - }), - ), - ); - }); - - const ensurePreTurnBaselineFromDomainTurnStart = Effect.fn( - "ensurePreTurnBaselineFromDomainTurnStart", - )(function* ( - event: Extract< - OrchestrationEvent, - { type: "thread.turn-start-requested" | "thread.message-sent" } - >, - ) { - if (event.type === "thread.message-sent") { - if ( - event.payload.role !== "user" || - event.payload.streaming || - event.payload.turnId !== null - ) { - return; - } - } - - const threadId = event.payload.threadId; - const thread = yield* resolveThreadDetail(threadId); - if (!thread) { - return; - } - - const projects = yield* resolveThreadProjects(thread.projectId); - const checkpointCwd = yield* resolveCheckpointCwd({ - threadId, - thread, - projects, - preferSessionRuntime: false, - }); - if (!checkpointCwd) { - return; - } - - const currentTurnCount = thread.checkpoints.reduce( - (maxTurnCount, checkpoint) => Math.max(maxTurnCount, checkpoint.checkpointTurnCount), - 0, - ); - const baselineCheckpointRef = checkpointRefForThreadTurn(threadId, currentTurnCount); - const baselineExists = yield* checkpointStore.hasCheckpointRef({ - cwd: checkpointCwd, - checkpointRef: baselineCheckpointRef, - }); - if (baselineExists) { - return; - } - - yield* checkpointStore.captureCheckpoint({ - cwd: checkpointCwd, - checkpointRef: baselineCheckpointRef, - }); - yield* receiptBus.publish({ - type: "checkpoint.baseline.captured", - threadId, - checkpointTurnCount: currentTurnCount, - checkpointRef: baselineCheckpointRef, - createdAt: event.occurredAt, - }); - }); - - const handleRevertRequested = Effect.fn("handleRevertRequested")(function* ( - event: Extract, - ) { - const now = DateTime.formatIso(yield* DateTime.now); - - const thread = yield* resolveThreadDetail(event.payload.threadId); - if (!thread) { - yield* appendRevertFailureActivity({ - threadId: event.payload.threadId, - turnCount: event.payload.turnCount, - detail: "Thread was not found in read model.", - createdAt: now, - }).pipe(Effect.catch(() => Effect.void)); - return; - } - - const sessionRuntime = yield* resolveSessionRuntimeForThread(event.payload.threadId); - if (Option.isNone(sessionRuntime)) { - yield* appendRevertFailureActivity({ - threadId: event.payload.threadId, - turnCount: event.payload.turnCount, - detail: "No active provider session with workspace cwd is bound to this thread.", - createdAt: now, - }).pipe(Effect.catch(() => Effect.void)); - return; - } - if (!isGitWorkspace(sessionRuntime.value.cwd)) { - yield* appendRevertFailureActivity({ - threadId: event.payload.threadId, - turnCount: event.payload.turnCount, - detail: "Checkpoints are unavailable because this project is not a git repository.", - createdAt: now, - }).pipe(Effect.catch(() => Effect.void)); - return; - } - - const currentTurnCount = thread.checkpoints.reduce( - (maxTurnCount, checkpoint) => Math.max(maxTurnCount, checkpoint.checkpointTurnCount), - 0, - ); - - if (event.payload.turnCount > currentTurnCount) { - yield* appendRevertFailureActivity({ - threadId: event.payload.threadId, - turnCount: event.payload.turnCount, - detail: `Checkpoint turn count ${event.payload.turnCount} exceeds current turn count ${currentTurnCount}.`, - createdAt: now, - }).pipe(Effect.catch(() => Effect.void)); - return; - } - - const targetCheckpointRef = - event.payload.turnCount === 0 - ? checkpointRefForThreadTurn(event.payload.threadId, 0) - : thread.checkpoints.find( - (checkpoint) => checkpoint.checkpointTurnCount === event.payload.turnCount, - )?.checkpointRef; - - if (!targetCheckpointRef) { - yield* appendRevertFailureActivity({ - threadId: event.payload.threadId, - turnCount: event.payload.turnCount, - detail: `Checkpoint ref for turn ${event.payload.turnCount} is unavailable in read model.`, - createdAt: now, - }).pipe(Effect.catch(() => Effect.void)); - return; - } - - const restored = yield* checkpointStore.restoreCheckpoint({ - cwd: sessionRuntime.value.cwd, - checkpointRef: targetCheckpointRef, - fallbackToHead: event.payload.turnCount === 0, - }); - if (!restored) { - yield* appendRevertFailureActivity({ - threadId: event.payload.threadId, - turnCount: event.payload.turnCount, - detail: `Filesystem checkpoint is unavailable for turn ${event.payload.turnCount}.`, - createdAt: now, - }).pipe(Effect.catch(() => Effect.void)); - return; - } - - // Refresh the workspace entry index so the @-mention file picker - // reflects the reverted filesystem state. - yield* workspaceEntries.refresh(sessionRuntime.value.cwd); - - const rolledBackTurns = Math.max(0, currentTurnCount - event.payload.turnCount); - if (rolledBackTurns > 0) { - yield* providerService.rollbackConversation({ - threadId: sessionRuntime.value.threadId, - numTurns: rolledBackTurns, - }); - } - - const staleCheckpointRefs: Array = []; - for (const checkpoint of thread.checkpoints) { - if (checkpoint.checkpointTurnCount > event.payload.turnCount) { - staleCheckpointRefs.push(checkpoint.checkpointRef); - } - } - - if (staleCheckpointRefs.length > 0) { - yield* checkpointStore.deleteCheckpointRefs({ - cwd: sessionRuntime.value.cwd, - checkpointRefs: staleCheckpointRefs, - }); - } - - yield* orchestrationEngine - .dispatch({ - type: "thread.revert.complete", - commandId: yield* serverCommandId("checkpoint-revert-complete"), - threadId: event.payload.threadId, - turnCount: event.payload.turnCount, - createdAt: now, - }) - .pipe( - Effect.catch((error) => - appendRevertFailureActivity({ - threadId: event.payload.threadId, - turnCount: event.payload.turnCount, - detail: error.message, - createdAt: now, - }), - ), - Effect.asVoid, - ); - }); - - const processDomainEvent = Effect.fn("processDomainEvent")(function* (event: OrchestrationEvent) { - if (event.type === "thread.turn-start-requested" || event.type === "thread.message-sent") { - yield* ensurePreTurnBaselineFromDomainTurnStart(event); - return; - } - - if (event.type === "thread.checkpoint-revert-requested") { - yield* handleRevertRequested(event).pipe( - Effect.catch((error) => - Effect.flatMap(nowIso, (createdAt) => - appendRevertFailureActivity({ - threadId: event.payload.threadId, - turnCount: event.payload.turnCount, - detail: error.message, - createdAt, - }), - ), - ), - ); - return; - } - - // When ProviderRuntimeIngestion creates a placeholder checkpoint (status "missing") - // from a turn.diff.updated runtime event, capture the real git checkpoint to - // replace it. The providerService.streamEvents PubSub does not reliably deliver - // turn.completed runtime events to this reactor (shared subscription), so - // reacting to the domain event is the reliable path. - if (event.type === "thread.turn-diff-completed") { - yield* captureCheckpointFromPlaceholder(event).pipe( - Effect.catch((error) => - Effect.flatMap(nowIso, (createdAt) => - appendCaptureFailureActivity({ - threadId: event.payload.threadId, - turnId: event.payload.turnId, - detail: error.message, - createdAt, - }).pipe(Effect.catch(() => Effect.void)), - ), - ), - ); - } - }); - - const processRuntimeEvent = Effect.fn("processRuntimeEvent")(function* ( - event: ProviderRuntimeEvent, - ) { - if (event.type === "turn.started") { - yield* ensurePreTurnBaselineFromTurnStart(event); - return; - } - - if (event.type === "turn.completed") { - const turnId = toTurnId(event.turnId); - yield* refreshLocalGitStatusFromTurnCompletion(event); - yield* captureCheckpointFromTurnCompletion(event).pipe( - Effect.catch((error) => - Effect.flatMap(nowIso, (createdAt) => - appendCaptureFailureActivity({ - threadId: event.threadId, - turnId, - detail: error.message, - createdAt, - }).pipe(Effect.catch(() => Effect.void)), - ), - ), - ); - return; - } - }); - - const processInput = ( - input: ReactorInput, - ): Effect.Effect< - void, - CheckpointStoreError | OrchestrationDispatchError | PlatformError.PlatformError, - never - > => - input.source === "domain" ? processDomainEvent(input.event) : processRuntimeEvent(input.event); - - const processInputSafely = (input: ReactorInput) => - processInput(input).pipe( - Effect.catchCause((cause) => { - if (Cause.hasInterruptsOnly(cause)) { - return Effect.failCause(cause); - } - return Effect.logWarning("checkpoint reactor failed to process input", { - source: input.source, - eventType: input.event.type, - cause: Cause.pretty(cause), - }); - }), - ); - - const worker = yield* makeDrainableWorker(processInputSafely); - - const start: CheckpointReactorShape["start"] = Effect.fn("start")(function* () { - yield* Effect.forkScoped( - Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { - if ( - event.type !== "thread.turn-start-requested" && - event.type !== "thread.message-sent" && - event.type !== "thread.checkpoint-revert-requested" && - event.type !== "thread.turn-diff-completed" - ) { - return Effect.void; - } - return worker.enqueue({ source: "domain", event }); - }), - ); - - yield* Effect.forkScoped( - Stream.runForEach(providerService.streamEvents, (event) => { - if (event.type !== "turn.started" && event.type !== "turn.completed") { - return Effect.void; - } - return worker.enqueue({ source: "runtime", event }); - }), - ); - }); - - return { - start, - drain: worker.drain, - } satisfies CheckpointReactorShape; -}); - -export const CheckpointReactorLive = Layer.effect(CheckpointReactor, make); diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts deleted file mode 100644 index b2ef0fed0f9..00000000000 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts +++ /dev/null @@ -1,1083 +0,0 @@ -import { - CheckpointRef, - CommandId, - DEFAULT_PROVIDER_INTERACTION_MODE, - MessageId, - ProjectId, - ThreadId, - TurnId, - type OrchestrationEvent, - ProviderInstanceId, -} from "@t3tools/contracts"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as ManagedRuntime from "effect/ManagedRuntime"; -import * as Metric from "effect/Metric"; -import * as Option from "effect/Option"; -import * as Queue from "effect/Queue"; -import * as Stream from "effect/Stream"; -import { describe, expect, it } from "vite-plus/test"; - -import { PersistenceSqlError } from "../../persistence/Errors.ts"; -import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts"; -import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts"; -import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; -import { - OrchestrationEventStore, - type OrchestrationEventStoreShape, -} from "../../persistence/Services/OrchestrationEventStore.ts"; -import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; -import { OrchestrationEngineLive } from "./OrchestrationEngine.ts"; -import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts"; -import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts"; -import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; -import { - OrchestrationProjectionPipeline, - type OrchestrationProjectionPipelineShape, -} from "../Services/ProjectionPipeline.ts"; -import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; -import { ServerConfig } from "../../config.ts"; - -const asProjectId = (value: string): ProjectId => ProjectId.make(value); -const asMessageId = (value: string): MessageId => MessageId.make(value); -const asTurnId = (value: string): TurnId => TurnId.make(value); -const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.make(value); - -async function createOrchestrationSystem() { - const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { - prefix: "t3-orchestration-engine-test-", - }); - const orchestrationLayer = Layer.mergeAll( - OrchestrationEngineLive.pipe( - Layer.provide(OrchestrationProjectionSnapshotQueryLive), - Layer.provide(OrchestrationProjectionPipelineLive), - ), - OrchestrationProjectionSnapshotQueryLive, - ).pipe( - Layer.provide(OrchestrationEventStoreLive), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - Layer.provideMerge(ServerConfigLayer), - Layer.provideMerge(NodeServices.layer), - ); - const runtime = ManagedRuntime.make(orchestrationLayer); - const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); - const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery)); - return { - engine, - readModel: () => runtime.runPromise(snapshotQuery.getSnapshot()), - run: (effect: Effect.Effect) => runtime.runPromise(effect), - dispose: () => runtime.dispose(), - }; -} - -function now() { - return "2026-01-01T00:00:00.000Z"; -} - -const hasMetricSnapshot = ( - snapshots: ReadonlyArray, - id: string, - attributes: Readonly>, -) => - snapshots.some( - (snapshot) => - snapshot.id === id && - Object.entries(attributes).every(([key, value]) => snapshot.attributes?.[key] === value), - ); - -describe("OrchestrationEngine", () => { - it("bootstraps command handling from persisted projections without reading the full snapshot", async () => { - let nextSequence = 8; - const eventStore: OrchestrationEventStoreShape = { - append: (event) => - Effect.sync(() => { - const savedEvent = { - ...event, - sequence: nextSequence, - } as OrchestrationEvent; - nextSequence += 1; - return savedEvent; - }), - readFromSequence: () => Stream.empty, - readAll: () => - Stream.fail( - new PersistenceSqlError({ - operation: "test.readAll", - detail: "historical replay should not be used during bootstrap", - }), - ), - }; - - const projectionSnapshot = { - snapshotSequence: 7, - updatedAt: "2026-03-03T00:00:04.000Z", - projects: [ - { - id: asProjectId("project-bootstrap"), - title: "Bootstrap Project", - workspaceRoot: "/tmp/project-bootstrap", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - scripts: [], - createdAt: "2026-03-03T00:00:00.000Z", - updatedAt: "2026-03-03T00:00:01.000Z", - deletedAt: null, - }, - ], - threads: [ - { - id: ThreadId.make("thread-bootstrap"), - projectId: asProjectId("project-bootstrap"), - title: "Bootstrap Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access" as const, - branch: null, - worktreePath: null, - latestTurn: null, - createdAt: "2026-03-03T00:00:02.000Z", - updatedAt: "2026-03-03T00:00:03.000Z", - archivedAt: null, - deletedAt: null, - messages: [], - proposedPlans: [], - activities: [], - checkpoints: [], - session: null, - }, - ], - }; - const commandReadModel = { - ...projectionSnapshot, - threads: projectionSnapshot.threads.map((thread) => ({ - ...thread, - messages: [], - proposedPlans: [], - activities: [], - checkpoints: [], - })), - }; - let fullSnapshotReadCount = 0; - - const layer = OrchestrationEngineLive.pipe( - Layer.provide( - Layer.succeed(ProjectionSnapshotQuery, { - getCommandReadModel: () => Effect.succeed(commandReadModel), - getSnapshot: () => - Effect.sync(() => { - fullSnapshotReadCount += 1; - return projectionSnapshot; - }), - getShellSnapshot: () => - Effect.succeed({ - snapshotSequence: projectionSnapshot.snapshotSequence, - projects: [], - threads: [], - updatedAt: projectionSnapshot.updatedAt, - }), - getArchivedShellSnapshot: () => - Effect.succeed({ - snapshotSequence: projectionSnapshot.snapshotSequence, - projects: [], - threads: [], - updatedAt: projectionSnapshot.updatedAt, - }), - getSnapshotSequence: () => - Effect.succeed({ snapshotSequence: projectionSnapshot.snapshotSequence }), - getCounts: () => Effect.succeed({ projectCount: 1, threadCount: 1 }), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getProjectShellById: () => Effect.succeed(Option.none()), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => Effect.succeed(Option.none()), - getFullThreadDiffContext: () => Effect.succeed(Option.none()), - getThreadShellById: () => Effect.succeed(Option.none()), - getThreadDetailById: () => Effect.succeed(Option.none()), - }), - ), - Layer.provide( - Layer.succeed(OrchestrationProjectionPipeline, { - bootstrap: Effect.void, - projectEvent: () => Effect.void, - } satisfies OrchestrationProjectionPipelineShape), - ), - Layer.provide(Layer.succeed(OrchestrationEventStore, eventStore)), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - Layer.provide(SqlitePersistenceMemory), - Layer.provideMerge(NodeServices.layer), - ); - - const runtime = ManagedRuntime.make(layer); - - const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); - const result = await runtime.runPromise( - engine.dispatch({ - type: "thread.meta.update", - commandId: CommandId.make("cmd-bootstrap-thread-update"), - threadId: ThreadId.make("thread-bootstrap"), - title: "Updated Bootstrap Thread", - }), - ); - - expect(result.sequence).toBe(8); - expect(fullSnapshotReadCount).toBe(0); - - await runtime.dispose(); - }); - - it("persists deterministic read models for repeated snapshot reads", async () => { - const createdAt = now(); - const system = await createOrchestrationSystem(); - const { engine } = system; - - await system.run( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-1-create"), - projectId: asProjectId("project-1"), - title: "Project 1", - workspaceRoot: "/tmp/project-1", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - await system.run( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-1-create"), - threadId: ThreadId.make("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await system.run( - engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("msg-1"), - role: "user", - text: "hello", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt, - }), - ); - - const readModelA = await system.readModel(); - const readModelB = await system.readModel(); - expect(readModelB).toEqual(readModelA); - await system.dispose(); - }); - - it("archives and unarchives threads through orchestration commands", async () => { - const system = await createOrchestrationSystem(); - const { engine } = system; - const createdAt = now(); - - await system.run( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-archive-create"), - projectId: asProjectId("project-archive"), - title: "Project Archive", - workspaceRoot: "/tmp/project-archive", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - await system.run( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-archive-create"), - threadId: ThreadId.make("thread-archive"), - projectId: asProjectId("project-archive"), - title: "Archive me", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - }), - ); - - await system.run( - engine.dispatch({ - type: "thread.archive", - commandId: CommandId.make("cmd-thread-archive"), - threadId: ThreadId.make("thread-archive"), - }), - ); - expect( - (await system.readModel()).threads.find((thread) => thread.id === "thread-archive") - ?.archivedAt, - ).not.toBeNull(); - - await system.run( - engine.dispatch({ - type: "thread.unarchive", - commandId: CommandId.make("cmd-thread-unarchive"), - threadId: ThreadId.make("thread-archive"), - }), - ); - expect( - (await system.readModel()).threads.find((thread) => thread.id === "thread-archive") - ?.archivedAt, - ).toBeNull(); - - await system.dispose(); - }); - - it("replays append-only events from sequence", async () => { - const system = await createOrchestrationSystem(); - const { engine } = system; - const createdAt = now(); - - await system.run( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-replay-create"), - projectId: asProjectId("project-replay"), - title: "Replay Project", - workspaceRoot: "/tmp/project-replay", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - await system.run( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-replay-create"), - threadId: ThreadId.make("thread-replay"), - projectId: asProjectId("project-replay"), - title: "replay", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await system.run( - engine.dispatch({ - type: "thread.delete", - commandId: CommandId.make("cmd-thread-replay-delete"), - threadId: ThreadId.make("thread-replay"), - }), - ); - - const events = await system.run( - Stream.runCollect(engine.readEvents(0)).pipe( - Effect.map((chunk): OrchestrationEvent[] => Array.from(chunk)), - ), - ); - expect(events.map((event) => event.type)).toEqual([ - "project.created", - "thread.created", - "thread.deleted", - ]); - await system.dispose(); - }); - - it("streams persisted domain events in order", async () => { - const system = await createOrchestrationSystem(); - const { engine } = system; - const createdAt = now(); - - await system.run( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-stream-create"), - projectId: asProjectId("project-stream"), - title: "Stream Project", - workspaceRoot: "/tmp/project-stream", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - - const eventTypes: string[] = []; - await system.run( - Effect.gen(function* () { - const eventQueue = yield* Queue.unbounded(); - yield* Effect.forkScoped( - Stream.take(engine.streamDomainEvents, 2).pipe( - Stream.runForEach((event) => Queue.offer(eventQueue, event).pipe(Effect.asVoid)), - ), - ); - yield* Effect.sleep("10 millis"); - yield* engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-stream-thread-create"), - threadId: ThreadId.make("thread-stream"), - projectId: asProjectId("project-stream"), - title: "domain-stream", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }); - yield* engine.dispatch({ - type: "thread.meta.update", - commandId: CommandId.make("cmd-stream-thread-update"), - threadId: ThreadId.make("thread-stream"), - title: "domain-stream-updated", - }); - eventTypes.push((yield* Queue.take(eventQueue)).type); - eventTypes.push((yield* Queue.take(eventQueue)).type); - }).pipe(Effect.scoped), - ); - - expect(eventTypes).toEqual(["thread.created", "thread.meta-updated"]); - await system.dispose(); - }); - - it("records command ack duration using the first committed event type", async () => { - const system = await createOrchestrationSystem(); - const { engine } = system; - const createdAt = now(); - - await system.run( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-ack-create"), - projectId: asProjectId("project-ack"), - title: "Ack Project", - workspaceRoot: "/tmp/project-ack", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - - await system.run( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-ack-create"), - threadId: ThreadId.make("thread-ack"), - projectId: asProjectId("project-ack"), - title: "Ack Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - }), - ); - - const snapshots = await system.run(Metric.snapshot); - expect( - hasMetricSnapshot(snapshots, "t3_orchestration_command_ack_duration", { - commandType: "thread.create", - aggregateKind: "thread", - ackEventType: "thread.created", - }), - ).toBe(true); - - await system.dispose(); - }); - - it("records failed command dispatches as metric failures", async () => { - const system = await createOrchestrationSystem(); - const { engine } = system; - const createdAt = now(); - - await expect( - system.run( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-missing-project"), - threadId: ThreadId.make("thread-missing-project"), - projectId: asProjectId("project-missing"), - title: "Missing Project Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - }), - ), - ).rejects.toThrow("does not exist"); - - const snapshots = await system.run(Metric.snapshot); - expect( - hasMetricSnapshot(snapshots, "t3_orchestration_commands_total", { - commandType: "thread.create", - aggregateKind: "thread", - outcome: "failure", - }), - ).toBe(true); - - await system.dispose(); - }); - - it("stores completed checkpoint summaries even when no files changed", async () => { - const system = await createOrchestrationSystem(); - const { engine } = system; - const createdAt = now(); - - await system.run( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-turn-diff-create"), - projectId: asProjectId("project-turn-diff"), - title: "Turn Diff Project", - workspaceRoot: "/tmp/project-turn-diff", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - await system.run( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-turn-diff-create"), - threadId: ThreadId.make("thread-turn-diff"), - projectId: asProjectId("project-turn-diff"), - title: "Turn diff thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await system.run( - engine.dispatch({ - type: "thread.turn.diff.complete", - commandId: CommandId.make("cmd-turn-diff-complete"), - threadId: ThreadId.make("thread-turn-diff"), - turnId: asTurnId("turn-1"), - completedAt: createdAt, - checkpointRef: asCheckpointRef("refs/t3/checkpoints/thread-turn-diff/turn/1"), - status: "ready", - files: [], - checkpointTurnCount: 1, - createdAt, - }), - ); - - const thread = (await system.readModel()).threads.find( - (entry) => entry.id === "thread-turn-diff", - ); - expect(thread?.checkpoints).toEqual([ - { - turnId: asTurnId("turn-1"), - checkpointTurnCount: 1, - checkpointRef: asCheckpointRef("refs/t3/checkpoints/thread-turn-diff/turn/1"), - status: "ready", - files: [], - assistantMessageId: null, - completedAt: createdAt, - }, - ]); - await system.dispose(); - }); - - it("keeps processing queued commands after a storage failure", async () => { - type StoredEvent = - ReturnType extends Effect.Effect - ? A - : never; - const events: StoredEvent[] = []; - let nextSequence = 1; - let shouldFailFirstAppend = true; - - const flakyStore: OrchestrationEventStoreShape = { - append(event) { - if (shouldFailFirstAppend && event.commandId === CommandId.make("cmd-flaky-1")) { - shouldFailFirstAppend = false; - return Effect.fail( - new PersistenceSqlError({ - operation: "test.append", - detail: "append failed", - }), - ); - } - const savedEvent = { - ...event, - sequence: nextSequence, - } as StoredEvent; - nextSequence += 1; - events.push(savedEvent); - return Effect.succeed(savedEvent); - }, - readFromSequence(sequenceExclusive) { - return Stream.fromIterable(events.filter((event) => event.sequence > sequenceExclusive)); - }, - readAll() { - return Stream.fromIterable(events); - }, - }; - - const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { - prefix: "t3-orchestration-engine-test-", - }); - - const runtime = ManagedRuntime.make( - OrchestrationEngineLive.pipe( - Layer.provide(OrchestrationProjectionSnapshotQueryLive), - Layer.provide(OrchestrationProjectionPipelineLive), - Layer.provide(Layer.succeed(OrchestrationEventStore, flakyStore)), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - Layer.provideMerge(ServerConfigLayer), - Layer.provideMerge(NodeServices.layer), - ), - ); - const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); - const createdAt = now(); - - await runtime.runPromise( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-flaky-create"), - projectId: asProjectId("project-flaky"), - title: "Flaky Project", - workspaceRoot: "/tmp/project-flaky", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - - await expect( - runtime.runPromise( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-flaky-1"), - threadId: ThreadId.make("thread-flaky-fail"), - projectId: asProjectId("project-flaky"), - title: "flaky-fail", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ), - ).rejects.toThrow("append failed"); - - const result = await runtime.runPromise( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-flaky-2"), - threadId: ThreadId.make("thread-flaky-ok"), - projectId: asProjectId("project-flaky"), - title: "flaky-ok", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - - expect(result.sequence).toBe(2); - const eventsAfterRetry = await runtime.runPromise( - Stream.runCollect(engine.readEvents(0)).pipe( - Effect.map((chunk): OrchestrationEvent[] => Array.from(chunk)), - ), - ); - expect(eventsAfterRetry.map((event) => event.type)).toEqual([ - "project.created", - "thread.created", - ]); - await runtime.dispose(); - }); - - it("rolls back all events for a multi-event command when projection fails mid-dispatch", async () => { - let shouldFailRequestedProjection = true; - const flakyProjectionPipeline: OrchestrationProjectionPipelineShape = { - bootstrap: Effect.void, - projectEvent: (event) => { - if ( - shouldFailRequestedProjection && - event.commandId === CommandId.make("cmd-turn-start-atomic") && - event.type === "thread.turn-start-requested" - ) { - shouldFailRequestedProjection = false; - return Effect.fail( - new PersistenceSqlError({ - operation: "test.projection", - detail: "projection failed", - }), - ); - } - return Effect.void; - }, - }; - - const runtime = ManagedRuntime.make( - OrchestrationEngineLive.pipe( - Layer.provide(OrchestrationProjectionSnapshotQueryLive), - Layer.provide(Layer.succeed(OrchestrationProjectionPipeline, flakyProjectionPipeline)), - Layer.provide(OrchestrationEventStoreLive), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - Layer.provide(NodeServices.layer), - ), - ); - const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); - const createdAt = now(); - - await runtime.runPromise( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-atomic-create"), - projectId: asProjectId("project-atomic"), - title: "Atomic Project", - workspaceRoot: "/tmp/project-atomic", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - await runtime.runPromise( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-atomic-create"), - threadId: ThreadId.make("thread-atomic"), - projectId: asProjectId("project-atomic"), - title: "atomic", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - - const turnStartCommand = { - type: "thread.turn.start" as const, - commandId: CommandId.make("cmd-turn-start-atomic"), - threadId: ThreadId.make("thread-atomic"), - message: { - messageId: asMessageId("msg-atomic-1"), - role: "user" as const, - text: "hello", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required" as const, - createdAt, - }; - - await expect(runtime.runPromise(engine.dispatch(turnStartCommand))).rejects.toThrow( - "projection failed", - ); - - const eventsAfterFailure = await runtime.runPromise( - Stream.runCollect(engine.readEvents(0)).pipe( - Effect.map((chunk): OrchestrationEvent[] => Array.from(chunk)), - ), - ); - expect(eventsAfterFailure.map((event) => event.type)).toEqual([ - "project.created", - "thread.created", - ]); - - const retryResult = await runtime.runPromise(engine.dispatch(turnStartCommand)); - expect(retryResult.sequence).toBe(4); - - const eventsAfterRetry = await runtime.runPromise( - Stream.runCollect(engine.readEvents(0)).pipe( - Effect.map((chunk): OrchestrationEvent[] => Array.from(chunk)), - ), - ); - expect(eventsAfterRetry.map((event) => event.type)).toEqual([ - "project.created", - "thread.created", - "thread.message-sent", - "thread.turn-start-requested", - ]); - expect( - eventsAfterRetry.filter((event) => event.commandId === turnStartCommand.commandId), - ).toHaveLength(2); - - await runtime.dispose(); - }); - - it("reconciles command state when append persists but projection fails", async () => { - type StoredEvent = - ReturnType extends Effect.Effect - ? A - : never; - const events: StoredEvent[] = []; - let nextSequence = 1; - - const nonTransactionalStore: OrchestrationEventStoreShape = { - append(event) { - const savedEvent = { - ...event, - sequence: nextSequence, - } as StoredEvent; - nextSequence += 1; - events.push(savedEvent); - return Effect.succeed(savedEvent); - }, - readFromSequence(sequenceExclusive) { - return Stream.fromIterable(events.filter((event) => event.sequence > sequenceExclusive)); - }, - readAll() { - return Stream.fromIterable(events); - }, - }; - - let shouldFailProjection = true; - const flakyProjectionPipeline: OrchestrationProjectionPipelineShape = { - bootstrap: Effect.void, - projectEvent: (event) => { - if ( - shouldFailProjection && - event.commandId === CommandId.make("cmd-thread-archive-sync-fail") - ) { - shouldFailProjection = false; - return Effect.fail( - new PersistenceSqlError({ - operation: "test.projection", - detail: "projection failed", - }), - ); - } - return Effect.void; - }, - }; - - const runtime = ManagedRuntime.make( - OrchestrationEngineLive.pipe( - Layer.provide(OrchestrationProjectionSnapshotQueryLive), - Layer.provide(Layer.succeed(OrchestrationProjectionPipeline, flakyProjectionPipeline)), - Layer.provide(Layer.succeed(OrchestrationEventStore, nonTransactionalStore)), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - Layer.provide(NodeServices.layer), - ), - ); - const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); - const createdAt = now(); - - await runtime.runPromise( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-sync-create"), - projectId: asProjectId("project-sync"), - title: "Sync Project", - workspaceRoot: "/tmp/project-sync", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - await runtime.runPromise( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-sync-create"), - threadId: ThreadId.make("thread-sync"), - projectId: asProjectId("project-sync"), - title: "sync-before", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - - await expect( - runtime.runPromise( - engine.dispatch({ - type: "thread.archive", - commandId: CommandId.make("cmd-thread-archive-sync-fail"), - threadId: ThreadId.make("thread-sync"), - }), - ), - ).rejects.toThrow("projection failed"); - - await expect( - runtime.runPromise( - engine.dispatch({ - type: "thread.archive", - commandId: CommandId.make("cmd-thread-archive-sync-retry"), - threadId: ThreadId.make("thread-sync"), - }), - ), - ).rejects.toThrow("already archived"); - - await runtime.dispose(); - }); - - it("fails command dispatch when command invariants are violated", async () => { - const system = await createOrchestrationSystem(); - const { engine } = system; - - await expect( - system.run( - engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-invariant-missing-thread"), - threadId: ThreadId.make("thread-missing"), - message: { - messageId: asMessageId("msg-missing"), - role: "user", - text: "hello", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now(), - }), - ), - ).rejects.toThrow("Thread 'thread-missing' does not exist"); - - await system.dispose(); - }); - - it("rejects duplicate thread creation", async () => { - const system = await createOrchestrationSystem(); - const { engine } = system; - const createdAt = now(); - - await system.run( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-duplicate-create"), - projectId: asProjectId("project-duplicate"), - title: "Duplicate Project", - workspaceRoot: "/tmp/project-duplicate", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - - await system.run( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-duplicate-1"), - threadId: ThreadId.make("thread-duplicate"), - projectId: asProjectId("project-duplicate"), - title: "duplicate", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - - await expect( - system.run( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-duplicate-2"), - threadId: ThreadId.make("thread-duplicate"), - projectId: asProjectId("project-duplicate"), - title: "duplicate", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ), - ).rejects.toThrow("already exists"); - - await system.dispose(); - }); -}); diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts index 7277663e948..36bc428f2d3 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts @@ -2,9 +2,8 @@ import type { OrchestrationEvent, OrchestrationReadModel, ProjectId, - ThreadId, + ProjectOrchestrationCommand, } from "@t3tools/contracts"; -import { OrchestrationCommand } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Crypto from "effect/Crypto"; @@ -51,29 +50,16 @@ const isOrchestrationCommandPreviouslyRejectedError = Schema.is( const isOrchestrationCommandInvariantError = Schema.is(OrchestrationCommandInvariantError); interface CommandEnvelope { - command: OrchestrationCommand; + command: ProjectOrchestrationCommand; result: Deferred.Deferred<{ sequence: number }, OrchestrationDispatchError>; startedAtMs: number; } -function commandToAggregateRef(command: OrchestrationCommand): { - readonly aggregateKind: "project" | "thread"; - readonly aggregateId: ProjectId | ThreadId; +function commandToAggregateRef(command: ProjectOrchestrationCommand): { + readonly aggregateKind: "project"; + readonly aggregateId: ProjectId; } { - switch (command.type) { - case "project.create": - case "project.meta.update": - case "project.delete": - return { - aggregateKind: "project", - aggregateId: command.projectId, - }; - default: - return { - aggregateKind: "thread", - aggregateId: command.threadId, - }; - } + return { aggregateKind: "project", aggregateId: command.projectId }; } const makeOrchestrationEngine = Effect.gen(function* () { @@ -120,6 +106,15 @@ const makeOrchestrationEngine = Effect.gen(function* () { commandReadModel = yield* projectEventsOntoReadModel(commandReadModel, persistedEvents); + yield* eventStore.publishCommitted( + persistedEvents.filter( + (event) => + event.type === "project.created" || + event.type === "project.meta-updated" || + event.type === "project.deleted", + ), + ); + for (const persistedEvent of persistedEvents) { yield* PubSub.publish(eventPubSub, persistedEvent); } @@ -191,6 +186,7 @@ const makeOrchestrationEngine = Effect.gen(function* () { commandId: envelope.command.commandId, aggregateKind: lastSavedEvent.aggregateKind, aggregateId: lastSavedEvent.aggregateId, + commandType: envelope.command.type, acceptedAt: lastSavedEvent.occurredAt, resultSequence: lastSavedEvent.sequence, status: "accepted", @@ -213,6 +209,14 @@ const makeOrchestrationEngine = Effect.gen(function* () { ); commandReadModel = committedCommand.nextCommandReadModel; + yield* eventStore.publishCommitted( + committedCommand.committedEvents.filter( + (event) => + event.type === "project.created" || + event.type === "project.meta-updated" || + event.type === "project.deleted", + ), + ); for (const [index, event] of committedCommand.committedEvents.entries()) { yield* PubSub.publish(eventPubSub, event); if (index === 0) { @@ -282,6 +286,7 @@ const makeOrchestrationEngine = Effect.gen(function* () { commandId: envelope.command.commandId, aggregateKind: aggregateRef.aggregateKind, aggregateId: aggregateRef.aggregateId, + commandType: envelope.command.type, acceptedAt: yield* nowIso, resultSequence: commandReadModel.snapshotSequence, status: "rejected", diff --git a/apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts b/apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts deleted file mode 100644 index 300d1526bb9..00000000000 --- a/apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Layer from "effect/Layer"; -import * as ManagedRuntime from "effect/ManagedRuntime"; -import * as Scope from "effect/Scope"; -import { afterEach, describe, expect, it } from "vite-plus/test"; - -import { CheckpointReactor } from "../Services/CheckpointReactor.ts"; -import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts"; -import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts"; -import { ThreadDeletionReactor } from "../Services/ThreadDeletionReactor.ts"; -import { OrchestrationReactor } from "../Services/OrchestrationReactor.ts"; -import { makeOrchestrationReactor } from "./OrchestrationReactor.ts"; -import * as AgentAwarenessRelay from "../../relay/AgentAwarenessRelay.ts"; - -describe("OrchestrationReactor", () => { - let runtime: ManagedRuntime.ManagedRuntime | null = null; - - afterEach(async () => { - if (runtime) { - await runtime.dispose(); - } - runtime = null; - }); - - it("starts provider ingestion, provider command, checkpoint, and thread deletion reactors", async () => { - const started: string[] = []; - - runtime = ManagedRuntime.make( - Layer.effect(OrchestrationReactor, makeOrchestrationReactor).pipe( - Layer.provideMerge( - Layer.succeed(ProviderRuntimeIngestionService, { - start: () => { - started.push("provider-runtime-ingestion"); - return Effect.void; - }, - drain: Effect.void, - }), - ), - Layer.provideMerge( - Layer.succeed(ProviderCommandReactor, { - start: () => { - started.push("provider-command-reactor"); - return Effect.void; - }, - drain: Effect.void, - }), - ), - Layer.provideMerge( - Layer.succeed(CheckpointReactor, { - start: () => { - started.push("checkpoint-reactor"); - return Effect.void; - }, - drain: Effect.void, - }), - ), - Layer.provideMerge( - Layer.succeed(ThreadDeletionReactor, { - start: () => { - started.push("thread-deletion-reactor"); - return Effect.void; - }, - drain: Effect.void, - }), - ), - Layer.provideMerge( - Layer.succeed(AgentAwarenessRelay.AgentAwarenessRelay, { - publishThread: () => Effect.void, - start: () => { - started.push("agent-awareness-relay"); - return Effect.void; - }, - }), - ), - ), - ); - - const reactor = await runtime!.runPromise(Effect.service(OrchestrationReactor)); - const scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reactor.start().pipe(Scope.provide(scope))); - - expect(started).toEqual([ - "provider-runtime-ingestion", - "provider-command-reactor", - "checkpoint-reactor", - "thread-deletion-reactor", - "agent-awareness-relay", - ]); - - await Effect.runPromise(Scope.close(scope, Exit.void)); - }); -}); diff --git a/apps/server/src/orchestration/Layers/OrchestrationReactor.ts b/apps/server/src/orchestration/Layers/OrchestrationReactor.ts deleted file mode 100644 index fb7543e31af..00000000000 --- a/apps/server/src/orchestration/Layers/OrchestrationReactor.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; - -import { - OrchestrationReactor, - type OrchestrationReactorShape, -} from "../Services/OrchestrationReactor.ts"; -import { CheckpointReactor } from "../Services/CheckpointReactor.ts"; -import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts"; -import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts"; -import { ThreadDeletionReactor } from "../Services/ThreadDeletionReactor.ts"; -import * as AgentAwarenessRelay from "../../relay/AgentAwarenessRelay.ts"; - -export const makeOrchestrationReactor = Effect.gen(function* () { - const providerRuntimeIngestion = yield* ProviderRuntimeIngestionService; - const providerCommandReactor = yield* ProviderCommandReactor; - const checkpointReactor = yield* CheckpointReactor; - const threadDeletionReactor = yield* ThreadDeletionReactor; - const agentAwarenessRelay = yield* AgentAwarenessRelay.AgentAwarenessRelay; - - const start: OrchestrationReactorShape["start"] = Effect.fn("start")(function* () { - yield* providerRuntimeIngestion.start(); - yield* providerCommandReactor.start(); - yield* checkpointReactor.start(); - yield* threadDeletionReactor.start(); - yield* agentAwarenessRelay.start(); - }); - - return { - start, - } satisfies OrchestrationReactorShape; -}); - -export const OrchestrationReactorLive = Layer.effect( - OrchestrationReactor, - makeOrchestrationReactor, -); diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts deleted file mode 100644 index 0999000ed4f..00000000000 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts +++ /dev/null @@ -1,2645 +0,0 @@ -import { - CheckpointRef, - CommandId, - CorrelationId, - EventId, - MessageId, - ProjectId, - ThreadId, - TurnId, - ProviderInstanceId, -} from "@t3tools/contracts"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { assert, it } from "@effect/vitest"; -import * as Effect from "effect/Effect"; -import * as FileSystem from "effect/FileSystem"; -import * as Layer from "effect/Layer"; -import * as Path from "effect/Path"; -import * as SqlClient from "effect/unstable/sql/SqlClient"; - -import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts"; -import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts"; -import { - makeSqlitePersistenceLive, - SqlitePersistenceMemory, -} from "../../persistence/Layers/Sqlite.ts"; -import { OrchestrationEventStore } from "../../persistence/Services/OrchestrationEventStore.ts"; -import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; -import { OrchestrationEngineLive } from "./OrchestrationEngine.ts"; -import { - ORCHESTRATION_PROJECTOR_NAMES, - OrchestrationProjectionPipelineLive, -} from "./ProjectionPipeline.ts"; -import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts"; -import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; -import { OrchestrationProjectionPipeline } from "../Services/ProjectionPipeline.ts"; -import { ServerConfig } from "../../config.ts"; - -const makeProjectionPipelinePrefixedTestLayer = (prefix: string) => - OrchestrationProjectionPipelineLive.pipe( - Layer.provideMerge(OrchestrationEventStoreLive), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix })), - Layer.provideMerge(SqlitePersistenceMemory), - Layer.provideMerge(NodeServices.layer), - ); - -const exists = (filePath: string) => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const fileInfo = yield* Effect.result(fileSystem.stat(filePath)); - return fileInfo._tag === "Success"; - }); - -const BaseTestLayer = makeProjectionPipelinePrefixedTestLayer("t3-projection-pipeline-test-"); - -it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => { - it.effect("bootstraps all projection states and writes projection rows", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - - yield* eventStore.append({ - type: "project.created", - eventId: EventId.make("evt-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-1"), - occurredAt: now, - commandId: CommandId.make("cmd-1"), - causationEventId: null, - correlationId: CommandId.make("cmd-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-1"), - title: "Project 1", - workspaceRoot: "/tmp/project-1", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.created", - eventId: EventId.make("evt-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), - occurredAt: now, - commandId: CommandId.make("cmd-2"), - causationEventId: null, - correlationId: CommandId.make("cmd-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-1"), - projectId: ProjectId.make("project-1"), - title: "Thread 1", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), - occurredAt: now, - commandId: CommandId.make("cmd-3"), - causationEventId: null, - correlationId: CommandId.make("cmd-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-1"), - messageId: MessageId.make("message-1"), - role: "assistant", - text: "hello", - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - yield* projectionPipeline.bootstrap; - - const projectRows = yield* sql<{ - readonly projectId: string; - readonly title: string; - readonly scriptsJson: string; - }>` - SELECT - project_id AS "projectId", - title, - scripts_json AS "scriptsJson" - FROM projection_projects - `; - assert.deepEqual(projectRows, [ - { projectId: "project-1", title: "Project 1", scriptsJson: "[]" }, - ]); - - const messageRows = yield* sql<{ - readonly messageId: string; - readonly text: string; - }>` - SELECT - message_id AS "messageId", - text - FROM projection_thread_messages - `; - assert.deepEqual(messageRows, [{ messageId: "message-1", text: "hello" }]); - - const stateRows = yield* sql<{ - readonly projector: string; - readonly lastAppliedSequence: number; - }>` - SELECT - projector, - last_applied_sequence AS "lastAppliedSequence" - FROM projection_state - ORDER BY projector ASC - `; - assert.equal(stateRows.length, Object.keys(ORCHESTRATION_PROJECTOR_NAMES).length); - for (const row of stateRows) { - assert.equal(row.lastAppliedSequence, 3); - } - }), - ); -}); - -it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-base-")))( - "OrchestrationProjectionPipeline", - (it) => { - it.effect("stores message attachment references without mutating payloads", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-attachments"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-attachments"), - occurredAt: now, - commandId: CommandId.make("cmd-attachments"), - causationEventId: null, - correlationId: CommandId.make("cmd-attachments"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-attachments"), - messageId: MessageId.make("message-attachments"), - role: "user", - text: "Inspect this", - attachments: [ - { - type: "image", - id: "thread-attachments-att-1", - name: "example.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - yield* projectionPipeline.bootstrap; - - const rows = yield* sql<{ - readonly attachmentsJson: string | null; - }>` - SELECT - attachments_json AS "attachmentsJson" - FROM projection_thread_messages - WHERE message_id = 'message-attachments' - `; - assert.equal(rows.length, 1); - // @effect-diagnostics-next-line preferSchemaOverJson:off - assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [ - { - type: "image", - id: "thread-attachments-att-1", - name: "example.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ]); - }), - ); - }, -); - -it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-attachments-safe-")))( - "OrchestrationProjectionPipeline", - (it) => { - it.effect("preserves mixed image attachment metadata as-is", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-attachments-safe"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-attachments-safe"), - occurredAt: now, - commandId: CommandId.make("cmd-attachments-safe"), - causationEventId: null, - correlationId: CommandId.make("cmd-attachments-safe"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-attachments-safe"), - messageId: MessageId.make("message-attachments-safe"), - role: "user", - text: "Inspect this", - attachments: [ - { - type: "image", - id: "thread-attachments-safe-att-1", - name: "untrusted.exe", - mimeType: "image/x-unknown", - sizeBytes: 5, - }, - { - type: "image", - id: "thread-attachments-safe-att-2", - name: "not-image.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - yield* projectionPipeline.bootstrap; - - const rows = yield* sql<{ - readonly attachmentsJson: string | null; - }>` - SELECT - attachments_json AS "attachmentsJson" - FROM projection_thread_messages - WHERE message_id = 'message-attachments-safe' - `; - assert.equal(rows.length, 1); - // @effect-diagnostics-next-line preferSchemaOverJson:off - assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [ - { - type: "image", - id: "thread-attachments-safe-att-1", - name: "untrusted.exe", - mimeType: "image/x-unknown", - sizeBytes: 5, - }, - { - type: "image", - id: "thread-attachments-safe-att-2", - name: "not-image.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ]); - }), - ); - }, -); - -it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => { - it.effect( - "passes explicit empty attachment arrays through the projection pipeline to clear attachments", - () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - const later = "2026-01-01T00:00:01.000Z"; - - yield* eventStore.append({ - type: "project.created", - eventId: EventId.make("evt-clear-attachments-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-clear-attachments"), - occurredAt: now, - commandId: CommandId.make("cmd-clear-attachments-1"), - causationEventId: null, - correlationId: CommandId.make("cmd-clear-attachments-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-clear-attachments"), - title: "Project Clear Attachments", - workspaceRoot: "/tmp/project-clear-attachments", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.created", - eventId: EventId.make("evt-clear-attachments-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-clear-attachments"), - occurredAt: now, - commandId: CommandId.make("cmd-clear-attachments-2"), - causationEventId: null, - correlationId: CommandId.make("cmd-clear-attachments-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-clear-attachments"), - projectId: ProjectId.make("project-clear-attachments"), - title: "Thread Clear Attachments", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-clear-attachments-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-clear-attachments"), - occurredAt: now, - commandId: CommandId.make("cmd-clear-attachments-3"), - causationEventId: null, - correlationId: CommandId.make("cmd-clear-attachments-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-clear-attachments"), - messageId: MessageId.make("message-clear-attachments"), - role: "user", - text: "Has attachments", - attachments: [ - { - type: "image", - id: "thread-clear-attachments-att-1", - name: "clear.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-clear-attachments-4"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-clear-attachments"), - occurredAt: later, - commandId: CommandId.make("cmd-clear-attachments-4"), - causationEventId: null, - correlationId: CommandId.make("cmd-clear-attachments-4"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-clear-attachments"), - messageId: MessageId.make("message-clear-attachments"), - role: "user", - text: "", - attachments: [], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: later, - }, - }); - - yield* projectionPipeline.bootstrap; - - const rows = yield* sql<{ - readonly attachmentsJson: string | null; - }>` - SELECT - attachments_json AS "attachmentsJson" - FROM projection_thread_messages - WHERE message_id = 'message-clear-attachments' - `; - assert.equal(rows.length, 1); - // @effect-diagnostics-next-line preferSchemaOverJson:off - assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), []); - }), - ); -}); - -it.layer( - Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-attachments-overwrite-")), -)("OrchestrationProjectionPipeline", (it) => { - it.effect("overwrites stored attachment references when a message updates attachments", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - const later = "2026-01-01T00:00:01.000Z"; - - yield* eventStore.append({ - type: "project.created", - eventId: EventId.make("evt-overwrite-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-overwrite"), - occurredAt: now, - commandId: CommandId.make("cmd-overwrite-1"), - causationEventId: null, - correlationId: CommandId.make("cmd-overwrite-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-overwrite"), - title: "Project Overwrite", - workspaceRoot: "/tmp/project-overwrite", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.created", - eventId: EventId.make("evt-overwrite-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-overwrite"), - occurredAt: now, - commandId: CommandId.make("cmd-overwrite-2"), - causationEventId: null, - correlationId: CommandId.make("cmd-overwrite-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-overwrite"), - projectId: ProjectId.make("project-overwrite"), - title: "Thread Overwrite", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-overwrite-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-overwrite"), - occurredAt: now, - commandId: CommandId.make("cmd-overwrite-3"), - causationEventId: null, - correlationId: CommandId.make("cmd-overwrite-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-overwrite"), - messageId: MessageId.make("message-overwrite"), - role: "user", - text: "first image", - attachments: [ - { - type: "image", - id: "thread-overwrite-att-1", - name: "file.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-overwrite-4"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-overwrite"), - occurredAt: later, - commandId: CommandId.make("cmd-overwrite-4"), - causationEventId: null, - correlationId: CommandId.make("cmd-overwrite-4"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-overwrite"), - messageId: MessageId.make("message-overwrite"), - role: "user", - text: "", - attachments: [ - { - type: "image", - id: "thread-overwrite-att-2", - name: "file.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: later, - }, - }); - - yield* projectionPipeline.bootstrap; - - const rows = yield* sql<{ - readonly attachmentsJson: string | null; - }>` - SELECT attachments_json AS "attachmentsJson" - FROM projection_thread_messages - WHERE message_id = 'message-overwrite' - `; - assert.equal(rows.length, 1); - // @effect-diagnostics-next-line preferSchemaOverJson:off - assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [ - { - type: "image", - id: "thread-overwrite-att-2", - name: "file.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ]); - }), - ); -}); - -it.layer( - Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-attachments-rollback-")), -)("OrchestrationProjectionPipeline", (it) => { - it.effect("does not persist attachment files when projector transaction rolls back", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const path = yield* Path.Path; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - - const appendAndProject = (event: Parameters[0]) => - eventStore - .append(event) - .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); - - yield* appendAndProject({ - type: "project.created", - eventId: EventId.make("evt-rollback-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-rollback"), - occurredAt: now, - commandId: CommandId.make("cmd-rollback-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-rollback-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-rollback"), - title: "Project Rollback", - workspaceRoot: "/tmp/project-rollback", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.make("evt-rollback-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-rollback"), - occurredAt: now, - commandId: CommandId.make("cmd-rollback-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-rollback-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-rollback"), - projectId: ProjectId.make("project-rollback"), - title: "Thread Rollback", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - yield* sql` - CREATE TRIGGER fail_thread_messages_projection_state_update - BEFORE UPDATE ON projection_state - WHEN NEW.projector = 'projection.thread-messages' - BEGIN - SELECT RAISE(ABORT, 'forced-projection-state-failure'); - END; - `; - - const result = yield* Effect.result( - appendAndProject({ - type: "thread.message-sent", - eventId: EventId.make("evt-rollback-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-rollback"), - occurredAt: now, - commandId: CommandId.make("cmd-rollback-3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-rollback-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-rollback"), - messageId: MessageId.make("message-rollback"), - role: "user", - text: "Rollback me", - attachments: [ - { - type: "image", - id: "thread-rollback-att-1", - name: "rollback.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }), - ); - assert.equal(result._tag, "Failure"); - - const rows = yield* sql<{ - readonly count: number; - }>` - SELECT COUNT(*) AS "count" - FROM projection_thread_messages - WHERE message_id = 'message-rollback' - `; - assert.equal(rows[0]?.count ?? 0, 0); - - const { attachmentsDir } = yield* ServerConfig; - const attachmentPath = path.join(attachmentsDir, "thread-rollback-att-1.png"); - assert.isFalse(yield* exists(attachmentPath)); - yield* sql`DROP TRIGGER IF EXISTS fail_thread_messages_projection_state_update`; - }), - ); -}); - -it.layer( - Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-attachments-overwrite-")), -)("OrchestrationProjectionPipeline", (it) => { - it.effect("removes unreferenced attachment files when a thread is reverted", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const { attachmentsDir } = yield* ServerConfig; - const now = "2026-01-01T00:00:00.000Z"; - const threadId = ThreadId.make("Thread Revert.Files"); - const keepAttachmentId = "thread-revert-files-00000000-0000-4000-8000-000000000001"; - const removeAttachmentId = "thread-revert-files-00000000-0000-4000-8000-000000000002"; - const otherThreadAttachmentId = - "thread-revert-files-extra-00000000-0000-4000-8000-000000000003"; - - const appendAndProject = (event: Parameters[0]) => - eventStore - .append(event) - .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); - - yield* appendAndProject({ - type: "project.created", - eventId: EventId.make("evt-revert-files-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-revert-files"), - occurredAt: now, - commandId: CommandId.make("cmd-revert-files-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-files-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-revert-files"), - title: "Project Revert Files", - workspaceRoot: "/tmp/project-revert-files", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.make("evt-revert-files-2"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-revert-files-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-files-2"), - metadata: {}, - payload: { - threadId, - projectId: ProjectId.make("project-revert-files"), - title: "Thread Revert Files", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - yield* appendAndProject({ - type: "thread.turn-diff-completed", - eventId: EventId.make("evt-revert-files-3"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-revert-files-3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-files-3"), - metadata: {}, - payload: { - threadId, - turnId: TurnId.make("turn-keep"), - checkpointTurnCount: 1, - checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread-revert-files/turn/1"), - status: "ready", - files: [], - assistantMessageId: MessageId.make("message-keep"), - completedAt: now, - }, - }); - - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.make("evt-revert-files-4"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-revert-files-4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-files-4"), - metadata: {}, - payload: { - threadId, - messageId: MessageId.make("message-keep"), - role: "assistant", - text: "Keep", - attachments: [ - { - type: "image", - id: keepAttachmentId, - name: "keep.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: TurnId.make("turn-keep"), - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - yield* appendAndProject({ - type: "thread.turn-diff-completed", - eventId: EventId.make("evt-revert-files-5"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-revert-files-5"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-files-5"), - metadata: {}, - payload: { - threadId, - turnId: TurnId.make("turn-remove"), - checkpointTurnCount: 2, - checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread-revert-files/turn/2"), - status: "ready", - files: [], - assistantMessageId: MessageId.make("message-remove"), - completedAt: now, - }, - }); - - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.make("evt-revert-files-6"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-revert-files-6"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-files-6"), - metadata: {}, - payload: { - threadId, - messageId: MessageId.make("message-remove"), - role: "assistant", - text: "Remove", - attachments: [ - { - type: "image", - id: removeAttachmentId, - name: "remove.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: TurnId.make("turn-remove"), - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - const keepPath = path.join(attachmentsDir, `${keepAttachmentId}.png`); - const removePath = path.join(attachmentsDir, `${removeAttachmentId}.png`); - yield* fileSystem.makeDirectory(attachmentsDir, { recursive: true }); - yield* fileSystem.writeFileString(keepPath, "keep"); - yield* fileSystem.writeFileString(removePath, "remove"); - const otherThreadPath = path.join(attachmentsDir, `${otherThreadAttachmentId}.png`); - yield* fileSystem.writeFileString(otherThreadPath, "other"); - assert.isTrue(yield* exists(keepPath)); - assert.isTrue(yield* exists(removePath)); - assert.isTrue(yield* exists(otherThreadPath)); - - yield* appendAndProject({ - type: "thread.reverted", - eventId: EventId.make("evt-revert-files-7"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-revert-files-7"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-files-7"), - metadata: {}, - payload: { - threadId, - turnCount: 1, - }, - }); - - assert.isTrue(yield* exists(keepPath)); - assert.isFalse(yield* exists(removePath)); - assert.isTrue(yield* exists(otherThreadPath)); - }), - ); -}); - -it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-attachments-revert-")))( - "OrchestrationProjectionPipeline", - (it) => { - it.effect("removes thread attachment directory when thread is deleted", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const { attachmentsDir } = yield* ServerConfig; - const now = "2026-01-01T00:00:00.000Z"; - const threadId = ThreadId.make("Thread Delete.Files"); - const attachmentId = "thread-delete-files-00000000-0000-4000-8000-000000000001"; - const otherThreadAttachmentId = - "thread-delete-files-extra-00000000-0000-4000-8000-000000000002"; - - const appendAndProject = (event: Parameters[0]) => - eventStore - .append(event) - .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); - - yield* appendAndProject({ - type: "project.created", - eventId: EventId.make("evt-delete-files-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-delete-files"), - occurredAt: now, - commandId: CommandId.make("cmd-delete-files-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-delete-files-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-delete-files"), - title: "Project Delete Files", - workspaceRoot: "/tmp/project-delete-files", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.make("evt-delete-files-2"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-delete-files-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-delete-files-2"), - metadata: {}, - payload: { - threadId, - projectId: ProjectId.make("project-delete-files"), - title: "Thread Delete Files", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.make("evt-delete-files-3"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-delete-files-3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-delete-files-3"), - metadata: {}, - payload: { - threadId, - messageId: MessageId.make("message-delete-files"), - role: "user", - text: "Delete", - attachments: [ - { - type: "image", - id: attachmentId, - name: "delete.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - const threadAttachmentPath = path.join(attachmentsDir, `${attachmentId}.png`); - const otherThreadAttachmentPath = path.join( - attachmentsDir, - `${otherThreadAttachmentId}.png`, - ); - yield* fileSystem.makeDirectory(attachmentsDir, { recursive: true }); - yield* fileSystem.writeFileString(threadAttachmentPath, "delete"); - yield* fileSystem.writeFileString(otherThreadAttachmentPath, "other-thread"); - assert.isTrue(yield* exists(threadAttachmentPath)); - assert.isTrue(yield* exists(otherThreadAttachmentPath)); - - yield* appendAndProject({ - type: "thread.deleted", - eventId: EventId.make("evt-delete-files-4"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-delete-files-4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-delete-files-4"), - metadata: {}, - payload: { - threadId, - deletedAt: now, - }, - }); - - assert.isFalse(yield* exists(threadAttachmentPath)); - assert.isTrue(yield* exists(otherThreadAttachmentPath)); - }), - ); - }, -); - -it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-attachments-delete-")))( - "OrchestrationProjectionPipeline", - (it) => { - it.effect("ignores unsafe thread ids for attachment cleanup paths", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const now = "2026-01-01T00:00:00.000Z"; - const { attachmentsDir: attachmentsRootDir, stateDir } = yield* ServerConfig; - const attachmentsSentinelPath = path.join(attachmentsRootDir, "sentinel.txt"); - const stateDirSentinelPath = path.join(stateDir, "state-sentinel.txt"); - yield* fileSystem.makeDirectory(attachmentsRootDir, { recursive: true }); - yield* fileSystem.writeFileString(attachmentsSentinelPath, "keep-attachments-root"); - yield* fileSystem.writeFileString(stateDirSentinelPath, "keep-state-dir"); - - yield* eventStore.append({ - type: "thread.deleted", - eventId: EventId.make("evt-unsafe-thread-delete"), - aggregateKind: "thread", - aggregateId: ThreadId.make(".."), - occurredAt: now, - commandId: CommandId.make("cmd-unsafe-thread-delete"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-unsafe-thread-delete"), - metadata: {}, - payload: { - threadId: ThreadId.make(".."), - deletedAt: now, - }, - }); - - yield* projectionPipeline.bootstrap; - - assert.isTrue(yield* exists(attachmentsRootDir)); - assert.isTrue(yield* exists(attachmentsSentinelPath)); - assert.isTrue(yield* exists(stateDirSentinelPath)); - }), - ); - }, -); - -it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => { - it.effect("resumes from projector last_applied_sequence without replaying older events", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - - yield* eventStore.append({ - type: "project.created", - eventId: EventId.make("evt-a1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-a"), - occurredAt: now, - commandId: CommandId.make("cmd-a1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-a1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-a"), - title: "Project A", - workspaceRoot: "/tmp/project-a", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.created", - eventId: EventId.make("evt-a2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-a"), - occurredAt: now, - commandId: CommandId.make("cmd-a2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-a2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-a"), - projectId: ProjectId.make("project-a"), - title: "Thread A", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-a3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-a"), - occurredAt: now, - commandId: CommandId.make("cmd-a3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-a3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-a"), - messageId: MessageId.make("message-a"), - role: "assistant", - text: "hello", - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - yield* projectionPipeline.bootstrap; - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-a4"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-a"), - occurredAt: now, - commandId: CommandId.make("cmd-a4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-a4"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-a"), - messageId: MessageId.make("message-a"), - role: "assistant", - text: " world", - turnId: null, - streaming: true, - createdAt: now, - updatedAt: now, - }, - }); - - yield* projectionPipeline.bootstrap; - yield* projectionPipeline.bootstrap; - - const messageRows = yield* sql<{ readonly text: string }>` - SELECT text FROM projection_thread_messages WHERE message_id = 'message-a' - `; - assert.deepEqual(messageRows, [{ text: "hello world" }]); - - const stateRows = yield* sql<{ - readonly projector: string; - readonly lastAppliedSequence: number; - }>` - SELECT - projector, - last_applied_sequence AS "lastAppliedSequence" - FROM projection_state - `; - const maxSequenceRows = yield* sql<{ readonly maxSequence: number }>` - SELECT MAX(sequence) AS "maxSequence" FROM orchestration_events - `; - const maxSequence = maxSequenceRows[0]?.maxSequence ?? 0; - for (const row of stateRows) { - assert.equal(row.lastAppliedSequence, maxSequence); - } - }), - ); - - it.effect("keeps the turn running across interim assistant messages until the session ends", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - const threadId = ThreadId.make("thread-turn-lifecycle"); - const turnId = TurnId.make("turn-lifecycle-1"); - - yield* eventStore.append({ - type: "thread.created", - eventId: EventId.make("evt-tl1"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-tl1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-tl1"), - metadata: {}, - payload: { - threadId, - projectId: ProjectId.make("project-turn-lifecycle"), - title: "Turn lifecycle", - modelSelection: { - instanceId: ProviderInstanceId.make("claude"), - model: "claude-opus", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.session-set", - eventId: EventId.make("evt-tl2"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: "2026-01-01T00:00:01.000Z", - commandId: CommandId.make("cmd-tl2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-tl2"), - metadata: {}, - payload: { - threadId, - session: { - threadId, - status: "running", - providerName: "claude", - runtimeMode: "full-access", - activeTurnId: turnId, - lastError: null, - updatedAt: "2026-01-01T00:00:01.000Z", - }, - }, - }); - - // Interim assistant message completes mid-turn (commentary between - // tool calls) — the turn must stay running and unsettled. - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-tl3"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: "2026-01-01T00:00:05.000Z", - commandId: CommandId.make("cmd-tl3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-tl3"), - metadata: {}, - payload: { - threadId, - messageId: MessageId.make("message-tl-interim"), - role: "assistant", - text: "interim commentary", - turnId, - streaming: false, - createdAt: "2026-01-01T00:00:05.000Z", - updatedAt: "2026-01-01T00:00:05.000Z", - }, - }); - - yield* projectionPipeline.bootstrap; - - const runningRows = yield* sql<{ - readonly state: string; - readonly completedAt: string | null; - }>` - SELECT state, completed_at AS "completedAt" - FROM projection_turns - WHERE thread_id = ${threadId} AND turn_id = ${turnId} - `; - assert.deepEqual(runningRows, [{ state: "running", completedAt: null }]); - - // The session leaving "running" is the turn-end signal. - yield* eventStore.append({ - type: "thread.session-set", - eventId: EventId.make("evt-tl4"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: "2026-01-01T00:01:00.000Z", - commandId: CommandId.make("cmd-tl4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-tl4"), - metadata: {}, - payload: { - threadId, - session: { - threadId, - status: "ready", - providerName: "claude", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: "2026-01-01T00:01:00.000Z", - }, - }, - }); - - yield* projectionPipeline.bootstrap; - - const settledRows = yield* sql<{ - readonly state: string; - readonly completedAt: string | null; - }>` - SELECT state, completed_at AS "completedAt" - FROM projection_turns - WHERE thread_id = ${threadId} AND turn_id = ${turnId} - `; - assert.deepEqual(settledRows, [ - { state: "completed", completedAt: "2026-01-01T00:01:00.000Z" }, - ]); - }), - ); - - it.effect("settles a superseded running turn when a new turn becomes active", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - const threadId = ThreadId.make("thread-turn-supersede"); - const oldTurnId = TurnId.make("turn-superseded"); - const newTurnId = TurnId.make("turn-steer"); - - yield* eventStore.append({ - type: "thread.created", - eventId: EventId.make("evt-ts1"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.make("cmd-ts1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-ts1"), - metadata: {}, - payload: { - threadId, - projectId: ProjectId.make("project-turn-supersede"), - title: "Turn supersede", - modelSelection: { - instanceId: ProviderInstanceId.make("opencode"), - model: "big-pickle", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - const appendRunningSessionSet = (eventId: string, turnId: TurnId, updatedAt: string) => - eventStore.append({ - type: "thread.session-set", - eventId: EventId.make(eventId), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: updatedAt, - commandId: CommandId.make(`cmd-${eventId}`), - causationEventId: null, - correlationId: CorrelationId.make(`cmd-${eventId}`), - metadata: {}, - payload: { - threadId, - session: { - threadId, - status: "running", - providerName: "opencode", - runtimeMode: "full-access", - activeTurnId: turnId, - lastError: null, - updatedAt, - }, - }, - }); - - yield* appendRunningSessionSet("evt-ts2", oldTurnId, "2026-01-01T00:00:01.000Z"); - // A steer: a new turn becomes active without the provider ever - // completing the previous one. - yield* appendRunningSessionSet("evt-ts3", newTurnId, "2026-01-01T00:00:30.000Z"); - - yield* projectionPipeline.bootstrap; - - const rows = yield* sql<{ - readonly turnId: string; - readonly state: string; - readonly completedAt: string | null; - }>` - SELECT turn_id AS "turnId", state, completed_at AS "completedAt" - FROM projection_turns - WHERE thread_id = ${threadId} - ORDER BY requested_at - `; - assert.deepEqual(rows, [ - { turnId: oldTurnId, state: "completed", completedAt: "2026-01-01T00:00:30.000Z" }, - { turnId: newTurnId, state: "running", completedAt: null }, - ]); - }), - ); - - it.effect("keeps accumulated assistant text when completion payload text is empty", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = "2026-01-01T00:00:00.000Z"; - - yield* eventStore.append({ - type: "project.created", - eventId: EventId.make("evt-empty-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-empty"), - occurredAt: now, - commandId: CommandId.make("cmd-empty-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-empty-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-empty"), - title: "Project Empty", - workspaceRoot: "/tmp/project-empty", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.created", - eventId: EventId.make("evt-empty-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-empty"), - occurredAt: now, - commandId: CommandId.make("cmd-empty-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-empty-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-empty"), - projectId: ProjectId.make("project-empty"), - title: "Thread Empty", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-empty-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-empty"), - occurredAt: now, - commandId: CommandId.make("cmd-empty-3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-empty-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-empty"), - messageId: MessageId.make("assistant-empty"), - role: "assistant", - text: "Hello", - turnId: null, - streaming: true, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-empty-4"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-empty"), - occurredAt: now, - commandId: CommandId.make("cmd-empty-4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-empty-4"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-empty"), - messageId: MessageId.make("assistant-empty"), - role: "assistant", - text: " world", - turnId: null, - streaming: true, - createdAt: now, - updatedAt: now, - }, - }); - - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.make("evt-empty-5"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-empty"), - occurredAt: now, - commandId: CommandId.make("cmd-empty-5"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-empty-5"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-empty"), - messageId: MessageId.make("assistant-empty"), - role: "assistant", - text: "", - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); - - yield* projectionPipeline.bootstrap; - - const messageRows = yield* sql<{ readonly text: string; readonly isStreaming: unknown }>` - SELECT - text, - is_streaming AS "isStreaming" - FROM projection_thread_messages - WHERE message_id = 'assistant-empty' - `; - assert.equal(messageRows.length, 1); - assert.equal(messageRows[0]?.text, "Hello world"); - assert.isFalse(Boolean(messageRows[0]?.isStreaming)); - }), - ); - - it.effect( - "resolves turn-count conflicts when checkpoint completion rewrites provisional turns", - () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const appendAndProject = (event: Parameters[0]) => - eventStore - .append(event) - .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); - - yield* appendAndProject({ - type: "project.created", - eventId: EventId.make("evt-conflict-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-conflict"), - occurredAt: "2026-02-26T13:00:00.000Z", - commandId: CommandId.make("cmd-conflict-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-conflict-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-conflict"), - title: "Project Conflict", - workspaceRoot: "/tmp/project-conflict", - defaultModelSelection: null, - scripts: [], - createdAt: "2026-02-26T13:00:00.000Z", - updatedAt: "2026-02-26T13:00:00.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.make("evt-conflict-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-conflict"), - occurredAt: "2026-02-26T13:00:01.000Z", - commandId: CommandId.make("cmd-conflict-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-conflict-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-conflict"), - projectId: ProjectId.make("project-conflict"), - title: "Thread Conflict", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: "2026-02-26T13:00:01.000Z", - updatedAt: "2026-02-26T13:00:01.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.turn-interrupt-requested", - eventId: EventId.make("evt-conflict-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-conflict"), - occurredAt: "2026-02-26T13:00:02.000Z", - commandId: CommandId.make("cmd-conflict-3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-conflict-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-conflict"), - turnId: TurnId.make("turn-interrupted"), - createdAt: "2026-02-26T13:00:02.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.make("evt-conflict-4"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-conflict"), - occurredAt: "2026-02-26T13:00:03.000Z", - commandId: CommandId.make("cmd-conflict-4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-conflict-4"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-conflict"), - messageId: MessageId.make("assistant-conflict"), - role: "assistant", - text: "done", - turnId: TurnId.make("turn-completed"), - streaming: false, - createdAt: "2026-02-26T13:00:03.000Z", - updatedAt: "2026-02-26T13:00:03.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.turn-diff-completed", - eventId: EventId.make("evt-conflict-5"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-conflict"), - occurredAt: "2026-02-26T13:00:04.000Z", - commandId: CommandId.make("cmd-conflict-5"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-conflict-5"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-conflict"), - turnId: TurnId.make("turn-completed"), - checkpointTurnCount: 1, - checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread-conflict/turn/1"), - status: "ready", - files: [], - assistantMessageId: MessageId.make("assistant-conflict"), - completedAt: "2026-02-26T13:00:04.000Z", - }, - }); - - const turnRows = yield* sql<{ - readonly turnId: string; - readonly checkpointTurnCount: number | null; - readonly status: string; - }>` - SELECT - turn_id AS "turnId", - checkpoint_turn_count AS "checkpointTurnCount", - state AS "status" - FROM projection_turns - WHERE thread_id = 'thread-conflict' - ORDER BY - CASE - WHEN checkpoint_turn_count IS NULL THEN 1 - ELSE 0 - END ASC, - checkpoint_turn_count ASC, - requested_at ASC - `; - assert.deepEqual(turnRows, [ - { turnId: "turn-completed", checkpointTurnCount: 1, status: "completed" }, - { turnId: "turn-interrupted", checkpointTurnCount: null, status: "interrupted" }, - ]); - }), - ); - - it.effect("clears stale pending approvals from projected shell summaries", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const appendAndProject = (event: Parameters[0]) => - eventStore - .append(event) - .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); - - yield* appendAndProject({ - type: "project.created", - eventId: EventId.make("evt-stale-approval-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-stale-approval"), - occurredAt: "2026-02-26T12:30:00.000Z", - commandId: CommandId.make("cmd-stale-approval-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-stale-approval-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-stale-approval"), - title: "Project Stale Approval", - workspaceRoot: "/tmp/project-stale-approval", - defaultModelSelection: null, - scripts: [], - createdAt: "2026-02-26T12:30:00.000Z", - updatedAt: "2026-02-26T12:30:00.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.make("evt-stale-approval-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-stale-approval"), - occurredAt: "2026-02-26T12:30:01.000Z", - commandId: CommandId.make("cmd-stale-approval-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-stale-approval-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-stale-approval"), - projectId: ProjectId.make("project-stale-approval"), - title: "Thread Stale Approval", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "approval-required", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: "2026-02-26T12:30:01.000Z", - updatedAt: "2026-02-26T12:30:01.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.activity-appended", - eventId: EventId.make("evt-stale-approval-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-stale-approval"), - occurredAt: "2026-02-26T12:30:02.000Z", - commandId: CommandId.make("cmd-stale-approval-3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-stale-approval-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-stale-approval"), - activity: { - id: EventId.make("activity-stale-approval-requested"), - tone: "approval", - kind: "approval.requested", - summary: "Command approval requested", - payload: { - requestId: "approval-request-stale-1", - requestKind: "command", - }, - turnId: null, - createdAt: "2026-02-26T12:30:02.000Z", - }, - }, - }); - - yield* appendAndProject({ - type: "thread.activity-appended", - eventId: EventId.make("evt-stale-approval-4"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-stale-approval"), - occurredAt: "2026-02-26T12:30:03.000Z", - commandId: CommandId.make("cmd-stale-approval-4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-stale-approval-4"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-stale-approval"), - activity: { - id: EventId.make("activity-stale-approval-failed"), - tone: "error", - kind: "provider.approval.respond.failed", - summary: "Provider approval response failed", - payload: { - requestId: "approval-request-stale-1", - detail: "Unknown pending permission request: approval-request-stale-1", - }, - turnId: null, - createdAt: "2026-02-26T12:30:03.000Z", - }, - }, - }); - - const approvalRows = yield* sql<{ - readonly requestId: string; - readonly status: string; - readonly resolvedAt: string | null; - }>` - SELECT - request_id AS "requestId", - status, - resolved_at AS "resolvedAt" - FROM projection_pending_approvals - WHERE request_id = 'approval-request-stale-1' - `; - assert.deepEqual(approvalRows, [ - { - requestId: "approval-request-stale-1", - status: "resolved", - resolvedAt: "2026-02-26T12:30:03.000Z", - }, - ]); - - const threadRows = yield* sql<{ - readonly pendingApprovalCount: number; - }>` - SELECT pending_approval_count AS "pendingApprovalCount" - FROM projection_threads - WHERE thread_id = 'thread-stale-approval' - `; - assert.deepEqual(threadRows, [{ pendingApprovalCount: 0 }]); - }), - ); - - it.effect("clears stale pending user input from projected shell summaries", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const appendAndProject = (event: Parameters[0]) => - eventStore - .append(event) - .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); - - yield* appendAndProject({ - type: "project.created", - eventId: EventId.make("evt-stale-user-input-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-stale-user-input"), - occurredAt: "2026-02-26T12:35:00.000Z", - commandId: CommandId.make("cmd-stale-user-input-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-stale-user-input-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-stale-user-input"), - title: "Project Stale User Input", - workspaceRoot: "/tmp/project-stale-user-input", - defaultModelSelection: null, - scripts: [], - createdAt: "2026-02-26T12:35:00.000Z", - updatedAt: "2026-02-26T12:35:00.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.make("evt-stale-user-input-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-stale-user-input"), - occurredAt: "2026-02-26T12:35:01.000Z", - commandId: CommandId.make("cmd-stale-user-input-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-stale-user-input-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-stale-user-input"), - projectId: ProjectId.make("project-stale-user-input"), - title: "Thread Stale User Input", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "approval-required", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: "2026-02-26T12:35:01.000Z", - updatedAt: "2026-02-26T12:35:01.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.activity-appended", - eventId: EventId.make("evt-stale-user-input-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-stale-user-input"), - occurredAt: "2026-02-26T12:35:02.000Z", - commandId: CommandId.make("cmd-stale-user-input-3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-stale-user-input-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-stale-user-input"), - activity: { - id: EventId.make("activity-stale-user-input-requested"), - tone: "info", - kind: "user-input.requested", - summary: "User input requested", - payload: { - requestId: "user-input-request-stale-1", - questions: [ - { - id: "sandbox_mode", - header: "Sandbox", - question: "Which mode should be used?", - options: [ - { - label: "workspace-write", - description: "Allow workspace writes only", - }, - ], - }, - ], - }, - turnId: null, - createdAt: "2026-02-26T12:35:02.000Z", - }, - }, - }); - - yield* appendAndProject({ - type: "thread.activity-appended", - eventId: EventId.make("evt-stale-user-input-4"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-stale-user-input"), - occurredAt: "2026-02-26T12:35:03.000Z", - commandId: CommandId.make("cmd-stale-user-input-4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-stale-user-input-4"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-stale-user-input"), - activity: { - id: EventId.make("activity-stale-user-input-failed"), - tone: "error", - kind: "provider.user-input.respond.failed", - summary: "Provider user input response failed", - payload: { - requestId: "user-input-request-stale-1", - detail: - "Provider adapter request failed (codex) for item/tool/requestUserInput: Unknown pending Codex user input request: user-input-request-stale-1", - }, - turnId: null, - createdAt: "2026-02-26T12:35:03.000Z", - }, - }, - }); - - const threadRows = yield* sql<{ - readonly pendingUserInputCount: number; - }>` - SELECT pending_user_input_count AS "pendingUserInputCount" - FROM projection_threads - WHERE thread_id = 'thread-stale-user-input' - `; - assert.deepEqual(threadRows, [{ pendingUserInputCount: 0 }]); - }), - ); - - it.effect("ignores non-stale provider approval response failures", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const appendAndProject = (event: Parameters[0]) => - eventStore - .append(event) - .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); - - yield* appendAndProject({ - type: "project.created", - eventId: EventId.make("evt-nonstale-approval-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-nonstale-approval"), - occurredAt: "2026-02-26T12:45:00.000Z", - commandId: CommandId.make("cmd-nonstale-approval-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-nonstale-approval-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-nonstale-approval"), - title: "Project Non-Stale Approval", - workspaceRoot: "/tmp/project-nonstale-approval", - defaultModelSelection: null, - scripts: [], - createdAt: "2026-02-26T12:45:00.000Z", - updatedAt: "2026-02-26T12:45:00.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.make("evt-nonstale-approval-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-nonstale-approval"), - occurredAt: "2026-02-26T12:45:01.000Z", - commandId: CommandId.make("cmd-nonstale-approval-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-nonstale-approval-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-nonstale-approval"), - projectId: ProjectId.make("project-nonstale-approval"), - title: "Thread Non-Stale Approval", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "approval-required", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: "2026-02-26T12:45:01.000Z", - updatedAt: "2026-02-26T12:45:01.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.activity-appended", - eventId: EventId.make("evt-nonstale-approval-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-nonstale-approval"), - occurredAt: "2026-02-26T12:45:02.000Z", - commandId: CommandId.make("cmd-nonstale-approval-3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-nonstale-approval-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-nonstale-approval"), - activity: { - id: EventId.make("activity-nonstale-approval-requested"), - tone: "approval", - kind: "approval.requested", - summary: "Command approval requested", - payload: { - requestId: "approval-request-nonstale-existing", - requestKind: "command", - }, - turnId: null, - createdAt: "2026-02-26T12:45:02.000Z", - }, - }, - }); - - yield* appendAndProject({ - type: "thread.activity-appended", - eventId: EventId.make("evt-nonstale-approval-4"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-nonstale-approval"), - occurredAt: "2026-02-26T12:45:03.000Z", - commandId: CommandId.make("cmd-nonstale-approval-4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-nonstale-approval-4"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-nonstale-approval"), - activity: { - id: EventId.make("activity-nonstale-approval-failed-existing"), - tone: "error", - kind: "provider.approval.respond.failed", - summary: "Provider approval response failed", - payload: { - requestId: "approval-request-nonstale-existing", - detail: "Provider timed out while responding to approval request", - }, - turnId: TurnId.make("turn-nonstale-failure"), - createdAt: "2026-02-26T12:45:03.000Z", - }, - }, - }); - - yield* appendAndProject({ - type: "thread.activity-appended", - eventId: EventId.make("evt-nonstale-approval-5"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-nonstale-approval"), - occurredAt: "2026-02-26T12:45:04.000Z", - commandId: CommandId.make("cmd-nonstale-approval-5"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-nonstale-approval-5"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-nonstale-approval"), - activity: { - id: EventId.make("activity-nonstale-approval-failed-missing"), - tone: "error", - kind: "provider.approval.respond.failed", - summary: "Provider approval response failed", - payload: { - requestId: "approval-request-nonstale-missing", - detail: "Provider timed out while responding to approval request", - }, - turnId: null, - createdAt: "2026-02-26T12:45:04.000Z", - }, - }, - }); - - const approvalRows = yield* sql<{ - readonly requestId: string; - readonly status: string; - readonly turnId: string | null; - readonly createdAt: string; - readonly resolvedAt: string | null; - }>` - SELECT - request_id AS "requestId", - status, - turn_id AS "turnId", - created_at AS "createdAt", - resolved_at AS "resolvedAt" - FROM projection_pending_approvals - WHERE request_id IN ( - 'approval-request-nonstale-existing', - 'approval-request-nonstale-missing' - ) - ORDER BY request_id - `; - assert.deepEqual(approvalRows, [ - { - requestId: "approval-request-nonstale-existing", - status: "pending", - turnId: null, - createdAt: "2026-02-26T12:45:02.000Z", - resolvedAt: null, - }, - ]); - - const threadRows = yield* sql<{ - readonly pendingApprovalCount: number; - }>` - SELECT pending_approval_count AS "pendingApprovalCount" - FROM projection_threads - WHERE thread_id = 'thread-nonstale-approval' - `; - assert.deepEqual(threadRows, [{ pendingApprovalCount: 1 }]); - }), - ); - - it.effect("does not fallback-retain messages whose turnId is removed by revert", () => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const appendAndProject = (event: Parameters[0]) => - eventStore - .append(event) - .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); - - yield* appendAndProject({ - type: "project.created", - eventId: EventId.make("evt-revert-1"), - aggregateKind: "project", - aggregateId: ProjectId.make("project-revert"), - occurredAt: "2026-02-26T12:00:00.000Z", - commandId: CommandId.make("cmd-revert-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-1"), - metadata: {}, - payload: { - projectId: ProjectId.make("project-revert"), - title: "Project Revert", - workspaceRoot: "/tmp/project-revert", - defaultModelSelection: null, - scripts: [], - createdAt: "2026-02-26T12:00:00.000Z", - updatedAt: "2026-02-26T12:00:00.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.make("evt-revert-2"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-revert"), - occurredAt: "2026-02-26T12:00:01.000Z", - commandId: CommandId.make("cmd-revert-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-2"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-revert"), - projectId: ProjectId.make("project-revert"), - title: "Thread Revert", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: "2026-02-26T12:00:01.000Z", - updatedAt: "2026-02-26T12:00:01.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.turn-diff-completed", - eventId: EventId.make("evt-revert-3"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-revert"), - occurredAt: "2026-02-26T12:00:02.000Z", - commandId: CommandId.make("cmd-revert-3"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-3"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-revert"), - turnId: TurnId.make("turn-1"), - checkpointTurnCount: 1, - checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread-revert/turn/1"), - status: "ready", - files: [], - assistantMessageId: MessageId.make("assistant-keep"), - completedAt: "2026-02-26T12:00:02.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.make("evt-revert-4"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-revert"), - occurredAt: "2026-02-26T12:00:02.100Z", - commandId: CommandId.make("cmd-revert-4"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-4"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-revert"), - messageId: MessageId.make("assistant-keep"), - role: "assistant", - text: "kept", - turnId: TurnId.make("turn-1"), - streaming: false, - createdAt: "2026-02-26T12:00:02.100Z", - updatedAt: "2026-02-26T12:00:02.100Z", - }, - }); - - yield* appendAndProject({ - type: "thread.turn-diff-completed", - eventId: EventId.make("evt-revert-5"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-revert"), - occurredAt: "2026-02-26T12:00:03.000Z", - commandId: CommandId.make("cmd-revert-5"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-5"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-revert"), - turnId: TurnId.make("turn-2"), - checkpointTurnCount: 2, - checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread-revert/turn/2"), - status: "ready", - files: [], - assistantMessageId: MessageId.make("assistant-remove"), - completedAt: "2026-02-26T12:00:03.000Z", - }, - }); - - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.make("evt-revert-6"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-revert"), - occurredAt: "2026-02-26T12:00:03.050Z", - commandId: CommandId.make("cmd-revert-6"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-6"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-revert"), - messageId: MessageId.make("user-remove"), - role: "user", - text: "removed", - turnId: TurnId.make("turn-2"), - streaming: false, - createdAt: "2026-02-26T12:00:03.050Z", - updatedAt: "2026-02-26T12:00:03.050Z", - }, - }); - - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.make("evt-revert-7"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-revert"), - occurredAt: "2026-02-26T12:00:03.100Z", - commandId: CommandId.make("cmd-revert-7"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-7"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-revert"), - messageId: MessageId.make("assistant-remove"), - role: "assistant", - text: "removed", - turnId: TurnId.make("turn-2"), - streaming: false, - createdAt: "2026-02-26T12:00:03.100Z", - updatedAt: "2026-02-26T12:00:03.100Z", - }, - }); - - yield* appendAndProject({ - type: "thread.reverted", - eventId: EventId.make("evt-revert-8"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-revert"), - occurredAt: "2026-02-26T12:00:04.000Z", - commandId: CommandId.make("cmd-revert-8"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-revert-8"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-revert"), - turnCount: 1, - }, - }); - - const messageRows = yield* sql<{ - readonly messageId: string; - readonly turnId: string | null; - readonly role: string; - }>` - SELECT - message_id AS "messageId", - turn_id AS "turnId", - role - FROM projection_thread_messages - WHERE thread_id = 'thread-revert' - ORDER BY created_at ASC, message_id ASC - `; - assert.deepEqual(messageRows, [ - { - messageId: "assistant-keep", - turnId: "turn-1", - role: "assistant", - }, - ]); - }), - ); -}); - -it.effect("restores pending turn-start metadata across projection pipeline restart", () => - Effect.gen(function* () { - const { dbPath } = yield* ServerConfig; - const persistenceLayer = makeSqlitePersistenceLive(dbPath); - const firstProjectionLayer = OrchestrationProjectionPipelineLive.pipe( - Layer.provideMerge(OrchestrationEventStoreLive), - Layer.provideMerge(persistenceLayer), - ); - const secondProjectionLayer = OrchestrationProjectionPipelineLive.pipe( - Layer.provideMerge(OrchestrationEventStoreLive), - Layer.provideMerge(persistenceLayer), - ); - - const threadId = ThreadId.make("thread-restart"); - const turnId = TurnId.make("turn-restart"); - const messageId = MessageId.make("message-restart"); - const sourcePlanThreadId = ThreadId.make("thread-plan-source"); - const sourcePlanId = "plan-source"; - const turnStartedAt = "2026-02-26T14:00:00.000Z"; - const sessionSetAt = "2026-02-26T14:00:05.000Z"; - - yield* Effect.gen(function* () { - const eventStore = yield* OrchestrationEventStore; - const projectionPipeline = yield* OrchestrationProjectionPipeline; - - yield* eventStore.append({ - type: "thread.turn-start-requested", - eventId: EventId.make("evt-restart-1"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: turnStartedAt, - commandId: CommandId.make("cmd-restart-1"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-restart-1"), - metadata: {}, - payload: { - threadId, - messageId, - sourceProposedPlan: { - threadId: sourcePlanThreadId, - planId: sourcePlanId, - }, - runtimeMode: "approval-required", - createdAt: turnStartedAt, - }, - }); - - yield* projectionPipeline.bootstrap; - }).pipe(Effect.provide(firstProjectionLayer)); - - const turnRows = yield* Effect.gen(function* () { - const eventStore = yield* OrchestrationEventStore; - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const sql = yield* SqlClient.SqlClient; - - yield* eventStore.append({ - type: "thread.session-set", - eventId: EventId.make("evt-restart-2"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: sessionSetAt, - commandId: CommandId.make("cmd-restart-2"), - causationEventId: null, - correlationId: CorrelationId.make("cmd-restart-2"), - metadata: {}, - payload: { - threadId, - session: { - threadId, - status: "running", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: turnId, - lastError: null, - updatedAt: sessionSetAt, - }, - }, - }); - - yield* projectionPipeline.bootstrap; - - const pendingRows = yield* sql<{ readonly threadId: string }>` - SELECT thread_id AS "threadId" - FROM projection_turns - WHERE thread_id = ${threadId} - AND turn_id IS NULL - AND state = 'pending' - `; - assert.deepEqual(pendingRows, []); - - return yield* sql<{ - readonly turnId: string; - readonly userMessageId: string | null; - readonly sourceProposedPlanThreadId: string | null; - readonly sourceProposedPlanId: string | null; - readonly startedAt: string; - }>` - SELECT - turn_id AS "turnId", - pending_message_id AS "userMessageId", - source_proposed_plan_thread_id AS "sourceProposedPlanThreadId", - source_proposed_plan_id AS "sourceProposedPlanId", - started_at AS "startedAt" - FROM projection_turns - WHERE turn_id = ${turnId} - `; - }).pipe(Effect.provide(secondProjectionLayer)); - - assert.deepEqual(turnRows, [ - { - turnId: "turn-restart", - userMessageId: "message-restart", - sourceProposedPlanThreadId: "thread-plan-source", - sourceProposedPlanId: "plan-source", - startedAt: turnStartedAt, - }, - ]); - }).pipe( - Effect.provide( - Layer.provideMerge( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3-projection-pipeline-restart-", - }), - NodeServices.layer, - ), - ), - ), -); - -const engineLayer = it.layer( - OrchestrationEngineLive.pipe( - Layer.provide(OrchestrationProjectionSnapshotQueryLive), - Layer.provide(OrchestrationProjectionPipelineLive), - Layer.provide(OrchestrationEventStoreLive), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provideMerge(SqlitePersistenceMemory), - Layer.provideMerge( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3-projection-pipeline-engine-dispatch-", - }), - ), - Layer.provideMerge(NodeServices.layer), - ), -); - -engineLayer("OrchestrationProjectionPipeline via engine dispatch", (it) => { - it.effect("projects dispatched engine events immediately", () => - Effect.gen(function* () { - const engine = yield* OrchestrationEngineService; - const sql = yield* SqlClient.SqlClient; - const createdAt = "2026-01-01T00:00:00.000Z"; - - yield* engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-live-project"), - projectId: ProjectId.make("project-live"), - title: "Live Project", - workspaceRoot: "/tmp/project-live", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }); - - const projectRows = yield* sql<{ readonly title: string; readonly scriptsJson: string }>` - SELECT - title, - scripts_json AS "scriptsJson" - FROM projection_projects - WHERE project_id = 'project-live' - `; - assert.deepEqual(projectRows, [{ title: "Live Project", scriptsJson: "[]" }]); - - const projectorRows = yield* sql<{ readonly lastAppliedSequence: number }>` - SELECT - last_applied_sequence AS "lastAppliedSequence" - FROM projection_state - WHERE projector = 'projection.projects' - `; - assert.deepEqual(projectorRows, [{ lastAppliedSequence: 1 }]); - }), - ); - - it.effect("projects persist updated scripts from project.meta.update", () => - Effect.gen(function* () { - const engine = yield* OrchestrationEngineService; - const sql = yield* SqlClient.SqlClient; - const createdAt = "2026-01-01T00:00:00.000Z"; - - yield* engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-scripts-project-create"), - projectId: ProjectId.make("project-scripts"), - title: "Scripts Project", - workspaceRoot: "/tmp/project-scripts", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }); - - yield* engine.dispatch({ - type: "project.meta.update", - commandId: CommandId.make("cmd-scripts-project-update"), - projectId: ProjectId.make("project-scripts"), - scripts: [ - { - id: "script-1", - name: "Build", - command: "bun run build", - icon: "build", - runOnWorktreeCreate: false, - }, - ], - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5", - }, - }); - - const projectRows = yield* sql<{ - readonly scriptsJson: string; - readonly defaultModelSelection: string; - }>` - SELECT - scripts_json AS "scriptsJson", - default_model_selection_json AS "defaultModelSelection" - FROM projection_projects - WHERE project_id = 'project-scripts' - `; - assert.deepEqual(projectRows, [ - { - scriptsJson: - '[{"id":"script-1","name":"Build","command":"bun run build","icon":"build","runOnWorktreeCreate":false}]', - defaultModelSelection: '{"instanceId":"codex","model":"gpt-5"}', - }, - ]); - }), - ); -}); diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts deleted file mode 100644 index 9a136b06872..00000000000 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ /dev/null @@ -1,1529 +0,0 @@ -import { - CheckpointRef, - EventId, - MessageId, - ProjectId, - ThreadId, - TurnId, - ProviderInstanceId, -} from "@t3tools/contracts"; -import { assert, it } from "@effect/vitest"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as SqlClient from "effect/unstable/sql/SqlClient"; - -import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; -import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; -import { ORCHESTRATION_PROJECTOR_NAMES } from "./ProjectionPipeline.ts"; -import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts"; -import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; - -const asProjectId = (value: string): ProjectId => ProjectId.make(value); -const asTurnId = (value: string): TurnId => TurnId.make(value); -const asMessageId = (value: string): MessageId => MessageId.make(value); -const asEventId = (value: string): EventId => EventId.make(value); -const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.make(value); - -const projectionSnapshotLayer = it.layer( - OrchestrationProjectionSnapshotQueryLive.pipe( - Layer.provideMerge(RepositoryIdentityResolver.layer), - Layer.provideMerge(SqlitePersistenceMemory), - Layer.provideMerge(NodeServices.layer), - ), -); - -projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { - it.effect("hydrates read model from projection tables and computes snapshot sequence", () => - Effect.gen(function* () { - const snapshotQuery = yield* ProjectionSnapshotQuery; - const sql = yield* SqlClient.SqlClient; - - yield* sql`DELETE FROM projection_projects`; - yield* sql`DELETE FROM projection_state`; - yield* sql`DELETE FROM projection_thread_proposed_plans`; - yield* sql`DELETE FROM projection_turns`; - - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-1', - 'Project 1', - '/tmp/project-1', - '{"provider":"codex","model":"gpt-5-codex"}', - '[{"id":"script-1","name":"Build","command":"bun run build","icon":"build","runOnWorktreeCreate":false}]', - '2026-02-24T00:00:00.000Z', - '2026-02-24T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'thread-1', - 'project-1', - 'Thread 1', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - 'turn-1', - '2026-02-24T00:00:04.000Z', - 1, - 0, - 0, - '2026-02-24T00:00:02.000Z', - '2026-02-24T00:00:03.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_thread_messages ( - message_id, - thread_id, - turn_id, - role, - text, - is_streaming, - created_at, - updated_at - ) - VALUES ( - 'message-1', - 'thread-1', - 'turn-1', - 'assistant', - 'hello from projection', - 0, - '2026-02-24T00:00:04.000Z', - '2026-02-24T00:00:05.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_thread_proposed_plans ( - plan_id, - thread_id, - turn_id, - plan_markdown, - implemented_at, - implementation_thread_id, - created_at, - updated_at - ) - VALUES ( - 'plan-1', - 'thread-1', - 'turn-1', - '# Ship it', - '2026-02-24T00:00:05.500Z', - 'thread-2', - '2026-02-24T00:00:05.000Z', - '2026-02-24T00:00:05.500Z' - ) - `; - - yield* sql` - INSERT INTO projection_thread_activities ( - activity_id, - thread_id, - turn_id, - tone, - kind, - summary, - payload_json, - created_at - ) - VALUES ( - 'activity-1', - 'thread-1', - 'turn-1', - 'info', - 'runtime.note', - 'provider started', - '{"stage":"start"}', - '2026-02-24T00:00:06.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_thread_sessions ( - thread_id, - status, - provider_name, - provider_session_id, - provider_thread_id, - runtime_mode, - active_turn_id, - last_error, - updated_at - ) - VALUES ( - 'thread-1', - 'running', - 'codex', - 'provider-session-1', - 'provider-thread-1', - 'approval-required', - 'turn-1', - NULL, - '2026-02-24T00:00:07.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES ( - 'thread-1', - 'turn-1', - NULL, - 'thread-1', - 'plan-1', - 'message-1', - 'completed', - '2026-02-24T00:00:08.000Z', - '2026-02-24T00:00:08.000Z', - '2026-02-24T00:00:08.000Z', - 1, - 'checkpoint-1', - 'ready', - '[{"path":"README.md","kind":"modified","additions":2,"deletions":1}]' - ) - `; - - let sequence = 5; - for (const projector of Object.values(ORCHESTRATION_PROJECTOR_NAMES)) { - yield* sql` - INSERT INTO projection_state ( - projector, - last_applied_sequence, - updated_at - ) - VALUES ( - ${projector}, - ${sequence}, - '2026-02-24T00:00:09.000Z' - ) - `; - sequence += 1; - } - - const snapshot = yield* snapshotQuery.getSnapshot(); - - assert.equal(snapshot.snapshotSequence, 5); - assert.equal(snapshot.updatedAt, "2026-02-24T00:00:09.000Z"); - assert.deepEqual(snapshot.projects, [ - { - id: asProjectId("project-1"), - title: "Project 1", - workspaceRoot: "/tmp/project-1", - repositoryIdentity: null, - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - scripts: [ - { - id: "script-1", - name: "Build", - command: "bun run build", - icon: "build", - runOnWorktreeCreate: false, - }, - ], - createdAt: "2026-02-24T00:00:00.000Z", - updatedAt: "2026-02-24T00:00:01.000Z", - deletedAt: null, - }, - ]); - assert.deepEqual(snapshot.threads, [ - { - id: ThreadId.make("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread 1", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: "default", - runtimeMode: "full-access", - branch: null, - worktreePath: null, - latestTurn: { - turnId: asTurnId("turn-1"), - state: "completed", - requestedAt: "2026-02-24T00:00:08.000Z", - startedAt: "2026-02-24T00:00:08.000Z", - completedAt: "2026-02-24T00:00:08.000Z", - assistantMessageId: asMessageId("message-1"), - sourceProposedPlan: { - threadId: ThreadId.make("thread-1"), - planId: "plan-1", - }, - }, - createdAt: "2026-02-24T00:00:02.000Z", - updatedAt: "2026-02-24T00:00:03.000Z", - archivedAt: null, - deletedAt: null, - messages: [ - { - id: asMessageId("message-1"), - role: "assistant", - text: "hello from projection", - turnId: asTurnId("turn-1"), - streaming: false, - createdAt: "2026-02-24T00:00:04.000Z", - updatedAt: "2026-02-24T00:00:05.000Z", - }, - ], - proposedPlans: [ - { - id: "plan-1", - turnId: asTurnId("turn-1"), - planMarkdown: "# Ship it", - implementedAt: "2026-02-24T00:00:05.500Z", - implementationThreadId: ThreadId.make("thread-2"), - createdAt: "2026-02-24T00:00:05.000Z", - updatedAt: "2026-02-24T00:00:05.500Z", - }, - ], - activities: [ - { - id: asEventId("activity-1"), - tone: "info", - kind: "runtime.note", - summary: "provider started", - payload: { stage: "start" }, - turnId: asTurnId("turn-1"), - createdAt: "2026-02-24T00:00:06.000Z", - }, - ], - checkpoints: [ - { - turnId: asTurnId("turn-1"), - checkpointTurnCount: 1, - checkpointRef: asCheckpointRef("checkpoint-1"), - status: "ready", - files: [{ path: "README.md", kind: "modified", additions: 2, deletions: 1 }], - assistantMessageId: asMessageId("message-1"), - completedAt: "2026-02-24T00:00:08.000Z", - }, - ], - session: { - threadId: ThreadId.make("thread-1"), - status: "running", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: asTurnId("turn-1"), - lastError: null, - updatedAt: "2026-02-24T00:00:07.000Z", - }, - }, - ]); - - const shellSnapshot = yield* snapshotQuery.getShellSnapshot(); - assert.equal(shellSnapshot.snapshotSequence, 5); - assert.deepEqual(shellSnapshot.projects, [ - { - id: asProjectId("project-1"), - title: "Project 1", - workspaceRoot: "/tmp/project-1", - repositoryIdentity: null, - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - scripts: [ - { - id: "script-1", - name: "Build", - command: "bun run build", - icon: "build", - runOnWorktreeCreate: false, - }, - ], - createdAt: "2026-02-24T00:00:00.000Z", - updatedAt: "2026-02-24T00:00:01.000Z", - }, - ]); - assert.deepEqual(shellSnapshot.threads, [ - { - id: ThreadId.make("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread 1", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: "default", - runtimeMode: "full-access", - branch: null, - worktreePath: null, - latestTurn: { - turnId: asTurnId("turn-1"), - state: "completed", - requestedAt: "2026-02-24T00:00:08.000Z", - startedAt: "2026-02-24T00:00:08.000Z", - completedAt: "2026-02-24T00:00:08.000Z", - assistantMessageId: asMessageId("message-1"), - sourceProposedPlan: { - threadId: ThreadId.make("thread-1"), - planId: "plan-1", - }, - }, - createdAt: "2026-02-24T00:00:02.000Z", - updatedAt: "2026-02-24T00:00:03.000Z", - archivedAt: null, - session: { - threadId: ThreadId.make("thread-1"), - status: "running", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: asTurnId("turn-1"), - lastError: null, - updatedAt: "2026-02-24T00:00:07.000Z", - }, - latestUserMessageAt: "2026-02-24T00:00:04.000Z", - hasPendingApprovals: true, - hasPendingUserInput: false, - hasActionableProposedPlan: false, - }, - ]); - - const threadDetail = yield* snapshotQuery.getThreadDetailById(ThreadId.make("thread-1")); - assert.equal(threadDetail._tag, "Some"); - if (threadDetail._tag === "Some") { - assert.deepEqual(threadDetail.value, snapshot.threads[0]); - } - }), - ); - - it.effect("keeps archived threads out of the main shell snapshot", () => - Effect.gen(function* () { - const snapshotQuery = yield* ProjectionSnapshotQuery; - const sql = yield* SqlClient.SqlClient; - - yield* sql`DELETE FROM projection_projects`; - yield* sql`DELETE FROM projection_threads`; - yield* sql`DELETE FROM projection_state`; - - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-archive-test', - 'Archive Test', - '/tmp/archive-test', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-06T00:00:00.000Z', - '2026-04-06T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES - ( - 'thread-active', - 'project-archive-test', - 'Active Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - NULL, - 0, - 0, - 0, - '2026-04-06T00:00:02.000Z', - '2026-04-06T00:00:03.000Z', - NULL, - NULL - ), - ( - 'thread-archived', - 'project-archive-test', - 'Archived Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - NULL, - 0, - 0, - 0, - '2026-04-06T00:00:04.000Z', - '2026-04-06T00:00:05.000Z', - '2026-04-06T00:00:06.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_state (projector, last_applied_sequence, updated_at) - VALUES - (${ORCHESTRATION_PROJECTOR_NAMES.projects}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threads}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadMessages}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadActivities}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadSessions}, 4, '2026-04-06T00:00:07.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.checkpoints}, 4, '2026-04-06T00:00:07.000Z') - `; - - const shellSnapshot = yield* snapshotQuery.getShellSnapshot(); - assert.deepEqual( - shellSnapshot.threads.map((thread) => thread.id), - [ThreadId.make("thread-active")], - ); - - const archivedShellSnapshot = yield* snapshotQuery.getArchivedShellSnapshot(); - assert.deepEqual( - archivedShellSnapshot.threads.map((thread) => thread.id), - [ThreadId.make("thread-archived")], - ); - assert.equal(archivedShellSnapshot.threads[0]?.archivedAt, "2026-04-06T00:00:06.000Z"); - }), - ); - - it.effect( - "reads targeted project, thread, and count queries without hydrating the full snapshot", - () => - Effect.gen(function* () { - const snapshotQuery = yield* ProjectionSnapshotQuery; - const sql = yield* SqlClient.SqlClient; - - yield* sql`DELETE FROM projection_projects`; - yield* sql`DELETE FROM projection_threads`; - yield* sql`DELETE FROM projection_turns`; - - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES - ( - 'project-active', - 'Active Project', - '/tmp/workspace', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-03-01T00:00:00.000Z', - '2026-03-01T00:00:01.000Z', - NULL - ), - ( - 'project-deleted', - 'Deleted Project', - '/tmp/deleted', - NULL, - '[]', - '2026-03-01T00:00:02.000Z', - '2026-03-01T00:00:03.000Z', - '2026-03-01T00:00:04.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES - ( - 'thread-first', - 'project-active', - 'First Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - '2026-03-01T00:00:05.000Z', - '2026-03-01T00:00:06.000Z', - NULL, - NULL - ), - ( - 'thread-second', - 'project-active', - 'Second Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - '2026-03-01T00:00:07.000Z', - '2026-03-01T00:00:08.000Z', - NULL, - NULL - ), - ( - 'thread-deleted', - 'project-active', - 'Deleted Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - '2026-03-01T00:00:09.000Z', - '2026-03-01T00:00:10.000Z', - NULL, - '2026-03-01T00:00:11.000Z' - ) - `; - - const counts = yield* snapshotQuery.getCounts(); - assert.deepEqual(counts, { - projectCount: 2, - threadCount: 3, - }); - - const project = yield* snapshotQuery.getActiveProjectByWorkspaceRoot("/tmp/workspace"); - assert.equal(project._tag, "Some"); - if (project._tag === "Some") { - assert.equal(project.value.id, asProjectId("project-active")); - } - - const missingProject = yield* snapshotQuery.getActiveProjectByWorkspaceRoot("/tmp/missing"); - assert.equal(missingProject._tag, "None"); - - const firstThreadId = yield* snapshotQuery.getFirstActiveThreadIdByProjectId( - asProjectId("project-active"), - ); - assert.equal(firstThreadId._tag, "Some"); - if (firstThreadId._tag === "Some") { - assert.equal(firstThreadId.value, ThreadId.make("thread-first")); - } - }), - ); - - it.effect("reads single-thread checkpoint context without hydrating unrelated threads", () => - Effect.gen(function* () { - const snapshotQuery = yield* ProjectionSnapshotQuery; - const sql = yield* SqlClient.SqlClient; - - yield* sql`DELETE FROM projection_projects`; - yield* sql`DELETE FROM projection_threads`; - yield* sql`DELETE FROM projection_turns`; - - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-context', - 'Context Project', - '/tmp/context-workspace', - NULL, - '[]', - '2026-03-02T00:00:00.000Z', - '2026-03-02T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES ( - 'thread-context', - 'project-context', - 'Context Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - 'feature/perf', - '/tmp/context-worktree', - NULL, - '2026-03-02T00:00:02.000Z', - '2026-03-02T00:00:03.000Z', - NULL, - NULL - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES - ( - 'thread-context', - 'turn-1', - NULL, - NULL, - NULL, - NULL, - 'completed', - '2026-03-02T00:00:04.000Z', - '2026-03-02T00:00:04.000Z', - '2026-03-02T00:00:04.000Z', - 1, - 'checkpoint-a', - 'ready', - '[]' - ), - ( - 'thread-context', - 'turn-2', - NULL, - NULL, - NULL, - NULL, - 'completed', - '2026-03-02T00:00:05.000Z', - '2026-03-02T00:00:05.000Z', - '2026-03-02T00:00:05.000Z', - 2, - 'checkpoint-b', - 'ready', - '[]' - ) - `; - - const context = yield* snapshotQuery.getThreadCheckpointContext( - ThreadId.make("thread-context"), - ); - assert.equal(context._tag, "Some"); - if (context._tag === "Some") { - assert.deepEqual(context.value, { - threadId: ThreadId.make("thread-context"), - projectId: asProjectId("project-context"), - workspaceRoot: "/tmp/context-workspace", - worktreePath: "/tmp/context-worktree", - checkpoints: [ - { - turnId: asTurnId("turn-1"), - checkpointTurnCount: 1, - checkpointRef: asCheckpointRef("checkpoint-a"), - status: "ready", - files: [], - assistantMessageId: null, - completedAt: "2026-03-02T00:00:04.000Z", - }, - { - turnId: asTurnId("turn-2"), - checkpointTurnCount: 2, - checkpointRef: asCheckpointRef("checkpoint-b"), - status: "ready", - files: [], - assistantMessageId: null, - completedAt: "2026-03-02T00:00:05.000Z", - }, - ], - }); - } - }), - ); - - it.effect("keeps thread detail activity ordering consistent with shell snapshot ordering", () => - Effect.gen(function* () { - const snapshotQuery = yield* ProjectionSnapshotQuery; - const sql = yield* SqlClient.SqlClient; - - yield* sql`DELETE FROM projection_projects`; - yield* sql`DELETE FROM projection_threads`; - yield* sql`DELETE FROM projection_thread_activities`; - yield* sql`DELETE FROM projection_state`; - - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-1', - 'Project 1', - '/tmp/project-1', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-01T00:00:00.000Z', - '2026-04-01T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'thread-1', - 'project-1', - 'Thread 1', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - NULL, - NULL, - 0, - 0, - 0, - '2026-04-01T00:00:02.000Z', - '2026-04-01T00:00:03.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_thread_activities ( - activity_id, - thread_id, - turn_id, - tone, - kind, - summary, - payload_json, - sequence, - created_at - ) - VALUES - ( - 'activity-unsequenced', - 'thread-1', - NULL, - 'info', - 'runtime.note', - 'unsequenced first', - '{"source":"unsequenced"}', - NULL, - '2026-04-01T00:00:06.000Z' - ), - ( - 'activity-sequence-2', - 'thread-1', - NULL, - 'info', - 'runtime.note', - 'sequence two', - '{"source":"sequence-2"}', - 2, - '2026-04-01T00:00:04.000Z' - ), - ( - 'activity-sequence-1', - 'thread-1', - NULL, - 'info', - 'runtime.note', - 'sequence one', - '{"source":"sequence-1"}', - 1, - '2026-04-01T00:00:05.000Z' - ) - `; - - const snapshot = yield* snapshotQuery.getSnapshot(); - const threadDetail = yield* snapshotQuery.getThreadDetailById(ThreadId.make("thread-1")); - - assert.equal(threadDetail._tag, "Some"); - if (threadDetail._tag === "Some") { - assert.deepEqual(threadDetail.value.activities, snapshot.threads[0]?.activities ?? []); - } - - assert.deepEqual(snapshot.threads[0]?.activities ?? [], [ - { - id: asEventId("activity-unsequenced"), - tone: "info", - kind: "runtime.note", - summary: "unsequenced first", - payload: { source: "unsequenced" }, - turnId: null, - createdAt: "2026-04-01T00:00:06.000Z", - }, - { - id: asEventId("activity-sequence-1"), - tone: "info", - kind: "runtime.note", - summary: "sequence one", - payload: { source: "sequence-1" }, - turnId: null, - sequence: 1, - createdAt: "2026-04-01T00:00:05.000Z", - }, - { - id: asEventId("activity-sequence-2"), - tone: "info", - kind: "runtime.note", - summary: "sequence two", - payload: { source: "sequence-2" }, - turnId: null, - sequence: 2, - createdAt: "2026-04-01T00:00:04.000Z", - }, - ]); - }), - ); - - it.effect("uses projection_threads.latest_turn_id for targeted thread latest turn queries", () => - Effect.gen(function* () { - const snapshotQuery = yield* ProjectionSnapshotQuery; - const sql = yield* SqlClient.SqlClient; - - yield* sql`DELETE FROM projection_projects`; - yield* sql`DELETE FROM projection_threads`; - yield* sql`DELETE FROM projection_turns`; - - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-1', - 'Project 1', - '/tmp/project-1', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-02T00:00:00.000Z', - '2026-04-02T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES ( - 'thread-1', - 'project-1', - 'Thread 1', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - 'turn-running', - '2026-04-02T00:00:04.000Z', - 0, - 0, - 0, - '2026-04-02T00:00:02.000Z', - '2026-04-02T00:00:03.000Z', - NULL, - NULL - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES - ( - 'thread-1', - 'turn-completed', - 'message-user-1', - NULL, - NULL, - 'message-assistant-1', - 'completed', - '2026-04-02T00:00:05.000Z', - '2026-04-02T00:00:06.000Z', - '2026-04-02T00:00:20.000Z', - 5, - 'checkpoint-5', - 'ready', - '[]' - ), - ( - 'thread-1', - 'turn-running', - 'message-user-2', - NULL, - NULL, - NULL, - 'running', - '2026-04-02T00:00:30.000Z', - '2026-04-02T00:00:30.000Z', - NULL, - NULL, - NULL, - NULL, - '[]' - ) - `; - - const threadShell = yield* snapshotQuery.getThreadShellById(ThreadId.make("thread-1")); - assert.equal(threadShell._tag, "Some"); - if (threadShell._tag === "Some") { - assert.equal(threadShell.value.latestTurn?.turnId, asTurnId("turn-running")); - assert.equal(threadShell.value.latestTurn?.state, "running"); - assert.equal(threadShell.value.latestTurn?.startedAt, "2026-04-02T00:00:30.000Z"); - } - - const threadDetail = yield* snapshotQuery.getThreadDetailById(ThreadId.make("thread-1")); - assert.equal(threadDetail._tag, "Some"); - if (threadDetail._tag === "Some") { - assert.equal(threadDetail.value.latestTurn?.turnId, asTurnId("turn-running")); - assert.equal(threadDetail.value.latestTurn?.state, "running"); - assert.equal(threadDetail.value.latestTurn?.startedAt, "2026-04-02T00:00:30.000Z"); - } - }), - ); - - it.effect("uses projection_threads.latest_turn_id for bulk command and shell snapshots", () => - Effect.gen(function* () { - const snapshotQuery = yield* ProjectionSnapshotQuery; - const sql = yield* SqlClient.SqlClient; - - yield* sql`DELETE FROM projection_projects`; - yield* sql`DELETE FROM projection_threads`; - yield* sql`DELETE FROM projection_turns`; - yield* sql`DELETE FROM projection_state`; - - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-1', - 'Project 1', - '/tmp/project-1', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-03T00:00:00.000Z', - '2026-04-03T00:00:01.000Z', - NULL - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES ( - 'thread-1', - 'project-1', - 'Thread 1', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - 'turn-running', - '2026-04-03T00:00:04.000Z', - 0, - 0, - 0, - '2026-04-03T00:00:02.000Z', - '2026-04-03T00:00:03.000Z', - NULL, - NULL - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES - ( - 'thread-1', - 'turn-running', - 'message-user-2', - NULL, - NULL, - NULL, - 'running', - '2026-04-03T00:00:30.000Z', - '2026-04-03T00:00:30.000Z', - NULL, - NULL, - NULL, - NULL, - '[]' - ), - ( - 'thread-1', - 'turn-completed', - 'message-user-1', - NULL, - NULL, - 'message-assistant-1', - 'completed', - '2026-04-03T00:00:05.000Z', - '2026-04-03T00:00:06.000Z', - '2026-04-03T00:00:20.000Z', - NULL, - NULL, - NULL, - '[]' - ) - `; - - yield* sql` - INSERT INTO projection_state (projector, last_applied_sequence, updated_at) - VALUES - (${ORCHESTRATION_PROJECTOR_NAMES.projects}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threads}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadMessages}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadActivities}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.threadSessions}, 3, '2026-04-03T00:00:40.000Z'), - (${ORCHESTRATION_PROJECTOR_NAMES.checkpoints}, 3, '2026-04-03T00:00:40.000Z') - `; - - const commandReadModel = yield* snapshotQuery.getCommandReadModel(); - assert.equal(commandReadModel.threads[0]?.latestTurn?.turnId, asTurnId("turn-running")); - assert.equal(commandReadModel.threads[0]?.latestTurn?.state, "running"); - - const shellSnapshot = yield* snapshotQuery.getShellSnapshot(); - assert.equal(shellSnapshot.threads[0]?.latestTurn?.turnId, asTurnId("turn-running")); - assert.equal(shellSnapshot.threads[0]?.latestTurn?.state, "running"); - - const fullSnapshot = yield* snapshotQuery.getSnapshot(); - assert.equal(fullSnapshot.threads[0]?.latestTurn?.turnId, asTurnId("turn-running")); - assert.equal(fullSnapshot.threads[0]?.latestTurn?.state, "running"); - }), - ); - - it.effect("keeps deleted project and thread tombstones in the command read model", () => - Effect.gen(function* () { - const snapshotQuery = yield* ProjectionSnapshotQuery; - const sql = yield* SqlClient.SqlClient; - - yield* sql`DELETE FROM projection_projects`; - yield* sql`DELETE FROM projection_threads`; - yield* sql`DELETE FROM projection_turns`; - yield* sql`DELETE FROM projection_state`; - - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES ( - 'project-deleted', - 'Deleted Project', - '/tmp/deleted-project', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-05T00:00:00.000Z', - '2026-04-05T00:00:01.000Z', - '2026-04-05T00:00:02.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_threads ( - thread_id, - project_id, - title, - model_selection_json, - runtime_mode, - interaction_mode, - branch, - worktree_path, - latest_turn_id, - latest_user_message_at, - pending_approval_count, - pending_user_input_count, - has_actionable_proposed_plan, - created_at, - updated_at, - archived_at, - deleted_at - ) - VALUES ( - 'thread-deleted', - 'project-deleted', - 'Deleted Thread', - '{"provider":"codex","model":"gpt-5-codex"}', - 'full-access', - 'default', - NULL, - NULL, - 'turn-deleted', - NULL, - 0, - 0, - 0, - '2026-04-05T00:00:03.000Z', - '2026-04-05T00:00:04.000Z', - NULL, - '2026-04-05T00:00:05.000Z' - ) - `; - - yield* sql` - INSERT INTO projection_turns ( - thread_id, - turn_id, - pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, - assistant_message_id, - state, - requested_at, - started_at, - completed_at, - checkpoint_turn_count, - checkpoint_ref, - checkpoint_status, - checkpoint_files_json - ) - VALUES ( - 'thread-deleted', - 'turn-deleted', - 'message-deleted-user', - NULL, - NULL, - 'message-deleted-assistant', - 'completed', - '2026-04-05T00:00:04.100Z', - '2026-04-05T00:00:04.200Z', - '2026-04-05T00:00:04.300Z', - NULL, - NULL, - NULL, - '[]' - ) - `; - - const commandReadModel = yield* snapshotQuery.getCommandReadModel(); - assert.equal(commandReadModel.projects[0]?.id, asProjectId("project-deleted")); - assert.equal(commandReadModel.projects[0]?.deletedAt, "2026-04-05T00:00:02.000Z"); - assert.equal(commandReadModel.threads[0]?.id, ThreadId.make("thread-deleted")); - assert.equal(commandReadModel.threads[0]?.deletedAt, "2026-04-05T00:00:05.000Z"); - assert.equal(commandReadModel.threads[0]?.latestTurn?.turnId, asTurnId("turn-deleted")); - assert.equal(commandReadModel.threads[0]?.latestTurn?.state, "completed"); - - const fullSnapshot = yield* snapshotQuery.getSnapshot(); - assert.equal(fullSnapshot.threads[0]?.id, ThreadId.make("thread-deleted")); - assert.equal(fullSnapshot.threads[0]?.latestTurn?.turnId, asTurnId("turn-deleted")); - assert.equal(fullSnapshot.threads[0]?.latestTurn?.state, "completed"); - - const shellSnapshot = yield* snapshotQuery.getShellSnapshot(); - assert.equal(shellSnapshot.projects.length, 0); - assert.equal(shellSnapshot.threads.length, 0); - }), - ); -}); - -it.effect( - "ProjectionSnapshotQuery dedupes repository identity resolution by workspace root and skips deleted projects for shell snapshots", - () => { - const resolveCalls: string[] = []; - const layer = OrchestrationProjectionSnapshotQueryLive.pipe( - Layer.provideMerge( - Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, { - resolve: (cwd: string) => - Effect.sync(() => { - resolveCalls.push(cwd); - return { - canonicalKey: `github.com/acme${cwd}`, - locator: { - source: "git-remote" as const, - remoteName: "origin", - remoteUrl: `https://github.com/acme${cwd}.git`, - }, - rootPath: cwd, - }; - }), - }), - ), - Layer.provideMerge(SqlitePersistenceMemory), - ); - - return Effect.gen(function* () { - const snapshotQuery = yield* ProjectionSnapshotQuery; - const sql = yield* SqlClient.SqlClient; - - yield* sql`DELETE FROM projection_projects`; - yield* sql`DELETE FROM projection_threads`; - yield* sql`DELETE FROM projection_turns`; - yield* sql`DELETE FROM projection_state`; - - yield* sql` - INSERT INTO projection_projects ( - project_id, - title, - workspace_root, - default_model_selection_json, - scripts_json, - created_at, - updated_at, - deleted_at - ) - VALUES - ( - 'project-1', - 'Shared Project 1', - '/tmp/shared-root', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-04T00:00:00.000Z', - '2026-04-04T00:00:01.000Z', - NULL - ), - ( - 'project-2', - 'Shared Project 2', - '/tmp/shared-root', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-04T00:00:02.000Z', - '2026-04-04T00:00:03.000Z', - NULL - ), - ( - 'project-3', - 'Deleted Project', - '/tmp/deleted-root', - '{"provider":"codex","model":"gpt-5-codex"}', - '[]', - '2026-04-04T00:00:04.000Z', - '2026-04-04T00:00:05.000Z', - '2026-04-04T00:00:06.000Z' - ) - `; - - const shellSnapshot = yield* snapshotQuery.getShellSnapshot(); - assert.deepStrictEqual(resolveCalls.toSorted(), ["/tmp/shared-root"]); - assert.equal(shellSnapshot.projects.length, 2); - assert.equal(shellSnapshot.projects[0]?.repositoryIdentity?.rootPath, "/tmp/shared-root"); - assert.equal(shellSnapshot.projects[1]?.repositoryIdentity?.rootPath, "/tmp/shared-root"); - - resolveCalls.length = 0; - - const fullSnapshot = yield* snapshotQuery.getSnapshot(); - assert.deepStrictEqual(resolveCalls.toSorted(), ["/tmp/deleted-root", "/tmp/shared-root"]); - assert.equal(fullSnapshot.projects.length, 3); - assert.equal(fullSnapshot.projects[2]?.repositoryIdentity?.rootPath, "/tmp/deleted-root"); - }).pipe(Effect.provide(layer)); - }, -); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts deleted file mode 100644 index ce464565dc5..00000000000 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ /dev/null @@ -1,2097 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; - -import { - ModelSelection, - ProviderRuntimeEvent, - ProviderSession, - ProviderDriverKind, - ProviderInstanceId, -} from "@t3tools/contracts"; -import { createModelSelection } from "@t3tools/shared/model"; -import { - ApprovalRequestId, - CommandId, - DEFAULT_PROVIDER_INTERACTION_MODE, - EventId, - MessageId, - ProjectId, - ThreadId, - TurnId, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Layer from "effect/Layer"; -import * as ManagedRuntime from "effect/ManagedRuntime"; -import * as PubSub from "effect/PubSub"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import { it as effectIt } from "@effect/vitest"; -import { afterEach, describe, expect, it, vi } from "vite-plus/test"; - -import { deriveServerPaths, ServerConfig } from "../../config.ts"; -import { TextGenerationError } from "@t3tools/contracts"; -import { ProviderAdapterRequestError } from "../../provider/Errors.ts"; -import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts"; -import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts"; -import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; -import { - ProviderService, - type ProviderServiceShape, -} from "../../provider/Services/ProviderService.ts"; -import { makeProviderRegistryLayer } from "../../provider/testUtils/providerRegistryMock.ts"; -import { TextGeneration, type TextGenerationShape } from "../../textGeneration/TextGeneration.ts"; -import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; -import { OrchestrationEngineLive } from "./OrchestrationEngine.ts"; -import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts"; -import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts"; -import { - providerErrorLabel, - providerErrorLabelFromInstanceHint, - ProviderCommandReactorLive, -} from "./ProviderCommandReactor.ts"; -import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; -import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts"; -import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import * as Clock from "effect/Clock"; -import { ServerSettingsService } from "../../serverSettings.ts"; -import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts"; -import * as GitWorkflowService from "../../git/GitWorkflowService.ts"; - -const asProjectId = (value: string): ProjectId => ProjectId.make(value); -const asApprovalRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make(value); -const asMessageId = (value: string): MessageId => MessageId.make(value); -const asTurnId = (value: string): TurnId => TurnId.make(value); - -const deriveServerPathsSync = (baseDir: string, devUrl: URL | undefined) => - Effect.runSync(deriveServerPaths(baseDir, devUrl).pipe(Effect.provide(NodeServices.layer))); - -async function waitFor( - predicate: () => boolean | Promise, - timeoutMs = 10_000, -): Promise { - const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs; - const poll = async (): Promise => { - if (await predicate()) { - return; - } - if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) { - throw new Error("Timed out waiting for expectation."); - } - await Effect.runPromise(Effect.yieldNow); - return poll(); - }; - - return poll(); -} - -describe("ProviderCommandReactor", () => { - let runtime: ManagedRuntime.ManagedRuntime< - OrchestrationEngineService | ProviderCommandReactor | ProjectionSnapshotQuery, - unknown - > | null = null; - let scope: Scope.Closeable | null = null; - const createdStateDirs = new Set(); - const createdBaseDirs = new Set(); - - afterEach(async () => { - if (scope) { - await Effect.runPromise(Scope.close(scope, Exit.void)); - } - scope = null; - if (runtime) { - await runtime.dispose(); - } - runtime = null; - for (const stateDir of createdStateDirs) { - NodeFS.rmSync(stateDir, { recursive: true, force: true }); - } - createdStateDirs.clear(); - for (const baseDir of createdBaseDirs) { - NodeFS.rmSync(baseDir, { recursive: true, force: true }); - } - createdBaseDirs.clear(); - }); - - describe("provider error attribution", () => { - it("uses the current provider instance slug when current instance lookup fails", () => { - expect( - providerErrorLabelFromInstanceHint({ - instanceId: "codex_personal", - modelSelectionInstanceId: "codex", - sessionProvider: "codex", - }), - ).toBe("codex_personal"); - }); - - it("uses the desired provider instance slug when desired instance lookup fails", () => { - expect( - providerErrorLabelFromInstanceHint({ - instanceId: "claude_openrouter", - }), - ).toBe("claude_openrouter"); - }); - - it("uses the unknown driver kind when the resolved driver is not registered locally", () => { - expect(providerErrorLabel("third_party_driver")).toBe("third_party_driver"); - }); - }); - - async function createHarness(input?: { - readonly baseDir?: string; - readonly threadModelSelection?: ModelSelection; - readonly sessionModelSwitch?: "unsupported" | "in-session"; - readonly requiresNewThreadForModelChange?: boolean; - }) { - const now = "2026-01-01T00:00:00.000Z"; - const baseDir = - input?.baseDir ?? NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3code-reactor-")); - createdBaseDirs.add(baseDir); - const { stateDir } = deriveServerPathsSync(baseDir, undefined); - createdStateDirs.add(stateDir); - const runtimeEventPubSub = Effect.runSync(PubSub.unbounded()); - let nextSessionIndex = 1; - const runtimeSessions: Array = []; - const modelSelection = input?.threadModelSelection ?? { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }; - const startSession = vi.fn((_: unknown, input: unknown) => { - const sessionIndex = nextSessionIndex++; - const resumeCursor = - typeof input === "object" && input !== null && "resumeCursor" in input - ? input.resumeCursor - : undefined; - const threadId = - typeof input === "object" && - input !== null && - "threadId" in input && - typeof input.threadId === "string" - ? ThreadId.make(input.threadId) - : ThreadId.make(`thread-${sessionIndex}`); - const inputModelSelection = - typeof input === "object" && input !== null && "modelSelection" in input - ? (input.modelSelection as ModelSelection | undefined) - : undefined; - const providerInstanceId = - typeof input === "object" && input !== null && "providerInstanceId" in input - ? (input.providerInstanceId as ProviderInstanceId | undefined) - : inputModelSelection?.instanceId; - const provider = - typeof input === "object" && - input !== null && - "provider" in input && - typeof input.provider === "string" - ? (input.provider as ProviderSession["provider"]) - : ProviderDriverKind.make(inputModelSelection?.instanceId ?? modelSelection.instanceId); - const session: ProviderSession = { - provider, - ...(providerInstanceId ? { providerInstanceId } : {}), - status: "ready" as const, - runtimeMode: - typeof input === "object" && - input !== null && - "runtimeMode" in input && - (input.runtimeMode === "approval-required" || input.runtimeMode === "full-access") - ? input.runtimeMode - : "full-access", - ...(typeof input === "object" && - input !== null && - "cwd" in input && - typeof input.cwd === "string" - ? { cwd: input.cwd } - : {}), - ...((inputModelSelection?.model ?? modelSelection.model) - ? { model: inputModelSelection?.model ?? modelSelection.model } - : {}), - threadId, - resumeCursor: resumeCursor ?? { opaque: `resume-${sessionIndex}` }, - createdAt: now, - updatedAt: now, - }; - runtimeSessions.push(session); - return Effect.succeed(session); - }); - const sendTurn = vi.fn((_: unknown) => - Effect.succeed({ - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-1"), - }), - ); - const interruptTurn = vi.fn((_: unknown) => Effect.void); - const respondToRequest = vi.fn(() => Effect.void); - const respondToUserInput = vi.fn(() => Effect.void); - const stopSession = vi.fn((input: unknown) => - Effect.sync(() => { - const threadId = - typeof input === "object" && input !== null && "threadId" in input - ? (input as { threadId?: ThreadId }).threadId - : undefined; - if (!threadId) { - return; - } - const index = runtimeSessions.findIndex((session) => session.threadId === threadId); - if (index >= 0) { - runtimeSessions.splice(index, 1); - } - }), - ); - const renameBranch = vi.fn((input: unknown) => - Effect.succeed({ - branch: - typeof input === "object" && - input !== null && - "newBranch" in input && - typeof input.newBranch === "string" - ? input.newBranch - : "renamed-branch", - }), - ); - const refreshStatus = vi.fn((_: string) => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: false, - refName: "renamed-branch", - hasWorkingTreeChanges: false, - workingTree: { - files: [], - insertions: 0, - deletions: 0, - }, - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }), - ); - const generateBranchName = vi.fn((_) => - Effect.fail( - new TextGenerationError({ - operation: "generateBranchName", - detail: "disabled in test harness", - }), - ), - ); - const generateThreadTitle = vi.fn((_) => - Effect.fail( - new TextGenerationError({ - operation: "generateThreadTitle", - detail: "disabled in test harness", - }), - ), - ); - const providerSnapshots = [ - { - instanceId: modelSelection.instanceId, - ...(input?.requiresNewThreadForModelChange === true - ? { requiresNewThreadForModelChange: true } - : {}), - }, - ]; - - const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never; - const service: ProviderServiceShape = { - startSession: startSession as ProviderServiceShape["startSession"], - sendTurn: sendTurn as ProviderServiceShape["sendTurn"], - interruptTurn: interruptTurn as ProviderServiceShape["interruptTurn"], - respondToRequest: respondToRequest as ProviderServiceShape["respondToRequest"], - respondToUserInput: respondToUserInput as ProviderServiceShape["respondToUserInput"], - stopSession: stopSession as ProviderServiceShape["stopSession"], - listSessions: () => Effect.succeed(runtimeSessions), - getCapabilities: (_provider) => - Effect.succeed({ - sessionModelSwitch: input?.sessionModelSwitch ?? "in-session", - }), - getInstanceInfo: (instanceId) => { - const raw = String(instanceId); - const driverKind = ProviderDriverKind.make( - raw.startsWith("claude") ? "claudeAgent" : raw.startsWith("codex") ? "codex" : raw, - ); - return Effect.succeed({ - instanceId, - driverKind, - displayName: undefined, - enabled: true, - continuationIdentity: { - driverKind, - continuationKey: - driverKind === ProviderDriverKind.make("codex") - ? "codex:home:/shared-codex" - : `${driverKind}:instance:${instanceId}`, - }, - }); - }, - rollbackConversation: () => unsupported(), - get streamEvents() { - return Stream.fromPubSub(runtimeEventPubSub); - }, - }; - - const orchestrationLayer = OrchestrationEngineLive.pipe( - Layer.provide(OrchestrationProjectionSnapshotQueryLive), - Layer.provide(OrchestrationProjectionPipelineLive), - Layer.provide(OrchestrationEventStoreLive), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - ); - const projectionSnapshotLayer = OrchestrationProjectionSnapshotQueryLive.pipe( - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - ); - const layer = ProviderCommandReactorLive.pipe( - Layer.provideMerge(orchestrationLayer), - Layer.provideMerge(projectionSnapshotLayer), - Layer.provideMerge(Layer.succeed(ProviderService, service)), - Layer.provideMerge(makeProviderRegistryLayer(providerSnapshots as never)), - Layer.provideMerge( - Layer.mock(GitWorkflowService.GitWorkflowService)({ - renameBranch, - } satisfies Partial), - ), - Layer.provideMerge( - Layer.succeed(VcsStatusBroadcaster, { - getStatus: () => Effect.die("getStatus should not be called in this test"), - refreshLocalStatus: () => - Effect.die("refreshLocalStatus should not be called in this test"), - refreshStatus, - streamStatus: () => Stream.die("streamStatus should not be called in this test"), - }), - ), - Layer.provideMerge( - Layer.mock(TextGeneration, { - generateBranchName, - generateThreadTitle, - }), - ), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), baseDir)), - Layer.provideMerge(NodeServices.layer), - ); - runtime = ManagedRuntime.make(layer); - - const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); - const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery)); - const reactor = await runtime.runPromise(Effect.service(ProviderCommandReactor)); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reactor.start().pipe(Scope.provide(scope))); - const drain = () => Effect.runPromise(reactor.drain); - - await Effect.runPromise( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-project-create"), - projectId: asProjectId("project-1"), - title: "Provider Project", - workspaceRoot: "/tmp/provider-project", - defaultModelSelection: modelSelection, - createdAt: now, - }), - ); - await Effect.runPromise( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create"), - threadId: ThreadId.make("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - modelSelection: modelSelection, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - }), - ); - - return { - engine, - readModel: () => Effect.runPromise(snapshotQuery.getSnapshot()), - startSession, - sendTurn, - interruptTurn, - respondToRequest, - respondToUserInput, - stopSession, - renameBranch, - refreshStatus, - generateBranchName, - generateThreadTitle, - runtimeSessions, - stateDir, - drain, - }; - } - - it("reacts to thread.turn.start by ensuring session and sending provider turn", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-1"), - role: "user", - text: "hello reactor", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - expect(harness.startSession.mock.calls[0]?.[0]).toEqual(ThreadId.make("thread-1")); - expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ - cwd: "/tmp/provider-project", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "approval-required", - }); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.session?.threadId).toBe("thread-1"); - expect(thread?.session?.runtimeMode).toBe("approval-required"); - }); - - it("generates a thread title on the first turn", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - const seededTitle = "Please investigate reconnect failures after restar..."; - harness.generateThreadTitle.mockReturnValue(Effect.succeed({ title: "Generated title" })); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.meta.update", - commandId: CommandId.make("cmd-thread-title-seed"), - threadId: ThreadId.make("thread-1"), - title: seededTitle, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-title"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-title"), - role: "user", - text: "Please investigate reconnect failures after restarting the session.", - attachments: [], - }, - titleSeed: seededTitle, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.generateThreadTitle.mock.calls.length === 1); - expect(harness.generateThreadTitle.mock.calls[0]?.[0]).toMatchObject({ - message: "Please investigate reconnect failures after restarting the session.", - }); - - await waitFor(async () => { - const readModel = await harness.readModel(); - return ( - readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"))?.title === - "Generated title" - ); - }); - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.title).toBe("Generated title"); - }); - - it("does not overwrite an existing custom thread title on the first turn", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - const seededTitle = "Please investigate reconnect failures after restar..."; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.meta.update", - commandId: CommandId.make("cmd-thread-title-custom"), - threadId: ThreadId.make("thread-1"), - title: "Keep this custom title", - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-title-preserve"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-title-preserve"), - role: "user", - text: "Please investigate reconnect failures after restarting the session.", - attachments: [], - }, - titleSeed: seededTitle, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - expect(harness.generateThreadTitle).not.toHaveBeenCalled(); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.title).toBe("Keep this custom title"); - }); - - it("matches the client-seeded title even when the outgoing prompt is reformatted", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - const seededTitle = "Fix reconnect spinner on resume"; - harness.generateThreadTitle.mockReturnValue( - Effect.succeed({ - title: "Reconnect spinner resume bug", - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.meta.update", - commandId: CommandId.make("cmd-thread-title-formatted-seed"), - threadId: ThreadId.make("thread-1"), - title: seededTitle, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-title-formatted"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-title-formatted"), - role: "user", - text: "[effort:high]\\n\\nFix reconnect spinner on resume", - attachments: [], - }, - titleSeed: seededTitle, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.generateThreadTitle.mock.calls.length === 1); - await waitFor(async () => { - const readModel = await harness.readModel(); - return ( - readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"))?.title === - "Reconnect spinner resume bug" - ); - }); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.title).toBe("Reconnect spinner resume bug"); - }); - - it("generates a worktree branch name for the first turn", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.meta.update", - commandId: CommandId.make("cmd-thread-branch"), - threadId: ThreadId.make("thread-1"), - branch: "t3code/1234abcd", - worktreePath: "/tmp/provider-project-worktree", - }), - ); - - harness.generateBranchName.mockImplementation((input: unknown) => - Effect.succeed({ - branch: - typeof input === "object" && - input !== null && - "modelSelection" in input && - typeof input.modelSelection === "object" && - input.modelSelection !== null && - "model" in input.modelSelection && - typeof input.modelSelection.model === "string" - ? `feature/${input.modelSelection.model}` - : "feature/generated", - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-branch-model"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-branch-model"), - role: "user", - text: "Add a safer reconnect backoff.", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.generateBranchName.mock.calls.length === 1); - await waitFor(() => harness.refreshStatus.mock.calls.length === 1); - expect(harness.generateBranchName.mock.calls[0]?.[0]).toMatchObject({ - message: "Add a safer reconnect backoff.", - }); - expect(harness.refreshStatus.mock.calls[0]?.[0]).toBe("/tmp/provider-project-worktree"); - }); - - it("forwards codex model options through session start and turn send", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-fast"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-fast"), - role: "user", - text: "hello fast mode", - attachments: [], - }, - modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ - { id: "reasoningEffort", value: "high" }, - { id: "fastMode", value: true }, - ]), - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ - modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ - { id: "reasoningEffort", value: "high" }, - { id: "fastMode", value: true }, - ]), - }); - expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ - { id: "reasoningEffort", value: "high" }, - { id: "fastMode", value: true }, - ]), - }); - }); - - it("forwards claude effort options through session start and turn send", async () => { - const harness = await createHarness({ - threadModelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-6", - }, - }); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-claude-effort"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-claude-effort"), - role: "user", - text: "hello with effort", - attachments: [], - }, - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "effort", value: "max" }], - ), - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "effort", value: "max" }], - ), - }); - expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "effort", value: "max" }], - ), - }); - }); - - it("forwards claude fast mode options through session start and turn send", async () => { - const harness = await createHarness({ - threadModelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }, - }); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-claude-fast-mode"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-claude-fast-mode"), - role: "user", - text: "hello with fast mode", - attachments: [], - }, - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-opus-4-6", - [{ id: "fastMode", value: true }], - ), - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-opus-4-6", - [{ id: "fastMode", value: true }], - ), - }); - expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-opus-4-6", - [{ id: "fastMode", value: true }], - ), - }); - }); - - it("forwards plan interaction mode to the provider turn request", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.interaction-mode.set", - commandId: CommandId.make("cmd-interaction-mode-set-plan"), - threadId: ThreadId.make("thread-1"), - interactionMode: "plan", - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-plan"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-plan"), - role: "user", - text: "plan this change", - attachments: [], - }, - interactionMode: "plan", - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - interactionMode: "plan", - }); - }); - - it("preserves the active session model when in-session model switching is unsupported", async () => { - const harness = await createHarness({ sessionModelSwitch: "unsupported" }); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-unsupported-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-unsupported-1"), - role: "user", - text: "first", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-unsupported-2"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-unsupported-2"), - role: "user", - text: "second", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.sendTurn.mock.calls.length === 2); - - expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - }); - }); - - effectIt.effect( - "rejects changing models after start when the provider requires a new thread", - () => - Effect.gen(function* () { - const harness = yield* Effect.promise(() => - createHarness({ requiresNewThreadForModelChange: true }), - ); - const now = "2026-01-01T00:00:00.000Z"; - - yield* harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-restricted-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-restricted-1"), - role: "user", - text: "first", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }); - - yield* Effect.promise(() => waitFor(() => harness.sendTurn.mock.calls.length === 1)); - - yield* harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-restricted-2"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-restricted-2"), - role: "user", - text: "second", - attachments: [], - }, - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5.1-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }); - - yield* Effect.promise(() => - waitFor(async () => { - const readModel = await harness.readModel(); - const thread = readModel.threads.find( - (entry) => entry.id === ThreadId.make("thread-1"), - ); - return ( - thread?.activities.some( - (activity) => activity.kind === "provider.turn.start.failed", - ) ?? false - ); - }), - ); - - expect(harness.sendTurn).toHaveBeenCalledTimes(1); - const readModel = yield* Effect.promise(() => harness.readModel()); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect( - thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"), - ).toMatchObject({ - payload: { - detail: expect.stringContaining( - "cannot switch models after the conversation has started", - ), - }, - }); - }), - ); - - it("starts a first turn on the requested provider instance even when it differs from the thread model", async () => { - const harness = await createHarness({ - threadModelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex" }, - }); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-provider-first"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-provider-first"), - role: "user", - text: "hello claude", - attachments: [], - }, - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - - expect(harness.startSession).toHaveBeenCalledTimes(1); - expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: ProviderInstanceId.make("claudeAgent"), - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }, - }); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.session?.providerName).toBe("claudeAgent"); - expect(thread?.session?.providerInstanceId).toBe(ProviderInstanceId.make("claudeAgent")); - expect( - thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"), - ).toBeUndefined(); - }); - - it("reuses the same provider session when runtime mode is unchanged", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-unchanged-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-unchanged-1"), - role: "user", - text: "first", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-unchanged-2"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-unchanged-2"), - role: "user", - text: "second", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.sendTurn.mock.calls.length === 2); - expect(harness.startSession.mock.calls.length).toBe(1); - expect(harness.stopSession.mock.calls.length).toBe(0); - }); - - it("restarts an existing Codex thread on a compatible requested instance", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-compatible-codex-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-compatible-codex-1"), - role: "user", - text: "first", - attachments: [], - }, - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-compatible-codex-2"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-compatible-codex-2"), - role: "user", - text: "second", - attachments: [], - }, - modelSelection: { - instanceId: ProviderInstanceId.make("codex_work"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: "2026-01-01T00:00:00.000Z", - }), - ); - - await waitFor(() => harness.sendTurn.mock.calls.length === 2); - - expect(harness.startSession).toHaveBeenCalledTimes(2); - expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ - provider: ProviderDriverKind.make("codex"), - providerInstanceId: ProviderInstanceId.make("codex_work"), - resumeCursor: { opaque: "resume-1" }, - }); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.session?.providerInstanceId).toBe(ProviderInstanceId.make("codex_work")); - }); - - it("restarts the provider session when the thread workspace changes", async () => { - const harness = await createHarness({ - threadModelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-6", - }, - }); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-workspace-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-workspace-1"), - role: "user", - text: "first in project root", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ - cwd: "/tmp/provider-project", - }); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.meta.update", - commandId: CommandId.make("cmd-thread-worktree-change"), - threadId: ThreadId.make("thread-1"), - worktreePath: "/tmp/provider-project-worktree", - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-workspace-2"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-workspace-2"), - role: "user", - text: "second in worktree", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 2); - await waitFor(() => harness.sendTurn.mock.calls.length === 2); - expect(harness.stopSession.mock.calls.length).toBe(0); - expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - cwd: "/tmp/provider-project-worktree", - resumeCursor: { opaque: "resume-1" }, - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-6", - }, - runtimeMode: "approval-required", - }); - }); - - it("restarts claude sessions when claude effort changes", async () => { - const harness = await createHarness({ - threadModelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-6", - }, - }); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-claude-effort-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-claude-effort-1"), - role: "user", - text: "first claude turn", - attachments: [], - }, - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "effort", value: "medium" }], - ), - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-claude-effort-2"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-claude-effort-2"), - role: "user", - text: "second claude turn", - attachments: [], - }, - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "effort", value: "max" }], - ), - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 2); - await waitFor(() => harness.sendTurn.mock.calls.length === 2); - expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ - resumeCursor: { opaque: "resume-1" }, - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "effort", value: "max" }], - ), - }); - }); - - it("restarts the provider session when runtime mode is updated on the thread", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.runtime-mode.set", - commandId: CommandId.make("cmd-runtime-mode-set-initial-full-access"), - threadId: ThreadId.make("thread-1"), - runtimeMode: "full-access", - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-runtime-mode-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-runtime-mode-1"), - role: "user", - text: "first", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.runtime-mode.set", - commandId: CommandId.make("cmd-runtime-mode-set-1"), - threadId: ThreadId.make("thread-1"), - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(async () => { - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - return thread?.runtimeMode === "approval-required"; - }); - await waitFor(() => harness.startSession.mock.calls.length === 2); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-runtime-mode-2"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-runtime-mode-2"), - role: "user", - text: "second", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - createdAt: now, - }), - ); - - await waitFor(() => harness.sendTurn.mock.calls.length === 2); - - expect(harness.stopSession.mock.calls.length).toBe(0); - expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - resumeCursor: { opaque: "resume-1" }, - runtimeMode: "approval-required", - }); - expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - }); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.session?.threadId).toBe("thread-1"); - expect(thread?.session?.runtimeMode).toBe("approval-required"); - }); - - it("does not inject derived model options when restarting claude on runtime mode changes", async () => { - const harness = await createHarness({ - threadModelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }, - }); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-runtime-mode-claude"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.runtime-mode.set", - commandId: CommandId.make("cmd-runtime-mode-set-claude-no-options"), - threadId: ThreadId.make("thread-1"), - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - - expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }, - runtimeMode: "approval-required", - }); - }); - - it("does not stop the active session when restart fails before rebind", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.runtime-mode.set", - commandId: CommandId.make("cmd-runtime-mode-set-initial-full-access-2"), - threadId: ThreadId.make("thread-1"), - runtimeMode: "full-access", - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-restart-failure-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-restart-failure-1"), - role: "user", - text: "first", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - - harness.startSession.mockImplementationOnce( - (_: unknown, __: unknown) => Effect.fail("simulated restart failure") as never, - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.runtime-mode.set", - commandId: CommandId.make("cmd-runtime-mode-set-restart-failure"), - threadId: ThreadId.make("thread-1"), - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(async () => { - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - return thread?.runtimeMode === "approval-required"; - }); - await waitFor(() => harness.startSession.mock.calls.length === 2); - await harness.drain(); - - expect(harness.stopSession.mock.calls.length).toBe(0); - expect(harness.sendTurn.mock.calls.length).toBe(1); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.session?.threadId).toBe("thread-1"); - expect(thread?.session?.runtimeMode).toBe("full-access"); - }); - - it("rejects provider changes after a thread is already bound to a session provider", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-provider-switch-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-provider-switch-1"), - role: "user", - text: "first", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-provider-switch-2"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-provider-switch-2"), - role: "user", - text: "second", - attachments: [], - }, - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(async () => { - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - return ( - thread?.activities.some((activity) => activity.kind === "provider.turn.start.failed") ?? - false - ); - }); - - expect(harness.startSession.mock.calls.length).toBe(1); - expect(harness.sendTurn.mock.calls.length).toBe(1); - expect(harness.stopSession.mock.calls.length).toBe(0); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.session?.threadId).toBe("thread-1"); - expect(thread?.session?.providerName).toBe("codex"); - expect(thread?.session?.runtimeMode).toBe("approval-required"); - expect( - thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"), - ).toMatchObject({ - payload: { - detail: expect.stringContaining("cannot switch to 'claudeAgent'"), - }, - }); - }); - - it("rejects cross-driver provider changes after the existing thread session has stopped", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-stopped-provider-switch"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "stopped", - providerName: "codex", - providerInstanceId: ProviderInstanceId.make("codex"), - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-stopped-provider-switch"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-stopped-provider-switch"), - role: "user", - text: "continue with claude", - attachments: [], - }, - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(async () => { - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - return ( - thread?.activities.some((activity) => activity.kind === "provider.turn.start.failed") ?? - false - ); - }); - - expect(harness.startSession.mock.calls.length).toBe(0); - expect(harness.sendTurn.mock.calls.length).toBe(0); - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect( - thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"), - ).toMatchObject({ - payload: { - detail: expect.stringContaining("cannot switch to 'claudeAgent'"), - }, - }); - }); - - it("reacts to thread.turn.interrupt-requested by calling provider interrupt", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "running", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: asTurnId("turn-1"), - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.interrupt", - commandId: CommandId.make("cmd-turn-interrupt"), - threadId: ThreadId.make("thread-1"), - turnId: asTurnId("turn-1"), - createdAt: now, - }), - ); - - await waitFor(() => harness.interruptTurn.mock.calls.length === 1); - expect(harness.interruptTurn.mock.calls[0]?.[0]).toEqual({ - threadId: "thread-1", - }); - }); - - it("starts a fresh session when only projected session state exists", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-stale"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-stale"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-stale"), - role: "user", - text: "resume codex", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - - expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "approval-required", - }); - expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({ - threadId: ThreadId.make("thread-1"), - }); - }); - - it("rejects active runtime sessions that are missing provider instance ids", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-missing-instance"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - harness.runtimeSessions.push({ - provider: ProviderDriverKind.make("codex"), - status: "ready", - runtimeMode: "approval-required", - threadId: ThreadId.make("thread-1"), - cwd: "/tmp/provider-project", - resumeCursor: { opaque: "resume-without-instance" }, - createdAt: now, - updatedAt: now, - }); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-missing-instance"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("user-message-missing-instance"), - role: "user", - text: "resume codex", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(async () => { - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - return ( - thread?.activities.some((activity) => activity.kind === "provider.turn.start.failed") ?? - false - ); - }); - - expect(harness.startSession.mock.calls.length).toBe(0); - expect(harness.sendTurn.mock.calls.length).toBe(0); - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect( - thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"), - ).toMatchObject({ - payload: { - detail: expect.stringContaining("without a provider instance id"), - }, - }); - }); - - it("reacts to thread.approval.respond by forwarding provider approval response", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-for-approval"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "running", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.approval.respond", - commandId: CommandId.make("cmd-approval-respond"), - threadId: ThreadId.make("thread-1"), - requestId: asApprovalRequestId("approval-request-1"), - decision: "accept", - createdAt: now, - }), - ); - - await waitFor(() => harness.respondToRequest.mock.calls.length === 1); - expect(harness.respondToRequest.mock.calls[0]?.[0]).toEqual({ - threadId: "thread-1", - requestId: "approval-request-1", - decision: "accept", - }); - }); - - it("reacts to thread.user-input.respond by forwarding structured user input answers", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-for-user-input"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "running", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.user-input.respond", - commandId: CommandId.make("cmd-user-input-respond"), - threadId: ThreadId.make("thread-1"), - requestId: asApprovalRequestId("user-input-request-1"), - answers: { - sandbox_mode: "workspace-write", - }, - createdAt: now, - }), - ); - - await waitFor(() => harness.respondToUserInput.mock.calls.length === 1); - expect(harness.respondToUserInput.mock.calls[0]?.[0]).toEqual({ - threadId: "thread-1", - requestId: "user-input-request-1", - answers: { - sandbox_mode: "workspace-write", - }, - }); - }); - - it("surfaces stale provider approval request failures without faking approval resolution", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - harness.respondToRequest.mockImplementation(() => - Effect.fail( - new ProviderAdapterRequestError({ - provider: ProviderDriverKind.make("codex"), - method: "session/request_permission", - detail: "Unknown pending permission request: approval-request-1", - }), - ), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-for-approval-error"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "running", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.activity.append", - commandId: CommandId.make("cmd-approval-requested"), - threadId: ThreadId.make("thread-1"), - activity: { - id: EventId.make("activity-approval-requested"), - tone: "approval", - kind: "approval.requested", - summary: "Command approval requested", - payload: { - requestId: "approval-request-1", - requestKind: "command", - }, - turnId: null, - createdAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.approval.respond", - commandId: CommandId.make("cmd-approval-respond-stale"), - threadId: ThreadId.make("thread-1"), - requestId: asApprovalRequestId("approval-request-1"), - decision: "acceptForSession", - createdAt: now, - }), - ); - - await waitFor(async () => { - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - if (!thread) return false; - return thread.activities.some( - (activity) => activity.kind === "provider.approval.respond.failed", - ); - }); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread).toBeDefined(); - - const failureActivity = thread?.activities.find( - (activity) => activity.kind === "provider.approval.respond.failed", - ); - expect(failureActivity).toBeDefined(); - expect(failureActivity?.payload).toMatchObject({ - requestId: "approval-request-1", - detail: expect.stringContaining("Stale pending approval request: approval-request-1"), - }); - - const resolvedActivity = thread?.activities.find( - (activity) => - activity.kind === "approval.resolved" && - typeof activity.payload === "object" && - activity.payload !== null && - (activity.payload as Record).requestId === "approval-request-1", - ); - expect(resolvedActivity).toBeUndefined(); - }); - - it("surfaces non-resumable provider user-input callbacks as stale failures", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - harness.respondToUserInput.mockImplementation(() => - Effect.fail( - new ProviderAdapterRequestError({ - provider: ProviderDriverKind.make("claudeAgent"), - method: "item/tool/respondToUserInput", - detail: "Unknown pending Codex user input request: user-input-request-1", - }), - ), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-for-user-input-error"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "running", - providerName: "claudeAgent", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.activity.append", - commandId: CommandId.make("cmd-user-input-requested"), - threadId: ThreadId.make("thread-1"), - activity: { - id: EventId.make("activity-user-input-requested"), - tone: "info", - kind: "user-input.requested", - summary: "User input requested", - payload: { - requestId: "user-input-request-1", - questions: [ - { - id: "sandbox_mode", - header: "Sandbox", - question: "Which mode should be used?", - options: [ - { - label: "workspace-write", - description: "Allow workspace writes only", - }, - ], - }, - ], - }, - turnId: null, - createdAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.user-input.respond", - commandId: CommandId.make("cmd-user-input-respond-stale"), - threadId: ThreadId.make("thread-1"), - requestId: asApprovalRequestId("user-input-request-1"), - answers: { - sandbox_mode: "workspace-write", - }, - createdAt: now, - }), - ); - - await waitFor(async () => { - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - if (!thread) return false; - return thread.activities.some( - (activity) => activity.kind === "provider.user-input.respond.failed", - ); - }); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread).toBeDefined(); - - const failureActivity = thread?.activities.find( - (activity) => activity.kind === "provider.user-input.respond.failed", - ); - expect(failureActivity).toBeDefined(); - expect(failureActivity?.payload).toMatchObject({ - requestId: "user-input-request-1", - detail: expect.stringContaining("Stale pending user-input request: user-input-request-1"), - }); - - const resolvedActivity = thread?.activities.find( - (activity) => - activity.kind === "user-input.resolved" && - typeof activity.payload === "object" && - activity.payload !== null && - (activity.payload as Record).requestId === "user-input-request-1", - ); - expect(resolvedActivity).toBeUndefined(); - }); - - it("reacts to thread.session.stop by stopping provider session and clearing thread session state", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-for-stop"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - providerInstanceId: ProviderInstanceId.make("codex_work"), - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - createdAt: now, - }), - ); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.stop", - commandId: CommandId.make("cmd-session-stop"), - threadId: ThreadId.make("thread-1"), - createdAt: now, - }), - ); - - await waitFor(() => harness.stopSession.mock.calls.length === 1); - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread?.session).not.toBeNull(); - expect(thread?.session?.status).toBe("stopped"); - expect(thread?.session?.threadId).toBe("thread-1"); - expect(thread?.session?.providerInstanceId).toBe(ProviderInstanceId.make("codex_work")); - expect(thread?.session?.activeTurnId).toBeNull(); - }); -}); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts deleted file mode 100644 index 9c7a7c94bb1..00000000000 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ /dev/null @@ -1,1088 +0,0 @@ -import { - type ChatAttachment, - CommandId, - EventId, - type ModelSelection, - type OrchestrationEvent, - ProviderDriverKind, - type ProjectId, - type OrchestrationSession, - ThreadId, - type ProviderSession, - type RuntimeMode, - type TurnId, -} from "@t3tools/contracts"; -import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git"; -import * as Cache from "effect/Cache"; -import * as Cause from "effect/Cause"; -import * as Crypto from "effect/Crypto"; -import * as Duration from "effect/Duration"; -import * as Effect from "effect/Effect"; -import * as Equal from "effect/Equal"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Schema from "effect/Schema"; -import * as Stream from "effect/Stream"; -import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; - -import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts"; -import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts"; -import { ProviderAdapterRequestError } from "../../provider/Errors.ts"; -import type { ProviderServiceError } from "../../provider/Errors.ts"; -import { TextGeneration } from "../../textGeneration/TextGeneration.ts"; -import { ProviderService } from "../../provider/Services/ProviderService.ts"; -import { ProviderRegistry } from "../../provider/Services/ProviderRegistry.ts"; -import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; -import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; -import { - ProviderCommandReactor, - type ProviderCommandReactorShape, -} from "../Services/ProviderCommandReactor.ts"; -import { ServerSettingsService } from "../../serverSettings.ts"; -import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts"; -import { GitWorkflowService } from "../../git/GitWorkflowService.ts"; -const isProviderAdapterRequestError = Schema.is(ProviderAdapterRequestError); -const isProviderDriverKind = Schema.is(ProviderDriverKind); - -type ProviderIntentEvent = Extract< - OrchestrationEvent, - { - type: - | "thread.runtime-mode-set" - | "thread.turn-start-requested" - | "thread.turn-interrupt-requested" - | "thread.approval-response-requested" - | "thread.user-input-response-requested" - | "thread.session-stop-requested"; - } ->; - -function toNonEmptyProviderInput(value: string | undefined): string | undefined { - const normalized = value?.trim(); - return normalized && normalized.length > 0 ? normalized : undefined; -} - -function mapProviderSessionStatusToOrchestrationStatus( - status: "connecting" | "ready" | "running" | "error" | "closed", -): OrchestrationSession["status"] { - switch (status) { - case "connecting": - return "starting"; - case "running": - return "running"; - case "error": - return "error"; - case "closed": - return "stopped"; - case "ready": - default: - return "ready"; - } -} - -const turnStartKeyForEvent = (event: ProviderIntentEvent): string => - event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`; - -const HANDLED_TURN_START_KEY_MAX = 10_000; -const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); -const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; -const DEFAULT_THREAD_TITLE = "New thread"; - -export function providerErrorLabel(value: string | undefined): string { - const normalized = value?.trim(); - return normalized && normalized.length > 0 ? normalized : "unknown"; -} - -export function providerErrorLabelFromInstanceHint(input: { - readonly instanceId?: string | undefined; - readonly modelSelectionInstanceId?: string | undefined; - readonly sessionProvider?: string | undefined; -}): string { - return providerErrorLabel( - input.instanceId ?? input.modelSelectionInstanceId ?? input.sessionProvider, - ); -} - -function canReplaceThreadTitle(currentTitle: string, titleSeed?: string): boolean { - const trimmedCurrentTitle = currentTitle.trim(); - if (trimmedCurrentTitle === DEFAULT_THREAD_TITLE) { - return true; - } - - const trimmedTitleSeed = titleSeed?.trim(); - return trimmedTitleSeed !== undefined && trimmedTitleSeed.length > 0 - ? trimmedCurrentTitle === trimmedTitleSeed - : false; -} - -function findProviderAdapterRequestError( - cause: Cause.Cause, -): ProviderAdapterRequestError | undefined { - const failReason = cause.reasons.find(Cause.isFailReason); - return isProviderAdapterRequestError(failReason?.error) ? failReason.error : undefined; -} - -function isUnknownPendingApprovalRequestError(cause: Cause.Cause): boolean { - const error = findProviderAdapterRequestError(cause); - if (error) { - const detail = error.detail.toLowerCase(); - return ( - detail.includes("unknown pending approval request") || - detail.includes("unknown pending permission request") - ); - } - const message = Cause.pretty(cause); - return ( - message.includes("unknown pending approval request") || - message.includes("unknown pending permission request") - ); -} - -function isUnknownPendingUserInputRequestError(cause: Cause.Cause): boolean { - const error = findProviderAdapterRequestError(cause); - if (error) { - const detail = error.detail.toLowerCase(); - return ( - detail.includes("unknown pending user-input request") || - detail.includes("unknown pending user input request") || - detail.includes("unknown pending codex user input request") - ); - } - const message = Cause.pretty(cause).toLowerCase(); - return ( - message.includes("unknown pending user-input request") || - message.includes("unknown pending user input request") || - message.includes("unknown pending codex user input request") - ); -} - -function stalePendingRequestDetail( - requestKind: "approval" | "user-input", - requestId: string, -): string { - return `Stale pending ${requestKind} request: ${requestId}. Provider callback state does not survive app restarts or recovered sessions. Restart the turn to continue.`; -} - -function buildGeneratedWorktreeBranchName(raw: string): string { - const normalized = raw - .trim() - .toLowerCase() - .replace(/^refs\/heads\//, "") - .replace(/['"`]/g, ""); - - const withoutPrefix = normalized.startsWith(`${WORKTREE_BRANCH_PREFIX}/`) - ? normalized.slice(`${WORKTREE_BRANCH_PREFIX}/`.length) - : normalized; - - const branchFragment = withoutPrefix - .replace(/[^a-z0-9/_-]+/g, "-") - .replace(/\/+/g, "/") - .replace(/-+/g, "-") - .replace(/^[./_-]+|[./_-]+$/g, "") - .slice(0, 64) - .replace(/[./_-]+$/g, ""); - - const safeFragment = branchFragment.length > 0 ? branchFragment : "update"; - return `${WORKTREE_BRANCH_PREFIX}/${safeFragment}`; -} - -const make = Effect.gen(function* () { - const crypto = yield* Crypto.Crypto; - const orchestrationEngine = yield* OrchestrationEngineService; - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; - const providerService = yield* ProviderService; - const providerRegistry = yield* ProviderRegistry; - const gitWorkflow = yield* GitWorkflowService; - const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; - const textGeneration = yield* TextGeneration; - const serverSettingsService = yield* ServerSettingsService; - const serverCommandId = (tag: string) => - crypto.randomUUIDv4.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`))); - const serverEventId = () => crypto.randomUUIDv4.pipe(Effect.map(EventId.make)); - const handledTurnStartKeys = yield* Cache.make({ - capacity: HANDLED_TURN_START_KEY_MAX, - timeToLive: HANDLED_TURN_START_KEY_TTL, - lookup: () => Effect.succeed(true), - }); - - const hasHandledTurnStartRecently = (key: string) => - Cache.getOption(handledTurnStartKeys, key).pipe( - Effect.flatMap((cached) => - Cache.set(handledTurnStartKeys, key, true).pipe(Effect.as(Option.isSome(cached))), - ), - ); - - const threadModelSelections = new Map(); - - const appendProviderFailureActivity = (input: { - readonly threadId: ThreadId; - readonly kind: - | "provider.turn.start.failed" - | "provider.turn.interrupt.failed" - | "provider.approval.respond.failed" - | "provider.user-input.respond.failed" - | "provider.session.stop.failed"; - readonly summary: string; - readonly detail: string; - readonly turnId: TurnId | null; - readonly createdAt: string; - readonly requestId?: string; - }) => - Effect.all({ - commandId: serverCommandId("provider-failure-activity"), - eventId: serverEventId(), - }).pipe( - Effect.flatMap(({ commandId, eventId }) => - orchestrationEngine.dispatch({ - type: "thread.activity.append", - commandId, - threadId: input.threadId, - activity: { - id: eventId, - tone: "error", - kind: input.kind, - summary: input.summary, - payload: { - detail: input.detail, - ...(input.requestId ? { requestId: input.requestId } : {}), - }, - turnId: input.turnId, - createdAt: input.createdAt, - }, - createdAt: input.createdAt, - }), - ), - ); - - const formatFailureDetail = (cause: Cause.Cause): string => { - const failReason = cause.reasons.find(Cause.isFailReason); - const providerError = isProviderAdapterRequestError(failReason?.error) - ? failReason.error - : undefined; - if (providerError) { - return providerError.detail; - } - return Cause.pretty(cause); - }; - - const setThreadSession = (input: { - readonly threadId: ThreadId; - readonly session: OrchestrationSession; - readonly createdAt: string; - }) => - serverCommandId("provider-session-set").pipe( - Effect.flatMap((commandId) => - orchestrationEngine.dispatch({ - type: "thread.session.set", - commandId, - threadId: input.threadId, - session: input.session, - createdAt: input.createdAt, - }), - ), - ); - - const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input: { - readonly threadId: ThreadId; - readonly detail: string; - readonly createdAt: string; - }) { - const thread = yield* resolveThread(input.threadId); - const session = thread?.session; - if (!session) { - return; - } - yield* setThreadSession({ - threadId: input.threadId, - session: { - ...session, - status: session.status === "stopped" ? "stopped" : "ready", - activeTurnId: null, - lastError: input.detail, - updatedAt: input.createdAt, - }, - createdAt: input.createdAt, - }); - }); - - const resolveProject = Effect.fnUntraced(function* (projectId: ProjectId) { - return yield* projectionSnapshotQuery - .getProjectShellById(projectId) - .pipe(Effect.map(Option.getOrUndefined)); - }); - - const resolveThread = Effect.fnUntraced(function* (threadId: ThreadId) { - return yield* projectionSnapshotQuery - .getThreadDetailById(threadId) - .pipe(Effect.map(Option.getOrUndefined)); - }); - - const rejectStartedThreadModelChangeIfRequired = Effect.fnUntraced(function* (input: { - readonly threadId: ThreadId; - readonly currentModelSelection: ModelSelection; - readonly requestedModelSelection: ModelSelection | undefined; - }) { - const requestedModelSelection = input.requestedModelSelection; - if ( - requestedModelSelection === undefined || - (input.currentModelSelection.instanceId === requestedModelSelection.instanceId && - input.currentModelSelection.model === requestedModelSelection.model) - ) { - return; - } - const providers = yield* providerRegistry.getProviders; - const requiresNewThread = - providers.find((snapshot) => snapshot.instanceId === input.currentModelSelection.instanceId) - ?.requiresNewThreadForModelChange === true || - providers.find((snapshot) => snapshot.instanceId === requestedModelSelection.instanceId) - ?.requiresNewThreadForModelChange === true; - if (!requiresNewThread) { - return; - } - return yield* new ProviderAdapterRequestError({ - provider: providerErrorLabelFromInstanceHint({ - instanceId: String(requestedModelSelection.instanceId), - modelSelectionInstanceId: String(input.currentModelSelection.instanceId), - }), - method: "thread.turn.start", - detail: `Thread '${input.threadId}' cannot switch models after the conversation has started. Start a new thread to use '${requestedModelSelection.model}'.`, - }); - }); - - const ensureSessionForThread = Effect.fn("ensureSessionForThread")(function* ( - threadId: ThreadId, - createdAt: string, - options?: { - readonly modelSelection?: ModelSelection; - }, - ) { - const thread = yield* resolveThread(threadId); - if (!thread) { - return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`)); - } - - const desiredRuntimeMode = thread.runtimeMode; - const requestedModelSelection = options?.modelSelection; - const resolveActiveSession = (threadId: ThreadId) => - providerService - .listSessions() - .pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === threadId))); - - const activeSession = yield* resolveActiveSession(threadId); - const activeThreadSession = - thread.session !== null && thread.session.status !== "stopped" && activeSession - ? thread.session - : null; - if ( - activeThreadSession !== null && - activeSession !== undefined && - (activeThreadSession.providerInstanceId === undefined || - activeSession.providerInstanceId === undefined) - ) { - return yield* new ProviderAdapterRequestError({ - provider: providerErrorLabel(activeThreadSession.providerName ?? undefined), - method: "thread.turn.start", - detail: `Thread '${threadId}' has an active provider session without a provider instance id.`, - }); - } - const currentInstanceId = - activeThreadSession !== null && - activeSession !== undefined && - activeSession.providerInstanceId !== undefined - ? activeSession.providerInstanceId - : thread.modelSelection.instanceId; - const desiredModelSelection = requestedModelSelection ?? thread.modelSelection; - const desiredInstanceId = desiredModelSelection.instanceId; - const currentInfo = yield* providerService.getInstanceInfo(currentInstanceId).pipe( - Effect.mapError( - () => - new ProviderAdapterRequestError({ - provider: providerErrorLabelFromInstanceHint({ - instanceId: String(currentInstanceId), - modelSelectionInstanceId: String(thread.modelSelection.instanceId), - sessionProvider: thread.session?.providerName ?? undefined, - }), - method: "thread.turn.start", - detail: `Thread '${threadId}' references unknown provider instance '${currentInstanceId}'. The instance is not configured in this build.`, - }), - ), - ); - const desiredInfo = yield* providerService.getInstanceInfo(desiredInstanceId).pipe( - Effect.mapError( - () => - new ProviderAdapterRequestError({ - provider: providerErrorLabelFromInstanceHint({ - instanceId: String(desiredModelSelection.instanceId), - }), - method: "thread.turn.start", - detail: `Requested provider instance '${desiredInstanceId}' is not configured in this build.`, - }), - ), - ); - const desiredDriverKind = desiredInfo.driverKind; - if (!isProviderDriverKind(desiredDriverKind)) { - return yield* new ProviderAdapterRequestError({ - provider: providerErrorLabel(String(desiredDriverKind)), - method: "thread.turn.start", - detail: `Requested provider instance '${desiredInstanceId}' uses unknown provider driver '${desiredDriverKind}'. The driver is not installed in this build.`, - }); - } - const preferredProvider: ProviderDriverKind = desiredDriverKind; - if (thread.session !== null) { - yield* rejectStartedThreadModelChangeIfRequired({ - threadId, - currentModelSelection: - activeSession?.model !== undefined - ? { - ...thread.modelSelection, - instanceId: currentInstanceId, - model: activeSession.model, - } - : thread.modelSelection, - requestedModelSelection, - }); - } - if ( - thread.session !== null && - requestedModelSelection !== undefined && - requestedModelSelection.instanceId !== currentInstanceId - ) { - if (currentInfo.driverKind !== desiredInfo.driverKind) { - return yield* new ProviderAdapterRequestError({ - provider: preferredProvider, - method: "thread.turn.start", - detail: `Thread '${threadId}' is bound to driver '${currentInfo.driverKind}' and cannot switch to '${desiredInfo.driverKind}'.`, - }); - } - if ( - currentInfo.continuationIdentity.continuationKey !== - desiredInfo.continuationIdentity.continuationKey - ) { - return yield* new ProviderAdapterRequestError({ - provider: preferredProvider, - method: "thread.turn.start", - detail: `Thread '${threadId}' cannot switch from instance '${currentInstanceId}' to '${desiredInstanceId}' because their provider resume state is incompatible.`, - }); - } - } - const project = yield* resolveProject(thread.projectId); - const effectiveCwd = resolveThreadWorkspaceCwd({ - thread, - projects: project ? [project] : [], - }); - - const startProviderSession = (input?: { - readonly resumeCursor?: unknown; - readonly provider?: ProviderDriverKind; - }) => - providerService.startSession(threadId, { - threadId, - ...(preferredProvider ? { provider: preferredProvider } : {}), - providerInstanceId: desiredInstanceId, - ...(effectiveCwd ? { cwd: effectiveCwd } : {}), - modelSelection: desiredModelSelection, - ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), - runtimeMode: desiredRuntimeMode, - }); - - const bindSessionToThread = (session: ProviderSession) => - Effect.gen(function* () { - if (session.providerInstanceId === undefined) { - return yield* new ProviderAdapterRequestError({ - provider: providerErrorLabel(session.provider), - method: "thread.turn.start", - detail: `Provider session '${session.threadId}' started without a provider instance id.`, - }); - } - yield* setThreadSession({ - threadId, - session: { - threadId, - status: mapProviderSessionStatusToOrchestrationStatus(session.status), - providerName: session.provider, - providerInstanceId: session.providerInstanceId, - runtimeMode: desiredRuntimeMode, - // Provider turn ids are not orchestration turn ids. - activeTurnId: null, - lastError: session.lastError ?? null, - updatedAt: session.updatedAt, - }, - createdAt, - }); - }); - - const existingSessionThreadId = - thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null; - if (existingSessionThreadId) { - const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode; - const cwdChanged = effectiveCwd !== activeSession?.cwd; - const sessionModelSwitch = (yield* providerService.getCapabilities(desiredInstanceId)) - .sessionModelSwitch; - const modelChanged = - requestedModelSelection !== undefined && - requestedModelSelection.model !== activeSession?.model; - const instanceChanged = - requestedModelSelection !== undefined && - activeSession?.providerInstanceId !== requestedModelSelection.instanceId; - const shouldRestartForModelChange = modelChanged && sessionModelSwitch === "unsupported"; - const previousModelSelection = threadModelSelections.get(threadId); - const shouldRestartForModelSelectionChange = - preferredProvider === "claudeAgent" && - requestedModelSelection !== undefined && - !Equal.equals(previousModelSelection, requestedModelSelection); - - if ( - !runtimeModeChanged && - !cwdChanged && - !instanceChanged && - !shouldRestartForModelChange && - !shouldRestartForModelSelectionChange - ) { - return existingSessionThreadId; - } - - const resumeCursor = shouldRestartForModelChange - ? undefined - : (activeSession?.resumeCursor ?? undefined); - yield* Effect.logInfo("provider command reactor restarting provider session", { - threadId, - existingSessionThreadId, - currentProvider: activeSession?.provider, - currentInstanceId, - desiredInstanceId, - desiredProvider: desiredModelSelection.instanceId, - currentRuntimeMode: thread.session?.runtimeMode, - desiredRuntimeMode: thread.runtimeMode, - runtimeModeChanged, - previousCwd: activeSession?.cwd, - desiredCwd: effectiveCwd, - cwdChanged, - modelChanged, - instanceChanged, - shouldRestartForModelChange, - shouldRestartForModelSelectionChange, - hasResumeCursor: resumeCursor !== undefined, - }); - const restartedSession = yield* startProviderSession( - resumeCursor !== undefined ? { resumeCursor } : undefined, - ); - yield* Effect.logInfo("provider command reactor restarted provider session", { - threadId, - previousSessionId: existingSessionThreadId, - restartedSessionThreadId: restartedSession.threadId, - provider: restartedSession.provider, - runtimeMode: restartedSession.runtimeMode, - cwd: restartedSession.cwd, - }); - yield* bindSessionToThread(restartedSession); - return restartedSession.threadId; - } - - const startedSession = yield* startProviderSession(undefined); - yield* bindSessionToThread(startedSession); - return startedSession.threadId; - }); - - const buildSendTurnRequestForThread = Effect.fnUntraced(function* (input: { - readonly threadId: ThreadId; - readonly messageText: string; - readonly attachments?: ReadonlyArray; - readonly modelSelection?: ModelSelection; - readonly interactionMode?: "default" | "plan"; - readonly createdAt: string; - }) { - const thread = yield* resolveThread(input.threadId); - if (!thread) { - return yield* Effect.die( - new Error(`Thread '${input.threadId}' was not found in read model.`), - ); - } - yield* ensureSessionForThread( - input.threadId, - input.createdAt, - input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}, - ); - if (input.modelSelection !== undefined) { - threadModelSelections.set(input.threadId, input.modelSelection); - } - const normalizedInput = toNonEmptyProviderInput(input.messageText); - const normalizedAttachments = input.attachments ?? []; - const activeSession = yield* providerService - .listSessions() - .pipe( - Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)), - ); - const sessionModelSwitch = - activeSession === undefined - ? "in-session" - : activeSession.providerInstanceId === undefined - ? yield* new ProviderAdapterRequestError({ - provider: providerErrorLabel(activeSession.provider), - method: "thread.turn.start", - detail: `Active provider session '${activeSession.threadId}' is missing a provider instance id.`, - }) - : (yield* providerService.getCapabilities(activeSession.providerInstanceId)) - .sessionModelSwitch; - const requestedModelSelection = - input.modelSelection ?? threadModelSelections.get(input.threadId) ?? thread.modelSelection; - const modelForTurn = - sessionModelSwitch === "unsupported" && input.modelSelection === undefined - ? activeSession?.model !== undefined - ? { - ...requestedModelSelection, - model: activeSession.model, - } - : requestedModelSelection - : input.modelSelection; - - return { - threadId: input.threadId, - ...(normalizedInput ? { input: normalizedInput } : {}), - ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), - ...(modelForTurn !== undefined ? { modelSelection: modelForTurn } : {}), - ...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}), - }; - }); - - const maybeGenerateAndRenameWorktreeBranchForFirstTurn = Effect.fn( - "maybeGenerateAndRenameWorktreeBranchForFirstTurn", - )(function* (input: { - readonly threadId: ThreadId; - readonly branch: string | null; - readonly worktreePath: string | null; - readonly messageText: string; - readonly attachments?: ReadonlyArray; - }) { - if (!input.branch || !input.worktreePath) { - return; - } - if (!isTemporaryWorktreeBranch(input.branch)) { - return; - } - - const oldBranch = input.branch; - const cwd = input.worktreePath; - const attachments = input.attachments ?? []; - yield* Effect.gen(function* () { - const { textGenerationModelSelection: modelSelection } = - yield* serverSettingsService.getSettings; - - const generated = yield* textGeneration.generateBranchName({ - cwd, - message: input.messageText, - ...(attachments.length > 0 ? { attachments } : {}), - modelSelection, - }); - if (!generated) return; - - const targetBranch = buildGeneratedWorktreeBranchName(generated.branch); - if (targetBranch === oldBranch) return; - - const renamed = yield* gitWorkflow.renameBranch({ cwd, oldBranch, newBranch: targetBranch }); - yield* orchestrationEngine.dispatch({ - type: "thread.meta.update", - commandId: yield* serverCommandId("worktree-branch-rename"), - threadId: input.threadId, - branch: renamed.branch, - worktreePath: cwd, - }); - yield* vcsStatusBroadcaster.refreshStatus(cwd).pipe(Effect.ignoreCause({ log: true })); - }).pipe( - Effect.catchCause((cause) => - Effect.logWarning("provider command reactor failed to generate or rename worktree branch", { - threadId: input.threadId, - cwd, - oldBranch, - cause: Cause.pretty(cause), - }), - ), - ); - }); - - const maybeGenerateThreadTitleForFirstTurn = Effect.fn("maybeGenerateThreadTitleForFirstTurn")( - function* (input: { - readonly threadId: ThreadId; - readonly cwd: string; - readonly messageText: string; - readonly attachments?: ReadonlyArray; - readonly titleSeed?: string; - }) { - const attachments = input.attachments ?? []; - yield* Effect.gen(function* () { - const { textGenerationModelSelection: modelSelection } = - yield* serverSettingsService.getSettings; - - const generated = yield* textGeneration.generateThreadTitle({ - cwd: input.cwd, - message: input.messageText, - ...(attachments.length > 0 ? { attachments } : {}), - modelSelection, - }); - if (!generated) return; - - const thread = yield* resolveThread(input.threadId); - if (!thread) return; - if (!canReplaceThreadTitle(thread.title, input.titleSeed)) { - return; - } - - yield* orchestrationEngine.dispatch({ - type: "thread.meta.update", - commandId: yield* serverCommandId("thread-title-rename"), - threadId: input.threadId, - title: generated.title, - }); - }).pipe( - Effect.catchCause((cause) => - Effect.logWarning("provider command reactor failed to generate or rename thread title", { - threadId: input.threadId, - cwd: input.cwd, - cause: Cause.pretty(cause), - }), - ), - ); - }, - ); - - const processTurnStartRequested = Effect.fn("processTurnStartRequested")(function* ( - event: Extract, - ) { - const key = turnStartKeyForEvent(event); - if (yield* hasHandledTurnStartRecently(key)) { - return; - } - - const thread = yield* resolveThread(event.payload.threadId); - if (!thread) { - return; - } - - const message = thread.messages.find((entry) => entry.id === event.payload.messageId); - if (!message || message.role !== "user") { - yield* appendProviderFailureActivity({ - threadId: event.payload.threadId, - kind: "provider.turn.start.failed", - summary: "Provider turn start failed", - detail: `User message '${event.payload.messageId}' was not found for turn start request.`, - turnId: null, - createdAt: event.payload.createdAt, - }); - return; - } - - const isFirstUserMessageTurn = - thread.messages.filter((entry) => entry.role === "user").length === 1; - if (isFirstUserMessageTurn) { - const project = yield* resolveProject(thread.projectId); - const generationCwd = - resolveThreadWorkspaceCwd({ - thread, - projects: project ? [project] : [], - }) ?? process.cwd(); - const generationInput = { - messageText: message.text, - ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), - ...(event.payload.titleSeed !== undefined ? { titleSeed: event.payload.titleSeed } : {}), - }; - - yield* maybeGenerateAndRenameWorktreeBranchForFirstTurn({ - threadId: event.payload.threadId, - branch: thread.branch, - worktreePath: thread.worktreePath, - ...generationInput, - }).pipe(Effect.forkScoped); - - if (canReplaceThreadTitle(thread.title, event.payload.titleSeed)) { - yield* maybeGenerateThreadTitleForFirstTurn({ - threadId: event.payload.threadId, - cwd: generationCwd, - ...generationInput, - }).pipe(Effect.forkScoped); - } - } - - const handleTurnStartFailure = (cause: Cause.Cause) => { - if (Cause.hasInterruptsOnly(cause)) { - return Effect.void; - } - const detail = formatFailureDetail(cause); - return setThreadSessionErrorOnTurnStartFailure({ - threadId: event.payload.threadId, - detail, - createdAt: event.payload.createdAt, - }).pipe( - Effect.flatMap(() => - appendProviderFailureActivity({ - threadId: event.payload.threadId, - kind: "provider.turn.start.failed", - summary: "Provider turn start failed", - detail, - turnId: null, - createdAt: event.payload.createdAt, - }), - ), - Effect.asVoid, - ); - }; - - const recoverTurnStartFailure = (cause: Cause.Cause) => - handleTurnStartFailure(cause).pipe( - Effect.catchCause((recoveryCause) => - Effect.logWarning("provider command reactor failed to recover turn start failure", { - eventType: event.type, - threadId: event.payload.threadId, - cause: Cause.pretty(recoveryCause), - originalCause: Cause.pretty(cause), - }), - ), - ); - - const sendTurnRequest = yield* buildSendTurnRequestForThread({ - threadId: event.payload.threadId, - messageText: message.text, - ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), - ...(event.payload.modelSelection !== undefined - ? { modelSelection: event.payload.modelSelection } - : {}), - interactionMode: event.payload.interactionMode, - createdAt: event.payload.createdAt, - }).pipe( - Effect.map(Option.some), - Effect.catchCause((cause) => handleTurnStartFailure(cause).pipe(Effect.as(Option.none()))), - ); - - if (Option.isNone(sendTurnRequest)) { - return; - } - - yield* providerService - .sendTurn(sendTurnRequest.value) - .pipe(Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped); - }); - - const processTurnInterruptRequested = Effect.fn("processTurnInterruptRequested")(function* ( - event: Extract, - ) { - const thread = yield* resolveThread(event.payload.threadId); - if (!thread) { - return; - } - const hasSession = thread.session && thread.session.status !== "stopped"; - if (!hasSession) { - return yield* appendProviderFailureActivity({ - threadId: event.payload.threadId, - kind: "provider.turn.interrupt.failed", - summary: "Provider turn interrupt failed", - detail: "No active provider session is bound to this thread.", - turnId: event.payload.turnId ?? null, - createdAt: event.payload.createdAt, - }); - } - - // Orchestration turn ids are not provider turn ids, so interrupt by session. - yield* providerService.interruptTurn({ threadId: event.payload.threadId }); - }); - - const processApprovalResponseRequested = Effect.fn("processApprovalResponseRequested")(function* ( - event: Extract, - ) { - const thread = yield* resolveThread(event.payload.threadId); - if (!thread) { - return; - } - const hasSession = thread.session && thread.session.status !== "stopped"; - if (!hasSession) { - return yield* appendProviderFailureActivity({ - threadId: event.payload.threadId, - kind: "provider.approval.respond.failed", - summary: "Provider approval response failed", - detail: "No active provider session is bound to this thread.", - turnId: null, - createdAt: event.payload.createdAt, - requestId: event.payload.requestId, - }); - } - - yield* providerService - .respondToRequest({ - threadId: event.payload.threadId, - requestId: event.payload.requestId, - decision: event.payload.decision, - }) - .pipe( - Effect.catchCause((cause) => - appendProviderFailureActivity({ - threadId: event.payload.threadId, - kind: "provider.approval.respond.failed", - summary: "Provider approval response failed", - detail: isUnknownPendingApprovalRequestError(cause) - ? stalePendingRequestDetail("approval", event.payload.requestId) - : Cause.pretty(cause), - turnId: null, - createdAt: event.payload.createdAt, - requestId: event.payload.requestId, - }), - ), - ); - }); - - const processUserInputResponseRequested = Effect.fn("processUserInputResponseRequested")( - function* ( - event: Extract, - ) { - const thread = yield* resolveThread(event.payload.threadId); - if (!thread) { - return; - } - const hasSession = thread.session && thread.session.status !== "stopped"; - if (!hasSession) { - return yield* appendProviderFailureActivity({ - threadId: event.payload.threadId, - kind: "provider.user-input.respond.failed", - summary: "Provider user input response failed", - detail: "No active provider session is bound to this thread.", - turnId: null, - createdAt: event.payload.createdAt, - requestId: event.payload.requestId, - }); - } - - yield* providerService - .respondToUserInput({ - threadId: event.payload.threadId, - requestId: event.payload.requestId, - answers: event.payload.answers, - }) - .pipe( - Effect.catchCause((cause) => - appendProviderFailureActivity({ - threadId: event.payload.threadId, - kind: "provider.user-input.respond.failed", - summary: "Provider user input response failed", - detail: isUnknownPendingUserInputRequestError(cause) - ? stalePendingRequestDetail("user-input", event.payload.requestId) - : Cause.pretty(cause), - turnId: null, - createdAt: event.payload.createdAt, - requestId: event.payload.requestId, - }), - ), - ); - }, - ); - - const processSessionStopRequested = Effect.fn("processSessionStopRequested")(function* ( - event: Extract, - ) { - const thread = yield* resolveThread(event.payload.threadId); - if (!thread) { - return; - } - - const now = event.payload.createdAt; - if (thread.session && thread.session.status !== "stopped") { - yield* providerService.stopSession({ threadId: thread.id }); - } - - yield* setThreadSession({ - threadId: thread.id, - session: { - threadId: thread.id, - status: "stopped", - providerName: thread.session?.providerName ?? null, - ...(thread.session?.providerInstanceId !== undefined - ? { providerInstanceId: thread.session.providerInstanceId } - : {}), - runtimeMode: thread.session?.runtimeMode ?? DEFAULT_RUNTIME_MODE, - activeTurnId: null, - lastError: thread.session?.lastError ?? null, - updatedAt: now, - }, - createdAt: now, - }); - }); - - const processDomainEvent = Effect.fn("processDomainEvent")(function* ( - event: ProviderIntentEvent, - ) { - yield* Effect.annotateCurrentSpan({ - "orchestration.event_type": event.type, - "orchestration.thread_id": event.payload.threadId, - ...(event.commandId ? { "orchestration.command_id": event.commandId } : {}), - }); - yield* increment(orchestrationEventsProcessedTotal, { - eventType: event.type, - }); - switch (event.type) { - case "thread.runtime-mode-set": { - const thread = yield* resolveThread(event.payload.threadId); - if (!thread?.session || thread.session.status === "stopped") { - return; - } - const cachedModelSelection = threadModelSelections.get(event.payload.threadId); - yield* ensureSessionForThread( - event.payload.threadId, - event.occurredAt, - cachedModelSelection !== undefined ? { modelSelection: cachedModelSelection } : {}, - ); - return; - } - case "thread.turn-start-requested": - yield* processTurnStartRequested(event); - return; - case "thread.turn-interrupt-requested": - yield* processTurnInterruptRequested(event); - return; - case "thread.approval-response-requested": - yield* processApprovalResponseRequested(event); - return; - case "thread.user-input-response-requested": - yield* processUserInputResponseRequested(event); - return; - case "thread.session-stop-requested": - yield* processSessionStopRequested(event); - return; - } - }); - - const processDomainEventSafely = (event: ProviderIntentEvent) => - processDomainEvent(event).pipe( - Effect.catchCause((cause) => { - if (Cause.hasInterruptsOnly(cause)) { - return Effect.failCause(cause); - } - return Effect.logWarning("provider command reactor failed to process event", { - eventType: event.type, - cause: Cause.pretty(cause), - }); - }), - ); - - const worker = yield* makeDrainableWorker(processDomainEventSafely); - - const start: ProviderCommandReactorShape["start"] = Effect.fn("start")(function* () { - const processEvent = Effect.fn("processEvent")(function* (event: OrchestrationEvent) { - if ( - event.type === "thread.runtime-mode-set" || - event.type === "thread.turn-start-requested" || - event.type === "thread.turn-interrupt-requested" || - event.type === "thread.approval-response-requested" || - event.type === "thread.user-input-response-requested" || - event.type === "thread.session-stop-requested" - ) { - return yield* worker.enqueue(event); - } - }); - - yield* Effect.forkScoped( - Stream.runForEach(orchestrationEngine.streamDomainEvents, processEvent), - ); - }); - - return { - start, - drain: worker.drain, - } satisfies ProviderCommandReactorShape; -}); - -export const ProviderCommandReactorLive = Layer.effect(ProviderCommandReactor, make); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts deleted file mode 100644 index 001ba388949..00000000000 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ /dev/null @@ -1,3063 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; - -import { - OrchestrationReadModel, - ProviderDriverKind, - ProviderRuntimeEvent, - ProviderSession, - ProviderInstanceId, -} from "@t3tools/contracts"; -import { - ApprovalRequestId, - CommandId, - DEFAULT_PROVIDER_INTERACTION_MODE, - EventId, - MessageId, - ProjectId, - ProviderItemId, - type ServerSettings, - ThreadId, - TurnId, -} from "@t3tools/contracts"; -import * as Clock from "effect/Clock"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Layer from "effect/Layer"; -import * as ManagedRuntime from "effect/ManagedRuntime"; -import * as PubSub from "effect/PubSub"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import { afterEach, describe, expect, it } from "vite-plus/test"; - -import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts"; -import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts"; -import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; -import { - ProviderService, - type ProviderServiceShape, -} from "../../provider/Services/ProviderService.ts"; -import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; -import { OrchestrationEngineLive } from "./OrchestrationEngine.ts"; -import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts"; -import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts"; -import { ProviderRuntimeIngestionLive } from "./ProviderRuntimeIngestion.ts"; -import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; -import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts"; -import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; -import { ServerConfig } from "../../config.ts"; -import { ServerSettingsService } from "../../serverSettings.ts"; -import * as NodeServices from "@effect/platform-node/NodeServices"; - -function makeTestServerSettingsLayer(overrides: Partial = {}) { - return ServerSettingsService.layerTest(overrides); -} - -const asProjectId = (value: string): ProjectId => ProjectId.make(value); -const asItemId = (value: string): ProviderItemId => ProviderItemId.make(value); -const asEventId = (value: string): EventId => EventId.make(value); -const asMessageId = (value: string): MessageId => MessageId.make(value); -const asThreadId = (value: string): ThreadId => ThreadId.make(value); -const asTurnId = (value: string): TurnId => TurnId.make(value); - -type LegacyProviderRuntimeEvent = { - readonly type: string; - readonly eventId: EventId; - readonly provider: ProviderRuntimeEvent["provider"]; - readonly createdAt: string; - readonly threadId: ThreadId; - readonly turnId?: string | undefined; - readonly itemId?: string | undefined; - readonly requestId?: string | undefined; - readonly payload?: unknown | undefined; - readonly [key: string]: unknown; -}; - -type LegacyTurnCompletedEvent = LegacyProviderRuntimeEvent & { - readonly type: "turn.completed"; - readonly payload?: undefined; - readonly status: "completed" | "failed" | "interrupted" | "cancelled"; - readonly errorMessage?: string | undefined; -}; - -function isLegacyTurnCompletedEvent( - event: LegacyProviderRuntimeEvent, -): event is LegacyTurnCompletedEvent { - return ( - event.type === "turn.completed" && - event.payload === undefined && - typeof event.status === "string" - ); -} - -function createProviderServiceHarness() { - const runtimeEventPubSub = Effect.runSync(PubSub.unbounded()); - const runtimeSessions: ProviderSession[] = []; - - const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never; - const service: ProviderServiceShape = { - startSession: () => unsupported(), - sendTurn: () => unsupported(), - interruptTurn: () => unsupported(), - respondToRequest: () => unsupported(), - respondToUserInput: () => unsupported(), - stopSession: () => unsupported(), - listSessions: () => Effect.succeed([...runtimeSessions]), - getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }), - getInstanceInfo: (instanceId) => { - const driverKind = ProviderDriverKind.make(String(instanceId)); - return Effect.succeed({ - instanceId, - driverKind, - displayName: undefined, - enabled: true, - continuationIdentity: { - driverKind, - continuationKey: `${driverKind}:instance:${instanceId}`, - }, - }); - }, - rollbackConversation: () => unsupported(), - get streamEvents() { - return Stream.fromPubSub(runtimeEventPubSub); - }, - }; - - const setSession = (session: ProviderSession): void => { - const existingIndex = runtimeSessions.findIndex((entry) => entry.threadId === session.threadId); - if (existingIndex >= 0) { - runtimeSessions[existingIndex] = session; - return; - } - runtimeSessions.push(session); - }; - - const normalizeLegacyEvent = (event: LegacyProviderRuntimeEvent): ProviderRuntimeEvent => { - if (isLegacyTurnCompletedEvent(event)) { - const normalized: Extract = { - ...(event as Omit, "payload">), - payload: { - state: event.status, - ...(typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}), - }, - }; - return normalized; - } - - return event as ProviderRuntimeEvent; - }; - - const emit = (event: LegacyProviderRuntimeEvent): void => { - Effect.runSync(PubSub.publish(runtimeEventPubSub, normalizeLegacyEvent(event))); - }; - - return { - service, - emit, - setSession, - }; -} - -type ProviderRuntimeTestReadModel = OrchestrationReadModel; -type ProviderRuntimeTestThread = ProviderRuntimeTestReadModel["threads"][number]; -type ProviderRuntimeTestMessage = ProviderRuntimeTestThread["messages"][number]; -type ProviderRuntimeTestProposedPlan = ProviderRuntimeTestThread["proposedPlans"][number]; -type ProviderRuntimeTestActivity = ProviderRuntimeTestThread["activities"][number]; -type ProviderRuntimeTestCheckpoint = ProviderRuntimeTestThread["checkpoints"][number]; - -async function waitForThread( - readModel: () => Promise, - predicate: (thread: ProviderRuntimeTestThread) => boolean, - timeoutMs = 2000, - threadId: ThreadId = asThreadId("thread-1"), -) { - const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs; - const poll = async (): Promise => { - const snapshot = await readModel(); - const thread = snapshot.threads.find((entry) => entry.id === threadId); - if (thread && predicate(thread)) { - return thread; - } - if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) { - throw new Error("Timed out waiting for thread state"); - } - await Effect.runPromise(Effect.yieldNow); - return poll(); - }; - return poll(); -} - -describe("ProviderRuntimeIngestion", () => { - let runtime: ManagedRuntime.ManagedRuntime< - OrchestrationEngineService | ProviderRuntimeIngestionService | ProjectionSnapshotQuery, - unknown - > | null = null; - let scope: Scope.Closeable | null = null; - const tempDirs: string[] = []; - - function makeTempDir(prefix: string): string { - const dir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), prefix)); - tempDirs.push(dir); - return dir; - } - - afterEach(async () => { - if (scope) { - await Effect.runPromise(Scope.close(scope, Exit.void)); - } - scope = null; - if (runtime) { - await runtime.dispose(); - } - runtime = null; - for (const dir of tempDirs.splice(0)) { - NodeFS.rmSync(dir, { recursive: true, force: true }); - } - }); - - async function createHarness(options?: { serverSettings?: Partial }) { - const workspaceRoot = makeTempDir("t3-provider-project-"); - NodeFS.mkdirSync(NodePath.join(workspaceRoot, ".git")); - const provider = createProviderServiceHarness(); - const orchestrationLayer = OrchestrationEngineLive.pipe( - Layer.provide(OrchestrationProjectionSnapshotQueryLive), - Layer.provide(OrchestrationProjectionPipelineLive), - Layer.provide(OrchestrationEventStoreLive), - Layer.provide(OrchestrationCommandReceiptRepositoryLive), - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - ); - const projectionSnapshotLayer = OrchestrationProjectionSnapshotQueryLive.pipe( - Layer.provide(RepositoryIdentityResolver.layer), - Layer.provide(SqlitePersistenceMemory), - ); - const layer = ProviderRuntimeIngestionLive.pipe( - Layer.provideMerge(orchestrationLayer), - Layer.provideMerge(projectionSnapshotLayer), - Layer.provideMerge(SqlitePersistenceMemory), - Layer.provideMerge(Layer.succeed(ProviderService, provider.service)), - Layer.provideMerge(makeTestServerSettingsLayer(options?.serverSettings)), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(NodeServices.layer), - ); - runtime = ManagedRuntime.make(layer); - const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); - const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery)); - const ingestion = await runtime.runPromise(Effect.service(ProviderRuntimeIngestionService)); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(ingestion.start().pipe(Scope.provide(scope))); - const drain = () => Effect.runPromise(ingestion.drain); - - const createdAt = "2026-01-01T00:00:00.000Z"; - await Effect.runPromise( - engine.dispatch({ - type: "project.create", - commandId: CommandId.make("cmd-provider-project-create"), - projectId: asProjectId("project-1"), - title: "Provider Project", - workspaceRoot, - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt, - }), - ); - await Effect.runPromise( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create"), - threadId: ThreadId.make("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await Effect.runPromise( - engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-seed"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: createdAt, - lastError: null, - }, - createdAt, - }), - ); - provider.setSession({ - provider: ProviderDriverKind.make("codex"), - status: "ready", - runtimeMode: "approval-required", - threadId: ThreadId.make("thread-1"), - createdAt, - updatedAt: createdAt, - }); - - return { - engine, - readModel: () => Effect.runPromise(snapshotQuery.getSnapshot()), - emit: provider.emit, - setProviderSession: provider.setSession, - drain, - }; - } - - it("maps turn started/completed events into thread session updates", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started"), - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: now, - turnId: asTurnId("turn-1"), - }); - - await waitForThread( - harness.readModel, - (thread) => thread.session?.status === "running" && thread.session?.activeTurnId === "turn-1", - ); - - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed"), - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - turnId: asTurnId("turn-1"), - payload: { - state: "failed", - errorMessage: "turn failed", - }, - }); - - const thread = await waitForThread( - harness.readModel, - (entry) => - entry.session?.status === "error" && - entry.session?.activeTurnId === null && - entry.session?.lastError === "turn failed", - ); - expect(thread.session?.status).toBe("error"); - expect(thread.session?.lastError).toBe("turn failed"); - }); - - it("applies provider session.state.changed transitions directly", async () => { - const harness = await createHarness(); - const waitingAt = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "session.state.changed", - eventId: asEventId("evt-session-state-waiting"), - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: waitingAt, - payload: { - state: "waiting", - reason: "awaiting approval", - }, - }); - - let thread = await waitForThread( - harness.readModel, - (entry) => entry.session?.status === "running" && entry.session?.activeTurnId === null, - ); - expect(thread.session?.status).toBe("running"); - expect(thread.session?.lastError).toBeNull(); - - harness.emit({ - type: "session.state.changed", - eventId: asEventId("evt-session-state-error"), - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - payload: { - state: "error", - reason: "provider crashed", - }, - }); - - thread = await waitForThread( - harness.readModel, - (entry) => - entry.session?.status === "error" && - entry.session?.activeTurnId === null && - entry.session?.lastError === "provider crashed", - ); - expect(thread.session?.status).toBe("error"); - expect(thread.session?.lastError).toBe("provider crashed"); - - harness.emit({ - type: "session.state.changed", - eventId: asEventId("evt-session-state-stopped"), - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - payload: { - state: "stopped", - }, - }); - - thread = await waitForThread( - harness.readModel, - (entry) => - entry.session?.status === "stopped" && - entry.session?.activeTurnId === null && - entry.session?.lastError === "provider crashed", - ); - expect(thread.session?.status).toBe("stopped"); - expect(thread.session?.lastError).toBe("provider crashed"); - - harness.emit({ - type: "session.state.changed", - eventId: asEventId("evt-session-state-ready"), - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - payload: { - state: "ready", - }, - }); - - thread = await waitForThread( - harness.readModel, - (entry) => - entry.session?.status === "ready" && - entry.session?.activeTurnId === null && - entry.session?.lastError === null, - ); - expect(thread.session?.status).toBe("ready"); - expect(thread.session?.lastError).toBeNull(); - }); - - it("does not clear active turn when session/thread started arrives mid-turn", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-midturn-lifecycle"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-midturn-lifecycle"), - }); - - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-midturn-lifecycle", - ); - - harness.emit({ - type: "thread.started", - eventId: asEventId("evt-thread-started-midturn-lifecycle"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - }); - harness.emit({ - type: "session.started", - eventId: asEventId("evt-session-started-midturn-lifecycle"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - }); - - await harness.drain(); - const midReadModel = await harness.readModel(); - const midThread = midReadModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(midThread?.session?.status).toBe("running"); - expect(midThread?.session?.activeTurnId).toBe("turn-midturn-lifecycle"); - - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed-midturn-lifecycle"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-midturn-lifecycle"), - status: "completed", - }); - - await waitForThread( - harness.readModel, - (thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null, - ); - }); - - it("accepts claude turn lifecycle when seeded thread id is a synthetic placeholder", async () => { - const harness = await createHarness(); - const seededAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-seed-claude-placeholder"), - threadId: ThreadId.make("thread-1"), - session: { - threadId: ThreadId.make("thread-1"), - status: "ready", - providerName: "claudeAgent", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: seededAt, - lastError: null, - }, - createdAt: seededAt, - }), - ); - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-claude-placeholder"), - provider: ProviderDriverKind.make("claudeAgent"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-claude-placeholder"), - }); - - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-claude-placeholder", - ); - - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed-claude-placeholder"), - provider: ProviderDriverKind.make("claudeAgent"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-claude-placeholder"), - status: "completed", - }); - - await waitForThread( - harness.readModel, - (thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null, - ); - }); - - it("ignores auxiliary turn completions from a different provider thread", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-primary"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-primary"), - }); - - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && thread.session?.activeTurnId === "turn-primary", - ); - - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed-aux"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-aux"), - status: "completed", - }); - - await harness.drain(); - const midReadModel = await harness.readModel(); - const midThread = midReadModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(midThread?.session?.status).toBe("running"); - expect(midThread?.session?.activeTurnId).toBe("turn-primary"); - - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed-primary"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-primary"), - status: "completed", - }); - - await waitForThread( - harness.readModel, - (thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null, - ); - }); - - it("ignores non-active turn completion when runtime omits thread id", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-guarded"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-guarded-main"), - }); - - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-guarded-main", - ); - - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed-guarded-other"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-guarded-other"), - status: "completed", - }); - - await harness.drain(); - const midReadModel = await harness.readModel(); - const midThread = midReadModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(midThread?.session?.status).toBe("running"); - expect(midThread?.session?.activeTurnId).toBe("turn-guarded-main"); - - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed-guarded-main"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-guarded-main"), - status: "completed", - }); - - await waitForThread( - harness.readModel, - (thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null, - ); - }); - - it("maps canonical content delta/item completed into finalized assistant messages", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-1"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-2"), - itemId: asItemId("item-1"), - payload: { - streamKind: "assistant_text", - delta: "hello", - }, - }); - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-2"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-2"), - itemId: asItemId("item-1"), - payload: { - streamKind: "assistant_text", - delta: " world", - }, - }); - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-message-completed"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-2"), - itemId: asItemId("item-1"), - payload: { - itemType: "assistant_message", - status: "completed", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-1" && !message.streaming, - ), - ); - const message = thread.messages.find( - (entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-1", - ); - expect(message?.text).toBe("hello world"); - expect(message?.streaming).toBe(false); - }); - - it("uses assistant item completion detail when no assistant deltas were streamed", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-assistant-item-completed-no-delta"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-no-delta"), - itemId: asItemId("item-no-delta"), - payload: { - itemType: "assistant_message", - status: "completed", - detail: "assistant-only final text", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-no-delta" && !message.streaming, - ), - ); - const message = thread.messages.find( - (entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-no-delta", - ); - expect(message?.text).toBe("assistant-only final text"); - expect(message?.streaming).toBe(false); - }); - - it("preserves completed tool metadata on projected tool activities", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-tool-completed-with-data"), - provider: ProviderDriverKind.make("cursor"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-tool-completed"), - itemId: asItemId("item-tool-completed"), - payload: { - itemType: "dynamic_tool_call", - status: "completed", - title: "Read file", - data: { - toolCallId: "tool-read-1", - kind: "read", - rawOutput: { - content: 'import * as Effect from "effect/Effect"\n', - }, - }, - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-tool-completed-with-data", - ), - ); - const activity = thread.activities.find( - (entry: ProviderRuntimeTestActivity) => entry.id === "evt-tool-completed-with-data", - ); - const payload = - activity?.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : undefined; - const data = - payload?.data && typeof payload.data === "object" - ? (payload.data as Record) - : undefined; - const rawOutput = - data?.rawOutput && typeof data.rawOutput === "object" - ? (data.rawOutput as Record) - : undefined; - - expect(activity?.kind).toBe("tool.completed"); - expect(activity?.summary).toBe("Read file"); - expect(payload?.itemType).toBe("dynamic_tool_call"); - expect(payload?.detail).toBeUndefined(); - expect(data?.toolCallId).toBe("tool-read-1"); - expect(data?.kind).toBe("read"); - expect(rawOutput?.content).toBe('import * as Effect from "effect/Effect"\n'); - }); - - it("normalizes command execution activities to ran-command summaries", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-command-completed"), - provider: ProviderDriverKind.make("cursor"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-command-completed"), - itemId: asItemId("item-command-completed"), - payload: { - itemType: "command_execution", - status: "completed", - title: "Ran command", - detail: "bun run lint", - data: { - toolCallId: "tool-command-1", - kind: "execute", - command: "bun run lint", - }, - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-command-completed", - ), - ); - const activity = thread.activities.find( - (entry: ProviderRuntimeTestActivity) => entry.id === "evt-command-completed", - ); - const payload = - activity?.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : undefined; - - expect(activity?.summary).toBe("Ran command"); - expect(payload?.detail).toBe("bun run lint"); - }); - - it("uses structured read-file paths when available", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-read-path-completed"), - provider: ProviderDriverKind.make("cursor"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-read-path"), - itemId: asItemId("item-read-path"), - payload: { - itemType: "dynamic_tool_call", - status: "completed", - title: "Read file", - detail: "/tmp/app.ts", - data: { - toolCallId: "tool-read-path-1", - kind: "read", - locations: [{ path: "/tmp/app.ts" }], - }, - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-read-path-completed", - ), - ); - const activity = thread.activities.find( - (entry: ProviderRuntimeTestActivity) => entry.id === "evt-read-path-completed", - ); - const payload = - activity?.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : undefined; - - expect(activity?.summary).toBe("Read file"); - expect(payload?.detail).toBe("/tmp/app.ts"); - }); - - it("projects completed plan items into first-class proposed plans", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.proposed.completed", - eventId: asEventId("evt-plan-item-completed"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-plan-final"), - payload: { - planMarkdown: "## Ship plan\n\n- wire projection\n- render follow-up", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === "plan:thread-1:turn:turn-plan-final", - ), - ); - const proposedPlan = thread.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => entry.id === "plan:thread-1:turn:turn-plan-final", - ); - expect(proposedPlan?.planMarkdown).toBe( - "## Ship plan\n\n- wire projection\n- render follow-up", - ); - }); - - it("marks the source proposed plan implemented only after the target turn starts", async () => { - const harness = await createHarness(); - const sourceThreadId = asThreadId("thread-plan"); - const targetThreadId = asThreadId("thread-implement"); - const sourceTurnId = asTurnId("turn-plan-source"); - const targetTurnId = asTurnId("turn-plan-implement"); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create-plan-source"), - threadId: sourceThreadId, - projectId: asProjectId("project-1"), - title: "Plan Source", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: "plan", - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-plan-source"), - threadId: sourceThreadId, - session: { - threadId: sourceThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: createdAt, - lastError: null, - }, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create-plan-target"), - threadId: targetThreadId, - projectId: asProjectId("project-1"), - title: "Plan Target", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-plan-target"), - threadId: targetThreadId, - session: { - threadId: targetThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: createdAt, - lastError: null, - }, - createdAt, - }), - ); - harness.setProviderSession({ - provider: ProviderDriverKind.make("codex"), - status: "ready", - runtimeMode: "approval-required", - threadId: targetThreadId, - createdAt, - updatedAt: createdAt, - activeTurnId: targetTurnId, - }); - - harness.emit({ - type: "turn.proposed.completed", - eventId: asEventId("evt-plan-source-completed"), - provider: ProviderDriverKind.make("codex"), - createdAt, - threadId: sourceThreadId, - turnId: sourceTurnId, - payload: { - planMarkdown: "# Source plan", - }, - }); - - const sourceThreadWithPlan = await waitForThread( - harness.readModel, - (thread) => - thread.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === "plan:thread-plan:turn:turn-plan-source" && - proposedPlan.implementedAt === null, - ), - 2_000, - sourceThreadId, - ); - const sourcePlan = sourceThreadWithPlan.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => - entry.id === "plan:thread-plan:turn:turn-plan-source", - ); - expect(sourcePlan).toBeDefined(); - if (!sourcePlan) { - throw new Error("Expected source plan to exist."); - } - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-plan-target"), - threadId: targetThreadId, - message: { - messageId: asMessageId("msg-plan-target"), - role: "user", - text: "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", - attachments: [], - }, - sourceProposedPlan: { - threadId: sourceThreadId, - planId: sourcePlan.id, - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: "2026-01-01T00:00:00.000Z", - }), - ); - - const sourceThreadBeforeStart = await waitForThread( - harness.readModel, - (thread) => - thread.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === sourcePlan.id && proposedPlan.implementedAt === null, - ), - 2_000, - sourceThreadId, - ); - expect( - sourceThreadBeforeStart.proposedPlans.find((entry) => entry.id === sourcePlan.id), - ).toMatchObject({ - implementedAt: null, - implementationThreadId: null, - }); - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-plan-target-started"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: targetThreadId, - turnId: targetTurnId, - }); - - const sourceThreadAfterStart = await waitForThread( - harness.readModel, - (thread) => - thread.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === sourcePlan.id && - proposedPlan.implementedAt !== null && - proposedPlan.implementationThreadId === targetThreadId, - ), - 2_000, - sourceThreadId, - ); - expect( - sourceThreadAfterStart.proposedPlans.find((entry) => entry.id === sourcePlan.id), - ).toMatchObject({ - implementationThreadId: "thread-implement", - }); - }); - - it("does not mark the source proposed plan implemented for a rejected turn.started event", async () => { - const harness = await createHarness(); - const sourceThreadId = asThreadId("thread-plan"); - const targetThreadId = asThreadId("thread-1"); - const sourceTurnId = asTurnId("turn-plan-source"); - const activeTurnId = asTurnId("turn-already-running"); - const staleTurnId = asTurnId("turn-stale-start"); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - Effect.andThen( - harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create-plan-source-guarded"), - threadId: sourceThreadId, - projectId: asProjectId("project-1"), - title: "Plan Source", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: "plan", - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-plan-source-guarded"), - threadId: sourceThreadId, - session: { - threadId: sourceThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: createdAt, - lastError: null, - }, - createdAt, - }), - ), - ); - harness.setProviderSession({ - provider: ProviderDriverKind.make("codex"), - status: "running", - runtimeMode: "approval-required", - threadId: targetThreadId, - createdAt, - updatedAt: createdAt, - activeTurnId, - }); - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-already-running"), - provider: ProviderDriverKind.make("codex"), - createdAt, - threadId: targetThreadId, - turnId: activeTurnId, - }); - - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && thread.session?.activeTurnId === activeTurnId, - 2_000, - targetThreadId, - ); - - harness.emit({ - type: "turn.proposed.completed", - eventId: asEventId("evt-plan-source-completed-guarded"), - provider: ProviderDriverKind.make("codex"), - createdAt, - threadId: sourceThreadId, - turnId: sourceTurnId, - payload: { - planMarkdown: "# Source plan", - }, - }); - - const sourceThreadWithPlan = await waitForThread( - harness.readModel, - (thread) => - thread.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === "plan:thread-plan:turn:turn-plan-source" && - proposedPlan.implementedAt === null, - ), - 2_000, - sourceThreadId, - ); - const sourcePlan = sourceThreadWithPlan.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => - entry.id === "plan:thread-plan:turn:turn-plan-source", - ); - expect(sourcePlan).toBeDefined(); - if (!sourcePlan) { - throw new Error("Expected source plan to exist."); - } - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-plan-target-guarded"), - threadId: targetThreadId, - message: { - messageId: asMessageId("msg-plan-target-guarded"), - role: "user", - text: "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", - attachments: [], - }, - sourceProposedPlan: { - threadId: sourceThreadId, - planId: sourcePlan.id, - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: "2026-01-01T00:00:00.000Z", - }), - ); - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-stale-plan-implementation"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: targetThreadId, - turnId: staleTurnId, - }); - - await harness.drain(); - - const readModel = await harness.readModel(); - const sourceThreadAfterRejectedStart = readModel.threads.find( - (entry) => entry.id === sourceThreadId, - ); - expect( - sourceThreadAfterRejectedStart?.proposedPlans.find((entry) => entry.id === sourcePlan.id), - ).toMatchObject({ - implementedAt: null, - implementationThreadId: null, - }); - - const targetThreadAfterRejectedStart = readModel.threads.find( - (entry) => entry.id === targetThreadId, - ); - expect(targetThreadAfterRejectedStart?.session?.status).toBe("running"); - expect(targetThreadAfterRejectedStart?.session?.activeTurnId).toBe(activeTurnId); - }); - - it("accepts a conflicting turn.started for a pending turn start when the provider expects that turn", async () => { - // Steering a running turn: the server requests a new turn while the old - // one is still active, and providers like opencode open the new turn - // without ever completing the superseded one. The new turn.started must - // replace the active turn instead of being rejected as stale. - const harness = await createHarness(); - const threadId = asThreadId("thread-1"); - const oldTurnId = asTurnId("turn-steered-over"); - const newTurnId = asTurnId("turn-from-steer"); - const createdAt = "2026-01-01T00:00:00.000Z"; - - harness.setProviderSession({ - provider: ProviderDriverKind.make("codex"), - status: "running", - runtimeMode: "approval-required", - threadId, - createdAt, - updatedAt: createdAt, - activeTurnId: oldTurnId, - }); - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-steered-over"), - provider: ProviderDriverKind.make("codex"), - createdAt, - threadId, - turnId: oldTurnId, - }); - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && thread.session?.activeTurnId === oldTurnId, - 2_000, - threadId, - ); - - // The steer: a user-requested turn start while the old turn still runs. - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-steer"), - threadId, - message: { - messageId: asMessageId("msg-steer"), - role: "user", - text: "actually, do 15 instead", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt, - }), - ); - - // The provider session tracks the new turn before emitting turn.started - // (sendTurn updates the session first). - harness.setProviderSession({ - provider: ProviderDriverKind.make("codex"), - status: "running", - runtimeMode: "approval-required", - threadId, - createdAt, - updatedAt: createdAt, - activeTurnId: newTurnId, - }); - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-from-steer"), - provider: ProviderDriverKind.make("codex"), - createdAt, - threadId, - turnId: newTurnId, - }); - - const threadAfterSteer = await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && thread.session?.activeTurnId === newTurnId, - 2_000, - threadId, - ); - expect(threadAfterSteer.session?.activeTurnId).toBe(newTurnId); - expect(threadAfterSteer.latestTurn?.turnId).toBe(newTurnId); - expect(threadAfterSteer.latestTurn?.state).toBe("running"); - }); - - it("does not mark the source proposed plan implemented for an unrelated turn.started when no thread active turn is tracked", async () => { - const harness = await createHarness(); - const sourceThreadId = asThreadId("thread-plan"); - const targetThreadId = asThreadId("thread-implement"); - const sourceTurnId = asTurnId("turn-plan-source"); - const expectedTurnId = asTurnId("turn-plan-implement"); - const replayedTurnId = asTurnId("turn-replayed"); - const createdAt = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create-plan-source-unrelated"), - threadId: sourceThreadId, - projectId: asProjectId("project-1"), - title: "Plan Source", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: "plan", - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-plan-source-unrelated"), - threadId: sourceThreadId, - session: { - threadId: sourceThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: createdAt, - lastError: null, - }, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.make("cmd-thread-create-plan-target-unrelated"), - threadId: targetThreadId, - projectId: asProjectId("project-1"), - title: "Plan Target", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.make("cmd-session-set-plan-target-unrelated"), - threadId: targetThreadId, - session: { - threadId: targetThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: createdAt, - lastError: null, - }, - createdAt, - }), - ); - - harness.emit({ - type: "turn.proposed.completed", - eventId: asEventId("evt-plan-source-completed-unrelated"), - provider: ProviderDriverKind.make("codex"), - createdAt, - threadId: sourceThreadId, - turnId: sourceTurnId, - payload: { - planMarkdown: "# Source plan", - }, - }); - - const sourceThreadWithPlan = await waitForThread( - harness.readModel, - (thread) => - thread.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === "plan:thread-plan:turn:turn-plan-source" && - proposedPlan.implementedAt === null, - ), - 2_000, - sourceThreadId, - ); - const sourcePlan = sourceThreadWithPlan.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => - entry.id === "plan:thread-plan:turn:turn-plan-source", - ); - expect(sourcePlan).toBeDefined(); - if (!sourcePlan) { - throw new Error("Expected source plan to exist."); - } - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-plan-target-unrelated"), - threadId: targetThreadId, - message: { - messageId: asMessageId("msg-plan-target-unrelated"), - role: "user", - text: "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", - attachments: [], - }, - sourceProposedPlan: { - threadId: sourceThreadId, - planId: sourcePlan.id, - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: "2026-01-01T00:00:00.000Z", - }), - ); - - harness.setProviderSession({ - provider: ProviderDriverKind.make("codex"), - status: "running", - runtimeMode: "approval-required", - threadId: targetThreadId, - createdAt, - updatedAt: createdAt, - activeTurnId: expectedTurnId, - }); - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-unrelated-plan-implementation"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: targetThreadId, - turnId: replayedTurnId, - }); - - await harness.drain(); - - const readModel = await harness.readModel(); - const sourceThreadAfterUnrelatedStart = readModel.threads.find( - (entry) => entry.id === sourceThreadId, - ); - expect( - sourceThreadAfterUnrelatedStart?.proposedPlans.find((entry) => entry.id === sourcePlan.id), - ).toMatchObject({ - implementedAt: null, - implementationThreadId: null, - }); - }); - - it("finalizes buffered proposed-plan deltas into a first-class proposed plan on turn completion", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-plan-buffer"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-plan-buffer"), - }); - - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && thread.session?.activeTurnId === "turn-plan-buffer", - ); - - harness.emit({ - type: "turn.proposed.delta", - eventId: asEventId("evt-plan-delta-1"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-plan-buffer"), - payload: { - delta: "## Buffered plan\n\n- first", - }, - }); - harness.emit({ - type: "turn.proposed.delta", - eventId: asEventId("evt-plan-delta-2"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-plan-buffer"), - payload: { - delta: "\n- second", - }, - }); - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed-plan-buffer"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-plan-buffer"), - payload: { - state: "completed", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === "plan:thread-1:turn:turn-plan-buffer", - ), - ); - const proposedPlan = thread.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => - entry.id === "plan:thread-1:turn:turn-plan-buffer", - ); - expect(proposedPlan?.planMarkdown).toBe("## Buffered plan\n\n- first\n- second"); - }); - - it("buffers assistant deltas by default until completion", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-buffered"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered"), - }); - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && thread.session?.activeTurnId === "turn-buffered", - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-buffered"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered"), - itemId: asItemId("item-buffered"), - payload: { - streamKind: "assistant_text", - delta: "buffer me", - }, - }); - - await harness.drain(); - const midReadModel = await harness.readModel(); - const midThread = midReadModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect( - midThread?.messages.some( - (message: ProviderRuntimeTestMessage) => message.id === "assistant:item-buffered", - ), - ).toBe(false); - - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-message-completed-buffered"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered"), - itemId: asItemId("item-buffered"), - payload: { - itemType: "assistant_message", - status: "completed", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-buffered" && !message.streaming, - ), - ); - const message = thread.messages.find( - (entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-buffered", - ); - expect(message?.text).toBe("buffer me"); - expect(message?.streaming).toBe(false); - }); - - it("flushes and completes buffered assistant text when an approval request opens", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-buffered-request-flush"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-request-flush"), - }); - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-buffered-request-flush", - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-buffered-request-flush"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-request-flush"), - itemId: asItemId("item-buffered-request-flush"), - payload: { - streamKind: "assistant_text", - delta: "visible before approval", - }, - }); - harness.emit({ - type: "request.opened", - eventId: asEventId("evt-request-opened-buffered-request-flush"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-request-flush"), - requestId: ApprovalRequestId.make("req-buffered-request-flush"), - payload: { - requestType: "command_execution_approval", - detail: "pwd", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-buffered-request-flush" && - !message.streaming && - message.text === "visible before approval", - ), - ); - const message = thread.messages.find( - (entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-buffered-request-flush", - ); - expect(message?.streaming).toBe(false); - }); - - it("flushes and completes buffered assistant text when user input is requested", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-buffered-user-input-flush"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-user-input-flush"), - }); - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-buffered-user-input-flush", - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-buffered-user-input-flush"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-user-input-flush"), - itemId: asItemId("item-buffered-user-input-flush"), - payload: { - streamKind: "assistant_text", - delta: "visible before user input", - }, - }); - harness.emit({ - type: "user-input.requested", - eventId: asEventId("evt-user-input-requested-buffered-user-input-flush"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-user-input-flush"), - requestId: ApprovalRequestId.make("req-buffered-user-input-flush"), - payload: { - questions: [ - { - id: "choice", - header: "Choice", - question: "Pick one", - options: [{ label: "A", description: "Option A" }], - }, - ], - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-buffered-user-input-flush" && - !message.streaming && - message.text === "visible before user input", - ), - ); - const message = thread.messages.find( - (entry: ProviderRuntimeTestMessage) => - entry.id === "assistant:item-buffered-user-input-flush", - ); - expect(message?.streaming).toBe(false); - }); - - it("does not create assistant segments for whitespace-only buffered text at approval boundaries", async () => { - const harness = await createHarness(); - const startedAt = "2026-03-28T06:28:00.000Z"; - const pausedAt = "2026-03-28T06:28:01.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-buffered-whitespace-request"), - provider: ProviderDriverKind.make("codex"), - createdAt: startedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-whitespace-request"), - }); - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-buffered-whitespace-request", - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-buffered-whitespace-request"), - provider: ProviderDriverKind.make("codex"), - createdAt: startedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-whitespace-request"), - itemId: asItemId("item-buffered-whitespace-request"), - payload: { - streamKind: "assistant_text", - delta: "\n\n\n", - }, - }); - harness.emit({ - type: "request.opened", - eventId: asEventId("evt-request-opened-buffered-whitespace-request"), - provider: ProviderDriverKind.make("codex"), - createdAt: pausedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-whitespace-request"), - requestId: ApprovalRequestId.make("req-buffered-whitespace-request"), - payload: { - requestType: "command_execution_approval", - detail: "pwd", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "approval.requested", - ), - ); - expect( - thread.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-buffered-whitespace-request", - ), - ).toBe(false); - }); - - it("starts a new buffered assistant message segment after approval and completes without duplication", async () => { - const harness = await createHarness(); - const startedAt = "2026-03-28T06:07:00.000Z"; - const pausedAt = "2026-03-28T06:07:01.000Z"; - const resumedAt = "2026-03-28T06:07:02.000Z"; - const completedAt = "2026-03-28T06:07:03.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-buffered-request-append"), - provider: ProviderDriverKind.make("codex"), - createdAt: startedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-request-append"), - }); - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-buffered-request-append", - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-buffered-request-append-initial"), - provider: ProviderDriverKind.make("codex"), - createdAt: startedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-request-append"), - itemId: asItemId("item-buffered-request-append"), - payload: { - streamKind: "assistant_text", - delta: "first half", - }, - }); - harness.emit({ - type: "request.opened", - eventId: asEventId("evt-request-opened-buffered-request-append"), - provider: ProviderDriverKind.make("codex"), - createdAt: pausedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-request-append"), - requestId: ApprovalRequestId.make("req-buffered-request-append"), - payload: { - requestType: "command_execution_approval", - detail: "pwd", - }, - }); - - await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-buffered-request-append" && - !message.streaming && - message.text === "first half", - ), - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-buffered-request-append-followup"), - provider: ProviderDriverKind.make("codex"), - createdAt: resumedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-request-append"), - itemId: asItemId("item-buffered-request-append"), - payload: { - streamKind: "assistant_text", - delta: " second half", - }, - }); - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-message-completed-buffered-request-append"), - provider: ProviderDriverKind.make("codex"), - createdAt: completedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffered-request-append"), - itemId: asItemId("item-buffered-request-append"), - payload: { - itemType: "assistant_message", - status: "completed", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-buffered-request-append:segment:1" && - !message.streaming && - message.text === " second half", - ), - ); - const firstMessage = thread.messages.find( - (entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-buffered-request-append", - ); - const resumedMessage = thread.messages.find( - (entry: ProviderRuntimeTestMessage) => - entry.id === "assistant:item-buffered-request-append:segment:1", - ); - expect(firstMessage?.text).toBe("first half"); - expect(firstMessage?.streaming).toBe(false); - expect(resumedMessage?.text).toBe(" second half"); - expect(resumedMessage?.streaming).toBe(false); - - const events = await Effect.runPromise( - Stream.runCollect(harness.engine.readEvents(0)).pipe( - Effect.map((chunk) => Array.from(chunk)), - ), - ); - const assistantEvents = events.filter( - (event): event is Extract<(typeof events)[number], { type: "thread.message-sent" }> => - event.type === "thread.message-sent" && - event.payload.messageId.startsWith("assistant:item-buffered-request-append"), - ); - expect(assistantEvents).toHaveLength(4); - expect(assistantEvents[0]?.payload.streaming).toBe(true); - expect(assistantEvents[0]?.payload.text).toBe("first half"); - expect(assistantEvents[1]?.payload.streaming).toBe(false); - expect(assistantEvents[1]?.payload.text).toBe(""); - expect(assistantEvents[2]?.payload.messageId).toBe( - "assistant:item-buffered-request-append:segment:1", - ); - expect(assistantEvents[2]?.payload.streaming).toBe(true); - expect(assistantEvents[2]?.payload.text).toBe(" second half"); - expect(assistantEvents[3]?.payload.messageId).toBe( - "assistant:item-buffered-request-append:segment:1", - ); - expect(assistantEvents[3]?.payload.streaming).toBe(false); - expect(assistantEvents[3]?.payload.text).toBe(""); - }); - - it("starts a new streaming assistant message segment after approval", async () => { - const harness = await createHarness({ serverSettings: { enableAssistantStreaming: true } }); - const startedAt = "2026-03-28T07:00:00.000Z"; - const pausedAt = "2026-03-28T07:00:01.000Z"; - const resumedAt = "2026-03-28T07:00:02.000Z"; - const completedAt = "2026-03-28T07:00:03.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-streaming-request-segment"), - provider: ProviderDriverKind.make("codex"), - createdAt: startedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-streaming-request-segment"), - }); - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-streaming-request-segment", - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-streaming-request-segment-initial"), - provider: ProviderDriverKind.make("codex"), - createdAt: startedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-streaming-request-segment"), - itemId: asItemId("item-streaming-request-segment"), - payload: { - streamKind: "assistant_text", - delta: "before approval", - }, - }); - harness.emit({ - type: "request.opened", - eventId: asEventId("evt-request-opened-streaming-request-segment"), - provider: ProviderDriverKind.make("codex"), - createdAt: pausedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-streaming-request-segment"), - requestId: ApprovalRequestId.make("req-streaming-request-segment"), - payload: { - requestType: "command_execution_approval", - detail: "pwd", - }, - }); - - await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-streaming-request-segment" && - !message.streaming && - message.text === "before approval", - ), - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-streaming-request-segment-followup"), - provider: ProviderDriverKind.make("codex"), - createdAt: resumedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-streaming-request-segment"), - itemId: asItemId("item-streaming-request-segment"), - payload: { - streamKind: "assistant_text", - delta: " after approval", - }, - }); - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-message-completed-streaming-request-segment"), - provider: ProviderDriverKind.make("codex"), - createdAt: completedAt, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-streaming-request-segment"), - itemId: asItemId("item-streaming-request-segment"), - payload: { - itemType: "assistant_message", - status: "completed", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-streaming-request-segment:segment:1" && - !message.streaming && - message.text === " after approval", - ), - ); - expect( - thread.messages.find( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-streaming-request-segment", - )?.text, - ).toBe("before approval"); - expect( - thread.messages.find( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-streaming-request-segment:segment:1", - )?.text, - ).toBe(" after approval"); - }); - - it("streams assistant deltas when thread.turn.start requests streaming mode", async () => { - const harness = await createHarness({ serverSettings: { enableAssistantStreaming: true } }); - const now = "2026-01-01T00:00:00.000Z"; - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start-streaming-mode"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("message-streaming-mode"), - role: "user", - text: "stream please", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - await harness.drain(); - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-streaming-mode"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-streaming-mode"), - }); - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-streaming-mode", - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-streaming-mode"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-streaming-mode"), - itemId: asItemId("item-streaming-mode"), - payload: { - streamKind: "assistant_text", - delta: "hello live", - }, - }); - - const liveThread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-streaming-mode" && - message.streaming && - message.text === "hello live", - ), - ); - const liveMessage = liveThread.messages.find( - (entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-streaming-mode", - ); - expect(liveMessage?.streaming).toBe(true); - - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-message-completed-streaming-mode"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-streaming-mode"), - itemId: asItemId("item-streaming-mode"), - payload: { - itemType: "assistant_message", - status: "completed", - detail: "hello live", - }, - }); - - const finalThread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-streaming-mode" && !message.streaming, - ), - ); - const finalMessage = finalThread.messages.find( - (entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-streaming-mode", - ); - expect(finalMessage?.text).toBe("hello live"); - expect(finalMessage?.streaming).toBe(false); - }); - - it("spills oversized buffered deltas and still finalizes full assistant text", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - const oversizedText = "x".repeat(40_000); - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-buffer-spill"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffer-spill"), - }); - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-buffer-spill", - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-buffer-spill"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffer-spill"), - itemId: asItemId("item-buffer-spill"), - payload: { - streamKind: "assistant_text", - delta: oversizedText, - }, - }); - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-message-completed-buffer-spill"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-buffer-spill"), - itemId: asItemId("item-buffer-spill"), - payload: { - itemType: "assistant_message", - status: "completed", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-buffer-spill" && !message.streaming, - ), - ); - const message = thread.messages.find( - (entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-buffer-spill", - ); - expect(message?.text.length).toBe(oversizedText.length); - expect(message?.text).toBe(oversizedText); - expect(message?.streaming).toBe(false); - }); - - it("does not duplicate assistant completion when item.completed is followed by turn.completed", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-for-complete-dedup"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-complete-dedup"), - }); - - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-complete-dedup", - ); - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-message-delta-for-complete-dedup"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-complete-dedup"), - itemId: asItemId("item-complete-dedup"), - payload: { - streamKind: "assistant_text", - delta: "done", - }, - }); - harness.emit({ - type: "item.completed", - eventId: asEventId("evt-message-completed-for-complete-dedup"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-complete-dedup"), - itemId: asItemId("item-complete-dedup"), - payload: { - itemType: "assistant_message", - status: "completed", - }, - }); - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed-for-complete-dedup"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-complete-dedup"), - payload: { - state: "completed", - }, - }); - - await waitForThread( - harness.readModel, - (thread) => - thread.session?.status === "ready" && - thread.session?.activeTurnId === null && - thread.messages.some( - (message: ProviderRuntimeTestMessage) => - message.id === "assistant:item-complete-dedup" && !message.streaming, - ), - ); - - const events = await Effect.runPromise( - Stream.runCollect(harness.engine.readEvents(0)).pipe( - Effect.map((chunk) => Array.from(chunk)), - ), - ); - const completionEvents = events.filter((event) => { - if (event.type !== "thread.message-sent") { - return false; - } - return ( - event.payload.messageId === "assistant:item-complete-dedup" && - event.payload.streaming === false - ); - }); - expect(completionEvents).toHaveLength(1); - }); - - it("maps canonical request events into approval activities with requestKind", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "request.opened", - eventId: asEventId("evt-request-opened"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - requestId: ApprovalRequestId.make("req-open"), - payload: { - requestType: "command_execution_approval", - detail: "pwd", - }, - }); - - harness.emit({ - type: "request.resolved", - eventId: asEventId("evt-request-resolved"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - requestId: ApprovalRequestId.make("req-open"), - payload: { - requestType: "command_execution_approval", - decision: "accept", - }, - }); - - await waitForThread( - harness.readModel, - (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "approval.requested", - ) && - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "approval.resolved", - ), - ); - - const readModel = await harness.readModel(); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); - expect(thread).toBeDefined(); - - const requested = thread?.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-request-opened", - ); - const requestedPayload = - requested?.payload && typeof requested.payload === "object" - ? (requested.payload as Record) - : undefined; - expect(requestedPayload?.requestKind).toBe("command"); - expect(requestedPayload?.requestType).toBe("command_execution_approval"); - - const resolved = thread?.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-request-resolved", - ); - const resolvedPayload = - resolved?.payload && typeof resolved.payload === "object" - ? (resolved.payload as Record) - : undefined; - expect(resolvedPayload?.requestKind).toBe("command"); - expect(resolvedPayload?.requestType).toBe("command_execution_approval"); - }); - - it("maps runtime.error into errored session state", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "runtime.error", - eventId: asEventId("evt-runtime-error"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-3"), - payload: { - message: "runtime exploded", - }, - }); - - const thread = await waitForThread( - harness.readModel, - (entry) => - entry.session?.status === "error" && - entry.session?.activeTurnId === "turn-3" && - entry.session?.lastError === "runtime exploded", - ); - expect(thread.session?.status).toBe("error"); - expect(thread.session?.lastError).toBe("runtime exploded"); - }); - - it("records runtime.error activities from the typed payload message", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "runtime.error", - eventId: asEventId("evt-runtime-error-activity"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-runtime-error-activity"), - payload: { - message: "runtime activity exploded", - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some((activity) => activity.id === "evt-runtime-error-activity"), - ); - const activity = thread.activities.find( - (entry: ProviderRuntimeTestActivity) => entry.id === "evt-runtime-error-activity", - ); - const activityPayload = - activity?.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : undefined; - - expect(activity?.kind).toBe("runtime.error"); - expect(activityPayload?.message).toBe("runtime activity exploded"); - }); - - it("keeps the session running when a runtime.warning arrives during an active turn", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-warning-turn-started"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-warning"), - payload: {}, - }); - - harness.emit({ - type: "runtime.warning", - eventId: asEventId("evt-warning-runtime"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-warning"), - payload: { - message: "Reconnecting... 2/5", - detail: { - willRetry: true, - }, - }, - }); - - const thread = await waitForThread( - harness.readModel, - (entry) => - entry.session?.status === "running" && - entry.session?.activeTurnId === "turn-warning" && - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => - activity.id === "evt-warning-runtime" && activity.kind === "runtime.warning", - ), - ); - expect(thread.session?.status).toBe("running"); - expect(thread.session?.activeTurnId).toBe("turn-warning"); - expect(thread.session?.lastError).toBeNull(); - }); - - it("maps session/thread lifecycle and item.started into session/activity projections", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "session.started", - eventId: asEventId("evt-session-started"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - message: "session started", - }); - harness.emit({ - type: "thread.started", - eventId: asEventId("evt-thread-started"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - }); - harness.emit({ - type: "item.started", - eventId: asEventId("evt-tool-started"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-9"), - payload: { - itemType: "command_execution", - status: "in_progress", - title: "Read file", - detail: "/tmp/file.ts", - }, - }); - - const thread = await waitForThread( - harness.readModel, - (entry) => - entry.session?.status === "ready" && - entry.session?.activeTurnId === null && - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "tool.started", - ), - ); - - expect(thread.session?.status).toBe("ready"); - expect( - thread.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "tool.started", - ), - ).toBe(true); - }); - - it("consumes P1 runtime events into thread metadata, diff checkpoints, and activities", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "thread.metadata.updated", - eventId: asEventId("evt-thread-metadata-updated"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - payload: { - name: "Renamed by provider", - metadata: { source: "provider" }, - }, - }); - - harness.emit({ - type: "turn.plan.updated", - eventId: asEventId("evt-turn-plan-updated"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-p1"), - payload: { - explanation: "Working through the plan", - plan: [ - { step: "Inspect files", status: "completed" }, - { step: "Apply patch", status: "in_progress" }, - ], - }, - }); - - harness.emit({ - type: "item.updated", - eventId: asEventId("evt-item-updated"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-p1"), - itemId: asItemId("item-p1-tool"), - payload: { - itemType: "command_execution", - status: "in_progress", - title: "Run tests", - detail: "bun test", - data: { pid: 123 }, - }, - }); - - harness.emit({ - type: "runtime.warning", - eventId: asEventId("evt-runtime-warning"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-p1"), - payload: { - message: "Provider got slow", - detail: { latencyMs: 1500 }, - }, - }); - - harness.emit({ - type: "turn.diff.updated", - eventId: asEventId("evt-turn-diff-updated"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-p1"), - itemId: asItemId("item-p1-assistant"), - payload: { - unifiedDiff: "diff --git a/file.txt b/file.txt\n+hello\n", - }, - }); - - const thread = await waitForThread( - harness.readModel, - (entry) => - entry.title === "Renamed by provider" && - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "turn.plan.updated", - ) && - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "tool.updated", - ) && - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "runtime.warning", - ) && - entry.checkpoints.some( - (checkpoint: ProviderRuntimeTestCheckpoint) => checkpoint.turnId === "turn-p1", - ), - ); - - expect(thread.title).toBe("Renamed by provider"); - - const planActivity = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-turn-plan-updated", - ); - const planPayload = - planActivity?.payload && typeof planActivity.payload === "object" - ? (planActivity.payload as Record) - : undefined; - expect(planActivity?.kind).toBe("turn.plan.updated"); - expect(Array.isArray(planPayload?.plan)).toBe(true); - - const toolUpdate = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-item-updated", - ); - const toolUpdatePayload = - toolUpdate?.payload && typeof toolUpdate.payload === "object" - ? (toolUpdate.payload as Record) - : undefined; - expect(toolUpdate?.kind).toBe("tool.updated"); - expect(toolUpdatePayload?.itemType).toBe("command_execution"); - expect(toolUpdatePayload?.status).toBe("in_progress"); - - const warning = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-runtime-warning", - ); - const warningPayload = - warning?.payload && typeof warning.payload === "object" - ? (warning.payload as Record) - : undefined; - expect(warning?.kind).toBe("runtime.warning"); - expect(warningPayload?.message).toBe("Provider got slow"); - - const checkpoint = thread.checkpoints.find( - (entry: ProviderRuntimeTestCheckpoint) => entry.turnId === "turn-p1", - ); - expect(checkpoint?.status).toBe("missing"); - expect(checkpoint?.assistantMessageId).toBe("assistant:item-p1-assistant"); - expect(checkpoint?.checkpointRef).toBe("provider-diff:evt-turn-diff-updated"); - }); - - it("projects context window updates into normalized thread activities", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "thread.token-usage.updated", - eventId: asEventId("evt-thread-token-usage-updated"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - payload: { - usage: { - usedTokens: 1075, - totalProcessedTokens: 10_200, - maxTokens: 128_000, - inputTokens: 1000, - cachedInputTokens: 500, - outputTokens: 50, - reasoningOutputTokens: 25, - lastUsedTokens: 1075, - lastInputTokens: 1000, - lastCachedInputTokens: 500, - lastOutputTokens: 50, - lastReasoningOutputTokens: 25, - compactsAutomatically: true, - }, - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "context-window.updated", - ), - ); - - const usageActivity = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.kind === "context-window.updated", - ); - expect(usageActivity).toBeDefined(); - expect(usageActivity?.payload).toMatchObject({ - usedTokens: 1075, - totalProcessedTokens: 10_200, - maxTokens: 128_000, - inputTokens: 1000, - cachedInputTokens: 500, - outputTokens: 50, - reasoningOutputTokens: 25, - lastUsedTokens: 1075, - compactsAutomatically: true, - }); - }); - - it("projects Codex camelCase token usage payloads into normalized thread activities", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "thread.token-usage.updated", - eventId: asEventId("evt-thread-token-usage-updated-camel"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - payload: { - usage: { - usedTokens: 126, - totalProcessedTokens: 11_839, - maxTokens: 258_400, - inputTokens: 120, - cachedInputTokens: 0, - outputTokens: 6, - reasoningOutputTokens: 0, - lastUsedTokens: 126, - lastInputTokens: 120, - lastCachedInputTokens: 0, - lastOutputTokens: 6, - lastReasoningOutputTokens: 0, - compactsAutomatically: true, - }, - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "context-window.updated", - ), - ); - - const usageActivity = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.kind === "context-window.updated", - ); - expect(usageActivity?.payload).toMatchObject({ - usedTokens: 126, - totalProcessedTokens: 11_839, - maxTokens: 258_400, - inputTokens: 120, - cachedInputTokens: 0, - outputTokens: 6, - reasoningOutputTokens: 0, - lastUsedTokens: 126, - lastInputTokens: 120, - lastOutputTokens: 6, - compactsAutomatically: true, - }); - }); - - it("projects Claude usage snapshots with context window into normalized thread activities", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "thread.token-usage.updated", - eventId: asEventId("evt-thread-token-usage-updated-claude-window"), - provider: ProviderDriverKind.make("claudeAgent"), - createdAt: now, - threadId: asThreadId("thread-1"), - payload: { - usage: { - usedTokens: 31_251, - lastUsedTokens: 31_251, - maxTokens: 200_000, - toolUses: 25, - durationMs: 43_567, - }, - }, - raw: { - source: "claude.sdk.message", - method: "claude/result/success", - payload: {}, - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "context-window.updated", - ), - ); - - const usageActivity = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.kind === "context-window.updated", - ); - expect(usageActivity?.payload).toMatchObject({ - usedTokens: 31_251, - lastUsedTokens: 31_251, - maxTokens: 200_000, - toolUses: 25, - durationMs: 43_567, - }); - }); - - it("projects compacted thread state into context compaction activities", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "thread.state.changed", - eventId: asEventId("evt-thread-compacted"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-1"), - payload: { - state: "compacted", - detail: { source: "provider" }, - }, - }); - - const thread = await waitForThread(harness.readModel, (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "context-compaction", - ), - ); - - const activity = thread.activities.find( - (candidate: ProviderRuntimeTestActivity) => candidate.kind === "context-compaction", - ); - expect(activity?.summary).toBe("Context compacted"); - expect(activity?.tone).toBe("info"); - }); - - it("projects Codex task lifecycle chunks into thread activities", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "task.started", - eventId: asEventId("evt-task-started"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-task-1"), - payload: { - taskId: "turn-task-1", - taskType: "plan", - }, - }); - - harness.emit({ - type: "task.progress", - eventId: asEventId("evt-task-progress"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-task-1"), - payload: { - taskId: "turn-task-1", - description: "Comparing the desktop rollout chunks to the app-server stream.", - summary: "Code reviewer is validating the desktop rollout chunks.", - }, - }); - - harness.emit({ - type: "task.completed", - eventId: asEventId("evt-task-completed"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-task-1"), - payload: { - taskId: "turn-task-1", - status: "completed", - summary: "\n# Plan title\n", - }, - }); - harness.emit({ - type: "turn.proposed.completed", - eventId: asEventId("evt-task-proposed-plan-completed"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-task-1"), - payload: { - planMarkdown: "# Plan title", - }, - }); - - const thread = await waitForThread( - harness.readModel, - (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "task.completed", - ) && - entry.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === "plan:thread-1:turn:turn-task-1", - ), - ); - - const started = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-task-started", - ); - const progress = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-task-progress", - ); - const completed = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-task-completed", - ); - - const progressPayload = - progress?.payload && typeof progress.payload === "object" - ? (progress.payload as Record) - : undefined; - const completedPayload = - completed?.payload && typeof completed.payload === "object" - ? (completed.payload as Record) - : undefined; - - expect(started?.kind).toBe("task.started"); - expect(started?.summary).toBe("Plan task started"); - expect(progress?.kind).toBe("task.progress"); - expect(progressPayload?.detail).toBe("Code reviewer is validating the desktop rollout chunks."); - expect(progressPayload?.summary).toBe( - "Code reviewer is validating the desktop rollout chunks.", - ); - expect(completed?.kind).toBe("task.completed"); - expect(completedPayload?.detail).toBe("\n# Plan title\n"); - expect( - thread.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => entry.id === "plan:thread-1:turn:turn-task-1", - )?.planMarkdown, - ).toBe("# Plan title"); - }); - - it("projects structured user input request and resolution as thread activities", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "user-input.requested", - eventId: asEventId("evt-user-input-requested"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-user-input"), - requestId: ApprovalRequestId.make("req-user-input-1"), - payload: { - questions: [ - { - id: "sandbox_mode", - header: "Sandbox", - question: "Which mode should be used?", - options: [ - { - label: "workspace-write", - description: "Allow workspace writes only", - }, - ], - }, - ], - }, - }); - - harness.emit({ - type: "user-input.resolved", - eventId: asEventId("evt-user-input-resolved"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-user-input"), - requestId: ApprovalRequestId.make("req-user-input-1"), - payload: { - answers: { - sandbox_mode: "workspace-write", - }, - }, - }); - - const thread = await waitForThread( - harness.readModel, - (entry) => - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "user-input.requested", - ) && - entry.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "user-input.resolved", - ), - ); - - const requested = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-user-input-requested", - ); - expect(requested?.kind).toBe("user-input.requested"); - - const resolved = thread.activities.find( - (activity: ProviderRuntimeTestActivity) => activity.id === "evt-user-input-resolved", - ); - const resolvedPayload = - resolved?.payload && typeof resolved.payload === "object" - ? (resolved.payload as Record) - : undefined; - expect(resolved?.kind).toBe("user-input.resolved"); - expect(resolvedPayload?.answers).toEqual({ - sandbox_mode: "workspace-write", - }); - }); - - it("continues processing runtime events after a single event handler failure", async () => { - const harness = await createHarness(); - const now = "2026-01-01T00:00:00.000Z"; - - harness.emit({ - type: "content.delta", - eventId: asEventId("evt-invalid-delta"), - provider: ProviderDriverKind.make("codex"), - createdAt: now, - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-invalid"), - itemId: asItemId("item-invalid"), - payload: { - streamKind: "assistant_text", - delta: undefined, - }, - } as unknown as ProviderRuntimeEvent); - - harness.emit({ - type: "runtime.error", - eventId: asEventId("evt-runtime-error-after-failure"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-after-failure"), - payload: { - message: "runtime still processed", - }, - }); - - const thread = await waitForThread( - harness.readModel, - (entry) => - entry.session?.status === "error" && - entry.session?.activeTurnId === "turn-after-failure" && - entry.session?.lastError === "runtime still processed", - ); - expect(thread.session?.status).toBe("error"); - expect(thread.session?.lastError).toBe("runtime still processed"); - }); -}); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts deleted file mode 100644 index 3e5978f4846..00000000000 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ /dev/null @@ -1,1721 +0,0 @@ -import { - ApprovalRequestId, - type AssistantDeliveryMode, - CommandId, - MessageId, - type OrchestrationEvent, - type OrchestrationMessage, - type OrchestrationProposedPlanId, - CheckpointRef, - isToolLifecycleItemType, - ThreadId, - type ThreadTokenUsageSnapshot, - TurnId, - type OrchestrationCheckpointSummary, - type OrchestrationProposedPlan, - type OrchestrationThread, - type OrchestrationThreadActivity, - type ProviderRuntimeEvent, -} from "@t3tools/contracts"; -import * as Cache from "effect/Cache"; -import * as Cause from "effect/Cause"; -import * as Crypto from "effect/Crypto"; -import * as Duration from "effect/Duration"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Stream from "effect/Stream"; -import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; - -import { ProviderService } from "../../provider/Services/ProviderService.ts"; -import { ProjectionTurnRepository } from "../../persistence/Services/ProjectionTurns.ts"; -import { ProjectionTurnRepositoryLive } from "../../persistence/Layers/ProjectionTurns.ts"; -import { isGitRepository } from "../../git/Utils.ts"; -import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; -import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; -import { - ProviderRuntimeIngestionService, - type ProviderRuntimeIngestionShape, -} from "../Services/ProviderRuntimeIngestion.ts"; -import { ServerSettingsService } from "../../serverSettings.ts"; - -const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`; - -interface AssistantSegmentState { - baseKey: string; - nextSegmentIndex: number; - activeMessageId: MessageId | null; -} - -const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; -const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); -const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000; -const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120); -const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000; -const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120); -const MAX_BUFFERED_ASSISTANT_CHARS = 24_000; -const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0"; - -type TurnStartRequestedDomainEvent = Extract< - OrchestrationEvent, - { type: "thread.turn-start-requested" } ->; - -type RuntimeIngestionInput = - | { - source: "runtime"; - event: ProviderRuntimeEvent; - } - | { - source: "domain"; - event: TurnStartRequestedDomainEvent; - }; - -function toTurnId(value: TurnId | string | undefined): TurnId | undefined { - return value === undefined ? undefined : TurnId.make(String(value)); -} - -function toApprovalRequestId(value: string | undefined): ApprovalRequestId | undefined { - return value === undefined ? undefined : ApprovalRequestId.make(value); -} - -function sameId(left: string | null | undefined, right: string | null | undefined): boolean { - if (left === null || left === undefined || right === null || right === undefined) { - return false; - } - return left === right; -} - -function hasAssistantMessageForTurn( - messages: ReadonlyArray, - turnId: TurnId, - options?: { readonly streamingOnly?: boolean }, -): boolean { - for (let index = 0; index < messages.length; index += 1) { - const message = messages[index]; - if (!message) { - continue; - } - if (message.role !== "assistant" || message.turnId !== turnId) { - continue; - } - if (options?.streamingOnly === true && !message.streaming) { - continue; - } - return true; - } - return false; -} - -function findMessageById( - messages: ReadonlyArray, - messageId: MessageId, -): OrchestrationMessage | undefined { - for (let index = 0; index < messages.length; index += 1) { - const message = messages[index]; - if (message?.id === messageId) { - return message; - } - } - return undefined; -} - -function findProposedPlanById( - proposedPlans: ReadonlyArray< - Pick - >, - planId: string, -): - | Pick - | undefined { - for (let index = 0; index < proposedPlans.length; index += 1) { - const proposedPlan = proposedPlans[index]; - if (proposedPlan?.id === planId) { - return proposedPlan; - } - } - return undefined; -} - -function hasCheckpointForTurn( - checkpoints: ReadonlyArray, - turnId: TurnId, -): boolean { - for (let index = 0; index < checkpoints.length; index += 1) { - if (checkpoints[index]?.turnId === turnId) { - return true; - } - } - return false; -} - -function maxCheckpointTurnCount( - checkpoints: ReadonlyArray, -): number { - let maxTurnCount = 0; - for (let index = 0; index < checkpoints.length; index += 1) { - const checkpoint = checkpoints[index]; - if (checkpoint && checkpoint.checkpointTurnCount > maxTurnCount) { - maxTurnCount = checkpoint.checkpointTurnCount; - } - } - return maxTurnCount; -} - -function truncateDetail(value: string, limit = 180): string { - return value.length > limit ? `${value.slice(0, limit - 3)}...` : value; -} - -function normalizeProposedPlanMarkdown(planMarkdown: string | undefined): string | undefined { - const trimmed = planMarkdown?.trim(); - if (!trimmed) { - return undefined; - } - return trimmed; -} - -function hasRenderableAssistantText(text: string | undefined): boolean { - return (text?.trim().length ?? 0) > 0; -} - -function proposedPlanIdForTurn(threadId: ThreadId, turnId: TurnId): string { - return `plan:${threadId}:turn:${turnId}`; -} - -function proposedPlanIdFromEvent(event: ProviderRuntimeEvent, threadId: ThreadId): string { - const turnId = toTurnId(event.turnId); - if (turnId) { - return proposedPlanIdForTurn(threadId, turnId); - } - if (event.itemId) { - return `plan:${threadId}:item:${event.itemId}`; - } - return `plan:${threadId}:event:${event.eventId}`; -} - -function assistantSegmentBaseKeyFromEvent(event: ProviderRuntimeEvent): string { - return String(event.itemId ?? event.turnId ?? event.eventId); -} - -function assistantSegmentMessageId(baseKey: string, segmentIndex: number): MessageId { - return MessageId.make( - segmentIndex === 0 ? `assistant:${baseKey}` : `assistant:${baseKey}:segment:${segmentIndex}`, - ); -} -function buildContextWindowActivityPayload( - event: ProviderRuntimeEvent, -): ThreadTokenUsageSnapshot | undefined { - if (event.type !== "thread.token-usage.updated" || event.payload.usage.usedTokens <= 0) { - return undefined; - } - return event.payload.usage; -} - -function normalizeRuntimeTurnState( - value: string | undefined, -): "completed" | "failed" | "interrupted" | "cancelled" { - switch (value) { - case "failed": - case "interrupted": - case "cancelled": - case "completed": - return value; - default: - return "completed"; - } -} - -function orchestrationSessionStatusFromRuntimeState( - state: "starting" | "running" | "waiting" | "ready" | "interrupted" | "stopped" | "error", -): "starting" | "running" | "ready" | "interrupted" | "stopped" | "error" { - switch (state) { - case "starting": - return "starting"; - case "running": - case "waiting": - return "running"; - case "ready": - return "ready"; - case "interrupted": - return "interrupted"; - case "stopped": - return "stopped"; - case "error": - return "error"; - } -} - -function requestKindFromCanonicalRequestType( - requestType: string | undefined, -): "command" | "file-read" | "file-change" | undefined { - switch (requestType) { - case "command_execution_approval": - case "exec_command_approval": - return "command"; - case "file_read_approval": - return "file-read"; - case "file_change_approval": - case "apply_patch_approval": - return "file-change"; - default: - return undefined; - } -} - -function runtimeEventToActivities( - event: ProviderRuntimeEvent, -): ReadonlyArray { - const maybeSequence = (() => { - const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number }; - return eventWithSequence.sessionSequence !== undefined - ? { sequence: eventWithSequence.sessionSequence } - : {}; - })(); - switch (event.type) { - case "request.opened": { - if (event.payload.requestType === "tool_user_input") { - return []; - } - const requestKind = requestKindFromCanonicalRequestType(event.payload.requestType); - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "approval", - kind: "approval.requested", - summary: - requestKind === "command" - ? "Command approval requested" - : requestKind === "file-read" - ? "File-read approval requested" - : requestKind === "file-change" - ? "File-change approval requested" - : "Approval requested", - payload: { - requestId: toApprovalRequestId(event.requestId), - ...(requestKind ? { requestKind } : {}), - requestType: event.payload.requestType, - ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "request.resolved": { - if (event.payload.requestType === "tool_user_input") { - return []; - } - const requestKind = requestKindFromCanonicalRequestType(event.payload.requestType); - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "approval", - kind: "approval.resolved", - summary: "Approval resolved", - payload: { - requestId: toApprovalRequestId(event.requestId), - ...(requestKind ? { requestKind } : {}), - requestType: event.payload.requestType, - ...(event.payload.decision ? { decision: event.payload.decision } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "runtime.error": { - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "error", - kind: "runtime.error", - summary: "Runtime error", - payload: { - message: truncateDetail(event.payload.message), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "tool.denied": { - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "error", - kind: "tool.denied", - summary: `Tool denied: ${event.payload.toolName}`, - payload: { - toolName: event.payload.toolName, - ...(event.payload.toolUseId ? { toolUseId: event.payload.toolUseId } : {}), - ...(event.payload.reason ? { detail: truncateDetail(event.payload.reason) } : {}), - ...(event.payload.agentId ? { agentId: event.payload.agentId } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "runtime.warning": { - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "info", - kind: "runtime.warning", - // Use the adapter-supplied message as the row label so the work log - // shows what the warning was about, not a generic "Runtime warning". - summary: truncateDetail(event.payload.message, 120), - payload: { - message: truncateDetail(event.payload.message), - ...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "turn.plan.updated": { - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "info", - kind: "turn.plan.updated", - summary: "Plan updated", - payload: { - plan: event.payload.plan, - ...(event.payload.explanation !== undefined - ? { explanation: event.payload.explanation } - : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "user-input.requested": { - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "info", - kind: "user-input.requested", - summary: "User input requested", - payload: { - ...(event.requestId ? { requestId: event.requestId } : {}), - questions: event.payload.questions, - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "user-input.resolved": { - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "info", - kind: "user-input.resolved", - summary: "User input submitted", - payload: { - ...(event.requestId ? { requestId: event.requestId } : {}), - answers: event.payload.answers, - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "task.started": { - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "info", - kind: "task.started", - summary: - event.payload.taskType === "plan" - ? "Plan task started" - : event.payload.taskType - ? `${event.payload.taskType} task started` - : "Task started", - payload: { - taskId: event.payload.taskId, - ...(event.payload.taskType ? { taskType: event.payload.taskType } : {}), - ...(event.payload.description - ? { detail: truncateDetail(event.payload.description) } - : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "task.progress": { - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "info", - kind: "task.progress", - summary: "Reasoning update", - payload: { - taskId: event.payload.taskId, - detail: truncateDetail(event.payload.summary ?? event.payload.description), - ...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}), - ...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}), - ...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "task.completed": { - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: event.payload.status === "failed" ? "error" : "info", - kind: "task.completed", - summary: - event.payload.status === "failed" - ? "Task failed" - : event.payload.status === "stopped" - ? "Task stopped" - : "Task completed", - payload: { - taskId: event.payload.taskId, - status: event.payload.status, - ...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}), - ...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "thread.state.changed": { - if (event.payload.state !== "compacted") { - return []; - } - - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "info", - kind: "context-compaction", - summary: "Context compacted", - payload: { - state: event.payload.state, - ...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "thread.token-usage.updated": { - const payload = buildContextWindowActivityPayload(event); - if (!payload) { - return []; - } - - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "info", - kind: "context-window.updated", - summary: "Context window updated", - payload, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "item.updated": { - if (!isToolLifecycleItemType(event.payload.itemType)) { - return []; - } - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "tool", - kind: "tool.updated", - summary: event.payload.title ?? "Tool updated", - payload: { - itemType: event.payload.itemType, - ...(event.payload.status ? { status: event.payload.status } : {}), - ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), - ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "item.completed": { - if (!isToolLifecycleItemType(event.payload.itemType)) { - return []; - } - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "tool", - kind: "tool.completed", - summary: event.payload.title ?? "Tool", - payload: { - itemType: event.payload.itemType, - ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), - ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - case "item.started": { - if (!isToolLifecycleItemType(event.payload.itemType)) { - return []; - } - return [ - { - id: event.eventId, - createdAt: event.createdAt, - tone: "tool", - kind: "tool.started", - summary: `${event.payload.title ?? "Tool"} started`, - payload: { - itemType: event.payload.itemType, - ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), - }, - turnId: toTurnId(event.turnId) ?? null, - ...maybeSequence, - }, - ]; - } - - default: - break; - } - - return []; -} - -const make = Effect.gen(function* () { - const crypto = yield* Crypto.Crypto; - const orchestrationEngine = yield* OrchestrationEngineService; - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; - const providerService = yield* ProviderService; - const projectionTurnRepository = yield* ProjectionTurnRepository; - const serverSettingsService = yield* ServerSettingsService; - const providerCommandId = (event: ProviderRuntimeEvent, tag: string) => - crypto.randomUUIDv4.pipe( - Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)), - ); - - const turnMessageIdsByTurnKey = yield* Cache.make>({ - capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY, - timeToLive: TURN_MESSAGE_IDS_BY_TURN_TTL, - lookup: () => Effect.succeed(new Set()), - }); - - const bufferedAssistantTextByMessageId = yield* Cache.make({ - capacity: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY, - timeToLive: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL, - lookup: () => Effect.succeed(""), - }); - - const assistantSegmentStateByTurnKey = yield* Cache.make({ - capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY, - timeToLive: TURN_MESSAGE_IDS_BY_TURN_TTL, - lookup: () => - Effect.die( - new Error("assistant segment state should be read through getOption before initialization"), - ), - }); - - const bufferedProposedPlanById = yield* Cache.make({ - capacity: BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY, - timeToLive: BUFFERED_PROPOSED_PLAN_BY_ID_TTL, - lookup: () => Effect.succeed({ text: "", createdAt: "" }), - }); - - const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) { - return yield* projectionSnapshotQuery - .getThreadDetailById(threadId) - .pipe(Effect.map(Option.getOrUndefined)); - }); - - const resolveThreadShell = Effect.fn("resolveThreadShell")(function* (threadId: ThreadId) { - return yield* projectionSnapshotQuery - .getThreadShellById(threadId) - .pipe(Effect.map(Option.getOrUndefined)); - }); - - const rememberAssistantMessageId = (threadId: ThreadId, turnId: TurnId, messageId: MessageId) => - Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe( - Effect.flatMap((existingIds) => - Cache.set( - turnMessageIdsByTurnKey, - providerTurnKey(threadId, turnId), - Option.match(existingIds, { - onNone: () => new Set([messageId]), - onSome: (ids) => { - const nextIds = new Set(ids); - nextIds.add(messageId); - return nextIds; - }, - }), - ), - ), - ); - - const forgetAssistantMessageId = (threadId: ThreadId, turnId: TurnId, messageId: MessageId) => - Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe( - Effect.flatMap((existingIds) => - Option.match(existingIds, { - onNone: () => Effect.void, - onSome: (ids) => { - const nextIds = new Set(ids); - nextIds.delete(messageId); - if (nextIds.size === 0) { - return Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)); - } - return Cache.set(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), nextIds); - }, - }), - ), - ); - - const getAssistantMessageIdsForTurn = (threadId: ThreadId, turnId: TurnId) => - Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe( - Effect.map((existingIds) => - Option.getOrElse(existingIds, (): Set => new Set()), - ), - ); - - const clearAssistantMessageIdsForTurn = (threadId: ThreadId, turnId: TurnId) => - Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)); - - const getAssistantSegmentStateForTurn = (threadId: ThreadId, turnId: TurnId) => - Cache.getOption(assistantSegmentStateByTurnKey, providerTurnKey(threadId, turnId)); - - const setAssistantSegmentStateForTurn = ( - threadId: ThreadId, - turnId: TurnId, - state: AssistantSegmentState, - ) => Cache.set(assistantSegmentStateByTurnKey, providerTurnKey(threadId, turnId), state); - - const clearAssistantSegmentStateForTurn = (threadId: ThreadId, turnId: TurnId) => - Cache.invalidate(assistantSegmentStateByTurnKey, providerTurnKey(threadId, turnId)); - - const getActiveAssistantMessageIdForTurn = (threadId: ThreadId, turnId: TurnId) => - getAssistantSegmentStateForTurn(threadId, turnId).pipe( - Effect.map((state) => - Option.flatMap(state, (entry) => - entry.activeMessageId ? Option.some(entry.activeMessageId) : Option.none(), - ), - ), - ); - - const startAssistantSegmentForTurn = (input: { - threadId: ThreadId; - turnId: TurnId; - baseKey: string; - }) => - getAssistantSegmentStateForTurn(input.threadId, input.turnId).pipe( - Effect.flatMap((existingState) => - Effect.gen(function* () { - const nextState = Option.match(existingState, { - onNone: () => ({ - baseKey: input.baseKey, - nextSegmentIndex: 1, - activeMessageId: assistantSegmentMessageId(input.baseKey, 0), - }), - onSome: (state) => { - const segmentIndex = state.baseKey === input.baseKey ? state.nextSegmentIndex : 0; - const messageId = assistantSegmentMessageId(input.baseKey, segmentIndex); - return { - baseKey: input.baseKey, - nextSegmentIndex: state.baseKey === input.baseKey ? state.nextSegmentIndex + 1 : 1, - activeMessageId: messageId, - } satisfies AssistantSegmentState; - }, - }); - yield* setAssistantSegmentStateForTurn(input.threadId, input.turnId, nextState); - return nextState.activeMessageId!; - }), - ), - ); - - const getOrCreateAssistantMessageId = (input: { - threadId: ThreadId; - event: ProviderRuntimeEvent; - turnId?: TurnId; - }) => - Effect.gen(function* () { - if (!input.turnId) { - return assistantSegmentMessageId(assistantSegmentBaseKeyFromEvent(input.event), 0); - } - - const activeMessageId = yield* getActiveAssistantMessageIdForTurn( - input.threadId, - input.turnId, - ); - if (Option.isSome(activeMessageId)) { - return activeMessageId.value; - } - - return yield* startAssistantSegmentForTurn({ - threadId: input.threadId, - turnId: input.turnId, - baseKey: assistantSegmentBaseKeyFromEvent(input.event), - }); - }); - - const appendBufferedAssistantText = (messageId: MessageId, delta: string) => - Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( - Effect.flatMap((existingText) => - Effect.gen(function* () { - const nextText = Option.match(existingText, { - onNone: () => delta, - onSome: (text) => `${text}${delta}`, - }); - if (nextText.length <= MAX_BUFFERED_ASSISTANT_CHARS) { - yield* Cache.set(bufferedAssistantTextByMessageId, messageId, nextText); - return ""; - } - - // Safety valve: flush full buffered text as an assistant delta to cap memory. - yield* Cache.invalidate(bufferedAssistantTextByMessageId, messageId); - return nextText; - }), - ), - ); - - const takeBufferedAssistantText = (messageId: MessageId) => - Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( - Effect.flatMap((existingText) => - Cache.invalidate(bufferedAssistantTextByMessageId, messageId).pipe( - Effect.as(Option.getOrElse(existingText, () => "")), - ), - ), - ); - - const clearBufferedAssistantText = (messageId: MessageId) => - Cache.invalidate(bufferedAssistantTextByMessageId, messageId); - - const appendBufferedProposedPlan = (planId: string, delta: string, createdAt: string) => - Cache.getOption(bufferedProposedPlanById, planId).pipe( - Effect.flatMap((existingEntry) => { - const existing = Option.getOrUndefined(existingEntry); - return Cache.set(bufferedProposedPlanById, planId, { - text: `${existing?.text ?? ""}${delta}`, - createdAt: - existing?.createdAt && existing.createdAt.length > 0 ? existing.createdAt : createdAt, - }); - }), - ); - - const takeBufferedProposedPlan = (planId: string) => - Cache.getOption(bufferedProposedPlanById, planId).pipe( - Effect.flatMap((existingEntry) => - Cache.invalidate(bufferedProposedPlanById, planId).pipe( - Effect.as(Option.getOrUndefined(existingEntry)), - ), - ), - ); - - const clearBufferedProposedPlan = (planId: string) => - Cache.invalidate(bufferedProposedPlanById, planId); - - const clearAssistantMessageState = (messageId: MessageId) => - clearBufferedAssistantText(messageId); - - const flushBufferedAssistantMessage = (input: { - event: ProviderRuntimeEvent; - threadId: ThreadId; - messageId: MessageId; - turnId?: TurnId; - createdAt: string; - commandTag: string; - }) => - Effect.gen(function* () { - const bufferedText = yield* takeBufferedAssistantText(input.messageId); - if (!hasRenderableAssistantText(bufferedText)) { - return false; - } - - yield* orchestrationEngine.dispatch({ - type: "thread.message.assistant.delta", - commandId: yield* providerCommandId(input.event, input.commandTag), - threadId: input.threadId, - messageId: input.messageId, - delta: bufferedText, - ...(input.turnId ? { turnId: input.turnId } : {}), - createdAt: input.createdAt, - }); - return true; - }); - - const flushBufferedAssistantMessagesForTurn = (input: { - event: ProviderRuntimeEvent; - threadId: ThreadId; - turnId: TurnId; - createdAt: string; - commandTag: string; - }) => - Effect.gen(function* () { - const assistantMessageIds = yield* getAssistantMessageIdsForTurn( - input.threadId, - input.turnId, - ); - const flushedMessageIds = new Set(); - yield* Effect.forEach( - assistantMessageIds, - (messageId) => - flushBufferedAssistantMessage({ - event: input.event, - threadId: input.threadId, - messageId, - turnId: input.turnId, - createdAt: input.createdAt, - commandTag: input.commandTag, - }).pipe( - Effect.tap((flushed) => - flushed ? Effect.sync(() => flushedMessageIds.add(messageId)) : Effect.void, - ), - ), - { concurrency: 1 }, - ).pipe(Effect.asVoid); - return flushedMessageIds; - }); - - const finalizeAssistantMessage = (input: { - event: ProviderRuntimeEvent; - threadId: ThreadId; - messageId: MessageId; - turnId?: TurnId; - createdAt: string; - commandTag: string; - finalDeltaCommandTag: string; - fallbackText?: string; - hasProjectedMessage?: boolean; - }) => - Effect.gen(function* () { - const bufferedText = yield* takeBufferedAssistantText(input.messageId); - const text = - bufferedText.length > 0 - ? bufferedText - : (input.fallbackText?.trim().length ?? 0) > 0 - ? input.fallbackText! - : ""; - const hasRenderableText = hasRenderableAssistantText(text); - - if (hasRenderableText) { - yield* orchestrationEngine.dispatch({ - type: "thread.message.assistant.delta", - commandId: yield* providerCommandId(input.event, input.finalDeltaCommandTag), - threadId: input.threadId, - messageId: input.messageId, - delta: text, - ...(input.turnId ? { turnId: input.turnId } : {}), - createdAt: input.createdAt, - }); - } - - if (input.hasProjectedMessage || hasRenderableText) { - yield* orchestrationEngine.dispatch({ - type: "thread.message.assistant.complete", - commandId: yield* providerCommandId(input.event, input.commandTag), - threadId: input.threadId, - messageId: input.messageId, - ...(input.turnId ? { turnId: input.turnId } : {}), - createdAt: input.createdAt, - }); - } - yield* clearAssistantMessageState(input.messageId); - }); - - const finalizeActiveAssistantSegmentForTurn = (input: { - event: ProviderRuntimeEvent; - threadId: ThreadId; - turnId: TurnId; - createdAt: string; - commandTag: string; - finalDeltaCommandTag: string; - hasProjectedMessage: boolean; - flushedMessageIds?: ReadonlySet; - }) => - Effect.gen(function* () { - const activeMessageId = yield* getActiveAssistantMessageIdForTurn( - input.threadId, - input.turnId, - ); - if (Option.isNone(activeMessageId)) { - return; - } - - yield* finalizeAssistantMessage({ - event: input.event, - threadId: input.threadId, - messageId: activeMessageId.value, - turnId: input.turnId, - createdAt: input.createdAt, - commandTag: input.commandTag, - finalDeltaCommandTag: input.finalDeltaCommandTag, - hasProjectedMessage: - input.hasProjectedMessage || - (input.flushedMessageIds?.has(activeMessageId.value) ?? false), - }); - yield* forgetAssistantMessageId(input.threadId, input.turnId, activeMessageId.value); - - const state = yield* getAssistantSegmentStateForTurn(input.threadId, input.turnId); - if (Option.isSome(state)) { - yield* setAssistantSegmentStateForTurn(input.threadId, input.turnId, { - ...state.value, - activeMessageId: null, - }); - } - }); - - const upsertProposedPlan = (input: { - event: ProviderRuntimeEvent; - threadId: ThreadId; - threadProposedPlans: ReadonlyArray<{ - id: string; - createdAt: string; - implementedAt: string | null; - implementationThreadId: ThreadId | null; - }>; - planId: string; - turnId?: TurnId; - planMarkdown: string | undefined; - createdAt: string; - updatedAt: string; - }) => - Effect.gen(function* () { - const planMarkdown = normalizeProposedPlanMarkdown(input.planMarkdown); - if (!planMarkdown) { - return; - } - - const existingPlan = findProposedPlanById(input.threadProposedPlans, input.planId); - yield* orchestrationEngine.dispatch({ - type: "thread.proposed-plan.upsert", - commandId: yield* providerCommandId(input.event, "proposed-plan-upsert"), - threadId: input.threadId, - proposedPlan: { - id: input.planId, - turnId: input.turnId ?? null, - planMarkdown, - implementedAt: existingPlan?.implementedAt ?? null, - implementationThreadId: existingPlan?.implementationThreadId ?? null, - createdAt: existingPlan?.createdAt ?? input.createdAt, - updatedAt: input.updatedAt, - }, - createdAt: input.updatedAt, - }); - }); - - const finalizeBufferedProposedPlan = (input: { - event: ProviderRuntimeEvent; - threadId: ThreadId; - threadProposedPlans: ReadonlyArray<{ - id: string; - createdAt: string; - implementedAt: string | null; - implementationThreadId: ThreadId | null; - }>; - planId: string; - turnId?: TurnId; - fallbackMarkdown?: string; - updatedAt: string; - }) => - Effect.gen(function* () { - const bufferedPlan = yield* takeBufferedProposedPlan(input.planId); - const bufferedMarkdown = normalizeProposedPlanMarkdown(bufferedPlan?.text); - const fallbackMarkdown = normalizeProposedPlanMarkdown(input.fallbackMarkdown); - const planMarkdown = bufferedMarkdown ?? fallbackMarkdown; - if (!planMarkdown) { - return; - } - - yield* upsertProposedPlan({ - event: input.event, - threadId: input.threadId, - threadProposedPlans: input.threadProposedPlans, - planId: input.planId, - ...(input.turnId ? { turnId: input.turnId } : {}), - planMarkdown, - createdAt: - bufferedPlan?.createdAt && bufferedPlan.createdAt.length > 0 - ? bufferedPlan.createdAt - : input.updatedAt, - updatedAt: input.updatedAt, - }); - yield* clearBufferedProposedPlan(input.planId); - }); - - const clearTurnStateForSession = (threadId: ThreadId) => - Effect.gen(function* () { - const prefix = `${threadId}:`; - const proposedPlanPrefix = `plan:${threadId}:`; - const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey)); - const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey)); - const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById)); - yield* Effect.forEach( - turnKeys, - (key) => - Effect.gen(function* () { - if (!key.startsWith(prefix)) { - return; - } - - const messageIds = yield* Cache.getOption(turnMessageIdsByTurnKey, key); - if (Option.isSome(messageIds)) { - yield* Effect.forEach(messageIds.value, clearAssistantMessageState, { - concurrency: 1, - }).pipe(Effect.asVoid); - } - - yield* Cache.invalidate(turnMessageIdsByTurnKey, key); - }), - { concurrency: 1 }, - ).pipe(Effect.asVoid); - yield* Effect.forEach( - assistantSegmentKeys, - (key) => - key.startsWith(prefix) - ? Cache.invalidate(assistantSegmentStateByTurnKey, key) - : Effect.void, - { concurrency: 1 }, - ).pipe(Effect.asVoid); - yield* Effect.forEach( - proposedPlanKeys, - (key) => - key.startsWith(proposedPlanPrefix) - ? Cache.invalidate(bufferedProposedPlanById, key) - : Effect.void, - { concurrency: 1 }, - ).pipe(Effect.asVoid); - }); - - const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn( - "getSourceProposedPlanReferenceForPendingTurnStart", - )(function* (threadId: ThreadId) { - const pendingTurnStart = yield* projectionTurnRepository.getPendingTurnStartByThreadId({ - threadId, - }); - if (Option.isNone(pendingTurnStart)) { - return null; - } - - const sourceThreadId = pendingTurnStart.value.sourceProposedPlanThreadId; - const sourcePlanId = pendingTurnStart.value.sourceProposedPlanId; - if (sourceThreadId === null || sourcePlanId === null) { - return null; - } - - return { - sourceThreadId, - sourcePlanId, - } as const; - }); - - const getExpectedProviderTurnIdForThread = Effect.fn("getExpectedProviderTurnIdForThread")( - function* (threadId: ThreadId) { - const sessions = yield* providerService.listSessions(); - const session = sessions.find((entry) => entry.threadId === threadId); - return session?.activeTurnId; - }, - ); - - const getSourceProposedPlanReferenceForAcceptedTurnStart = Effect.fn( - "getSourceProposedPlanReferenceForAcceptedTurnStart", - )(function* (threadId: ThreadId, eventTurnId: TurnId | undefined) { - if (eventTurnId === undefined) { - return null; - } - - const expectedTurnId = yield* getExpectedProviderTurnIdForThread(threadId); - if (!sameId(expectedTurnId, eventTurnId)) { - return null; - } - - return yield* getSourceProposedPlanReferenceForPendingTurnStart(threadId); - }); - - const markSourceProposedPlanImplemented = Effect.fn("markSourceProposedPlanImplemented")( - function* ( - sourceThreadId: ThreadId, - sourcePlanId: OrchestrationProposedPlanId, - implementationThreadId: ThreadId, - implementedAt: string, - ) { - const sourceThread = yield* resolveThreadDetail(sourceThreadId); - const sourcePlan = sourceThread?.proposedPlans.find((entry) => entry.id === sourcePlanId); - if (!sourceThread || !sourcePlan || sourcePlan.implementedAt !== null) { - return; - } - - const commandUuid = yield* crypto.randomUUIDv4; - yield* orchestrationEngine.dispatch({ - type: "thread.proposed-plan.upsert", - commandId: CommandId.make( - `provider:source-proposed-plan-implemented:${implementationThreadId}:${commandUuid}`, - ), - threadId: sourceThread.id, - proposedPlan: { - ...sourcePlan, - implementedAt, - implementationThreadId, - updatedAt: implementedAt, - }, - createdAt: implementedAt, - }); - }, - ); - - const processRuntimeEvent = (event: ProviderRuntimeEvent) => - Effect.gen(function* () { - const thread = yield* resolveThreadShell(event.threadId); - if (!thread) return; - - let loadedThreadDetail: OrchestrationThread | null | undefined; - const getLoadedThreadDetail = () => - Effect.gen(function* () { - if (loadedThreadDetail !== undefined) { - return loadedThreadDetail; - } - loadedThreadDetail = (yield* resolveThreadDetail(thread.id)) ?? null; - return loadedThreadDetail; - }); - - const now = event.createdAt; - const eventTurnId = toTurnId(event.turnId); - const activeTurnId = thread.session?.activeTurnId ?? null; - - const conflictsWithActiveTurn = - activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId); - const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined; - - // A turn.started that conflicts with the active turn is legitimate when - // the server itself has a turn start pending for this thread AND the - // provider session already tracks the event's turn as its active turn: - // steering a running turn makes some providers (e.g. opencode) open a - // new turn without ever completing the superseded one. A stale - // turn.started for some other turn id still gets rejected. - const conflictingTurnStartIsPendingTurnStart = - event.type === "turn.started" && conflictsWithActiveTurn - ? sameId(yield* getExpectedProviderTurnIdForThread(thread.id), eventTurnId) && - Option.isSome( - yield* projectionTurnRepository.getPendingTurnStartByThreadId({ - threadId: thread.id, - }), - ) - : false; - - const shouldApplyThreadLifecycle = (() => { - if (!STRICT_PROVIDER_LIFECYCLE_GUARD) { - return true; - } - switch (event.type) { - case "session.exited": - return true; - case "session.started": - case "thread.started": - return true; - case "turn.started": - return !conflictsWithActiveTurn || conflictingTurnStartIsPendingTurnStart; - case "turn.completed": - if (conflictsWithActiveTurn || missingTurnForActiveTurn) { - return false; - } - // Only the active turn may close the lifecycle state. - if (activeTurnId !== null && eventTurnId !== undefined) { - return sameId(activeTurnId, eventTurnId); - } - // If no active turn is tracked, accept completion scoped to this thread. - return true; - default: - return true; - } - })(); - const acceptedTurnStartedSourcePlan = - event.type === "turn.started" && shouldApplyThreadLifecycle - ? yield* getSourceProposedPlanReferenceForAcceptedTurnStart(thread.id, eventTurnId) - : null; - - if ( - event.type === "session.started" || - event.type === "session.state.changed" || - event.type === "session.exited" || - event.type === "thread.started" || - event.type === "turn.started" || - event.type === "turn.completed" - ) { - const nextActiveTurnId = - event.type === "turn.started" - ? (eventTurnId ?? null) - : event.type === "turn.completed" || event.type === "session.exited" - ? null - : activeTurnId; - const status = (() => { - switch (event.type) { - case "session.state.changed": - return orchestrationSessionStatusFromRuntimeState(event.payload.state); - case "turn.started": - return "running"; - case "session.exited": - return "stopped"; - case "turn.completed": - return normalizeRuntimeTurnState(event.payload.state) === "failed" - ? "error" - : "ready"; - case "session.started": - case "thread.started": - // Provider thread/session start notifications can arrive during an - // active turn; preserve turn-running state in that case. - return activeTurnId !== null ? "running" : "ready"; - } - })(); - const lastError = - event.type === "session.state.changed" && event.payload.state === "error" - ? (event.payload.reason ?? thread.session?.lastError ?? "Provider session error") - : event.type === "turn.completed" && - normalizeRuntimeTurnState(event.payload.state) === "failed" - ? (event.payload.errorMessage ?? thread.session?.lastError ?? "Turn failed") - : status === "ready" - ? null - : (thread.session?.lastError ?? null); - - if (shouldApplyThreadLifecycle) { - if (event.type === "turn.started" && acceptedTurnStartedSourcePlan !== null) { - yield* markSourceProposedPlanImplemented( - acceptedTurnStartedSourcePlan.sourceThreadId, - acceptedTurnStartedSourcePlan.sourcePlanId, - thread.id, - now, - ).pipe( - Effect.catchCause((cause) => - Effect.logWarning( - "provider runtime ingestion failed to mark source proposed plan", - { - eventId: event.eventId, - eventType: event.type, - cause: Cause.pretty(cause), - }, - ), - ), - ); - } - - yield* orchestrationEngine.dispatch({ - type: "thread.session.set", - commandId: yield* providerCommandId(event, "thread-session-set"), - threadId: thread.id, - session: { - threadId: thread.id, - status, - providerName: event.provider, - ...(event.providerInstanceId !== undefined - ? { providerInstanceId: event.providerInstanceId } - : {}), - runtimeMode: thread.session?.runtimeMode ?? "full-access", - activeTurnId: nextActiveTurnId, - lastError, - updatedAt: now, - }, - createdAt: now, - }); - } - } - - const assistantDelta = - event.type === "content.delta" && event.payload.streamKind === "assistant_text" - ? event.payload.delta - : undefined; - const proposedPlanDelta = - event.type === "turn.proposed.delta" ? event.payload.delta : undefined; - - if (assistantDelta && assistantDelta.length > 0) { - const turnId = toTurnId(event.turnId); - const assistantMessageId = yield* getOrCreateAssistantMessageId({ - threadId: thread.id, - event, - ...(turnId ? { turnId } : {}), - }); - if (turnId) { - yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId); - } - - const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map( - serverSettingsService.getSettings, - (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"), - ); - if (assistantDeliveryMode === "buffered") { - const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta); - if (spillChunk.length > 0) { - yield* orchestrationEngine.dispatch({ - type: "thread.message.assistant.delta", - commandId: yield* providerCommandId(event, "assistant-delta-buffer-spill"), - threadId: thread.id, - messageId: assistantMessageId, - delta: spillChunk, - ...(turnId ? { turnId } : {}), - createdAt: now, - }); - } - } else { - yield* orchestrationEngine.dispatch({ - type: "thread.message.assistant.delta", - commandId: yield* providerCommandId(event, "assistant-delta"), - threadId: thread.id, - messageId: assistantMessageId, - delta: assistantDelta, - ...(turnId ? { turnId } : {}), - createdAt: now, - }); - } - } - - const pauseForUserTurnId = - event.type === "request.opened" || event.type === "user-input.requested" - ? toTurnId(event.turnId) - : undefined; - if (pauseForUserTurnId) { - const detailedThread = yield* getLoadedThreadDetail(); - const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map( - serverSettingsService.getSettings, - (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"), - ); - const flushedMessageIds = - assistantDeliveryMode === "buffered" - ? yield* flushBufferedAssistantMessagesForTurn({ - event, - threadId: thread.id, - turnId: pauseForUserTurnId, - createdAt: now, - commandTag: - event.type === "request.opened" - ? "assistant-delta-flush-on-request-opened" - : "assistant-delta-flush-on-user-input-requested", - }) - : new Set(); - yield* finalizeActiveAssistantSegmentForTurn({ - event, - threadId: thread.id, - turnId: pauseForUserTurnId, - createdAt: now, - commandTag: - event.type === "request.opened" - ? "assistant-complete-on-request-opened" - : "assistant-complete-on-user-input-requested", - finalDeltaCommandTag: - event.type === "request.opened" - ? "assistant-delta-finalize-on-request-opened" - : "assistant-delta-finalize-on-user-input-requested", - hasProjectedMessage: - detailedThread !== null && - hasAssistantMessageForTurn(detailedThread.messages, pauseForUserTurnId, { - streamingOnly: true, - }), - flushedMessageIds, - }); - } - - if (proposedPlanDelta && proposedPlanDelta.length > 0) { - const planId = proposedPlanIdFromEvent(event, thread.id); - yield* appendBufferedProposedPlan(planId, proposedPlanDelta, now); - } - - const assistantCompletion = - event.type === "item.completed" && event.payload.itemType === "assistant_message" - ? { - messageId: MessageId.make( - `assistant:${event.itemId ?? event.turnId ?? event.eventId}`, - ), - fallbackText: event.payload.detail, - } - : undefined; - const proposedPlanCompletion = - event.type === "turn.proposed.completed" - ? { - planId: proposedPlanIdFromEvent(event, thread.id), - turnId: toTurnId(event.turnId), - planMarkdown: event.payload.planMarkdown, - } - : undefined; - - if (assistantCompletion) { - const detailedThread = yield* getLoadedThreadDetail(); - const messages = detailedThread?.messages ?? []; - const turnId = toTurnId(event.turnId); - const activeAssistantMessageId = turnId - ? yield* getActiveAssistantMessageIdForTurn(thread.id, turnId) - : Option.none(); - const hasAssistantMessagesForTurn = - turnId !== undefined ? hasAssistantMessageForTurn(messages, turnId) : false; - const assistantMessageId = Option.getOrElse( - activeAssistantMessageId, - () => assistantCompletion.messageId, - ); - const existingAssistantMessage = findMessageById(messages, assistantMessageId); - const shouldApplyFallbackCompletionText = - !existingAssistantMessage || existingAssistantMessage.text.length === 0; - - const shouldSkipRedundantCompletion = - Option.isNone(activeAssistantMessageId) && - turnId !== undefined && - hasAssistantMessagesForTurn && - (assistantCompletion.fallbackText?.trim().length ?? 0) === 0; - - if (!shouldSkipRedundantCompletion) { - if (turnId && Option.isNone(activeAssistantMessageId)) { - yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId); - } - - yield* finalizeAssistantMessage({ - event, - threadId: thread.id, - messageId: assistantMessageId, - ...(turnId ? { turnId } : {}), - createdAt: now, - commandTag: "assistant-complete", - finalDeltaCommandTag: "assistant-delta-finalize", - hasProjectedMessage: existingAssistantMessage !== undefined, - ...(assistantCompletion.fallbackText !== undefined && shouldApplyFallbackCompletionText - ? { fallbackText: assistantCompletion.fallbackText } - : {}), - }); - - if (turnId) { - yield* forgetAssistantMessageId(thread.id, turnId, assistantMessageId); - } - } - - if (turnId) { - yield* clearAssistantSegmentStateForTurn(thread.id, turnId); - } - } - - if (proposedPlanCompletion) { - const detailedThread = yield* getLoadedThreadDetail(); - yield* finalizeBufferedProposedPlan({ - event, - threadId: thread.id, - threadProposedPlans: detailedThread?.proposedPlans ?? [], - planId: proposedPlanCompletion.planId, - ...(proposedPlanCompletion.turnId ? { turnId: proposedPlanCompletion.turnId } : {}), - fallbackMarkdown: proposedPlanCompletion.planMarkdown, - updatedAt: now, - }); - } - - if (event.type === "turn.completed") { - const detailedThread = yield* getLoadedThreadDetail(); - const messages = detailedThread?.messages ?? []; - const proposedPlans = detailedThread?.proposedPlans ?? []; - const turnId = toTurnId(event.turnId); - if (turnId) { - const assistantMessageIds = yield* getAssistantMessageIdsForTurn(thread.id, turnId); - yield* Effect.forEach( - assistantMessageIds, - (assistantMessageId) => - finalizeAssistantMessage({ - event, - threadId: thread.id, - messageId: assistantMessageId, - turnId, - createdAt: now, - commandTag: "assistant-complete-finalize", - finalDeltaCommandTag: "assistant-delta-finalize-fallback", - hasProjectedMessage: findMessageById(messages, assistantMessageId) !== undefined, - }), - { concurrency: 1 }, - ).pipe(Effect.asVoid); - yield* clearAssistantMessageIdsForTurn(thread.id, turnId); - yield* clearAssistantSegmentStateForTurn(thread.id, turnId); - - yield* finalizeBufferedProposedPlan({ - event, - threadId: thread.id, - threadProposedPlans: proposedPlans, - planId: proposedPlanIdForTurn(thread.id, turnId), - turnId, - updatedAt: now, - }); - } - } - - if (event.type === "session.exited") { - yield* clearTurnStateForSession(thread.id); - } - - if (event.type === "runtime.error") { - const runtimeErrorMessage = event.payload.message; - - const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD - ? true - : activeTurnId === null || eventTurnId === undefined || sameId(activeTurnId, eventTurnId); - - if (shouldApplyRuntimeError) { - yield* orchestrationEngine.dispatch({ - type: "thread.session.set", - commandId: yield* providerCommandId(event, "runtime-error-session-set"), - threadId: thread.id, - session: { - threadId: thread.id, - status: "error", - providerName: event.provider, - ...(event.providerInstanceId !== undefined - ? { providerInstanceId: event.providerInstanceId } - : {}), - runtimeMode: thread.session?.runtimeMode ?? "full-access", - activeTurnId: eventTurnId ?? null, - lastError: runtimeErrorMessage, - updatedAt: now, - }, - createdAt: now, - }); - } - } - - if (event.type === "thread.metadata.updated" && event.payload.name) { - yield* orchestrationEngine.dispatch({ - type: "thread.meta.update", - commandId: yield* providerCommandId(event, "thread-meta-update"), - threadId: thread.id, - title: event.payload.name, - }); - } - - if (event.type === "turn.diff.updated") { - const turnId = toTurnId(event.turnId); - const checkpointContext = turnId - ? yield* projectionSnapshotQuery - .getThreadCheckpointContext(thread.id) - .pipe(Effect.map(Option.getOrUndefined)) - : undefined; - const workspaceCwd = - checkpointContext?.worktreePath ?? checkpointContext?.workspaceRoot ?? undefined; - if (turnId && checkpointContext && workspaceCwd && isGitRepository(workspaceCwd)) { - // Skip if a checkpoint already exists for this turn. A real - // (non-placeholder) capture from CheckpointReactor should not - // be clobbered, and dispatching a duplicate placeholder for the - // same turnId would produce an unstable checkpointTurnCount. - if (hasCheckpointForTurn(checkpointContext.checkpoints, turnId)) { - // Already tracked; no-op. - } else { - const assistantMessageId = MessageId.make( - `assistant:${event.itemId ?? event.turnId ?? event.eventId}`, - ); - yield* orchestrationEngine.dispatch({ - type: "thread.turn.diff.complete", - commandId: yield* providerCommandId(event, "thread-turn-diff-complete"), - threadId: thread.id, - turnId, - completedAt: now, - checkpointRef: CheckpointRef.make(`provider-diff:${event.eventId}`), - status: "missing", - files: [], - assistantMessageId, - checkpointTurnCount: maxCheckpointTurnCount(checkpointContext.checkpoints) + 1, - createdAt: now, - }); - } - } - } - - const activities = runtimeEventToActivities(event); - yield* Effect.forEach(activities, (activity) => - providerCommandId(event, "thread-activity-append").pipe( - Effect.flatMap((commandId) => - orchestrationEngine.dispatch({ - type: "thread.activity.append", - commandId, - threadId: thread.id, - activity, - createdAt: activity.createdAt, - }), - ), - ), - ).pipe(Effect.asVoid); - }); - - const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void; - - const processInput = (input: RuntimeIngestionInput) => - input.source === "runtime" ? processRuntimeEvent(input.event) : processDomainEvent(input.event); - - const processInputSafely = (input: RuntimeIngestionInput) => - processInput(input).pipe( - Effect.catchCause((cause) => { - if (Cause.hasInterruptsOnly(cause)) { - return Effect.failCause(cause); - } - return Effect.logWarning("provider runtime ingestion failed to process event", { - source: input.source, - eventId: input.event.eventId, - eventType: input.event.type, - cause: Cause.pretty(cause), - }); - }), - ); - - const worker = yield* makeDrainableWorker(processInputSafely); - - const start: ProviderRuntimeIngestionShape["start"] = () => - Effect.gen(function* () { - yield* Effect.forkScoped( - Stream.runForEach(providerService.streamEvents, (event) => - worker.enqueue({ source: "runtime", event }), - ), - ); - yield* Effect.forkScoped( - Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { - if (event.type !== "thread.turn-start-requested") { - return Effect.void; - } - return worker.enqueue({ source: "domain", event }); - }), - ); - }); - - return { - start, - drain: worker.drain, - } satisfies ProviderRuntimeIngestionShape; -}); - -export const ProviderRuntimeIngestionLive = Layer.effect( - ProviderRuntimeIngestionService, - make, -).pipe(Layer.provide(ProjectionTurnRepositoryLive)); diff --git a/apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts b/apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts deleted file mode 100644 index 586281dc9c6..00000000000 --- a/apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * RuntimeReceiptBus layers. - * - * `RuntimeReceiptBusLive` is the production default and intentionally does not - * retain or broadcast receipts. `RuntimeReceiptBusTest` installs the in-memory - * PubSub-backed implementation used by integration tests that need to await - * checkpoint-reactor milestones precisely. - * - * @module RuntimeReceiptBus - */ -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as PubSub from "effect/PubSub"; -import * as Stream from "effect/Stream"; - -import { - RuntimeReceiptBus, - type RuntimeReceiptBusShape, - type OrchestrationRuntimeReceipt, -} from "../Services/RuntimeReceiptBus.ts"; - -const makeRuntimeReceiptBus = Effect.succeed({ - publish: () => Effect.void, - streamEventsForTest: Stream.empty, -} satisfies RuntimeReceiptBusShape); - -const makeRuntimeReceiptBusTest = Effect.gen(function* () { - const pubSub = yield* PubSub.unbounded(); - - return { - publish: (receipt) => PubSub.publish(pubSub, receipt).pipe(Effect.asVoid), - get streamEventsForTest() { - return Stream.fromPubSub(pubSub); - }, - } satisfies RuntimeReceiptBusShape; -}); - -export const RuntimeReceiptBusLive = Layer.effect(RuntimeReceiptBus, makeRuntimeReceiptBus); -export const RuntimeReceiptBusTest = Layer.effect(RuntimeReceiptBus, makeRuntimeReceiptBusTest); diff --git a/apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts b/apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts deleted file mode 100644 index 34b1b995a3a..00000000000 --- a/apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ThreadId } from "@t3tools/contracts"; -import * as Cause from "effect/Cause"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import { describe, expect, it } from "vite-plus/test"; - -import { logCleanupCauseUnlessInterrupted } from "./ThreadDeletionReactor.ts"; - -describe("logCleanupCauseUnlessInterrupted", () => { - const threadId = ThreadId.make("thread-deletion-reactor-test"); - - it("swallows ordinary cleanup failures", async () => { - const exit = await Effect.runPromiseExit( - logCleanupCauseUnlessInterrupted({ - effect: Effect.fail("cleanup failed"), - message: "thread deletion cleanup skipped provider session stop", - threadId, - }), - ); - - expect(Exit.isSuccess(exit)).toBe(true); - }); - - it("preserves interrupt causes", async () => { - const exit = await Effect.runPromiseExit( - logCleanupCauseUnlessInterrupted({ - effect: Effect.interrupt, - message: "thread deletion cleanup skipped provider session stop", - threadId, - }), - ); - - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true); - } - }); -}); diff --git a/apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts b/apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts deleted file mode 100644 index 7d8a24069a3..00000000000 --- a/apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { OrchestrationEvent } from "@t3tools/contracts"; -import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; -import * as Cause from "effect/Cause"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Stream from "effect/Stream"; - -import { ProviderService } from "../../provider/Services/ProviderService.ts"; -import * as TerminalManager from "../../terminal/Manager.ts"; -import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; -import { - ThreadDeletionReactor, - type ThreadDeletionReactorShape, -} from "../Services/ThreadDeletionReactor.ts"; - -type ThreadDeletedEvent = Extract; - -export const logCleanupCauseUnlessInterrupted = ({ - effect, - message, - threadId, -}: { - readonly effect: Effect.Effect; - readonly message: string; - readonly threadId: ThreadDeletedEvent["payload"]["threadId"]; -}): Effect.Effect => - effect.pipe( - Effect.catchCause((cause) => { - if (Cause.hasInterruptsOnly(cause)) { - return Effect.failCause(cause); - } - return Effect.logDebug(message, { - threadId, - cause: Cause.pretty(cause), - }); - }), - ); - -const make = Effect.gen(function* () { - const orchestrationEngine = yield* OrchestrationEngineService; - const providerService = yield* ProviderService; - const terminalManager = yield* TerminalManager.TerminalManager; - - const stopProviderSession = (threadId: ThreadDeletedEvent["payload"]["threadId"]) => - logCleanupCauseUnlessInterrupted({ - effect: providerService.stopSession({ threadId }), - message: "thread deletion cleanup skipped provider session stop", - threadId, - }); - - const closeThreadTerminals = (threadId: ThreadDeletedEvent["payload"]["threadId"]) => - logCleanupCauseUnlessInterrupted({ - effect: terminalManager.close({ threadId, deleteHistory: true }), - message: "thread deletion cleanup skipped terminal close", - threadId, - }); - - const processThreadDeleted = Effect.fn("processThreadDeleted")(function* ( - event: ThreadDeletedEvent, - ) { - const { threadId } = event.payload; - yield* stopProviderSession(threadId); - yield* closeThreadTerminals(threadId); - }); - - const processThreadDeletedSafely = (event: ThreadDeletedEvent) => - processThreadDeleted(event).pipe( - Effect.catchCause((cause) => { - if (Cause.hasInterruptsOnly(cause)) { - return Effect.failCause(cause); - } - return Effect.logWarning("thread deletion reactor failed to process event", { - eventType: event.type, - threadId: event.payload.threadId, - cause: Cause.pretty(cause), - }); - }), - ); - - const worker = yield* makeDrainableWorker(processThreadDeletedSafely); - - const start: ThreadDeletionReactorShape["start"] = Effect.fn("start")(function* () { - yield* Effect.forkScoped( - Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { - if (event.type !== "thread.deleted") { - return Effect.void; - } - return worker.enqueue(event); - }), - ); - }); - - return { - start, - drain: worker.drain, - } satisfies ThreadDeletionReactorShape; -}); - -export const ThreadDeletionReactorLive = Layer.effect(ThreadDeletionReactor, make); diff --git a/apps/server/src/orchestration/Normalizer.ts b/apps/server/src/orchestration/Normalizer.ts deleted file mode 100644 index bed166eba45..00000000000 --- a/apps/server/src/orchestration/Normalizer.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as Effect from "effect/Effect"; -import * as FileSystem from "effect/FileSystem"; -import * as Path from "effect/Path"; -import { - type ClientOrchestrationCommand, - type OrchestrationCommand, - OrchestrationDispatchCommandError, - PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, -} from "@t3tools/contracts"; - -import { createAttachmentId, resolveAttachmentPath } from "../attachmentStore.ts"; -import { ServerConfig } from "../config.ts"; -import { parseBase64DataUrl } from "../imageMime.ts"; -import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; - -export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const serverConfig = yield* ServerConfig; - const workspacePaths = yield* WorkspacePaths.WorkspacePaths; - - const normalizeProjectWorkspaceRoot = (workspaceRoot: string) => - workspacePaths.normalizeWorkspaceRoot(workspaceRoot).pipe( - Effect.mapError( - (cause) => - new OrchestrationDispatchCommandError({ - message: cause.message, - }), - ), - ); - - const normalizeProjectWorkspaceRootForCreate = ( - workspaceRoot: string, - createIfMissing: boolean | undefined, - ) => - workspacePaths - .normalizeWorkspaceRoot(workspaceRoot, { - createIfMissing: createIfMissing === true, - }) - .pipe( - Effect.mapError( - (cause) => - new OrchestrationDispatchCommandError({ - message: cause.message, - }), - ), - ); - - if (command.type === "project.create") { - return { - ...command, - workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate( - command.workspaceRoot, - command.createWorkspaceRootIfMissing, - ), - createWorkspaceRootIfMissing: command.createWorkspaceRootIfMissing === true, - } satisfies OrchestrationCommand; - } - - if (command.type === "project.meta.update" && command.workspaceRoot !== undefined) { - return { - ...command, - workspaceRoot: yield* normalizeProjectWorkspaceRoot(command.workspaceRoot), - } satisfies OrchestrationCommand; - } - - if (command.type !== "thread.turn.start") { - return command as OrchestrationCommand; - } - - const normalizedAttachments = yield* Effect.forEach( - command.message.attachments, - (attachment) => - Effect.gen(function* () { - const parsed = parseBase64DataUrl(attachment.dataUrl); - if (!parsed || !parsed.mimeType.startsWith("image/")) { - return yield* new OrchestrationDispatchCommandError({ - message: `Invalid image attachment payload for '${attachment.name}'.`, - }); - } - - const bytes = Buffer.from(parsed.base64, "base64"); - if (bytes.byteLength === 0 || bytes.byteLength > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) { - return yield* new OrchestrationDispatchCommandError({ - message: `Image attachment '${attachment.name}' is empty or too large.`, - }); - } - - const attachmentId = createAttachmentId(command.threadId); - if (!attachmentId) { - return yield* new OrchestrationDispatchCommandError({ - message: "Failed to create a safe attachment id.", - }); - } - - const persistedAttachment = { - type: "image" as const, - id: attachmentId, - name: attachment.name, - mimeType: parsed.mimeType.toLowerCase(), - sizeBytes: bytes.byteLength, - }; - - const attachmentPath = resolveAttachmentPath({ - attachmentsDir: serverConfig.attachmentsDir, - attachment: persistedAttachment, - }); - if (!attachmentPath) { - return yield* new OrchestrationDispatchCommandError({ - message: `Failed to resolve persisted path for '${attachment.name}'.`, - }); - } - - yield* fileSystem.makeDirectory(path.dirname(attachmentPath), { recursive: true }).pipe( - Effect.mapError( - () => - new OrchestrationDispatchCommandError({ - message: `Failed to create attachment directory for '${attachment.name}'.`, - }), - ), - ); - yield* fileSystem.writeFile(attachmentPath, bytes).pipe( - Effect.mapError( - () => - new OrchestrationDispatchCommandError({ - message: `Failed to persist attachment '${attachment.name}'.`, - }), - ), - ); - - return persistedAttachment; - }), - { concurrency: 1 }, - ); - - return { - ...command, - message: { - ...command.message, - attachments: normalizedAttachments, - }, - } satisfies OrchestrationCommand; - }); diff --git a/apps/server/src/orchestration/Services/CheckpointReactor.ts b/apps/server/src/orchestration/Services/CheckpointReactor.ts deleted file mode 100644 index bd3ee3e88f9..00000000000 --- a/apps/server/src/orchestration/Services/CheckpointReactor.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * CheckpointReactor - Checkpoint reaction service interface. - * - * Owns background workers that react to orchestration checkpoint lifecycle - * events and apply checkpoint side effects. - * - * @module CheckpointReactor - */ -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; -import type * as Scope from "effect/Scope"; - -/** - * CheckpointReactorShape - Service API for checkpoint reactor lifecycle. - */ -export interface CheckpointReactorShape { - /** - * Start the checkpoint reactor. - * - * The returned effect must be run in a scope so all worker fibers can be - * finalized on shutdown. - * - * Consumes both orchestration-domain and provider-runtime events via an - * internal queue. - */ - readonly start: () => Effect.Effect; - - /** - * Resolves when the internal processing queue is empty and idle. - * Intended for test use to replace timing-sensitive sleeps. - */ - readonly drain: Effect.Effect; -} - -/** - * CheckpointReactor - Service tag for checkpoint reactor workers. - */ -export class CheckpointReactor extends Context.Service()( - "t3/orchestration/Services/CheckpointReactor", -) {} diff --git a/apps/server/src/orchestration/Services/OrchestrationEngine.ts b/apps/server/src/orchestration/Services/OrchestrationEngine.ts index acb2b7b042d..728ab18ed77 100644 --- a/apps/server/src/orchestration/Services/OrchestrationEngine.ts +++ b/apps/server/src/orchestration/Services/OrchestrationEngine.ts @@ -1,16 +1,16 @@ /** - * OrchestrationEngineService - Service interface for orchestration command handling. + * Historical name for the application event-sourcing engine. * - * Owns command validation/dispatch and in-memory read-model updates backed by - * `OrchestrationEventStore` persistence. It does not own provider process - * management or transport concerns (e.g. websocket request parsing). + * This is not the agent orchestrator. It retains serialized project-command + * validation, append, receipt, and projection transactions. Agent execution is + * owned by orchestration V2, whose thread events use the same event store. * * Uses Effect `Context.Service` for dependency injection. Command dispatch, * replay, and unknown-input decoding all return typed domain errors. * * @module OrchestrationEngineService */ -import type { OrchestrationCommand, OrchestrationEvent } from "@t3tools/contracts"; +import type { OrchestrationEvent, ProjectOrchestrationCommand } from "@t3tools/contracts"; import * as Context from "effect/Context"; import type * as Effect from "effect/Effect"; import type * as Stream from "effect/Stream"; @@ -42,7 +42,7 @@ export interface OrchestrationEngineShape { * command receipts. */ readonly dispatch: ( - command: OrchestrationCommand, + command: ProjectOrchestrationCommand, ) => Effect.Effect<{ sequence: number }, OrchestrationDispatchError, never>; /** diff --git a/apps/server/src/orchestration/Services/OrchestrationReactor.ts b/apps/server/src/orchestration/Services/OrchestrationReactor.ts deleted file mode 100644 index eb2d95954bb..00000000000 --- a/apps/server/src/orchestration/Services/OrchestrationReactor.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * OrchestrationReactor - Composite orchestration reactor service interface. - * - * Coordinates startup of orchestration runtime reactors that translate domain - * events into downstream side effects. - * - * @module OrchestrationReactor - */ -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; -import type * as Scope from "effect/Scope"; - -/** - * OrchestrationReactorShape - Service API for orchestration reactor lifecycle. - */ -export interface OrchestrationReactorShape { - /** - * Start orchestration-side reactors for provider/runtime/checkpoint flows. - * - * The returned effect must be run in a scope so all worker fibers can be - * finalized on shutdown. - */ - readonly start: () => Effect.Effect; -} - -/** - * OrchestrationReactor - Service tag for orchestration reactor coordination. - */ -export class OrchestrationReactor extends Context.Service< - OrchestrationReactor, - OrchestrationReactorShape ->()("t3/orchestration/Services/OrchestrationReactor") {} diff --git a/apps/server/src/orchestration/Services/ProviderCommandReactor.ts b/apps/server/src/orchestration/Services/ProviderCommandReactor.ts deleted file mode 100644 index 65aa9949fe1..00000000000 --- a/apps/server/src/orchestration/Services/ProviderCommandReactor.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * ProviderCommandReactor - Provider command reaction service interface. - * - * Owns background workers that react to orchestration intent events and - * dispatch provider-side command execution. - * - * @module ProviderCommandReactor - */ -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; -import type * as Scope from "effect/Scope"; - -/** - * ProviderCommandReactorShape - Service API for provider command reactors. - */ -export interface ProviderCommandReactorShape { - /** - * Start reacting to provider-intent orchestration domain events. - * - * The returned effect must be run in a scope so all worker fibers can be - * finalized on shutdown. - * - * Filters orchestration domain events to provider-intent types before - * processing. - */ - readonly start: () => Effect.Effect; - - /** - * Resolves when the internal processing queue is empty and idle. - * Intended for test use to replace timing-sensitive sleeps. - */ - readonly drain: Effect.Effect; -} - -/** - * ProviderCommandReactor - Service tag for provider command reaction workers. - */ -export class ProviderCommandReactor extends Context.Service< - ProviderCommandReactor, - ProviderCommandReactorShape ->()("t3/orchestration/Services/ProviderCommandReactor") {} diff --git a/apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts deleted file mode 100644 index b6fa2711b94..00000000000 --- a/apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * ProviderRuntimeIngestionService - Provider runtime ingestion service interface. - * - * Owns background workers that consume provider runtime streams and emit - * orchestration commands/events. - * - * @module ProviderRuntimeIngestionService - */ -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; -import type * as Scope from "effect/Scope"; - -/** - * ProviderRuntimeIngestionShape - Service API for runtime ingestion lifecycle. - */ -export interface ProviderRuntimeIngestionShape { - /** - * Start ingesting provider runtime events into orchestration commands. - * - * The returned effect must be run in a scope so all worker fibers can be - * finalized on shutdown. - * - * Uses an internal queue and continues after non-interrupt failures by - * logging warnings. - */ - readonly start: () => Effect.Effect; - - /** - * Resolves when the internal processing queue is empty and idle. - * Intended for test use to replace timing-sensitive sleeps. - */ - readonly drain: Effect.Effect; -} - -/** - * ProviderRuntimeIngestionService - Service tag for runtime ingestion workers. - */ -export class ProviderRuntimeIngestionService extends Context.Service< - ProviderRuntimeIngestionService, - ProviderRuntimeIngestionShape ->()("t3/orchestration/Services/ProviderRuntimeIngestion/ProviderRuntimeIngestionService") {} diff --git a/apps/server/src/orchestration/Services/RuntimeReceiptBus.ts b/apps/server/src/orchestration/Services/RuntimeReceiptBus.ts deleted file mode 100644 index 0b880ee6999..00000000000 --- a/apps/server/src/orchestration/Services/RuntimeReceiptBus.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * RuntimeReceiptBus - Internal checkpoint-reactor synchronization receipts. - * - * This service exists to expose short-lived orchestration milestones that are - * useful in tests and harnesses but are not part of the production runtime - * event model. `CheckpointReactor` publishes receipts such as baseline capture, - * diff finalization, and turn-processing quiescence so integration tests can - * wait for those exact points without inferring them indirectly from persisted - * state. - * - * Production code should only call `publish`. Test code may subscribe via - * `streamEventsForTest`, which is intentionally named to make the intended - * usage explicit. - * - * @module RuntimeReceiptBus - */ -import { CheckpointRef, IsoDateTime, NonNegativeInt, ThreadId, TurnId } from "@t3tools/contracts"; -import * as Schema from "effect/Schema"; -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; -import type * as Stream from "effect/Stream"; - -export const CheckpointBaselineCapturedReceipt = Schema.Struct({ - type: Schema.Literal("checkpoint.baseline.captured"), - threadId: ThreadId, - checkpointTurnCount: NonNegativeInt, - checkpointRef: CheckpointRef, - createdAt: IsoDateTime, -}); -export type CheckpointBaselineCapturedReceipt = typeof CheckpointBaselineCapturedReceipt.Type; - -export const CheckpointDiffFinalizedReceipt = Schema.Struct({ - type: Schema.Literal("checkpoint.diff.finalized"), - threadId: ThreadId, - turnId: TurnId, - checkpointTurnCount: NonNegativeInt, - checkpointRef: CheckpointRef, - status: Schema.Literals(["ready", "missing", "error"]), - createdAt: IsoDateTime, -}); -export type CheckpointDiffFinalizedReceipt = typeof CheckpointDiffFinalizedReceipt.Type; - -export const TurnProcessingQuiescedReceipt = Schema.Struct({ - type: Schema.Literal("turn.processing.quiesced"), - threadId: ThreadId, - turnId: TurnId, - checkpointTurnCount: NonNegativeInt, - createdAt: IsoDateTime, -}); -export type TurnProcessingQuiescedReceipt = typeof TurnProcessingQuiescedReceipt.Type; - -export const OrchestrationRuntimeReceipt = Schema.Union([ - CheckpointBaselineCapturedReceipt, - CheckpointDiffFinalizedReceipt, - TurnProcessingQuiescedReceipt, -]); -export type OrchestrationRuntimeReceipt = typeof OrchestrationRuntimeReceipt.Type; - -export interface RuntimeReceiptBusShape { - readonly publish: (receipt: OrchestrationRuntimeReceipt) => Effect.Effect; - readonly streamEventsForTest: Stream.Stream; -} - -export class RuntimeReceiptBus extends Context.Service()( - "t3/orchestration/Services/RuntimeReceiptBus", -) {} diff --git a/apps/server/src/orchestration/Services/ThreadDeletionReactor.ts b/apps/server/src/orchestration/Services/ThreadDeletionReactor.ts deleted file mode 100644 index 7c6718965a6..00000000000 --- a/apps/server/src/orchestration/Services/ThreadDeletionReactor.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * ThreadDeletionReactor - Thread deletion cleanup reactor service interface. - * - * Owns background workers that react to thread deletion domain events and - * perform best-effort runtime cleanup for provider sessions and terminals. - * - * @module ThreadDeletionReactor - */ -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; -import type * as Scope from "effect/Scope"; - -/** - * ThreadDeletionReactorShape - Service API for thread deletion cleanup. - */ -export interface ThreadDeletionReactorShape { - /** - * Start reacting to thread.deleted orchestration domain events. - * - * The returned effect must be run in a scope so all worker fibers can be - * finalized on shutdown. - */ - readonly start: () => Effect.Effect; - - /** - * Resolves when the internal processing queue is empty and idle. - * Intended for test use to replace timing-sensitive sleeps. - */ - readonly drain: Effect.Effect; -} - -/** - * ThreadDeletionReactor - Service tag for thread deletion cleanup workers. - */ -export class ThreadDeletionReactor extends Context.Service< - ThreadDeletionReactor, - ThreadDeletionReactorShape ->()("t3/orchestration/Services/ThreadDeletionReactor") {} diff --git a/apps/server/src/orchestration/commandInvariants.test.ts b/apps/server/src/orchestration/commandInvariants.test.ts deleted file mode 100644 index 9c6c8bd2a18..00000000000 --- a/apps/server/src/orchestration/commandInvariants.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { describe, expect, it } from "vite-plus/test"; -import { - MessageId, - CommandId, - DEFAULT_PROVIDER_INTERACTION_MODE, - ProjectId, - ThreadId, - type OrchestrationCommand, - type OrchestrationReadModel, - ProviderInstanceId, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; - -import { - findThreadById, - listThreadsByProjectId, - requireNonNegativeInteger, - requireThread, - requireThreadAbsent, -} from "./commandInvariants.ts"; - -const now = "2026-01-01T00:00:00.000Z"; - -const readModel: OrchestrationReadModel = { - snapshotSequence: 2, - updatedAt: now, - projects: [ - { - id: ProjectId.make("project-a"), - title: "Project A", - workspaceRoot: "/tmp/project-a", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - scripts: [], - createdAt: now, - updatedAt: now, - deletedAt: null, - }, - { - id: ProjectId.make("project-b"), - title: "Project B", - workspaceRoot: "/tmp/project-b", - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - scripts: [], - createdAt: now, - updatedAt: now, - deletedAt: null, - }, - ], - threads: [ - { - id: ThreadId.make("thread-1"), - projectId: ProjectId.make("project-a"), - title: "Thread A", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - archivedAt: null, - latestTurn: null, - messages: [], - session: null, - activities: [], - proposedPlans: [], - checkpoints: [], - deletedAt: null, - }, - { - id: ThreadId.make("thread-2"), - projectId: ProjectId.make("project-b"), - title: "Thread B", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - archivedAt: null, - latestTurn: null, - messages: [], - session: null, - activities: [], - proposedPlans: [], - checkpoints: [], - deletedAt: null, - }, - ], -}; - -const messageSendCommand: OrchestrationCommand = { - type: "thread.turn.start", - commandId: CommandId.make("cmd-1"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: MessageId.make("msg-1"), - role: "user", - text: "hello", - attachments: [], - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, -}; - -describe("commandInvariants", () => { - it("finds threads by id and project", () => { - expect(findThreadById(readModel, ThreadId.make("thread-1"))?.projectId).toBe("project-a"); - expect(findThreadById(readModel, ThreadId.make("missing"))).toBeUndefined(); - expect( - listThreadsByProjectId(readModel, ProjectId.make("project-b")).map((thread) => thread.id), - ).toEqual([ThreadId.make("thread-2")]); - }); - - it("requires existing thread", async () => { - const thread = await Effect.runPromise( - requireThread({ - readModel, - command: messageSendCommand, - threadId: ThreadId.make("thread-1"), - }), - ); - expect(thread.id).toBe(ThreadId.make("thread-1")); - - await expect( - Effect.runPromise( - requireThread({ - readModel, - command: messageSendCommand, - threadId: ThreadId.make("missing"), - }), - ), - ).rejects.toThrow("does not exist"); - }); - - it("requires missing thread for create flows", async () => { - await Effect.runPromise( - requireThreadAbsent({ - readModel, - command: { - type: "thread.create", - commandId: CommandId.make("cmd-2"), - threadId: ThreadId.make("thread-3"), - projectId: ProjectId.make("project-a"), - title: "new", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - }, - threadId: ThreadId.make("thread-3"), - }), - ); - - await expect( - Effect.runPromise( - requireThreadAbsent({ - readModel, - command: { - type: "thread.create", - commandId: CommandId.make("cmd-3"), - threadId: ThreadId.make("thread-1"), - projectId: ProjectId.make("project-a"), - title: "dup", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - }, - threadId: ThreadId.make("thread-1"), - }), - ), - ).rejects.toThrow("already exists"); - }); - - it("requires non-negative integers", async () => { - await Effect.runPromise( - requireNonNegativeInteger({ - commandType: "thread.checkpoint.revert", - field: "turnCount", - value: 0, - }), - ); - - await expect( - Effect.runPromise( - requireNonNegativeInteger({ - commandType: "thread.checkpoint.revert", - field: "turnCount", - value: -1, - }), - ), - ).rejects.toThrow("greater than or equal to 0"); - }); -}); diff --git a/apps/server/src/orchestration/decider.delete.test.ts b/apps/server/src/orchestration/decider.delete.test.ts deleted file mode 100644 index fea36b5717f..00000000000 --- a/apps/server/src/orchestration/decider.delete.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { - CommandId, - DEFAULT_PROVIDER_INTERACTION_MODE, - EventId, - ProjectId, - ThreadId, - type OrchestrationCommand, - type OrchestrationEvent, - ProviderInstanceId, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { expect, it } from "@effect/vitest"; - -import { decideOrchestrationCommand } from "./decider.ts"; -import { createEmptyReadModel, projectEvent } from "./projector.ts"; - -const asCommandId = (value: string): CommandId => CommandId.make(value); -const asEventId = (value: string): EventId => EventId.make(value); -const asProjectId = (value: string): ProjectId => ProjectId.make(value); -const asThreadId = (value: string): ThreadId => ThreadId.make(value); - -const seedReadModel = Effect.gen(function* () { - const now = "2026-01-01T00:00:00.000Z"; - const initial = createEmptyReadModel(now); - const withProject = yield* projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-delete"), - type: "project.created", - occurredAt: now, - commandId: asCommandId("cmd-project-create"), - causationEventId: null, - correlationId: asCommandId("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-delete"), - title: "Project Delete", - workspaceRoot: "/tmp/project-delete", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - const withFirstThread = yield* projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create-1"), - aggregateKind: "thread", - aggregateId: asThreadId("thread-delete-1"), - type: "thread.created", - occurredAt: now, - commandId: asCommandId("cmd-thread-create-1"), - causationEventId: null, - correlationId: asCommandId("cmd-thread-create-1"), - metadata: {}, - payload: { - threadId: asThreadId("thread-delete-1"), - projectId: asProjectId("project-delete"), - title: "Thread Delete 1", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - return yield* projectEvent(withFirstThread, { - sequence: 3, - eventId: asEventId("evt-thread-create-2"), - aggregateKind: "thread", - aggregateId: asThreadId("thread-delete-2"), - type: "thread.created", - occurredAt: now, - commandId: asCommandId("cmd-thread-create-2"), - causationEventId: null, - correlationId: asCommandId("cmd-thread-create-2"), - metadata: {}, - payload: { - threadId: asThreadId("thread-delete-2"), - projectId: asProjectId("project-delete"), - title: "Thread Delete 2", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); -}); - -type PlannedEvent = Omit; - -function normalizeDeleteEvent(event: PlannedEvent | ReadonlyArray) { - const events = Array.isArray(event) ? event : [event]; - return events.map((entry) => { - switch (entry.type) { - case "thread.deleted": - return { - type: entry.type, - aggregateKind: entry.aggregateKind, - aggregateId: entry.aggregateId, - commandId: entry.commandId, - correlationId: entry.correlationId, - payload: { - threadId: entry.payload.threadId, - }, - }; - case "project.deleted": - return { - type: entry.type, - aggregateKind: entry.aggregateKind, - aggregateId: entry.aggregateId, - commandId: entry.commandId, - correlationId: entry.correlationId, - payload: { - projectId: entry.payload.projectId, - }, - }; - default: - return entry; - } - }); -} - -it.layer(NodeServices.layer)("decider deletion flows", (it) => { - it.effect("rejects deleting a non-empty project without force", () => - Effect.gen(function* () { - const readModel = yield* seedReadModel; - const error = yield* Effect.flip( - decideOrchestrationCommand({ - command: { - type: "project.delete", - commandId: asCommandId("cmd-project-delete-no-force"), - projectId: asProjectId("project-delete"), - }, - readModel, - }), - ); - expect(error.message).toContain("cannot be deleted without force=true"); - }), - ); - - it.effect("reuses thread.delete semantics when force-deleting a non-empty project", () => - Effect.gen(function* () { - const readModel = yield* seedReadModel; - const projectDeleteCommand: Extract = { - type: "project.delete", - commandId: asCommandId("cmd-project-delete-force"), - projectId: asProjectId("project-delete"), - force: true, - }; - - const forcedResult = yield* decideOrchestrationCommand({ - command: projectDeleteCommand, - readModel, - }); - const forcedEvents = Array.isArray(forcedResult) ? forcedResult : [forcedResult]; - - expect(forcedEvents.map((event) => event.type)).toEqual([ - "thread.deleted", - "thread.deleted", - "project.deleted", - ]); - - let sequentialReadModel = readModel; - let nextSequence = readModel.snapshotSequence; - const sequentialEvents: PlannedEvent[] = []; - for (const nextCommand of [ - { - type: "thread.delete", - commandId: projectDeleteCommand.commandId, - threadId: asThreadId("thread-delete-1"), - }, - { - type: "thread.delete", - commandId: projectDeleteCommand.commandId, - threadId: asThreadId("thread-delete-2"), - }, - { - type: "project.delete", - commandId: projectDeleteCommand.commandId, - projectId: asProjectId("project-delete"), - }, - ] satisfies ReadonlyArray) { - const decided = yield* decideOrchestrationCommand({ - command: nextCommand, - readModel: sequentialReadModel, - }); - const nextEvents = Array.isArray(decided) ? decided : [decided]; - sequentialEvents.push(...nextEvents); - for (const nextEvent of nextEvents) { - nextSequence += 1; - sequentialReadModel = yield* projectEvent(sequentialReadModel, { - ...nextEvent, - sequence: nextSequence, - }); - } - } - - expect(normalizeDeleteEvent(forcedResult)).toEqual(normalizeDeleteEvent(sequentialEvents)); - }), - ); -}); diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts deleted file mode 100644 index 64ba159c740..00000000000 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { - CommandId, - DEFAULT_PROVIDER_INTERACTION_MODE, - EventId, - MessageId, - ProjectId, - ThreadId, - ProviderInstanceId, -} from "@t3tools/contracts"; -import { createModelSelection } from "@t3tools/shared/model"; -import { expect, it } from "@effect/vitest"; -import * as Effect from "effect/Effect"; -import * as NodeServices from "@effect/platform-node/NodeServices"; - -import { decideOrchestrationCommand } from "./decider.ts"; -import { createEmptyReadModel, projectEvent } from "./projector.ts"; - -const asEventId = (value: string): EventId => EventId.make(value); -const asProjectId = (value: string): ProjectId => ProjectId.make(value); -const asMessageId = (value: string): MessageId => MessageId.make(value); -it.layer(NodeServices.layer)("decider project scripts", (it) => { - it.effect("emits empty scripts on project.create", () => - Effect.gen(function* () { - const now = "2026-01-01T00:00:00.000Z"; - const readModel = createEmptyReadModel(now); - - const result = yield* decideOrchestrationCommand({ - command: { - type: "project.create", - commandId: CommandId.make("cmd-project-create-scripts"), - projectId: asProjectId("project-scripts"), - title: "Scripts", - workspaceRoot: "/tmp/scripts", - createdAt: now, - }, - readModel, - }); - - const event = Array.isArray(result) ? result[0] : result; - expect(event.type).toBe("project.created"); - expect((event.payload as { scripts: unknown[] }).scripts).toEqual([]); - }), - ); - - it.effect("propagates scripts in project.meta.update payload", () => - Effect.gen(function* () { - const now = "2026-01-01T00:00:00.000Z"; - const initial = createEmptyReadModel(now); - const readModel = yield* projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create-scripts"), - aggregateKind: "project", - aggregateId: asProjectId("project-scripts"), - type: "project.created", - occurredAt: now, - commandId: CommandId.make("cmd-project-create-scripts"), - causationEventId: null, - correlationId: CommandId.make("cmd-project-create-scripts"), - metadata: {}, - payload: { - projectId: asProjectId("project-scripts"), - title: "Scripts", - workspaceRoot: "/tmp/scripts", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - - const scripts = [ - { - id: "lint", - name: "Lint", - command: "bun run lint", - icon: "lint", - runOnWorktreeCreate: false, - }, - ] as const; - - const result = yield* decideOrchestrationCommand({ - command: { - type: "project.meta.update", - commandId: CommandId.make("cmd-project-update-scripts"), - projectId: asProjectId("project-scripts"), - scripts: Array.from(scripts), - }, - readModel, - }); - - const event = Array.isArray(result) ? result[0] : result; - expect(event.type).toBe("project.meta-updated"); - expect((event.payload as { scripts?: unknown[] }).scripts).toEqual(scripts); - }), - ); - - it.effect("emits user message and turn-start-requested events for thread.turn.start", () => - Effect.gen(function* () { - const now = "2026-01-01T00:00:00.000Z"; - const initial = createEmptyReadModel(now); - const withProject = yield* projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.make("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.make("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - const readModel = yield* projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.make("cmd-thread-create"), - causationEventId: null, - correlationId: CommandId.make("cmd-thread-create"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - const result = yield* decideOrchestrationCommand({ - command: { - type: "thread.turn.start", - commandId: CommandId.make("cmd-turn-start"), - threadId: ThreadId.make("thread-1"), - message: { - messageId: asMessageId("message-user-1"), - role: "user", - text: "hello", - attachments: [], - }, - modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ - { id: "reasoningEffort", value: "high" }, - { id: "fastMode", value: true }, - ]), - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }, - readModel, - }); - - expect(Array.isArray(result)).toBe(true); - const events = Array.isArray(result) ? result : [result]; - expect(events).toHaveLength(2); - expect(events[0]?.type).toBe("thread.message-sent"); - const turnStartEvent = events[1]; - expect(turnStartEvent?.type).toBe("thread.turn-start-requested"); - expect(turnStartEvent?.causationEventId).toBe(events[0]?.eventId ?? null); - if (turnStartEvent?.type !== "thread.turn-start-requested") { - return; - } - expect(turnStartEvent.payload).toMatchObject({ - threadId: ThreadId.make("thread-1"), - messageId: asMessageId("message-user-1"), - modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ - { id: "reasoningEffort", value: "high" }, - { id: "fastMode", value: true }, - ]), - runtimeMode: "approval-required", - }); - }), - ); - - it.effect("emits thread.runtime-mode-set from thread.runtime-mode.set", () => - Effect.gen(function* () { - const now = "2026-01-01T00:00:00.000Z"; - const initial = createEmptyReadModel(now); - const withProject = yield* projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.make("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.make("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - const readModel = yield* projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.make("cmd-thread-create"), - causationEventId: null, - correlationId: CommandId.make("cmd-thread-create"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - const result = yield* decideOrchestrationCommand({ - command: { - type: "thread.runtime-mode.set", - commandId: CommandId.make("cmd-runtime-mode-set"), - threadId: ThreadId.make("thread-1"), - runtimeMode: "approval-required", - createdAt: now, - }, - readModel, - }); - - const singleResult = Array.isArray(result) ? null : result; - if (singleResult === null) { - throw new Error("Expected a single runtime-mode-set event."); - } - expect(singleResult).toMatchObject({ - type: "thread.runtime-mode-set", - payload: { - threadId: ThreadId.make("thread-1"), - runtimeMode: "approval-required", - }, - }); - }), - ); - - it.effect("emits thread.interaction-mode-set from thread.interaction-mode.set", () => - Effect.gen(function* () { - const now = "2026-01-01T00:00:00.000Z"; - const initial = createEmptyReadModel(now); - const withProject = yield* projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.make("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.make("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); - const readModel = yield* projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create"), - aggregateKind: "thread", - aggregateId: ThreadId.make("thread-1"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.make("cmd-thread-create"), - causationEventId: null, - correlationId: CommandId.make("cmd-thread-create"), - metadata: {}, - payload: { - threadId: ThreadId.make("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); - - const result = yield* decideOrchestrationCommand({ - command: { - type: "thread.interaction-mode.set", - commandId: CommandId.make("cmd-interaction-mode-set"), - threadId: ThreadId.make("thread-1"), - interactionMode: "plan", - createdAt: now, - }, - readModel, - }); - - const singleResult = Array.isArray(result) ? null : result; - if (singleResult === null) { - throw new Error("Expected a single interaction-mode-set event."); - } - expect(singleResult).toMatchObject({ - type: "thread.interaction-mode-set", - payload: { - threadId: ThreadId.make("thread-1"), - interactionMode: "plan", - }, - }); - }), - ); -}); diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 0d4af771ca8..0e051f0ad57 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -125,7 +125,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" title: command.title, workspaceRoot: command.workspaceRoot, defaultModelSelection: command.defaultModelSelection ?? null, - scripts: [], + scripts: command.scripts ?? [], createdAt: command.createdAt, updatedAt: command.createdAt, }, diff --git a/apps/server/src/orchestration/http.ts b/apps/server/src/orchestration/http.ts deleted file mode 100644 index a148f98474b..00000000000 --- a/apps/server/src/orchestration/http.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - AuthOrchestrationOperateScope, - AuthOrchestrationReadScope, - EnvironmentHttpApi, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; - -import { normalizeDispatchCommand } from "./Normalizer.ts"; -import { - annotateEnvironmentRequest, - failEnvironmentInternal, - failEnvironmentInvalidRequest, - requireEnvironmentScope, -} from "../auth/http.ts"; -import { OrchestrationEngineService } from "./Services/OrchestrationEngine.ts"; -import { ProjectionSnapshotQuery } from "./Services/ProjectionSnapshotQuery.ts"; - -export const orchestrationHttpApiLayer = HttpApiBuilder.group( - EnvironmentHttpApi, - "orchestration", - Effect.fnUntraced(function* (handlers) { - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; - const orchestrationEngine = yield* OrchestrationEngineService; - - return handlers - .handle( - "snapshot", - Effect.fn("environment.orchestration.snapshot")(function* (args) { - yield* annotateEnvironmentRequest(args.endpoint.name); - yield* requireEnvironmentScope(AuthOrchestrationReadScope); - return yield* projectionSnapshotQuery - .getSnapshot() - .pipe( - Effect.catch((cause) => - failEnvironmentInternal("orchestration_snapshot_failed", cause), - ), - ); - }), - ) - .handle( - "dispatch", - Effect.fn("environment.orchestration.dispatch")(function* (args) { - yield* annotateEnvironmentRequest(args.endpoint.name); - yield* requireEnvironmentScope(AuthOrchestrationOperateScope); - const normalizedCommand = yield* normalizeDispatchCommand(args.payload).pipe( - Effect.catch(() => failEnvironmentInvalidRequest("invalid_command")), - ); - return yield* orchestrationEngine - .dispatch(normalizedCommand) - .pipe( - Effect.catch((cause) => - failEnvironmentInternal("orchestration_dispatch_failed", cause), - ), - ); - }), - ); - }), -); diff --git a/apps/server/src/orchestration/projector.test.ts b/apps/server/src/orchestration/projector.test.ts deleted file mode 100644 index fadd5078026..00000000000 --- a/apps/server/src/orchestration/projector.test.ts +++ /dev/null @@ -1,955 +0,0 @@ -import { - CommandId, - EventId, - ProjectId, - ProviderDriverKind, - ThreadId, - type OrchestrationEvent, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import { describe, expect, it } from "vite-plus/test"; - -import { createEmptyReadModel, projectEvent } from "./projector.ts"; - -function makeEvent(input: { - sequence: number; - type: OrchestrationEvent["type"]; - occurredAt: string; - aggregateKind: OrchestrationEvent["aggregateKind"]; - aggregateId: string; - commandId: string | null; - payload: unknown; -}): OrchestrationEvent { - return { - sequence: input.sequence, - eventId: EventId.make(`event-${input.sequence}`), - type: input.type, - aggregateKind: input.aggregateKind, - aggregateId: - input.aggregateKind === "project" - ? ProjectId.make(input.aggregateId) - : ThreadId.make(input.aggregateId), - occurredAt: input.occurredAt, - commandId: input.commandId === null ? null : CommandId.make(input.commandId), - causationEventId: null, - correlationId: null, - metadata: {}, - payload: input.payload as never, - } as OrchestrationEvent; -} - -describe("orchestration projector", () => { - it("applies thread.created events", async () => { - const now = "2026-01-01T00:00:00.000Z"; - const model = createEmptyReadModel(now); - - const next = await Effect.runPromise( - projectEvent( - model, - makeEvent({ - sequence: 1, - type: "thread.created", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: now, - commandId: "cmd-thread-create", - payload: { - threadId: "thread-1", - projectId: "project-1", - title: "demo", - modelSelection: { - provider: ProviderDriverKind.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ), - ); - - expect(next.snapshotSequence).toBe(1); - expect(next.threads).toEqual([ - { - id: "thread-1", - projectId: "project-1", - title: "demo", - modelSelection: { - instanceId: "codex", - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - latestTurn: null, - createdAt: now, - updatedAt: now, - archivedAt: null, - deletedAt: null, - messages: [], - proposedPlans: [], - activities: [], - checkpoints: [], - session: null, - }, - ]); - }); - - it("fails when event payload cannot be decoded by runtime schema", async () => { - const now = "2026-01-01T00:00:00.000Z"; - const model = createEmptyReadModel(now); - - await expect( - Effect.runPromise( - projectEvent( - model, - makeEvent({ - sequence: 1, - type: "thread.created", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: now, - commandId: "cmd-invalid", - payload: { - // missing required threadId - projectId: "project-1", - title: "demo", - modelSelection: { - provider: ProviderDriverKind.make("codex"), - model: "gpt-5-codex", - }, - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ), - ), - ).rejects.toBeDefined(); - }); - - it("applies thread.archived and thread.unarchived events", async () => { - const now = "2026-01-01T00:00:00.000Z"; - const later = "2026-01-01T00:00:01.000Z"; - const created = await Effect.runPromise( - projectEvent( - createEmptyReadModel(now), - makeEvent({ - sequence: 1, - type: "thread.created", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: now, - commandId: "cmd-thread-create", - payload: { - threadId: "thread-1", - projectId: "project-1", - title: "demo", - modelSelection: { - provider: ProviderDriverKind.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ), - ); - - const archived = await Effect.runPromise( - projectEvent( - created, - makeEvent({ - sequence: 2, - type: "thread.archived", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: later, - commandId: "cmd-thread-archive", - payload: { - threadId: "thread-1", - archivedAt: later, - updatedAt: later, - }, - }), - ), - ); - expect(archived.threads[0]?.archivedAt).toBe(later); - - const unarchived = await Effect.runPromise( - projectEvent( - archived, - makeEvent({ - sequence: 3, - type: "thread.unarchived", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: later, - commandId: "cmd-thread-unarchive", - payload: { - threadId: "thread-1", - updatedAt: later, - }, - }), - ), - ); - expect(unarchived.threads[0]?.archivedAt).toBeNull(); - }); - - it("keeps projector forward-compatible for unhandled event types", async () => { - const now = "2026-01-01T00:00:00.000Z"; - const model = createEmptyReadModel(now); - - const next = await Effect.runPromise( - projectEvent( - model, - makeEvent({ - sequence: 7, - type: "thread.turn-start-requested", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-01-01T00:00:00.000Z", - commandId: "cmd-unhandled", - payload: { - threadId: "thread-1", - messageId: "message-1", - runtimeMode: "approval-required", - createdAt: "2026-01-01T00:00:00.000Z", - }, - }), - ), - ); - - expect(next.snapshotSequence).toBe(7); - expect(next.updatedAt).toBe("2026-01-01T00:00:00.000Z"); - expect(next.threads).toEqual([]); - }); - - it("tracks latest turn id from session lifecycle events", async () => { - const createdAt = "2026-02-23T08:00:00.000Z"; - const startedAt = "2026-02-23T08:00:05.000Z"; - const model = createEmptyReadModel(createdAt); - - const afterCreate = await Effect.runPromise( - projectEvent( - model, - makeEvent({ - sequence: 1, - type: "thread.created", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: createdAt, - commandId: "cmd-create", - payload: { - threadId: "thread-1", - projectId: "project-1", - title: "demo", - modelSelection: { - provider: ProviderDriverKind.make("codex"), - model: "gpt-5.3-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - updatedAt: createdAt, - }, - }), - ), - ); - - const settledAt = "2026-02-23T08:01:00.000Z"; - const [afterRunning, afterReady] = await Effect.runPromise( - Effect.flatMap( - projectEvent( - afterCreate, - makeEvent({ - sequence: 2, - type: "thread.session-set", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: startedAt, - commandId: "cmd-running", - payload: { - threadId: "thread-1", - session: { - threadId: "thread-1", - status: "running", - providerName: "codex", - providerSessionId: "session-1", - providerThreadId: "provider-thread-1", - runtimeMode: "approval-required", - activeTurnId: "turn-1", - lastError: null, - updatedAt: startedAt, - }, - }, - }), - ), - (running) => - Effect.map( - projectEvent( - running, - makeEvent({ - sequence: 3, - type: "thread.session-set", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: settledAt, - commandId: "cmd-ready", - payload: { - threadId: "thread-1", - session: { - threadId: "thread-1", - status: "ready", - providerName: "codex", - providerSessionId: "session-1", - providerThreadId: "provider-thread-1", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: settledAt, - }, - }, - }), - ), - (ready) => [running, ready] as const, - ), - ), - ); - - const thread = afterRunning.threads[0]; - expect(thread?.latestTurn?.turnId).toBe("turn-1"); - expect(thread?.session?.status).toBe("running"); - - // Leaving the "running" session status settles the running turn with the - // session timestamp as the turn end. - const settledThread = afterReady.threads[0]; - expect(settledThread?.latestTurn?.turnId).toBe("turn-1"); - expect(settledThread?.latestTurn?.state).toBe("completed"); - expect(settledThread?.latestTurn?.completedAt).toBe(settledAt); - }); - - it("updates canonical thread runtime mode from thread.runtime-mode-set", async () => { - const createdAt = "2026-02-23T08:00:00.000Z"; - const updatedAt = "2026-02-23T08:00:05.000Z"; - const model = createEmptyReadModel(createdAt); - - const afterCreate = await Effect.runPromise( - projectEvent( - model, - makeEvent({ - sequence: 1, - type: "thread.created", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: createdAt, - commandId: "cmd-create", - payload: { - threadId: "thread-1", - projectId: "project-1", - title: "demo", - modelSelection: { - provider: ProviderDriverKind.make("codex"), - model: "gpt-5.3-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - updatedAt: createdAt, - }, - }), - ), - ); - - const afterUpdate = await Effect.runPromise( - projectEvent( - afterCreate, - makeEvent({ - sequence: 2, - type: "thread.runtime-mode-set", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: updatedAt, - commandId: "cmd-runtime-mode-set", - payload: { - threadId: "thread-1", - runtimeMode: "approval-required", - updatedAt, - }, - }), - ), - ); - - expect(afterUpdate.threads[0]?.runtimeMode).toBe("approval-required"); - expect(afterUpdate.threads[0]?.updatedAt).toBe(updatedAt); - }); - - it("marks assistant messages completed with non-streaming updates", async () => { - const createdAt = "2026-02-23T09:00:00.000Z"; - const deltaAt = "2026-02-23T09:00:01.000Z"; - const completeAt = "2026-02-23T09:00:03.500Z"; - const model = createEmptyReadModel(createdAt); - - const afterCreate = await Effect.runPromise( - projectEvent( - model, - makeEvent({ - sequence: 1, - type: "thread.created", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: createdAt, - commandId: "cmd-create", - payload: { - threadId: "thread-1", - projectId: "project-1", - title: "demo", - modelSelection: { - provider: ProviderDriverKind.make("codex"), - model: "gpt-5.3-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - updatedAt: createdAt, - }, - }), - ), - ); - - const afterDelta = await Effect.runPromise( - projectEvent( - afterCreate, - makeEvent({ - sequence: 2, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: deltaAt, - commandId: "cmd-delta", - payload: { - threadId: "thread-1", - messageId: "assistant:msg-1", - role: "assistant", - text: "hello", - turnId: "turn-1", - streaming: true, - createdAt: deltaAt, - updatedAt: deltaAt, - }, - }), - ), - ); - - const afterComplete = await Effect.runPromise( - projectEvent( - afterDelta, - makeEvent({ - sequence: 3, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: completeAt, - commandId: "cmd-complete", - payload: { - threadId: "thread-1", - messageId: "assistant:msg-1", - role: "assistant", - text: "", - turnId: "turn-1", - streaming: false, - createdAt: completeAt, - updatedAt: completeAt, - }, - }), - ), - ); - - const message = afterComplete.threads[0]?.messages[0]; - expect(message?.id).toBe("assistant:msg-1"); - expect(message?.text).toBe("hello"); - expect(message?.streaming).toBe(false); - expect(message?.updatedAt).toBe(completeAt); - }); - - it("prunes reverted turn messages from in-memory thread snapshot", async () => { - const createdAt = "2026-02-23T10:00:00.000Z"; - const model = createEmptyReadModel(createdAt); - - const afterCreate = await Effect.runPromise( - projectEvent( - model, - makeEvent({ - sequence: 1, - type: "thread.created", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: createdAt, - commandId: "cmd-create", - payload: { - threadId: "thread-1", - projectId: "project-1", - title: "demo", - modelSelection: { - provider: ProviderDriverKind.make("codex"), - model: "gpt-5.3-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - updatedAt: createdAt, - }, - }), - ), - ); - - const events: ReadonlyArray = [ - makeEvent({ - sequence: 2, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-02-23T10:00:01.000Z", - commandId: "cmd-user-1", - payload: { - threadId: "thread-1", - messageId: "user-msg-1", - role: "user", - text: "First edit", - turnId: null, - streaming: false, - createdAt: "2026-02-23T10:00:01.000Z", - updatedAt: "2026-02-23T10:00:01.000Z", - }, - }), - makeEvent({ - sequence: 3, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-02-23T10:00:02.000Z", - commandId: "cmd-assistant-1", - payload: { - threadId: "thread-1", - messageId: "assistant-msg-1", - role: "assistant", - text: "Updated README to v2.\n", - turnId: "turn-1", - streaming: false, - createdAt: "2026-02-23T10:00:02.000Z", - updatedAt: "2026-02-23T10:00:02.000Z", - }, - }), - makeEvent({ - sequence: 4, - type: "thread.turn-diff-completed", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-02-23T10:00:02.500Z", - commandId: "cmd-turn-1-complete", - payload: { - threadId: "thread-1", - turnId: "turn-1", - checkpointTurnCount: 1, - checkpointRef: "refs/t3/checkpoints/thread-1/turn/1", - status: "ready", - files: [], - assistantMessageId: "assistant-msg-1", - completedAt: "2026-02-23T10:00:02.500Z", - }, - }), - makeEvent({ - sequence: 5, - type: "thread.activity-appended", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-02-23T10:00:02.750Z", - commandId: "cmd-activity-1", - payload: { - threadId: "thread-1", - activity: { - id: "activity-1", - tone: "tool", - kind: "tool.started", - summary: "Edit file started", - payload: { toolKind: "command" }, - turnId: "turn-1", - createdAt: "2026-02-23T10:00:02.750Z", - }, - }, - }), - makeEvent({ - sequence: 6, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-02-23T10:00:03.000Z", - commandId: "cmd-user-2", - payload: { - threadId: "thread-1", - messageId: "user-msg-2", - role: "user", - text: "Second edit", - turnId: null, - streaming: false, - createdAt: "2026-02-23T10:00:03.000Z", - updatedAt: "2026-02-23T10:00:03.000Z", - }, - }), - makeEvent({ - sequence: 7, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-02-23T10:00:04.000Z", - commandId: "cmd-assistant-2", - payload: { - threadId: "thread-1", - messageId: "assistant-msg-2", - role: "assistant", - text: "Updated README to v3.\n", - turnId: "turn-2", - streaming: false, - createdAt: "2026-02-23T10:00:04.000Z", - updatedAt: "2026-02-23T10:00:04.000Z", - }, - }), - makeEvent({ - sequence: 8, - type: "thread.turn-diff-completed", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-02-23T10:00:04.500Z", - commandId: "cmd-turn-2-complete", - payload: { - threadId: "thread-1", - turnId: "turn-2", - checkpointTurnCount: 2, - checkpointRef: "refs/t3/checkpoints/thread-1/turn/2", - status: "ready", - files: [], - assistantMessageId: "assistant-msg-2", - completedAt: "2026-02-23T10:00:04.500Z", - }, - }), - makeEvent({ - sequence: 9, - type: "thread.activity-appended", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-02-23T10:00:04.750Z", - commandId: "cmd-activity-2", - payload: { - threadId: "thread-1", - activity: { - id: "activity-2", - tone: "tool", - kind: "tool.completed", - summary: "Edit file complete", - payload: { toolKind: "command" }, - turnId: "turn-2", - createdAt: "2026-02-23T10:00:04.750Z", - }, - }, - }), - makeEvent({ - sequence: 10, - type: "thread.reverted", - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: "2026-02-23T10:00:05.000Z", - commandId: "cmd-revert", - payload: { - threadId: "thread-1", - turnCount: 1, - }, - }), - ]; - - const afterRevert = await events.reduce>>( - (statePromise, event) => - statePromise.then((state) => Effect.runPromise(projectEvent(state, event))), - Promise.resolve(afterCreate), - ); - - const thread = afterRevert.threads[0]; - expect(thread?.messages.map((message) => ({ role: message.role, text: message.text }))).toEqual( - [ - { role: "user", text: "First edit" }, - { role: "assistant", text: "Updated README to v2.\n" }, - ], - ); - expect( - thread?.activities.map((activity) => ({ id: activity.id, turnId: activity.turnId })), - ).toEqual([{ id: "activity-1", turnId: "turn-1" }]); - expect(thread?.checkpoints.map((checkpoint) => checkpoint.checkpointTurnCount)).toEqual([1]); - expect(thread?.latestTurn?.turnId).toBe("turn-1"); - }); - - it("does not fallback-retain messages tied to removed turn IDs", async () => { - const createdAt = "2026-02-26T12:00:00.000Z"; - const model = createEmptyReadModel(createdAt); - - const afterCreate = await Effect.runPromise( - projectEvent( - model, - makeEvent({ - sequence: 1, - type: "thread.created", - aggregateKind: "thread", - aggregateId: "thread-revert", - occurredAt: createdAt, - commandId: "cmd-create-revert", - payload: { - threadId: "thread-revert", - projectId: "project-1", - title: "demo", - modelSelection: { - provider: ProviderDriverKind.make("codex"), - model: "gpt-5.3-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - updatedAt: createdAt, - }, - }), - ), - ); - - const events: ReadonlyArray = [ - makeEvent({ - sequence: 2, - type: "thread.turn-diff-completed", - aggregateKind: "thread", - aggregateId: "thread-revert", - occurredAt: "2026-02-26T12:00:01.000Z", - commandId: "cmd-turn-1", - payload: { - threadId: "thread-revert", - turnId: "turn-1", - checkpointTurnCount: 1, - checkpointRef: "refs/t3/checkpoints/thread-revert/turn/1", - status: "ready", - files: [], - assistantMessageId: "assistant-keep", - completedAt: "2026-02-26T12:00:01.000Z", - }, - }), - makeEvent({ - sequence: 3, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-revert", - occurredAt: "2026-02-26T12:00:01.100Z", - commandId: "cmd-assistant-keep", - payload: { - threadId: "thread-revert", - messageId: "assistant-keep", - role: "assistant", - text: "kept", - turnId: "turn-1", - streaming: false, - createdAt: "2026-02-26T12:00:01.100Z", - updatedAt: "2026-02-26T12:00:01.100Z", - }, - }), - makeEvent({ - sequence: 4, - type: "thread.turn-diff-completed", - aggregateKind: "thread", - aggregateId: "thread-revert", - occurredAt: "2026-02-26T12:00:02.000Z", - commandId: "cmd-turn-2", - payload: { - threadId: "thread-revert", - turnId: "turn-2", - checkpointTurnCount: 2, - checkpointRef: "refs/t3/checkpoints/thread-revert/turn/2", - status: "ready", - files: [], - assistantMessageId: "assistant-remove", - completedAt: "2026-02-26T12:00:02.000Z", - }, - }), - makeEvent({ - sequence: 5, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-revert", - occurredAt: "2026-02-26T12:00:02.050Z", - commandId: "cmd-user-remove", - payload: { - threadId: "thread-revert", - messageId: "user-remove", - role: "user", - text: "removed", - turnId: "turn-2", - streaming: false, - createdAt: "2026-02-26T12:00:02.050Z", - updatedAt: "2026-02-26T12:00:02.050Z", - }, - }), - makeEvent({ - sequence: 6, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-revert", - occurredAt: "2026-02-26T12:00:02.100Z", - commandId: "cmd-assistant-remove", - payload: { - threadId: "thread-revert", - messageId: "assistant-remove", - role: "assistant", - text: "removed", - turnId: "turn-2", - streaming: false, - createdAt: "2026-02-26T12:00:02.100Z", - updatedAt: "2026-02-26T12:00:02.100Z", - }, - }), - makeEvent({ - sequence: 7, - type: "thread.reverted", - aggregateKind: "thread", - aggregateId: "thread-revert", - occurredAt: "2026-02-26T12:00:03.000Z", - commandId: "cmd-revert", - payload: { - threadId: "thread-revert", - turnCount: 1, - }, - }), - ]; - - const afterRevert = await events.reduce>>( - (statePromise, event) => - statePromise.then((state) => Effect.runPromise(projectEvent(state, event))), - Promise.resolve(afterCreate), - ); - - const thread = afterRevert.threads[0]; - expect( - thread?.messages.map((message) => ({ - id: message.id, - role: message.role, - turnId: message.turnId, - })), - ).toEqual([{ id: "assistant-keep", role: "assistant", turnId: "turn-1" }]); - }); - - it("caps message and checkpoint retention for long-lived threads", async () => { - const createdAt = "2026-03-01T10:00:00.000Z"; - const model = createEmptyReadModel(createdAt); - - const afterCreate = await Effect.runPromise( - projectEvent( - model, - makeEvent({ - sequence: 1, - type: "thread.created", - aggregateKind: "thread", - aggregateId: "thread-capped", - occurredAt: createdAt, - commandId: "cmd-create-capped", - payload: { - threadId: "thread-capped", - projectId: "project-1", - title: "capped", - modelSelection: { - provider: ProviderDriverKind.make("codex"), - model: "gpt-5-codex", - }, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - updatedAt: createdAt, - }, - }), - ), - ); - - const messageEvents: ReadonlyArray = Array.from( - { length: 2_100 }, - (_, index) => - makeEvent({ - sequence: index + 2, - type: "thread.message-sent", - aggregateKind: "thread", - aggregateId: "thread-capped", - occurredAt: `2026-03-01T10:00:${String(index % 60).padStart(2, "0")}.000Z`, - commandId: `cmd-message-${index}`, - payload: { - threadId: "thread-capped", - messageId: `msg-${index}`, - role: "assistant", - text: `message-${index}`, - turnId: `turn-${index}`, - streaming: false, - createdAt: `2026-03-01T10:00:${String(index % 60).padStart(2, "0")}.000Z`, - updatedAt: `2026-03-01T10:00:${String(index % 60).padStart(2, "0")}.000Z`, - }, - }), - ); - const afterMessages = await messageEvents.reduce< - Promise> - >( - (statePromise, event) => - statePromise.then((state) => Effect.runPromise(projectEvent(state, event))), - Promise.resolve(afterCreate), - ); - - const checkpointEvents: ReadonlyArray = Array.from( - { length: 600 }, - (_, index) => - makeEvent({ - sequence: index + 2_102, - type: "thread.turn-diff-completed", - aggregateKind: "thread", - aggregateId: "thread-capped", - occurredAt: `2026-03-01T10:30:${String(index % 60).padStart(2, "0")}.000Z`, - commandId: `cmd-checkpoint-${index}`, - payload: { - threadId: "thread-capped", - turnId: `turn-${index}`, - checkpointTurnCount: index + 1, - checkpointRef: `refs/t3/checkpoints/thread-capped/turn/${index + 1}`, - status: "ready", - files: [], - assistantMessageId: `msg-${index}`, - completedAt: `2026-03-01T10:30:${String(index % 60).padStart(2, "0")}.000Z`, - }, - }), - ); - const finalState = await checkpointEvents.reduce< - Promise> - >( - (statePromise, event) => - statePromise.then((state) => Effect.runPromise(projectEvent(state, event))), - Promise.resolve(afterMessages), - ); - - const thread = finalState.threads[0]; - expect(thread?.messages).toHaveLength(2_000); - expect(thread?.messages[0]?.id).toBe("msg-100"); - expect(thread?.messages.at(-1)?.id).toBe("msg-2099"); - expect(thread?.checkpoints).toHaveLength(500); - expect(thread?.checkpoints[0]?.turnId).toBe("turn-100"); - expect(thread?.checkpoints.at(-1)?.turnId).toBe("turn-599"); - }); -}); diff --git a/apps/server/src/persistence/Layers/OrchestrationCommandReceipts.ts b/apps/server/src/persistence/Layers/OrchestrationCommandReceipts.ts index 989f5d78f8d..33ebeb8a2b1 100644 --- a/apps/server/src/persistence/Layers/OrchestrationCommandReceipts.ts +++ b/apps/server/src/persistence/Layers/OrchestrationCommandReceipts.ts @@ -23,6 +23,7 @@ const makeOrchestrationCommandReceiptRepository = Effect.gen(function* () { command_id, aggregate_kind, aggregate_id, + command_type, accepted_at, result_sequence, status, @@ -32,6 +33,7 @@ const makeOrchestrationCommandReceiptRepository = Effect.gen(function* () { ${receipt.commandId}, ${receipt.aggregateKind}, ${receipt.aggregateId}, + ${receipt.commandType}, ${receipt.acceptedAt}, ${receipt.resultSequence}, ${receipt.status}, @@ -40,7 +42,8 @@ const makeOrchestrationCommandReceiptRepository = Effect.gen(function* () { ON CONFLICT (command_id) DO UPDATE SET aggregate_kind = excluded.aggregate_kind, - aggregate_id = excluded.aggregate_id, + aggregate_id = excluded.aggregate_id, + command_type = excluded.command_type, accepted_at = excluded.accepted_at, result_sequence = excluded.result_sequence, status = excluded.status, @@ -57,6 +60,7 @@ const makeOrchestrationCommandReceiptRepository = Effect.gen(function* () { command_id AS "commandId", aggregate_kind AS "aggregateKind", aggregate_id AS "aggregateId", + command_type AS "commandType", accepted_at AS "acceptedAt", result_sequence AS "resultSequence", status, @@ -71,6 +75,37 @@ const makeOrchestrationCommandReceiptRepository = Effect.gen(function* () { Effect.mapError(toPersistenceSqlError("OrchestrationCommandReceiptRepository.upsert:query")), ); + const insertIfAbsent: OrchestrationCommandReceiptRepositoryShape["insertIfAbsent"] = (receipt) => + sql<{ readonly command_id: string }>` + INSERT INTO orchestration_command_receipts ( + command_id, + aggregate_kind, + aggregate_id, + command_type, + accepted_at, + result_sequence, + status, + error + ) + VALUES ( + ${receipt.commandId}, + ${receipt.aggregateKind}, + ${receipt.aggregateId}, + ${receipt.commandType}, + ${receipt.acceptedAt}, + ${receipt.resultSequence}, + ${receipt.status}, + ${receipt.error} + ) + ON CONFLICT(command_id) DO NOTHING + RETURNING command_id + `.pipe( + Effect.map((rows) => rows.length === 1), + Effect.mapError( + toPersistenceSqlError("OrchestrationCommandReceiptRepository.insertIfAbsent:query"), + ), + ); + const getByCommandId: OrchestrationCommandReceiptRepositoryShape["getByCommandId"] = (input) => findReceiptByCommandId(input).pipe( Effect.mapError( @@ -79,6 +114,7 @@ const makeOrchestrationCommandReceiptRepository = Effect.gen(function* () { ); return { + insertIfAbsent, upsert, getByCommandId, } satisfies OrchestrationCommandReceiptRepositoryShape; diff --git a/apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts b/apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts index 2bac5de920c..88979fae04b 100644 --- a/apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts +++ b/apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts @@ -1,5 +1,6 @@ -import { CommandId, EventId, ProjectId } from "@t3tools/contracts"; +import { CommandId, EventId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; import { assert, it } from "@effect/vitest"; +import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; @@ -120,4 +121,96 @@ layer("OrchestrationEventStore", (it) => { } }), ); + + it.effect("orders project and V2 agent events in the retained application event source", () => + Effect.gen(function* () { + const eventStore = yield* OrchestrationEventStore; + const projectId = ProjectId.make("project-shared-stream"); + const threadId = ThreadId.make("thread-shared-stream"); + const providerInstanceId = ProviderInstanceId.make("codex"); + const occurredAt = DateTime.makeUnsafe("2026-01-02T00:00:00.000Z"); + const now = DateTime.formatIso(occurredAt); + const baselineSequence = yield* eventStore.latestApplicationSequence; + + const projectEvent = yield* eventStore.append({ + type: "project.created", + eventId: EventId.make("event-project-shared-stream"), + aggregateKind: "project", + aggregateId: projectId, + occurredAt: now, + commandId: CommandId.make("command-project-shared-stream"), + causationEventId: null, + correlationId: CommandId.make("command-project-shared-stream"), + metadata: {}, + payload: { + projectId, + title: "Shared stream", + workspaceRoot: "/tmp/shared-stream", + defaultModelSelection: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }); + const [threadEvent] = yield* eventStore.appendAgentEvents({ + commandId: CommandId.make("command-thread-shared-stream"), + events: [ + { + id: EventId.make("event-thread-shared-stream"), + type: "thread.created", + threadId, + providerInstanceId, + occurredAt, + payload: { + id: threadId, + projectId, + title: "Thread", + providerInstanceId, + modelSelection: { instanceId: providerInstanceId, model: "gpt-5.4" }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: null, + lineage: { + rootThreadId: threadId, + parentThreadId: null, + relationshipToParent: null, + }, + forkedFrom: null, + createdBy: "user", + creationSource: "web", + createdAt: occurredAt, + updatedAt: occurredAt, + archivedAt: null, + deletedAt: null, + }, + }, + ], + }); + + const applicationEvents = yield* eventStore + .streamApplicationEvents({ afterSequence: baselineSequence }) + .pipe( + Stream.take(2), + Stream.runCollect, + Effect.map((chunk) => Array.from(chunk)), + ); + assert.deepEqual( + applicationEvents.map((event) => event.sequence), + [projectEvent.sequence, threadEvent!.sequence], + ); + assert.isTrue("aggregateKind" in applicationEvents[0]!); + assert.isTrue("event" in applicationEvents[1]!); + + const legacyReplay = yield* eventStore.readFromSequence(projectEvent.sequence - 1).pipe( + Stream.runCollect, + Effect.map((chunk) => Array.from(chunk)), + ); + assert.deepEqual( + legacyReplay.map((event) => event.type), + ["project.created"], + ); + }), + ); }); diff --git a/apps/server/src/persistence/Layers/OrchestrationEventStore.ts b/apps/server/src/persistence/Layers/OrchestrationEventStore.ts index 18d0e9aa578..e303b84a606 100644 --- a/apps/server/src/persistence/Layers/OrchestrationEventStore.ts +++ b/apps/server/src/persistence/Layers/OrchestrationEventStore.ts @@ -1,4 +1,5 @@ import { + type ApplicationStoredEvent, CommandId, EventId, IsoDateTime, @@ -8,13 +9,17 @@ import { OrchestrationEvent, OrchestrationEventMetadata, OrchestrationEventType, + OrchestrationV2DomainEventJson, + OrchestrationV2StoredEvent, ProjectId, ThreadId, + type OrchestrationV2DomainEvent, } from "@t3tools/contracts"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as SqlSchema from "effect/unstable/sql/SqlSchema"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as PubSub from "effect/PubSub"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; @@ -44,6 +49,7 @@ const AppendEventRequestSchema = Schema.Struct({ commandId: Schema.NullOr(CommandId), payloadJson: UnknownFromJsonString, metadataJson: EventMetadataFromJsonString, + applicationEventVersion: Schema.Number, }); const OrchestrationEventPersistedRowSchema = Schema.Struct({ @@ -67,6 +73,99 @@ const ReadFromSequenceRequestSchema = Schema.Struct({ const DEFAULT_READ_FROM_SEQUENCE_LIMIT = 1_000; const READ_PAGE_SIZE = 500; +interface ApplicationEventRow { + readonly sequence: number; + readonly event_id: string; + readonly command_id: string | null; + readonly aggregate_kind: "project" | "thread"; + readonly stream_id: string; + readonly event_type: string; + readonly occurred_at: string; + readonly payload_json: string; + readonly metadata_json: string; + readonly application_event_version: number; + readonly causation_event_id: string | null; + readonly correlation_id: string | null; +} + +const decodeV2EventJson = Schema.decodeUnknownEffect(OrchestrationV2DomainEventJson); +const encodeV2EventJson = Schema.encodeEffect(OrchestrationV2DomainEventJson); +const decodeV2StoredEvent = Schema.decodeUnknownEffect(OrchestrationV2StoredEvent); +const decodeJson = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString); +const encodeJson = Schema.encodeEffect(Schema.UnknownFromJsonString); + +function metadataForV2Event(event: OrchestrationV2DomainEvent): Record { + return { + ...(event.runId === undefined ? {} : { runId: event.runId }), + ...(event.nodeId === undefined ? {} : { nodeId: event.nodeId }), + ...(event.driver === undefined ? {} : { driver: event.driver }), + ...(event.providerInstanceId === undefined + ? {} + : { providerInstanceId: event.providerInstanceId }), + ...(event.rawEventId === undefined ? {} : { rawEventId: event.rawEventId }), + }; +} + +const rowToV2StoredEvent = Effect.fn("OrchestrationEventStore.rowToV2StoredEvent")(function* ( + row: ApplicationEventRow, +) { + const payload = yield* decodeJson(row.payload_json); + const metadata = yield* decodeJson(row.metadata_json); + const values = + typeof metadata === "object" && metadata !== null ? (metadata as Record) : {}; + const event = yield* decodeV2EventJson({ + id: row.event_id, + threadId: row.stream_id, + type: row.event_type, + occurredAt: row.occurred_at, + payload, + ...(values.runId === undefined ? {} : { runId: values.runId }), + ...(values.nodeId === undefined ? {} : { nodeId: values.nodeId }), + ...(values.driver === undefined ? {} : { driver: values.driver }), + ...(values.providerInstanceId === undefined + ? {} + : { providerInstanceId: values.providerInstanceId }), + ...(values.rawEventId === undefined ? {} : { rawEventId: values.rawEventId }), + }); + return yield* decodeV2StoredEvent({ + sequence: row.sequence, + commandId: row.command_id, + event, + }); +}); + +const rowToProjectEvent = Effect.fn("OrchestrationEventStore.rowToProjectEvent")(function* ( + row: ApplicationEventRow, +) { + const event = yield* decodeEvent({ + sequence: row.sequence, + eventId: row.event_id, + type: row.event_type, + aggregateKind: row.aggregate_kind, + aggregateId: row.stream_id, + occurredAt: row.occurred_at, + commandId: row.command_id, + causationEventId: row.causation_event_id, + correlationId: row.correlation_id, + payload: yield* decodeJson(row.payload_json), + metadata: yield* decodeJson(row.metadata_json), + }); + switch (event.type) { + case "project.created": + case "project.meta-updated": + case "project.deleted": + return event; + default: + return yield* Effect.die(`Expected a project event, received '${event.type}'.`); + } +}); + +function rowToApplicationStoredEvent( + row: ApplicationEventRow, +): Effect.Effect { + return row.aggregate_kind === "project" ? rowToProjectEvent(row) : rowToV2StoredEvent(row); +} + function inferActorKind( event: Omit, ): Schema.Schema.Type { @@ -98,6 +197,7 @@ function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: st const makeEventStore = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; + const committedEvents = yield* PubSub.unbounded(); const appendEventRow = SqlSchema.findOne({ Request: AppendEventRequestSchema, @@ -117,6 +217,7 @@ const makeEventStore = Effect.gen(function* () { actor_kind, payload_json, metadata_json + , application_event_version ) VALUES ( ${request.eventId}, @@ -141,6 +242,7 @@ const makeEventStore = Effect.gen(function* () { ${request.actorKind}, ${request.payloadJson}, ${request.metadataJson} + , ${request.applicationEventVersion} ) RETURNING sequence, @@ -176,6 +278,7 @@ const makeEventStore = Effect.gen(function* () { metadata_json AS "metadata" FROM orchestration_events WHERE sequence > ${request.sequenceExclusive} + AND (application_event_version = 1 OR aggregate_kind = 'project') ORDER BY sequence ASC LIMIT ${request.limit} `, @@ -194,6 +297,7 @@ const makeEventStore = Effect.gen(function* () { commandId: event.commandId, payloadJson: event.payload, metadataJson: event.metadata, + applicationEventVersion: event.aggregateKind === "project" ? 2 : 1, }).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( @@ -260,10 +364,229 @@ const makeEventStore = Effect.gen(function* () { return readPage(sequenceExclusive, normalizedLimit); }; + const readApplicationRows = (input: { + readonly afterSequence: number; + readonly throughSequence?: number; + readonly threadId?: ThreadId; + readonly commandId?: CommandId; + readonly onlyAgentEvents?: boolean; + readonly limit: number; + }) => + sql` + SELECT + sequence, + event_id, + command_id, + aggregate_kind, + stream_id, + event_type, + occurred_at, + payload_json, + metadata_json, + application_event_version, + causation_event_id, + correlation_id + FROM orchestration_events + WHERE sequence > ${input.afterSequence} + AND sequence <= ${input.throughSequence ?? Number.MAX_SAFE_INTEGER} + AND ( + (${input.onlyAgentEvents === true ? 1 : 0} = 0 AND aggregate_kind = 'project') + OR (application_event_version = 2 AND aggregate_kind = 'thread') + ) + AND (${input.threadId ?? null} IS NULL OR stream_id = ${input.threadId ?? null}) + AND (${input.commandId ?? null} IS NULL OR command_id = ${input.commandId ?? null}) + ORDER BY sequence ASC + LIMIT ${input.limit} + `; + + const appendAgentEvents: OrchestrationEventStoreShape["appendAgentEvents"] = (input) => + Effect.forEach( + input.events, + (event) => + Effect.gen(function* () { + const encoded = yield* encodeV2EventJson(event); + const rows = yield* sql<{ readonly sequence: number }>` + INSERT INTO orchestration_events ( + event_id, + aggregate_kind, + stream_id, + stream_version, + event_type, + occurred_at, + command_id, + causation_event_id, + correlation_id, + actor_kind, + payload_json, + metadata_json, + application_event_version + ) + VALUES ( + ${event.id}, + 'thread', + ${event.threadId}, + COALESCE( + ( + SELECT MAX(stream_version) + 1 + FROM orchestration_events + WHERE aggregate_kind = 'thread' AND stream_id = ${event.threadId} + ), + 0 + ), + ${event.type}, + ${encoded.occurredAt}, + ${input.commandId ?? null}, + NULL, + ${input.commandId ?? null}, + ${event.rawEventId === undefined ? "server" : "provider"}, + ${yield* encodeJson(encoded.payload)}, + ${yield* encodeJson(metadataForV2Event(event))}, + 2 + ) + RETURNING sequence + `; + return yield* decodeV2StoredEvent({ + sequence: rows[0]?.sequence, + commandId: input.commandId ?? null, + event, + }); + }), + { concurrency: 1 }, + ).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "OrchestrationEventStore.appendAgentEvents:insert", + "OrchestrationEventStore.appendAgentEvents:decode", + ), + ), + ); + + const readAgentEvents: OrchestrationEventStoreShape["readAgentEvents"] = (input) => + Stream.fromEffect( + readApplicationRows({ + afterSequence: input?.afterSequence ?? 0, + ...(input?.throughSequence === undefined ? {} : { throughSequence: input.throughSequence }), + ...(input?.threadId === undefined ? {} : { threadId: input.threadId }), + ...(input?.commandId === undefined ? {} : { commandId: input.commandId }), + onlyAgentEvents: true, + limit: input?.limit ?? DEFAULT_READ_FROM_SEQUENCE_LIMIT, + }).pipe( + Effect.mapError(toPersistenceSqlError("OrchestrationEventStore.readAgentEvents:query")), + ), + ).pipe( + Stream.flatMap(Stream.fromIterable), + Stream.mapEffect((row) => + rowToV2StoredEvent(row).pipe( + Effect.mapError( + toPersistenceDecodeError("OrchestrationEventStore.readAgentEvents:decode"), + ), + ), + ), + ); + + const latestAgentSequence: OrchestrationEventStoreShape["latestAgentSequence"] = (threadId) => + sql<{ readonly sequence: number | null }>` + SELECT MAX(sequence) AS sequence + FROM orchestration_events + WHERE application_event_version = 2 + AND aggregate_kind = 'thread' + AND (${threadId ?? null} IS NULL OR stream_id = ${threadId ?? null}) + `.pipe( + Effect.map((rows) => rows[0]?.sequence ?? 0), + Effect.mapError(toPersistenceSqlError("OrchestrationEventStore.latestAgentSequence:query")), + ); + + const latestApplicationSequence = sql<{ readonly sequence: number | null }>` + SELECT MAX(sequence) AS sequence + FROM orchestration_events + WHERE aggregate_kind = 'project' + OR (application_event_version = 2 AND aggregate_kind = 'thread') + `.pipe( + Effect.map((rows) => rows[0]?.sequence ?? 0), + Effect.mapError( + toPersistenceSqlError("OrchestrationEventStore.latestApplicationSequence:query"), + ), + ); + + const readApplicationEvents = (input: { + readonly afterSequence: number; + readonly throughSequence: number; + readonly limit: number; + }): Stream.Stream => + Stream.fromEffect( + readApplicationRows(input).pipe( + Effect.mapError( + toPersistenceSqlError("OrchestrationEventStore.readApplicationEvents:query"), + ), + ), + ).pipe( + Stream.flatMap(Stream.fromIterable), + Stream.mapEffect((row) => + rowToApplicationStoredEvent(row).pipe( + Effect.mapError( + toPersistenceDecodeError("OrchestrationEventStore.readApplicationEvents:decode"), + ), + ), + ), + ); + + const catchUpApplicationEvents = (input: { + readonly afterSequence: number; + readonly throughSequence: number; + }): Stream.Stream => { + const loop = ( + afterSequence: number, + ): Stream.Stream => + Stream.unwrap( + readApplicationEvents({ + afterSequence, + throughSequence: input.throughSequence, + limit: READ_PAGE_SIZE, + }).pipe( + Stream.runCollect, + Effect.map((chunk) => Array.from(chunk)), + Effect.map((events) => { + if (events.length === 0) return Stream.empty; + const current = Stream.fromIterable(events); + const last = events.at(-1)?.sequence ?? input.throughSequence; + return events.length < READ_PAGE_SIZE || last >= input.throughSequence + ? current + : Stream.concat(current, loop(last)); + }), + ), + ); + return loop(input.afterSequence); + }; + + const streamApplicationEvents: OrchestrationEventStoreShape["streamApplicationEvents"] = ( + input, + ) => + Stream.unwrap( + Effect.gen(function* () { + const subscription = yield* PubSub.subscribe(committedEvents); + const highWater = yield* latestApplicationSequence; + const afterSequence = input?.afterSequence ?? 0; + const replay = catchUpApplicationEvents({ + afterSequence, + throughSequence: highWater, + }); + const live = Stream.fromSubscription(subscription).pipe( + Stream.filter((event) => event.sequence > Math.max(highWater, afterSequence)), + ); + return Stream.concat(replay, live); + }), + ); + return { append, readFromSequence, readAll: () => readFromSequence(0, Number.MAX_SAFE_INTEGER), + appendAgentEvents, + readAgentEvents, + latestAgentSequence, + latestApplicationSequence, + publishCommitted: (events) => PubSub.publishAll(committedEvents, events).pipe(Effect.asVoid), + streamApplicationEvents, } satisfies OrchestrationEventStoreShape; }); diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index ba1131ee259..cb6e5ea33af 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -45,6 +45,12 @@ import Migration0029 from "./Migrations/029_ProjectionThreadDetailOrderingIndexe import Migration0030 from "./Migrations/030_ProjectionThreadShellArchiveIndexes.ts"; import Migration0031 from "./Migrations/031_AuthAuthorizationScopes.ts"; import Migration0032 from "./Migrations/032_AuthPairingProofKeyThumbprint.ts"; +import Migration0033 from "./Migrations/033_OrchestrationV2.ts"; +import Migration0034 from "./Migrations/034_OrchestrationV2Subagents.ts"; +import Migration0035 from "./Migrations/035_OrchestrationV2Foundation.ts"; +import Migration0036 from "./Migrations/036_OrchestrationV2ProviderSessionBindings.ts"; +import Migration0037 from "./Migrations/037_OrchestrationV2ThreadLaunchWorkflows.ts"; +import Migration0038 from "./Migrations/038_ApplicationEventSource.ts"; /** * Migration loader with all migrations defined inline. @@ -89,6 +95,12 @@ export const migrationEntries = [ [30, "ProjectionThreadShellArchiveIndexes", Migration0030], [31, "AuthAuthorizationScopes", Migration0031], [32, "AuthPairingProofKeyThumbprint", Migration0032], + [33, "OrchestrationV2", Migration0033], + [34, "OrchestrationV2Subagents", Migration0034], + [35, "OrchestrationV2Foundation", Migration0035], + [36, "OrchestrationV2ProviderSessionBindings", Migration0036], + [37, "OrchestrationV2ThreadLaunchWorkflows", Migration0037], + [38, "ApplicationEventSource", Migration0038], ] as const; export const makeMigrationLoader = (throughId?: number) => diff --git a/apps/server/src/persistence/Migrations/033_034_OrchestrationV2.test.ts b/apps/server/src/persistence/Migrations/033_034_OrchestrationV2.test.ts new file mode 100644 index 00000000000..1715c84a74c --- /dev/null +++ b/apps/server/src/persistence/Migrations/033_034_OrchestrationV2.test.ts @@ -0,0 +1,95 @@ +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { runMigrations } from "../Migrations.ts"; +import * as NodeSqliteClient from "../NodeSqliteClient.ts"; + +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); + +layer("033_034_OrchestrationV2", (it) => { + it.effect("installs the orchestration v2 and subagent schemas", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* runMigrations({ toMigrationInclusive: 34 }); + + const migrations = yield* sql<{ + readonly migration_id: number; + readonly name: string; + }>` + SELECT migration_id, name + FROM effect_sql_migrations + WHERE migration_id IN (33, 34) + ORDER BY migration_id + `; + assert.deepStrictEqual(migrations, [ + { + migration_id: 33, + name: "OrchestrationV2", + }, + { + migration_id: 34, + name: "OrchestrationV2Subagents", + }, + ]); + + const eventColumns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(orchestration_v2_events) + `; + const subagentColumns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(orchestration_v2_projection_subagents) + `; + + assert.ok(eventColumns.some((column) => column.name === "event_id")); + assert.ok(subagentColumns.some((column) => column.name === "child_thread_id")); + }), + ); + + it.effect("backfills provider-session thread bindings in migration 036", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* runMigrations({ toMigrationInclusive: 35 }); + yield* sql` + INSERT INTO orchestration_v2_projection_provider_sessions ( + provider_session_id, + thread_id, + provider, + driver, + provider_instance_id, + status, + model, + updated_at, + payload_json + ) VALUES ( + 'provider-session:shared', + 'thread:existing', + 'codex', + 'codex', + 'codex', + 'ready', + 'gpt-5.4', + '2026-01-01T00:00:00.000Z', + '{}' + ) + `; + + yield* runMigrations({ toMigrationInclusive: 36 }); + + const bindings = yield* sql<{ + readonly provider_session_id: string; + readonly thread_id: string; + }>` + SELECT provider_session_id, thread_id + FROM orchestration_v2_projection_provider_session_bindings + `; + assert.deepStrictEqual(bindings, [ + { + provider_session_id: "provider-session:shared", + thread_id: "thread:existing", + }, + ]); + }), + ); +}); diff --git a/apps/server/src/persistence/Migrations/033_OrchestrationV2.ts b/apps/server/src/persistence/Migrations/033_OrchestrationV2.ts new file mode 100644 index 00000000000..99508f8efc0 --- /dev/null +++ b/apps/server/src/persistence/Migrations/033_OrchestrationV2.ts @@ -0,0 +1,303 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +// Base schema for the orchestration V2 event log and projections. +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql` + CREATE TABLE orchestration_v2_events ( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + event_id TEXT NOT NULL UNIQUE, + command_id TEXT, + thread_id TEXT NOT NULL, + run_id TEXT, + node_id TEXT, + provider TEXT, + raw_event_id TEXT, + event_type TEXT NOT NULL, + occurred_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_events_command_idx ON orchestration_v2_events(command_id, sequence)`; + yield* sql`CREATE INDEX orchestration_v2_events_thread_sequence_idx ON orchestration_v2_events(thread_id, sequence)`; + yield* sql`CREATE INDEX orchestration_v2_events_thread_type_sequence_idx ON orchestration_v2_events(thread_id, event_type, sequence)`; + yield* sql`CREATE INDEX orchestration_v2_events_run_sequence_idx ON orchestration_v2_events(run_id, sequence)`; + yield* sql`CREATE INDEX orchestration_v2_events_node_sequence_idx ON orchestration_v2_events(node_id, sequence)`; + yield* sql`CREATE INDEX orchestration_v2_events_raw_event_idx ON orchestration_v2_events(raw_event_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_command_receipts ( + command_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + command_type TEXT NOT NULL, + accepted_at TEXT NOT NULL, + result_sequence INTEGER NOT NULL, + status TEXT NOT NULL, + error TEXT + ) + `; + yield* sql`CREATE INDEX orchestration_v2_command_receipts_thread_sequence_idx ON orchestration_v2_command_receipts(thread_id, result_sequence)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_threads ( + thread_id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + title TEXT NOT NULL, + default_provider TEXT NOT NULL, + runtime_mode TEXT NOT NULL, + interaction_mode TEXT NOT NULL, + active_provider_thread_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + archived_at TEXT, + deleted_at TEXT, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_threads_project_updated_idx ON orchestration_v2_projection_threads(project_id, updated_at)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_runs ( + run_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + ordinal INTEGER NOT NULL, + provider TEXT NOT NULL, + provider_thread_id TEXT, + status TEXT NOT NULL, + requested_at TEXT NOT NULL, + completed_at TEXT, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE UNIQUE INDEX orchestration_v2_projection_runs_thread_ordinal_idx ON orchestration_v2_projection_runs(thread_id, ordinal)`; + yield* sql`CREATE INDEX orchestration_v2_projection_runs_provider_thread_idx ON orchestration_v2_projection_runs(provider_thread_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_runs_thread_status_idx ON orchestration_v2_projection_runs(thread_id, status)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_run_attempts ( + attempt_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + run_id TEXT NOT NULL, + attempt_ordinal INTEGER NOT NULL, + root_node_id TEXT NOT NULL, + provider TEXT NOT NULL, + provider_thread_id TEXT NOT NULL, + provider_turn_id TEXT, + status TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE UNIQUE INDEX orchestration_v2_projection_run_attempts_run_ordinal_idx ON orchestration_v2_projection_run_attempts(run_id, attempt_ordinal)`; + yield* sql`CREATE INDEX orchestration_v2_projection_run_attempts_thread_idx ON orchestration_v2_projection_run_attempts(thread_id, run_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_nodes ( + node_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + run_id TEXT, + parent_node_id TEXT, + root_node_id TEXT NOT NULL, + kind TEXT NOT NULL, + status TEXT NOT NULL, + provider_thread_id TEXT, + provider_turn_id TEXT, + runtime_request_id TEXT, + checkpoint_scope_id TEXT, + started_at TEXT, + completed_at TEXT, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_nodes_thread_run_idx ON orchestration_v2_projection_nodes(thread_id, run_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_nodes_parent_idx ON orchestration_v2_projection_nodes(parent_node_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_nodes_provider_turn_idx ON orchestration_v2_projection_nodes(provider_turn_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_provider_sessions ( + provider_session_id TEXT PRIMARY KEY, + thread_id TEXT, + provider TEXT NOT NULL, + status TEXT NOT NULL, + model TEXT, + updated_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_provider_sessions_thread_idx ON orchestration_v2_projection_provider_sessions(thread_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_provider_sessions_provider_status_idx ON orchestration_v2_projection_provider_sessions(provider, status)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_provider_threads ( + provider_thread_id TEXT PRIMARY KEY, + thread_id TEXT, + owner_node_id TEXT, + provider TEXT NOT NULL, + provider_session_id TEXT, + status TEXT NOT NULL, + first_run_ordinal INTEGER, + last_run_ordinal INTEGER, + updated_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_provider_threads_thread_idx ON orchestration_v2_projection_provider_threads(thread_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_provider_threads_session_idx ON orchestration_v2_projection_provider_threads(provider_session_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_provider_threads_owner_idx ON orchestration_v2_projection_provider_threads(owner_node_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_provider_turns ( + provider_turn_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + provider_thread_id TEXT NOT NULL, + node_id TEXT NOT NULL, + run_attempt_id TEXT, + ordinal INTEGER NOT NULL, + status TEXT NOT NULL, + started_at TEXT, + completed_at TEXT, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_provider_turns_thread_idx ON orchestration_v2_projection_provider_turns(thread_id)`; + yield* sql`CREATE UNIQUE INDEX orchestration_v2_projection_provider_turns_thread_ordinal_idx ON orchestration_v2_projection_provider_turns(provider_thread_id, ordinal)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_runtime_requests ( + runtime_request_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + node_id TEXT NOT NULL, + provider_turn_id TEXT, + kind TEXT NOT NULL, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + resolved_at TEXT, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_runtime_requests_thread_status_idx ON orchestration_v2_projection_runtime_requests(thread_id, status)`; + yield* sql`CREATE INDEX orchestration_v2_projection_runtime_requests_provider_turn_idx ON orchestration_v2_projection_runtime_requests(provider_turn_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_messages ( + message_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + run_id TEXT, + node_id TEXT, + role TEXT NOT NULL, + streaming INTEGER NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_messages_thread_created_idx ON orchestration_v2_projection_messages(thread_id, created_at, message_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_messages_run_idx ON orchestration_v2_projection_messages(run_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_messages_node_idx ON orchestration_v2_projection_messages(node_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_plans ( + plan_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + run_id TEXT, + node_id TEXT NOT NULL, + kind TEXT NOT NULL, + status TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_plans_thread_idx ON orchestration_v2_projection_plans(thread_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_plans_run_idx ON orchestration_v2_projection_plans(run_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_turn_items ( + turn_item_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + run_id TEXT, + node_id TEXT, + provider_thread_id TEXT, + provider_turn_id TEXT, + parent_item_id TEXT, + ordinal INTEGER NOT NULL, + type TEXT NOT NULL, + status TEXT NOT NULL, + updated_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_turn_items_thread_ordinal_idx ON orchestration_v2_projection_turn_items(thread_id, ordinal, turn_item_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_turn_items_run_ordinal_idx ON orchestration_v2_projection_turn_items(run_id, ordinal)`; + yield* sql`CREATE INDEX orchestration_v2_projection_turn_items_node_ordinal_idx ON orchestration_v2_projection_turn_items(node_id, ordinal)`; + yield* sql`CREATE INDEX orchestration_v2_projection_turn_items_provider_turn_idx ON orchestration_v2_projection_turn_items(provider_turn_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_checkpoint_scopes ( + scope_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + run_id TEXT, + node_id TEXT NOT NULL, + parent_scope_id TEXT, + provider_thread_id TEXT, + kind TEXT NOT NULL, + ordinal_within_parent INTEGER NOT NULL, + advances_app_run_count INTEGER NOT NULL, + created_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_checkpoint_scopes_thread_idx ON orchestration_v2_projection_checkpoint_scopes(thread_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_checkpoint_scopes_parent_idx ON orchestration_v2_projection_checkpoint_scopes(parent_scope_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_checkpoints ( + checkpoint_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + scope_id TEXT NOT NULL, + run_id TEXT, + node_id TEXT NOT NULL, + parent_checkpoint_id TEXT, + ordinal_within_scope INTEGER NOT NULL, + app_run_ordinal INTEGER, + status TEXT NOT NULL, + captured_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE UNIQUE INDEX orchestration_v2_projection_checkpoints_scope_ordinal_idx ON orchestration_v2_projection_checkpoints(scope_id, ordinal_within_scope)`; + yield* sql`CREATE INDEX orchestration_v2_projection_checkpoints_thread_idx ON orchestration_v2_projection_checkpoints(thread_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_checkpoints_parent_idx ON orchestration_v2_projection_checkpoints(parent_checkpoint_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_context_handoffs ( + context_handoff_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + target_run_id TEXT NOT NULL, + to_provider_thread_id TEXT NOT NULL, + strategy TEXT NOT NULL, + status TEXT NOT NULL, + updated_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_context_handoffs_thread_idx ON orchestration_v2_projection_context_handoffs(thread_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_context_handoffs_target_run_idx ON orchestration_v2_projection_context_handoffs(target_run_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_projection_context_transfers ( + context_transfer_id TEXT PRIMARY KEY, + source_thread_id TEXT NOT NULL, + target_thread_id TEXT NOT NULL, + target_run_id TEXT, + type TEXT NOT NULL, + status TEXT NOT NULL, + source_provider TEXT, + target_provider TEXT, + updated_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_context_transfers_source_thread_idx ON orchestration_v2_projection_context_transfers(source_thread_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_context_transfers_target_thread_idx ON orchestration_v2_projection_context_transfers(target_thread_id, status)`; + yield* sql`CREATE INDEX orchestration_v2_projection_context_transfers_target_run_idx ON orchestration_v2_projection_context_transfers(target_run_id)`; +}); diff --git a/apps/server/src/persistence/Migrations/034_OrchestrationV2Subagents.ts b/apps/server/src/persistence/Migrations/034_OrchestrationV2Subagents.ts new file mode 100644 index 00000000000..b43b3a8e3d3 --- /dev/null +++ b/apps/server/src/persistence/Migrations/034_OrchestrationV2Subagents.ts @@ -0,0 +1,28 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql` + CREATE TABLE orchestration_v2_projection_subagents ( + subagent_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + run_id TEXT, + parent_node_id TEXT NOT NULL, + provider TEXT NOT NULL, + provider_thread_id TEXT, + child_thread_id TEXT, + origin TEXT NOT NULL, + status TEXT NOT NULL, + started_at TEXT, + completed_at TEXT, + updated_at TEXT NOT NULL, + payload_json TEXT NOT NULL + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_subagents_thread_idx ON orchestration_v2_projection_subagents(thread_id, started_at, subagent_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_subagents_parent_node_idx ON orchestration_v2_projection_subagents(parent_node_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_subagents_provider_thread_idx ON orchestration_v2_projection_subagents(provider_thread_id)`; + yield* sql`CREATE INDEX orchestration_v2_projection_subagents_child_thread_idx ON orchestration_v2_projection_subagents(child_thread_id)`; +}); diff --git a/apps/server/src/persistence/Migrations/035_OrchestrationV2Foundation.ts b/apps/server/src/persistence/Migrations/035_OrchestrationV2Foundation.ts new file mode 100644 index 00000000000..8c333707af6 --- /dev/null +++ b/apps/server/src/persistence/Migrations/035_OrchestrationV2Foundation.ts @@ -0,0 +1,172 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +/** + * Production-facing V2 persistence additions. + * + * The original V2 schema used a single `provider` column for both configured + * instance routing and driver identity. Keep those columns in place for + * migration compatibility. They remain write-through shadows where the old + * schema declared them NOT NULL; all V2 reads and indexes use the explicit + * columns added here. + */ +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql`ALTER TABLE orchestration_v2_events ADD COLUMN driver TEXT`; + yield* sql`ALTER TABLE orchestration_v2_events ADD COLUMN provider_instance_id TEXT`; + yield* sql` + UPDATE orchestration_v2_events + SET + driver = provider, + provider_instance_id = COALESCE( + json_extract(payload_json, '$.providerInstanceId'), + provider + ) + `; + yield* sql`CREATE INDEX orchestration_v2_events_instance_sequence_idx ON orchestration_v2_events(provider_instance_id, sequence)`; + + yield* sql`ALTER TABLE orchestration_v2_projection_threads ADD COLUMN provider_instance_id TEXT`; + yield* sql` + UPDATE orchestration_v2_projection_threads + SET provider_instance_id = COALESCE( + json_extract(payload_json, '$.providerInstanceId'), + default_provider + ) + `; + + yield* sql`ALTER TABLE orchestration_v2_projection_runs ADD COLUMN provider_instance_id TEXT`; + yield* sql` + UPDATE orchestration_v2_projection_runs + SET provider_instance_id = COALESCE( + json_extract(payload_json, '$.providerInstanceId'), + provider + ) + `; + + yield* sql`ALTER TABLE orchestration_v2_projection_run_attempts ADD COLUMN provider_instance_id TEXT`; + yield* sql` + UPDATE orchestration_v2_projection_run_attempts + SET provider_instance_id = COALESCE( + json_extract(payload_json, '$.providerInstanceId'), + provider + ) + `; + + yield* sql`ALTER TABLE orchestration_v2_projection_provider_sessions ADD COLUMN driver TEXT`; + yield* sql`ALTER TABLE orchestration_v2_projection_provider_sessions ADD COLUMN provider_instance_id TEXT`; + yield* sql` + UPDATE orchestration_v2_projection_provider_sessions + SET + driver = COALESCE(json_extract(payload_json, '$.driver'), provider), + provider_instance_id = COALESCE( + json_extract(payload_json, '$.providerInstanceId'), + provider + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_provider_sessions_instance_status_idx ON orchestration_v2_projection_provider_sessions(provider_instance_id, status)`; + + yield* sql`ALTER TABLE orchestration_v2_projection_provider_threads ADD COLUMN driver TEXT`; + yield* sql`ALTER TABLE orchestration_v2_projection_provider_threads ADD COLUMN provider_instance_id TEXT`; + yield* sql` + UPDATE orchestration_v2_projection_provider_threads + SET + driver = COALESCE(json_extract(payload_json, '$.driver'), provider), + provider_instance_id = COALESCE( + json_extract(payload_json, '$.providerInstanceId'), + provider + ) + `; + yield* sql`CREATE INDEX orchestration_v2_projection_provider_threads_instance_status_idx ON orchestration_v2_projection_provider_threads(provider_instance_id, status)`; + + yield* sql`ALTER TABLE orchestration_v2_projection_subagents ADD COLUMN driver TEXT`; + yield* sql`ALTER TABLE orchestration_v2_projection_subagents ADD COLUMN provider_instance_id TEXT`; + yield* sql` + UPDATE orchestration_v2_projection_subagents + SET + driver = json_extract(payload_json, '$.driver'), + provider_instance_id = COALESCE( + json_extract(payload_json, '$.providerInstanceId'), + provider + ) + `; + + yield* sql`ALTER TABLE orchestration_v2_projection_context_transfers ADD COLUMN source_provider_instance_id TEXT`; + yield* sql`ALTER TABLE orchestration_v2_projection_context_transfers ADD COLUMN target_provider_instance_id TEXT`; + yield* sql` + UPDATE orchestration_v2_projection_context_transfers + SET + source_provider_instance_id = COALESCE( + json_extract(payload_json, '$.sourceProviderInstanceId'), + source_provider + ), + target_provider_instance_id = COALESCE( + json_extract(payload_json, '$.targetProviderInstanceId'), + target_provider + ) + `; + + yield* sql` + CREATE TABLE orchestration_v2_effect_outbox ( + effect_id TEXT PRIMARY KEY, + command_id TEXT NOT NULL, + thread_id TEXT NOT NULL, + effect_type TEXT NOT NULL, + payload_json TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'succeeded', 'failed')), + attempt_count INTEGER NOT NULL DEFAULT 0, + available_at TEXT NOT NULL, + lease_owner TEXT, + lease_expires_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + completed_at TEXT, + last_error TEXT + ) + `; + yield* sql`CREATE INDEX orchestration_v2_effect_outbox_claim_idx ON orchestration_v2_effect_outbox(status, available_at, lease_expires_at, created_at)`; + yield* sql`CREATE INDEX orchestration_v2_effect_outbox_command_idx ON orchestration_v2_effect_outbox(command_id, effect_id)`; + + yield* sql` + CREATE TABLE orchestration_v2_turn_item_positions ( + thread_id TEXT NOT NULL, + turn_item_id TEXT NOT NULL, + ordinal INTEGER NOT NULL, + PRIMARY KEY (thread_id, turn_item_id), + UNIQUE (thread_id, ordinal) + ) + `; + yield* sql` + INSERT OR IGNORE INTO orchestration_v2_turn_item_positions ( + thread_id, + turn_item_id, + ordinal + ) + SELECT thread_id, turn_item_id, ordinal + FROM orchestration_v2_projection_turn_items + ORDER BY thread_id, ordinal, turn_item_id + `; + + yield* sql` + CREATE TABLE orchestration_v2_projection_metadata ( + projection_name TEXT PRIMARY KEY, + schema_version INTEGER NOT NULL, + last_sequence INTEGER NOT NULL, + updated_at TEXT NOT NULL + ) + `; + yield* sql` + INSERT INTO orchestration_v2_projection_metadata ( + projection_name, + schema_version, + last_sequence, + updated_at + ) + VALUES ( + 'thread-projections', + 1, + COALESCE((SELECT MAX(sequence) FROM orchestration_v2_events), 0), + strftime('%Y-%m-%dT%H:%M:%fZ', 'now') + ) + `; +}); diff --git a/apps/server/src/persistence/Migrations/036_OrchestrationV2ProviderSessionBindings.ts b/apps/server/src/persistence/Migrations/036_OrchestrationV2ProviderSessionBindings.ts new file mode 100644 index 00000000000..22abde6264e --- /dev/null +++ b/apps/server/src/persistence/Migrations/036_OrchestrationV2ProviderSessionBindings.ts @@ -0,0 +1,27 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql` + CREATE TABLE orchestration_v2_projection_provider_session_bindings ( + provider_session_id TEXT NOT NULL, + thread_id TEXT NOT NULL, + PRIMARY KEY (provider_session_id, thread_id) + ) + `; + yield* sql` + INSERT OR IGNORE INTO orchestration_v2_projection_provider_session_bindings ( + provider_session_id, + thread_id + ) + SELECT provider_session_id, thread_id + FROM orchestration_v2_projection_provider_sessions + WHERE thread_id IS NOT NULL + `; + yield* sql` + CREATE INDEX orchestration_v2_projection_provider_session_bindings_thread_idx + ON orchestration_v2_projection_provider_session_bindings(thread_id) + `; +}); diff --git a/apps/server/src/persistence/Migrations/037_OrchestrationV2ThreadLaunchWorkflows.ts b/apps/server/src/persistence/Migrations/037_OrchestrationV2ThreadLaunchWorkflows.ts new file mode 100644 index 00000000000..1f898f648b0 --- /dev/null +++ b/apps/server/src/persistence/Migrations/037_OrchestrationV2ThreadLaunchWorkflows.ts @@ -0,0 +1,23 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* sql` + CREATE TABLE IF NOT EXISTS orchestration_v2_thread_launch_workflows ( + command_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + project_id TEXT NOT NULL, + status TEXT NOT NULL, + title TEXT NOT NULL, + worktree_path TEXT, + branch TEXT, + setup_committed INTEGER NOT NULL DEFAULT 0, + thread_committed INTEGER NOT NULL DEFAULT 0, + message_committed INTEGER NOT NULL DEFAULT 0, + last_error TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + `; +}); diff --git a/apps/server/src/persistence/Migrations/038_ApplicationEventSource.test.ts b/apps/server/src/persistence/Migrations/038_ApplicationEventSource.test.ts new file mode 100644 index 00000000000..8919c6840a5 --- /dev/null +++ b/apps/server/src/persistence/Migrations/038_ApplicationEventSource.test.ts @@ -0,0 +1,135 @@ +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { runMigrations } from "../Migrations.ts"; +import * as NodeSqliteClient from "../NodeSqliteClient.ts"; + +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); + +layer("038_ApplicationEventSource", (it) => { + it.effect("moves V2 events and current project state behind one global sequence", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* runMigrations({ toMigrationInclusive: 37 }); + + yield* sql` + INSERT INTO orchestration_v2_events ( + event_id, + command_id, + thread_id, + run_id, + node_id, + provider, + driver, + provider_instance_id, + raw_event_id, + event_type, + occurred_at, + payload_json + ) VALUES ( + 'event:v2:existing', + 'command:v2:existing', + 'thread:v2:existing', + NULL, + NULL, + 'codex', + 'codex', + 'codex', + NULL, + 'thread.created', + '2026-06-20T00:00:00.000Z', + '{}' + ) + `; + yield* sql` + INSERT INTO orchestration_v2_command_receipts ( + command_id, + thread_id, + command_type, + accepted_at, + result_sequence, + status, + error + ) VALUES ( + 'command:v2:existing', + 'thread:v2:existing', + 'thread.create', + '2026-06-20T00:00:00.000Z', + 1, + 'accepted', + NULL + ) + `; + yield* sql` + INSERT INTO projection_projects ( + project_id, + title, + workspace_root, + default_model_selection_json, + scripts_json, + created_at, + updated_at, + deleted_at + ) VALUES ( + 'project:existing', + 'Existing project', + '/work/existing', + '{"instanceId":"codex","model":"gpt-5.4"}', + '[]', + '2026-06-19T00:00:00.000Z', + '2026-06-20T00:00:00.000Z', + NULL + ) + `; + + yield* runMigrations({ toMigrationInclusive: 38 }); + + const events = yield* sql<{ + readonly sequence: number; + readonly aggregate_kind: string; + readonly stream_id: string; + readonly event_type: string; + readonly application_event_version: number; + }>` + SELECT + sequence, + aggregate_kind, + stream_id, + event_type, + application_event_version + FROM orchestration_events + WHERE application_event_version = 2 + ORDER BY sequence ASC + `; + assert.deepStrictEqual( + events.map((event) => [event.aggregate_kind, event.stream_id, event.event_type]), + [ + ["thread", "thread:v2:existing", "thread.created"], + ["project", "project:existing", "project.created"], + ], + ); + assert.ok(events[0]!.sequence < events[1]!.sequence); + + const receipts = yield* sql<{ + readonly aggregate_kind: string; + readonly aggregate_id: string; + readonly command_type: string; + readonly result_sequence: number; + }>` + SELECT aggregate_kind, aggregate_id, command_type, result_sequence + FROM orchestration_command_receipts + WHERE command_id = 'command:v2:existing' + `; + assert.deepStrictEqual(receipts, [ + { + aggregate_kind: "thread", + aggregate_id: "thread:v2:existing", + command_type: "thread.create", + result_sequence: events[0]!.sequence, + }, + ]); + }), + ); +}); diff --git a/apps/server/src/persistence/Migrations/038_ApplicationEventSource.ts b/apps/server/src/persistence/Migrations/038_ApplicationEventSource.ts new file mode 100644 index 00000000000..e1ddef62cd9 --- /dev/null +++ b/apps/server/src/persistence/Migrations/038_ApplicationEventSource.ts @@ -0,0 +1,281 @@ +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +interface LegacyV2EventRow { + readonly event_id: string; + readonly command_id: string | null; + readonly thread_id: string; + readonly run_id: string | null; + readonly node_id: string | null; + readonly driver: string | null; + readonly provider_instance_id: string | null; + readonly raw_event_id: string | null; + readonly event_type: string; + readonly occurred_at: string; + readonly payload_json: string; +} + +interface ProjectProjectionRow { + readonly project_id: string; + readonly title: string; + readonly workspace_root: string; + readonly default_model_selection_json: string | null; + readonly scripts_json: string; + readonly created_at: string; + readonly updated_at: string; + readonly deleted_at: string | null; +} + +const decodeJson = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString); +const encodeJson = Schema.encodeEffect(Schema.UnknownFromJsonString); + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql` + ALTER TABLE orchestration_events + ADD COLUMN application_event_version INTEGER NOT NULL DEFAULT 1 + `; + yield* sql` + CREATE INDEX idx_orchestration_events_application_sequence + ON orchestration_events(application_event_version, sequence) + `; + yield* sql` + ALTER TABLE orchestration_command_receipts + ADD COLUMN command_type TEXT NOT NULL DEFAULT 'legacy' + `; + + const legacyV2Events = yield* sql` + SELECT + event_id, + command_id, + thread_id, + run_id, + node_id, + driver, + provider_instance_id, + raw_event_id, + event_type, + occurred_at, + payload_json + FROM orchestration_v2_events + ORDER BY sequence ASC + `; + + yield* Effect.forEach( + legacyV2Events, + (event) => + Effect.gen(function* () { + const metadata = yield* encodeJson({ + applicationEventVersion: 2, + ...(event.run_id === null ? {} : { runId: event.run_id }), + ...(event.node_id === null ? {} : { nodeId: event.node_id }), + ...(event.driver === null ? {} : { driver: event.driver }), + ...(event.provider_instance_id === null + ? {} + : { providerInstanceId: event.provider_instance_id }), + ...(event.raw_event_id === null ? {} : { rawEventId: event.raw_event_id }), + }); + yield* sql` + INSERT INTO orchestration_events ( + event_id, + aggregate_kind, + stream_id, + stream_version, + event_type, + occurred_at, + command_id, + causation_event_id, + correlation_id, + actor_kind, + payload_json, + metadata_json, + application_event_version + ) + SELECT + ${event.event_id}, + 'thread', + ${event.thread_id}, + COALESCE( + ( + SELECT MAX(stream_version) + 1 + FROM orchestration_events + WHERE aggregate_kind = 'thread' AND stream_id = ${event.thread_id} + ), + 0 + ), + ${event.event_type}, + ${event.occurred_at}, + ${event.command_id}, + NULL, + ${event.command_id}, + ${event.raw_event_id === null ? "server" : "provider"}, + ${event.payload_json}, + ${metadata}, + 2 + WHERE NOT EXISTS ( + SELECT 1 FROM orchestration_events WHERE event_id = ${event.event_id} + ) + `; + }), + { concurrency: 1, discard: true }, + ); + + // ProjectService wrote this projection directly before the application event + // boundary was restored. Re-baseline every current row into the shared log so + // projection rebuilds preserve the exact pre-migration project state. + const projectRows = yield* sql` + SELECT + project_id, + title, + workspace_root, + default_model_selection_json, + scripts_json, + created_at, + updated_at, + deleted_at + FROM projection_projects + ORDER BY created_at ASC, project_id ASC + `; + yield* Effect.forEach( + projectRows, + (project) => + Effect.gen(function* () { + const createdEventId = `migration:38:project:${project.project_id}:baseline`; + const createdPayload = { + projectId: project.project_id, + title: project.title, + workspaceRoot: project.workspace_root, + defaultModelSelection: + project.default_model_selection_json === null + ? null + : yield* decodeJson(project.default_model_selection_json), + scripts: yield* decodeJson(project.scripts_json), + createdAt: project.created_at, + updatedAt: project.updated_at, + }; + const createdPayloadJson = yield* encodeJson(createdPayload); + const migrationMetadataJson = yield* encodeJson({ + applicationEventVersion: 2, + migration: 38, + }); + yield* sql` + INSERT INTO orchestration_events ( + event_id, + aggregate_kind, + stream_id, + stream_version, + event_type, + occurred_at, + command_id, + causation_event_id, + correlation_id, + actor_kind, + payload_json, + metadata_json, + application_event_version + ) + VALUES ( + ${createdEventId}, + 'project', + ${project.project_id}, + COALESCE( + ( + SELECT MAX(stream_version) + 1 + FROM orchestration_events + WHERE aggregate_kind = 'project' AND stream_id = ${project.project_id} + ), + 0 + ), + 'project.created', + ${project.updated_at}, + NULL, + NULL, + NULL, + 'server', + ${createdPayloadJson}, + ${migrationMetadataJson}, + 2 + ) + `; + + if (project.deleted_at !== null) { + const deletedPayloadJson = yield* encodeJson({ + projectId: project.project_id, + deletedAt: project.deleted_at, + }); + yield* sql` + INSERT INTO orchestration_events ( + event_id, + aggregate_kind, + stream_id, + stream_version, + event_type, + occurred_at, + command_id, + causation_event_id, + correlation_id, + actor_kind, + payload_json, + metadata_json, + application_event_version + ) + VALUES ( + ${`migration:38:project:${project.project_id}:deleted`}, + 'project', + ${project.project_id}, + ( + SELECT MAX(stream_version) + 1 + FROM orchestration_events + WHERE aggregate_kind = 'project' AND stream_id = ${project.project_id} + ), + 'project.deleted', + ${project.deleted_at}, + NULL, + ${createdEventId}, + NULL, + 'server', + ${deletedPayloadJson}, + ${migrationMetadataJson}, + 2 + ) + `; + } + }), + { concurrency: 1, discard: true }, + ); + + yield* sql` + INSERT INTO orchestration_command_receipts ( + command_id, + aggregate_kind, + aggregate_id, + accepted_at, + result_sequence, + status, + error, + command_type + ) + SELECT + command_id, + 'thread', + thread_id, + accepted_at, + COALESCE( + ( + SELECT MAX(events.sequence) + FROM orchestration_events events + WHERE events.application_event_version = 2 + AND events.command_id = receipts.command_id + ), + 0 + ), + status, + error, + command_type + FROM orchestration_v2_command_receipts receipts + WHERE TRUE + ON CONFLICT(command_id) DO NOTHING + `; +}); diff --git a/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts b/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts index 1498984827e..5c4eef2cc50 100644 --- a/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts +++ b/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts @@ -26,6 +26,7 @@ export const OrchestrationCommandReceipt = Schema.Struct({ commandId: CommandId, aggregateKind: OrchestrationAggregateKind, aggregateId: Schema.Union([ProjectId, ThreadId]), + commandType: Schema.String, acceptedAt: IsoDateTime, resultSequence: NonNegativeInt, status: OrchestrationCommandReceiptStatus, @@ -42,6 +43,10 @@ export type GetByCommandIdInput = typeof GetByCommandIdInput.Type; * OrchestrationCommandReceiptRepositoryShape - Service API for command receipts. */ export interface OrchestrationCommandReceiptRepositoryShape { + readonly insertIfAbsent: ( + receipt: OrchestrationCommandReceipt, + ) => Effect.Effect; + /** * Insert or replace a command receipt row. * diff --git a/apps/server/src/persistence/Services/OrchestrationEventStore.ts b/apps/server/src/persistence/Services/OrchestrationEventStore.ts index 8b465e7713e..85f108bae53 100644 --- a/apps/server/src/persistence/Services/OrchestrationEventStore.ts +++ b/apps/server/src/persistence/Services/OrchestrationEventStore.ts @@ -1,15 +1,23 @@ /** - * OrchestrationEventStore - Event store interface for orchestration events. + * Historical name for the shared application event store. * - * Owns durable append/replay access to the orchestration event stream. It does - * not reduce events into read models or apply command validation rules. + * Owns durable append/replay access for project events and V2 agent-thread + * events under one global sequence. It does not reduce events into read models + * or apply command validation rules. * * Uses Effect `Context.Service` for dependency injection and exposes typed * persistence/decode errors for event append and replay operations. * * @module OrchestrationEventStore */ -import { OrchestrationEvent } from "@t3tools/contracts"; +import type { + ApplicationStoredEvent, + CommandId, + OrchestrationEvent, + OrchestrationV2DomainEvent, + OrchestrationV2StoredEvent, + ThreadId, +} from "@t3tools/contracts"; import * as Context from "effect/Context"; import type * as Effect from "effect/Effect"; import type * as Stream from "effect/Stream"; @@ -52,6 +60,35 @@ export interface OrchestrationEventStoreShape { * @returns Stream containing all stored events. */ readonly readAll: () => Stream.Stream; + + /** Append V2 agent events to the same globally ordered application log. */ + readonly appendAgentEvents: (input: { + readonly commandId?: CommandId; + readonly events: ReadonlyArray; + }) => Effect.Effect, OrchestrationEventStoreError>; + + /** Read only V2 thread events from the application log. */ + readonly readAgentEvents: (input?: { + readonly afterSequence?: number; + readonly throughSequence?: number; + readonly threadId?: ThreadId; + readonly commandId?: CommandId; + readonly limit?: number; + }) => Stream.Stream; + + readonly latestAgentSequence: ( + threadId?: ThreadId, + ) => Effect.Effect; + + readonly latestApplicationSequence: Effect.Effect; + + /** Publish only after the surrounding event/projection transaction commits. */ + readonly publishCommitted: (events: ReadonlyArray) => Effect.Effect; + + /** Race-free replay-to-live stream for project and V2 thread events. */ + readonly streamApplicationEvents: (input?: { + readonly afterSequence?: number; + }) => Stream.Stream; } /** diff --git a/apps/server/src/project/ProjectService.test.ts b/apps/server/src/project/ProjectService.test.ts new file mode 100644 index 00000000000..7ee869bcac9 --- /dev/null +++ b/apps/server/src/project/ProjectService.test.ts @@ -0,0 +1,165 @@ +import { assert, it } from "@effect/vitest"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { CommandId, ProjectId, ProviderInstanceId } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import { TestClock } from "effect/testing"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { ServerConfig } from "../config.ts"; +import { ProjectServiceLayerLive } from "../orchestration-v2/runtimeLayer.ts"; +import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; +import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; +import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts"; +import * as ProjectService from "./ProjectService.ts"; +import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts"; + +const workspacePathsLayer = Layer.succeed(WorkspacePaths.WorkspacePaths, { + normalizeWorkspaceRoot: (workspaceRoot) => Effect.succeed(workspaceRoot.replace(/\/$/, "")), + resolveRelativePathWithinRoot: ({ workspaceRoot, relativePath }) => + Effect.succeed({ absolutePath: `${workspaceRoot}/${relativePath}`, relativePath }), +}); + +const metadataLayer = Layer.merge( + Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, { + resolve: (workspaceRoot) => + Effect.succeed({ + canonicalKey: `github.com/t3tools/${workspaceRoot.split("/").at(-1)}`, + locator: { + source: "git-remote" as const, + remoteName: "origin", + remoteUrl: `git@github.com:t3tools/${workspaceRoot.split("/").at(-1)}.git`, + }, + rootPath: workspaceRoot, + }), + }), + Layer.succeed(ProjectFaviconResolver.ProjectFaviconResolver, { + resolvePath: (workspaceRoot) => Effect.succeed(`${workspaceRoot}/favicon.svg`), + }), +); + +const TestLayer = ProjectServiceLayerLive.pipe( + Layer.provideMerge(workspacePathsLayer), + Layer.provideMerge(metadataLayer), + Layer.provideMerge(SqlitePersistenceMemory), + Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "project-service-test-" })), + Layer.provide(NodeServices.layer), +); + +it.layer(TestLayer)("ProjectService", (it) => { + it.effect("creates, updates, resolves, snapshots, and soft-deletes projects", () => + Effect.gen(function* () { + const service = yield* ProjectService.ProjectService; + const projectId = ProjectId.make("project:service-test"); + const modelSelection = { + instanceId: ProviderInstanceId.make("codex_custom"), + model: "gpt-5.1-codex", + } as const; + yield* TestClock.setTime(Date.parse("2026-06-20T10:00:00.000Z")); + + const created = yield* service.create({ + commandId: CommandId.make("command:project:create"), + projectId, + title: "Project", + workspaceRoot: "/work/project/", + defaultModelSelection: modelSelection, + scripts: [ + { + id: "setup", + name: "Setup", + command: "vp install", + icon: "configure", + runOnWorktreeCreate: true, + }, + ], + }); + assert.equal(created.workspaceRoot, "/work/project"); + assert.equal(created.repositoryIdentity?.canonicalKey, "github.com/t3tools/project"); + assert.equal(created.faviconPath, "/work/project/favicon.svg"); + + const updated = yield* service.update({ + commandId: CommandId.make("command:project:update"), + projectId, + title: "Renamed", + }); + assert.equal(updated.title, "Renamed"); + assert.equal(updated.createdAt, created.createdAt); + + const byId = yield* service.getById(projectId); + const byWorkspace = yield* service.getByWorkspaceRoot("/work/project/"); + assert.isTrue(Option.isSome(byId)); + assert.isTrue(Option.isSome(byWorkspace)); + assert.equal(Option.getOrThrow(byWorkspace).id, projectId); + assert.deepEqual( + (yield* service.snapshot).projects.map((project) => project.id), + [projectId], + ); + + const deleted = yield* service.delete({ + commandId: CommandId.make("command:project:delete"), + projectId, + }); + assert.isNotNull(deleted.deletedAt); + assert.isTrue(Option.isNone(yield* service.getById(projectId))); + assert.isTrue(Option.isSome(yield* service.getById(projectId, { includeDeleted: true }))); + assert.deepEqual((yield* service.snapshot).projects, []); + + const sql = yield* SqlClient.SqlClient; + const changes = yield* sql<{ readonly event_type: string }>` + SELECT event_type + FROM orchestration_events + WHERE aggregate_kind = 'project' AND stream_id = ${projectId} + ORDER BY sequence ASC + `; + assert.deepEqual( + changes.map((change) => change.event_type), + ["project.created", "project.meta-updated", "project.deleted"], + ); + }), + ); + + it.effect("rejects active workspace collisions", () => + Effect.gen(function* () { + const service = yield* ProjectService.ProjectService; + yield* TestClock.setTime(Date.parse("2026-06-20T10:00:00.000Z")); + yield* service.create({ + commandId: CommandId.make("command:collision:first"), + projectId: ProjectId.make("project:collision:first"), + title: "First", + workspaceRoot: "/work/shared", + }); + const error = yield* service + .create({ + commandId: CommandId.make("command:collision:second"), + projectId: ProjectId.make("project:collision:second"), + title: "Second", + workspaceRoot: "/work/shared", + }) + .pipe(Effect.flip); + assert.equal(error._tag, "ProjectConflictError"); + }), + ); + + it.effect("auto-bootstraps a workspace exactly once", () => + Effect.gen(function* () { + const service = yield* ProjectService.ProjectService; + yield* TestClock.setTime(Date.parse("2026-06-20T10:00:00.000Z")); + const input = { + commandId: CommandId.make("command:bootstrap:first"), + projectId: ProjectId.make("project:bootstrap"), + title: "Bootstrap", + workspaceRoot: "/work/bootstrap/", + }; + const first = yield* service.bootstrap(input); + const second = yield* service.bootstrap({ + ...input, + commandId: CommandId.make("command:bootstrap:second"), + projectId: ProjectId.make("project:bootstrap:unused"), + }); + assert.isTrue(first.created); + assert.isFalse(second.created); + assert.equal(second.project.id, first.project.id); + }), + ); +}); diff --git a/apps/server/src/project/ProjectService.ts b/apps/server/src/project/ProjectService.ts new file mode 100644 index 00000000000..1f3d9869f5b --- /dev/null +++ b/apps/server/src/project/ProjectService.ts @@ -0,0 +1,393 @@ +import { + type CommandId, + ModelSelection, + ProjectId, + type Project, + type ProjectScript, + type ProjectSnapshot, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; + +import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts"; +import * as ProjectionProjects from "../persistence/Services/ProjectionProjects.ts"; +import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts"; +import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts"; +import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; + +export interface ProjectCreateInput { + readonly commandId: CommandId; + readonly projectId: ProjectId; + readonly title: string; + readonly workspaceRoot: string; + readonly createWorkspaceRootIfMissing?: boolean; + readonly defaultModelSelection?: ModelSelection | null; + readonly scripts?: ReadonlyArray; +} + +export interface ProjectUpdateInput { + readonly commandId: CommandId; + readonly projectId: ProjectId; + readonly title?: string; + readonly workspaceRoot?: string; + readonly defaultModelSelection?: ModelSelection | null; + readonly scripts?: ReadonlyArray; +} + +export interface ProjectBootstrapInput extends ProjectCreateInput {} + +export interface ProjectDeleteInput { + readonly commandId: CommandId; + readonly projectId: ProjectId; +} + +export class ProjectNotFoundError extends Schema.TaggedErrorClass()( + "ProjectNotFoundError", + { projectId: ProjectId }, +) { + override get message(): string { + return `Project ${this.projectId} was not found.`; + } +} + +export class ProjectConflictError extends Schema.TaggedErrorClass()( + "ProjectConflictError", + { + projectId: ProjectId, + workspaceRoot: Schema.String, + conflictingProjectId: ProjectId, + }, +) { + override get message(): string { + return `Workspace ${this.workspaceRoot} already belongs to project ${this.conflictingProjectId}.`; + } +} + +export class ProjectOperationError extends Schema.TaggedErrorClass()( + "ProjectOperationError", + { + operation: Schema.Literals([ + "normalize-workspace", + "read-project", + "list-projects", + "dispatch-project-command", + "resolve-favicon", + ]), + projectId: Schema.optional(ProjectId), + workspaceRoot: Schema.optional(Schema.String), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Project operation '${this.operation}' failed${this.projectId === undefined ? "" : ` for ${this.projectId}`}.`; + } +} + +export type ProjectServiceError = + | ProjectNotFoundError + | ProjectConflictError + | ProjectOperationError; + +export class ProjectService extends Context.Service< + ProjectService, + { + readonly create: (input: ProjectCreateInput) => Effect.Effect; + readonly bootstrap: ( + input: ProjectBootstrapInput, + ) => Effect.Effect< + { readonly project: Project; readonly created: boolean }, + ProjectServiceError + >; + readonly update: (input: ProjectUpdateInput) => Effect.Effect; + readonly delete: (input: ProjectDeleteInput) => Effect.Effect; + readonly getById: ( + projectId: ProjectId, + options?: { readonly includeDeleted?: boolean }, + ) => Effect.Effect, ProjectOperationError>; + readonly getByWorkspaceRoot: ( + workspaceRoot: string, + options?: { readonly includeDeleted?: boolean }, + ) => Effect.Effect, ProjectOperationError>; + readonly snapshot: Effect.Effect; + } +>()("t3/project/ProjectService") {} + +export const make = Effect.gen(function* () { + const engine = yield* OrchestrationEngineService; + const projects = yield* ProjectionProjects.ProjectionProjectRepository; + const repositoryIdentityResolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver; + const faviconResolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; + const workspacePaths = yield* WorkspacePaths.WorkspacePaths; + + const hydrate = Effect.fn("ProjectService.hydrate")(function* ( + row: ProjectionProjects.ProjectionProject, + ): Effect.fn.Return { + const repositoryIdentity = yield* repositoryIdentityResolver.resolve(row.workspaceRoot); + const faviconPath = yield* faviconResolver.resolvePath(row.workspaceRoot).pipe( + Effect.mapError( + (cause) => + new ProjectOperationError({ + operation: "resolve-favicon", + projectId: row.projectId, + workspaceRoot: row.workspaceRoot, + cause, + }), + ), + ); + return { + id: row.projectId, + title: row.title, + workspaceRoot: row.workspaceRoot, + repositoryIdentity, + faviconPath, + defaultModelSelection: row.defaultModelSelection, + scripts: row.scripts, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + deletedAt: row.deletedAt, + }; + }); + + const readRows = Effect.fn("ProjectService.readRows")(function* () { + return yield* projects + .listAll() + .pipe( + Effect.mapError( + (cause) => new ProjectOperationError({ operation: "list-projects", cause }), + ), + ); + }); + + const getById: ProjectService["Service"]["getById"] = Effect.fn("ProjectService.getById")( + function* (projectId, options) { + const row = yield* projects + .getById({ projectId }) + .pipe( + Effect.mapError( + (cause) => new ProjectOperationError({ operation: "read-project", projectId, cause }), + ), + ); + if (Option.isNone(row) || (row.value.deletedAt !== null && !options?.includeDeleted)) { + return Option.none(); + } + return Option.some(yield* hydrate(row.value)); + }, + ); + + const getByWorkspaceRoot: ProjectService["Service"]["getByWorkspaceRoot"] = Effect.fn( + "ProjectService.getByWorkspaceRoot", + )(function* (workspaceRoot, options) { + const normalized = yield* workspacePaths.normalizeWorkspaceRoot(workspaceRoot).pipe( + Effect.mapError( + (cause) => + new ProjectOperationError({ + operation: "normalize-workspace", + workspaceRoot, + cause, + }), + ), + ); + const row = (yield* readRows()).find( + (candidate) => + candidate.workspaceRoot === normalized && + (options?.includeDeleted === true || candidate.deletedAt === null), + ); + return row === undefined ? Option.none() : Option.some(yield* hydrate(row)); + }); + + const readCommitted = Effect.fn("ProjectService.readCommitted")(function* (projectId: ProjectId) { + const row = yield* projects + .getById({ projectId }) + .pipe( + Effect.mapError( + (cause) => new ProjectOperationError({ operation: "read-project", projectId, cause }), + ), + ); + if (Option.isNone(row)) { + return yield* new ProjectOperationError({ + operation: "read-project", + projectId, + cause: "The accepted project command did not produce a project projection.", + }); + } + return yield* hydrate(row.value); + }); + + const dispatch = ( + projectId: ProjectId, + command: Parameters[0], + onCommitted: Effect.Effect, + ) => + engine.dispatch(command).pipe( + Effect.mapError( + (cause) => + new ProjectOperationError({ + operation: "dispatch-project-command", + projectId, + cause, + }), + ), + Effect.andThen(onCommitted), + ); + + const assertWorkspaceAvailable = Effect.fn("ProjectService.assertWorkspaceAvailable")(function* ( + projectId: ProjectId, + workspaceRoot: string, + ) { + const conflicting = (yield* readRows()).find( + (candidate) => + candidate.deletedAt === null && + candidate.projectId !== projectId && + candidate.workspaceRoot === workspaceRoot, + ); + if (conflicting !== undefined) { + return yield* new ProjectConflictError({ + projectId, + workspaceRoot, + conflictingProjectId: conflicting.projectId, + }); + } + }); + + const create: ProjectService["Service"]["create"] = Effect.fn("ProjectService.create")( + function* (input) { + const workspaceRoot = yield* workspacePaths + .normalizeWorkspaceRoot(input.workspaceRoot, { + createIfMissing: input.createWorkspaceRootIfMissing ?? false, + }) + .pipe( + Effect.mapError( + (cause) => + new ProjectOperationError({ + operation: "normalize-workspace", + projectId: input.projectId, + workspaceRoot: input.workspaceRoot, + cause, + }), + ), + ); + yield* assertWorkspaceAvailable(input.projectId, workspaceRoot); + const now = DateTime.formatIso(yield* DateTime.now); + return yield* dispatch( + input.projectId, + { + type: "project.create", + commandId: input.commandId, + projectId: input.projectId, + title: input.title, + workspaceRoot, + defaultModelSelection: input.defaultModelSelection ?? null, + scripts: [...(input.scripts ?? [])], + createdAt: now, + }, + readCommitted(input.projectId), + ); + }, + ); + + const update: ProjectService["Service"]["update"] = Effect.fn("ProjectService.update")( + function* (input) { + const existing = yield* projects.getById({ projectId: input.projectId }).pipe( + Effect.mapError( + (cause) => + new ProjectOperationError({ + operation: "read-project", + projectId: input.projectId, + cause, + }), + ), + ); + if (Option.isNone(existing) || existing.value.deletedAt !== null) { + return yield* new ProjectNotFoundError({ projectId: input.projectId }); + } + const workspaceRoot = + input.workspaceRoot === undefined + ? existing.value.workspaceRoot + : yield* workspacePaths.normalizeWorkspaceRoot(input.workspaceRoot).pipe( + Effect.mapError( + (cause) => + new ProjectOperationError({ + operation: "normalize-workspace", + projectId: input.projectId, + workspaceRoot: input.workspaceRoot, + cause, + }), + ), + ); + yield* assertWorkspaceAvailable(input.projectId, workspaceRoot); + return yield* dispatch( + input.projectId, + { + type: "project.meta.update", + commandId: input.commandId, + projectId: input.projectId, + ...(input.title === undefined ? {} : { title: input.title }), + ...(workspaceRoot === existing.value.workspaceRoot ? {} : { workspaceRoot }), + ...(input.defaultModelSelection === undefined + ? {} + : { defaultModelSelection: input.defaultModelSelection }), + ...(input.scripts === undefined ? {} : { scripts: [...input.scripts] }), + }, + readCommitted(input.projectId), + ); + }, + ); + + const bootstrap: ProjectService["Service"]["bootstrap"] = Effect.fn("ProjectService.bootstrap")( + function* (input) { + const existing = yield* getByWorkspaceRoot(input.workspaceRoot); + if (Option.isSome(existing)) return { project: existing.value, created: false }; + return { project: yield* create(input), created: true }; + }, + ); + + const deleteProject: ProjectService["Service"]["delete"] = Effect.fn("ProjectService.delete")( + function* (input) { + const { projectId } = input; + const existing = yield* projects + .getById({ projectId }) + .pipe( + Effect.mapError( + (cause) => new ProjectOperationError({ operation: "read-project", projectId, cause }), + ), + ); + if (Option.isNone(existing) || existing.value.deletedAt !== null) { + return yield* new ProjectNotFoundError({ projectId }); + } + return yield* dispatch( + projectId, + { + type: "project.delete", + commandId: input.commandId, + projectId, + }, + readCommitted(projectId), + ); + }, + ); + + const snapshot = Effect.gen(function* () { + const rows = (yield* readRows()).filter((row) => row.deletedAt === null); + const hydrated = yield* Effect.forEach(rows, hydrate, { concurrency: 8 }); + return { + projects: hydrated, + updatedAt: DateTime.formatIso(yield* DateTime.now), + } satisfies ProjectSnapshot; + }); + + return ProjectService.of({ + create, + bootstrap, + update, + delete: deleteProject, + getById, + getByWorkspaceRoot, + snapshot, + }); +}); + +export const layer = Layer.effect(ProjectService, make); diff --git a/apps/server/src/project/ProjectSetupScriptRunner.test.ts b/apps/server/src/project/ProjectSetupScriptRunner.test.ts index fdf95df0b99..734627df095 100644 --- a/apps/server/src/project/ProjectSetupScriptRunner.test.ts +++ b/apps/server/src/project/ProjectSetupScriptRunner.test.ts @@ -1,198 +1,79 @@ -import { describe, expect, it, vi } from "@effect/vitest"; -import { type OrchestrationProject, ProjectId } from "@t3tools/contracts"; +import { assert, it, vi } from "@effect/vitest"; +import { ProjectId } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; -import * as Schema from "effect/Schema"; -import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; import * as TerminalManager from "../terminal/Manager.ts"; +import * as ProjectService from "./ProjectService.ts"; import * as ProjectSetupScriptRunner from "./ProjectSetupScriptRunner.ts"; -const isProjectSetupScriptOperationError = Schema.is( - ProjectSetupScriptRunner.ProjectSetupScriptOperationError, -); - -const makeProject = (scripts: OrchestrationProject["scripts"]): OrchestrationProject => ({ - id: ProjectId.make("project-1"), - title: "Project", - workspaceRoot: "/repo/project", - defaultModelSelection: null, - scripts, - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", - deletedAt: null, -}); - -const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) => - Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => Effect.die("unused"), - getSnapshot: () => Effect.die("unused"), - getShellSnapshot: () => Effect.die("unused"), - getArchivedShellSnapshot: () => Effect.die("unused"), - getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 1 }), - getCounts: () => Effect.die("unused"), - getActiveProjectByWorkspaceRoot: (workspaceRoot) => - Effect.succeed( - workspaceRoot === project.workspaceRoot ? Option.some(project) : Option.none(), - ), - getProjectShellById: (projectId) => - Effect.succeed(projectId === project.id ? Option.some(project) : Option.none()), - getFirstActiveThreadIdByProjectId: () => Effect.die("unused"), - getThreadCheckpointContext: () => Effect.die("unused"), - getFullThreadDiffContext: () => Effect.die("unused"), - getThreadShellById: () => Effect.die("unused"), - getThreadDetailById: () => Effect.die("unused"), - }); - -const makeTerminalManagerLayer = ( - overrides: Pick, -) => - Layer.succeed(TerminalManager.TerminalManager, { - ...overrides, - attachStream: () => Effect.die(new Error("unused")), - resize: () => Effect.void, - clear: () => Effect.void, - restart: () => Effect.die(new Error("unused")), - close: () => Effect.void, - subscribe: () => Effect.succeed(() => undefined), - subscribeMetadata: () => Effect.succeed(() => undefined), - }); - -const testLayer = ( - project: OrchestrationProject, - terminal: Pick, -) => - ProjectSetupScriptRunner.layer.pipe( - Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)), - Layer.provideMerge(makeTerminalManagerLayer(terminal)), +it.effect("resolves setup scripts through the standalone project service", () => { + const open = vi.fn((input: Parameters[0]) => + Effect.succeed({ + threadId: input.threadId, + terminalId: input.terminalId, + cwd: input.cwd, + worktreePath: input.worktreePath ?? null, + status: "running" as const, + pid: 123, + history: "", + exitCode: null, + exitSignal: null, + label: "Shell", + updatedAt: "2026-06-20T00:00:00.000Z", + }), ); - -describe("ProjectSetupScriptRunner", () => { - it.effect("returns no-script when no setup script exists", () => { - const open = vi.fn(() => Effect.die("unexpected open")); - const write = vi.fn(() => Effect.die("unexpected write")); - const project = makeProject([]); - - return Effect.gen(function* () { - const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner; - const result = yield* runner.runForThread({ - threadId: "thread-1", - projectId: "project-1", - worktreePath: "/repo/worktrees/a", - }); - - expect(result).toEqual({ status: "no-script" }); - expect(open).not.toHaveBeenCalled(); - expect(write).not.toHaveBeenCalled(); - }).pipe(Effect.provide(testLayer(project, { open, write }))); - }); - - it.effect( - "opens the deterministic setup terminal with worktree env and writes the command", - () => { - const open = vi.fn(() => - Effect.succeed({ - threadId: "thread-1", - terminalId: "setup-setup", - cwd: "/repo/worktrees/a", - worktreePath: "/repo/worktrees/a", - status: "running" as const, - pid: 123, - history: "", - exitCode: null, - exitSignal: null, - label: "setup-setup", - updatedAt: "2026-01-01T00:00:00.000Z", - }), - ); - const write = vi.fn(() => Effect.void); - const project = makeProject([ - { - id: "setup", - name: "Setup", - command: "bun install", - icon: "configure", - runOnWorktreeCreate: true, - }, - ]); - - return Effect.gen(function* () { - const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner; - const result = yield* runner.runForThread({ - threadId: "thread-1", - projectCwd: "/repo/project", - worktreePath: "/repo/worktrees/a", - }); - - expect(result).toEqual({ - status: "started", - scriptId: "setup", - scriptName: "Setup", - terminalId: "setup-setup", - cwd: "/repo/worktrees/a", - }); - expect(open).toHaveBeenCalledWith({ - threadId: "thread-1", - terminalId: "setup-setup", - cwd: "/repo/worktrees/a", - worktreePath: "/repo/worktrees/a", - env: { - T3CODE_PROJECT_ROOT: "/repo/project", - T3CODE_WORKTREE_PATH: "/repo/worktrees/a", - }, - }); - expect(write).toHaveBeenCalledWith({ - threadId: "thread-1", - terminalId: "setup-setup", - data: "bun install\r", - }); - }).pipe(Effect.provide(testLayer(project, { open, write }))); - }, + const write = vi.fn( + (_input: Parameters[0]) => Effect.void, ); - - it.effect("keeps terminal failures as the exact cause of a structured operation error", () => { - const rootCause = new Error("stat failed"); - const terminalError = new TerminalManager.TerminalCwdStatError({ - cwd: "/repo/worktrees/a", - cause: rootCause, - }); - const project = makeProject([ + const projectId = ProjectId.make("project:setup-runner-v2"); + const project = { + id: projectId, + title: "Project", + workspaceRoot: "/repo", + repositoryIdentity: null, + faviconPath: null, + defaultModelSelection: null, + scripts: [ { id: "setup", name: "Setup", - command: "bun install", - icon: "configure", + command: "vp install", + icon: "configure" as const, runOnWorktreeCreate: true, }, - ]); - - return Effect.gen(function* () { - const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner; - const error = yield* runner - .runForThread({ - threadId: "thread-1", - projectId: "project-1", - worktreePath: "/repo/worktrees/a", - }) - .pipe(Effect.flip); - - expect(isProjectSetupScriptOperationError(error)).toBe(true); - if (isProjectSetupScriptOperationError(error)) { - expect(error.operation).toBe("openTerminal"); - expect(error.threadId).toBe("thread-1"); - expect(error.projectId).toBe("project-1"); - expect(error.worktreePath).toBe("/repo/worktrees/a"); - expect(error.cause).toBe(terminalError); - expect(terminalError.cause).toBe(rootCause); - } - }).pipe( - Effect.provide( - testLayer(project, { - open: () => Effect.fail(terminalError), - write: () => Effect.die("unexpected write"), + ], + createdAt: "2026-06-20T00:00:00.000Z", + updatedAt: "2026-06-20T00:00:00.000Z", + deletedAt: null, + }; + const layer = ProjectSetupScriptRunner.layer.pipe( + Layer.provide( + Layer.mergeAll( + Layer.mock(ProjectService.ProjectService)({ + getById: () => Effect.succeed(Option.some(project)), }), + Layer.mock(TerminalManager.TerminalManager)({ open, write }), ), - ); - }); + ), + ); + + return Effect.gen(function* () { + const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner; + const result = yield* runner.runForThread({ + threadId: "thread-1", + projectId, + worktreePath: "/repo-worktree", + }); + assert.deepEqual(result, { + status: "started", + scriptId: "setup", + scriptName: "Setup", + terminalId: "setup-setup", + cwd: "/repo-worktree", + }); + assert.equal(open.mock.calls[0]?.[0].cwd, "/repo-worktree"); + assert.equal(write.mock.calls[0]?.[0].data, "vp install\r"); + }).pipe(Effect.provide(layer)); }); diff --git a/apps/server/src/project/ProjectSetupScriptRunner.ts b/apps/server/src/project/ProjectSetupScriptRunner.ts index 41bf0fabf48..eb997265eb1 100644 --- a/apps/server/src/project/ProjectSetupScriptRunner.ts +++ b/apps/server/src/project/ProjectSetupScriptRunner.ts @@ -1,4 +1,4 @@ -import { ProjectId } from "@t3tools/contracts"; +import { ProjectId, type ProjectScript } from "@t3tools/contracts"; import { projectScriptRuntimeEnv, setupProjectScript } from "@t3tools/shared/projectScripts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -6,8 +6,8 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; -import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; import * as TerminalManager from "../terminal/Manager.ts"; +import * as ProjectService from "./ProjectService.ts"; export interface ProjectSetupScriptRunnerResultNoScript { readonly status: "no-script"; @@ -31,6 +31,10 @@ export interface ProjectSetupScriptRunnerInput { readonly projectCwd?: string; readonly worktreePath: string; readonly preferredTerminalId?: string; + readonly project?: { + readonly workspaceRoot: string; + readonly scripts: ReadonlyArray; + }; } export class ProjectSetupScriptOperationError extends Schema.TaggedErrorClass()( @@ -79,7 +83,7 @@ export class ProjectSetupScriptRunner extends Context.Service< >()("t3/project/ProjectSetupScriptRunner") {} export const make = Effect.gen(function* () { - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; + const projects = yield* ProjectService.ProjectService; const terminalManager = yield* TerminalManager.TerminalManager; const runForThread: ProjectSetupScriptRunner["Service"]["runForThread"] = Effect.fn( @@ -91,23 +95,27 @@ export const make = Effect.gen(function* () { ...(input.projectId === undefined ? {} : { projectId: input.projectId }), ...(input.projectCwd === undefined ? {} : { projectCwd: input.projectCwd }), }; - const projectById = input.projectId - ? yield* projectionSnapshotQuery.getProjectShellById(ProjectId.make(input.projectId)).pipe( - Effect.map(Option.getOrUndefined), - Effect.mapError( - (cause) => - new ProjectSetupScriptOperationError({ - ...errorContext, - operation: "resolveProject", - cause, - }), - ), - ) - : null; + const suppliedProject = input.project; + const projectById = + suppliedProject ?? + (input.projectId + ? yield* projects.getById(ProjectId.make(input.projectId)).pipe( + Effect.map(Option.getOrUndefined), + Effect.mapError( + (cause) => + new ProjectSetupScriptOperationError({ + ...errorContext, + operation: "resolveProject", + cause, + }), + ), + ) + : null); const project = + suppliedProject ?? projectById ?? (input.projectCwd - ? yield* projectionSnapshotQuery.getActiveProjectByWorkspaceRoot(input.projectCwd).pipe( + ? yield* projects.getByWorkspaceRoot(input.projectCwd).pipe( Effect.map(Option.getOrUndefined), Effect.mapError( (cause) => diff --git a/apps/server/src/project/http.ts b/apps/server/src/project/http.ts new file mode 100644 index 00000000000..c7c19f087f6 --- /dev/null +++ b/apps/server/src/project/http.ts @@ -0,0 +1,78 @@ +import { + AuthOrchestrationOperateScope, + AuthOrchestrationReadScope, + EnvironmentHttpApi, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; + +import { + annotateEnvironmentRequest, + failEnvironmentInternal, + requireEnvironmentScope, +} from "../auth/http.ts"; +import * as ServerRuntimeStartup from "../serverRuntimeStartup.ts"; +import { ProjectService } from "./ProjectService.ts"; + +export const projectHttpApiLayer = HttpApiBuilder.group( + EnvironmentHttpApi, + "projects", + Effect.fnUntraced(function* (handlers) { + const projects = yield* ProjectService; + const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup; + + return handlers + .handle( + "snapshot", + Effect.fn("environment.projects.snapshot")(function* (args) { + yield* annotateEnvironmentRequest(args.endpoint.name); + yield* requireEnvironmentScope(AuthOrchestrationReadScope); + return yield* projects.snapshot.pipe( + Effect.catch((cause) => failEnvironmentInternal("project_snapshot_failed", cause)), + ); + }), + ) + .handle( + "mutate", + Effect.fn("environment.projects.mutate")(function* (args) { + yield* annotateEnvironmentRequest(args.endpoint.name); + yield* requireEnvironmentScope(AuthOrchestrationOperateScope); + const mutation = args.payload; + const operation = + mutation.type === "project.create" + ? projects.create({ + commandId: mutation.commandId, + projectId: mutation.projectId, + title: mutation.title, + workspaceRoot: mutation.workspaceRoot, + ...(mutation.defaultModelSelection === undefined + ? {} + : { defaultModelSelection: mutation.defaultModelSelection }), + ...(mutation.scripts === undefined ? {} : { scripts: mutation.scripts }), + }) + : mutation.type === "project.update" + ? projects.update({ + commandId: mutation.commandId, + projectId: mutation.projectId, + ...(mutation.title === undefined ? {} : { title: mutation.title }), + ...(mutation.workspaceRoot === undefined + ? {} + : { workspaceRoot: mutation.workspaceRoot }), + ...(mutation.defaultModelSelection === undefined + ? {} + : { defaultModelSelection: mutation.defaultModelSelection }), + ...(mutation.scripts === undefined ? {} : { scripts: mutation.scripts }), + }) + : projects.delete({ + commandId: mutation.commandId, + projectId: mutation.projectId, + }); + return yield* startup + .enqueueCommand(operation) + .pipe( + Effect.catch((cause) => failEnvironmentInternal("project_mutation_failed", cause)), + ); + }), + ); + }), +); diff --git a/apps/server/src/provider/CodexDeveloperInstructions.ts b/apps/server/src/provider/CodexDeveloperInstructions.ts index b46a4ce1ba3..1b5bd2fe290 100644 --- a/apps/server/src/provider/CodexDeveloperInstructions.ts +++ b/apps/server/src/provider/CodexDeveloperInstructions.ts @@ -9,6 +9,15 @@ For browser work, first call \`preview_status\`. If no automation-capable previe Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable. `; +const T3_CODE_THREAD_ORCHESTRATION_INSTRUCTIONS = ` + +## T3 Code thread orchestration + +When the \`t3-code\` MCP server exposes \`t3_thread_*\` tools, you can run a bounded orchestration loop without asking the user to relay updates. Start independent work with \`t3_thread_start\` (or \`create_threads\` for a batch), retain the returned thread/run IDs, wait with \`t3_thread_wait\`, and collect durable output with \`t3_thread_read\`. Use \`t3_thread_send\` for follow-up or steering and \`t3_thread_interrupt\` when work is no longer needed. Use stable \`clientRequestId\` values when retrying mutating calls, and use \`t3_thread_list\` to recover IDs after context loss. + +Keep loops bounded by an explicit completion condition and timeout. A wait timeout does not stop the target thread. Do not repeatedly start equivalent threads or send duplicate work when a durable run is already active. +`; + export const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = `# Plan Mode (Conversational) You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions. @@ -144,4 +153,5 @@ The \`request_user_input\` tool is unavailable in Default mode. If you call it w In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message. ${T3_CODE_BROWSER_TOOL_INSTRUCTIONS} +${T3_CODE_THREAD_ORCHESTRATION_INSTRUCTIONS} `; diff --git a/apps/server/src/provider/Drivers/AcpRegistryDriver.ts b/apps/server/src/provider/Drivers/AcpRegistryDriver.ts new file mode 100644 index 00000000000..20a7e5b5ce9 --- /dev/null +++ b/apps/server/src/provider/Drivers/AcpRegistryDriver.ts @@ -0,0 +1,144 @@ +import { + AcpRegistrySettings, + ProviderDriverKind, + TextGenerationError, + type ServerProvider, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as DateTime from "effect/DateTime"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; + +import { + AcpRegistryAdapterV2Driver, + type AcpRegistryAdapterV2DriverEnv, +} from "../../orchestration-v2/Adapters/AcpRegistryAdapterV2.ts"; +import type { TextGenerationShape } from "../../textGeneration/TextGeneration.ts"; +import { ProviderDriverError } from "../Errors.ts"; +import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts"; +import { + defaultProviderContinuationIdentity, + type ProviderDriver, + type ProviderInstance, +} from "../ProviderDriver.ts"; + +const DRIVER_KIND = ProviderDriverKind.make("acpRegistry"); +const decodeSettings = Schema.decodeSync(AcpRegistrySettings); + +const makeUnsupportedTextGeneration = (): TextGenerationShape => { + const unsupported = (operation: string) => + Effect.fail( + new TextGenerationError({ + operation, + detail: "ACP Registry instances do not provide application text generation.", + }), + ); + return { + generateCommitMessage: () => unsupported("generateCommitMessage"), + generatePrContent: () => unsupported("generatePrContent"), + generateBranchName: () => unsupported("generateBranchName"), + generateThreadTitle: () => unsupported("generateThreadTitle"), + }; +}; + +const makeSnapshot = (input: { + readonly instanceId: ProviderInstance["instanceId"]; + readonly displayName: string | undefined; + readonly accentColor: string | undefined; + readonly enabled: boolean; + readonly settings: AcpRegistrySettings; + readonly continuationKey: string; + readonly checkedAt: string; +}): ServerProvider => { + const modelIds = Array.from(new Set(["default", ...input.settings.customModels])); + return { + instanceId: input.instanceId, + driver: DRIVER_KIND, + ...(input.displayName ? { displayName: input.displayName } : {}), + ...(input.accentColor ? { accentColor: input.accentColor } : {}), + continuation: { groupKey: input.continuationKey }, + enabled: input.enabled, + installed: true, + version: null, + status: input.enabled ? "ready" : "disabled", + auth: { status: "unknown" }, + checkedAt: input.checkedAt, + models: modelIds.map((model) => ({ + slug: model, + name: model, + isCustom: model !== "default", + capabilities: null, + })), + slashCommands: [], + skills: [], + }; +}; + +export type AcpRegistryDriverEnv = AcpRegistryAdapterV2DriverEnv; + +/** Canonical provider-instance wrapper for ACP Registry orchestration adapters. */ +export const AcpRegistryDriver: ProviderDriver = { + driverKind: DRIVER_KIND, + metadata: { + displayName: "ACP Registry", + supportsMultipleInstances: true, + }, + configSchema: AcpRegistrySettings, + defaultConfig: () => decodeSettings({}), + create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => + Effect.gen(function* () { + const continuationIdentity = defaultProviderContinuationIdentity({ + driverKind: DRIVER_KIND, + instanceId, + }); + const checkedAt = DateTime.formatIso(yield* DateTime.now); + const orchestrationAdapter = yield* AcpRegistryAdapterV2Driver.create({ + instanceId, + displayName, + accentColor, + environment, + enabled, + config, + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: "Failed to build ACP Registry orchestration adapter.", + cause, + }), + ), + ); + const currentSnapshot = () => + makeSnapshot({ + instanceId, + displayName, + accentColor, + enabled, + settings: config, + continuationKey: continuationIdentity.continuationKey, + checkedAt, + }); + + return { + instanceId, + driverKind: DRIVER_KIND, + continuationIdentity, + displayName, + accentColor, + enabled, + snapshot: { + maintenanceCapabilities: makeManualOnlyProviderMaintenanceCapabilities({ + provider: DRIVER_KIND, + packageName: null, + }), + getSnapshot: Effect.sync(currentSnapshot), + refresh: Effect.sync(currentSnapshot), + streamChanges: Stream.empty, + }, + orchestrationAdapter, + textGeneration: makeUnsupportedTextGeneration(), + } satisfies ProviderInstance; + }), +}; diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index f2b04b3a282..c4b95525b1e 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -25,15 +25,17 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { makeClaudeTextGeneration } from "../../textGeneration/ClaudeTextGeneration.ts"; import { ServerConfig } from "../../config.ts"; +import { + ClaudeAdapterV2Driver, + type ClaudeAdapterV2DriverEnv, +} from "../../orchestration-v2/Adapters/ClaudeAdapterV2.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderDriverError } from "../Errors.ts"; -import { makeClaudeAdapter } from "../Layers/ClaudeAdapter.ts"; import { checkClaudeProviderStatus, makePendingClaudeProvider, probeClaudeCapabilities, } from "../Layers/ClaudeProvider.ts"; -import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; import { defaultProviderContinuationIdentity, @@ -82,12 +84,12 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ }); export type ClaudeDriverEnv = + | ClaudeAdapterV2DriverEnv | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path - | ProviderEventLoggers | ServerConfig | ServerSettingsService; @@ -121,7 +123,6 @@ export const ClaudeDriver: ProviderDriver = { const path = yield* Path.Path; const httpClient = yield* HttpClient.HttpClient; const serverSettings = yield* ServerSettingsService; - const eventLoggers = yield* ProviderEventLoggers; const processEnv = mergeProviderInstanceEnvironment(environment); const fallbackContinuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, @@ -140,12 +141,24 @@ export const ClaudeDriver: ProviderDriver = { continuationGroupKey, }); - const adapterOptions = { + const orchestrationAdapter = yield* ClaudeAdapterV2Driver.create({ instanceId, - environment: processEnv, - ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), - }; - const adapter = yield* makeClaudeAdapter(effectiveConfig, adapterOptions); + displayName, + accentColor, + environment, + enabled, + config, + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: "Failed to build Claude orchestration adapter.", + cause, + }), + ), + ); const textGeneration = yield* makeClaudeTextGeneration(effectiveConfig, processEnv); // Per-instance capabilities cache: keyed on binary + resolved HOME so @@ -210,7 +223,7 @@ export const ClaudeDriver: ProviderDriver = { accentColor, enabled, snapshot, - adapter, + orchestrationAdapter, textGeneration, } satisfies ProviderInstance; }), diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index ffcc94ca77d..9960ab68bf4 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -33,11 +33,13 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { makeCodexTextGeneration } from "../../textGeneration/CodexTextGeneration.ts"; import { ServerConfig } from "../../config.ts"; +import { + CodexAdapterV2Driver, + type CodexAdapterV2DriverEnv, +} from "../../orchestration-v2/Adapters/CodexAdapterV2.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderDriverError } from "../Errors.ts"; -import { makeCodexAdapter } from "../Layers/CodexAdapter.ts"; import { checkCodexProviderStatus, makePendingCodexProvider } from "../Layers/CodexProvider.ts"; -import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts"; import type { ServerProviderDraft } from "../providerSnapshot.ts"; @@ -74,12 +76,12 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ * registered driver and the runtime satisfies them once. */ export type CodexDriverEnv = + | CodexAdapterV2DriverEnv | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path - | ProviderEventLoggers | ServerConfig | ServerSettingsService; @@ -118,7 +120,6 @@ export const CodexDriver: ProviderDriver = { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const httpClient = yield* HttpClient.HttpClient; const serverSettings = yield* ServerSettingsService; - const eventLoggers = yield* ProviderEventLoggers; const processEnv = mergeProviderInstanceEnvironment(environment); const homeLayout = yield* resolveCodexHomeLayout(config); const continuationIdentity = codexContinuationIdentity(homeLayout); @@ -149,17 +150,24 @@ export const CodexDriver: ProviderDriver = { env: processEnv, }); - // `makeCodexAdapter` and `makeCodexTextGeneration` have `never` error - // channels at construction time — their failure modes are all on the - // per-operation closures they return. No `mapError` wrapper is needed - // here; the registry only has to worry about snapshot-build and - // spawner-availability failures surfaced from `checkCodexProviderStatus` - // below. - const adapter = yield* makeCodexAdapter(effectiveConfig, { + const orchestrationAdapter = yield* CodexAdapterV2Driver.create({ instanceId, - environment: processEnv, - ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), - }); + displayName, + accentColor, + environment, + enabled, + config, + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: "Failed to build Codex orchestration adapter.", + cause, + }), + ), + ); const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv); // Build a managed snapshot whose settings never change — mutations come @@ -207,7 +215,7 @@ export const CodexDriver: ProviderDriver = { accentColor, enabled, snapshot, - adapter, + orchestrationAdapter, textGeneration, } satisfies ProviderInstance; }), diff --git a/apps/server/src/provider/Drivers/CursorDriver.ts b/apps/server/src/provider/Drivers/CursorDriver.ts index c394a7d1b43..854984ab93a 100644 --- a/apps/server/src/provider/Drivers/CursorDriver.ts +++ b/apps/server/src/provider/Drivers/CursorDriver.ts @@ -1,9 +1,9 @@ /** * CursorDriver — `ProviderDriver` for the Cursor Agent (`agent`) runtime. * - * Cursor exposes an ACP-based CLI. Model catalog and capability refreshes - * happen during the managed provider status check via Cursor's - * `list_available_models` extension method. + * Provider status and model discovery use the official Cursor SDK when an API + * key is configured. The ACP CLI remains the compatibility fallback for + * installations that authenticate through the local Cursor binary. * * Text generation is supported via the ACP runtime — `makeCursorTextGeneration` * drives `runtime.prompt` with a structured-output schema and collects the @@ -24,14 +24,17 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { makeCursorTextGeneration } from "../../textGeneration/CursorTextGeneration.ts"; +import { + CursorAdapterV2Driver, + type CursorAdapterV2DriverEnv, +} from "../../orchestration-v2/Adapters/CursorAdapterV2.ts"; import { ProviderDriverError } from "../Errors.ts"; -import { makeCursorAdapter } from "../Layers/CursorAdapter.ts"; import { buildInitialCursorProviderSnapshot, checkCursorProviderStatus, enrichCursorSnapshot, } from "../Layers/CursorProvider.ts"; -import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; +import { CursorSdkCatalogLive } from "../Layers/CursorSdkCatalog.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; import { defaultProviderContinuationIdentity, @@ -65,12 +68,12 @@ const UPDATE = makeStaticProviderMaintenanceResolver( ); export type CursorDriverEnv = + | CursorAdapterV2DriverEnv | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path - | ProviderEventLoggers | ServerConfig | ServerSettingsService; @@ -105,7 +108,6 @@ export const CursorDriver: ProviderDriver = { const path = yield* Path.Path; const httpClient = yield* HttpClient.HttpClient; const serverSettings = yield* ServerSettingsService; - const eventLoggers = yield* ProviderEventLoggers; const processEnv = mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, @@ -123,15 +125,29 @@ export const CursorDriver: ProviderDriver = { env: processEnv, }); - const adapter = yield* makeCursorAdapter(effectiveConfig, { - environment: processEnv, - ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), + const orchestrationAdapter = yield* CursorAdapterV2Driver.create({ instanceId, - }); + displayName, + accentColor, + environment, + enabled, + config, + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: "Failed to build Cursor orchestration adapter.", + cause, + }), + ), + ); const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv); const checkProvider = checkCursorProviderStatus(effectiveConfig, processEnv).pipe( Effect.map(stampIdentity), + Effect.provide(CursorSdkCatalogLive), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), @@ -179,7 +195,7 @@ export const CursorDriver: ProviderDriver = { accentColor, enabled, snapshot, - adapter, + orchestrationAdapter, textGeneration, } satisfies ProviderInstance; }), diff --git a/apps/server/src/provider/Drivers/GrokDriver.ts b/apps/server/src/provider/Drivers/GrokDriver.ts index d855d1a4515..f5cc9528cdd 100644 --- a/apps/server/src/provider/Drivers/GrokDriver.ts +++ b/apps/server/src/provider/Drivers/GrokDriver.ts @@ -11,14 +11,16 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { makeGrokTextGeneration } from "../../textGeneration/GrokTextGeneration.ts"; +import { + GrokAdapterV2Driver, + type GrokAdapterV2DriverEnv, +} from "../../orchestration-v2/Adapters/GrokAdapterV2.ts"; import { ProviderDriverError } from "../Errors.ts"; -import { makeGrokAdapter } from "../Layers/GrokAdapter.ts"; import { buildInitialGrokProviderSnapshot, checkGrokProviderStatus, enrichGrokSnapshot, } from "../Layers/GrokProvider.ts"; -import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; import { defaultProviderContinuationIdentity, @@ -49,12 +51,12 @@ const UPDATE = makeStaticProviderMaintenanceResolver( ); export type GrokDriverEnv = + | GrokAdapterV2DriverEnv | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path - | ProviderEventLoggers | ServerConfig | ServerSettingsService; @@ -87,7 +89,6 @@ export const GrokDriver: ProviderDriver = { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const httpClient = yield* HttpClient.HttpClient; const serverSettings = yield* ServerSettingsService; - const eventLoggers = yield* ProviderEventLoggers; const processEnv = mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, @@ -105,11 +106,24 @@ export const GrokDriver: ProviderDriver = { env: processEnv, }); - const adapter = yield* makeGrokAdapter(effectiveConfig, { - environment: processEnv, - ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), + const orchestrationAdapter = yield* GrokAdapterV2Driver.create({ instanceId, - }); + displayName, + accentColor, + environment, + enabled, + config, + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: "Failed to build Grok orchestration adapter.", + cause, + }), + ), + ); const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv); const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv).pipe( @@ -155,7 +169,7 @@ export const GrokDriver: ProviderDriver = { accentColor, enabled, snapshot, - adapter, + orchestrationAdapter, textGeneration, } satisfies ProviderInstance; }), diff --git a/apps/server/src/provider/Drivers/OpenCodeDriver.ts b/apps/server/src/provider/Drivers/OpenCodeDriver.ts index 6342d176590..e42845615ba 100644 --- a/apps/server/src/provider/Drivers/OpenCodeDriver.ts +++ b/apps/server/src/provider/Drivers/OpenCodeDriver.ts @@ -24,14 +24,16 @@ import { ChildProcessSpawner } from "effect/unstable/process"; import { makeOpenCodeTextGeneration } from "../../textGeneration/OpenCodeTextGeneration.ts"; import { ServerConfig } from "../../config.ts"; +import { + OpenCodeAdapterV2Driver, + type OpenCodeAdapterV2DriverEnv, +} from "../../orchestration-v2/Adapters/OpenCodeAdapterV2.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderDriverError } from "../Errors.ts"; -import { makeOpenCodeAdapter } from "../Layers/OpenCodeAdapter.ts"; import { checkOpenCodeProviderStatus, makePendingOpenCodeProvider, } from "../Layers/OpenCodeProvider.ts"; -import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; import { OpenCodeRuntime } from "../opencodeRuntime.ts"; import { @@ -78,13 +80,13 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ }); export type OpenCodeDriverEnv = + | OpenCodeAdapterV2DriverEnv | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | OpenCodeRuntime | Path.Path - | ProviderEventLoggers | ServerConfig | ServerSettingsService; @@ -118,7 +120,6 @@ export const OpenCodeDriver: ProviderDriver const serverConfig = yield* ServerConfig; const httpClient = yield* HttpClient.HttpClient; const serverSettings = yield* ServerSettingsService; - const eventLoggers = yield* ProviderEventLoggers; const processEnv = mergeProviderInstanceEnvironment(environment); const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, @@ -136,11 +137,24 @@ export const OpenCodeDriver: ProviderDriver env: processEnv, }); - const adapter = yield* makeOpenCodeAdapter(effectiveConfig, { + const orchestrationAdapter = yield* OpenCodeAdapterV2Driver.create({ instanceId, - environment: processEnv, - ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), - }); + displayName, + accentColor, + environment, + enabled, + config, + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: "Failed to build OpenCode orchestration adapter.", + cause, + }), + ), + ); const textGeneration = yield* makeOpenCodeTextGeneration(effectiveConfig, processEnv); const checkProvider = checkOpenCodeProviderStatus( @@ -188,7 +202,7 @@ export const OpenCodeDriver: ProviderDriver accentColor, enabled, snapshot, - adapter, + orchestrationAdapter, textGeneration, } satisfies ProviderInstance; }), diff --git a/apps/server/src/provider/Errors.ts b/apps/server/src/provider/Errors.ts index 0cf1522399b..35244cc7156 100644 --- a/apps/server/src/provider/Errors.ts +++ b/apps/server/src/provider/Errors.ts @@ -1,126 +1,10 @@ import * as Schema from "effect/Schema"; -import type { CheckpointServiceError } from "../checkpointing/Errors.ts"; - -/** - * ProviderAdapterValidationError - Invalid adapter API input. - */ -export class ProviderAdapterValidationError extends Schema.TaggedErrorClass()( - "ProviderAdapterValidationError", - { - provider: Schema.String, - operation: Schema.String, - issue: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Provider adapter validation failed (${this.provider}) in ${this.operation}: ${this.issue}`; - } -} - -/** - * ProviderAdapterSessionNotFoundError - Adapter-owned session id is unknown. - */ -export class ProviderAdapterSessionNotFoundError extends Schema.TaggedErrorClass()( - "ProviderAdapterSessionNotFoundError", - { - provider: Schema.String, - threadId: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Unknown ${this.provider} adapter thread: ${this.threadId}`; - } -} - -/** - * ProviderAdapterSessionClosedError - Adapter session exists but is closed. - */ -export class ProviderAdapterSessionClosedError extends Schema.TaggedErrorClass()( - "ProviderAdapterSessionClosedError", - { - provider: Schema.String, - threadId: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `${this.provider} adapter thread is closed: ${this.threadId}`; - } -} - -/** - * ProviderAdapterRequestError - Provider protocol request failed or timed out. - */ -export class ProviderAdapterRequestError extends Schema.TaggedErrorClass()( - "ProviderAdapterRequestError", - { - provider: Schema.String, - method: Schema.String, - detail: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Provider adapter request failed (${this.provider}) for ${this.method}: ${this.detail}`; - } -} - -/** - * ProviderAdapterProcessError - Provider process lifecycle failure. - */ -export class ProviderAdapterProcessError extends Schema.TaggedErrorClass()( - "ProviderAdapterProcessError", - { - provider: Schema.String, - threadId: Schema.String, - detail: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Provider adapter process error (${this.provider}) for thread ${this.threadId}: ${this.detail}`; - } -} - -/** - * ProviderValidationError - Invalid provider API input. - */ -export class ProviderValidationError extends Schema.TaggedErrorClass()( - "ProviderValidationError", - { - operation: Schema.String, - issue: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Provider validation failed in ${this.operation}: ${this.issue}`; - } -} - -/** - * ProviderUnsupportedError - Requested provider is not implemented. - */ -export class ProviderUnsupportedError extends Schema.TaggedErrorClass()( - "ProviderUnsupportedError", - { - provider: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Provider '${this.provider}' is not implemented`; - } -} - /** * ProviderInstanceNotFoundError - Lookup against the instance registry failed. * - * Distinct from `ProviderUnsupportedError`: the driver is registered, but no - * instance with the requested id has been bootstrapped — typically because + * The driver may be registered, but no instance with the requested id has + * been bootstrapped — typically because * the persisted instance id refers to an instance the user removed from * settings, or because routing is asked for an instance before the registry * has finished its first reload. @@ -155,50 +39,3 @@ export class ProviderDriverError extends Schema.TaggedErrorClass()( - "ProviderSessionNotFoundError", - { - threadId: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Unknown provider thread: ${this.threadId}`; - } -} - -/** - * ProviderSessionDirectoryPersistenceError - Session directory persistence failure. - */ -export class ProviderSessionDirectoryPersistenceError extends Schema.TaggedErrorClass()( - "ProviderSessionDirectoryPersistenceError", - { - operation: Schema.String, - detail: Schema.String, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Provider session directory persistence error in ${this.operation}: ${this.detail}`; - } -} - -export type ProviderAdapterError = - | ProviderAdapterValidationError - | ProviderAdapterSessionNotFoundError - | ProviderAdapterSessionClosedError - | ProviderAdapterRequestError - | ProviderAdapterProcessError; - -export type ProviderServiceError = - | ProviderValidationError - | ProviderUnsupportedError - | ProviderInstanceNotFoundError - | ProviderSessionNotFoundError - | ProviderSessionDirectoryPersistenceError - | ProviderAdapterError - | CheckpointServiceError; diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts deleted file mode 100644 index 191bf8e27db..00000000000 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ /dev/null @@ -1,3789 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; - -import * as NodeServices from "@effect/platform-node/NodeServices"; -import type { - Options as ClaudeQueryOptions, - PermissionMode, - PermissionResult, - SDKMessage, - SDKUserMessage, -} from "@anthropic-ai/claude-agent-sdk"; -import { - ApprovalRequestId, - ClaudeSettings, - ProviderDriverKind, - ProviderItemId, - ProviderRuntimeEvent, - type RuntimeMode, - ThreadId, - ProviderInstanceId, -} from "@t3tools/contracts"; -import { createModelSelection } from "@t3tools/shared/model"; -import { assert, describe, it } from "@effect/vitest"; -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Fiber from "effect/Fiber"; -import * as Layer from "effect/Layer"; -import * as Random from "effect/Random"; -import * as Schema from "effect/Schema"; -import * as Stream from "effect/Stream"; -import * as TestClock from "effect/testing/TestClock"; - -import { attachmentRelativePath } from "../../attachmentStore.ts"; -import { ServerConfig } from "../../config.ts"; -import { ServerSettingsService } from "../../serverSettings.ts"; -import { ProviderAdapterProcessError, ProviderAdapterValidationError } from "../Errors.ts"; -import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; -import { makeClaudeAdapter, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts"; -const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); - -// Test-local service tag so the rest of the file can keep using `yield* ClaudeAdapter`. -class ClaudeAdapter extends Context.Service()( - "t3/provider/Layers/ClaudeAdapter.test/ClaudeAdapter", -) {} - -class FakeClaudeQuery implements AsyncIterable { - private readonly queue: Array = []; - private readonly waiters: Array<{ - readonly resolve: (value: IteratorResult) => void; - readonly reject: (reason: unknown) => void; - }> = []; - private done = false; - private failure: unknown | undefined; - - public readonly interruptCalls: Array = []; - public readonly setModelCalls: Array = []; - public readonly setPermissionModeCalls: Array = []; - public readonly setMaxThinkingTokensCalls: Array = []; - public closeCalls = 0; - - emit(message: SDKMessage): void { - if (this.done) { - return; - } - const waiter = this.waiters.shift(); - if (waiter) { - waiter.resolve({ done: false, value: message }); - return; - } - this.queue.push(message); - } - - fail(cause: unknown): void { - if (this.done) { - return; - } - this.done = true; - this.failure = cause; - for (const waiter of this.waiters.splice(0)) { - waiter.reject(cause); - } - } - - finish(): void { - if (this.done) { - return; - } - this.done = true; - this.failure = undefined; - for (const waiter of this.waiters.splice(0)) { - waiter.resolve({ done: true, value: undefined }); - } - } - - readonly interrupt = async (): Promise => { - this.interruptCalls.push(undefined); - }; - - readonly setModel = async (model?: string): Promise => { - this.setModelCalls.push(model); - }; - - readonly setPermissionMode = async (mode: PermissionMode): Promise => { - this.setPermissionModeCalls.push(mode); - }; - - readonly setMaxThinkingTokens = async (maxThinkingTokens: number | null): Promise => { - this.setMaxThinkingTokensCalls.push(maxThinkingTokens); - }; - - readonly close = (): void => { - this.closeCalls += 1; - this.finish(); - }; - - [Symbol.asyncIterator](): AsyncIterator { - return { - next: () => { - if (this.queue.length > 0) { - const value = this.queue.shift(); - if (value) { - return Promise.resolve({ - done: false, - value, - }); - } - } - if (this.failure !== undefined) { - const failure = this.failure; - this.failure = undefined; - return Promise.reject(failure); - } - if (this.done) { - return Promise.resolve({ - done: true, - value: undefined, - }); - } - return new Promise((resolve, reject) => { - this.waiters.push({ - resolve, - reject, - }); - }); - }, - }; - } -} - -function makeHarness(config?: { - readonly nativeEventLogPath?: string; - readonly nativeEventLogger?: ClaudeAdapterLiveOptions["nativeEventLogger"]; - readonly cwd?: string; - readonly baseDir?: string; - readonly claudeConfig?: Partial; - readonly instanceId?: ProviderInstanceId; -}) { - const query = new FakeClaudeQuery(); - let createInput: - | { - readonly prompt: AsyncIterable; - readonly options: ClaudeQueryOptions; - } - | undefined; - - const adapterOptions: ClaudeAdapterLiveOptions = { - ...(config?.instanceId ? { instanceId: config.instanceId } : {}), - createQuery: (input) => { - createInput = input; - return query; - }, - ...(config?.nativeEventLogger - ? { - nativeEventLogger: config.nativeEventLogger, - } - : {}), - ...(config?.nativeEventLogPath - ? { - nativeEventLogPath: config.nativeEventLogPath, - } - : {}), - }; - - return { - layer: Layer.effect( - ClaudeAdapter, - Effect.gen(function* () { - const claudeConfig = decodeClaudeSettings(config?.claudeConfig ?? {}); - return yield* makeClaudeAdapter(claudeConfig, adapterOptions); - }), - ).pipe( - Layer.provideMerge( - ServerConfig.layerTest( - config?.cwd ?? "/tmp/claude-adapter-test", - config?.baseDir ?? "/tmp", - ), - ), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(NodeServices.layer), - ), - query, - getLastCreateQueryInput: () => createInput, - }; -} - -function makeDeterministicRandomService(seed = 0x1234_5678): { - nextIntUnsafe: () => number; - nextDoubleUnsafe: () => number; -} { - let state = seed >>> 0; - const nextIntUnsafe = (): number => { - state = (Math.imul(1_664_525, state) + 1_013_904_223) >>> 0; - return state; - }; - - return { - nextIntUnsafe, - nextDoubleUnsafe: () => nextIntUnsafe() / 0x1_0000_0000, - }; -} - -async function readFirstPromptText( - input: - | { - readonly prompt: AsyncIterable; - } - | undefined, -): Promise { - const iterator = input?.prompt[Symbol.asyncIterator](); - if (!iterator) { - return undefined; - } - const next = await iterator.next(); - if (next.done) { - return undefined; - } - if (typeof next.value.message.content === "string") { - return next.value.message.content; - } - const content = next.value.message.content[0]; - if (!content || content.type !== "text") { - return undefined; - } - return content.text; -} - -async function readFirstPromptMessage( - input: - | { - readonly prompt: AsyncIterable; - } - | undefined, -): Promise { - const iterator = input?.prompt[Symbol.asyncIterator](); - if (!iterator) { - return undefined; - } - const next = await iterator.next(); - if (next.done) { - return undefined; - } - return next.value; -} - -const THREAD_ID = ThreadId.make("thread-claude-1"); -const RESUME_THREAD_ID = ThreadId.make("thread-claude-resume"); - -describe("ClaudeAdapterLive", () => { - it.effect("returns validation error for non-claude provider on startSession", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - const result = yield* adapter - .startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("codex"), - runtimeMode: "full-access", - }) - .pipe(Effect.result); - - assert.equal(result._tag, "Failure"); - if (result._tag !== "Failure") { - return; - } - assert.deepEqual( - result.failure, - new ProviderAdapterValidationError({ - provider: ProviderDriverKind.make("claudeAgent"), - operation: "startSession", - issue: "Expected provider 'claudeAgent' but received 'codex'.", - }), - ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("retains Claude session startup causes without exposing their messages", () => { - const cause = new Error("credential material that must remain in the cause chain"); - const layer = Layer.effect( - ClaudeAdapter, - Effect.gen(function* () { - const claudeConfig = decodeClaudeSettings({}); - return yield* makeClaudeAdapter(claudeConfig, { - createQuery: () => { - throw cause; - }, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest("/tmp/claude-adapter-test", "/tmp")), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(NodeServices.layer), - ); - - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - const error = yield* adapter - .startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }) - .pipe(Effect.flip); - - assert.instanceOf(error, ProviderAdapterProcessError); - assert.equal(error.detail, "Failed to start Claude runtime session."); - assert.strictEqual(error.cause, cause); - assert.notMatch(error.message, /credential material/u); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(layer), - ); - }); - - it.effect("derives bypass permission mode from full-access runtime policy", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.deepEqual(createInput?.options.settingSources, ["user", "project", "local"]); - assert.equal(createInput?.options.permissionMode, "bypassPermissions"); - assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("loads Claude filesystem settings sources for SDK sessions", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "approval-required", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.deepEqual(createInput?.options.settingSources, ["user", "project", "local"]); - assert.equal(createInput?.options.permissionMode, undefined); - assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("uses bypass permissions for full-access claude sessions", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.permissionMode, "bypassPermissions"); - assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("forwards claude effort levels into query options", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-opus-4-6", - [{ id: "effort", value: "max" }], - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.effort, "max"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("runs Claude SDK sessions with the configured Claude HOME", () => { - const harness = makeHarness({ claudeConfig: { homePath: "~/.claude-work" } }); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-opus-4-6", - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.env?.HOME, NodePath.join(NodeOS.homedir(), ".claude-work")); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("maps the Claude Opus 4.7 default effort to the SDK-supported max value", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-7", - }, - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.effort, "max"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("maps xhigh effort for Claude Opus 4.7 to the SDK-supported max value", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-opus-4-7", - [{ id: "effort", value: "xhigh" }], - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.effort, "max"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("preserves xhigh effort for Claude Fable 5", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-fable-5", - [{ id: "effort", value: "xhigh" }], - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.effort, "xhigh"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "effort", value: "max" }], - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.effort, "high"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("ignores adaptive effort for Haiku 4.5", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-haiku-4-5", - [{ id: "effort", value: "high" }], - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.effort, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("forwards Claude thinking toggle into SDK settings for Haiku 4.5", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-haiku-4-5", - [{ id: "thinking", value: false }], - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.deepEqual(createInput?.options.settings, { - alwaysThinkingEnabled: false, - }); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("ignores Claude thinking toggle for non-Haiku models", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "thinking", value: false }], - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.settings, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("forwards claude fast mode into SDK settings", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-opus-4-6", - [{ id: "fastMode", value: true }], - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.deepEqual(createInput?.options.settings, { - fastMode: true, - }); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("ignores claude fast mode for non-opus models", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "fastMode", value: true }], - ), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.settings, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("treats ultrathink as a prompt keyword instead of a session effort", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "effort", value: "ultrathink" }], - ), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "Investigate the edge cases", - attachments: [], - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-sonnet-4-6", - [{ id: "effort", value: "ultrathink" }], - ), - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.effort, "high"); - const promptText = yield* Effect.promise(() => readFirstPromptText(createInput)); - assert.equal(promptText, "Ultrathink:\nInvestigate the edge cases"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("embeds image attachments in Claude user messages", () => { - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "claude-attachments-")); - const harness = makeHarness({ - cwd: "/tmp/project-claude-attachments", - baseDir, - }); - return Effect.gen(function* () { - yield* Effect.addFinalizer(() => - Effect.sync(() => - NodeFS.rmSync(baseDir, { - recursive: true, - force: true, - }), - ), - ); - - const adapter = yield* ClaudeAdapter; - const { attachmentsDir } = yield* ServerConfig; - - const attachment = { - type: "image" as const, - id: "thread-claude-attachment-12345678-1234-1234-1234-123456789abc", - name: "diagram.png", - mimeType: "image/png", - sizeBytes: 4, - }; - const attachmentPath = NodePath.join(attachmentsDir, attachmentRelativePath(attachment)); - NodeFS.mkdirSync(NodePath.dirname(attachmentPath), { recursive: true }); - NodeFS.writeFileSync(attachmentPath, Uint8Array.from([1, 2, 3, 4])); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "What's in this image?", - attachments: [attachment], - }); - - const createInput = harness.getLastCreateQueryInput(); - const promptMessage = yield* Effect.promise(() => readFirstPromptMessage(createInput)); - assert.isDefined(promptMessage); - assert.deepEqual(promptMessage?.message.content, [ - { - type: "text", - text: "What's in this image?", - }, - { - type: "image", - source: { - type: "base64", - media_type: "image/png", - data: "AQIDBA==", - }, - }, - ]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 10).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-sonnet-4-5", - }, - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-1", - uuid: "stream-0", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 0, - content_block: { - type: "text", - text: "", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-1", - uuid: "stream-1", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "Hi", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-1", - uuid: "stream-2", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 0, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-1", - uuid: "stream-3", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 1, - content_block: { - type: "tool_use", - id: "tool-1", - name: "Bash", - input: { - command: "ls", - }, - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-1", - uuid: "stream-4", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 1, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-1", - uuid: "assistant-1", - parent_tool_use_id: null, - message: { - id: "assistant-message-1", - content: [{ type: "text", text: "Hi" }], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-1", - uuid: "result-1", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "content.delta", - "item.completed", - "item.started", - "item.completed", - "turn.completed", - ], - ); - - const turnStarted = runtimeEvents[3]; - assert.equal(turnStarted?.type, "turn.started"); - if (turnStarted?.type === "turn.started") { - assert.equal(String(turnStarted.turnId), String(turn.turnId)); - } - - const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); - assert.equal(deltaEvent?.type, "content.delta"); - if (deltaEvent?.type === "content.delta") { - assert.equal(deltaEvent.payload.delta, "Hi"); - assert.equal(String(deltaEvent.turnId), String(turn.turnId)); - } - - const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); - assert.equal(toolStarted?.type, "item.started"); - if (toolStarted?.type === "item.started") { - assert.equal(toolStarted.payload.itemType, "command_execution"); - } - - const assistantCompletedIndex = runtimeEvents.findIndex( - (event) => - event.type === "item.completed" && event.payload.itemType === "assistant_message", - ); - const toolStartedIndex = runtimeEvents.findIndex((event) => event.type === "item.started"); - assert.equal( - assistantCompletedIndex >= 0 && - toolStartedIndex >= 0 && - assistantCompletedIndex < toolStartedIndex, - true, - ); - - const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; - assert.equal(turnCompleted?.type, "turn.completed"); - if (turnCompleted?.type === "turn.completed") { - assert.equal(String(turnCompleted.turnId), String(turn.turnId)); - assert.equal(turnCompleted.payload.state, "completed"); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("steers a running turn instead of opening a new one on mid-turn sendTurn", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.takeUntil( - adapter.streamEvents, - (event) => event.type === "turn.completed", - ).pipe(Stream.runCollect, Effect.forkChild); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "run 5 commands", - attachments: [], - }); - - // Steer: a second sendTurn while the turn is still running continues - // the same turn — the message is queued into the live agent loop. - const steeredTurn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "actually run 15", - attachments: [], - }); - assert.equal(String(steeredTurn.turnId), String(turn.turnId)); - - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-steer", - uuid: "assistant-steer-1", - parent_tool_use_id: null, - message: { - id: "assistant-message-steer-1", - content: [{ type: "text", text: "Adjusting to 15." }], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-steer", - uuid: "result-steer-1", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const turnStartedEvents = runtimeEvents.filter((event) => event.type === "turn.started"); - const turnCompletedEvents = runtimeEvents.filter((event) => event.type === "turn.completed"); - - // One turn boundary for the whole run: the steer produced no - // turn.completed/turn.started pair. - assert.equal(turnStartedEvents.length, 1); - assert.equal(String(turnStartedEvents[0]?.turnId), String(turn.turnId)); - assert.equal(turnCompletedEvents.length, 1); - assert.equal(String(turnCompletedEvents[0]?.turnId), String(turn.turnId)); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("maps Claude reasoning deltas, streamed tool inputs, and tool results", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 11).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-tool-streams", - uuid: "stream-thinking", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "thinking_delta", - thinking: "Let", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-tool-streams", - uuid: "stream-tool-start", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 1, - content_block: { - type: "tool_use", - id: "tool-grep-1", - name: "Grep", - input: {}, - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-tool-streams", - uuid: "stream-tool-input-1", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 1, - delta: { - type: "input_json_delta", - partial_json: '{"pattern":"foo","path":"src"}', - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-tool-streams", - uuid: "stream-tool-stop", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 1, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "user", - session_id: "sdk-session-tool-streams", - uuid: "user-tool-result", - parent_tool_use_id: null, - message: { - role: "user", - content: [ - { - type: "tool_result", - tool_use_id: "tool-grep-1", - content: "src/example.ts:1:foo", - }, - ], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-tool-streams", - uuid: "result-tool-streams", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "content.delta", - "item.started", - "item.updated", - "item.updated", - "item.completed", - "turn.completed", - ], - ); - - const reasoningDelta = runtimeEvents.find( - (event) => event.type === "content.delta" && event.payload.streamKind === "reasoning_text", - ); - assert.equal(reasoningDelta?.type, "content.delta"); - if (reasoningDelta?.type === "content.delta") { - assert.equal(reasoningDelta.payload.delta, "Let"); - assert.equal(String(reasoningDelta.turnId), String(turn.turnId)); - } - - const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); - assert.equal(toolStarted?.type, "item.started"); - if (toolStarted?.type === "item.started") { - assert.equal(toolStarted.payload.itemType, "dynamic_tool_call"); - } - - const toolInputUpdated = runtimeEvents.find( - (event) => - event.type === "item.updated" && - (event.payload.data as { input?: { pattern?: string; path?: string } } | undefined)?.input - ?.pattern === "foo", - ); - assert.equal(toolInputUpdated?.type, "item.updated"); - if (toolInputUpdated?.type === "item.updated") { - assert.deepEqual(toolInputUpdated.payload.data, { - toolName: "Grep", - input: { - pattern: "foo", - path: "src", - }, - }); - } - - const toolResultUpdated = runtimeEvents.find( - (event) => - event.type === "item.updated" && - (event.payload.data as { result?: { tool_use_id?: string } } | undefined)?.result - ?.tool_use_id === "tool-grep-1", - ); - assert.equal(toolResultUpdated?.type, "item.updated"); - if (toolResultUpdated?.type === "item.updated") { - assert.equal( - ( - toolResultUpdated.payload.data as { - result?: { content?: string }; - } - ).result?.content, - "src/example.ts:1:foo", - ); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("falls back to a default plan step label for blank TodoWrite content", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 10).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-todo-plan", - uuid: "stream-todo-start", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 1, - content_block: { - type: "tool_use", - id: "tool-todo-1", - name: "TodoWrite", - input: {}, - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-todo-plan", - uuid: "stream-todo-input", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 1, - delta: { - type: "input_json_delta", - partial_json: - '{"todos":[{"content":" ","status":"in_progress"},{"content":"Ship it","status":"completed"}]}', - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-todo-plan", - uuid: "stream-todo-stop", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 1, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-todo-plan", - uuid: "result-todo-plan", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const planUpdated = runtimeEvents.find((event) => event.type === "turn.plan.updated"); - assert.equal(planUpdated?.type, "turn.plan.updated"); - if (planUpdated?.type === "turn.plan.updated") { - assert.equal(String(planUpdated.turnId), String(turn.turnId)); - assert.deepEqual(planUpdated.payload.plan, [ - { step: "Task", status: "inProgress" }, - { step: "Ship it", status: "completed" }, - ]); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("classifies Claude Task tool invocations as collaboration agent work", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "delegate this", - attachments: [], - }); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-task", - uuid: "stream-task-1", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 0, - content_block: { - type: "tool_use", - id: "tool-task-1", - name: "Task", - input: { - description: "Review the database layer", - prompt: "Audit the SQL changes", - subagent_type: "code-reviewer", - }, - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-task", - uuid: "assistant-task-1", - parent_tool_use_id: null, - message: { - id: "assistant-message-task-1", - content: [{ type: "text", text: "Delegated" }], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-task", - uuid: "result-task-1", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); - assert.equal(toolStarted?.type, "item.started"); - if (toolStarted?.type === "item.started") { - assert.equal(toolStarted.payload.itemType, "collab_agent_tool_call"); - assert.equal(toolStarted.payload.title, "Subagent task"); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("treats user-aborted Claude results as interrupted without a runtime error", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "result", - subtype: "error_during_execution", - is_error: false, - errors: ["Error: Request was aborted."], - stop_reason: "tool_use", - session_id: "sdk-session-abort", - uuid: "result-abort", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "turn.completed", - ], - ); - - const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; - assert.equal(turnCompleted?.type, "turn.completed"); - if (turnCompleted?.type === "turn.completed") { - assert.equal(String(turnCompleted.turnId), String(turn.turnId)); - assert.equal(turnCompleted.payload.state, "interrupted"); - assert.equal(turnCompleted.payload.errorMessage, "Error: Request was aborted."); - assert.equal(turnCompleted.payload.stopReason, "tool_use"); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("closes the session when the Claude stream aborts after a turn starts", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - const runtimeEvents: Array = []; - - const runtimeEventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => - Effect.sync(() => { - runtimeEvents.push(event); - }), - ).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: THREAD_ID, - input: "hello", - attachments: [], - }); - - harness.query.fail(new Error("All fibers interrupted without error")); - - yield* Effect.yieldNow; - yield* Effect.yieldNow; - yield* Effect.yieldNow; - runtimeEventsFiber.interruptUnsafe(); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "turn.completed", - "session.exited", - ], - ); - - const turnCompleted = runtimeEvents[4]; - assert.equal(turnCompleted?.type, "turn.completed"); - if (turnCompleted?.type === "turn.completed") { - assert.equal(String(turnCompleted.turnId), String(turn.turnId)); - assert.equal(turnCompleted.payload.state, "interrupted"); - assert.equal(turnCompleted.payload.errorMessage, "Claude runtime interrupted."); - } - - const sessionExited = runtimeEvents[5]; - assert.equal(sessionExited?.type, "session.exited"); - - assert.equal(yield* adapter.hasSession(THREAD_ID), false); - const sessions = yield* adapter.listSessions(); - assert.equal(sessions.length, 0); - assert.equal(harness.query.closeCalls, 1); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("keeps Claude stream failure events structural", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - const runtimeEvents: Array = []; - const runtimeEventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => - Effect.sync(() => { - runtimeEvents.push(event); - }), - ).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - yield* adapter.sendTurn({ - threadId: THREAD_ID, - input: "hello", - attachments: [], - }); - - harness.query.fail(new Error("credential material that must stay in the cause chain")); - - yield* Effect.yieldNow; - yield* Effect.yieldNow; - yield* Effect.yieldNow; - runtimeEventsFiber.interruptUnsafe(); - - const runtimeError = runtimeEvents.find((event) => event.type === "runtime.error"); - assert.equal(runtimeError?.type, "runtime.error"); - if (runtimeError?.type === "runtime.error") { - assert.equal(runtimeError.payload.message, "Claude runtime stream failed."); - assert.deepEqual(runtimeError.payload.detail, { - failureCount: 1, - failureTags: ["ProviderAdapterProcessError"], - }); - } - - const completed = runtimeEvents.find((event) => event.type === "turn.completed"); - assert.equal(completed?.type, "turn.completed"); - if (completed?.type === "turn.completed") { - assert.equal(completed.payload.state, "failed"); - assert.equal(completed.payload.errorMessage, "Claude runtime stream failed."); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("closes the previous session before replacing an existing thread session", () => { - const queries: FakeClaudeQuery[] = []; - const layer = Layer.effect( - ClaudeAdapter, - Effect.gen(function* () { - const claudeConfig = decodeClaudeSettings({}); - return yield* makeClaudeAdapter(claudeConfig, { - createQuery: () => { - const query = new FakeClaudeQuery(); - queries.push(query); - return query; - }, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest("/tmp/claude-adapter-test", "/tmp")), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(NodeServices.layer), - ); - - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const firstSession = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const secondSession = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - resumeCursor: firstSession.resumeCursor, - }); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const activeSessions = yield* adapter.listSessions(); - - assert.equal(queries.length, 2); - assert.equal(queries[0]?.closeCalls, 1); - assert.equal(queries[1]?.closeCalls, 0); - assert.equal(yield* adapter.hasSession(THREAD_ID), true); - assert.equal(activeSessions.length, 1); - assert.deepEqual(activeSessions[0]?.resumeCursor, secondSession.resumeCursor); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "session.started", - "session.configured", - "session.state.changed", - ], - ); - assert.equal( - runtimeEvents.some((event) => event.type === "session.exited"), - false, - ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(layer), - ); - }); - - it.effect("stopSession does not throw into the SDK prompt consumer", () => { - // The SDK consumes user messages via `for await (... of prompt)`. - // Stopping a session must end that loop cleanly — not throw an error. - // - // FakeClaudeQuery.close() masks this by resolving pending iterators - // before the shutdown propagates. Override it to match real SDK behavior - // where close() does not resolve the prompt consumer. - const query = new FakeClaudeQuery(); - (query as { close: () => void }).close = () => { - query.closeCalls += 1; - }; - - let promptConsumerError: unknown = undefined; - - const layer = Layer.effect( - ClaudeAdapter, - Effect.gen(function* () { - const claudeConfig = decodeClaudeSettings({}); - return yield* makeClaudeAdapter(claudeConfig, { - createQuery: (input) => { - // Simulate the SDK consuming the prompt iterable - (async () => { - try { - for await (const _message of input.prompt) { - /* SDK processes user messages */ - } - } catch (error) { - promptConsumerError = error; - } - })(); - return query; - }, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest("/tmp/claude-adapter-test", "/tmp")), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(NodeServices.layer), - ); - - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.runForEach( - adapter.streamEvents, - () => Effect.void, - ).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* adapter.stopSession(THREAD_ID); - - yield* Effect.yieldNow; - yield* Effect.yieldNow; - yield* Effect.yieldNow; - yield* TestClock.adjust("50 millis"); - yield* Effect.yieldNow; - - runtimeEventsFiber.interruptUnsafe(); - - assert.equal( - promptConsumerError, - undefined, - `Prompt consumer should not receive a thrown error on session stop, ` + - `but got: "${promptConsumerError instanceof Error ? promptConsumerError.message : String(promptConsumerError)}"`, - ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(layer), - ); - }); - - it.effect("forwards Claude task progress summaries for subagent updates", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - harness.query.emit({ - type: "system", - subtype: "task_progress", - task_id: "task-subagent-1", - description: "Running background teammate", - summary: "Code reviewer checked the migration edge cases.", - usage: { - total_tokens: 123, - tool_uses: 4, - duration_ms: 987, - }, - session_id: "sdk-session-task-summary", - uuid: "task-progress-1", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const progressEvent = runtimeEvents.find((event) => event.type === "task.progress"); - assert.equal(progressEvent?.type, "task.progress"); - if (progressEvent?.type === "task.progress") { - assert.equal( - progressEvent.payload.summary, - "Code reviewer checked the migration edge cases.", - ); - assert.equal(progressEvent.payload.description, "Running background teammate"); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("emits thread token usage updates from Claude task progress", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - harness.query.emit({ - type: "system", - subtype: "task_progress", - task_id: "task-usage-1", - description: "Thinking through the patch", - usage: { - total_tokens: 321, - tool_uses: 2, - duration_ms: 654, - }, - session_id: "sdk-session-task-usage", - uuid: "task-usage-progress-1", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const usageEvent = runtimeEvents.find((event) => event.type === "thread.token-usage.updated"); - const progressEvent = runtimeEvents.find((event) => event.type === "task.progress"); - assert.equal(usageEvent?.type, "thread.token-usage.updated"); - if (usageEvent?.type === "thread.token-usage.updated") { - assert.deepEqual(usageEvent.payload, { - usage: { - usedTokens: 321, - lastUsedTokens: 321, - toolUses: 2, - durationMs: 654, - }, - }); - } - assert.equal(progressEvent?.type, "task.progress"); - if (usageEvent && progressEvent) { - assert.notStrictEqual(usageEvent.eventId, progressEvent.eventId); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("emits Claude context window on result completion usage snapshots", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 7).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: THREAD_ID, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - duration_ms: 1234, - duration_api_ms: 1200, - num_turns: 1, - result: "done", - stop_reason: "end_turn", - session_id: "sdk-session-result-usage", - usage: { - input_tokens: 4, - cache_creation_input_tokens: 2715, - cache_read_input_tokens: 21144, - output_tokens: 679, - }, - modelUsage: { - "claude-opus-4-6": { - contextWindow: 200000, - maxOutputTokens: 64000, - }, - }, - } as unknown as SDKMessage); - harness.query.finish(); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const usageEvent = runtimeEvents.find((event) => event.type === "thread.token-usage.updated"); - assert.equal(usageEvent?.type, "thread.token-usage.updated"); - if (usageEvent?.type === "thread.token-usage.updated") { - assert.deepEqual(usageEvent.payload, { - usage: { - usedTokens: 24542, - lastUsedTokens: 24542, - inputTokens: 23863, - outputTokens: 679, - maxTokens: 200000, - }, - }); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("clamps oversized Claude usage to the reported context window", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 7).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: THREAD_ID, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - duration_ms: 1234, - duration_api_ms: 1200, - num_turns: 1, - result: "done", - stop_reason: "end_turn", - session_id: "sdk-session-result-usage-clamped", - usage: { - total_tokens: 535000, - }, - modelUsage: { - "claude-opus-4-6": { - contextWindow: 200000, - maxOutputTokens: 64000, - }, - }, - } as unknown as SDKMessage); - harness.query.finish(); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const usageEvent = runtimeEvents.find((event) => event.type === "thread.token-usage.updated"); - assert.equal(usageEvent?.type, "thread.token-usage.updated"); - if (usageEvent?.type === "thread.token-usage.updated") { - assert.deepEqual(usageEvent.payload, { - usage: { - usedTokens: 200000, - lastUsedTokens: 200000, - totalProcessedTokens: 535000, - maxTokens: 200000, - }, - }); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect( - "preserves oversized Claude result totals after task progress snapshots are recorded", - () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: THREAD_ID, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "system", - subtype: "task_progress", - task_id: "task-usage-clamped", - description: "Thinking through the patch", - usage: { - total_tokens: 190000, - }, - session_id: "sdk-session-task-usage-clamped", - uuid: "task-usage-progress-clamped", - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - duration_ms: 1234, - duration_api_ms: 1200, - num_turns: 1, - result: "done", - stop_reason: "end_turn", - session_id: "sdk-session-result-usage-clamped-after-progress", - usage: { - total_tokens: 535000, - }, - modelUsage: { - "claude-opus-4-6": { - contextWindow: 200000, - maxOutputTokens: 64000, - }, - }, - } as unknown as SDKMessage); - harness.query.finish(); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const usageEvents = runtimeEvents.filter( - (event) => event.type === "thread.token-usage.updated", - ); - const finalUsageEvent = usageEvents.at(-1); - assert.equal(finalUsageEvent?.type, "thread.token-usage.updated"); - if (finalUsageEvent?.type === "thread.token-usage.updated") { - assert.deepEqual(finalUsageEvent.payload, { - usage: { - usedTokens: 190000, - lastUsedTokens: 190000, - totalProcessedTokens: 535000, - maxTokens: 200000, - }, - }); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }, - ); - - it.effect( - "emits completion only after turn result when assistant frames arrive before deltas", - () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-early-assistant", - uuid: "assistant-early", - parent_tool_use_id: null, - message: { - id: "assistant-message-early", - content: [ - { type: "tool_use", id: "tool-early", name: "Read", input: { path: "a.ts" } }, - ], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-early-assistant", - uuid: "stream-early", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "Late text", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-early-assistant", - uuid: "result-early", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "content.delta", - "item.completed", - "turn.completed", - ], - ); - - const deltaIndex = runtimeEvents.findIndex((event) => event.type === "content.delta"); - const completedIndex = runtimeEvents.findIndex((event) => event.type === "item.completed"); - assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true); - - const deltaEvent = runtimeEvents[deltaIndex]; - assert.equal(deltaEvent?.type, "content.delta"); - if (deltaEvent?.type === "content.delta") { - assert.equal(deltaEvent.payload.delta, "Late text"); - assert.equal(String(deltaEvent.turnId), String(turn.turnId)); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }, - ); - - it.effect("creates a fresh assistant message when Claude reuses a text block index", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-reused-text-index", - uuid: "stream-reused-start-1", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 0, - content_block: { - type: "text", - text: "", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-reused-text-index", - uuid: "stream-reused-delta-1", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "First", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-reused-text-index", - uuid: "stream-reused-stop-1", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 0, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-reused-text-index", - uuid: "stream-reused-start-2", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 0, - content_block: { - type: "text", - text: "", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-reused-text-index", - uuid: "stream-reused-delta-2", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "Second", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-reused-text-index", - uuid: "stream-reused-stop-2", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 0, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-reused-text-index", - uuid: "result-reused-text-index", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "content.delta", - "item.completed", - "content.delta", - "item.completed", - ], - ); - - const assistantDeltas = runtimeEvents.filter( - (event) => event.type === "content.delta" && event.payload.streamKind === "assistant_text", - ); - assert.equal(assistantDeltas.length, 2); - if (assistantDeltas.length !== 2) { - return; - } - const [firstAssistantDelta, secondAssistantDelta] = assistantDeltas; - assert.equal(firstAssistantDelta?.type, "content.delta"); - assert.equal(secondAssistantDelta?.type, "content.delta"); - if ( - firstAssistantDelta?.type !== "content.delta" || - secondAssistantDelta?.type !== "content.delta" - ) { - return; - } - assert.equal(firstAssistantDelta.payload.delta, "First"); - assert.equal(secondAssistantDelta.payload.delta, "Second"); - assert.notEqual(firstAssistantDelta.itemId, secondAssistantDelta.itemId); - - const assistantCompletions = runtimeEvents.filter( - (event) => - event.type === "item.completed" && event.payload.itemType === "assistant_message", - ); - assert.equal(assistantCompletions.length, 2); - assert.equal(String(assistantCompletions[0]?.itemId), String(firstAssistantDelta.itemId)); - assert.equal(String(assistantCompletions[1]?.itemId), String(secondAssistantDelta.itemId)); - assert.notEqual( - String(assistantCompletions[0]?.itemId), - String(assistantCompletions[1]?.itemId), - ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("falls back to assistant payload text when stream deltas are absent", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-fallback-text", - uuid: "assistant-fallback", - parent_tool_use_id: null, - message: { - id: "assistant-message-fallback", - content: [{ type: "text", text: "Fallback hello" }], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-fallback-text", - uuid: "result-fallback", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "content.delta", - "item.completed", - "turn.completed", - ], - ); - - const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); - assert.equal(deltaEvent?.type, "content.delta"); - if (deltaEvent?.type === "content.delta") { - assert.equal(deltaEvent.payload.delta, "Fallback hello"); - assert.equal(String(deltaEvent.turnId), String(turn.turnId)); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("segments Claude assistant text blocks around tool calls", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 13).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-interleaved", - uuid: "stream-text-1-start", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 0, - content_block: { - type: "text", - text: "", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-interleaved", - uuid: "stream-text-1-delta", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "First message.", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-interleaved", - uuid: "stream-text-1-stop", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 0, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-interleaved", - uuid: "stream-tool-start", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 1, - content_block: { - type: "tool_use", - id: "tool-interleaved-1", - name: "Grep", - input: { - pattern: "assistant", - path: "src", - }, - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-interleaved", - uuid: "stream-tool-stop", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 1, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "user", - session_id: "sdk-session-interleaved", - uuid: "user-tool-result-interleaved", - parent_tool_use_id: null, - message: { - role: "user", - content: [ - { - type: "tool_result", - tool_use_id: "tool-interleaved-1", - content: "src/example.ts:1:assistant", - }, - ], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-interleaved", - uuid: "stream-text-2-start", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 2, - content_block: { - type: "text", - text: "", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-interleaved", - uuid: "stream-text-2-delta", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 2, - delta: { - type: "text_delta", - text: "Second message.", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-interleaved", - uuid: "stream-text-2-stop", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 2, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-interleaved", - uuid: "result-interleaved", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "content.delta", - "item.completed", - "item.started", - "item.updated", - "item.completed", - "content.delta", - "item.completed", - "turn.completed", - ], - ); - - const assistantTextDeltas = runtimeEvents.filter( - (event) => event.type === "content.delta" && event.payload.streamKind === "assistant_text", - ); - assert.equal(assistantTextDeltas.length, 2); - if (assistantTextDeltas.length !== 2) { - return; - } - const [firstAssistantDelta, secondAssistantDelta] = assistantTextDeltas; - if (!firstAssistantDelta || !secondAssistantDelta) { - return; - } - assert.notEqual(String(firstAssistantDelta.itemId), String(secondAssistantDelta.itemId)); - - const firstAssistantCompletedIndex = runtimeEvents.findIndex( - (event) => - event.type === "item.completed" && - event.payload.itemType === "assistant_message" && - String(event.itemId) === String(firstAssistantDelta.itemId), - ); - const toolStartedIndex = runtimeEvents.findIndex((event) => event.type === "item.started"); - const secondAssistantDeltaIndex = runtimeEvents.findIndex( - (event) => - event.type === "content.delta" && - event.payload.streamKind === "assistant_text" && - String(event.itemId) === String(secondAssistantDelta.itemId), - ); - - assert.equal( - firstAssistantCompletedIndex >= 0 && - toolStartedIndex >= 0 && - secondAssistantDeltaIndex >= 0 && - firstAssistantCompletedIndex < toolStartedIndex && - toolStartedIndex < secondAssistantDeltaIndex, - true, - ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("does not fabricate provider thread ids before first SDK session_id", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 5).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - assert.equal(session.threadId, THREAD_ID); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - assert.equal(turn.threadId, THREAD_ID); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-thread-real", - uuid: "stream-thread-real", - parent_tool_use_id: null, - event: { - type: "message_start", - message: { - id: "msg-thread-real", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-thread-real", - uuid: "result-thread-real", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - ], - ); - - const sessionStarted = runtimeEvents[0]; - assert.equal(sessionStarted?.type, "session.started"); - if (sessionStarted?.type === "session.started") { - assert.equal(sessionStarted.threadId, THREAD_ID); - } - - const threadStarted = runtimeEvents[4]; - assert.equal(threadStarted?.type, "thread.started"); - if (threadStarted?.type === "thread.started") { - assert.equal(threadStarted.threadId, THREAD_ID); - assert.deepEqual(threadStarted.payload, { - providerThreadId: "sdk-thread-real", - }); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("bridges approval request/response lifecycle through canUseTool", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "approval-required", - }); - - yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "approve this", - attachments: [], - }); - yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-approval-1", - uuid: "stream-approval-thread", - parent_tool_use_id: null, - event: { - type: "message_start", - message: { - id: "msg-approval-thread", - }, - }, - } as unknown as SDKMessage); - - const threadStarted = yield* Stream.runHead(adapter.streamEvents); - assert.equal(threadStarted._tag, "Some"); - if (threadStarted._tag !== "Some" || threadStarted.value.type !== "thread.started") { - return; - } - - const createInput = harness.getLastCreateQueryInput(); - const canUseTool = createInput?.options.canUseTool; - assert.equal(typeof canUseTool, "function"); - if (!canUseTool) { - return; - } - - const permissionPromise = canUseTool( - "Bash", - { command: "pwd" }, - { - signal: new AbortController().signal, - suggestions: [ - { - type: "setMode", - mode: "default", - destination: "session", - }, - ], - toolUseID: "tool-use-1", - }, - ); - - const requested = yield* Stream.runHead(adapter.streamEvents); - assert.equal(requested._tag, "Some"); - if (requested._tag !== "Some") { - return; - } - assert.equal(requested.value.type, "request.opened"); - if (requested.value.type !== "request.opened") { - return; - } - assert.deepEqual(requested.value.providerRefs, { - providerItemId: ProviderItemId.make("tool-use-1"), - }); - const runtimeRequestId = requested.value.requestId; - assert.equal(typeof runtimeRequestId, "string"); - if (runtimeRequestId === undefined) { - return; - } - - yield* adapter.respondToRequest( - session.threadId, - ApprovalRequestId.make(runtimeRequestId), - "accept", - ); - - const resolved = yield* Stream.runHead(adapter.streamEvents); - assert.equal(resolved._tag, "Some"); - if (resolved._tag !== "Some") { - return; - } - assert.equal(resolved.value.type, "request.resolved"); - if (resolved.value.type !== "request.resolved") { - return; - } - assert.equal(resolved.value.requestId, requested.value.requestId); - assert.equal(resolved.value.payload.decision, "accept"); - assert.deepEqual(resolved.value.providerRefs, { - providerItemId: ProviderItemId.make("tool-use-1"), - }); - - const permissionResult = yield* Effect.promise(() => permissionPromise); - assert.equal((permissionResult as PermissionResult).behavior, "allow"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("classifies Agent tools and read-only Claude tools correctly for approvals", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "approval-required", - }); - - yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); - - const createInput = harness.getLastCreateQueryInput(); - const canUseTool = createInput?.options.canUseTool; - assert.equal(typeof canUseTool, "function"); - if (!canUseTool) { - return; - } - - const agentPermissionPromise = canUseTool( - "Agent", - {}, - { - signal: new AbortController().signal, - toolUseID: "tool-agent-1", - }, - ); - - const agentRequested = yield* Stream.runHead(adapter.streamEvents); - assert.equal(agentRequested._tag, "Some"); - if (agentRequested._tag !== "Some" || agentRequested.value.type !== "request.opened") { - return; - } - assert.equal(agentRequested.value.payload.requestType, "dynamic_tool_call"); - - yield* adapter.respondToRequest( - session.threadId, - ApprovalRequestId.make(String(agentRequested.value.requestId)), - "accept", - ); - yield* Stream.runHead(adapter.streamEvents); - yield* Effect.promise(() => agentPermissionPromise); - - const grepPermissionPromise = canUseTool( - "Grep", - { pattern: "foo", path: "src" }, - { - signal: new AbortController().signal, - toolUseID: "tool-grep-approval-1", - }, - ); - - const grepRequested = yield* Stream.runHead(adapter.streamEvents); - assert.equal(grepRequested._tag, "Some"); - if (grepRequested._tag !== "Some" || grepRequested.value.type !== "request.opened") { - return; - } - assert.equal(grepRequested.value.payload.requestType, "file_read_approval"); - - yield* adapter.respondToRequest( - session.threadId, - ApprovalRequestId.make(String(grepRequested.value.requestId)), - "accept", - ); - yield* Stream.runHead(adapter.streamEvents); - yield* Effect.promise(() => grepPermissionPromise); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("passes Claude resume ids without pinning a stale assistant checkpoint", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: RESUME_THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - resumeCursor: { - threadId: "resume-thread-1", - resume: "550e8400-e29b-41d4-a716-446655440000", - resumeSessionAt: "assistant-99", - turnCount: 3, - }, - runtimeMode: "full-access", - }); - - assert.equal(session.threadId, RESUME_THREAD_ID); - assert.deepEqual(session.resumeCursor, { - threadId: RESUME_THREAD_ID, - resume: "550e8400-e29b-41d4-a716-446655440000", - resumeSessionAt: "assistant-99", - turnCount: 3, - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.resume, "550e8400-e29b-41d4-a716-446655440000"); - assert.equal(createInput?.options.sessionId, undefined); - assert.equal(createInput?.options.resumeSessionAt, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("preserves durable resume ids across Claude resume hooks", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - const durableSessionId = "550e8400-e29b-41d4-a716-446655440000"; - const transientHookSessionId = "7368d0c7-40a3-4d8a-bcc1-ac80c49f2719"; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 7).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - threadId: RESUME_THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - resumeCursor: { - threadId: RESUME_THREAD_ID, - resume: durableSessionId, - resumeSessionAt: "assistant-99", - turnCount: 3, - }, - runtimeMode: "full-access", - }); - - harness.query.emit({ - type: "system", - subtype: "hook_started", - hook_id: "resume-hook-1", - hook_name: "SessionStart:resume", - hook_event: "SessionStart", - session_id: transientHookSessionId, - uuid: "resume-hook-started", - } as unknown as SDKMessage); - - harness.query.emit({ - type: "system", - subtype: "hook_response", - hook_id: "resume-hook-1", - hook_name: "SessionStart:resume", - hook_event: "SessionStart", - output: "", - stdout: "", - stderr: "", - outcome: "success", - session_id: transientHookSessionId, - uuid: "resume-hook-response", - } as unknown as SDKMessage); - - harness.query.emit({ - type: "system", - subtype: "init", - apiKeySource: "none", - claude_code_version: "test", - cwd: "/tmp/claude-adapter-test", - tools: [], - mcp_servers: [], - model: "claude-sonnet-4-5", - permissionMode: "bypassPermissions", - slash_commands: [], - output_style: "default", - skills: [], - plugins: [], - session_id: durableSessionId, - uuid: "resume-init", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const threadStartedEvents = runtimeEvents.filter((event) => event.type === "thread.started"); - assert.equal(threadStartedEvents.length, 1); - const threadStarted = threadStartedEvents[0]; - assert.equal(threadStarted?.type, "thread.started"); - if (threadStarted?.type === "thread.started") { - assert.deepEqual(threadStarted.payload, { - providerThreadId: durableSessionId, - }); - } - - const activeSessions = yield* adapter.listSessions(); - const resumeCursor = activeSessions[0]?.resumeCursor as - | { - readonly resume?: string; - } - | undefined; - assert.equal(resumeCursor?.resume, durableSessionId); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("uses an app-generated Claude session id for fresh sessions", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - const sessionResumeCursor = session.resumeCursor as { - threadId?: string; - resume?: string; - turnCount?: number; - }; - assert.equal(sessionResumeCursor.threadId, THREAD_ID); - assert.equal(typeof sessionResumeCursor.resume, "string"); - assert.equal(sessionResumeCursor.turnCount, 0); - assert.match( - sessionResumeCursor.resume ?? "", - /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - ); - assert.equal(createInput?.options.resume, undefined); - assert.equal(createInput?.options.sessionId, sessionResumeCursor.resume); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect( - "supports rollbackThread by trimming in-memory turns and preserving earlier turns", - () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - const firstTurn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "first", - attachments: [], - }); - - const firstCompletedFiber = yield* Stream.filter( - adapter.streamEvents, - (event) => event.type === "turn.completed", - ).pipe(Stream.runHead, Effect.forkChild); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-rollback", - uuid: "result-first", - } as unknown as SDKMessage); - - const firstCompleted = yield* Fiber.join(firstCompletedFiber); - assert.equal(firstCompleted._tag, "Some"); - if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") { - assert.equal(String(firstCompleted.value.turnId), String(firstTurn.turnId)); - } - - const secondTurn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "second", - attachments: [], - }); - - const secondCompletedFiber = yield* Stream.filter( - adapter.streamEvents, - (event) => event.type === "turn.completed", - ).pipe(Stream.runHead, Effect.forkChild); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-rollback", - uuid: "result-second", - } as unknown as SDKMessage); - - const secondCompleted = yield* Fiber.join(secondCompletedFiber); - assert.equal(secondCompleted._tag, "Some"); - if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") { - assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); - } - - const threadBeforeRollback = yield* adapter.readThread(session.threadId); - assert.equal(threadBeforeRollback.turns.length, 2); - - const rolledBack = yield* adapter.rollbackThread(session.threadId, 1); - assert.equal(rolledBack.turns.length, 1); - assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId); - - const threadAfterRollback = yield* adapter.readThread(session.threadId); - assert.equal(threadAfterRollback.turns.length, 1); - assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }, - ); - - it.effect("updates model on sendTurn when model override is provided", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }, - attachments: [], - }); - - assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6"]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("updates model on sendTurn for the adapter's bound custom instance id", () => { - const customInstanceId = ProviderInstanceId.make("claude_openrouter"); - const harness = makeHarness({ instanceId: customInstanceId }); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - modelSelection: { - instanceId: customInstanceId, - model: "openai/gpt-5.5", - }, - attachments: [], - }); - - assert.deepEqual(harness.query.setModelCalls, ["openai/gpt-5.5"]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect( - "does not re-set the Claude model when the session already uses the same effective API model", - () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - const modelSelection = { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - modelSelection, - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - modelSelection, - attachments: [], - }); - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello again", - modelSelection, - attachments: [], - }); - - assert.deepEqual(harness.query.setModelCalls, []); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }, - ); - - it.effect("re-sets the Claude model when the effective API model changes", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-opus-4-6", - [{ id: "contextWindow", value: "1m" }], - ), - attachments: [], - }); - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello again", - modelSelection: { - instanceId: ProviderInstanceId.make("claudeAgent"), - model: "claude-opus-4-6", - }, - attachments: [], - }); - - assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6[1m]", "claude-opus-4-6"]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("sets plan permission mode on sendTurn when interactionMode is plan", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "plan this for me", - interactionMode: "plan", - attachments: [], - }); - - assert.deepEqual(harness.query.setPermissionModeCalls, ["plan"]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect.each<{ runtimeMode: RuntimeMode; expectedBase: PermissionMode }>([ - { runtimeMode: "full-access", expectedBase: "bypassPermissions" }, - { runtimeMode: "approval-required", expectedBase: "default" }, - { runtimeMode: "auto-accept-edits", expectedBase: "acceptEdits" }, - ])( - "restores $expectedBase permission mode after plan turn ($runtimeMode)", - ({ runtimeMode, expectedBase }) => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode, - }); - - // First turn in plan mode - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "plan this", - interactionMode: "plan", - attachments: [], - }); - - // Complete the turn so we can send another - const turnCompletedFiber = yield* Stream.filter( - adapter.streamEvents, - (event) => event.type === "turn.completed", - ).pipe(Stream.runHead, Effect.forkChild); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: `sdk-session-${runtimeMode}`, - uuid: `result-${runtimeMode}`, - } as unknown as SDKMessage); - - yield* Fiber.join(turnCompletedFiber); - - // Second turn back to default - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "now do it", - interactionMode: "default", - attachments: [], - }); - - assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", expectedBase]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }, - ); - - it.effect("does not call setPermissionMode when interactionMode is absent", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - assert.deepEqual(harness.query.setPermissionModeCalls, []); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("captures ExitPlanMode as a proposed plan and denies auto-exit", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "plan this", - interactionMode: "plan", - attachments: [], - }); - yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); - - const createInput = harness.getLastCreateQueryInput(); - const canUseTool = createInput?.options.canUseTool; - assert.equal(typeof canUseTool, "function"); - if (!canUseTool) { - return; - } - - const permissionPromise = canUseTool( - "ExitPlanMode", - { - plan: "# Ship it\n\n- one\n- two", - allowedPrompts: [{ tool: "Bash", prompt: "run tests" }], - }, - { - signal: new AbortController().signal, - toolUseID: "tool-exit-1", - }, - ); - - const proposedEvent = yield* Stream.runHead(adapter.streamEvents); - assert.equal(proposedEvent._tag, "Some"); - if (proposedEvent._tag !== "Some") { - return; - } - assert.equal(proposedEvent.value.type, "turn.proposed.completed"); - if (proposedEvent.value.type !== "turn.proposed.completed") { - return; - } - assert.equal(proposedEvent.value.payload.planMarkdown, "# Ship it\n\n- one\n- two"); - assert.deepEqual(proposedEvent.value.providerRefs, { - providerItemId: ProviderItemId.make("tool-exit-1"), - }); - - const permissionResult = yield* Effect.promise(() => permissionPromise); - assert.equal((permissionResult as PermissionResult).behavior, "deny"); - const deniedResult = permissionResult as PermissionResult & { - message?: string; - }; - assert.equal(deniedResult.message?.includes("captured your proposed plan"), true); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("extracts proposed plans from assistant ExitPlanMode snapshots", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "plan this", - interactionMode: "plan", - attachments: [], - }); - yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); - - const proposedEventFiber = yield* Stream.filter( - adapter.streamEvents, - (event) => event.type === "turn.proposed.completed", - ).pipe(Stream.runHead, Effect.forkChild); - - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-exit-plan", - uuid: "assistant-exit-plan", - parent_tool_use_id: null, - message: { - model: "claude-opus-4-6", - id: "msg-exit-plan", - type: "message", - role: "assistant", - content: [ - { - type: "tool_use", - id: "tool-exit-2", - name: "ExitPlanMode", - input: { - plan: "# Final plan\n\n- capture it", - }, - }, - ], - stop_reason: null, - stop_sequence: null, - usage: {}, - }, - } as unknown as SDKMessage); - - const proposedEvent = yield* Fiber.join(proposedEventFiber); - assert.equal(proposedEvent._tag, "Some"); - if (proposedEvent._tag !== "Some") { - return; - } - assert.equal(proposedEvent.value.type, "turn.proposed.completed"); - if (proposedEvent.value.type !== "turn.proposed.completed") { - return; - } - assert.equal(proposedEvent.value.payload.planMarkdown, "# Final plan\n\n- capture it"); - assert.deepEqual(proposedEvent.value.providerRefs, { - providerItemId: ProviderItemId.make("tool-exit-2"), - }); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("handles AskUserQuestion via user-input.requested/resolved lifecycle", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - // Start session in approval-required mode so canUseTool fires. - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "approval-required", - }); - - // Drain the session startup events (started, configured, state.changed). - yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); - - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "question turn", - attachments: [], - }); - yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-user-input-1", - uuid: "stream-user-input-thread", - parent_tool_use_id: null, - event: { - type: "message_start", - message: { - id: "msg-user-input-thread", - }, - }, - } as unknown as SDKMessage); - - const threadStarted = yield* Stream.runHead(adapter.streamEvents); - assert.equal(threadStarted._tag, "Some"); - if (threadStarted._tag !== "Some" || threadStarted.value.type !== "thread.started") { - return; - } - - const createInput = harness.getLastCreateQueryInput(); - const canUseTool = createInput?.options.canUseTool; - assert.equal(typeof canUseTool, "function"); - if (!canUseTool) { - return; - } - - // Simulate Claude calling AskUserQuestion with structured questions. - const askInput = { - questions: [ - { - question: "Which framework?", - header: "Framework", - options: [ - { label: "React", description: "React.js" }, - { label: "Vue", description: "Vue.js" }, - ], - multiSelect: false, - }, - ], - }; - - const permissionPromise = canUseTool("AskUserQuestion", askInput, { - signal: new AbortController().signal, - toolUseID: "tool-ask-1", - }); - - // The adapter should emit a user-input.requested event. - const requestedEvent = yield* Stream.runHead(adapter.streamEvents); - assert.equal(requestedEvent._tag, "Some"); - if (requestedEvent._tag !== "Some") { - return; - } - assert.equal(requestedEvent.value.type, "user-input.requested"); - if (requestedEvent.value.type !== "user-input.requested") { - return; - } - const requestId = requestedEvent.value.requestId; - assert.equal(typeof requestId, "string"); - assert.equal(requestedEvent.value.payload.questions.length, 1); - assert.equal(requestedEvent.value.payload.questions[0]?.question, "Which framework?"); - // Regression for #2388: `id` must equal the full question text so the - // UI's draft-answer key matches what the SDK looks up downstream. - assert.equal(requestedEvent.value.payload.questions[0]?.id, "Which framework?"); - assert.deepEqual(requestedEvent.value.providerRefs, { - providerItemId: ProviderItemId.make("tool-ask-1"), - }); - - // Respond with the user's answers. - yield* adapter.respondToUserInput(session.threadId, ApprovalRequestId.make(requestId!), { - "Which framework?": "React", - }); - - // The adapter should emit a user-input.resolved event. - const resolvedEvent = yield* Stream.runHead(adapter.streamEvents); - assert.equal(resolvedEvent._tag, "Some"); - if (resolvedEvent._tag !== "Some") { - return; - } - assert.equal(resolvedEvent.value.type, "user-input.resolved"); - if (resolvedEvent.value.type !== "user-input.resolved") { - return; - } - assert.deepEqual(resolvedEvent.value.payload.answers, { - "Which framework?": "React", - }); - assert.deepEqual(resolvedEvent.value.providerRefs, { - providerItemId: ProviderItemId.make("tool-ask-1"), - }); - - // The canUseTool promise should resolve with the answers in SDK format. - const permissionResult = yield* Effect.promise(() => permissionPromise); - assert.equal((permissionResult as PermissionResult).behavior, "allow"); - const updatedInput = (permissionResult as { updatedInput: Record }) - .updatedInput; - assert.deepEqual(updatedInput.answers, { "Which framework?": "React" }); - // Original questions should be passed through. - assert.deepEqual(updatedInput.questions, askInput.questions); - - // Compatibility check for #2388: the answers shape we hand to the SDK - // must produce a non-empty rendered tool_result on BOTH SDK iteration - // patterns we have seen, so we don't regress the issue and we don't - // break users still on the older Claude CLI. - const sdkAnswers = updatedInput.answers as Record; - const sdkQuestions = updatedInput.questions as ReadonlyArray<{ - readonly question: string; - }>; - - // Claude CLI 2.1.119 — key-agnostic Object.entries iteration. Any key - // works here, but it must at least round-trip into a non-empty string. - const v119Rendered = Object.entries(sdkAnswers) - .map(([key, value]) => `"${key}"="${String(value)}"`) - .join(", "); - assert.equal(v119Rendered, '"Which framework?"="React"'); - - // Claude CLI 2.1.121 — lookup by full question text. This is the path - // that regressed in #2388 when the answers were keyed by `header`. - const v121Rendered = sdkQuestions - .map(({ question }) => { - const answer = sdkAnswers[question]; - return answer === undefined ? null : `"${question}"="${String(answer)}"`; - }) - .filter((entry): entry is string => entry !== null) - .join(", "); - assert.notEqual(v121Rendered, "", "Expected non-empty SDK 2.1.121 tool_result (#2388)"); - assert.equal(v121Rendered, '"Which framework?"="React"'); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("routes AskUserQuestion through user-input flow even in full-access mode", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - // In full-access mode, regular tools are auto-approved. - // AskUserQuestion should still go through the user-input flow. - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - - yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); - - const createInput = harness.getLastCreateQueryInput(); - const canUseTool = createInput?.options.canUseTool; - assert.equal(typeof canUseTool, "function"); - if (!canUseTool) { - return; - } - - const askInput = { - questions: [ - { - question: "Deploy to which env?", - header: "Env", - options: [ - { label: "Staging", description: "Staging environment" }, - { label: "Production", description: "Production environment" }, - ], - multiSelect: false, - }, - ], - }; - - const permissionPromise = canUseTool("AskUserQuestion", askInput, { - signal: new AbortController().signal, - toolUseID: "tool-ask-2", - }); - - // Should still get user-input.requested even in full-access mode. - const requestedEvent = yield* Stream.runHead(adapter.streamEvents); - assert.equal(requestedEvent._tag, "Some"); - if (requestedEvent._tag !== "Some" || requestedEvent.value.type !== "user-input.requested") { - assert.fail("Expected user-input.requested event"); - return; - } - const requestId = requestedEvent.value.requestId; - - yield* adapter.respondToUserInput(session.threadId, ApprovalRequestId.make(requestId!), { - "Deploy to which env?": "Staging", - }); - - // Drain the resolved event. - yield* Stream.runHead(adapter.streamEvents); - - const permissionResult = yield* Effect.promise(() => permissionPromise); - assert.equal((permissionResult as PermissionResult).behavior, "allow"); - const updatedInput = (permissionResult as { updatedInput: Record }) - .updatedInput; - assert.deepEqual(updatedInput.answers, { "Deploy to which env?": "Staging" }); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("denies AskUserQuestion when the waiting turn is aborted", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "approval-required", - }); - - yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); - - const createInput = harness.getLastCreateQueryInput(); - const canUseTool = createInput?.options.canUseTool; - assert.equal(typeof canUseTool, "function"); - if (!canUseTool) { - return; - } - - const controller = new AbortController(); - const permissionPromise = canUseTool( - "AskUserQuestion", - { - questions: [ - { - question: "Continue?", - header: "Continue", - options: [{ label: "Yes", description: "Proceed" }], - multiSelect: false, - }, - ], - }, - { - signal: controller.signal, - toolUseID: "tool-ask-abort", - }, - ); - - const requestedEvent = yield* Stream.runHead(adapter.streamEvents); - assert.equal(requestedEvent._tag, "Some"); - if (requestedEvent._tag !== "Some" || requestedEvent.value.type !== "user-input.requested") { - assert.fail("Expected user-input.requested event"); - return; - } - assert.equal(requestedEvent.value.threadId, session.threadId); - - controller.abort(); - - const resolvedEvent = yield* Stream.runHead(adapter.streamEvents); - assert.equal(resolvedEvent._tag, "Some"); - if (resolvedEvent._tag !== "Some" || resolvedEvent.value.type !== "user-input.resolved") { - assert.fail("Expected user-input.resolved event"); - return; - } - assert.deepEqual(resolvedEvent.value.payload.answers, {}); - - const permissionResult = yield* Effect.promise(() => permissionPromise); - assert.deepEqual(permissionResult, { - behavior: "deny", - message: "User cancelled tool execution.", - } satisfies PermissionResult); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("writes provider-native observability records when enabled", () => { - const nativeEvents: Array<{ - event?: { - provider?: string; - method?: string; - threadId?: string; - turnId?: string; - }; - }> = []; - const nativeThreadIds: Array = []; - const harness = makeHarness({ - nativeEventLogger: { - filePath: "memory://claude-native-events", - write: (event, threadId) => { - nativeEvents.push(event as (typeof nativeEvents)[number]); - nativeThreadIds.push(threadId ?? null); - return Effect.void; - }, - close: () => Effect.void, - }, - }); - return Effect.gen(function* () { - const adapter = yield* ClaudeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: ProviderDriverKind.make("claudeAgent"), - runtimeMode: "full-access", - }); - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - const turnCompletedFiber = yield* Stream.filter( - adapter.streamEvents, - (event) => event.type === "turn.completed", - ).pipe(Stream.runHead, Effect.forkChild); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-native-log", - uuid: "stream-native-log", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "hi", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-native-log", - uuid: "result-native-log", - } as unknown as SDKMessage); - - const turnCompleted = yield* Fiber.join(turnCompletedFiber); - assert.equal(turnCompleted._tag, "Some"); - - assert.equal(nativeEvents.length > 0, true); - assert.equal( - nativeEvents.some((record) => record.event?.provider === "claudeAgent"), - true, - ); - assert.equal( - nativeEvents.some( - (record) => - String( - (record.event as { readonly providerThreadId?: string } | undefined) - ?.providerThreadId, - ) === "sdk-session-native-log", - ), - true, - ); - assert.equal( - nativeEvents.some((record) => String(record.event?.turnId) === String(turn.turnId)), - true, - ); - assert.equal( - nativeEvents.some( - (record) => record.event?.method === "claude/stream_event/content_block_delta/text_delta", - ), - true, - ); - assert.equal( - nativeThreadIds.every((threadId) => threadId === String(THREAD_ID)), - true, - ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); -}); diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts deleted file mode 100644 index 97a93f85829..00000000000 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ /dev/null @@ -1,3869 +0,0 @@ -/** - * ClaudeAdapterLive - Scoped live implementation for the Claude Agent provider adapter. - * - * Wraps `@anthropic-ai/claude-agent-sdk` query sessions behind the generic - * provider adapter contract and emits canonical runtime events. - * - * @module ClaudeAdapterLive - */ -import { - type CanUseTool, - query, - type Options as ClaudeQueryOptions, - type PermissionMode, - type PermissionResult, - type PermissionUpdate, - type SDKMessage, - type SDKControlGetContextUsageResponse, - type SDKResultMessage, - type SettingSource, - type SDKUserMessage, - type ModelUsage, -} from "@anthropic-ai/claude-agent-sdk"; -import { parseCliArgs } from "@t3tools/shared/cliArgs"; -import { - ApprovalRequestId, - type CanonicalItemType, - type CanonicalRequestType, - type ClaudeSettings, - EventId, - type ProviderApprovalDecision, - ProviderDriverKind, - ProviderInstanceId, - type ModelSelection, - ProviderItemId, - type ProviderRuntimeEvent, - type ProviderRuntimeTurnStatus, - type ProviderSendTurnInput, - type ProviderSession, - type ThreadTokenUsageSnapshot, - type ProviderUserInputAnswers, - type RuntimeContentStreamKind, - RuntimeItemId, - RuntimeRequestId, - RuntimeTaskId, - ThreadId, - TurnId, - type UserInputQuestion, -} from "@t3tools/contracts"; -import { - applyClaudePromptEffortPrefix, - getModelSelectionBooleanOptionValue, - getModelSelectionStringOptionValue, - getProviderOptionDescriptors, - resolvePromptInjectedEffort, -} from "@t3tools/shared/model"; -import * as Cause from "effect/Cause"; -import * as Crypto from "effect/Crypto"; -import * as DateTime from "effect/DateTime"; -import * as Deferred from "effect/Deferred"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as FileSystem from "effect/FileSystem"; -import * as Fiber from "effect/Fiber"; -import * as Path from "effect/Path"; -import * as Queue from "effect/Queue"; -import * as Ref from "effect/Ref"; -import * as Schema from "effect/Schema"; -import * as Stream from "effect/Stream"; - -import { resolveAttachmentPath } from "../../attachmentStore.ts"; -import { ServerConfig } from "../../config.ts"; -import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; -import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; -import { - getClaudeModelCapabilities, - isClaudeUltracodeEffort, - normalizeClaudeCliEffort, - resolveClaudeApiModelId, - resolveClaudeEffort, -} from "./ClaudeProvider.ts"; -import { - ProviderAdapterProcessError, - ProviderAdapterRequestError, - ProviderAdapterSessionClosedError, - ProviderAdapterSessionNotFoundError, - ProviderAdapterValidationError, - type ProviderAdapterError, -} from "../Errors.ts"; -import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; -import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; -const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); -const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.UnknownFromJsonString); - -const PROVIDER = ProviderDriverKind.make("claudeAgent"); -type ClaudeTextStreamKind = Extract; -type ClaudeToolResultStreamKind = Extract< - RuntimeContentStreamKind, - "command_output" | "file_change_output" ->; -type ClaudeSdkEffort = NonNullable; - -function encodeJsonStringForDiagnostics(input: unknown): string | undefined { - const result = encodeUnknownJsonStringExit(input); - return Exit.isSuccess(result) ? result.value : undefined; -} - -type PromptQueueItem = - | { - readonly type: "message"; - readonly message: SDKUserMessage; - } - | { - readonly type: "terminate"; - }; - -interface ClaudeResumeState { - readonly threadId?: ThreadId; - readonly resume?: string; - readonly resumeSessionAt?: string; - readonly turnCount?: number; -} - -interface ClaudeTurnState { - readonly turnId: TurnId; - readonly startedAt: string; - /** - * True for turns auto-started by assistant output arriving without an - * active turn (background agent/subagent responses between user prompts). - * Synthetic turns are auto-closed by the next sendTurn; real turns are - * steered instead (the queued message continues the same turn). - */ - readonly synthetic?: boolean; - readonly items: Array; - readonly assistantTextBlocks: Map; - readonly assistantTextBlockOrder: Array; - readonly capturedProposedPlanKeys: Set; - nextSyntheticAssistantBlockIndex: number; -} - -interface AssistantTextBlockState { - readonly itemId: string; - readonly blockIndex: number; - emittedTextDelta: boolean; - fallbackText: string; - streamClosed: boolean; - completionEmitted: boolean; -} - -interface PendingApproval { - readonly requestType: CanonicalRequestType; - readonly detail?: string; - readonly suggestions?: ReadonlyArray; - readonly decision: Deferred.Deferred; -} - -interface PendingUserInput { - readonly questions: ReadonlyArray; - readonly answers: Deferred.Deferred; -} - -interface ToolInFlight { - readonly itemId: string; - readonly itemType: CanonicalItemType; - readonly toolName: string; - readonly title: string; - readonly detail?: string; - readonly input: Record; - readonly partialInputJson: string; - readonly lastEmittedInputFingerprint?: string; -} - -interface ClaudeTaskState { - readonly id: string; - subject: string; - status: PlanStep["status"]; - readonly blockedBy: Set; -} - -interface ClaudeSessionContext { - session: ProviderSession; - readonly promptQueue: Queue.Queue; - readonly query: ClaudeQueryRuntime; - streamFiber: Fiber.Fiber | undefined; - readonly startedAt: string; - readonly basePermissionMode: PermissionMode | undefined; - currentApiModelId: string | undefined; - resumeSessionId: string | undefined; - readonly pendingApprovals: Map; - readonly pendingUserInputs: Map; - readonly turns: Array<{ - id: TurnId; - items: Array; - }>; - readonly inFlightTools: Map; - readonly claudeTasks: Map; - turnState: ClaudeTurnState | undefined; - lastKnownContextWindow: number | undefined; - lastKnownTokenUsage: ThreadTokenUsageSnapshot | undefined; - lastKnownTotalProcessedTokens: number | undefined; - lastAssistantUuid: string | undefined; - lastThreadStartedId: string | undefined; - stopped: boolean; -} - -interface ClaudeQueryRuntime extends AsyncIterable { - readonly interrupt: () => Promise; - readonly setModel: (model?: string) => Promise; - readonly setPermissionMode: (mode: PermissionMode) => Promise; - readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise; - readonly getContextUsage?: () => Promise; - readonly close: () => void; -} - -export interface ClaudeAdapterLiveOptions { - readonly instanceId?: ProviderInstanceId; - readonly environment?: NodeJS.ProcessEnv; - readonly createQuery?: (input: { - readonly prompt: AsyncIterable; - readonly options: ClaudeQueryOptions; - }) => ClaudeQueryRuntime; - readonly nativeEventLogPath?: string; - readonly nativeEventLogger?: EventNdjsonLogger; -} - -function isUuid(value: string): boolean { - return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); -} - -function isSyntheticClaudeThreadId(value: string): boolean { - return value.startsWith("claude-thread-"); -} - -function hasDurableClaudeSessionId(message: SDKMessage): boolean { - if (message.type !== "system") { - return true; - } - - return ( - message.subtype !== "hook_started" && - message.subtype !== "hook_progress" && - message.subtype !== "hook_response" - ); -} - -function toMessage(cause: unknown, fallback: string): string { - if (cause instanceof Error && cause.message.length > 0) { - return cause.message; - } - return fallback; -} - -function normalizeClaudeStreamMessages( - cause: Cause.Cause, -): ReadonlyArray { - const errors: Array = []; - for (const error of Cause.prettyErrors(cause)) { - const message = error.message.trim(); - if (message.length > 0) { - errors.push(message); - } - } - if (errors.length > 0) { - return errors; - } - - const squashed = toMessage(Cause.squash(cause), "").trim(); - return squashed.length > 0 ? [squashed] : []; -} - -function getEffectiveClaudeAgentEffort( - effort: string | null | undefined, - model: string | null | undefined, -): ClaudeSdkEffort | null { - const normalized = normalizeClaudeCliEffort(effort, model); - return normalized ? (normalized as ClaudeSdkEffort) : null; -} - -function isClaudeInterruptedMessage(message: string): boolean { - const normalized = message.toLowerCase(); - return ( - normalized.includes("all fibers interrupted without error") || - normalized.includes("request was aborted") || - normalized.includes("interrupted by user") - ); -} - -function isClaudeInterruptedCause(cause: Cause.Cause): boolean { - return ( - Cause.hasInterruptsOnly(cause) || - normalizeClaudeStreamMessages(cause).some(isClaudeInterruptedMessage) || - cause.reasons.some( - (reason) => - Cause.isFailReason(reason) && isClaudeInterruptedMessage(toMessage(reason.error.cause, "")), - ) - ); -} - -function resultErrorsText(result: SDKResultMessage): string { - return "errors" in result && Array.isArray(result.errors) - ? result.errors.join(" ").toLowerCase() - : ""; -} - -function isInterruptedResult(result: SDKResultMessage): boolean { - const errors = resultErrorsText(result); - if (errors.includes("interrupt")) { - return true; - } - - return ( - result.subtype === "error_during_execution" && - result.is_error === false && - (errors.includes("request was aborted") || - errors.includes("interrupted by user") || - errors.includes("aborted")) - ); -} - -function asRuntimeItemId(value: string): RuntimeItemId { - return RuntimeItemId.make(value); -} - -function maxClaudeContextWindowFromModelUsage( - modelUsage: Record | undefined, -): number | undefined { - if (!modelUsage) return undefined; - - let maxContextWindow: number | undefined; - for (const value of Object.values(modelUsage)) { - const contextWindow = value.contextWindow; - maxContextWindow = Math.max(maxContextWindow ?? 0, contextWindow); - } - - return maxContextWindow; -} - -function selectedClaudeContextWindow( - modelSelection: ModelSelection | undefined, -): number | undefined { - switch (modelSelection?.model) { - case "claude-opus-4-8": - case "claude-opus-4-7": - return 1_000_000; - } - - const optionValue = getModelSelectionStringOptionValue(modelSelection, "contextWindow"); - if (optionValue === "1m") { - return 1_000_000; - } - if (optionValue === "200k") { - return 200_000; - } - const caps = getClaudeModelCapabilities(modelSelection?.model); - const hasContextWindowOption = getProviderOptionDescriptors({ caps }).some( - (descriptor) => descriptor.type === "select" && descriptor.id === "contextWindow", - ); - if (hasContextWindowOption) { - return 200_000; - } - return undefined; -} - -function finiteNonNegativeInteger(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) && value >= 0 - ? Math.round(value) - : undefined; -} - -function finitePositiveInteger(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) && value > 0 - ? Math.round(value) - : undefined; -} - -function claudeUsageInputTokens(usage: Record): number { - return ( - (finiteNonNegativeInteger(usage.input_tokens) ?? 0) + - (finiteNonNegativeInteger(usage.cache_creation_input_tokens) ?? 0) + - (finiteNonNegativeInteger(usage.cache_read_input_tokens) ?? 0) - ); -} - -function claudeUsageOutputTokens(usage: Record): number { - return finiteNonNegativeInteger(usage.output_tokens) ?? 0; -} - -function lastClaudeUsageIteration( - value: Record, -): Record | undefined { - const iterations = Array.isArray(value.iterations) ? value.iterations : []; - return iterations.findLast( - (iteration): iteration is Record => - iteration !== null && typeof iteration === "object" && !Array.isArray(iteration), - ); -} - -function claudeTotalProcessedTokens(value: unknown): number | undefined { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return undefined; - } - - const usage = value as Record; - const explicitTotal = finiteNonNegativeInteger(usage.total_tokens); - if (explicitTotal !== undefined && explicitTotal > 0) { - return explicitTotal; - } - - const total = claudeUsageInputTokens(usage) + claudeUsageOutputTokens(usage); - return total > 0 ? total : undefined; -} - -function makeClaudeTokenUsageSnapshot(input: { - readonly activeTokens: number; - readonly inputTokens?: number; - readonly outputTokens?: number; - readonly contextWindow?: number; - readonly totalProcessedTokens?: number; - readonly lastUsedTokens?: number; - readonly compactsAutomatically?: boolean; -}): ThreadTokenUsageSnapshot | undefined { - const activeTokens = finiteNonNegativeInteger(input.activeTokens); - if (activeTokens === undefined || activeTokens <= 0) { - return undefined; - } - - const maxTokens = finitePositiveInteger(input.contextWindow); - const usedTokens = maxTokens !== undefined ? Math.min(activeTokens, maxTokens) : activeTokens; - const lastUsedTokens = - finiteNonNegativeInteger(input.lastUsedTokens) ?? - (maxTokens !== undefined ? Math.min(activeTokens, maxTokens) : activeTokens); - const totalProcessedTokens = finiteNonNegativeInteger(input.totalProcessedTokens); - const inputTokens = finiteNonNegativeInteger(input.inputTokens); - const outputTokens = finiteNonNegativeInteger(input.outputTokens); - - return { - usedTokens, - lastUsedTokens, - ...(totalProcessedTokens !== undefined && totalProcessedTokens > usedTokens - ? { totalProcessedTokens } - : {}), - ...(inputTokens !== undefined && inputTokens > 0 ? { inputTokens } : {}), - ...(outputTokens !== undefined && outputTokens > 0 ? { outputTokens } : {}), - ...(maxTokens !== undefined ? { maxTokens } : {}), - ...(input.compactsAutomatically !== undefined - ? { compactsAutomatically: input.compactsAutomatically } - : {}), - }; -} - -function normalizeClaudeActiveTokenUsage( - value: unknown, - contextWindow?: number, - totalProcessedTokens?: number, -): ThreadTokenUsageSnapshot | undefined { - if (!value || typeof value !== "object") { - return undefined; - } - - const usage = value as Record; - const activeUsage = lastClaudeUsageIteration(usage) ?? usage; - const inputTokens = claudeUsageInputTokens(activeUsage); - const outputTokens = claudeUsageOutputTokens(activeUsage); - const activeTokens = claudeTotalProcessedTokens(activeUsage) ?? inputTokens + outputTokens; - if (activeTokens <= 0) { - return undefined; - } - - return makeClaudeTokenUsageSnapshot({ - activeTokens, - inputTokens, - outputTokens, - ...(contextWindow !== undefined ? { contextWindow } : {}), - ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}), - }); -} - -function normalizeClaudeContextUsageApiSnapshot( - value: SDKControlGetContextUsageResponse, - totalProcessedTokens?: number, -): ThreadTokenUsageSnapshot | undefined { - return makeClaudeTokenUsageSnapshot({ - activeTokens: value.totalTokens, - contextWindow: value.maxTokens, - ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}), - compactsAutomatically: value.isAutoCompactEnabled, - }); -} - -function compactBoundaryTokenUsageSnapshot( - message: Record, - contextWindow?: number, - totalProcessedTokens?: number, -): ThreadTokenUsageSnapshot | undefined { - const metadata = message.compact_metadata; - if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { - return undefined; - } - - const compactMetadata = metadata as Record; - const postTokens = finiteNonNegativeInteger(compactMetadata.post_tokens); - if (postTokens === undefined || postTokens <= 0) { - return undefined; - } - - const preTokens = finiteNonNegativeInteger(compactMetadata.pre_tokens); - return makeClaudeTokenUsageSnapshot({ - activeTokens: postTokens, - ...(preTokens !== undefined ? { lastUsedTokens: preTokens } : {}), - ...(contextWindow !== undefined ? { contextWindow } : {}), - ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}), - }); -} - -function normalizeClaudeTaskProgressTokenUsage( - value: unknown, - context: ClaudeSessionContext, -): ThreadTokenUsageSnapshot | undefined { - const totalTokens = claudeTotalProcessedTokens(value); - if (totalTokens === undefined || totalTokens <= 0) { - return undefined; - } - - const lastUsedTokens = context.lastKnownTokenUsage?.usedTokens; - const activeTokens = - lastUsedTokens !== undefined ? Math.max(totalTokens, lastUsedTokens) : totalTokens; - if (lastUsedTokens !== undefined && activeTokens === lastUsedTokens) { - return undefined; - } - - const usage = value as Record; - const snapshot = makeClaudeTokenUsageSnapshot({ - activeTokens, - ...(context.lastKnownContextWindow !== undefined - ? { contextWindow: context.lastKnownContextWindow } - : {}), - totalProcessedTokens: Math.max( - totalTokens, - context.lastKnownTotalProcessedTokens ?? totalTokens, - ), - }); - if (!snapshot) { - return undefined; - } - - const toolUses = finiteNonNegativeInteger(usage.tool_uses); - const durationMs = finiteNonNegativeInteger(usage.duration_ms); - return { - ...snapshot, - ...(toolUses !== undefined ? { toolUses } : {}), - ...(durationMs !== undefined ? { durationMs } : {}), - }; -} - -function asCanonicalTurnId(value: TurnId): TurnId { - return value; -} - -function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { - return RuntimeRequestId.make(value); -} - -function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undefined { - if (!resumeCursor || typeof resumeCursor !== "object") { - return undefined; - } - const cursor = resumeCursor as { - threadId?: unknown; - resume?: unknown; - sessionId?: unknown; - resumeSessionAt?: unknown; - turnCount?: unknown; - }; - - const threadIdCandidate = typeof cursor.threadId === "string" ? cursor.threadId : undefined; - const threadId = - threadIdCandidate && !isSyntheticClaudeThreadId(threadIdCandidate) - ? ThreadId.make(threadIdCandidate) - : undefined; - const resumeCandidate = - typeof cursor.resume === "string" - ? cursor.resume - : typeof cursor.sessionId === "string" - ? cursor.sessionId - : undefined; - const resume = resumeCandidate && isUuid(resumeCandidate) ? resumeCandidate : undefined; - const resumeSessionAt = - typeof cursor.resumeSessionAt === "string" ? cursor.resumeSessionAt : undefined; - const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined; - - return { - ...(threadId ? { threadId } : {}), - ...(resume ? { resume } : {}), - ...(resumeSessionAt ? { resumeSessionAt } : {}), - ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0 - ? { turnCount: turnCountValue } - : {}), - }; -} - -function classifyToolItemType(toolName: string): CanonicalItemType { - const normalized = toolName.toLowerCase(); - if (normalized.includes("agent")) { - return "collab_agent_tool_call"; - } - if ( - normalized === "task" || - normalized === "agent" || - normalized.includes("subagent") || - normalized.includes("sub-agent") - ) { - return "collab_agent_tool_call"; - } - if ( - normalized.includes("bash") || - normalized.includes("command") || - normalized.includes("shell") || - normalized.includes("terminal") - ) { - return "command_execution"; - } - if ( - normalized.includes("edit") || - normalized.includes("write") || - normalized.includes("file") || - normalized.includes("patch") || - normalized.includes("replace") || - normalized.includes("create") || - normalized.includes("delete") - ) { - return "file_change"; - } - if (normalized.includes("mcp")) { - return "mcp_tool_call"; - } - if (normalized.includes("websearch") || normalized.includes("web search")) { - return "web_search"; - } - if (normalized.includes("image")) { - return "image_view"; - } - return "dynamic_tool_call"; -} - -function isReadOnlyToolName(toolName: string): boolean { - const normalized = toolName.toLowerCase(); - return ( - normalized === "read" || - normalized.includes("read file") || - normalized.includes("view") || - normalized.includes("grep") || - normalized.includes("glob") || - normalized.includes("search") - ); -} - -function classifyRequestType(toolName: string): CanonicalRequestType { - if (isReadOnlyToolName(toolName)) { - return "file_read_approval"; - } - const itemType = classifyToolItemType(toolName); - return itemType === "command_execution" - ? "command_execution_approval" - : itemType === "file_change" - ? "file_change_approval" - : "dynamic_tool_call"; -} - -function isTodoTool(toolName: string): boolean { - return toolName.toLowerCase().includes("todowrite"); -} - -type PlanStep = { - step: string; - status: "pending" | "inProgress" | "completed"; -}; - -function extractPlanStepsFromTodoInput(input: Record): PlanStep[] | null { - // TodoWrite format: { todos: [{ content, status, activeForm? }] } - const todos = input.todos; - if (!Array.isArray(todos) || todos.length === 0) { - return null; - } - return todos - .filter((t): t is Record => t !== null && typeof t === "object") - .map((todo) => ({ - step: - typeof todo.content === "string" && todo.content.trim().length > 0 - ? todo.content.trim() - : "Task", - status: - todo.status === "completed" - ? "completed" - : todo.status === "in_progress" - ? "inProgress" - : "pending", - })); -} - -function isClaudeTaskTool(toolName: string): boolean { - return toolName === "TaskCreate" || toolName === "TaskUpdate" || toolName === "TaskList"; -} - -function normalizeClaudeTaskStatus(value: unknown): PlanStep["status"] { - return value === "completed" ? "completed" : value === "in_progress" ? "inProgress" : "pending"; -} - -function readString(value: unknown): string | undefined { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; -} - -function readStringArray(value: unknown): Array { - return Array.isArray(value) - ? value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0) - : []; -} - -function readClaudeToolUseResult(message: SDKMessage): Record | undefined { - if (message.type !== "user") { - return undefined; - } - const result = (message as { readonly tool_use_result?: unknown }).tool_use_result; - return result !== null && typeof result === "object" && !Array.isArray(result) - ? (result as Record) - : undefined; -} - -function readClaudeTaskFromResult( - result: Record | undefined, -): Record | undefined { - const task = result?.task; - return task !== null && typeof task === "object" && !Array.isArray(task) - ? (task as Record) - : undefined; -} - -function applyClaudeTaskToolResult( - tasks: Map, - tool: ToolInFlight, - result: Record | undefined, -): boolean { - if (!isClaudeTaskTool(tool.toolName)) { - return false; - } - - let changed = false; - if (tool.toolName === "TaskList") { - const resultTasks = result?.tasks; - if (!Array.isArray(resultTasks)) { - return false; - } - tasks.clear(); - for (const entry of resultTasks) { - if (entry === null || typeof entry !== "object" || Array.isArray(entry)) { - continue; - } - const task = entry as Record; - const id = readString(task.id); - const subject = readString(task.subject); - if (!id || !subject) { - continue; - } - tasks.set(id, { - id, - subject, - status: normalizeClaudeTaskStatus(task.status), - blockedBy: new Set(readStringArray(task.blockedBy)), - }); - } - return tasks.size > 0; - } - - if (tool.toolName === "TaskCreate") { - const resultTask = readClaudeTaskFromResult(result); - const id = readString(resultTask?.id); - const subject = readString(resultTask?.subject) ?? readString(tool.input.subject); - if (!id || !subject) { - return false; - } - tasks.set(id, { - id, - subject, - status: normalizeClaudeTaskStatus(tool.input.status), - blockedBy: new Set(readStringArray(tool.input.blockedBy)), - }); - return true; - } - - const taskId = readString(tool.input.taskId) ?? readString(result?.taskId); - if (!taskId) { - return false; - } - const task = tasks.get(taskId); - if (!task) { - return false; - } - const subject = readString(tool.input.subject); - if (subject && task.subject !== subject) { - task.subject = subject; - changed = true; - } - if (typeof tool.input.status === "string") { - const status = normalizeClaudeTaskStatus(tool.input.status); - if (task.status !== status) { - task.status = status; - changed = true; - } - } - for (const dependency of readStringArray(tool.input.addBlockedBy)) { - if (!task.blockedBy.has(dependency)) { - task.blockedBy.add(dependency); - changed = true; - } - } - for (const dependency of readStringArray(tool.input.removeBlockedBy)) { - if (task.blockedBy.delete(dependency)) { - changed = true; - } - } - return changed; -} - -function planStepsFromClaudeTasks(tasks: Map): PlanStep[] { - return Array.from(tasks.values()).map((task) => { - const blockedBy = Array.from(task.blockedBy); - const blockedSuffix = blockedBy.length > 0 ? ` (blocked by #${blockedBy.join(", #")})` : ""; - return { - step: `${task.subject}${blockedSuffix}`, - status: task.status, - }; - }); -} - -function summarizeToolRequest(toolName: string, input: Record): string { - const commandValue = input.command ?? input.cmd; - const command = typeof commandValue === "string" ? commandValue : undefined; - if (command && command.trim().length > 0) { - return `${toolName}: ${command.trim().slice(0, 400)}`; - } - - // For agent/subagent tools, prefer human-readable description or prompt over raw JSON - const itemType = classifyToolItemType(toolName); - if (itemType === "collab_agent_tool_call") { - const description = - typeof input.description === "string" ? input.description.trim() : undefined; - const prompt = typeof input.prompt === "string" ? input.prompt.trim() : undefined; - const subagentType = - typeof input.subagent_type === "string" ? input.subagent_type.trim() : undefined; - const label = description || (prompt ? prompt.slice(0, 200) : undefined); - if (label) { - return subagentType ? `${subagentType}: ${label}` : label; - } - } - - const serialized = encodeJsonStringForDiagnostics(input) ?? "[unserializable input]"; - if (serialized.length <= 400) { - return `${toolName}: ${serialized}`; - } - return `${toolName}: ${serialized.slice(0, 397)}...`; -} - -function titleForTool(itemType: CanonicalItemType): string { - switch (itemType) { - case "command_execution": - return "Command run"; - case "file_change": - return "File change"; - case "mcp_tool_call": - return "MCP tool call"; - case "collab_agent_tool_call": - return "Subagent task"; - case "web_search": - return "Web search"; - case "image_view": - return "Image view"; - case "dynamic_tool_call": - return "Tool call"; - default: - return "Item"; - } -} - -const SUPPORTED_CLAUDE_IMAGE_MIME_TYPES = new Set([ - "image/gif", - "image/jpeg", - "image/png", - "image/webp", -]); -const CLAUDE_SETTING_SOURCES = [ - "user", - "project", - "local", -] as const satisfies ReadonlyArray; - -function buildPromptText( - input: ProviderSendTurnInput, - boundInstanceId: ProviderInstanceId, -): string { - const rawEffort = - input.modelSelection?.instanceId === boundInstanceId - ? getModelSelectionStringOptionValue(input.modelSelection, "effort") - : null; - const claudeModel = - input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection.model : undefined; - const caps = getClaudeModelCapabilities(claudeModel); - - const promptEffort = resolvePromptInjectedEffort(caps, rawEffort); - return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort); -} - -function buildUserMessage(input: { - readonly sdkContent: Array>; -}): SDKUserMessage { - return { - type: "user", - session_id: "", - parent_tool_use_id: null, - message: { - role: "user", - content: input.sdkContent as unknown as SDKUserMessage["message"]["content"], - }, - } as SDKUserMessage; -} - -function buildClaudeImageContentBlock(input: { - readonly mimeType: string; - readonly bytes: Uint8Array; -}): Record { - return { - type: "image", - source: { - type: "base64", - media_type: input.mimeType, - data: Buffer.from(input.bytes).toString("base64"), - }, - }; -} - -const buildUserMessageEffect = Effect.fn("buildUserMessageEffect")(function* ( - input: ProviderSendTurnInput, - dependencies: { - readonly fileSystem: FileSystem.FileSystem; - readonly attachmentsDir: string; - readonly boundInstanceId: ProviderInstanceId; - }, -) { - const text = buildPromptText(input, dependencies.boundInstanceId); - const sdkContent: Array> = []; - - if (text.length > 0) { - sdkContent.push({ type: "text", text }); - } - - for (const attachment of input.attachments ?? []) { - if (attachment.type !== "image") { - continue; - } - - if (!SUPPORTED_CLAUDE_IMAGE_MIME_TYPES.has(attachment.mimeType)) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "turn/start", - detail: `Unsupported Claude image attachment type '${attachment.mimeType}'.`, - }); - } - - const attachmentPath = resolveAttachmentPath({ - attachmentsDir: dependencies.attachmentsDir, - attachment, - }); - if (!attachmentPath) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "turn/start", - detail: `Invalid attachment id '${attachment.id}'.`, - }); - } - - const bytes = yield* dependencies.fileSystem.readFile(attachmentPath).pipe( - Effect.mapError( - (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "turn/start", - detail: "Failed to read attachment file.", - cause, - }), - ), - ); - - sdkContent.push( - buildClaudeImageContentBlock({ - mimeType: attachment.mimeType, - bytes, - }), - ); - } - - return buildUserMessage({ sdkContent }); -}); - -function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus { - if (result.subtype === "success") { - return "completed"; - } - - const errors = resultErrorsText(result); - if (isInterruptedResult(result)) { - return "interrupted"; - } - if (errors.includes("cancel")) { - return "cancelled"; - } - return "failed"; -} - -function streamKindFromDeltaType(deltaType: string): ClaudeTextStreamKind { - return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text"; -} - -function nativeProviderRefs( - _context: ClaudeSessionContext, - options?: { - readonly providerItemId?: string | undefined; - }, -): NonNullable { - if (options?.providerItemId) { - return { - providerItemId: ProviderItemId.make(options.providerItemId), - }; - } - return {}; -} - -function extractAssistantTextBlocks(message: SDKMessage): Array { - if (message.type !== "assistant") { - return []; - } - - const content = (message.message as { content?: unknown } | undefined)?.content; - if (!Array.isArray(content)) { - return []; - } - - const fragments: string[] = []; - for (const block of content) { - if (!block || typeof block !== "object") { - continue; - } - const candidate = block as { type?: unknown; text?: unknown }; - if ( - candidate.type === "text" && - typeof candidate.text === "string" && - candidate.text.length > 0 - ) { - fragments.push(candidate.text); - } - } - - return fragments; -} - -function extractContentBlockText(block: unknown): string { - if (!block || typeof block !== "object") { - return ""; - } - - const candidate = block as { type?: unknown; text?: unknown }; - return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : ""; -} - -function extractTextContent(value: unknown): string { - if (typeof value === "string") { - return value; - } - - if (Array.isArray(value)) { - return value.map((entry) => extractTextContent(entry)).join(""); - } - - if (!value || typeof value !== "object") { - return ""; - } - - const record = value as { - text?: unknown; - content?: unknown; - }; - - if (typeof record.text === "string") { - return record.text; - } - - return extractTextContent(record.content); -} - -function extractExitPlanModePlan(value: unknown): string | undefined { - if (!value || typeof value !== "object") { - return undefined; - } - - const record = value as { - plan?: unknown; - }; - return typeof record.plan === "string" && record.plan.trim().length > 0 - ? record.plan.trim() - : undefined; -} - -function exitPlanCaptureKey(input: { - readonly toolUseId?: string | undefined; - readonly planMarkdown: string; -}): string { - return input.toolUseId && input.toolUseId.length > 0 - ? `tool:${input.toolUseId}` - : `plan:${input.planMarkdown}`; -} - -function tryParseJsonRecord(value: string): Record | undefined { - const result = decodeUnknownJsonStringExit(value); - if (!Exit.isSuccess(result)) { - return undefined; - } - const parsed = result.value; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as Record) - : undefined; -} - -function toolInputFingerprint(input: Record): string | undefined { - return encodeJsonStringForDiagnostics(input); -} - -function toolResultStreamKind(itemType: CanonicalItemType): ClaudeToolResultStreamKind | undefined { - switch (itemType) { - case "command_execution": - return "command_output"; - case "file_change": - return "file_change_output"; - default: - return undefined; - } -} - -function toolResultBlocksFromUserMessage(message: SDKMessage): Array<{ - readonly toolUseId: string; - readonly block: Record; - readonly text: string; - readonly isError: boolean; -}> { - if (message.type !== "user") { - return []; - } - - const content = (message.message as { content?: unknown } | undefined)?.content; - if (!Array.isArray(content)) { - return []; - } - - const blocks: Array<{ - readonly toolUseId: string; - readonly block: Record; - readonly text: string; - readonly isError: boolean; - }> = []; - - for (const entry of content) { - if (!entry || typeof entry !== "object") { - continue; - } - - const block = entry as Record; - if (block.type !== "tool_result") { - continue; - } - - const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined; - if (!toolUseId) { - continue; - } - - blocks.push({ - toolUseId, - block, - text: extractTextContent(block.content), - isError: block.is_error === true, - }); - } - - return blocks; -} - -function toSessionError( - threadId: ThreadId, - cause: unknown, -): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { - const normalized = toMessage(cause, "").toLowerCase(); - if (normalized.includes("unknown session") || normalized.includes("not found")) { - return new ProviderAdapterSessionNotFoundError({ - provider: PROVIDER, - threadId, - cause, - }); - } - if (normalized.includes("closed")) { - return new ProviderAdapterSessionClosedError({ - provider: PROVIDER, - threadId, - cause, - }); - } - return undefined; -} - -function toRequestError(threadId: ThreadId, method: string, cause: unknown): ProviderAdapterError { - const sessionError = toSessionError(threadId, cause); - if (sessionError) { - return sessionError; - } - return new ProviderAdapterRequestError({ - provider: PROVIDER, - method, - detail: `${method} failed`, - cause, - }); -} - -function sdkMessageType(value: unknown): string | undefined { - if (!value || typeof value !== "object") { - return undefined; - } - const record = value as { type?: unknown }; - return typeof record.type === "string" ? record.type : undefined; -} - -function sdkMessageSubtype(value: unknown): string | undefined { - if (!value || typeof value !== "object") { - return undefined; - } - const record = value as { subtype?: unknown }; - return typeof record.subtype === "string" ? record.subtype : undefined; -} - -function sdkNativeMethod(message: SDKMessage): string { - const subtype = sdkMessageSubtype(message); - if (subtype) { - return `claude/${message.type}/${subtype}`; - } - - if (message.type === "stream_event") { - const streamType = sdkMessageType(message.event); - if (streamType) { - const deltaType = - streamType === "content_block_delta" - ? sdkMessageType((message.event as { delta?: unknown }).delta) - : undefined; - if (deltaType) { - return `claude/${message.type}/${streamType}/${deltaType}`; - } - return `claude/${message.type}/${streamType}`; - } - } - - return `claude/${message.type}`; -} - -// Discriminator/identity keys carry no human-readable content; everything else -// on an unmodeled SDK message is potentially worth surfacing in the work log. -const SDK_MESSAGE_NOISE_KEYS = new Set([ - "type", - "subtype", - "uuid", - "parent_uuid", - "session_id", - "parent_tool_use_id", - "request_id", -]); - -// Pull the salient scalar content out of a message the adapter doesn't model -// yet, so the work-log row shows what actually arrived (e.g. a notification's -// text) instead of an opaque "unhandled subtype" placeholder. Nested structures -// are left to the full payload retained in the event's `detail`. -function previewUnknownSdkContent(message: unknown): string | undefined { - if (!message || typeof message !== "object") { - return undefined; - } - const parts: string[] = []; - for (const [key, value] of Object.entries(message as Record)) { - if (SDK_MESSAGE_NOISE_KEYS.has(key)) { - continue; - } - if (typeof value === "string") { - const trimmed = value.trim(); - if (trimmed.length > 0) { - parts.push(`${key}: ${trimmed}`); - } - } else if (typeof value === "number" || typeof value === "boolean") { - parts.push(`${key}: ${String(value)}`); - } - } - if (parts.length === 0) { - return undefined; - } - const joined = parts.join(" · "); - return joined.length > 280 ? `${joined.slice(0, 279)}…` : joined; -} - -function describeUnknownSdkMessage(kind: string, message: unknown): string { - const preview = previewUnknownSdkContent(message); - return preview ? `${kind} — ${preview}` : `${kind} (no displayable text content)`; -} - -function sdkNativeItemId(message: SDKMessage): string | undefined { - if (message.type === "assistant") { - const maybeId = (message.message as { id?: unknown }).id; - if (typeof maybeId === "string") { - return maybeId; - } - return undefined; - } - - if (message.type === "user") { - return toolResultBlocksFromUserMessage(message)[0]?.toolUseId; - } - - if (message.type === "stream_event") { - const event = message.event as { - type?: unknown; - content_block?: { id?: unknown }; - }; - if (event.type === "content_block_start" && typeof event.content_block?.id === "string") { - return event.content_block.id; - } - } - - return undefined; -} - -export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( - claudeSettings: ClaudeSettings, - options?: ClaudeAdapterLiveOptions, -) { - const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("claudeAgent"); - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const serverConfig = yield* ServerConfig; - const crypto = yield* Crypto.Crypto; - const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, options?.environment).pipe( - Effect.provideService(Path.Path, path), - ); - const nativeEventLogger = - options?.nativeEventLogger ?? - (options?.nativeEventLogPath !== undefined - ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { - stream: "native", - }) - : undefined); - - const createQuery = - options?.createQuery ?? - ((input: { - readonly prompt: AsyncIterable; - readonly options: ClaudeQueryOptions; - }) => - query({ - prompt: input.prompt, - options: input.options, - }) as ClaudeQueryRuntime); - - const sessions = new Map(); - const runtimeEventQueue = yield* Queue.unbounded(); - - const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - const randomUUIDv4 = crypto.randomUUIDv4.pipe( - Effect.mapError( - (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "crypto/randomUUIDv4", - detail: "Failed to generate Claude runtime identifier.", - cause, - }), - ), - ); - const nextEventId = Effect.map(randomUUIDv4, (id) => EventId.make(id)); - const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); - - const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect => - Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid); - - const logNativeSdkMessage = Effect.fn("logNativeSdkMessage")(function* ( - context: ClaudeSessionContext, - message: SDKMessage, - ) { - if (!nativeEventLogger) { - return; - } - - const observedAt = yield* nowIso; - const itemId = sdkNativeItemId(message); - - yield* nativeEventLogger.write( - { - observedAt, - event: { - id: - "uuid" in message && typeof message.uuid === "string" - ? message.uuid - : yield* randomUUIDv4, - kind: "notification", - provider: PROVIDER, - createdAt: observedAt, - method: sdkNativeMethod(message), - ...(typeof message.session_id === "string" - ? { providerThreadId: message.session_id } - : {}), - ...(context.turnState - ? { - turnId: asCanonicalTurnId(context.turnState.turnId), - } - : {}), - ...(itemId ? { itemId: ProviderItemId.make(itemId) } : {}), - payload: message, - }, - }, - context.session.threadId, - ); - }); - - const snapshotThread = Effect.fn("snapshotThread")(function* (context: ClaudeSessionContext) { - const threadId = context.session.threadId; - if (!threadId) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "readThread", - issue: "Session thread id is not initialized yet.", - }); - } - return { - threadId, - turns: context.turns.map((turn) => ({ - id: turn.id, - items: [...turn.items], - })), - }; - }); - - const updateResumeCursor = Effect.fn("updateResumeCursor")(function* ( - context: ClaudeSessionContext, - ) { - const threadId = context.session.threadId; - if (!threadId) return; - - const resumeCursor = { - threadId, - ...(context.resumeSessionId ? { resume: context.resumeSessionId } : {}), - ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}), - turnCount: context.turns.length, - }; - - context.session = { - ...context.session, - resumeCursor, - updatedAt: yield* nowIso, - }; - }); - - const ensureAssistantTextBlock = Effect.fn("ensureAssistantTextBlock")(function* ( - context: ClaudeSessionContext, - blockIndex: number, - options?: { - readonly fallbackText?: string; - readonly streamClosed?: boolean; - }, - ) { - const turnState = context.turnState; - if (!turnState) { - return undefined; - } - - const existing = turnState.assistantTextBlocks.get(blockIndex); - if (existing && !existing.completionEmitted) { - if (existing.fallbackText.length === 0 && options?.fallbackText) { - existing.fallbackText = options.fallbackText; - } - if (options?.streamClosed) { - existing.streamClosed = true; - } - return { blockIndex, block: existing }; - } - - const block: AssistantTextBlockState = { - itemId: yield* randomUUIDv4, - blockIndex, - emittedTextDelta: false, - fallbackText: options?.fallbackText ?? "", - streamClosed: options?.streamClosed ?? false, - completionEmitted: false, - }; - turnState.assistantTextBlocks.set(blockIndex, block); - turnState.assistantTextBlockOrder.push(block); - return { blockIndex, block }; - }); - - const createSyntheticAssistantTextBlock = Effect.fn("createSyntheticAssistantTextBlock")( - function* (context: ClaudeSessionContext, fallbackText: string) { - const turnState = context.turnState; - if (!turnState) { - return undefined; - } - - const blockIndex = turnState.nextSyntheticAssistantBlockIndex; - turnState.nextSyntheticAssistantBlockIndex -= 1; - return yield* ensureAssistantTextBlock(context, blockIndex, { - fallbackText, - streamClosed: true, - }); - }, - ); - - const completeAssistantTextBlock = Effect.fn("completeAssistantTextBlock")(function* ( - context: ClaudeSessionContext, - block: AssistantTextBlockState, - options?: { - readonly force?: boolean; - readonly rawMethod?: string; - readonly rawPayload?: unknown; - }, - ) { - const turnState = context.turnState; - if (!turnState || block.completionEmitted) { - return; - } - - if (!options?.force && !block.streamClosed) { - return; - } - - if (!block.emittedTextDelta && block.fallbackText.length > 0) { - const deltaStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "content.delta", - eventId: deltaStamp.eventId, - provider: PROVIDER, - createdAt: deltaStamp.createdAt, - threadId: context.session.threadId, - turnId: turnState.turnId, - itemId: asRuntimeItemId(block.itemId), - payload: { - streamKind: "assistant_text", - delta: block.fallbackText, - }, - providerRefs: nativeProviderRefs(context), - ...(options?.rawMethod || options?.rawPayload - ? { - raw: { - source: "claude.sdk.message" as const, - ...(options.rawMethod ? { method: options.rawMethod } : {}), - payload: options?.rawPayload, - }, - } - : {}), - }); - } - - block.completionEmitted = true; - if (turnState.assistantTextBlocks.get(block.blockIndex) === block) { - turnState.assistantTextBlocks.delete(block.blockIndex); - } - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.completed", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - itemId: asRuntimeItemId(block.itemId), - threadId: context.session.threadId, - turnId: turnState.turnId, - payload: { - itemType: "assistant_message", - status: "completed", - title: "Assistant message", - ...(block.fallbackText.length > 0 ? { detail: block.fallbackText } : {}), - }, - providerRefs: nativeProviderRefs(context), - ...(options?.rawMethod || options?.rawPayload - ? { - raw: { - source: "claude.sdk.message" as const, - ...(options.rawMethod ? { method: options.rawMethod } : {}), - payload: options?.rawPayload, - }, - } - : {}), - }); - }); - - const backfillAssistantTextBlocksFromSnapshot = Effect.fn( - "backfillAssistantTextBlocksFromSnapshot", - )(function* (context: ClaudeSessionContext, message: SDKMessage) { - const turnState = context.turnState; - if (!turnState) { - return; - } - - const snapshotTextBlocks = extractAssistantTextBlocks(message); - if (snapshotTextBlocks.length === 0) { - return; - } - - const orderedBlocks = turnState.assistantTextBlockOrder.map((block) => ({ - blockIndex: block.blockIndex, - block, - })); - - for (const [position, text] of snapshotTextBlocks.entries()) { - const existingEntry = orderedBlocks[position]; - const entry = - existingEntry ?? - (yield* createSyntheticAssistantTextBlock(context, text).pipe( - Effect.map((created) => { - if (!created) { - return undefined; - } - orderedBlocks.push(created); - return created; - }), - )); - if (!entry) { - continue; - } - - if (entry.block.fallbackText.length === 0) { - entry.block.fallbackText = text; - } - - if (entry.block.streamClosed && !entry.block.completionEmitted) { - yield* completeAssistantTextBlock(context, entry.block, { - rawMethod: "claude/assistant", - rawPayload: message, - }); - } - } - }); - - const ensureThreadId = Effect.fn("ensureThreadId")(function* ( - context: ClaudeSessionContext, - message: SDKMessage, - ) { - if (typeof message.session_id !== "string" || message.session_id.length === 0) { - return; - } - if (!hasDurableClaudeSessionId(message)) { - return; - } - const nextThreadId = message.session_id; - context.resumeSessionId = message.session_id; - yield* updateResumeCursor(context); - - if (context.lastThreadStartedId !== nextThreadId) { - context.lastThreadStartedId = nextThreadId; - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "thread.started", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - payload: { - providerThreadId: nextThreadId, - }, - providerRefs: {}, - raw: { - source: "claude.sdk.message", - method: "claude/thread/started", - payload: { - session_id: message.session_id, - }, - }, - }); - } - }); - - const emitRuntimeError = Effect.fn("emitRuntimeError")(function* ( - context: ClaudeSessionContext, - message: string, - cause?: unknown, - ) { - if (cause !== undefined) { - void cause; - } - const turnState = context.turnState; - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "runtime.error", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), - payload: { - message, - class: "provider_error", - ...(cause !== undefined ? { detail: cause } : {}), - }, - providerRefs: nativeProviderRefs(context), - }); - }); - - const emitRuntimeWarning = Effect.fn("emitRuntimeWarning")(function* ( - context: ClaudeSessionContext, - message: string, - detail?: unknown, - ) { - const turnState = context.turnState; - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "runtime.warning", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), - payload: { - message, - ...(detail !== undefined ? { detail } : {}), - }, - providerRefs: nativeProviderRefs(context), - }); - }); - - const emitThreadTokenUsage = Effect.fn("emitThreadTokenUsage")(function* ( - context: ClaudeSessionContext, - usage: ThreadTokenUsageSnapshot | undefined, - options?: { - readonly rawMethod?: string; - readonly rawPayload?: unknown; - }, - ) { - if (!usage) { - return; - } - - context.lastKnownTokenUsage = usage; - context.lastKnownTotalProcessedTokens = - usage.totalProcessedTokens ?? context.lastKnownTotalProcessedTokens; - - const turnState = context.turnState; - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "thread.token-usage.updated", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(turnState ? { turnId: turnState.turnId } : {}), - payload: { - usage, - }, - providerRefs: nativeProviderRefs(context), - ...(options?.rawMethod || options?.rawPayload - ? { - raw: { - source: "claude.sdk.message" as const, - ...(options.rawMethod ? { method: options.rawMethod } : {}), - payload: options.rawPayload, - }, - } - : {}), - }); - }); - - const queryCurrentContextUsage = Effect.fn("queryCurrentContextUsage")(function* ( - context: ClaudeSessionContext, - totalProcessedTokens?: number, - ) { - if (!context.query.getContextUsage) { - return undefined; - } - - const usage = yield* Effect.promise(async () => { - try { - return await context.query.getContextUsage?.(); - } catch { - return undefined; - } - }); - if (!usage) { - return undefined; - } - - context.lastKnownContextWindow = usage.maxTokens; - return normalizeClaudeContextUsageApiSnapshot(usage, totalProcessedTokens); - }); - - const emitProposedPlanCompleted = Effect.fn("emitProposedPlanCompleted")(function* ( - context: ClaudeSessionContext, - input: { - readonly planMarkdown: string; - readonly toolUseId?: string | undefined; - readonly rawSource: "claude.sdk.message" | "claude.sdk.permission"; - readonly rawMethod: string; - readonly rawPayload: unknown; - }, - ) { - const turnState = context.turnState; - const planMarkdown = input.planMarkdown.trim(); - if (!turnState || planMarkdown.length === 0) { - return; - } - - const captureKey = exitPlanCaptureKey({ - toolUseId: input.toolUseId, - planMarkdown, - }); - if (turnState.capturedProposedPlanKeys.has(captureKey)) { - return; - } - turnState.capturedProposedPlanKeys.add(captureKey); - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.proposed.completed", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - turnId: turnState.turnId, - payload: { - planMarkdown, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: input.toolUseId, - }), - raw: { - source: input.rawSource, - method: input.rawMethod, - payload: input.rawPayload, - }, - }); - }); - - const emitClaudeTaskPlanUpdated = Effect.fn("emitClaudeTaskPlanUpdated")(function* ( - context: ClaudeSessionContext, - input: { - readonly toolUseId: string; - readonly rawMethod: string; - readonly rawPayload: unknown; - }, - ) { - const plan = planStepsFromClaudeTasks(context.claudeTasks); - if (plan.length === 0) { - return; - } - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.plan.updated", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - payload: { - explanation: "Claude Tasks", - plan, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: input.toolUseId, - }), - raw: { - source: "claude.sdk.message", - method: input.rawMethod, - payload: input.rawPayload, - }, - }); - }); - - const completeTurn = Effect.fn("completeTurn")(function* ( - context: ClaudeSessionContext, - status: ProviderRuntimeTurnStatus, - errorMessage?: string, - result?: SDKResultMessage, - ) { - const resultContextWindow = maxClaudeContextWindowFromModelUsage(result?.modelUsage); - if (resultContextWindow !== undefined) { - context.lastKnownContextWindow = resultContextWindow; - } - - const maxTokens = resultContextWindow ?? context.lastKnownContextWindow; - const accumulatedTotalProcessedTokens = claudeTotalProcessedTokens(result?.usage); - if (accumulatedTotalProcessedTokens !== undefined) { - context.lastKnownTotalProcessedTokens = accumulatedTotalProcessedTokens; - } - - const contextUsageSnapshot = yield* queryCurrentContextUsage( - context, - accumulatedTotalProcessedTokens ?? context.lastKnownTotalProcessedTokens, - ); - const resultUsageRecord = - result?.usage && typeof result.usage === "object" && !Array.isArray(result.usage) - ? (result.usage as Record) - : undefined; - const hasResultUsageIteration = - resultUsageRecord !== undefined && lastClaudeUsageIteration(resultUsageRecord) !== undefined; - const resultHasActiveUsage = - resultUsageRecord !== undefined && - (hasResultUsageIteration || - claudeUsageInputTokens(resultUsageRecord) + claudeUsageOutputTokens(resultUsageRecord) > 0); - const resultTotalOnly = - resultUsageRecord !== undefined && - !resultHasActiveUsage && - claudeTotalProcessedTokens(resultUsageRecord) !== undefined; - const resultIterationSnapshot = resultUsageRecord - ? normalizeClaudeActiveTokenUsage( - resultUsageRecord, - maxTokens, - accumulatedTotalProcessedTokens ?? context.lastKnownTotalProcessedTokens, - ) - : undefined; - const lastGoodUsage = context.lastKnownTokenUsage; - const usageSnapshot: ThreadTokenUsageSnapshot | undefined = - contextUsageSnapshot ?? - (resultTotalOnly && lastGoodUsage - ? { - ...lastGoodUsage, - ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 - ? { maxTokens } - : {}), - ...(typeof accumulatedTotalProcessedTokens === "number" && - Number.isFinite(accumulatedTotalProcessedTokens) && - accumulatedTotalProcessedTokens > lastGoodUsage.usedTokens - ? { - totalProcessedTokens: accumulatedTotalProcessedTokens, - } - : {}), - } - : resultIterationSnapshot) ?? - (lastGoodUsage - ? { - ...lastGoodUsage, - ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 - ? { maxTokens } - : {}), - ...(typeof accumulatedTotalProcessedTokens === "number" && - Number.isFinite(accumulatedTotalProcessedTokens) && - accumulatedTotalProcessedTokens > lastGoodUsage.usedTokens - ? { - totalProcessedTokens: accumulatedTotalProcessedTokens, - } - : {}), - } - : undefined); - - const turnState = context.turnState; - if (!turnState) { - yield* emitThreadTokenUsage(context, usageSnapshot, { - rawMethod: "claude/result", - rawPayload: result ?? { status }, - }); - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.completed", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - payload: { - state: status, - ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), - ...(result?.usage ? { usage: result.usage } : {}), - ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), - ...(typeof result?.total_cost_usd === "number" - ? { totalCostUsd: result.total_cost_usd } - : {}), - ...(errorMessage ? { errorMessage } : {}), - }, - providerRefs: {}, - }); - return; - } - - for (const [index, tool] of context.inFlightTools.entries()) { - const toolStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.completed", - eventId: toolStamp.eventId, - provider: PROVIDER, - createdAt: toolStamp.createdAt, - threadId: context.session.threadId, - turnId: turnState.turnId, - itemId: asRuntimeItemId(tool.itemId), - payload: { - itemType: tool.itemType, - status: status === "completed" ? "completed" : "failed", - title: tool.title, - ...(tool.detail ? { detail: tool.detail } : {}), - data: { - toolName: tool.toolName, - input: tool.input, - }, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: tool.itemId, - }), - raw: { - source: "claude.sdk.message", - method: "claude/result", - payload: result ?? { status }, - }, - }); - context.inFlightTools.delete(index); - } - // Clear any remaining stale entries (e.g. from interrupted content blocks) - context.inFlightTools.clear(); - - for (const block of turnState.assistantTextBlockOrder) { - yield* completeAssistantTextBlock(context, block, { - force: true, - rawMethod: "claude/result", - rawPayload: result ?? { status }, - }); - } - - context.turns.push({ - id: turnState.turnId, - items: [...turnState.items], - }); - - yield* emitThreadTokenUsage(context, usageSnapshot, { - rawMethod: "claude/result", - rawPayload: result ?? { status }, - }); - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.completed", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - turnId: turnState.turnId, - payload: { - state: status, - ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), - ...(result?.usage ? { usage: result.usage } : {}), - ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), - ...(typeof result?.total_cost_usd === "number" - ? { totalCostUsd: result.total_cost_usd } - : {}), - ...(errorMessage ? { errorMessage } : {}), - }, - providerRefs: nativeProviderRefs(context), - }); - - const updatedAt = yield* nowIso; - context.turnState = undefined; - context.session = { - ...context.session, - status: "ready", - activeTurnId: undefined, - updatedAt, - ...(status === "failed" && errorMessage ? { lastError: errorMessage } : {}), - }; - yield* updateResumeCursor(context); - }); - - const handleStreamEvent = Effect.fn("handleStreamEvent")(function* ( - context: ClaudeSessionContext, - message: SDKMessage, - ) { - if (message.type !== "stream_event") { - return; - } - - const { event } = message; - - if (event.type === "message_delta") { - if (message.parent_tool_use_id !== null && message.parent_tool_use_id !== undefined) { - return; - } - - const snapshot = normalizeClaudeActiveTokenUsage( - event.usage, - context.lastKnownContextWindow, - context.lastKnownTotalProcessedTokens, - ); - yield* emitThreadTokenUsage(context, snapshot, { - rawMethod: "claude/stream_event/message_delta", - rawPayload: message, - }); - return; - } - - if (event.type === "content_block_delta") { - if ( - (event.delta.type === "text_delta" || event.delta.type === "thinking_delta") && - context.turnState - ) { - const deltaText = - event.delta.type === "text_delta" - ? event.delta.text - : typeof event.delta.thinking === "string" - ? event.delta.thinking - : ""; - if (deltaText.length === 0) { - return; - } - const streamKind = streamKindFromDeltaType(event.delta.type); - const assistantBlockEntry = - event.delta.type === "text_delta" - ? yield* ensureAssistantTextBlock(context, event.index) - : context.turnState.assistantTextBlocks.get(event.index) - ? { - blockIndex: event.index, - block: context.turnState.assistantTextBlocks.get( - event.index, - ) as AssistantTextBlockState, - } - : undefined; - if (assistantBlockEntry?.block && event.delta.type === "text_delta") { - assistantBlockEntry.block.emittedTextDelta = true; - } - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "content.delta", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - turnId: context.turnState.turnId, - ...(assistantBlockEntry?.block - ? { - itemId: asRuntimeItemId(assistantBlockEntry.block.itemId), - } - : {}), - payload: { - streamKind, - delta: deltaText, - }, - providerRefs: nativeProviderRefs(context), - raw: { - source: "claude.sdk.message", - method: "claude/stream_event/content_block_delta", - payload: message, - }, - }); - return; - } - - if (event.delta.type === "input_json_delta") { - const tool = context.inFlightTools.get(event.index); - if (!tool || typeof event.delta.partial_json !== "string") { - return; - } - - const partialInputJson = tool.partialInputJson + event.delta.partial_json; - const parsedInput = tryParseJsonRecord(partialInputJson); - const detail = parsedInput ? summarizeToolRequest(tool.toolName, parsedInput) : tool.detail; - let nextTool: ToolInFlight = { - ...tool, - partialInputJson, - ...(parsedInput ? { input: parsedInput } : {}), - ...(detail ? { detail } : {}), - }; - - const nextFingerprint = - parsedInput && Object.keys(parsedInput).length > 0 - ? toolInputFingerprint(parsedInput) - : undefined; - context.inFlightTools.set(event.index, nextTool); - - if ( - !parsedInput || - !nextFingerprint || - tool.lastEmittedInputFingerprint === nextFingerprint - ) { - return; - } - - nextTool = { - ...nextTool, - lastEmittedInputFingerprint: nextFingerprint, - }; - context.inFlightTools.set(event.index, nextTool); - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.updated", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState - ? { - turnId: asCanonicalTurnId(context.turnState.turnId), - } - : {}), - itemId: asRuntimeItemId(nextTool.itemId), - payload: { - itemType: nextTool.itemType, - status: "inProgress", - title: nextTool.title, - ...(nextTool.detail ? { detail: nextTool.detail } : {}), - data: { - toolName: nextTool.toolName, - input: nextTool.input, - }, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: nextTool.itemId, - }), - raw: { - source: "claude.sdk.message", - method: "claude/stream_event/content_block_delta/input_json_delta", - payload: message, - }, - }); - - // Emit plan update when TodoWrite input is parsed - if (parsedInput && isTodoTool(nextTool.toolName)) { - const planSteps = extractPlanStepsFromTodoInput(parsedInput); - if (planSteps && planSteps.length > 0) { - const planStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.plan.updated", - eventId: planStamp.eventId, - provider: PROVIDER, - createdAt: planStamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState - ? { - turnId: asCanonicalTurnId(context.turnState.turnId), - } - : {}), - payload: { - plan: planSteps, - }, - providerRefs: nativeProviderRefs(context), - }); - } - } - } - return; - } - - if (event.type === "content_block_start") { - const { index, content_block: block } = event; - if (block.type === "text") { - yield* ensureAssistantTextBlock(context, index, { - fallbackText: extractContentBlockText(block), - }); - return; - } - if ( - block.type !== "tool_use" && - block.type !== "server_tool_use" && - block.type !== "mcp_tool_use" - ) { - return; - } - - const toolName = block.name; - const itemType = classifyToolItemType(toolName); - const toolInput = - typeof block.input === "object" && block.input !== null - ? (block.input as Record) - : {}; - const itemId = block.id; - const detail = summarizeToolRequest(toolName, toolInput); - const inputFingerprint = - Object.keys(toolInput).length > 0 ? toolInputFingerprint(toolInput) : undefined; - - const tool: ToolInFlight = { - itemId, - itemType, - toolName, - title: titleForTool(itemType), - detail, - input: toolInput, - partialInputJson: "", - ...(inputFingerprint ? { lastEmittedInputFingerprint: inputFingerprint } : {}), - }; - context.inFlightTools.set(index, tool); - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.started", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - itemId: asRuntimeItemId(tool.itemId), - payload: { - itemType: tool.itemType, - status: "inProgress", - title: tool.title, - ...(tool.detail ? { detail: tool.detail } : {}), - data: { - toolName: tool.toolName, - input: toolInput, - }, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: tool.itemId, - }), - raw: { - source: "claude.sdk.message", - method: "claude/stream_event/content_block_start", - payload: message, - }, - }); - return; - } - - if (event.type === "content_block_stop") { - const { index } = event; - const assistantBlock = context.turnState?.assistantTextBlocks.get(index); - if (assistantBlock) { - assistantBlock.streamClosed = true; - yield* completeAssistantTextBlock(context, assistantBlock, { - rawMethod: "claude/stream_event/content_block_stop", - rawPayload: message, - }); - return; - } - const tool = context.inFlightTools.get(index); - if (!tool) { - return; - } - } - }); - - const handleUserMessage = Effect.fn("handleUserMessage")(function* ( - context: ClaudeSessionContext, - message: SDKMessage, - ) { - if (message.type !== "user") { - return; - } - - if (context.turnState) { - context.turnState.items.push(message.message); - } - - for (const toolResult of toolResultBlocksFromUserMessage(message)) { - const toolEntry = Array.from(context.inFlightTools.entries()).find( - ([, tool]) => tool.itemId === toolResult.toolUseId, - ); - if (!toolEntry) { - continue; - } - - const [index, tool] = toolEntry; - const itemStatus = toolResult.isError ? "failed" : "completed"; - const toolUseResult = readClaudeToolUseResult(message); - const toolData = { - toolName: tool.toolName, - input: tool.input, - result: toolResult.block, - }; - - const updatedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.updated", - eventId: updatedStamp.eventId, - provider: PROVIDER, - createdAt: updatedStamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - itemId: asRuntimeItemId(tool.itemId), - payload: { - itemType: tool.itemType, - status: toolResult.isError ? "failed" : "inProgress", - title: tool.title, - ...(tool.detail ? { detail: tool.detail } : {}), - data: toolData, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: tool.itemId, - }), - raw: { - source: "claude.sdk.message", - method: "claude/user", - payload: message, - }, - }); - - const streamKind = toolResultStreamKind(tool.itemType); - if (streamKind && toolResult.text.length > 0 && context.turnState) { - const deltaStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "content.delta", - eventId: deltaStamp.eventId, - provider: PROVIDER, - createdAt: deltaStamp.createdAt, - threadId: context.session.threadId, - turnId: context.turnState.turnId, - itemId: asRuntimeItemId(tool.itemId), - payload: { - streamKind, - delta: toolResult.text, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: tool.itemId, - }), - raw: { - source: "claude.sdk.message", - method: "claude/user", - payload: message, - }, - }); - } - - const completedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.completed", - eventId: completedStamp.eventId, - provider: PROVIDER, - createdAt: completedStamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - itemId: asRuntimeItemId(tool.itemId), - payload: { - itemType: tool.itemType, - status: itemStatus, - title: tool.title, - ...(tool.detail ? { detail: tool.detail } : {}), - data: toolData, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: tool.itemId, - }), - raw: { - source: "claude.sdk.message", - method: "claude/user", - payload: message, - }, - }); - - if ( - !toolResult.isError && - applyClaudeTaskToolResult(context.claudeTasks, tool, toolUseResult) - ) { - yield* emitClaudeTaskPlanUpdated(context, { - toolUseId: tool.itemId, - rawMethod: "claude/user", - rawPayload: message, - }); - } - - context.inFlightTools.delete(index); - } - }); - - const handleAssistantMessage = Effect.fn("handleAssistantMessage")(function* ( - context: ClaudeSessionContext, - message: SDKMessage, - ) { - if (message.type !== "assistant") { - return; - } - - // Auto-start a synthetic turn for assistant messages that arrive without - // an active turn (e.g., background agent/subagent responses between user prompts). - if (!context.turnState) { - const turnId = TurnId.make(yield* randomUUIDv4); - const startedAt = yield* nowIso; - context.turnState = { - turnId, - startedAt, - synthetic: true, - items: [], - assistantTextBlocks: new Map(), - assistantTextBlockOrder: [], - capturedProposedPlanKeys: new Set(), - nextSyntheticAssistantBlockIndex: -1, - }; - context.session = { - ...context.session, - status: "running", - activeTurnId: turnId, - updatedAt: startedAt, - }; - const turnStartedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.started", - eventId: turnStartedStamp.eventId, - provider: PROVIDER, - createdAt: turnStartedStamp.createdAt, - threadId: context.session.threadId, - turnId, - payload: {}, - providerRefs: { - ...nativeProviderRefs(context), - providerTurnId: turnId, - }, - raw: { - source: "claude.sdk.message", - method: "claude/synthetic-turn-start", - payload: {}, - }, - }); - } - - const content = message.message?.content; - if (Array.isArray(content)) { - for (const block of content) { - if (!block || typeof block !== "object") { - continue; - } - const toolUse = block as { - type?: unknown; - id?: unknown; - name?: unknown; - input?: unknown; - }; - if (toolUse.type !== "tool_use" || toolUse.name !== "ExitPlanMode") { - continue; - } - const planMarkdown = extractExitPlanModePlan(toolUse.input); - if (!planMarkdown) { - continue; - } - yield* emitProposedPlanCompleted(context, { - planMarkdown, - toolUseId: typeof toolUse.id === "string" ? toolUse.id : undefined, - rawSource: "claude.sdk.message", - rawMethod: "claude/assistant", - rawPayload: message, - }); - } - } - - if (context.turnState) { - context.turnState.items.push(message.message); - yield* backfillAssistantTextBlocksFromSnapshot(context, message); - } - - context.lastAssistantUuid = message.uuid; - yield* updateResumeCursor(context); - }); - - const handleResultMessage = Effect.fn("handleResultMessage")(function* ( - context: ClaudeSessionContext, - message: SDKMessage, - ) { - if (message.type !== "result") { - return; - } - - const status = turnStatusFromResult(message); - const errorMessage = message.subtype === "success" ? undefined : message.errors[0]; - - if (status === "failed") { - yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed."); - } - - yield* completeTurn(context, status, errorMessage, message); - }); - - const handleSystemMessage = Effect.fn("handleSystemMessage")(function* ( - context: ClaudeSessionContext, - message: SDKMessage, - ) { - if (message.type !== "system") { - return; - } - - const stamp = yield* makeEventStamp(); - const base = { - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - providerRefs: nativeProviderRefs(context), - raw: { - source: "claude.sdk.message" as const, - method: sdkNativeMethod(message), - messageType: `${message.type}:${message.subtype}`, - payload: message, - }, - }; - - switch (message.subtype) { - case "init": - yield* offerRuntimeEvent({ - ...base, - type: "session.configured", - payload: { - config: message as Record, - }, - }); - return; - case "status": - yield* offerRuntimeEvent({ - ...base, - type: "session.state.changed", - payload: { - state: message.status === "compacting" ? "waiting" : "running", - reason: `status:${message.status ?? "active"}`, - detail: message, - }, - }); - return; - case "compact_boundary": - yield* emitThreadTokenUsage( - context, - compactBoundaryTokenUsageSnapshot( - message as unknown as Record, - context.lastKnownContextWindow, - context.lastKnownTotalProcessedTokens, - ), - { - rawMethod: "claude/system/compact_boundary", - rawPayload: message, - }, - ); - yield* offerRuntimeEvent({ - ...base, - type: "thread.state.changed", - payload: { - state: "compacted", - detail: message, - }, - }); - return; - case "hook_started": - yield* offerRuntimeEvent({ - ...base, - type: "hook.started", - payload: { - hookId: message.hook_id, - hookName: message.hook_name, - hookEvent: message.hook_event, - }, - }); - return; - case "hook_progress": - yield* offerRuntimeEvent({ - ...base, - type: "hook.progress", - payload: { - hookId: message.hook_id, - output: message.output, - stdout: message.stdout, - stderr: message.stderr, - }, - }); - return; - case "hook_response": - yield* offerRuntimeEvent({ - ...base, - type: "hook.completed", - payload: { - hookId: message.hook_id, - outcome: message.outcome, - output: message.output, - stdout: message.stdout, - stderr: message.stderr, - ...(typeof message.exit_code === "number" ? { exitCode: message.exit_code } : {}), - }, - }); - return; - case "task_started": - yield* offerRuntimeEvent({ - ...base, - type: "task.started", - payload: { - taskId: RuntimeTaskId.make(message.task_id), - description: message.description, - ...(message.task_type ? { taskType: message.task_type } : {}), - }, - }); - return; - case "task_progress": - yield* emitThreadTokenUsage( - context, - normalizeClaudeTaskProgressTokenUsage(message.usage, context), - { - rawMethod: "claude/system/task_progress", - rawPayload: message, - }, - ); - yield* offerRuntimeEvent({ - ...base, - type: "task.progress", - payload: { - taskId: RuntimeTaskId.make(message.task_id), - description: message.description, - ...(message.summary ? { summary: message.summary } : {}), - ...(message.usage ? { usage: message.usage } : {}), - ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}), - }, - }); - return; - case "task_notification": - yield* emitThreadTokenUsage( - context, - normalizeClaudeTaskProgressTokenUsage(message.usage, context), - { - rawMethod: "claude/system/task_notification", - rawPayload: message, - }, - ); - yield* offerRuntimeEvent({ - ...base, - type: "task.completed", - payload: { - taskId: RuntimeTaskId.make(message.task_id), - status: message.status, - ...(message.summary ? { summary: message.summary } : {}), - ...(message.usage ? { usage: message.usage } : {}), - }, - }); - return; - case "files_persisted": - yield* offerRuntimeEvent({ - ...base, - type: "files.persisted", - payload: { - files: Array.isArray(message.files) - ? message.files.map((file: { filename: string; file_id: string }) => ({ - filename: file.filename, - fileId: file.file_id, - })) - : [], - ...(Array.isArray(message.failed) - ? { - failed: message.failed.map((entry: { filename: string; error: string }) => ({ - filename: entry.filename, - error: entry.error, - })), - } - : {}), - }, - }); - return; - case "thinking_tokens": - return; - case "permission_denied": - yield* offerRuntimeEvent({ - ...base, - type: "tool.denied", - payload: { - toolName: message.tool_name, - ...(message.tool_use_id ? { toolUseId: message.tool_use_id } : {}), - ...(message.decision_reason ? { reason: message.decision_reason } : {}), - ...(message.agent_id ? { agentId: message.agent_id } : {}), - }, - }); - return; - case "mirror_error": - yield* emitRuntimeError( - context, - `Claude workspace mirror error: ${message.error}`, - message, - ); - return; - default: - yield* emitRuntimeWarning( - context, - describeUnknownSdkMessage(`Claude system message '${message.subtype}'`, message), - message, - ); - return; - } - }); - - const handleSdkTelemetryMessage = Effect.fn("handleSdkTelemetryMessage")(function* ( - context: ClaudeSessionContext, - message: SDKMessage, - ) { - const stamp = yield* makeEventStamp(); - const base = { - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - providerRefs: nativeProviderRefs(context), - raw: { - source: "claude.sdk.message" as const, - method: sdkNativeMethod(message), - messageType: message.type, - payload: message, - }, - }; - - if (message.type === "tool_progress") { - yield* offerRuntimeEvent({ - ...base, - type: "tool.progress", - payload: { - toolUseId: message.tool_use_id, - toolName: message.tool_name, - elapsedSeconds: message.elapsed_time_seconds, - ...(message.task_id ? { summary: `task:${message.task_id}` } : {}), - }, - }); - return; - } - - if (message.type === "tool_use_summary") { - yield* offerRuntimeEvent({ - ...base, - type: "tool.summary", - payload: { - summary: message.summary, - ...(message.preceding_tool_use_ids.length > 0 - ? { - precedingToolUseIds: message.preceding_tool_use_ids, - } - : {}), - }, - }); - return; - } - - if (message.type === "auth_status") { - yield* offerRuntimeEvent({ - ...base, - type: "auth.status", - payload: { - isAuthenticating: message.isAuthenticating, - output: message.output, - ...(message.error ? { error: message.error } : {}), - }, - }); - return; - } - - if (message.type === "rate_limit_event") { - yield* offerRuntimeEvent({ - ...base, - type: "account.rate-limits.updated", - payload: { - rateLimits: message, - }, - }); - return; - } - }); - - const handleSdkMessage = Effect.fn("handleSdkMessage")(function* ( - context: ClaudeSessionContext, - message: SDKMessage, - ) { - yield* logNativeSdkMessage(context, message); - yield* ensureThreadId(context, message); - - switch (message.type) { - case "stream_event": - yield* handleStreamEvent(context, message); - return; - case "user": - yield* handleUserMessage(context, message); - return; - case "assistant": - yield* handleAssistantMessage(context, message); - return; - case "result": - yield* handleResultMessage(context, message); - return; - case "system": - yield* handleSystemMessage(context, message); - return; - case "tool_progress": - case "tool_use_summary": - case "auth_status": - case "rate_limit_event": - yield* handleSdkTelemetryMessage(context, message); - return; - default: - yield* emitRuntimeWarning( - context, - describeUnknownSdkMessage(`Claude SDK message '${message.type}'`, message), - message, - ); - return; - } - }); - - const runSdkStream = ( - context: ClaudeSessionContext, - ): Effect.Effect => - Stream.fromAsyncIterable( - context.query, - (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId: context.session.threadId, - detail: "Claude runtime stream failed.", - cause, - }), - ).pipe( - Stream.takeWhile(() => !context.stopped), - Stream.runForEach((message) => - handleSdkMessage(context, message).pipe( - Effect.mapError( - (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId: context.session.threadId, - detail: "Failed to process Claude runtime event.", - cause, - }), - ), - ), - ), - ); - - const handleStreamExit = Effect.fn("handleStreamExit")(function* ( - context: ClaudeSessionContext, - exit: Exit.Exit, - ) { - if (context.stopped) { - return; - } - - if (Exit.isFailure(exit)) { - if (isClaudeInterruptedCause(exit.cause)) { - if (context.turnState) { - yield* completeTurn(context, "interrupted", "Claude runtime interrupted."); - } - } else { - const failures = exit.cause.reasons.flatMap((reason) => - Cause.isFailReason(reason) ? [reason.error] : [], - ); - const message = failures[0]?.detail ?? "Claude runtime stream failed."; - yield* emitRuntimeError(context, message, { - failureCount: failures.length, - failureTags: failures.map((failure) => failure._tag), - }); - yield* completeTurn(context, "failed", message); - } - } else if (context.turnState) { - yield* completeTurn(context, "interrupted", "Claude runtime stream ended."); - } - - yield* stopSessionInternal(context, { - emitExitEvent: true, - }); - }); - - const stopSessionInternal = Effect.fn("stopSessionInternal")(function* ( - context: ClaudeSessionContext, - options?: { readonly emitExitEvent?: boolean }, - ) { - if (context.stopped) return; - - context.stopped = true; - - for (const [requestId, pending] of context.pendingApprovals) { - yield* Deferred.succeed(pending.decision, "cancel"); - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "request.resolved", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - requestId: asRuntimeRequestId(requestId), - payload: { - requestType: pending.requestType, - decision: "cancel", - }, - providerRefs: nativeProviderRefs(context), - }); - } - context.pendingApprovals.clear(); - - if (context.turnState) { - yield* completeTurn(context, "interrupted", "Session stopped."); - } - - yield* Queue.shutdown(context.promptQueue); - - const streamFiber = context.streamFiber; - context.streamFiber = undefined; - if (streamFiber && streamFiber.pollUnsafe() === undefined) { - yield* Fiber.interrupt(streamFiber); - } - - yield* Effect.try({ - try: () => context.query.close(), - catch: (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId: context.session.threadId, - detail: "Failed to close Claude runtime query.", - cause, - }), - }).pipe( - Effect.catch((error) => - emitRuntimeError(context, "Failed to close Claude runtime query.", { - errorTag: error._tag, - provider: error.provider, - threadId: error.threadId, - detail: error.detail, - }), - ), - ); - - const updatedAt = yield* nowIso; - context.session = { - ...context.session, - status: "closed", - activeTurnId: undefined, - updatedAt, - }; - - if (options?.emitExitEvent !== false) { - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "session.exited", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - payload: { - reason: "Session stopped", - exitKind: "graceful", - }, - providerRefs: {}, - }); - } - - sessions.delete(context.session.threadId); - }); - - const requireSession = ( - threadId: ThreadId, - ): Effect.Effect => { - const context = sessions.get(threadId); - if (!context) { - return Effect.fail( - new ProviderAdapterSessionNotFoundError({ - provider: PROVIDER, - threadId, - }), - ); - } - if (context.stopped || context.session.status === "closed") { - return Effect.fail( - new ProviderAdapterSessionClosedError({ - provider: PROVIDER, - threadId, - }), - ); - } - return Effect.succeed(context); - }; - - const startSession: ClaudeAdapterShape["startSession"] = Effect.fn("startSession")( - function* (input) { - if (input.provider !== undefined && input.provider !== PROVIDER) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "startSession", - issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, - }); - } - - const existingContext = sessions.get(input.threadId); - if (existingContext) { - yield* Effect.logWarning("claude.session.replacing", { - threadId: input.threadId, - existingSessionStatus: existingContext.session.status, - reason: "startSession called with existing active session", - }); - yield* stopSessionInternal(existingContext, { - emitExitEvent: false, - }).pipe( - // Replacement cleanup is best-effort: never block the new session on - // either typed failures or unexpected defects from tearing down the old one. - Effect.catchCause((cause) => - Effect.logWarning("claude.session.replace.stop-failed", { - threadId: input.threadId, - cause, - }), - ), - ); - } - - const startedAt = yield* nowIso; - const resumeState = readClaudeResumeState(input.resumeCursor); - const threadId = input.threadId; - const existingResumeSessionId = resumeState?.resume; - const newSessionId = existingResumeSessionId === undefined ? yield* randomUUIDv4 : undefined; - const sessionId = existingResumeSessionId ?? newSessionId; - - const runtimeContext = yield* Effect.context(); - const runFork = Effect.runForkWith(runtimeContext); - const runPromise = Effect.runPromiseWith(runtimeContext); - - const promptQueue = yield* Queue.unbounded(); - const prompt = Stream.fromQueue(promptQueue).pipe( - Stream.filter((item) => item.type === "message"), - Stream.map((item) => item.message), - Stream.catchCause((cause) => - Cause.hasInterruptsOnly(cause) ? Stream.empty : Stream.failCause(cause), - ), - Stream.toAsyncIterable, - ); - - const pendingApprovals = new Map(); - const pendingUserInputs = new Map(); - const inFlightTools = new Map(); - const claudeTasks = new Map(); - - const contextRef = yield* Ref.make(undefined); - - /** - * Handle AskUserQuestion tool calls by emitting a `user-input.requested` - * runtime event and waiting for the user to respond via `respondToUserInput`. - */ - const handleAskUserQuestion = Effect.fn("handleAskUserQuestion")(function* ( - context: ClaudeSessionContext, - toolInput: Record, - callbackOptions: { - readonly signal: AbortSignal; - readonly toolUseID?: string; - }, - ) { - const requestId = ApprovalRequestId.make(yield* randomUUIDv4); - - // Parse questions from the SDK's AskUserQuestion input. - // `id` MUST equal the full question text — Claude SDK >= 2.1.121 looks - // up answers by question text in `mapToolResultToToolResultBlockParam`, - // so the key the UI uses to keep its draft answer must match the SDK's - // expected lookup key. See https://github.com/pingdotgg/t3code/issues/2388 - const rawQuestions = Array.isArray(toolInput.questions) ? toolInput.questions : []; - const questions: Array = rawQuestions.map( - (q: Record, idx: number) => ({ - id: typeof q.question === "string" && q.question.length > 0 ? q.question : `q-${idx}`, - header: typeof q.header === "string" ? q.header : `Question ${idx + 1}`, - question: typeof q.question === "string" ? q.question : "", - options: Array.isArray(q.options) - ? q.options.map((opt: Record) => ({ - label: typeof opt.label === "string" ? opt.label : "", - description: typeof opt.description === "string" ? opt.description : "", - })) - : [], - multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : false, - }), - ); - - const answersDeferred = yield* Deferred.make(); - let aborted = false; - const pendingInput: PendingUserInput = { - questions, - answers: answersDeferred, - }; - - // Emit user-input.requested so the UI can present the questions. - const requestedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "user-input.requested", - eventId: requestedStamp.eventId, - provider: PROVIDER, - createdAt: requestedStamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState - ? { - turnId: asCanonicalTurnId(context.turnState.turnId), - } - : {}), - requestId: asRuntimeRequestId(requestId), - payload: { questions }, - providerRefs: nativeProviderRefs(context, { - providerItemId: callbackOptions.toolUseID, - }), - raw: { - source: "claude.sdk.permission", - method: "canUseTool/AskUserQuestion", - payload: { - toolName: "AskUserQuestion", - input: toolInput, - }, - }, - }); - - pendingUserInputs.set(requestId, pendingInput); - - // Handle abort (e.g. turn interrupted while waiting for user input). - const onAbort = () => { - if (!pendingUserInputs.has(requestId)) { - return; - } - aborted = true; - pendingUserInputs.delete(requestId); - runFork(Deferred.succeed(answersDeferred, {} as ProviderUserInputAnswers)); - }; - callbackOptions.signal.addEventListener("abort", onAbort, { - once: true, - }); - - // Block until the user provides answers. - const answers = yield* Deferred.await(answersDeferred); - pendingUserInputs.delete(requestId); - - // Emit user-input.resolved so the UI knows the interaction completed. - const resolvedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "user-input.resolved", - eventId: resolvedStamp.eventId, - provider: PROVIDER, - createdAt: resolvedStamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState - ? { - turnId: asCanonicalTurnId(context.turnState.turnId), - } - : {}), - requestId: asRuntimeRequestId(requestId), - payload: { answers }, - providerRefs: nativeProviderRefs(context, { - providerItemId: callbackOptions.toolUseID, - }), - raw: { - source: "claude.sdk.permission", - method: "canUseTool/AskUserQuestion/resolved", - payload: { answers }, - }, - }); - - if (aborted) { - return { - behavior: "deny", - message: "User cancelled tool execution.", - } satisfies PermissionResult; - } - - // Return the answers to the SDK in the expected format: - // { questions: [...], answers: { questionText: selectedLabel } } - return { - behavior: "allow", - updatedInput: { - questions: toolInput.questions, - answers, - }, - } satisfies PermissionResult; - }); - - const canUseToolEffect = Effect.fn("canUseTool")(function* ( - toolName: Parameters[0], - toolInput: Parameters[1], - callbackOptions: Parameters[2], - ) { - const context = yield* Ref.get(contextRef); - if (!context) { - return { - behavior: "deny", - message: "Claude session context is unavailable.", - } satisfies PermissionResult; - } - - // Handle AskUserQuestion: surface clarifying questions to the - // user via the user-input runtime event channel, regardless of - // runtime mode (plan mode relies on this heavily). - if (toolName === "AskUserQuestion") { - return yield* handleAskUserQuestion(context, toolInput, callbackOptions); - } - - if (toolName === "ExitPlanMode") { - const planMarkdown = extractExitPlanModePlan(toolInput); - if (planMarkdown) { - yield* emitProposedPlanCompleted(context, { - planMarkdown, - toolUseId: callbackOptions.toolUseID, - rawSource: "claude.sdk.permission", - rawMethod: "canUseTool/ExitPlanMode", - rawPayload: { - toolName, - input: toolInput, - }, - }); - } - - return { - behavior: "deny", - message: - "The client captured your proposed plan. Stop here and wait for the user's feedback or implementation request in a later turn.", - } satisfies PermissionResult; - } - - const runtimeMode = input.runtimeMode ?? "full-access"; - if (runtimeMode === "full-access") { - return { - behavior: "allow", - updatedInput: toolInput, - } satisfies PermissionResult; - } - - const requestId = ApprovalRequestId.make(yield* randomUUIDv4); - const requestType = classifyRequestType(toolName); - const detail = summarizeToolRequest(toolName, toolInput); - const decisionDeferred = yield* Deferred.make(); - const pendingApproval: PendingApproval = { - requestType, - detail, - decision: decisionDeferred, - ...(callbackOptions.suggestions ? { suggestions: callbackOptions.suggestions } : {}), - }; - - const requestedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "request.opened", - eventId: requestedStamp.eventId, - provider: PROVIDER, - createdAt: requestedStamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - requestId: asRuntimeRequestId(requestId), - payload: { - requestType, - detail, - args: { - toolName, - input: toolInput, - ...(callbackOptions.toolUseID ? { toolUseId: callbackOptions.toolUseID } : {}), - }, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: callbackOptions.toolUseID, - }), - raw: { - source: "claude.sdk.permission", - method: "canUseTool/request", - payload: { - toolName, - input: toolInput, - }, - }, - }); - - pendingApprovals.set(requestId, pendingApproval); - - const onAbort = () => { - if (!pendingApprovals.has(requestId)) { - return; - } - pendingApprovals.delete(requestId); - runFork(Deferred.succeed(decisionDeferred, "cancel")); - }; - - callbackOptions.signal.addEventListener("abort", onAbort, { - once: true, - }); - - const decision = yield* Deferred.await(decisionDeferred); - pendingApprovals.delete(requestId); - - const resolvedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "request.resolved", - eventId: resolvedStamp.eventId, - provider: PROVIDER, - createdAt: resolvedStamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - requestId: asRuntimeRequestId(requestId), - payload: { - requestType, - decision, - }, - providerRefs: nativeProviderRefs(context, { - providerItemId: callbackOptions.toolUseID, - }), - raw: { - source: "claude.sdk.permission", - method: "canUseTool/decision", - payload: { - decision, - }, - }, - }); - - if (decision === "accept" || decision === "acceptForSession") { - return { - behavior: "allow", - updatedInput: toolInput, - ...(decision === "acceptForSession" && pendingApproval.suggestions - ? { - updatedPermissions: [...pendingApproval.suggestions], - } - : {}), - } satisfies PermissionResult; - } - - return { - behavior: "deny", - message: - decision === "cancel" - ? "User cancelled tool execution." - : "User declined tool execution.", - } satisfies PermissionResult; - }); - - const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) => - runPromise(canUseToolEffect(toolName, toolInput, callbackOptions)); - - const claudeBinaryPath = claudeSettings.binaryPath; - const extraArgs = parseCliArgs(claudeSettings.launchArgs).flags; - const modelSelection = - input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; - const caps = getClaudeModelCapabilities(modelSelection?.model); - const descriptors = getProviderOptionDescriptors({ caps }); - const apiModelId = modelSelection ? resolveClaudeApiModelId(modelSelection) : undefined; - const initialContextWindow = selectedClaudeContextWindow(modelSelection); - const rawEffort = getModelSelectionStringOptionValue(modelSelection, "effort"); - const effort = resolveClaudeEffort(caps, rawEffort) ?? null; - const fastModeSupported = descriptors.some( - (descriptor) => descriptor.type === "boolean" && descriptor.id === "fastMode", - ); - const thinkingSupported = descriptors.some( - (descriptor) => descriptor.type === "boolean" && descriptor.id === "thinking", - ); - const fastMode = - getModelSelectionBooleanOptionValue(modelSelection, "fastMode") === true && - fastModeSupported; - const thinking = thinkingSupported - ? getModelSelectionBooleanOptionValue(modelSelection, "thinking") - : undefined; - const ultracode = isClaudeUltracodeEffort(effort); - const effectiveEffort = getEffectiveClaudeAgentEffort(effort, modelSelection?.model); - const runtimeModeToPermission: Record = { - "auto-accept-edits": "acceptEdits", - "full-access": "bypassPermissions", - }; - const permissionMode = runtimeModeToPermission[input.runtimeMode]; - const settings = { - ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), - ...(fastMode ? { fastMode: true } : {}), - ...(ultracode ? { ultracode: true } : {}), - }; - const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); - const queryOptions: ClaudeQueryOptions = { - ...(input.cwd ? { cwd: input.cwd } : {}), - ...(apiModelId ? { model: apiModelId } : {}), - pathToClaudeCodeExecutable: claudeBinaryPath, - systemPrompt: { type: "preset", preset: "claude_code" }, - settingSources: [...CLAUDE_SETTING_SOURCES], - // `ultracode` is a Claude Code setting, not an API effort level. It is - // normalized to `xhigh` above and paired with `settings.ultracode`. - ...(effectiveEffort - ? { - effort: effectiveEffort as unknown as NonNullable, - } - : {}), - ...(permissionMode ? { permissionMode } : {}), - ...(permissionMode === "bypassPermissions" - ? { allowDangerouslySkipPermissions: true } - : {}), - ...(Object.keys(settings).length > 0 ? { settings } : {}), - ...(existingResumeSessionId ? { resume: existingResumeSessionId } : {}), - ...(newSessionId ? { sessionId: newSessionId } : {}), - includePartialMessages: true, - canUseTool, - env: claudeEnvironment, - ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}), - ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}), - ...(mcpSession - ? { - mcpServers: { - "t3-code": { - type: "http", - url: mcpSession.endpoint, - headers: { - Authorization: mcpSession.authorizationHeader, - }, - }, - }, - } - : {}), - }; - - yield* Effect.annotateCurrentSpan({ - "provider.kind": PROVIDER, - "provider.thread_id": threadId, - "provider.runtime_mode": input.runtimeMode, - "claude.resume.source": - existingResumeSessionId !== undefined ? "resume-session" : "generated-session", - "claude.resume.thread_id": resumeState?.threadId ?? "", - "claude.resume.session_id": existingResumeSessionId ?? "", - "claude.resume.session_at": resumeState?.resumeSessionAt ?? "", - "claude.resume.turn_count": resumeState?.turnCount ?? -1, - "claude.query.cwd": input.cwd ?? "", - "claude.query.model": apiModelId ?? "", - "claude.query.effort": effectiveEffort ?? "", - "claude.query.permission_mode": permissionMode ?? "", - "claude.query.allow_dangerously_skip_permissions": permissionMode === "bypassPermissions", - "claude.query.resume": existingResumeSessionId ?? "", - "claude.query.session_id": newSessionId ?? "", - "claude.query.include_partial_messages": true, - "claude.query.additional_directories": input.cwd ? [input.cwd] : [], - "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES], - "claude.query.settings_json": encodeJsonStringForDiagnostics(settings) ?? "", - "claude.query.extra_args_json": encodeJsonStringForDiagnostics(extraArgs) ?? "", - "claude.query.path_to_executable": claudeBinaryPath, - }); - - const queryRuntime = yield* Effect.try({ - try: () => - createQuery({ - prompt, - options: queryOptions, - }), - catch: (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId, - detail: "Failed to start Claude runtime session.", - cause, - }), - }); - - const session: ProviderSession = { - threadId, - provider: PROVIDER, - providerInstanceId: boundInstanceId, - status: "ready", - runtimeMode: input.runtimeMode, - ...(input.cwd ? { cwd: input.cwd } : {}), - ...(modelSelection?.model ? { model: modelSelection.model } : {}), - ...(threadId ? { threadId } : {}), - resumeCursor: { - ...(threadId ? { threadId } : {}), - ...(sessionId ? { resume: sessionId } : {}), - ...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}), - turnCount: resumeState?.turnCount ?? 0, - }, - createdAt: startedAt, - updatedAt: startedAt, - }; - - const context: ClaudeSessionContext = { - session, - promptQueue, - query: queryRuntime, - streamFiber: undefined, - startedAt, - basePermissionMode: permissionMode, - currentApiModelId: apiModelId, - resumeSessionId: sessionId, - pendingApprovals, - pendingUserInputs, - turns: [], - inFlightTools, - claudeTasks, - turnState: undefined, - lastKnownContextWindow: initialContextWindow, - lastKnownTokenUsage: undefined, - lastKnownTotalProcessedTokens: undefined, - lastAssistantUuid: resumeState?.resumeSessionAt, - lastThreadStartedId: undefined, - stopped: false, - }; - yield* Ref.set(contextRef, context); - sessions.set(threadId, context); - - const sessionStartedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "session.started", - eventId: sessionStartedStamp.eventId, - provider: PROVIDER, - createdAt: sessionStartedStamp.createdAt, - threadId, - payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {}, - providerRefs: {}, - }); - - const configuredStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "session.configured", - eventId: configuredStamp.eventId, - provider: PROVIDER, - createdAt: configuredStamp.createdAt, - threadId, - payload: { - config: { - ...(apiModelId ? { model: apiModelId } : {}), - ...(input.cwd ? { cwd: input.cwd } : {}), - ...(effectiveEffort ? { effort: effectiveEffort } : {}), - ...(permissionMode ? { permissionMode } : {}), - ...(fastMode ? { fastMode: true } : {}), - }, - }, - providerRefs: {}, - }); - - const readyStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "session.state.changed", - eventId: readyStamp.eventId, - provider: PROVIDER, - createdAt: readyStamp.createdAt, - threadId, - payload: { - state: "ready", - }, - providerRefs: {}, - }); - - let streamFiber: Fiber.Fiber; - streamFiber = runFork( - Effect.exit(runSdkStream(context)).pipe( - Effect.flatMap((exit) => { - if (context.stopped) { - return Effect.void; - } - if (context.streamFiber === streamFiber) { - context.streamFiber = undefined; - } - return handleStreamExit(context, exit).pipe( - Effect.catch((cause) => - Effect.logError("Failed to close Claude runtime stream.", { cause }), - ), - ); - }), - ), - ); - context.streamFiber = streamFiber; - streamFiber.addObserver(() => { - if (context.streamFiber === streamFiber) { - context.streamFiber = undefined; - } - }); - - return { - ...session, - }; - }, - ); - - const sendTurn: ClaudeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { - const context = yield* requireSession(input.threadId); - const modelSelection = - input.modelSelection !== undefined && input.modelSelection.instanceId === boundInstanceId - ? input.modelSelection - : undefined; - - // A sendTurn while a real turn is running is a steer: the message is - // queued into the live SDK agent loop and the work continues as the same - // turn — no synthetic turn boundary. Stale synthetic turns (from - // background agent responses between user prompts) are auto-closed - // instead, so they don't block the user's next turn. - const steeringTurnState = - context.turnState && context.turnState.synthetic !== true ? context.turnState : null; - if (context.turnState && steeringTurnState === null) { - yield* completeTurn(context, "completed"); - } - - if (modelSelection?.model) { - const apiModelId = resolveClaudeApiModelId(modelSelection); - if (context.currentApiModelId !== apiModelId) { - yield* Effect.tryPromise({ - try: () => context.query.setModel(apiModelId), - catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause), - }); - context.currentApiModelId = apiModelId; - } - context.session = { - ...context.session, - model: modelSelection.model, - }; - } - - // Apply interaction mode by switching the SDK's permission mode. - // "plan" maps directly to the SDK's "plan" permission mode; - // "default" restores the session's original permission mode. - // When interactionMode is absent we leave the current mode unchanged. - if (input.interactionMode === "plan") { - yield* Effect.tryPromise({ - try: () => context.query.setPermissionMode("plan"), - catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause), - }); - } else if (input.interactionMode === "default") { - yield* Effect.tryPromise({ - try: () => context.query.setPermissionMode(context.basePermissionMode ?? "default"), - catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause), - }); - } - - const turnId = steeringTurnState?.turnId ?? TurnId.make(yield* randomUUIDv4); - if (steeringTurnState === null) { - const turnState: ClaudeTurnState = { - turnId, - startedAt: yield* nowIso, - items: [], - assistantTextBlocks: new Map(), - assistantTextBlockOrder: [], - capturedProposedPlanKeys: new Set(), - nextSyntheticAssistantBlockIndex: -1, - }; - - const updatedAt = yield* nowIso; - context.turnState = turnState; - context.session = { - ...context.session, - status: "running", - activeTurnId: turnId, - updatedAt, - }; - - const turnStartedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.started", - eventId: turnStartedStamp.eventId, - provider: PROVIDER, - createdAt: turnStartedStamp.createdAt, - threadId: context.session.threadId, - turnId, - payload: modelSelection?.model ? { model: modelSelection.model } : {}, - providerRefs: {}, - }); - } - - const message = yield* buildUserMessageEffect(input, { - fileSystem, - attachmentsDir: serverConfig.attachmentsDir, - boundInstanceId, - }); - - yield* Queue.offer(context.promptQueue, { - type: "message", - message, - }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause))); - - return { - threadId: context.session.threadId, - turnId, - ...(context.session.resumeCursor !== undefined - ? { resumeCursor: context.session.resumeCursor } - : {}), - }; - }); - - const interruptTurn: ClaudeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")( - function* (threadId, _turnId) { - const context = yield* requireSession(threadId); - yield* Effect.tryPromise({ - try: () => context.query.interrupt(), - catch: (cause) => toRequestError(threadId, "turn/interrupt", cause), - }); - }, - ); - - const readThread: ClaudeAdapterShape["readThread"] = Effect.fn("readThread")( - function* (threadId) { - const context = yield* requireSession(threadId); - return yield* snapshotThread(context); - }, - ); - - const rollbackThread: ClaudeAdapterShape["rollbackThread"] = Effect.fn("rollbackThread")( - function* (threadId, numTurns) { - const context = yield* requireSession(threadId); - const nextLength = Math.max(0, context.turns.length - numTurns); - context.turns.splice(nextLength); - yield* updateResumeCursor(context); - return yield* snapshotThread(context); - }, - ); - - const respondToRequest: ClaudeAdapterShape["respondToRequest"] = Effect.fn("respondToRequest")( - function* (threadId, requestId, decision) { - const context = yield* requireSession(threadId); - const pending = context.pendingApprovals.get(requestId); - if (!pending) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "item/requestApproval/decision", - detail: `Unknown pending approval request: ${requestId}`, - }); - } - - context.pendingApprovals.delete(requestId); - yield* Deferred.succeed(pending.decision, decision); - }, - ); - - const respondToUserInput: ClaudeAdapterShape["respondToUserInput"] = Effect.fn( - "respondToUserInput", - )(function* (threadId, requestId, answers) { - const context = yield* requireSession(threadId); - const pending = context.pendingUserInputs.get(requestId); - if (!pending) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "item/tool/respondToUserInput", - detail: `Unknown pending user-input request: ${requestId}`, - }); - } - - context.pendingUserInputs.delete(requestId); - yield* Deferred.succeed(pending.answers, answers); - }); - - const stopSession: ClaudeAdapterShape["stopSession"] = Effect.fn("stopSession")( - function* (threadId) { - const context = yield* requireSession(threadId); - yield* stopSessionInternal(context, { - emitExitEvent: true, - }); - }, - ); - - const listSessions: ClaudeAdapterShape["listSessions"] = () => - Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); - - const hasSession: ClaudeAdapterShape["hasSession"] = (threadId) => - Effect.sync(() => { - const context = sessions.get(threadId); - return context !== undefined && !context.stopped; - }); - - const stopAll: ClaudeAdapterShape["stopAll"] = () => - Effect.forEach( - sessions, - ([, context]) => - stopSessionInternal(context, { - emitExitEvent: true, - }), - { discard: true }, - ); - - yield* Effect.addFinalizer(() => - Effect.forEach( - sessions, - ([, context]) => - stopSessionInternal(context, { - emitExitEvent: false, - }), - { discard: true }, - ).pipe( - Effect.catch((cause) => - Effect.logError("Failed to emit Claude session shutdown event.", { cause }), - ), - Effect.tap(() => Queue.shutdown(runtimeEventQueue)), - ), - ); - - return { - provider: PROVIDER, - capabilities: { - sessionModelSwitch: "in-session", - }, - startSession, - sendTurn, - interruptTurn, - readThread, - rollbackThread, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - stopAll, - get streamEvents() { - return Stream.fromQueue(runtimeEventQueue); - }, - } satisfies ClaudeAdapterShape; -}); diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts deleted file mode 100644 index 515a7c6fcbb..00000000000 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ /dev/null @@ -1,1241 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodeAssert from "node:assert/strict"; -import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; -import { - ApprovalRequestId, - CodexSettings, - EventId, - ProviderDriverKind, - ProviderInstanceId, - ProviderItemId, - type ProviderApprovalDecision, - type ProviderEvent, - type ProviderSession, - type ProviderTurnStartResult, - type ProviderUserInputAnswers, - ThreadId, - TurnId, -} from "@t3tools/contracts"; -import { createModelSelection } from "@t3tools/shared/model"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { it, vi } from "@effect/vitest"; - -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Fiber from "effect/Fiber"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Queue from "effect/Queue"; -import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import * as CodexErrors from "effect-codex-app-server/errors"; - -import { ServerConfig } from "../../config.ts"; -import { ServerSettingsService } from "../../serverSettings.ts"; -import { ProviderAdapterValidationError } from "../Errors.ts"; -import type { CodexAdapterShape } from "../Services/CodexAdapter.ts"; -import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; -import { - type CodexSessionRuntimeOptions, - type CodexSessionRuntimeSendTurnInput, - type CodexSessionRuntimeShape, - type CodexThreadSnapshot, -} from "./CodexSessionRuntime.ts"; -import { makeCodexAdapter } from "./CodexAdapter.ts"; -const decodeCodexSettings = Schema.decodeSync(CodexSettings); - -// Test-local service tag so the rest of the file can keep using `yield* CodexAdapter`. -class CodexAdapter extends Context.Service()( - "t3/provider/Layers/CodexAdapter.test/CodexAdapter", -) {} - -const asThreadId = (value: string): ThreadId => ThreadId.make(value); -const asTurnId = (value: string): TurnId => TurnId.make(value); -const asEventId = (value: string): EventId => EventId.make(value); -const asItemId = (value: string): ProviderItemId => ProviderItemId.make(value); - -class FakeCodexRuntime implements CodexSessionRuntimeShape { - private readonly eventQueue = Effect.runSync(Queue.unbounded()); - private readonly now = "2026-01-01T00:00:00.000Z"; - - public readonly startImpl = vi.fn(() => - Promise.resolve({ - provider: ProviderDriverKind.make("codex"), - status: "ready" as const, - runtimeMode: this.options.runtimeMode, - threadId: this.options.threadId, - cwd: this.options.cwd, - ...(this.options.model ? { model: this.options.model } : {}), - createdAt: this.now, - updatedAt: this.now, - } satisfies ProviderSession), - ); - - public readonly sendTurnImpl = vi.fn( - (_input: CodexSessionRuntimeSendTurnInput): Promise => - Promise.resolve({ - threadId: this.options.threadId, - turnId: asTurnId("turn-1"), - }), - ); - - public readonly interruptTurnImpl = vi.fn( - (_turnId?: TurnId): Promise => Promise.resolve(undefined), - ); - - public readonly readThreadImpl = vi.fn( - (): Promise => - Promise.resolve({ - threadId: "provider-thread-1", - turns: [], - }), - ); - - public readonly rollbackThreadImpl = vi.fn( - (_numTurns: number): Promise => - Promise.resolve({ - threadId: "provider-thread-1", - turns: [], - }), - ); - - public readonly respondToRequestImpl = vi.fn( - (_requestId: ApprovalRequestId, _decision: ProviderApprovalDecision): Promise => - Promise.resolve(undefined), - ); - - public readonly respondToUserInputImpl = vi.fn( - (_requestId: ApprovalRequestId, _answers: ProviderUserInputAnswers): Promise => - Promise.resolve(undefined), - ); - - public readonly closeImpl = vi.fn(() => Promise.resolve(undefined)); - - readonly options: CodexSessionRuntimeOptions; - - constructor(options: CodexSessionRuntimeOptions) { - this.options = options; - } - - start() { - return Effect.promise(() => this.startImpl()); - } - - getSession = Effect.promise(() => this.startImpl()); - - sendTurn(input: CodexSessionRuntimeSendTurnInput) { - return Effect.promise(() => this.sendTurnImpl(input)); - } - - interruptTurn(turnId?: TurnId) { - return Effect.promise(() => this.interruptTurnImpl(turnId)); - } - - readThread = Effect.promise(() => this.readThreadImpl()); - - rollbackThread(numTurns: number) { - return Effect.promise(() => this.rollbackThreadImpl(numTurns)); - } - - respondToRequest(requestId: ApprovalRequestId, decision: ProviderApprovalDecision) { - return Effect.promise(() => this.respondToRequestImpl(requestId, decision)); - } - - respondToUserInput(requestId: ApprovalRequestId, answers: ProviderUserInputAnswers) { - return Effect.promise(() => this.respondToUserInputImpl(requestId, answers)); - } - - get events() { - return Stream.fromQueue(this.eventQueue); - } - - close = Effect.promise(() => this.closeImpl()); - - emit(event: ProviderEvent) { - return Queue.offer(this.eventQueue, event).pipe(Effect.asVoid); - } -} - -function makeRuntimeFactory() { - const runtimes: Array = []; - const factory = vi.fn((options: CodexSessionRuntimeOptions) => { - const runtime = new FakeCodexRuntime(options); - runtimes.push(runtime); - return Effect.succeed(runtime); - }); - - return { - factory, - get lastRuntime(): FakeCodexRuntime | undefined { - return runtimes.at(-1); - }, - }; -} - -function makeScopedRuntimeFactory(options?: { readonly failConstruction?: boolean }) { - const runtimes: Array = []; - const releasedThreadIds: Array = []; - - const factory = vi.fn((runtimeOptions: CodexSessionRuntimeOptions) => - Effect.gen(function* () { - yield* Scope.Scope; - yield* Effect.addFinalizer(() => - Effect.sync(() => { - releasedThreadIds.push(runtimeOptions.threadId); - }), - ); - - if (options?.failConstruction) { - return yield* new CodexErrors.CodexAppServerSpawnError({ - command: `${runtimeOptions.binaryPath} app-server`, - cause: new Error("runtime construction failed"), - }); - } - - const runtime = new FakeCodexRuntime(runtimeOptions); - runtimes.push(runtime); - return runtime; - }), - ); - - return { - factory, - releasedThreadIds, - get lastRuntime(): FakeCodexRuntime | undefined { - return runtimes.at(-1); - }, - }; -} - -const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory, { - upsert: () => Effect.void, - getProvider: () => - Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")), - getBinding: () => Effect.succeed(Option.none()), - listThreadIds: () => Effect.succeed([]), - listBindings: () => Effect.succeed([]), -}); - -const validationRuntimeFactory = makeRuntimeFactory(); -const validationLayer = it.layer( - Layer.effect( - CodexAdapter, - Effect.gen(function* () { - const codexConfig = decodeCodexSettings({}); - return yield* makeCodexAdapter(codexConfig, { - makeRuntime: validationRuntimeFactory.factory, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ), -); - -validationLayer("CodexAdapterLive validation", (it) => { - it.effect("returns validation error for non-codex provider on startSession", () => - Effect.gen(function* () { - const adapter = yield* CodexAdapter; - const result = yield* adapter - .startSession({ - provider: ProviderDriverKind.make("claudeAgent"), - threadId: asThreadId("thread-1"), - runtimeMode: "full-access", - }) - .pipe(Effect.result); - - NodeAssert.equal(result._tag, "Failure"); - NodeAssert.deepStrictEqual( - result.failure, - new ProviderAdapterValidationError({ - provider: ProviderDriverKind.make("codex"), - operation: "startSession", - issue: "Expected provider 'codex' but received 'claudeAgent'.", - }), - ); - NodeAssert.equal(validationRuntimeFactory.factory.mock.calls.length, 0); - }), - ); - it.effect("maps codex model options before starting a session", () => - Effect.gen(function* () { - validationRuntimeFactory.factory.mockClear(); - const adapter = yield* CodexAdapter; - - yield* adapter.startSession({ - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ - { id: "serviceTier", value: "priority" }, - ]), - runtimeMode: "full-access", - }); - - NodeAssert.deepStrictEqual(validationRuntimeFactory.factory.mock.calls[0]?.[0], { - binaryPath: "codex", - cwd: process.cwd(), - model: "gpt-5.3-codex", - providerInstanceId: ProviderInstanceId.make("codex"), - serviceTier: "priority", - threadId: asThreadId("thread-1"), - runtimeMode: "full-access", - }); - }), - ); -}); - -const sessionRuntimeFactory = makeRuntimeFactory(); -const sessionErrorLayer = it.layer( - Layer.effect( - CodexAdapter, - Effect.gen(function* () { - const codexConfig = decodeCodexSettings({}); - return yield* makeCodexAdapter(codexConfig, { - makeRuntime: sessionRuntimeFactory.factory, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ), -); - -sessionErrorLayer("CodexAdapterLive session errors", (it) => { - it.effect("maps missing adapter sessions to ProviderAdapterSessionNotFoundError", () => - Effect.gen(function* () { - const adapter = yield* CodexAdapter; - const result = yield* adapter - .sendTurn({ - threadId: asThreadId("sess-missing"), - input: "hello", - attachments: [], - }) - .pipe(Effect.result); - - NodeAssert.equal(result._tag, "Failure"); - NodeAssert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError"); - NodeAssert.equal(result.failure.provider, "codex"); - NodeAssert.equal(result.failure.threadId, "sess-missing"); - }), - ); - - it.effect("maps codex model options before sending a turn", () => - Effect.gen(function* () { - const adapter = yield* CodexAdapter; - yield* adapter.startSession({ - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("sess-missing"), - runtimeMode: "full-access", - }); - const runtime = sessionRuntimeFactory.lastRuntime; - NodeAssert.ok(runtime); - runtime.sendTurnImpl.mockClear(); - - yield* Effect.ignore( - adapter.sendTurn({ - threadId: asThreadId("sess-missing"), - input: "hello", - modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ - { id: "reasoningEffort", value: "high" }, - { id: "serviceTier", value: "priority" }, - ]), - attachments: [], - }), - ); - - NodeAssert.deepStrictEqual(runtime.sendTurnImpl.mock.calls[0]?.[0], { - input: "hello", - model: "gpt-5.3-codex", - effort: "high", - serviceTier: "priority", - }); - }), - ); - - it.effect("maps codex model options for the adapter's bound custom instance id", () => { - const customInstanceId = ProviderInstanceId.make("codex_personal"); - const customRuntimeFactory = makeRuntimeFactory(); - const customLayer = Layer.effect( - CodexAdapter, - Effect.gen(function* () { - const codexConfig = decodeCodexSettings({}); - return yield* makeCodexAdapter(codexConfig, { - instanceId: customInstanceId, - makeRuntime: customRuntimeFactory.factory, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ); - - return Effect.gen(function* () { - const adapter = yield* CodexAdapter; - yield* adapter.startSession({ - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("sess-custom-instance"), - runtimeMode: "full-access", - }); - const runtime = customRuntimeFactory.lastRuntime; - NodeAssert.ok(runtime); - runtime.sendTurnImpl.mockClear(); - - yield* Effect.ignore( - adapter.sendTurn({ - threadId: asThreadId("sess-custom-instance"), - input: "hello", - modelSelection: createModelSelection( - ProviderInstanceId.make("codex_personal"), - "gpt-5.3-codex", - [ - { id: "reasoningEffort", value: "high" }, - { id: "serviceTier", value: "flex" }, - ], - ), - attachments: [], - }), - ); - - NodeAssert.deepStrictEqual(runtime.sendTurnImpl.mock.calls[0]?.[0], { - input: "hello", - model: "gpt-5.3-codex", - effort: "high", - serviceTier: "flex", - }); - }).pipe(Effect.provide(customLayer)); - }); -}); - -const lifecycleRuntimeFactory = makeRuntimeFactory(); -const lifecycleLayer = it.layer( - Layer.effect( - CodexAdapter, - Effect.gen(function* () { - const codexConfig = decodeCodexSettings({}); - return yield* makeCodexAdapter(codexConfig, { - makeRuntime: lifecycleRuntimeFactory.factory, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ), -); - -function startLifecycleRuntime() { - return Effect.gen(function* () { - const adapter = yield* CodexAdapter; - yield* adapter.startSession({ - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - runtimeMode: "full-access", - }); - const runtime = lifecycleRuntimeFactory.lastRuntime; - NodeAssert.ok(runtime); - return { adapter, runtime }; - }); -} - -lifecycleLayer("CodexAdapterLive lifecycle", (it) => { - it.effect("maps completed agent message items to canonical item.completed events", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - const event: ProviderEvent = { - id: asEventId("evt-msg-complete"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "item/completed", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-1"), - itemId: asItemId("msg_1"), - payload: { - completedAtMs: 1_778_000_000_000, - threadId: "thread-1", - turnId: "turn-1", - item: { - type: "agentMessage", - id: "msg_1", - text: "done", - }, - }, - }; - - yield* runtime.emit(event); - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "item.completed"); - if (firstEvent.value.type !== "item.completed") { - return; - } - NodeAssert.equal(firstEvent.value.itemId, "msg_1"); - NodeAssert.equal(firstEvent.value.turnId, "turn-1"); - NodeAssert.equal(firstEvent.value.payload.itemType, "assistant_message"); - }), - ); - - it.effect("labels MCP lifecycle entries with server and tool names", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - yield* runtime.emit({ - id: asEventId("evt-mcp-complete"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "item/completed", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-1"), - itemId: asItemId("mcp_1"), - payload: { - completedAtMs: 1_778_000_000_000, - threadId: "thread-1", - turnId: "turn-1", - item: { - type: "mcpToolCall", - id: "mcp_1", - server: "t3-code", - tool: "preview_status", - arguments: {}, - durationMs: 12, - error: null, - result: { content: [{ type: "text", text: "attached" }] }, - status: "completed", - }, - }, - }); - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some" || firstEvent.value.type !== "item.completed") { - return; - } - NodeAssert.equal(firstEvent.value.payload.itemType, "mcp_tool_call"); - NodeAssert.equal(firstEvent.value.payload.title, "t3-code · preview_status"); - NodeAssert.deepStrictEqual(firstEvent.value.payload.data, { - completedAtMs: 1_778_000_000_000, - threadId: "thread-1", - turnId: "turn-1", - item: { - type: "mcpToolCall", - id: "mcp_1", - server: "t3-code", - tool: "preview_status", - arguments: {}, - durationMs: 12, - error: null, - result: { content: [{ type: "text", text: "attached" }] }, - status: "completed", - }, - }); - }), - ); - - it.effect("maps completed plan items to canonical proposed-plan completion events", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - const event: ProviderEvent = { - id: asEventId("evt-plan-complete"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "item/completed", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-1"), - itemId: asItemId("plan_1"), - payload: { - completedAtMs: 1_778_000_000_000, - threadId: "thread-1", - turnId: "turn-1", - item: { - type: "plan", - id: "plan_1", - text: "## Final plan\n\n- one\n- two", - }, - }, - }; - - yield* runtime.emit(event); - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "turn.proposed.completed"); - if (firstEvent.value.type !== "turn.proposed.completed") { - return; - } - NodeAssert.equal(firstEvent.value.turnId, "turn-1"); - NodeAssert.equal(firstEvent.value.payload.planMarkdown, "## Final plan\n\n- one\n- two"); - }), - ); - - it.effect("maps plan deltas to canonical proposed-plan delta events", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - yield* runtime.emit({ - id: asEventId("evt-plan-delta"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "item/plan/delta", - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-1"), - itemId: asItemId("plan_1"), - payload: { - threadId: "thread-1", - turnId: "turn-1", - itemId: "plan_1", - delta: "## Final plan", - }, - } satisfies ProviderEvent); - - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "turn.proposed.delta"); - if (firstEvent.value.type !== "turn.proposed.delta") { - return; - } - NodeAssert.equal(firstEvent.value.turnId, "turn-1"); - NodeAssert.equal(firstEvent.value.payload.delta, "## Final plan"); - }), - ); - - it.effect("maps session/closed lifecycle events to canonical session.exited runtime events", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - const event: ProviderEvent = { - id: asEventId("evt-session-closed"), - kind: "session", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "session/closed", - message: "Session stopped", - }; - - yield* runtime.emit(event); - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "session.exited"); - if (firstEvent.value.type !== "session.exited") { - return; - } - NodeAssert.equal(firstEvent.value.threadId, "thread-1"); - NodeAssert.equal(firstEvent.value.payload.reason, "Session stopped"); - }), - ); - - it.effect("maps retryable Codex error notifications to runtime.warning", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - yield* runtime.emit({ - id: asEventId("evt-retryable-error"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "error", - turnId: asTurnId("turn-1"), - payload: { - threadId: "thread-1", - turnId: "turn-1", - error: { - message: "Reconnecting... 2/5", - }, - willRetry: true, - }, - } satisfies ProviderEvent); - - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "runtime.warning"); - if (firstEvent.value.type !== "runtime.warning") { - return; - } - NodeAssert.equal(firstEvent.value.turnId, "turn-1"); - NodeAssert.equal(firstEvent.value.payload.message, "Reconnecting... 2/5"); - }), - ); - - it.effect("maps process stderr notifications to runtime.warning", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - yield* runtime.emit({ - id: asEventId("evt-process-stderr"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "process/stderr", - turnId: asTurnId("turn-1"), - message: "The filename or extension is too long. (os error 206)", - } satisfies ProviderEvent); - - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "runtime.warning"); - if (firstEvent.value.type !== "runtime.warning") { - return; - } - NodeAssert.equal(firstEvent.value.turnId, "turn-1"); - NodeAssert.equal( - firstEvent.value.payload.message, - "The filename or extension is too long. (os error 206)", - ); - }), - ); - - it.effect("maps realtime started notifications with upstream realtime session ids", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - yield* runtime.emit({ - id: asEventId("evt-realtime-started"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "thread/realtime/started", - payload: { - threadId: "thread-1", - realtimeSessionId: "realtime-session-1", - version: "v2", - }, - } satisfies ProviderEvent); - - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "thread.realtime.started"); - if (firstEvent.value.type !== "thread.realtime.started") { - return; - } - NodeAssert.equal(firstEvent.value.threadId, "thread-1"); - NodeAssert.equal(firstEvent.value.payload.realtimeSessionId, "realtime-session-1"); - }), - ); - - it.effect("maps fatal websocket stderr notifications to runtime.error", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - yield* runtime.emit({ - id: asEventId("evt-process-stderr-websocket"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "process/stderr", - turnId: asTurnId("turn-1"), - message: - "2026-03-31T18:14:06.833399Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 503 Service Unavailable, url: wss://chatgpt.com/backend-api/codex/responses", - } satisfies ProviderEvent); - - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "runtime.error"); - if (firstEvent.value.type !== "runtime.error") { - return; - } - NodeAssert.equal(firstEvent.value.turnId, "turn-1"); - NodeAssert.equal(firstEvent.value.payload.class, "provider_error"); - NodeAssert.equal( - firstEvent.value.payload.message, - "2026-03-31T18:14:06.833399Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 503 Service Unavailable, url: wss://chatgpt.com/backend-api/codex/responses", - ); - }), - ); - - it.effect("preserves request type when mapping serverRequest/resolved", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - const event: ProviderEvent = { - id: asEventId("evt-request-resolved"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "serverRequest/resolved", - requestKind: "command", - requestId: ApprovalRequestId.make("req-1"), - payload: { - threadId: "thread-1", - requestId: "req-1", - }, - }; - - yield* runtime.emit(event); - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "request.resolved"); - if (firstEvent.value.type !== "request.resolved") { - return; - } - NodeAssert.equal(firstEvent.value.payload.requestType, "command_execution_approval"); - }), - ); - - it.effect("preserves file-read request type when mapping serverRequest/resolved", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - const event: ProviderEvent = { - id: asEventId("evt-file-read-request-resolved"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "serverRequest/resolved", - requestKind: "file-read", - requestId: ApprovalRequestId.make("req-file-read-1"), - payload: { - threadId: "thread-1", - requestId: "req-file-read-1", - }, - }; - - yield* runtime.emit(event); - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "request.resolved"); - if (firstEvent.value.type !== "request.resolved") { - return; - } - NodeAssert.equal(firstEvent.value.payload.requestType, "file_read_approval"); - }), - ); - - it.effect("preserves explicit empty multi-select user-input answers", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - const event: ProviderEvent = { - id: asEventId("evt-user-input-empty"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "item/tool/requestUserInput/answered", - payload: { - answers: { - scope: { - answers: [], - }, - }, - }, - }; - - yield* runtime.emit(event); - const firstEvent = yield* Fiber.join(firstEventFiber); - - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "user-input.resolved"); - if (firstEvent.value.type !== "user-input.resolved") { - return; - } - NodeAssert.deepEqual(firstEvent.value.payload.answers, { - scope: [], - }); - }), - ); - - it.effect("maps windowsSandbox/setupCompleted to session state and warning on failure", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const eventsFiber = yield* Stream.runCollect(Stream.take(adapter.streamEvents, 2)).pipe( - Effect.forkChild, - ); - - const event: ProviderEvent = { - id: asEventId("evt-windows-sandbox-failed"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "windowsSandbox/setupCompleted", - message: "Sandbox setup failed", - payload: { - mode: "unelevated", - success: false, - error: "unsupported environment", - }, - }; - - yield* runtime.emit(event); - const events = Array.from(yield* Fiber.join(eventsFiber)); - - NodeAssert.equal(events.length, 2); - - const firstEvent = events[0]; - const secondEvent = events[1]; - - NodeAssert.equal(firstEvent?.type, "session.state.changed"); - if (firstEvent?.type === "session.state.changed") { - NodeAssert.equal(firstEvent.payload.state, "error"); - NodeAssert.equal(firstEvent.payload.reason, "Sandbox setup failed"); - } - - NodeAssert.equal(secondEvent?.type, "runtime.warning"); - if (secondEvent?.type === "runtime.warning") { - NodeAssert.equal(secondEvent.payload.message, "Sandbox setup failed"); - } - }), - ); - - it.effect( - "maps requestUserInput requests and answered notifications to canonical user-input events", - () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const eventsFiber = yield* Stream.runCollect(Stream.take(adapter.streamEvents, 2)).pipe( - Effect.forkChild, - ); - - yield* runtime.emit({ - id: asEventId("evt-user-input-requested"), - kind: "request", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "item/tool/requestUserInput", - requestId: ApprovalRequestId.make("req-user-input-1"), - payload: { - itemId: "item-user-input-1", - threadId: "thread-1", - turnId: "turn-1", - questions: [ - { - id: "sandbox_mode", - header: "Sandbox", - question: "Which mode should be used?", - options: [ - { - label: "workspace-write", - description: "Allow workspace writes only", - }, - ], - }, - ], - }, - } satisfies ProviderEvent); - yield* runtime.emit({ - id: asEventId("evt-user-input-resolved"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "item/tool/requestUserInput/answered", - requestId: ApprovalRequestId.make("req-user-input-1"), - payload: { - answers: { - sandbox_mode: { - answers: ["workspace-write"], - }, - }, - }, - } satisfies ProviderEvent); - - const events = Array.from(yield* Fiber.join(eventsFiber)); - NodeAssert.equal(events[0]?.type, "user-input.requested"); - if (events[0]?.type === "user-input.requested") { - NodeAssert.equal(events[0].requestId, "req-user-input-1"); - NodeAssert.equal(events[0].payload.questions[0]?.id, "sandbox_mode"); - NodeAssert.equal(events[0].payload.questions[0]?.multiSelect, false); - } - - NodeAssert.equal(events[1]?.type, "user-input.resolved"); - if (events[1]?.type === "user-input.resolved") { - NodeAssert.equal(events[1].requestId, "req-user-input-1"); - NodeAssert.deepEqual(events[1].payload.answers, { - sandbox_mode: "workspace-write", - }); - } - }), - ); - - it.effect("unwraps Codex token usage payloads for context window events", () => - Effect.gen(function* () { - const { adapter, runtime } = yield* startLifecycleRuntime(); - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - - yield* runtime.emit({ - id: asEventId("evt-codex-thread-token-usage-updated"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-1"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "thread/tokenUsage/updated", - payload: { - threadId: "thread-1", - turnId: "turn-1", - tokenUsage: { - total: { - inputTokens: 11_833, - cachedInputTokens: 3456, - outputTokens: 6, - reasoningOutputTokens: 0, - totalTokens: 11_839, - }, - last: { - inputTokens: 120, - cachedInputTokens: 0, - outputTokens: 6, - reasoningOutputTokens: 0, - totalTokens: 126, - }, - modelContextWindow: 258_400, - }, - }, - } satisfies ProviderEvent); - - const firstEvent = yield* Fiber.join(firstEventFiber); - NodeAssert.equal(firstEvent._tag, "Some"); - if (firstEvent._tag !== "Some") { - return; - } - NodeAssert.equal(firstEvent.value.type, "thread.token-usage.updated"); - if (firstEvent.value.type !== "thread.token-usage.updated") { - return; - } - - NodeAssert.deepEqual(firstEvent.value.payload.usage, { - usedTokens: 126, - totalProcessedTokens: 11_839, - maxTokens: 258_400, - inputTokens: 120, - cachedInputTokens: 0, - outputTokens: 6, - reasoningOutputTokens: 0, - lastUsedTokens: 126, - lastInputTokens: 120, - lastCachedInputTokens: 0, - lastOutputTokens: 6, - lastReasoningOutputTokens: 0, - compactsAutomatically: true, - }); - }), - ); -}); - -const scopedLifecycleRuntimeFactory = makeScopedRuntimeFactory(); -const scopedLifecycleLayer = it.layer( - Layer.effect( - CodexAdapter, - Effect.gen(function* () { - const codexConfig = decodeCodexSettings({}); - return yield* makeCodexAdapter(codexConfig, { - makeRuntime: scopedLifecycleRuntimeFactory.factory, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ), -); - -scopedLifecycleLayer("CodexAdapterLive scoped lifecycle", (it) => { - it.effect("closes the externally owned session scope on stopSession", () => - Effect.gen(function* () { - scopedLifecycleRuntimeFactory.releasedThreadIds.length = 0; - const adapter = yield* CodexAdapter; - - yield* adapter.startSession({ - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-stop"), - runtimeMode: "full-access", - }); - - const runtime = scopedLifecycleRuntimeFactory.lastRuntime; - NodeAssert.ok(runtime); - - yield* adapter.stopSession(asThreadId("thread-stop")); - - NodeAssert.equal(runtime.closeImpl.mock.calls.length, 1); - NodeAssert.deepStrictEqual(scopedLifecycleRuntimeFactory.releasedThreadIds, [ - asThreadId("thread-stop"), - ]); - NodeAssert.equal(yield* adapter.hasSession(asThreadId("thread-stop")), false); - }), - ); -}); - -const scopedFailureRuntimeFactory = makeScopedRuntimeFactory({ failConstruction: true }); -const scopedFailureLayer = it.layer( - Layer.effect( - CodexAdapter, - Effect.gen(function* () { - const codexConfig = decodeCodexSettings({}); - return yield* makeCodexAdapter(codexConfig, { - makeRuntime: scopedFailureRuntimeFactory.factory, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ), -); - -scopedFailureLayer("CodexAdapterLive scoped startup failure", (it) => { - it.effect("closes the externally owned session scope when startSession fails", () => - Effect.gen(function* () { - scopedFailureRuntimeFactory.releasedThreadIds.length = 0; - const adapter = yield* CodexAdapter; - - const result = yield* adapter - .startSession({ - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-fail"), - runtimeMode: "full-access", - }) - .pipe(Effect.result); - - NodeAssert.equal(result._tag, "Failure"); - NodeAssert.equal(result.failure._tag, "ProviderAdapterProcessError"); - NodeAssert.deepStrictEqual(scopedFailureRuntimeFactory.releasedThreadIds, [ - asThreadId("thread-fail"), - ]); - NodeAssert.equal(yield* adapter.hasSession(asThreadId("thread-fail")), false); - }), - ); -}); - -it.effect("flushes managed native logs when the adapter layer shuts down", () => - Effect.gen(function* () { - const tempDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-codex-adapter-native-log-"), - ); - const basePath = NodePath.join(tempDir, "provider-native.ndjson"); - const runtimeFactory = makeRuntimeFactory(); - const scope = yield* Scope.make("sequential"); - let scopeClosed = false; - - try { - const layer = Layer.effect( - CodexAdapter, - Effect.gen(function* () { - const codexConfig = decodeCodexSettings({}); - return yield* makeCodexAdapter(codexConfig, { - makeRuntime: runtimeFactory.factory, - nativeEventLogPath: basePath, - }); - }), - ).pipe( - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ); - const context = yield* Layer.buildWithScope(layer, scope); - const adapter = yield* Effect.service(CodexAdapter).pipe(Effect.provide(context)); - - yield* adapter.startSession({ - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-logger"), - runtimeMode: "full-access", - }); - - const runtime = runtimeFactory.lastRuntime; - NodeAssert.ok(runtime); - - const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); - yield* runtime.emit({ - id: asEventId("evt-native-log"), - kind: "notification", - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-logger"), - createdAt: "2026-01-01T00:00:00.000Z", - method: "process/stderr", - message: "native flush test", - } satisfies ProviderEvent); - yield* Fiber.join(firstEventFiber); - - yield* Scope.close(scope, Exit.void); - scopeClosed = true; - - const threadLogPath = NodePath.join(tempDir, "thread-logger.log"); - NodeAssert.equal(NodeFS.existsSync(threadLogPath), true); - const contents = NodeFS.readFileSync(threadLogPath, "utf8"); - NodeAssert.match(contents, /NTIVE: .*"message":"native flush test"/); - } finally { - if (!scopeClosed) { - yield* Scope.close(scope, Exit.void); - } - NodeFS.rmSync(tempDir, { recursive: true, force: true }); - } - }), -); diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts deleted file mode 100644 index 270126e934b..00000000000 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ /dev/null @@ -1,1720 +0,0 @@ -/** - * CodexAdapterLive - Scoped live implementation for the Codex provider adapter. - * - * Wraps the typed Codex session runtime behind the `CodexAdapter` service - * contract and maps runtime failures into the shared `ProviderAdapterError` - * algebra. - * - * @module CodexAdapterLive - */ -import { - type CanonicalItemType, - type CanonicalRequestType, - type CodexSettings, - ProviderDriverKind, - type ProviderEvent, - ProviderInstanceId, - type ProviderRuntimeEvent, - type ProviderRequestKind, - type ThreadTokenUsageSnapshot, - type ProviderUserInputAnswers, - RuntimeItemId, - RuntimeRequestId, - ProviderApprovalDecision, - ThreadId, - ProviderSendTurnInput, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import * as Crypto from "effect/Crypto"; -import * as Exit from "effect/Exit"; -import * as Fiber from "effect/Fiber"; -import * as FileSystem from "effect/FileSystem"; -import * as Queue from "effect/Queue"; -import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import { ChildProcessSpawner } from "effect/unstable/process"; -import * as CodexErrors from "effect-codex-app-server/errors"; -import * as EffectCodexSchema from "effect-codex-app-server/schema"; - -import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; -import { getCodexServiceTierOptionValue } from "../../codexModelOptions.ts"; -import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; - -import { - ProviderAdapterRequestError, - ProviderAdapterProcessError, - ProviderAdapterSessionClosedError, - ProviderAdapterSessionNotFoundError, - ProviderAdapterValidationError, - type ProviderAdapterError, -} from "../Errors.ts"; -import { type CodexAdapterShape } from "../Services/CodexAdapter.ts"; -import { resolveAttachmentPath } from "../../attachmentStore.ts"; -import { ServerConfig } from "../../config.ts"; -import { - CodexResumeCursorSchema, - CodexSessionRuntimeThreadIdMissingError, - makeCodexSessionRuntime, - type CodexSessionRuntimeError, - type CodexSessionRuntimeOptions, - type CodexSessionRuntimeShape, -} from "./CodexSessionRuntime.ts"; -import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; -const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError); -const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError); -const isCodexSessionRuntimeThreadIdMissingError = Schema.is( - CodexSessionRuntimeThreadIdMissingError, -); -const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema); - -const PROVIDER = ProviderDriverKind.make("codex"); - -export interface CodexAdapterLiveOptions { - readonly instanceId?: ProviderInstanceId; - readonly environment?: NodeJS.ProcessEnv; - readonly makeRuntime?: ( - options: CodexSessionRuntimeOptions, - ) => Effect.Effect< - CodexSessionRuntimeShape, - CodexSessionRuntimeError, - ChildProcessSpawner.ChildProcessSpawner | Scope.Scope - >; - readonly nativeEventLogPath?: string; - readonly nativeEventLogger?: EventNdjsonLogger; -} - -interface CodexAdapterSessionContext { - readonly threadId: ThreadId; - readonly scope: Scope.Closeable; - readonly runtime: CodexSessionRuntimeShape; - readonly eventFiber: Fiber.Fiber; - stopped: boolean; -} - -function mapCodexRuntimeError( - threadId: ThreadId, - method: string, - error: CodexSessionRuntimeError, -): ProviderAdapterError { - if (isCodexAppServerProcessExitedError(error) || isCodexAppServerTransportError(error)) { - return new ProviderAdapterSessionClosedError({ - provider: PROVIDER, - threadId, - cause: error, - }); - } - - if (isCodexSessionRuntimeThreadIdMissingError(error)) { - return new ProviderAdapterSessionNotFoundError({ - provider: PROVIDER, - threadId, - cause: error, - }); - } - - return new ProviderAdapterRequestError({ - provider: PROVIDER, - method, - detail: error.message, - cause: error, - }); -} - -type CodexLifecycleItem = - | EffectCodexSchema.V2ItemStartedNotification["item"] - | EffectCodexSchema.V2ItemCompletedNotification["item"]; - -type CodexToolUserInputQuestion = - | EffectCodexSchema.ServerRequest__ToolRequestUserInputQuestion - | EffectCodexSchema.ToolRequestUserInputParams__ToolRequestUserInputQuestion; - -const ApprovalDecisionPayload = Schema.Struct({ - decision: ProviderApprovalDecision, -}); - -function readPayload( - schema: Schema.Schema, - payload: ProviderEvent["payload"], -): A | undefined { - const isPayload = Schema.is(schema); - return isPayload(payload) ? payload : undefined; -} - -function trimText(value: string | undefined | null): string | undefined { - const trimmed = value?.trim(); - return trimmed && trimmed.length > 0 ? trimmed : undefined; -} - -const FATAL_CODEX_STDERR_SNIPPETS = ["failed to connect to websocket"]; - -function isFatalCodexProcessStderrMessage(message: string): boolean { - const normalized = message.toLowerCase(); - return FATAL_CODEX_STDERR_SNIPPETS.some((snippet) => normalized.includes(snippet)); -} - -function normalizeCodexTokenUsage( - usage: EffectCodexSchema.V2ThreadTokenUsageUpdatedNotification["tokenUsage"], -): ThreadTokenUsageSnapshot | undefined { - const totalProcessedTokens = usage.total.totalTokens; - const usedTokens = usage.last.totalTokens; - if (usedTokens === undefined || usedTokens <= 0) { - return undefined; - } - - const maxTokens = usage.modelContextWindow ?? undefined; - const inputTokens = usage.last.inputTokens; - const cachedInputTokens = usage.last.cachedInputTokens; - const outputTokens = usage.last.outputTokens; - const reasoningOutputTokens = usage.last.reasoningOutputTokens; - - return { - usedTokens, - ...(totalProcessedTokens !== undefined && totalProcessedTokens > usedTokens - ? { totalProcessedTokens } - : {}), - ...(maxTokens !== undefined ? { maxTokens } : {}), - ...(inputTokens !== undefined ? { inputTokens } : {}), - ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}), - ...(outputTokens !== undefined ? { outputTokens } : {}), - ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}), - ...(usedTokens !== undefined ? { lastUsedTokens: usedTokens } : {}), - ...(inputTokens !== undefined ? { lastInputTokens: inputTokens } : {}), - ...(cachedInputTokens !== undefined ? { lastCachedInputTokens: cachedInputTokens } : {}), - ...(outputTokens !== undefined ? { lastOutputTokens: outputTokens } : {}), - ...(reasoningOutputTokens !== undefined - ? { lastReasoningOutputTokens: reasoningOutputTokens } - : {}), - compactsAutomatically: true, - }; -} - -function toTurnStatus( - value: EffectCodexSchema.V2TurnCompletedNotification["turn"]["status"] | "cancelled", -): "completed" | "failed" | "cancelled" | "interrupted" { - switch (value) { - case "completed": - case "failed": - case "cancelled": - case "interrupted": - return value; - default: - return "completed"; - } -} - -function normalizeItemType(raw: string | undefined | null): string { - const type = trimText(raw); - if (!type) return "item"; - return type - .replace(/([a-z0-9])([A-Z])/g, "$1 $2") - .replace(/[._/-]/g, " ") - .replace(/\s+/g, " ") - .trim() - .toLowerCase(); -} - -function toCanonicalItemType(raw: string | undefined | null): CanonicalItemType { - const type = normalizeItemType(raw); - if (type.includes("user")) return "user_message"; - if (type.includes("agent message") || type.includes("assistant")) return "assistant_message"; - if (type.includes("reasoning") || type.includes("thought")) return "reasoning"; - if (type.includes("plan") || type.includes("todo")) return "plan"; - if (type.includes("command")) return "command_execution"; - if (type.includes("file change") || type.includes("patch") || type.includes("edit")) - return "file_change"; - if (type.includes("mcp")) return "mcp_tool_call"; - if (type.includes("dynamic tool")) return "dynamic_tool_call"; - if (type.includes("collab")) return "collab_agent_tool_call"; - if (type.includes("web search")) return "web_search"; - if (type.includes("image")) return "image_view"; - if (type.includes("review entered")) return "review_entered"; - if (type.includes("review exited")) return "review_exited"; - if (type.includes("compact")) return "context_compaction"; - if (type.includes("error")) return "error"; - return "unknown"; -} - -function itemTitle(itemType: CanonicalItemType, item?: CodexLifecycleItem): string | undefined { - if (itemType === "mcp_tool_call" && item?.type === "mcpToolCall") { - return `${item.server} · ${item.tool}`; - } - switch (itemType) { - case "assistant_message": - return "Assistant message"; - case "user_message": - return "User message"; - case "reasoning": - return "Reasoning"; - case "plan": - return "Plan"; - case "command_execution": - return "Ran command"; - case "file_change": - return "File change"; - case "mcp_tool_call": - return "MCP tool call"; - case "dynamic_tool_call": - return "Tool call"; - case "web_search": - return "Web search"; - case "image_view": - return "Image view"; - case "error": - return "Error"; - default: - return undefined; - } -} - -function itemDetail(item: CodexLifecycleItem): string | undefined { - const candidates = [ - "command" in item ? item.command : undefined, - "title" in item ? item.title : undefined, - "summary" in item ? item.summary : undefined, - "text" in item ? item.text : undefined, - "path" in item ? item.path : undefined, - "prompt" in item ? item.prompt : undefined, - ]; - for (const candidate of candidates) { - const trimmed = typeof candidate === "string" ? trimText(candidate) : undefined; - if (!trimmed) continue; - return trimmed; - } - return undefined; -} - -function toRequestTypeFromMethod(method: string): CanonicalRequestType { - switch (method) { - case "item/commandExecution/requestApproval": - return "command_execution_approval"; - case "item/fileRead/requestApproval": - return "file_read_approval"; - case "item/fileChange/requestApproval": - return "file_change_approval"; - case "applyPatchApproval": - return "apply_patch_approval"; - case "execCommandApproval": - return "exec_command_approval"; - case "item/tool/requestUserInput": - return "tool_user_input"; - case "item/tool/call": - return "dynamic_tool_call"; - case "account/chatgptAuthTokens/refresh": - return "auth_tokens_refresh"; - default: - return "unknown"; - } -} - -function toRequestTypeFromKind(kind: ProviderRequestKind | undefined): CanonicalRequestType { - switch (kind) { - case "command": - return "command_execution_approval"; - case "file-read": - return "file_read_approval"; - case "file-change": - return "file_change_approval"; - default: - return "unknown"; - } -} - -function toCanonicalUserInputAnswers( - answers: EffectCodexSchema.ToolRequestUserInputResponse["answers"], -): ProviderUserInputAnswers { - return Object.fromEntries( - Object.entries(answers).map(([questionId, value]) => { - const normalizedAnswers = value.answers.length === 1 ? value.answers[0]! : [...value.answers]; - return [questionId, normalizedAnswers] as const; - }), - ); -} - -function toUserInputQuestions(questions: ReadonlyArray) { - const parsedQuestions = questions - .map((question) => { - const options = - question.options - ?.map((option) => { - const label = trimText(option.label); - const description = trimText(option.description); - if (!label || !description) { - return undefined; - } - return { label, description }; - }) - .filter((option) => option !== undefined) ?? []; - - const id = trimText(question.id); - const header = trimText(question.header); - const prompt = trimText(question.question); - if (!id || !header || !prompt || options.length === 0) { - return undefined; - } - return { - id, - header, - question: prompt, - options, - multiSelect: false, - }; - }) - .filter((question) => question !== undefined); - - return parsedQuestions.length > 0 ? parsedQuestions : undefined; -} - -function toThreadState( - status: EffectCodexSchema.V2ThreadStatusChangedNotification["status"], -): "active" | "idle" | "archived" | "closed" | "compacted" | "error" { - switch (status.type) { - case "idle": - return "idle"; - case "systemError": - return "error"; - default: - return "active"; - } -} - -function contentStreamKindFromMethod( - method: string, -): - | "assistant_text" - | "reasoning_text" - | "reasoning_summary_text" - | "plan_text" - | "command_output" - | "file_change_output" { - switch (method) { - case "item/agentMessage/delta": - return "assistant_text"; - case "item/reasoning/textDelta": - return "reasoning_text"; - case "item/reasoning/summaryTextDelta": - return "reasoning_summary_text"; - case "item/commandExecution/outputDelta": - return "command_output"; - case "item/fileChange/outputDelta": - return "file_change_output"; - default: - return "assistant_text"; - } -} - -function asRuntimeItemId(itemId: ProviderEvent["itemId"] & string): RuntimeItemId { - return RuntimeItemId.make(itemId); -} - -function asRuntimeRequestId(requestId: string): RuntimeRequestId { - return RuntimeRequestId.make(requestId); -} - -function eventRawSource(event: ProviderEvent): NonNullable["source"] { - return event.kind === "request" ? "codex.app-server.request" : "codex.app-server.notification"; -} - -function providerRefsFromEvent( - event: ProviderEvent, -): ProviderRuntimeEvent["providerRefs"] | undefined { - const refs: Record = {}; - if (event.turnId) refs.providerTurnId = event.turnId; - if (event.itemId) refs.providerItemId = event.itemId; - if (event.requestId) refs.providerRequestId = event.requestId; - - return Object.keys(refs).length > 0 ? (refs as ProviderRuntimeEvent["providerRefs"]) : undefined; -} - -function runtimeEventBase( - event: ProviderEvent, - canonicalThreadId: ThreadId, -): Omit { - const refs = providerRefsFromEvent(event); - return { - eventId: event.id, - provider: event.provider, - threadId: canonicalThreadId, - createdAt: event.createdAt, - ...(event.turnId ? { turnId: event.turnId } : {}), - ...(event.itemId ? { itemId: asRuntimeItemId(event.itemId) } : {}), - ...(event.requestId ? { requestId: asRuntimeRequestId(event.requestId) } : {}), - ...(refs ? { providerRefs: refs } : {}), - raw: { - source: eventRawSource(event), - method: event.method, - payload: event.payload ?? {}, - }, - }; -} - -function mapItemLifecycle( - event: ProviderEvent, - canonicalThreadId: ThreadId, - lifecycle: "item.started" | "item.updated" | "item.completed", -): ProviderRuntimeEvent | undefined { - const payload = - readPayload(EffectCodexSchema.V2ItemStartedNotification, event.payload) ?? - readPayload(EffectCodexSchema.V2ItemCompletedNotification, event.payload); - const item = payload?.item; - if (!item) { - return undefined; - } - const itemType = toCanonicalItemType(item.type); - if (itemType === "unknown" && lifecycle !== "item.updated") { - return undefined; - } - - const detail = itemDetail(item); - const status = - lifecycle === "item.started" - ? "inProgress" - : lifecycle === "item.completed" - ? "completed" - : undefined; - - return { - ...runtimeEventBase(event, canonicalThreadId), - type: lifecycle, - payload: { - itemType, - ...(status ? { status } : {}), - ...(itemTitle(itemType, item) ? { title: itemTitle(itemType, item) } : {}), - ...(detail ? { detail } : {}), - ...(event.payload !== undefined ? { data: event.payload } : {}), - }, - }; -} - -function mapToRuntimeEvents( - event: ProviderEvent, - canonicalThreadId: ThreadId, -): ReadonlyArray { - if (event.kind === "error") { - if (!event.message) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "runtime.error", - payload: { - message: event.message, - class: "provider_error", - ...(event.payload !== undefined ? { detail: event.payload } : {}), - }, - }, - ]; - } - - if (event.kind === "request") { - if (event.method === "item/tool/requestUserInput") { - const payload = - readPayload(EffectCodexSchema.ServerRequest__ToolRequestUserInputParams, event.payload) ?? - readPayload(EffectCodexSchema.ToolRequestUserInputParams, event.payload); - const questions = payload ? toUserInputQuestions(payload.questions) : undefined; - if (!questions) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "user-input.requested", - payload: { - questions, - }, - }, - ]; - } - - const detail = (() => { - switch (event.method) { - case "item/commandExecution/requestApproval": { - const payload = readPayload( - EffectCodexSchema.ServerRequest__CommandExecutionRequestApprovalParams, - event.payload, - ); - return payload?.command ?? payload?.reason ?? undefined; - } - case "item/fileChange/requestApproval": { - const payload = readPayload( - EffectCodexSchema.ServerRequest__FileChangeRequestApprovalParams, - event.payload, - ); - return payload?.reason ?? undefined; - } - case "applyPatchApproval": { - const payload = readPayload( - EffectCodexSchema.ServerRequest__ApplyPatchApprovalParams, - event.payload, - ); - return payload?.reason ?? undefined; - } - case "execCommandApproval": { - const payload = readPayload( - EffectCodexSchema.ServerRequest__ExecCommandApprovalParams, - event.payload, - ); - return payload?.reason ?? payload?.command.join(" "); - } - case "item/tool/call": { - const payload = readPayload( - EffectCodexSchema.ServerRequest__DynamicToolCallParams, - event.payload, - ); - return payload?.tool ?? undefined; - } - default: - return undefined; - } - })(); - - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "request.opened", - payload: { - requestType: toRequestTypeFromMethod(event.method), - ...(detail ? { detail } : {}), - ...(event.payload !== undefined ? { args: event.payload } : {}), - }, - }, - ]; - } - - if (event.method === "item/requestApproval/decision" && event.requestId) { - const payload = readPayload(ApprovalDecisionPayload, event.payload); - const requestType = - event.requestKind !== undefined - ? toRequestTypeFromKind(event.requestKind) - : toRequestTypeFromMethod(event.method); - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "request.resolved", - payload: { - requestType, - ...(payload ? { decision: payload.decision } : {}), - ...(event.payload !== undefined ? { resolution: event.payload } : {}), - }, - }, - ]; - } - - if (event.method === "session/connecting") { - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "session.state.changed", - payload: { - state: "starting", - ...(event.message ? { reason: event.message } : {}), - }, - }, - ]; - } - - if (event.method === "session/ready") { - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "session.state.changed", - payload: { - state: "ready", - ...(event.message ? { reason: event.message } : {}), - }, - }, - ]; - } - - if (event.method === "session/started") { - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "session.started", - payload: { - ...(event.message ? { message: event.message } : {}), - ...(event.payload !== undefined ? { resume: event.payload } : {}), - }, - }, - ]; - } - - if (event.method === "session/exited" || event.method === "session/closed") { - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "session.exited", - payload: { - ...(event.message ? { reason: event.message } : {}), - ...(event.method === "session/closed" ? { exitKind: "graceful" } : {}), - }, - }, - ]; - } - - if (event.method === "thread/started") { - const payload = readPayload(EffectCodexSchema.V2ThreadStartedNotification, event.payload); - if (!payload) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "thread.started", - payload: { - providerThreadId: payload.thread.id, - }, - }, - ]; - } - - if ( - event.method === "thread/status/changed" || - event.method === "thread/archived" || - event.method === "thread/unarchived" || - event.method === "thread/closed" || - event.method === "thread/compacted" - ) { - const payload = - event.method === "thread/status/changed" - ? readPayload(EffectCodexSchema.V2ThreadStatusChangedNotification, event.payload) - : undefined; - return [ - { - type: "thread.state.changed", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - state: - event.method === "thread/archived" - ? "archived" - : event.method === "thread/closed" - ? "closed" - : event.method === "thread/compacted" - ? "compacted" - : payload - ? toThreadState(payload.status) - : "active", - ...(event.payload !== undefined ? { detail: event.payload } : {}), - }, - }, - ]; - } - - if (event.method === "thread/name/updated") { - const payload = readPayload(EffectCodexSchema.V2ThreadNameUpdatedNotification, event.payload); - return [ - { - type: "thread.metadata.updated", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - ...(trimText(payload?.threadName) ? { name: trimText(payload?.threadName) } : {}), - ...(payload - ? { - metadata: { - threadId: payload.threadId, - ...(payload.threadName !== undefined && payload.threadName !== null - ? { threadName: payload.threadName } - : {}), - }, - } - : {}), - }, - }, - ]; - } - - if (event.method === "thread/tokenUsage/updated") { - const payload = readPayload( - EffectCodexSchema.V2ThreadTokenUsageUpdatedNotification, - event.payload, - ); - const normalizedUsage = payload ? normalizeCodexTokenUsage(payload.tokenUsage) : undefined; - if (!normalizedUsage) { - return []; - } - return [ - { - type: "thread.token-usage.updated", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - usage: normalizedUsage, - }, - }, - ]; - } - - if (event.method === "turn/started") { - const turnId = event.turnId; - if (!turnId) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - turnId, - type: "turn.started", - payload: {}, - }, - ]; - } - - if (event.method === "turn/completed") { - const payload = readPayload(EffectCodexSchema.V2TurnCompletedNotification, event.payload); - if (!payload) { - return []; - } - const errorMessage = trimText(payload.turn.error?.message); - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "turn.completed", - payload: { - state: toTurnStatus(payload.turn.status), - ...(errorMessage ? { errorMessage } : {}), - }, - }, - ]; - } - - if (event.method === "turn/aborted") { - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "turn.aborted", - payload: { - reason: event.message ?? "Turn aborted", - }, - }, - ]; - } - - if (event.method === "turn/plan/updated") { - const payload = readPayload(EffectCodexSchema.V2TurnPlanUpdatedNotification, event.payload); - if (!payload) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "turn.plan.updated", - payload: { - ...(trimText(payload.explanation) ? { explanation: trimText(payload.explanation) } : {}), - plan: payload.plan.map((step) => ({ - step: trimText(step.step) ?? "step", - status: - step.status === "completed" || step.status === "inProgress" ? step.status : "pending", - })), - }, - }, - ]; - } - - if (event.method === "turn/diff/updated") { - const payload = readPayload(EffectCodexSchema.V2TurnDiffUpdatedNotification, event.payload); - if (!payload) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "turn.diff.updated", - payload: { - unifiedDiff: payload.diff, - }, - }, - ]; - } - - if (event.method === "item/started") { - const started = mapItemLifecycle(event, canonicalThreadId, "item.started"); - return started ? [started] : []; - } - - if (event.method === "item/completed") { - const payload = readPayload(EffectCodexSchema.V2ItemCompletedNotification, event.payload); - const item = payload?.item; - if (!item) { - return []; - } - const itemType = toCanonicalItemType(item.type); - if (itemType === "plan") { - const detail = itemDetail(item); - if (!detail) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "turn.proposed.completed", - payload: { - planMarkdown: detail, - }, - }, - ]; - } - const completed = mapItemLifecycle(event, canonicalThreadId, "item.completed"); - return completed ? [completed] : []; - } - - if ( - event.method === "item/reasoning/summaryPartAdded" || - event.method === "item/commandExecution/terminalInteraction" - ) { - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "item.updated", - payload: { - itemType: - event.method === "item/reasoning/summaryPartAdded" ? "reasoning" : "command_execution", - ...(event.payload !== undefined ? { data: event.payload } : {}), - }, - }, - ]; - } - - if (event.method === "item/plan/delta") { - const payload = readPayload(EffectCodexSchema.V2PlanDeltaNotification, event.payload); - const delta = event.textDelta ?? payload?.delta; - if (!delta || delta.length === 0) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "turn.proposed.delta", - payload: { - delta, - }, - }, - ]; - } - - if (event.method === "item/agentMessage/delta") { - const payload = readPayload(EffectCodexSchema.V2AgentMessageDeltaNotification, event.payload); - const delta = event.textDelta ?? payload?.delta; - if (!delta || delta.length === 0) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "content.delta", - payload: { - streamKind: contentStreamKindFromMethod(event.method), - delta, - }, - }, - ]; - } - - if (event.method === "item/commandExecution/outputDelta") { - const payload = readPayload( - EffectCodexSchema.V2CommandExecutionOutputDeltaNotification, - event.payload, - ); - const delta = event.textDelta ?? payload?.delta; - if (!delta || delta.length === 0) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "content.delta", - payload: { - streamKind: "command_output", - delta, - }, - }, - ]; - } - - if (event.method === "item/fileChange/outputDelta") { - const payload = readPayload( - EffectCodexSchema.V2FileChangeOutputDeltaNotification, - event.payload, - ); - const delta = event.textDelta ?? payload?.delta; - if (!delta || delta.length === 0) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "content.delta", - payload: { - streamKind: "file_change_output", - delta, - }, - }, - ]; - } - - if (event.method === "item/reasoning/summaryTextDelta") { - const payload = readPayload( - EffectCodexSchema.V2ReasoningSummaryTextDeltaNotification, - event.payload, - ); - const delta = event.textDelta ?? payload?.delta; - if (!delta || delta.length === 0) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "content.delta", - payload: { - streamKind: "reasoning_summary_text", - delta, - ...(payload ? { summaryIndex: payload.summaryIndex } : {}), - }, - }, - ]; - } - - if (event.method === "item/reasoning/textDelta") { - const payload = readPayload(EffectCodexSchema.V2ReasoningTextDeltaNotification, event.payload); - const delta = event.textDelta ?? payload?.delta; - if (!delta || delta.length === 0) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "content.delta", - payload: { - streamKind: "reasoning_text", - delta, - ...(payload ? { contentIndex: payload.contentIndex } : {}), - }, - }, - ]; - } - - if (event.method === "item/mcpToolCall/progress") { - const payload = readPayload(EffectCodexSchema.V2McpToolCallProgressNotification, event.payload); - if (!payload) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "tool.progress", - payload: { - summary: payload.message, - }, - }, - ]; - } - - if (event.method === "serverRequest/resolved") { - const payload = readPayload( - EffectCodexSchema.V2ServerRequestResolvedNotification, - event.payload, - ); - if (!payload) { - return []; - } - const requestType = toRequestTypeFromKind(event.requestKind); - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "request.resolved", - payload: { - requestType, - ...(event.payload !== undefined ? { resolution: event.payload } : {}), - }, - }, - ]; - } - - if (event.method === "item/tool/requestUserInput/answered") { - const payload = readPayload(EffectCodexSchema.ToolRequestUserInputResponse, event.payload); - if (!payload) { - return []; - } - return [ - { - ...runtimeEventBase(event, canonicalThreadId), - type: "user-input.resolved", - payload: { - answers: toCanonicalUserInputAnswers(payload.answers), - }, - }, - ]; - } - - if (event.method === "model/rerouted") { - const payload = readPayload(EffectCodexSchema.V2ModelReroutedNotification, event.payload); - if (!payload) { - return []; - } - return [ - { - type: "model.rerouted", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - fromModel: payload.fromModel, - toModel: payload.toModel, - reason: payload.reason, - }, - }, - ]; - } - - if (event.method === "deprecationNotice") { - const payload = readPayload(EffectCodexSchema.V2DeprecationNoticeNotification, event.payload); - if (!payload) { - return []; - } - return [ - { - type: "deprecation.notice", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - summary: payload.summary, - ...(trimText(payload.details) ? { details: trimText(payload.details) } : {}), - }, - }, - ]; - } - - if (event.method === "configWarning") { - const payload = readPayload(EffectCodexSchema.V2ConfigWarningNotification, event.payload); - if (!payload) { - return []; - } - return [ - { - type: "config.warning", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - summary: payload.summary, - ...(trimText(payload.details) ? { details: trimText(payload.details) } : {}), - ...(trimText(payload.path) ? { path: trimText(payload.path) } : {}), - ...(payload.range !== undefined && payload.range !== null - ? { range: payload.range } - : {}), - }, - }, - ]; - } - - if (event.method === "account/updated") { - if (!readPayload(EffectCodexSchema.V2AccountUpdatedNotification, event.payload)) { - return []; - } - return [ - { - type: "account.updated", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - account: event.payload ?? {}, - }, - }, - ]; - } - - if (event.method === "account/rateLimits/updated") { - if (!readPayload(EffectCodexSchema.V2AccountRateLimitsUpdatedNotification, event.payload)) { - return []; - } - return [ - { - type: "account.rate-limits.updated", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - rateLimits: event.payload ?? {}, - }, - }, - ]; - } - - if (event.method === "mcpServer/oauthLogin/completed") { - const payload = readPayload( - EffectCodexSchema.V2McpServerOauthLoginCompletedNotification, - event.payload, - ); - if (!payload) { - return []; - } - return [ - { - type: "mcp.oauth.completed", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - success: payload.success, - name: payload.name, - ...(trimText(payload.error) ? { error: trimText(payload.error) } : {}), - }, - }, - ]; - } - - if (event.method === "thread/realtime/started") { - const payload = readPayload( - EffectCodexSchema.V2ThreadRealtimeStartedNotification, - event.payload, - ); - if (!payload) { - return []; - } - return [ - { - type: "thread.realtime.started", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - realtimeSessionId: payload.realtimeSessionId ?? undefined, - }, - }, - ]; - } - - if (event.method === "thread/realtime/itemAdded") { - const payload = readPayload( - EffectCodexSchema.V2ThreadRealtimeItemAddedNotification, - event.payload, - ); - if (!payload) { - return []; - } - return [ - { - type: "thread.realtime.item-added", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - item: payload.item, - }, - }, - ]; - } - - if (event.method === "thread/realtime/outputAudio/delta") { - const payload = readPayload( - EffectCodexSchema.V2ThreadRealtimeOutputAudioDeltaNotification, - event.payload, - ); - if (!payload) { - return []; - } - return [ - { - type: "thread.realtime.audio.delta", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - audio: payload.audio, - }, - }, - ]; - } - - if (event.method === "thread/realtime/error") { - const payload = readPayload(EffectCodexSchema.V2ThreadRealtimeErrorNotification, event.payload); - const message = payload?.message ?? event.message ?? "Realtime error"; - return [ - { - type: "thread.realtime.error", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - message, - }, - }, - ]; - } - - if (event.method === "thread/realtime/closed") { - const payload = readPayload( - EffectCodexSchema.V2ThreadRealtimeClosedNotification, - event.payload, - ); - return [ - { - type: "thread.realtime.closed", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - reason: payload?.reason ?? event.message, - }, - }, - ]; - } - - if (event.method === "error") { - const payload = readPayload(EffectCodexSchema.V2ErrorNotification, event.payload); - const message = payload?.error.message ?? event.message ?? "Provider runtime error"; - const willRetry = payload?.willRetry === true; - return [ - { - type: willRetry ? "runtime.warning" : "runtime.error", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - message, - ...(!willRetry ? { class: "provider_error" as const } : {}), - ...(event.payload !== undefined ? { detail: event.payload } : {}), - }, - }, - ]; - } - - if (event.method === "process/stderr") { - const message = event.message ?? "Codex process stderr"; - const isFatal = isFatalCodexProcessStderrMessage(message); - return [ - isFatal - ? { - type: "runtime.error", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - message, - class: "provider_error" as const, - ...(event.payload !== undefined ? { detail: event.payload } : {}), - }, - } - : { - type: "runtime.warning", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - message, - ...(event.payload !== undefined ? { detail: event.payload } : {}), - }, - }, - ]; - } - - if (event.method === "windows/worldWritableWarning") { - if (!readPayload(EffectCodexSchema.V2WindowsWorldWritableWarningNotification, event.payload)) { - return []; - } - return [ - { - type: "runtime.warning", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - message: event.message ?? "Windows world-writable warning", - ...(event.payload !== undefined ? { detail: event.payload } : {}), - }, - }, - ]; - } - - if (event.method === "windowsSandbox/setupCompleted") { - const payload = readPayload( - EffectCodexSchema.V2WindowsSandboxSetupCompletedNotification, - event.payload, - ); - if (!payload) { - return []; - } - const successMessage = event.message ?? "Windows sandbox setup completed"; - const failureMessage = event.message ?? "Windows sandbox setup failed"; - - return [ - { - type: "session.state.changed", - ...runtimeEventBase(event, canonicalThreadId), - payload: { - state: payload.success === false ? "error" : "ready", - reason: payload.success === false ? failureMessage : successMessage, - ...(event.payload !== undefined ? { detail: event.payload } : {}), - }, - }, - ...(payload.success === false - ? [ - { - type: "runtime.warning" as const, - ...runtimeEventBase(event, canonicalThreadId), - payload: { - message: failureMessage, - ...(event.payload !== undefined ? { detail: event.payload } : {}), - }, - }, - ] - : []), - ]; - } - - return []; -} - -/** - * Build a Codex provider adapter bound to a specific `CodexSettings` payload. - * - * The adapter is a captured closure over `codexConfig` — the `binaryPath` and - * `homePath` are read from that payload, not from `ServerSettingsService`. - * This is what makes multi-instance routing possible: each `ProviderInstance` - * in the registry owns its own closure with its own config, so two Codex - * instances with different `homePath`s cannot step on each other. - */ -export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( - codexConfig: CodexSettings, - options?: CodexAdapterLiveOptions, -) { - const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex"); - const fileSystem = yield* FileSystem.FileSystem; - const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const crypto = yield* Crypto.Crypto; - const serverConfig = yield* Effect.service(ServerConfig); - const nativeEventLogger = - options?.nativeEventLogger ?? - (options?.nativeEventLogPath !== undefined - ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { - stream: "native", - }) - : undefined); - const managedNativeEventLogger = - options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; - const runtimeEventQueue = yield* Queue.unbounded(); - const sessions = new Map(); - - const startSession: CodexAdapterShape["startSession"] = (input) => - Effect.scoped( - Effect.gen(function* () { - if (input.provider !== undefined && input.provider !== PROVIDER) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "startSession", - issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, - }); - } - - const existing = sessions.get(input.threadId); - if (existing && !existing.stopped) { - yield* Effect.suspend(() => stopSessionInternal(existing)); - } - - const serviceTier = - input.modelSelection?.instanceId === boundInstanceId - ? getCodexServiceTierOptionValue(input.modelSelection) - : undefined; - const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); - const runtimeInput: CodexSessionRuntimeOptions = { - threadId: input.threadId, - providerInstanceId: boundInstanceId, - cwd: input.cwd ?? process.cwd(), - binaryPath: codexConfig.binaryPath, - ...(options?.environment ? { environment: options.environment } : {}), - ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}), - ...(isCodexResumeCursorSchema(input.resumeCursor) - ? { resumeCursor: input.resumeCursor } - : {}), - runtimeMode: input.runtimeMode, - ...(input.modelSelection?.instanceId === boundInstanceId - ? { model: input.modelSelection.model } - : {}), - ...(serviceTier ? { serviceTier } : {}), - ...(mcpSession - ? { - environment: { - ...(options?.environment ?? process.env), - T3_MCP_BEARER_TOKEN: mcpSession.authorizationHeader.replace(/^Bearer\s+/, ""), - }, - appServerArgs: [ - "-c", - `mcp_servers.t3-code.url=${mcpSession.endpoint}`, - "-c", - 'mcp_servers.t3-code.bearer_token_env_var="T3_MCP_BEARER_TOKEN"', - ], - } - : {}), - }; - const sessionScope = yield* Scope.make("sequential"); - let sessionScopeTransferred = false; - yield* Effect.addFinalizer(() => - sessionScopeTransferred ? Effect.void : Scope.close(sessionScope, Exit.void), - ); - const createRuntime = options?.makeRuntime ?? makeCodexSessionRuntime; - const runtime = yield* createRuntime(runtimeInput).pipe( - Effect.provideService(Scope.Scope, sessionScope), - Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner), - Effect.provideService(Crypto.Crypto, crypto), - Effect.mapError( - (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId: input.threadId, - detail: cause.message, - cause, - }), - ), - ); - - const eventFiber = yield* Stream.runForEach(runtime.events, (event) => - Effect.gen(function* () { - yield* writeNativeEvent(event); - const runtimeEvents = mapToRuntimeEvents(event, event.threadId); - if (runtimeEvents.length === 0) { - yield* Effect.logDebug("ignoring unhandled Codex provider event", { - method: event.method, - threadId: event.threadId, - turnId: event.turnId, - itemId: event.itemId, - }); - return; - } - yield* Queue.offerAll(runtimeEventQueue, runtimeEvents); - }), - ).pipe(Effect.forkChild); - - const started = yield* runtime.start().pipe( - Effect.mapError( - (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId: input.threadId, - detail: cause.message, - cause, - }), - ), - Effect.onError(() => - runtime.close.pipe( - Effect.andThen(Effect.ignore(Scope.close(sessionScope, Exit.void))), - Effect.andThen(Fiber.interrupt(eventFiber)), - Effect.ignore, - ), - ), - ); - - sessions.set(input.threadId, { - threadId: input.threadId, - scope: sessionScope, - runtime, - eventFiber, - stopped: false, - }); - sessionScopeTransferred = true; - - return started; - }), - ); - - const resolveAttachment = Effect.fn("resolveAttachment")(function* ( - input: ProviderSendTurnInput, - attachment: NonNullable[number], - ) { - const attachmentPath = resolveAttachmentPath({ - attachmentsDir: serverConfig.attachmentsDir, - attachment, - }); - if (!attachmentPath) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "turn/start", - detail: `Invalid attachment id '${attachment.id}'.`, - }); - } - const bytes = yield* fileSystem.readFile(attachmentPath).pipe( - Effect.mapError( - (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "turn/start", - detail: `Failed to read attachment file: ${cause.message}.`, - cause, - }), - ), - ); - return { - type: "image" as const, - url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`, - }; - }); - - const sendTurn: CodexAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { - const codexAttachments = yield* Effect.forEach( - input.attachments ?? [], - (attachment) => resolveAttachment(input, attachment), - { concurrency: 1 }, - ); - - const session = yield* requireSession(input.threadId); - const reasoningEffort = - input.modelSelection?.instanceId === boundInstanceId - ? getModelSelectionStringOptionValue(input.modelSelection, "reasoningEffort") - : undefined; - const serviceTier = - input.modelSelection?.instanceId === boundInstanceId - ? getCodexServiceTierOptionValue(input.modelSelection) - : undefined; - return yield* session.runtime - .sendTurn({ - ...(input.input !== undefined ? { input: input.input } : {}), - ...(input.modelSelection?.instanceId === boundInstanceId - ? { model: input.modelSelection.model } - : {}), - ...(reasoningEffort - ? { - effort: reasoningEffort as EffectCodexSchema.V2TurnStartParams__ReasoningEffort, - } - : {}), - ...(serviceTier ? { serviceTier } : {}), - ...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}), - ...(codexAttachments.length > 0 ? { attachments: codexAttachments } : {}), - }) - .pipe(Effect.mapError((cause) => mapCodexRuntimeError(input.threadId, "turn/start", cause))); - }); - - const requireSession = Effect.fn("requireSession")(function* (threadId: ThreadId) { - const session = sessions.get(threadId); - if (!session || session.stopped) { - return yield* new ProviderAdapterSessionNotFoundError({ - provider: PROVIDER, - threadId, - }); - } - return session; - }); - - const interruptTurn: CodexAdapterShape["interruptTurn"] = (threadId, turnId) => - requireSession(threadId).pipe( - Effect.flatMap((session) => session.runtime.interruptTurn(turnId)), - Effect.mapError((cause) => - cause._tag === "ProviderAdapterSessionNotFoundError" - ? cause - : mapCodexRuntimeError(threadId, "turn/interrupt", cause), - ), - ); - - const readThread: CodexAdapterShape["readThread"] = (threadId) => - requireSession(threadId).pipe( - Effect.flatMap((session) => session.runtime.readThread), - Effect.mapError((cause) => - cause._tag === "ProviderAdapterSessionNotFoundError" - ? cause - : mapCodexRuntimeError(threadId, "thread/read", cause), - ), - Effect.map((snapshot) => ({ - threadId, - turns: snapshot.turns, - })), - ); - - const rollbackThread: CodexAdapterShape["rollbackThread"] = (threadId, numTurns) => { - if (!Number.isInteger(numTurns) || numTurns < 1) { - return Effect.fail( - new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "rollbackThread", - issue: "numTurns must be an integer >= 1.", - }), - ); - } - - return requireSession(threadId).pipe( - Effect.flatMap((session) => session.runtime.rollbackThread(numTurns)), - Effect.mapError((cause) => - cause._tag === "ProviderAdapterSessionNotFoundError" - ? cause - : mapCodexRuntimeError(threadId, "thread/rollback", cause), - ), - Effect.map((snapshot) => ({ - threadId, - turns: snapshot.turns, - })), - ); - }; - - const respondToRequest: CodexAdapterShape["respondToRequest"] = (threadId, requestId, decision) => - requireSession(threadId).pipe( - Effect.flatMap((session) => session.runtime.respondToRequest(requestId, decision)), - Effect.mapError((cause) => - cause._tag === "ProviderAdapterSessionNotFoundError" - ? cause - : mapCodexRuntimeError(threadId, "item/requestApproval/decision", cause), - ), - ); - - const respondToUserInput: CodexAdapterShape["respondToUserInput"] = ( - threadId, - requestId, - answers, - ) => - requireSession(threadId).pipe( - Effect.flatMap((session) => session.runtime.respondToUserInput(requestId, answers)), - Effect.mapError((cause) => - cause._tag === "ProviderAdapterSessionNotFoundError" - ? cause - : mapCodexRuntimeError(threadId, "item/tool/requestUserInput", cause), - ), - ); - - const writeNativeEvent = Effect.fn("writeNativeEvent")(function* (event: ProviderEvent) { - if (!nativeEventLogger) { - return; - } - yield* nativeEventLogger.write(event, event.threadId); - }); - - const stopSessionInternal = Effect.fn("stopSessionInternal")(function* ( - session: CodexAdapterSessionContext, - ) { - if (session.stopped) { - return; - } - session.stopped = true; - sessions.delete(session.threadId); - yield* session.runtime.close.pipe(Effect.ignore); - yield* Effect.ignore(Scope.close(session.scope, Exit.void)); - yield* Fiber.interrupt(session.eventFiber).pipe(Effect.ignore); - }); - - const stopSession: CodexAdapterShape["stopSession"] = (threadId) => - Effect.gen(function* () { - const session = sessions.get(threadId); - if (!session) { - return; - } - yield* stopSessionInternal(session); - }); - - const listSessions: CodexAdapterShape["listSessions"] = () => - Effect.forEach( - Array.from(sessions.values()).filter((session) => !session.stopped), - (session) => session.runtime.getSession, - { concurrency: 1 }, - ); - - const hasSession: CodexAdapterShape["hasSession"] = (threadId) => - Effect.succeed(Boolean(sessions.get(threadId) && !sessions.get(threadId)?.stopped)); - - const stopAll: CodexAdapterShape["stopAll"] = () => - Effect.forEach(Array.from(sessions.values()), stopSessionInternal, { - concurrency: 1, - discard: true, - }).pipe(Effect.asVoid); - - yield* Effect.acquireRelease(Effect.void, () => - stopAll().pipe( - Effect.andThen(Queue.shutdown(runtimeEventQueue)), - Effect.andThen(managedNativeEventLogger?.close() ?? Effect.void), - Effect.ignore, - ), - ); - - return { - provider: PROVIDER, - capabilities: { - sessionModelSwitch: "in-session", - }, - startSession, - sendTurn, - interruptTurn, - readThread, - rollbackThread, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - stopAll, - get streamEvents() { - return Stream.fromQueue(runtimeEventQueue); - }, - } satisfies CodexAdapterShape; -}); - -// NOTE: the old `CodexAdapterLive` / `makeCodexAdapterLive` singleton Layer -// exports have been removed as part of the per-instance-driver refactor. -// `makeCodexAdapter(codexConfig, options?)` is now invoked directly by -// `CodexDriver.create()` for each configured instance; downstream consumers -// (server bootstrap, integration harness, this module's tests) will be -// migrated to the registry in a follow-up pass. diff --git a/apps/server/src/provider/Layers/CursorAdapter.test.ts b/apps/server/src/provider/Layers/CursorAdapter.test.ts deleted file mode 100644 index 9795e5a0680..00000000000 --- a/apps/server/src/provider/Layers/CursorAdapter.test.ts +++ /dev/null @@ -1,1408 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodePath from "node:path"; -import * as NodeOS from "node:os"; -import * as NodeFSP from "node:fs/promises"; -import * as NodeURL from "node:url"; - -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { assert, it } from "@effect/vitest"; -import * as Context from "effect/Context"; -import * as Deferred from "effect/Deferred"; -import * as Effect from "effect/Effect"; -import * as Fiber from "effect/Fiber"; -import * as Layer from "effect/Layer"; -import * as Schema from "effect/Schema"; -import * as Stream from "effect/Stream"; -import * as TestClock from "effect/testing/TestClock"; -import { createModelSelection } from "@t3tools/shared/model"; - -import { - ApprovalRequestId, - CursorSettings, - ProviderDriverKind, - type ProviderRuntimeEvent, - ThreadId, - ProviderInstanceId, -} from "@t3tools/contracts"; - -import { ServerConfig } from "../../config.ts"; -import { ServerSettingsService } from "../../serverSettings.ts"; -import type { CursorAdapterShape } from "../Services/CursorAdapter.ts"; -import { makeCursorAdapter } from "./CursorAdapter.ts"; -const decodeCursorSettings = Schema.decodeSync(CursorSettings); - -// Test-local service tag so the rest of the file can keep using `yield* CursorAdapter`. -class CursorAdapter extends Context.Service()( - "t3/provider/Layers/CursorAdapter.test/CursorAdapter", -) {} - -const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); -const mockAgentPath = NodePath.join(__dirname, "../../../scripts/acp-mock-agent.ts"); -const mockAgentCommand = "node"; -const mockAgentArgs = [mockAgentPath] as const; - -async function makeMockAgentWrapper( - extraEnv?: Record, - options?: { initialDelaySeconds?: number }, -) { - const dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-mock-")); - const wrapperPath = NodePath.join(dir, "fake-agent.sh"); - const envExports = Object.entries(extraEnv ?? {}) - .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) - .join("\n"); - const script = `#!/bin/sh -${envExports} -${options?.initialDelaySeconds ? `sleep ${JSON.stringify(String(options.initialDelaySeconds))}` : ""} -exec ${JSON.stringify(mockAgentCommand)} ${mockAgentArgs.map((arg) => JSON.stringify(arg)).join(" ")} "$@" -`; - await NodeFSP.writeFile(wrapperPath, script, "utf8"); - await NodeFSP.chmod(wrapperPath, 0o755); - return wrapperPath; -} - -async function makeProbeWrapper( - requestLogPath: string, - argvLogPath: string, - extraEnv?: Record, -) { - const dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-probe-")); - const wrapperPath = NodePath.join(dir, "fake-agent.sh"); - const envExports = Object.entries(extraEnv ?? {}) - .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) - .join("\n"); - const script = `#!/bin/sh -printf '%s\t' "$@" >> ${JSON.stringify(argvLogPath)} -printf '\n' >> ${JSON.stringify(argvLogPath)} -export T3_ACP_REQUEST_LOG_PATH=${JSON.stringify(requestLogPath)} -${envExports} -exec ${JSON.stringify(mockAgentCommand)} ${mockAgentArgs.map((arg) => JSON.stringify(arg)).join(" ")} "$@" -`; - await NodeFSP.writeFile(wrapperPath, script, "utf8"); - await NodeFSP.chmod(wrapperPath, 0o755); - return wrapperPath; -} - -async function readArgvLog(filePath: string) { - const raw = await NodeFSP.readFile(filePath, "utf8"); - return raw - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.length > 0) - .map((line) => line.split("\t").filter((token) => token.length > 0)); -} - -async function readJsonLines(filePath: string) { - const raw = await NodeFSP.readFile(filePath, "utf8"); - return raw - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.length > 0) - .map((line) => JSON.parse(line) as Record); -} - -async function waitForFileContent(filePath: string, attempts = 40) { - for (let attempt = 0; attempt < attempts; attempt += 1) { - try { - const raw = await NodeFSP.readFile(filePath, "utf8"); - if (raw.trim().length > 0) { - return raw; - } - } catch {} - await Effect.runPromise(Effect.yieldNow); - } - throw new Error(`Timed out waiting for file content at ${filePath}`); -} - -// Tests mutate `ServerSettingsService` mid-flight (e.g. setting -// `providers.cursor.binaryPath` to a mock ACP wrapper). The adapter -// captures `cursorSettings` once at construction, so without a resolver -// the mutation is invisible — sessions would spawn the constructor's -// (empty) binary path. Wiring `resolveSettings` through -// `ServerSettingsService.getSettings` makes each session read the latest -// snapshot, matching the old "always read live" behavior that these -// tests assumed. -const makeResolveCursorSettings = Effect.gen(function* () { - const serverSettings = yield* ServerSettingsService; - return yield* Effect.succeed( - serverSettings.getSettings.pipe( - Effect.map((snapshot) => snapshot.providers.cursor), - Effect.orDie, - ), - ); -}); - -const cursorAdapterTestLayer = it.layer( - Layer.effect( - CursorAdapter, - Effect.gen(function* () { - const cursorConfig = decodeCursorSettings({}); - const resolveSettings = yield* makeResolveCursorSettings; - return yield* makeCursorAdapter(cursorConfig, { resolveSettings }); - }), - ).pipe( - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3code-cursor-adapter-test-", - }), - ), - Layer.provideMerge(NodeServices.layer), - ), -); - -cursorAdapterTestLayer("CursorAdapterLive", (it) => { - it.effect("starts a session and maps mock ACP prompt flow to runtime events", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const settings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-mock-thread"); - - const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper()); - yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - assert.equal(session.provider, "cursor"); - assert.deepStrictEqual(session.resumeCursor, { - schemaVersion: 1, - sessionId: "mock-session-1", - }); - - yield* adapter.sendTurn({ - threadId, - input: "hello mock", - attachments: [], - }); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const types = runtimeEvents.map((e) => e.type); - - for (const t of [ - "session.started", - "session.state.changed", - "thread.started", - "turn.started", - "turn.plan.updated", - "item.started", - "content.delta", - "item.completed", - "turn.completed", - ] as const) { - assert.include(types, t); - } - - const assistantStarted = runtimeEvents.find( - (event) => event.type === "item.started" && event.payload.itemType === "assistant_message", - ); - assert.isDefined(assistantStarted); - - const delta = runtimeEvents.find((e) => e.type === "content.delta"); - assert.isDefined(delta); - if (delta?.type === "content.delta") { - assert.equal(delta.payload.delta, "hello from mock"); - assert.match(String(delta.itemId), /^assistant:mock-session-1:segment:0$/); - } - - const assistantCompleted = runtimeEvents.find( - (event) => - event.type === "item.completed" && event.payload.itemType === "assistant_message", - ); - assert.isDefined(assistantCompleted); - - const planUpdate = runtimeEvents.find((event) => event.type === "turn.plan.updated"); - assert.isDefined(planUpdate); - if (planUpdate?.type === "turn.plan.updated") { - assert.deepStrictEqual(planUpdate.payload.plan, [ - { step: "Inspect mock ACP state", status: "completed" }, - { step: "Implement the requested change", status: "inProgress" }, - ]); - } - - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("steers a running turn instead of opening a new one on mid-turn sendTurn", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const settings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-steer-thread"); - - // Keep the first prompt in flight long enough for the steer to land. - const wrapperPath = yield* Effect.promise(() => - makeMockAgentWrapper({ T3_ACP_PROMPT_DELAY_MS: "1500" }), - ); - yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - const runtimeEventsFiber = yield* adapter.streamEvents.pipe( - Stream.filter((event) => event.threadId === threadId), - Stream.takeUntil((event) => event.type === "turn.completed"), - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - const firstTurnFiber = yield* adapter - .sendTurn({ - threadId, - input: "run 5 commands", - attachments: [], - }) - .pipe(Effect.forkChild); - - // Poll until the first prompt is in flight — sendTurn binds the active - // turn id before prompting. The mock agent runs on the real clock, so - // each TestClock.adjust just provides the scheduler hops for its stdio - // responses to land. - yield* Effect.gen(function* () { - for (let attempt = 0; attempt < 200; attempt += 1) { - const sessions = yield* adapter.listSessions(); - const session = sessions.find((entry) => entry.threadId === threadId); - if (session?.activeTurnId !== undefined) { - return; - } - yield* TestClock.adjust("10 millis"); - } - throw new Error("Timed out waiting for the first prompt to be in flight."); - }); - - // Steer: a second sendTurn while the first prompt is still in flight - // continues the same turn. - const steeredTurn = yield* adapter.sendTurn({ - threadId, - input: "actually run 15", - attachments: [], - }); - const firstTurn = yield* Fiber.join(firstTurnFiber); - assert.equal(String(steeredTurn.turnId), String(firstTurn.turnId)); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - const turnStartedEvents = runtimeEvents.filter((event) => event.type === "turn.started"); - const turnCompletedEvents = runtimeEvents.filter((event) => event.type === "turn.completed"); - - // One turn boundary for the whole run: the superseded first prompt - // resolving must not settle the merged turn. - assert.equal(turnStartedEvents.length, 1); - assert.equal(String(turnStartedEvents[0]?.turnId), String(firstTurn.turnId)); - assert.equal(turnCompletedEvents.length, 1); - assert.equal(String(turnCompletedEvents[0]?.turnId), String(firstTurn.turnId)); - - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("closes the ACP child process when a session stops", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const settings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-stop-session-close"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-adapter-exit-log-")), - ); - const exitLogPath = NodePath.join(tempDir, "exit.log"); - - const wrapperPath = yield* Effect.promise(() => - makeMockAgentWrapper({ - T3_ACP_EXIT_LOG_PATH: exitLogPath, - }), - ); - yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - yield* adapter.stopSession(threadId); - - const exitLog = yield* Effect.promise(() => waitForFileContent(exitLogPath)); - assert.include(exitLog, "SIGTERM"); - }), - ); - - it.effect( - "serializes concurrent startSession calls for the same thread and closes the replaced ACP session", - () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const settings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-concurrent-start-session"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-adapter-concurrent-exit-log-")), - ); - const exitLogPath = NodePath.join(tempDir, "exit.log"); - - const wrapperPath = yield* Effect.promise(() => - makeMockAgentWrapper( - { - T3_ACP_EXIT_LOG_PATH: exitLogPath, - }, - { initialDelaySeconds: 0.2 }, - ), - ); - yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - const [firstSession, secondSession] = yield* Effect.all( - [ - adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }), - adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }), - ], - { concurrency: "unbounded" }, - ); - - assert.equal(firstSession.threadId, threadId); - assert.equal(secondSession.threadId, threadId); - - yield* adapter.stopSession(threadId); - - const exitLog = yield* Effect.promise(() => waitForFileContent(exitLogPath)); - assert.equal(exitLog.match(/SIGTERM/g)?.length ?? 0, 2); - }), - ); - - it.effect("rejects startSession when provider mismatches", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const result = yield* adapter - .startSession({ - threadId: ThreadId.make("bad-provider"), - provider: ProviderDriverKind.make("codex"), - cwd: process.cwd(), - runtimeMode: "full-access", - }) - .pipe(Effect.result); - - assert.equal(result._tag, "Failure"); - }), - ); - - it.effect("maps app plan mode onto the ACP plan session mode", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-plan-mode-probe"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-")), - ); - const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); - const argvLogPath = NodePath.join(tempDir, "argv.txt"); - yield* Effect.promise(() => NodeFSP.writeFile(requestLogPath, "", "utf8")); - const wrapperPath = yield* Effect.promise(() => - makeProbeWrapper(requestLogPath, argvLogPath), - ); - yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "composer-2" }, - }); - - yield* adapter.sendTurn({ - threadId, - input: "plan this change", - attachments: [], - interactionMode: "plan", - }); - yield* adapter.stopSession(threadId); - - const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); - const modeRequest = requests - .toReversed() - .find( - (entry) => - entry.method === "session/set_mode" || - (entry.method === "session/set_config_option" && - (entry.params as Record | undefined)?.configId === "mode"), - ); - assert.isDefined(modeRequest); - assert.equal( - (modeRequest?.params as Record | undefined)?.sessionId, - "mock-session-1", - ); - assert.include( - ["architect", "plan"], - String( - (modeRequest?.params as Record | undefined)?.modeId ?? - (modeRequest?.params as Record | undefined)?.value, - ), - ); - }), - ); - - it.effect( - "applies initial model and mode configuration during startSession and skips repeating it on first send", - () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-initial-config-probe"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-")), - ); - const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); - const argvLogPath = NodePath.join(tempDir, "argv.txt"); - yield* Effect.promise(() => NodeFSP.writeFile(requestLogPath, "", "utf8")); - const wrapperPath = yield* Effect.promise(() => - makeProbeWrapper(requestLogPath, argvLogPath), - ); - yield* serverSettings.updateSettings({ - providers: { cursor: { binaryPath: wrapperPath } }, - }); - - const modelSelection = createModelSelection(ProviderInstanceId.make("cursor"), "gpt-5.4", [ - { id: "reasoning", value: "xhigh" }, - { id: "contextWindow", value: "1m" }, - { id: "fastMode", value: true }, - ]); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection, - }); - - yield* Effect.promise(() => waitForFileContent(requestLogPath)); - - const requestsAfterStart = yield* Effect.promise(() => readJsonLines(requestLogPath)); - const configIdsAfterStart = requestsAfterStart.flatMap((entry) => - entry.method === "session/set_config_option" && - typeof (entry.params as Record | undefined)?.configId === "string" - ? [String((entry.params as Record).configId)] - : [], - ); - assert.deepStrictEqual(configIdsAfterStart, [ - "model", - "reasoning", - "context", - "fast", - "mode", - ]); - - yield* adapter.sendTurn({ - threadId, - input: "hello mock", - attachments: [], - modelSelection, - interactionMode: "default", - }); - yield* adapter.stopSession(threadId); - - const finalRequests = yield* Effect.promise(() => readJsonLines(requestLogPath)); - const finalConfigIds = finalRequests.flatMap((entry) => - entry.method === "session/set_config_option" && - typeof (entry.params as Record | undefined)?.configId === "string" - ? [String((entry.params as Record).configId)] - : [], - ); - assert.deepStrictEqual(finalConfigIds, ["model", "reasoning", "context", "fast", "mode"]); - assert.equal(finalRequests.filter((entry) => entry.method === "session/prompt").length, 1); - }), - ); - - it.effect( - "streams ACP tool calls and approvals on the active turn in approval-required mode", - () => - Effect.gen(function* () { - const previousEmitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS; - process.env.T3_ACP_EMIT_TOOL_CALLS = "1"; - - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-tool-call-probe"); - const runtimeEvents: Array = []; - const settledEventTypes = new Set(); - const settledEventsReady = yield* Deferred.make(); - - const wrapperPath = yield* Effect.promise(() => - makeMockAgentWrapper({ T3_ACP_EMIT_TOOL_CALLS: "1" }), - ); - yield* serverSettings.updateSettings({ - providers: { cursor: { binaryPath: wrapperPath } }, - }); - - yield* Stream.runForEach(adapter.streamEvents, (event) => - Effect.gen(function* () { - runtimeEvents.push(event); - if (String(event.threadId) !== String(threadId)) { - return; - } - if (event.type === "request.opened" && event.requestId) { - yield* adapter.respondToRequest( - threadId, - ApprovalRequestId.make(String(event.requestId)), - "accept", - ); - } - if ( - event.type === "turn.completed" || - (event.type === "item.completed" && event.payload.itemType === "command_execution") || - event.type === "content.delta" - ) { - settledEventTypes.add(event.type); - if (settledEventTypes.size === 3) { - yield* Deferred.succeed(settledEventsReady, undefined).pipe(Effect.orDie); - } - } - }), - ).pipe(Effect.forkChild); - - const program = Effect.gen(function* () { - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "approval-required", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - const turn = yield* adapter.sendTurn({ - threadId, - input: "run a tool call", - attachments: [], - }); - yield* Deferred.await(settledEventsReady); - - const threadEvents = runtimeEvents.filter( - (event) => String(event.threadId) === String(threadId), - ); - assert.includeMembers( - threadEvents.map((event) => event.type), - [ - "session.started", - "session.state.changed", - "thread.started", - "turn.started", - "request.opened", - "request.resolved", - "item.updated", - "item.completed", - "content.delta", - "turn.completed", - ], - ); - - const turnEvents = threadEvents.filter( - (event) => String(event.turnId) === String(turn.turnId), - ); - const toolUpdates = turnEvents.filter((event) => event.type === "item.updated"); - // ACP updates can arrive either as distinct pending + in-progress events - // or as a single coalesced in-progress update before approval resolves. - assert.isAtLeast(toolUpdates.length, 1); - for (const toolUpdate of toolUpdates) { - if (toolUpdate.type !== "item.updated") { - continue; - } - assert.equal(toolUpdate.payload.itemType, "command_execution"); - assert.equal(toolUpdate.payload.status, "inProgress"); - assert.equal(toolUpdate.payload.detail, "cat server/package.json"); - assert.equal(String(toolUpdate.itemId), "tool-call-1"); - } - - const requestOpened = turnEvents.find((event) => event.type === "request.opened"); - assert.isDefined(requestOpened); - if (requestOpened?.type === "request.opened") { - assert.equal(String(requestOpened.turnId), String(turn.turnId)); - assert.equal(requestOpened.payload.requestType, "exec_command_approval"); - assert.equal(requestOpened.payload.detail, "cat server/package.json"); - } - - const requestResolved = turnEvents.find((event) => event.type === "request.resolved"); - assert.isDefined(requestResolved); - if (requestResolved?.type === "request.resolved") { - assert.equal(String(requestResolved.turnId), String(turn.turnId)); - assert.equal(requestResolved.payload.requestType, "exec_command_approval"); - assert.equal(requestResolved.payload.decision, "accept"); - } - - const toolCompleted = turnEvents.find( - (event) => - event.type === "item.completed" && event.payload.itemType === "command_execution", - ); - assert.isDefined(toolCompleted); - if (toolCompleted?.type === "item.completed") { - assert.equal(String(toolCompleted.turnId), String(turn.turnId)); - assert.equal(toolCompleted.payload.itemType, "command_execution"); - assert.equal(toolCompleted.payload.status, "completed"); - assert.equal(toolCompleted.payload.detail, "cat server/package.json"); - assert.equal(String(toolCompleted.itemId), "tool-call-1"); - } - - const contentDelta = turnEvents.find((event) => event.type === "content.delta"); - assert.isDefined(contentDelta); - if (contentDelta?.type === "content.delta") { - assert.equal(String(contentDelta.turnId), String(turn.turnId)); - assert.equal(contentDelta.payload.delta, "hello from mock"); - assert.equal(String(contentDelta.itemId), "assistant:mock-session-1:segment:0"); - } - }); - - yield* program.pipe( - Effect.ensuring( - Effect.sync(() => { - if (previousEmitToolCalls === undefined) { - delete process.env.T3_ACP_EMIT_TOOL_CALLS; - } else { - process.env.T3_ACP_EMIT_TOOL_CALLS = previousEmitToolCalls; - } - }), - ), - ); - }).pipe( - Effect.provide( - Layer.effect( - CursorAdapter, - Effect.gen(function* () { - const cursorConfig = decodeCursorSettings({}); - const resolveSettings = yield* makeResolveCursorSettings; - return yield* makeCursorAdapter(cursorConfig, { resolveSettings }); - }), - ).pipe( - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3code-cursor-adapter-test-", - }), - ), - Layer.provideMerge(NodeServices.layer), - ), - ), - ), - ); - - it.effect( - "auto-approves ACP tool permissions in full-access mode without approval runtime events", - () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-full-access-auto-approve"); - const runtimeEvents: Array = []; - const settledEventTypes = new Set(); - const settledEventsReady = yield* Deferred.make(); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-")), - ); - const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); - const argvLogPath = NodePath.join(tempDir, "argv.txt"); - yield* Effect.promise(() => NodeFSP.writeFile(requestLogPath, "", "utf8")); - const wrapperPath = yield* Effect.promise(() => - makeProbeWrapper(requestLogPath, argvLogPath, { T3_ACP_EMIT_TOOL_CALLS: "1" }), - ); - yield* serverSettings.updateSettings({ - providers: { cursor: { binaryPath: wrapperPath } }, - }); - - const runtimeEventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => - Effect.gen(function* () { - runtimeEvents.push(event); - if (String(event.threadId) !== String(threadId)) { - return; - } - if ( - event.type === "turn.completed" || - (event.type === "item.completed" && event.payload.itemType === "command_execution") || - event.type === "content.delta" - ) { - settledEventTypes.add(event.type); - if (settledEventTypes.size === 3) { - yield* Deferred.succeed(settledEventsReady, undefined).pipe(Effect.orDie); - } - } - }), - ).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - const turn = yield* adapter.sendTurn({ - threadId, - input: "run a tool call", - attachments: [], - }); - - yield* Deferred.await(settledEventsReady); - yield* Fiber.interrupt(runtimeEventsFiber); - - const turnEvents = runtimeEvents.filter( - (event) => - String(event.threadId) === String(threadId) && - String(event.turnId) === String(turn.turnId), - ); - assert.notInclude( - turnEvents.map((event) => event.type), - "request.opened", - ); - assert.notInclude( - turnEvents.map((event) => event.type), - "request.resolved", - ); - assert.includeMembers( - turnEvents.map((event) => event.type), - ["item.updated", "item.completed", "content.delta", "turn.completed"], - ); - - const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); - const permissionResponse = requests.find( - (entry) => - !("method" in entry) && - typeof entry.result === "object" && - entry.result !== null && - "outcome" in entry.result && - typeof entry.result.outcome === "object" && - entry.result.outcome !== null && - "outcome" in entry.result.outcome && - entry.result.outcome.outcome === "selected" && - "optionId" in entry.result.outcome && - entry.result.outcome.optionId === "allow-always", - ); - assert.isDefined(permissionResponse); - - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("segments assistant messages around ACP tool activity in full-access mode", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-assistant-tool-segmentation"); - const runtimeEvents: Array = []; - const settledEventTypes = new Set(); - const settledEventsReady = yield* Deferred.make(); - - const wrapperPath = yield* Effect.promise(() => - makeMockAgentWrapper({ T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS: "1" }), - ); - yield* serverSettings.updateSettings({ - providers: { cursor: { binaryPath: wrapperPath } }, - }); - - const runtimeEventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => - Effect.gen(function* () { - runtimeEvents.push(event); - if (String(event.threadId) !== String(threadId)) { - return; - } - if ( - event.type === "content.delta" || - (event.type === "item.completed" && event.payload.itemType === "command_execution") || - event.type === "turn.completed" - ) { - if (event.type === "content.delta") { - settledEventTypes.add(`delta:${event.payload.delta}`); - } else { - settledEventTypes.add(event.type); - } - if ( - settledEventTypes.has("delta:before tool") && - settledEventTypes.has("delta:after tool") && - settledEventTypes.has("item.completed") && - settledEventTypes.has("turn.completed") - ) { - yield* Deferred.succeed(settledEventsReady, undefined).pipe(Effect.orDie); - } - } - }), - ).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - const turn = yield* adapter.sendTurn({ - threadId, - input: "run an interleaved tool call", - attachments: [], - }); - - yield* Deferred.await(settledEventsReady); - yield* Fiber.interrupt(runtimeEventsFiber); - - const turnEvents = runtimeEvents.filter( - (event) => - String(event.threadId) === String(threadId) && - String(event.turnId) === String(turn.turnId), - ); - const firstAssistantStartIndex = turnEvents.findIndex( - (event) => event.type === "item.started" && event.payload.itemType === "assistant_message", - ); - const firstAssistantDeltaIndex = turnEvents.findIndex( - (event) => event.type === "content.delta" && event.payload.delta === "before tool", - ); - const assistantBoundaryIndex = turnEvents.findIndex( - (event) => - event.type === "item.completed" && event.payload.itemType === "assistant_message", - ); - const toolUpdateIndex = turnEvents.findIndex( - (event) => event.type === "item.updated" && event.payload.itemType === "command_execution", - ); - const toolCompletedIndex = turnEvents.findIndex( - (event) => - event.type === "item.completed" && event.payload.itemType === "command_execution", - ); - const secondAssistantStartIndex = turnEvents.findIndex( - (event, index) => - index > toolCompletedIndex && - event.type === "item.started" && - event.payload.itemType === "assistant_message", - ); - const secondAssistantDeltaIndex = turnEvents.findIndex( - (event) => event.type === "content.delta" && event.payload.delta === "after tool", - ); - - assert.isAtLeast(firstAssistantStartIndex, 0); - assert.isAtLeast(firstAssistantDeltaIndex, 0); - assert.isAtLeast(assistantBoundaryIndex, 0); - assert.isAtLeast(toolUpdateIndex, 0); - assert.isAtLeast(toolCompletedIndex, 0); - assert.isAtLeast(secondAssistantStartIndex, 0); - assert.isAtLeast(secondAssistantDeltaIndex, 0); - assert.isBelow(firstAssistantStartIndex, firstAssistantDeltaIndex); - assert.isBelow(firstAssistantDeltaIndex, assistantBoundaryIndex); - assert.isBelow(assistantBoundaryIndex, toolUpdateIndex); - assert.isBelow(toolUpdateIndex, toolCompletedIndex); - assert.isBelow(toolCompletedIndex, secondAssistantStartIndex); - assert.isBelow(secondAssistantStartIndex, secondAssistantDeltaIndex); - - const assistantStarts = turnEvents.filter( - (event) => event.type === "item.started" && event.payload.itemType === "assistant_message", - ); - const assistantDeltas = turnEvents.filter((event) => event.type === "content.delta"); - assert.lengthOf(assistantStarts, 2); - assert.lengthOf(assistantDeltas, 2); - if ( - assistantStarts[0]?.type === "item.started" && - assistantStarts[1]?.type === "item.started" && - assistantDeltas[0]?.type === "content.delta" && - assistantDeltas[1]?.type === "content.delta" - ) { - assert.notEqual(String(assistantStarts[0].itemId), String(assistantStarts[1].itemId)); - assert.equal(String(assistantDeltas[0].itemId), String(assistantStarts[0].itemId)); - assert.equal(String(assistantDeltas[1].itemId), String(assistantStarts[1].itemId)); - } - - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("cancels pending ACP approvals and marks the turn cancelled when interrupted", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-cancel-probe"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-")), - ); - const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); - const argvLogPath = NodePath.join(tempDir, "argv.txt"); - yield* Effect.promise(() => NodeFSP.writeFile(requestLogPath, "", "utf8")); - const wrapperPath = yield* Effect.promise(() => - makeProbeWrapper(requestLogPath, argvLogPath, { T3_ACP_EMIT_TOOL_CALLS: "1" }), - ); - yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - const requestResolvedReady = yield* Deferred.make(); - const turnCompletedReady = yield* Deferred.make(); - let interrupted = false; - - const runtimeEventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => - Effect.gen(function* () { - if (String(event.threadId) !== String(threadId)) { - return; - } - if (event.type === "request.opened" && !interrupted) { - interrupted = true; - yield* adapter.interruptTurn(threadId); - return; - } - if (event.type === "request.resolved") { - yield* Deferred.succeed(requestResolvedReady, event).pipe(Effect.ignore); - return; - } - if (event.type === "turn.completed") { - yield* Deferred.succeed(turnCompletedReady, event).pipe(Effect.ignore); - } - }), - ).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "approval-required", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - const sendTurnFiber = yield* adapter - .sendTurn({ - threadId, - input: "cancel this turn", - attachments: [], - }) - .pipe(Effect.forkChild); - - const requestResolved = yield* Deferred.await(requestResolvedReady); - const turnCompleted = yield* Deferred.await(turnCompletedReady); - yield* Fiber.join(sendTurnFiber); - yield* Fiber.interrupt(runtimeEventsFiber); - - assert.equal(requestResolved.type, "request.resolved"); - if (requestResolved.type === "request.resolved") { - assert.equal(requestResolved.payload.decision, "cancel"); - } - - assert.equal(turnCompleted.type, "turn.completed"); - if (turnCompleted.type === "turn.completed") { - assert.equal(turnCompleted.payload.state, "cancelled"); - assert.equal(turnCompleted.payload.stopReason, "cancelled"); - } - - const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); - assert.isTrue(requests.some((entry) => entry.method === "session/cancel")); - assert.isTrue( - requests.some( - (entry) => - !("method" in entry) && - typeof entry.result === "object" && - entry.result !== null && - "outcome" in entry.result && - typeof entry.result.outcome === "object" && - entry.result.outcome !== null && - "outcome" in entry.result.outcome && - entry.result.outcome.outcome === "cancelled", - ), - ); - - yield* adapter.stopSession(threadId); - }), - ); - it.effect("stopping a session settles pending approval waits", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-stop-pending-approval"); - const approvalRequested = yield* Deferred.make(); - - const wrapperPath = yield* Effect.promise(() => - makeMockAgentWrapper({ T3_ACP_EMIT_TOOL_CALLS: "1" }), - ); - yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - yield* Stream.runForEach(adapter.streamEvents, (event) => { - if (String(event.threadId) !== String(threadId) || event.type !== "request.opened") { - return Effect.void; - } - return Deferred.succeed(approvalRequested, undefined).pipe(Effect.ignore); - }).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "approval-required", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - const sendTurnFiber = yield* adapter - .sendTurn({ - threadId, - input: "run a tool call and then stop", - attachments: [], - }) - .pipe(Effect.forkChild); - - yield* Deferred.await(approvalRequested); - yield* adapter.stopSession(threadId); - yield* Fiber.await(sendTurnFiber); - - assert.equal(yield* adapter.hasSession(threadId), false); - }), - ); - - it.effect("stopping a session settles pending user-input waits", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-stop-pending-user-input"); - const userInputRequested = yield* Deferred.make(); - - const wrapperPath = yield* Effect.promise(() => - makeMockAgentWrapper({ T3_ACP_EMIT_ASK_QUESTION: "1" }), - ); - yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - yield* Stream.runForEach(adapter.streamEvents, (event) => { - if (String(event.threadId) !== String(threadId) || event.type !== "user-input.requested") { - return Effect.void; - } - return Deferred.succeed(userInputRequested, undefined).pipe(Effect.ignore); - }).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - const sendTurnFiber = yield* adapter - .sendTurn({ - threadId, - input: "ask me a question and then stop", - attachments: [], - }) - .pipe(Effect.forkChild); - - yield* Deferred.await(userInputRequested); - yield* adapter.stopSession(threadId); - yield* Fiber.await(sendTurnFiber); - - assert.equal(yield* adapter.hasSession(threadId), false); - }), - ); - - it.effect("interrupting a session settles pending user-input waits", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-interrupt-pending-user-input"); - const userInputRequested = yield* Deferred.make(); - - const wrapperPath = yield* Effect.promise(() => - makeMockAgentWrapper({ T3_ACP_EMIT_ASK_QUESTION: "1" }), - ); - yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - yield* Stream.runForEach(adapter.streamEvents, (event) => { - if (String(event.threadId) !== String(threadId) || event.type !== "user-input.requested") { - return Effect.void; - } - return Deferred.succeed(userInputRequested, undefined).pipe(Effect.ignore); - }).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - const sendTurnFiber = yield* adapter - .sendTurn({ - threadId, - input: "ask me a question and then interrupt", - attachments: [], - }) - .pipe(Effect.forkChild); - - yield* Deferred.await(userInputRequested); - yield* adapter.interruptTurn(threadId); - yield* Fiber.await(sendTurnFiber); - - assert.equal(yield* adapter.hasSession(threadId), true); - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("broadcasts runtime events to multiple stream consumers", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const settings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-runtime-event-broadcast"); - - const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper()); - yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - const firstConsumer = yield* Stream.take(adapter.streamEvents, 3).pipe( - Stream.runCollect, - Effect.forkChild, - ); - const secondConsumer = yield* Stream.take(adapter.streamEvents, 3).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" }, - }); - - const firstEvents = Array.from(yield* Fiber.join(firstConsumer)); - const secondEvents = Array.from(yield* Fiber.join(secondConsumer)); - - assert.deepStrictEqual( - firstEvents.map((event) => event.type), - ["session.started", "session.state.changed", "thread.started"], - ); - assert.deepStrictEqual( - secondEvents.map((event) => event.type), - ["session.started", "session.state.changed", "thread.started"], - ); - - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("switches model in-session via session/set_config_option", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-model-switch"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-")), - ); - const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); - const argvLogPath = NodePath.join(tempDir, "argv.txt"); - yield* Effect.promise(() => NodeFSP.writeFile(requestLogPath, "", "utf8")); - const wrapperPath = yield* Effect.promise(() => - makeProbeWrapper(requestLogPath, argvLogPath), - ); - yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "composer-2" }, - }); - - yield* adapter.sendTurn({ - threadId, - input: "first turn", - attachments: [], - }); - - yield* adapter.sendTurn({ - threadId, - input: "second turn after switching model", - attachments: [], - modelSelection: createModelSelection(ProviderInstanceId.make("cursor"), "composer-2", [ - { id: "fastMode", value: true }, - ]), - }); - - const argvRuns = yield* Effect.promise(() => readArgvLog(argvLogPath)); - assert.lengthOf(argvRuns, 1, "session should not restart — only one spawn"); - assert.deepStrictEqual(argvRuns[0], ["acp"]); - - const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); - const setConfigRequests = requests.filter( - (entry) => - entry.method === "session/set_config_option" && - (entry.params as Record | undefined)?.configId === "model", - ); - assert.isAbove(setConfigRequests.length, 0, "should call session/set_config_option"); - assert.equal((setConfigRequests[0]?.params as Record)?.value, "composer-2"); - - const fastConfigRequests = requests.filter( - (entry) => - entry.method === "session/set_config_option" && - (entry.params as Record | undefined)?.configId === "fast", - ); - assert.isAbove(fastConfigRequests.length, 0, "should apply fast mode as a separate config"); - const lastFastConfig = fastConfigRequests[fastConfigRequests.length - 1]; - assert.equal((lastFastConfig?.params as Record)?.value, "true"); - - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("clears prior fast mode in-session when the next turn sets fastMode: false", () => - Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-fast-mode-reset"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-")), - ); - const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); - const argvLogPath = NodePath.join(tempDir, "argv.txt"); - yield* Effect.promise(() => NodeFSP.writeFile(requestLogPath, "", "utf8")); - const wrapperPath = yield* Effect.promise(() => - makeProbeWrapper(requestLogPath, argvLogPath), - ); - yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "composer-2" }, - }); - - yield* adapter.sendTurn({ - threadId, - input: "first turn with fast mode", - attachments: [], - modelSelection: createModelSelection(ProviderInstanceId.make("cursor"), "composer-2", [ - { id: "fastMode", value: true }, - ]), - }); - - yield* adapter.sendTurn({ - threadId, - input: "second turn without fast mode", - attachments: [], - modelSelection: createModelSelection(ProviderInstanceId.make("cursor"), "composer-2", [ - { id: "fastMode", value: false }, - ]), - }); - - const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); - const fastConfigRequests = requests.filter( - (entry) => - entry.method === "session/set_config_option" && - (entry.params as Record | undefined)?.configId === "fast", - ); - assert.isAtLeast(fastConfigRequests.length, 2, "should set fast mode on and then off"); - - const lastFastConfig = fastConfigRequests[fastConfigRequests.length - 1]; - assert.equal((lastFastConfig?.params as Record)?.value, "false"); - - yield* adapter.stopSession(threadId); - }), - ); - - it.effect( - "applies fast mode on the first turn when modelSelection uses a non-default instance id", - () => { - const customInstanceId = ProviderInstanceId.make("cursor_secondary"); - // Custom-instance cases can't share the suite-level `CursorAdapter` - // layer because that one binds `instanceId: "cursor"`. We build a - // fresh layer graph — including a fresh `ServerSettingsService` — so - // mid-test `updateSettings` calls target the same service instance the - // adapter's `resolveSettings` reads from, and so the outer - // `yield* ServerSettingsService` sees the same snapshot as well. - const customAdapterLayer = Layer.effect( - CursorAdapter, - Effect.gen(function* () { - const cursorConfig = decodeCursorSettings({}); - const resolveSettings = yield* makeResolveCursorSettings; - return yield* makeCursorAdapter(cursorConfig, { - instanceId: customInstanceId, - resolveSettings, - }); - }), - ).pipe( - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3code-cursor-adapter-custom-instance-", - }), - ), - Layer.provideMerge(NodeServices.layer), - ); - - return Effect.gen(function* () { - const adapter = yield* CursorAdapter; - const serverSettings = yield* ServerSettingsService; - const threadId = ThreadId.make("cursor-fast-mode-custom-instance"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-")), - ); - const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); - const argvLogPath = NodePath.join(tempDir, "argv.txt"); - yield* Effect.promise(() => NodeFSP.writeFile(requestLogPath, "", "utf8")); - const wrapperPath = yield* Effect.promise(() => - makeProbeWrapper(requestLogPath, argvLogPath), - ); - yield* serverSettings.updateSettings({ - providers: { cursor: { binaryPath: wrapperPath } }, - }); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { - instanceId: customInstanceId, - model: "composer-2", - }, - }); - - yield* adapter.sendTurn({ - threadId, - input: "first turn with fast mode", - attachments: [], - modelSelection: { - ...createModelSelection(ProviderInstanceId.make("cursor"), "composer-2", [ - { id: "fastMode", value: true }, - ]), - instanceId: customInstanceId, - }, - }); - - const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); - const fastConfigRequests = requests.filter( - (entry) => - entry.method === "session/set_config_option" && - (entry.params as Record | undefined)?.configId === "fast", - ); - assert.isAbove( - fastConfigRequests.length, - 0, - "fast mode should apply when instance id matches the adapter binding", - ); - const lastFastConfig = fastConfigRequests[fastConfigRequests.length - 1]; - assert.equal((lastFastConfig?.params as Record)?.value, "true"); - - yield* adapter.stopSession(threadId); - }).pipe(Effect.provide(customAdapterLayer)); - }, - ); -}); diff --git a/apps/server/src/provider/Layers/CursorAdapter.ts b/apps/server/src/provider/Layers/CursorAdapter.ts deleted file mode 100644 index 9760b2f81fb..00000000000 --- a/apps/server/src/provider/Layers/CursorAdapter.ts +++ /dev/null @@ -1,1178 +0,0 @@ -/** - * CursorAdapterLive — Cursor CLI (`agent acp`) via ACP. - * - * @module CursorAdapterLive - */ - -import { - ApprovalRequestId, - type CursorSettings, - type ProviderOptionSelection, - EventId, - type ProviderApprovalDecision, - type ProviderInteractionMode, - type ProviderRuntimeEvent, - type ProviderSession, - type ProviderUserInputAnswers, - ProviderDriverKind, - ProviderInstanceId, - RuntimeRequestId, - type RuntimeMode, - type ThreadId, - TurnId, -} from "@t3tools/contracts"; -import * as DateTime from "effect/DateTime"; -import * as Crypto from "effect/Crypto"; -import * as Deferred from "effect/Deferred"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Fiber from "effect/Fiber"; -import * as FileSystem from "effect/FileSystem"; -import * as Option from "effect/Option"; -import * as Path from "effect/Path"; -import * as PubSub from "effect/PubSub"; -import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; -import * as Semaphore from "effect/Semaphore"; -import * as Stream from "effect/Stream"; -import * as SynchronizedRef from "effect/SynchronizedRef"; -import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; -import * as EffectAcpErrors from "effect-acp/errors"; -import type * as EffectAcpSchema from "effect-acp/schema"; - -import { resolveAttachmentPath } from "../../attachmentStore.ts"; -import { ServerConfig } from "../../config.ts"; -import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; -import { - ProviderAdapterProcessError, - ProviderAdapterRequestError, - ProviderAdapterSessionNotFoundError, - ProviderAdapterValidationError, -} from "../Errors.ts"; -import { acpPermissionOutcome, mapAcpToAdapterError } from "../acp/AcpAdapterSupport.ts"; -import type * as AcpSessionRuntime from "../acp/AcpSessionRuntime.ts"; -import { - makeAcpAssistantItemEvent, - makeAcpContentDeltaEvent, - makeAcpPlanUpdatedEvent, - makeAcpRequestOpenedEvent, - makeAcpRequestResolvedEvent, - makeAcpToolCallEvent, -} from "../acp/AcpCoreRuntimeEvents.ts"; -import { - type AcpSessionMode, - type AcpSessionModeState, - parsePermissionRequest, -} from "../acp/AcpRuntimeModel.ts"; -import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts"; -import { applyCursorAcpModelSelection, makeCursorAcpRuntime } from "../acp/CursorAcpSupport.ts"; -import { - CursorAskQuestionRequest, - CursorCreatePlanRequest, - CursorUpdateTodosRequest, - extractAskQuestions, - extractPlanMarkdown, - extractTodosAsPlan, -} from "../acp/CursorAcpExtension.ts"; -import { type CursorAdapterShape } from "../Services/CursorAdapter.ts"; -import { resolveCursorAcpBaseModelId } from "./CursorProvider.ts"; -import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; -const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); - -const PROVIDER = ProviderDriverKind.make("cursor"); -const CURSOR_RESUME_VERSION = 1 as const; -const ACP_PLAN_MODE_ALIASES = ["plan", "architect"]; -const ACP_IMPLEMENT_MODE_ALIASES = ["code", "agent", "default", "chat", "implement"]; -const ACP_APPROVAL_MODE_ALIASES = ["ask"]; - -function encodeJsonStringForDiagnostics(input: unknown): string | undefined { - const result = encodeUnknownJsonStringExit(input); - return Exit.isSuccess(result) ? result.value : undefined; -} - -export interface CursorAdapterLiveOptions { - readonly environment?: NodeJS.ProcessEnv; - readonly nativeEventLogPath?: string; - readonly nativeEventLogger?: EventNdjsonLogger; - /** - * Selections are honored when `modelSelection.instanceId` matches this value. - * Defaults to the legacy built-in instance id (`cursor`). - */ - readonly instanceId?: ProviderInstanceId; - /** - * Optional per-session settings resolver. When provided the adapter yields - * this effect at the start of every session and uses the result instead of - * the `cursorSettings` captured at construction. - * - * Production instances bind settings to the instance scope (the hydration - * layer rebuilds the adapter on config change) and leave this undefined. - * Test suites that mutate `ServerSettingsService` mid-flight — e.g. to - * swap `binaryPath` to a mock ACP wrapper — pass a resolver that reads - * the latest snapshot so the closure isn't stale. - */ - readonly resolveSettings?: Effect.Effect; -} - -interface PendingApproval { - readonly decision: Deferred.Deferred; - readonly kind: string | "unknown"; -} - -interface PendingUserInput { - readonly answers: Deferred.Deferred; -} - -interface CursorSessionContext { - readonly threadId: ThreadId; - session: ProviderSession; - readonly scope: Scope.Closeable; - readonly acp: AcpSessionRuntime.AcpSessionRuntime["Service"]; - notificationFiber: Fiber.Fiber | undefined; - readonly pendingApprovals: Map; - readonly pendingUserInputs: Map; - readonly turns: Array<{ id: TurnId; items: Array }>; - lastPlanFingerprint: string | undefined; - activeTurnId: TurnId | undefined; - /** Number of sendTurn prompts currently in flight or being prepared. - * >0 means a turn is actively running, so a new sendTurn is a steer that - * continues it, and only the last remaining prompt settles the turn. */ - promptsInFlight: number; - stopped: boolean; -} - -function settlePendingApprovalsAsCancelled( - pendingApprovals: ReadonlyMap, -): Effect.Effect { - const pendingEntries = Array.from(pendingApprovals.values()); - return Effect.forEach( - pendingEntries, - (pending) => Deferred.succeed(pending.decision, "cancel").pipe(Effect.ignore), - { - discard: true, - }, - ); -} - -function settlePendingUserInputsAsEmptyAnswers( - pendingUserInputs: ReadonlyMap, -): Effect.Effect { - const pendingEntries = Array.from(pendingUserInputs.values()); - return Effect.forEach( - pendingEntries, - (pending) => Deferred.succeed(pending.answers, {}).pipe(Effect.ignore), - { - discard: true, - }, - ); -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function parseCursorResume(raw: unknown): { sessionId: string } | undefined { - if (!isRecord(raw)) return undefined; - if (raw.schemaVersion !== CURSOR_RESUME_VERSION) return undefined; - if (typeof raw.sessionId !== "string" || !raw.sessionId.trim()) return undefined; - return { sessionId: raw.sessionId.trim() }; -} - -function normalizeModeSearchText(mode: AcpSessionMode): string { - return [mode.id, mode.name, mode.description] - .filter((value): value is string => typeof value === "string" && value.length > 0) - .join(" ") - .toLowerCase() - .replace(/[^a-z0-9]+/g, " ") - .trim(); -} - -function findModeByAliases( - modes: ReadonlyArray, - aliases: ReadonlyArray, -): AcpSessionMode | undefined { - const normalizedAliases = aliases.map((alias) => alias.toLowerCase()); - for (const alias of normalizedAliases) { - const exact = modes.find((mode) => { - const id = mode.id.toLowerCase(); - const name = mode.name.toLowerCase(); - return id === alias || name === alias; - }); - if (exact) { - return exact; - } - } - for (const alias of normalizedAliases) { - const partial = modes.find((mode) => normalizeModeSearchText(mode).includes(alias)); - if (partial) { - return partial; - } - } - return undefined; -} - -function isPlanMode(mode: AcpSessionMode): boolean { - return findModeByAliases([mode], ACP_PLAN_MODE_ALIASES) !== undefined; -} - -function resolveRequestedModeId(input: { - readonly interactionMode: ProviderInteractionMode | undefined; - readonly runtimeMode: RuntimeMode; - readonly modeState: AcpSessionModeState | undefined; -}): string | undefined { - const modeState = input.modeState; - if (!modeState) { - return undefined; - } - - if (input.interactionMode === "plan") { - return findModeByAliases(modeState.availableModes, ACP_PLAN_MODE_ALIASES)?.id; - } - - if (input.runtimeMode === "approval-required") { - return ( - findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ?? - findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ?? - modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ?? - modeState.currentModeId - ); - } - - return ( - findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ?? - findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ?? - modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ?? - modeState.currentModeId - ); -} - -function applyRequestedSessionConfiguration(input: { - readonly runtime: AcpSessionRuntime.AcpSessionRuntime["Service"]; - readonly runtimeMode: RuntimeMode; - readonly interactionMode: ProviderInteractionMode | undefined; - readonly modelSelection: - | { - readonly model: string; - readonly options?: ReadonlyArray | null | undefined; - } - | undefined; - readonly mapError: (context: { - readonly cause: import("effect-acp/errors").AcpError; - readonly method: "session/set_config_option" | "session/set_mode"; - }) => E; -}): Effect.Effect { - return Effect.gen(function* () { - if (input.modelSelection) { - yield* applyCursorAcpModelSelection({ - runtime: input.runtime, - model: input.modelSelection.model, - selections: input.modelSelection.options, - mapError: ({ cause }) => - input.mapError({ - cause, - method: "session/set_config_option", - }), - }); - } - - const requestedModeId = resolveRequestedModeId({ - interactionMode: input.interactionMode, - runtimeMode: input.runtimeMode, - modeState: yield* input.runtime.getModeState, - }); - if (!requestedModeId) { - return; - } - - yield* input.runtime.setMode(requestedModeId).pipe( - Effect.mapError((cause) => - input.mapError({ - cause, - method: "session/set_mode", - }), - ), - ); - }); -} - -function selectAutoApprovedPermissionOption( - request: EffectAcpSchema.RequestPermissionRequest, -): string | undefined { - const allowAlwaysOption = request.options.find((option) => option.kind === "allow_always"); - if (typeof allowAlwaysOption?.optionId === "string" && allowAlwaysOption.optionId.trim()) { - return allowAlwaysOption.optionId.trim(); - } - - const allowOnceOption = request.options.find((option) => option.kind === "allow_once"); - if (typeof allowOnceOption?.optionId === "string" && allowOnceOption.optionId.trim()) { - return allowOnceOption.optionId.trim(); - } - - return undefined; -} - -export function makeCursorAdapter( - cursorSettings: CursorSettings, - options?: CursorAdapterLiveOptions, -) { - return Effect.gen(function* () { - const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("cursor"); - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const serverConfig = yield* Effect.service(ServerConfig); - const crypto = yield* Crypto.Crypto; - const nativeEventLogger = - options?.nativeEventLogger ?? - (options?.nativeEventLogPath !== undefined - ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { - stream: "native", - }) - : undefined); - const managedNativeEventLogger = - options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; - const makeAcpNativeLoggers = yield* makeAcpNativeLoggerFactory(); - - const sessions = new Map(); - const threadLocksRef = yield* SynchronizedRef.make(new Map()); - const runtimeEventPubSub = yield* PubSub.unbounded(); - - const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - const randomUUIDv4 = crypto.randomUUIDv4.pipe( - Effect.mapError( - (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "crypto/randomUUIDv4", - detail: "Failed to generate Cursor runtime identifier.", - cause, - }), - ), - ); - const nextEventId = Effect.map(randomUUIDv4, (id) => EventId.make(id)); - const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); - const mapExtensionFailure = (effect: Effect.Effect) => - effect.pipe( - Effect.mapError( - (cause) => - new EffectAcpErrors.AcpTransportError({ - detail: "Failed to process Cursor ACP extension event.", - cause, - }), - ), - ); - - const offerRuntimeEvent = (event: ProviderRuntimeEvent) => - PubSub.publish(runtimeEventPubSub, event).pipe(Effect.asVoid); - - const getThreadSemaphore = (threadId: string) => - SynchronizedRef.modifyEffect(threadLocksRef, (current) => { - const existing: Option.Option = Option.fromNullishOr( - current.get(threadId), - ); - return Option.match(existing, { - onNone: () => - Semaphore.make(1).pipe( - Effect.map((semaphore) => { - const next = new Map(current); - next.set(threadId, semaphore); - return [semaphore, next] as const; - }), - ), - onSome: (semaphore) => Effect.succeed([semaphore, current] as const), - }); - }); - - const withThreadLock = (threadId: string, effect: Effect.Effect) => - Effect.flatMap(getThreadSemaphore(threadId), (semaphore) => semaphore.withPermit(effect)); - - const logNative = ( - threadId: ThreadId, - method: string, - payload: unknown, - _source: "acp.jsonrpc" | "acp.cursor.extension", - ) => - Effect.gen(function* () { - if (!nativeEventLogger) return; - const observedAt = yield* nowIso; - yield* nativeEventLogger.write( - { - observedAt, - event: { - id: yield* randomUUIDv4, - kind: "notification", - provider: PROVIDER, - createdAt: observedAt, - method, - threadId, - payload, - }, - }, - threadId, - ); - }); - - const emitPlanUpdate = ( - ctx: CursorSessionContext, - payload: { - readonly explanation?: string | null; - readonly plan: ReadonlyArray<{ - readonly step: string; - readonly status: "pending" | "inProgress" | "completed"; - }>; - }, - rawPayload: unknown, - source: "acp.jsonrpc" | "acp.cursor.extension", - method: string, - ) => - Effect.gen(function* () { - const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${encodeJsonStringForDiagnostics(payload) ?? "[unserializable payload]"}`; - if (ctx.lastPlanFingerprint === fingerprint) { - return; - } - ctx.lastPlanFingerprint = fingerprint; - yield* offerRuntimeEvent( - makeAcpPlanUpdatedEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - payload, - source, - method, - rawPayload, - }), - ); - }); - - const requireSession = ( - threadId: ThreadId, - ): Effect.Effect => { - const ctx = sessions.get(threadId); - if (!ctx || ctx.stopped) { - return Effect.fail( - new ProviderAdapterSessionNotFoundError({ provider: PROVIDER, threadId }), - ); - } - return Effect.succeed(ctx); - }; - - const stopSessionInternal = (ctx: CursorSessionContext) => - Effect.gen(function* () { - if (ctx.stopped) return; - ctx.stopped = true; - yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals); - yield* settlePendingUserInputsAsEmptyAnswers(ctx.pendingUserInputs); - if (ctx.notificationFiber) { - yield* Fiber.interrupt(ctx.notificationFiber); - } - yield* Effect.ignore(Scope.close(ctx.scope, Exit.void)); - sessions.delete(ctx.threadId); - yield* offerRuntimeEvent({ - type: "session.exited", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: ctx.threadId, - payload: { exitKind: "graceful" }, - }); - }); - - const startSession: CursorAdapterShape["startSession"] = (input) => - withThreadLock( - input.threadId, - Effect.gen(function* () { - if (input.provider !== undefined && input.provider !== PROVIDER) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "startSession", - issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, - }); - } - if (!input.cwd?.trim()) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "startSession", - issue: "cwd is required and must be non-empty.", - }); - } - - const cwd = path.resolve(input.cwd.trim()); - const cursorModelSelection = - input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; - const existing = sessions.get(input.threadId); - if (existing && !existing.stopped) { - yield* stopSessionInternal(existing); - } - - const pendingApprovals = new Map(); - const pendingUserInputs = new Map(); - const sessionScope = yield* Scope.make("sequential"); - let sessionScopeTransferred = false; - yield* Effect.addFinalizer(() => - sessionScopeTransferred ? Effect.void : Scope.close(sessionScope, Exit.void), - ); - let ctx!: CursorSessionContext; - - const resumeSessionId = parseCursorResume(input.resumeCursor)?.sessionId; - const acpNativeLoggers = makeAcpNativeLoggers({ - nativeEventLogger, - provider: PROVIDER, - threadId: input.threadId, - }); - - // Resolve the CursorSettings used to spawn the ACP child. Production - // leaves `options.resolveSettings` undefined so we use the value - // captured at adapter construction — per-instance isolation is - // enforced by the hydration layer rebuilding this adapter whenever - // its config changes. Tests set `resolveSettings` to pull the latest - // snapshot from `ServerSettingsService` so that mid-suite - // `updateSettings({ providers: { cursor: { binaryPath } } })` calls - // actually take effect when the next session spawns. - const effectiveCursorSettings = options?.resolveSettings - ? yield* options.resolveSettings - : cursorSettings; - - const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); - const acp = yield* makeCursorAcpRuntime({ - cursorSettings: effectiveCursorSettings, - ...(options?.environment ? { environment: options.environment } : {}), - childProcessSpawner, - cwd, - ...(resumeSessionId ? { resumeSessionId } : {}), - clientInfo: { name: "t3-code", version: "0.0.0" }, - ...(mcpSession - ? { - mcpServers: [ - { - type: "http" as const, - name: "t3-code", - url: mcpSession.endpoint, - headers: [ - { - name: "Authorization", - value: mcpSession.authorizationHeader, - }, - ], - }, - ], - } - : {}), - ...acpNativeLoggers, - }).pipe( - Effect.provideService(Scope.Scope, sessionScope), - Effect.mapError( - (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId: input.threadId, - detail: cause.message, - cause, - }), - ), - ); - const started = yield* Effect.gen(function* () { - yield* acp.handleExtRequest("cursor/ask_question", CursorAskQuestionRequest, (params) => - mapExtensionFailure( - Effect.gen(function* () { - yield* logNative( - input.threadId, - "cursor/ask_question", - params, - "acp.cursor.extension", - ); - const requestId = ApprovalRequestId.make(yield* randomUUIDv4); - const runtimeRequestId = RuntimeRequestId.make(requestId); - const answers = yield* Deferred.make(); - pendingUserInputs.set(requestId, { answers }); - yield* offerRuntimeEvent({ - type: "user-input.requested", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId: ctx?.activeTurnId, - requestId: runtimeRequestId, - payload: { questions: extractAskQuestions(params) }, - raw: { - source: "acp.cursor.extension", - method: "cursor/ask_question", - payload: params, - }, - }); - const resolved = yield* Deferred.await(answers); - pendingUserInputs.delete(requestId); - yield* offerRuntimeEvent({ - type: "user-input.resolved", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId: ctx?.activeTurnId, - requestId: runtimeRequestId, - payload: { answers: resolved }, - }); - return { answers: resolved }; - }), - ), - ); - yield* acp.handleExtRequest("cursor/create_plan", CursorCreatePlanRequest, (params) => - mapExtensionFailure( - Effect.gen(function* () { - yield* logNative( - input.threadId, - "cursor/create_plan", - params, - "acp.cursor.extension", - ); - yield* offerRuntimeEvent({ - type: "turn.proposed.completed", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId: ctx?.activeTurnId, - payload: { planMarkdown: extractPlanMarkdown(params) }, - raw: { - source: "acp.cursor.extension", - method: "cursor/create_plan", - payload: params, - }, - }); - return { accepted: true } as const; - }), - ), - ); - yield* acp.handleExtNotification( - "cursor/update_todos", - CursorUpdateTodosRequest, - (params) => - mapExtensionFailure( - Effect.gen(function* () { - yield* logNative( - input.threadId, - "cursor/update_todos", - params, - "acp.cursor.extension", - ); - if (ctx) { - yield* emitPlanUpdate( - ctx, - extractTodosAsPlan(params), - params, - "acp.cursor.extension", - "cursor/update_todos", - ); - } - }), - ), - ); - yield* acp.handleRequestPermission((params) => - mapExtensionFailure( - Effect.gen(function* () { - yield* logNative( - input.threadId, - "session/request_permission", - params, - "acp.jsonrpc", - ); - if (input.runtimeMode === "full-access") { - const autoApprovedOptionId = selectAutoApprovedPermissionOption(params); - if (autoApprovedOptionId !== undefined) { - return { - outcome: { - outcome: "selected" as const, - optionId: autoApprovedOptionId, - }, - }; - } - } - const permissionRequest = parsePermissionRequest(params); - const requestId = ApprovalRequestId.make(yield* randomUUIDv4); - const runtimeRequestId = RuntimeRequestId.make(requestId); - const decision = yield* Deferred.make(); - pendingApprovals.set(requestId, { - decision, - kind: permissionRequest.kind, - }); - yield* offerRuntimeEvent( - makeAcpRequestOpenedEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: input.threadId, - turnId: ctx?.activeTurnId, - requestId: runtimeRequestId, - permissionRequest, - detail: - permissionRequest.detail ?? - encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ?? - "[unserializable params]", - args: params, - source: "acp.jsonrpc", - method: "session/request_permission", - rawPayload: params, - }), - ); - const resolved = yield* Deferred.await(decision); - pendingApprovals.delete(requestId); - yield* offerRuntimeEvent( - makeAcpRequestResolvedEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: input.threadId, - turnId: ctx?.activeTurnId, - requestId: runtimeRequestId, - permissionRequest, - decision: resolved, - }), - ); - return { - outcome: - resolved === "cancel" - ? ({ outcome: "cancelled" } as const) - : { - outcome: "selected" as const, - optionId: acpPermissionOutcome(resolved), - }, - }; - }), - ), - ); - return yield* acp.start(); - }).pipe( - Effect.mapError((error) => - mapAcpToAdapterError(PROVIDER, input.threadId, "session/start", error), - ), - ); - - yield* applyRequestedSessionConfiguration({ - runtime: acp, - runtimeMode: input.runtimeMode, - interactionMode: undefined, - modelSelection: cursorModelSelection, - mapError: ({ cause, method }) => - mapAcpToAdapterError(PROVIDER, input.threadId, method, cause), - }); - - const now = yield* nowIso; - const session: ProviderSession = { - provider: PROVIDER, - providerInstanceId: boundInstanceId, - status: "ready", - runtimeMode: input.runtimeMode, - cwd, - model: cursorModelSelection?.model, - threadId: input.threadId, - resumeCursor: { - schemaVersion: CURSOR_RESUME_VERSION, - sessionId: started.sessionId, - }, - createdAt: now, - updatedAt: now, - }; - - ctx = { - threadId: input.threadId, - session, - scope: sessionScope, - acp, - notificationFiber: undefined, - pendingApprovals, - pendingUserInputs, - turns: [], - lastPlanFingerprint: undefined, - activeTurnId: undefined, - promptsInFlight: 0, - stopped: false, - }; - - const nf = yield* Stream.runDrain( - Stream.mapEffect(acp.getEvents(), (event) => - Effect.gen(function* () { - switch (event._tag) { - case "ModeChanged": - return; - case "AssistantItemStarted": - yield* offerRuntimeEvent( - makeAcpAssistantItemEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - itemId: event.itemId, - lifecycle: "item.started", - }), - ); - return; - case "AssistantItemCompleted": - yield* offerRuntimeEvent( - makeAcpAssistantItemEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - itemId: event.itemId, - lifecycle: "item.completed", - }), - ); - return; - case "PlanUpdated": - yield* logNative( - ctx.threadId, - "session/update", - event.rawPayload, - "acp.jsonrpc", - ); - yield* emitPlanUpdate( - ctx, - event.payload, - event.rawPayload, - "acp.jsonrpc", - "session/update", - ); - return; - case "ToolCallUpdated": - yield* logNative( - ctx.threadId, - "session/update", - event.rawPayload, - "acp.jsonrpc", - ); - yield* offerRuntimeEvent( - makeAcpToolCallEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - toolCall: event.toolCall, - rawPayload: event.rawPayload, - }), - ); - return; - case "ContentDelta": - yield* logNative( - ctx.threadId, - "session/update", - event.rawPayload, - "acp.jsonrpc", - ); - yield* offerRuntimeEvent( - makeAcpContentDeltaEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - ...(event.itemId ? { itemId: event.itemId } : {}), - text: event.text, - rawPayload: event.rawPayload, - }), - ); - return; - } - }), - ), - ).pipe( - Effect.catch((cause) => - Effect.logError("Failed to process Cursor runtime notification.", { cause }), - ), - Effect.forkChild, - ); - - ctx.notificationFiber = nf; - sessions.set(input.threadId, ctx); - sessionScopeTransferred = true; - - yield* offerRuntimeEvent({ - type: "session.started", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - payload: { resume: started.initializeResult }, - }); - yield* offerRuntimeEvent({ - type: "session.state.changed", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - payload: { state: "ready", reason: "Cursor ACP session ready" }, - }); - yield* offerRuntimeEvent({ - type: "thread.started", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - payload: { providerThreadId: started.sessionId }, - }); - - return session; - }).pipe(Effect.scoped), - ); - - const sendTurn: CursorAdapterShape["sendTurn"] = (input) => - Effect.gen(function* () { - const ctx = yield* requireSession(input.threadId); - // A sendTurn while a prompt is in flight is a steer: the agent folds - // the new prompt into the ongoing work, so the active turn id is - // reused instead of opening a new turn. - const steeringTurnId = ctx.promptsInFlight > 0 ? ctx.activeTurnId : undefined; - const turnId = steeringTurnId ?? TurnId.make(yield* randomUUIDv4); - // Count this prompt immediately so a superseded in-flight prompt - // resolving from here on does not settle the turn; the matching - // decrement is the `ensuring` below. - ctx.promptsInFlight += 1; - - return yield* Effect.gen(function* () { - const turnModelSelection = - input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; - const model = turnModelSelection?.model ?? ctx.session.model; - const resolvedModel = resolveCursorAcpBaseModelId(model); - yield* applyRequestedSessionConfiguration({ - runtime: ctx.acp, - runtimeMode: ctx.session.runtimeMode, - interactionMode: input.interactionMode, - modelSelection: - model === undefined - ? undefined - : { - model, - options: turnModelSelection?.options, - }, - mapError: ({ cause, method }) => - mapAcpToAdapterError(PROVIDER, input.threadId, method, cause), - }); - ctx.activeTurnId = turnId; - if (steeringTurnId === undefined) { - ctx.lastPlanFingerprint = undefined; - } - ctx.session = { - ...ctx.session, - activeTurnId: turnId, - updatedAt: yield* nowIso, - }; - - if (steeringTurnId === undefined) { - yield* offerRuntimeEvent({ - type: "turn.started", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId, - payload: { model: resolvedModel }, - }); - } - - const promptParts: Array = []; - if (input.input?.trim()) { - promptParts.push({ type: "text", text: input.input.trim() }); - } - if (input.attachments && input.attachments.length > 0) { - for (const attachment of input.attachments) { - const attachmentPath = resolveAttachmentPath({ - attachmentsDir: serverConfig.attachmentsDir, - attachment, - }); - if (!attachmentPath) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "session/prompt", - detail: `Invalid attachment id '${attachment.id}'.`, - }); - } - const bytes = yield* fileSystem.readFile(attachmentPath).pipe( - Effect.mapError( - (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "session/prompt", - detail: cause.message, - cause, - }), - ), - ); - promptParts.push({ - type: "image", - data: Buffer.from(bytes).toString("base64"), - mimeType: attachment.mimeType, - }); - } - } - - if (promptParts.length === 0) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "sendTurn", - issue: "Turn requires non-empty text or attachments.", - }); - } - - const result = yield* ctx.acp - .prompt({ - prompt: promptParts, - }) - .pipe( - Effect.mapError((error) => - mapAcpToAdapterError(PROVIDER, input.threadId, "session/prompt", error), - ), - ); - - const turnRecord = ctx.turns.find((turn) => turn.id === turnId); - if (turnRecord) { - turnRecord.items.push({ prompt: promptParts, result }); - } else { - ctx.turns.push({ id: turnId, items: [{ prompt: promptParts, result }] }); - } - ctx.session = { - ...ctx.session, - activeTurnId: turnId, - updatedAt: yield* nowIso, - model: resolvedModel, - }; - - // Only the last remaining prompt settles the turn — a steer- - // superseded prompt resolving (usually cancelled) while another is - // in flight or pending must leave the merged turn running. - if (ctx.promptsInFlight === 1) { - yield* offerRuntimeEvent({ - type: "turn.completed", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId, - payload: { - state: result.stopReason === "cancelled" ? "cancelled" : "completed", - stopReason: result.stopReason ?? null, - }, - }); - } - - return { - threadId: input.threadId, - turnId, - resumeCursor: ctx.session.resumeCursor, - }; - }).pipe( - Effect.ensuring( - Effect.sync(() => { - ctx.promptsInFlight = Math.max(0, ctx.promptsInFlight - 1); - }), - ), - ); - }); - - const interruptTurn: CursorAdapterShape["interruptTurn"] = (threadId) => - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals); - yield* settlePendingUserInputsAsEmptyAnswers(ctx.pendingUserInputs); - yield* Effect.ignore( - ctx.acp.cancel.pipe( - Effect.mapError((error) => - mapAcpToAdapterError(PROVIDER, threadId, "session/cancel", error), - ), - ), - ); - }); - - const respondToRequest: CursorAdapterShape["respondToRequest"] = ( - threadId, - requestId, - decision, - ) => - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - const pending = ctx.pendingApprovals.get(requestId); - if (!pending) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "session/request_permission", - detail: `Unknown pending approval request: ${requestId}`, - }); - } - yield* Deferred.succeed(pending.decision, decision); - }); - - const respondToUserInput: CursorAdapterShape["respondToUserInput"] = ( - threadId, - requestId, - answers, - ) => - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - const pending = ctx.pendingUserInputs.get(requestId); - if (!pending) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "cursor/ask_question", - detail: `Unknown pending user-input request: ${requestId}`, - }); - } - yield* Deferred.succeed(pending.answers, answers); - }); - - const readThread: CursorAdapterShape["readThread"] = (threadId) => - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - return { threadId, turns: ctx.turns }; - }); - - const rollbackThread: CursorAdapterShape["rollbackThread"] = (threadId, numTurns) => - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - if (!Number.isInteger(numTurns) || numTurns < 1) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "rollbackThread", - issue: "numTurns must be an integer >= 1.", - }); - } - const nextLength = Math.max(0, ctx.turns.length - numTurns); - ctx.turns.splice(nextLength); - return { threadId, turns: ctx.turns }; - }); - - const stopSession: CursorAdapterShape["stopSession"] = (threadId) => - withThreadLock( - threadId, - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - yield* stopSessionInternal(ctx); - }), - ); - - const listSessions: CursorAdapterShape["listSessions"] = () => - Effect.sync(() => Array.from(sessions.values(), (c) => ({ ...c.session }))); - - const hasSession: CursorAdapterShape["hasSession"] = (threadId) => - Effect.sync(() => { - const c = sessions.get(threadId); - return c !== undefined && !c.stopped; - }); - - const stopAll: CursorAdapterShape["stopAll"] = () => - Effect.forEach(sessions.values(), stopSessionInternal, { discard: true }); - - yield* Effect.addFinalizer(() => - Effect.forEach(sessions.values(), stopSessionInternal, { discard: true }).pipe( - Effect.catch((cause) => - Effect.logError("Failed to emit Cursor session shutdown event.", { cause }), - ), - Effect.tap(() => PubSub.shutdown(runtimeEventPubSub)), - Effect.tap(() => managedNativeEventLogger?.close() ?? Effect.void), - ), - ); - - const streamEvents = Stream.fromPubSub(runtimeEventPubSub); - - return { - provider: PROVIDER, - capabilities: { sessionModelSwitch: "in-session" }, - startSession, - sendTurn, - interruptTurn, - readThread, - rollbackThread, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - stopAll, - streamEvents, - } satisfies CursorAdapterShape; - }); -} diff --git a/apps/server/src/provider/Layers/CursorProvider.test.ts b/apps/server/src/provider/Layers/CursorProvider.test.ts index 60a7312eea3..fb79446027b 100644 --- a/apps/server/src/provider/Layers/CursorProvider.test.ts +++ b/apps/server/src/provider/Layers/CursorProvider.test.ts @@ -1,10 +1,12 @@ import * as NodeOS from "node:os"; +import type { SDKModel } from "@cursor/sdk"; +import { describe, expect, it } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; -import { describe, expect, it } from "vite-plus/test"; import type * as EffectAcpSchema from "effect-acp/schema"; import type { CursorSettings } from "@t3tools/contracts"; import { createModelCapabilities } from "@t3tools/shared/model"; @@ -12,6 +14,8 @@ import { createModelCapabilities } from "@t3tools/shared/model"; import { buildCursorProviderSnapshot, buildCursorCapabilitiesFromConfigOptions, + buildCursorCapabilitiesFromSdkModel, + buildCursorDiscoveredModelsFromSdk, checkCursorProviderStatus, discoverCursorModelsViaAcp, getCursorFallbackModels, @@ -22,6 +26,7 @@ import { resolveCursorAcpBaseModelId, resolveCursorAcpConfigUpdates, } from "./CursorProvider.ts"; +import { CursorSdkCatalogError, makeCursorSdkCatalogTestLayer } from "./CursorSdkCatalog.ts"; const runNode = ( effect: Effect.Effect, @@ -294,6 +299,51 @@ const baseCursorSettings: CursorSettings = { customModels: [], }; +const sdkParameterizedModel = { + id: "claude-opus-4-8", + displayName: "Opus 4.8", + parameters: [ + { + id: "thinking", + displayName: "Thinking", + values: [{ value: "false" }, { value: "true" }], + }, + { + id: "context", + displayName: "Context", + values: [ + { value: "300k", displayName: "300K" }, + { value: "1m", displayName: "1M" }, + ], + }, + { + id: "effort", + displayName: "Effort", + values: [ + { value: "low", displayName: "Low" }, + { value: "high", displayName: "High" }, + ], + }, + { + id: "fast", + displayName: "Fast", + values: [{ value: "false" }, { value: "true", displayName: "Fast" }], + }, + ], + variants: [ + { + displayName: "Opus 4.8", + isDefault: true, + params: [ + { id: "thinking", value: "true" }, + { id: "context", value: "1m" }, + { id: "effort", value: "high" }, + { id: "fast", value: "false" }, + ], + }, + ], +} satisfies SDKModel; + describe("getCursorFallbackModels", () => { it("does not publish any built-in cursor models before ACP discovery", () => { expect( @@ -410,7 +460,117 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => { }); }); +describe("Cursor SDK model discovery", () => { + it("maps native SDK parameter ids and default variant values to model capabilities", () => { + expect(buildCursorCapabilitiesFromSdkModel(sdkParameterizedModel)).toEqual( + createModelCapabilities({ + optionDescriptors: [ + selectDescriptor("effort", "Effort", [ + { id: "low", label: "Low" }, + { id: "high", label: "High", isDefault: true }, + ]), + selectDescriptor("contextWindow", "Context", [ + { id: "300k", label: "300K" }, + { id: "1m", label: "1M", isDefault: true }, + ]), + booleanDescriptor("fastMode", "Fast", false), + booleanDescriptor("thinking", "Thinking", true), + ], + }), + ); + }); + + it("filters invalid and duplicate SDK model entries", () => { + expect( + buildCursorDiscoveredModelsFromSdk([ + sdkParameterizedModel, + { ...sdkParameterizedModel, displayName: "Duplicate" }, + { id: "", displayName: "Invalid" }, + ]), + ).toEqual([ + { + slug: "claude-opus-4-8", + name: "Opus 4.8", + isCustom: false, + capabilities: buildCursorCapabilitiesFromSdkModel(sdkParameterizedModel), + }, + ]); + }); +}); + describe("checkCursorProviderStatus", () => { + it("uses the SDK catalog when CURSOR_API_KEY is configured", async () => { + const provider = await Effect.runPromise( + checkCursorProviderStatus( + { + ...baseCursorSettings, + binaryPath: "cursor-cli-must-not-be-invoked", + customModels: ["internal/cursor-model"], + }, + { CURSOR_API_KEY: "test-cursor-key" }, + ).pipe( + Effect.provide( + Layer.mergeAll( + makeCursorSdkCatalogTestLayer((apiKey) => { + expect(apiKey).toBe("test-cursor-key"); + return Effect.succeed({ + user: { + apiKeyName: "test-key", + userEmail: "cursor@example.com", + createdAt: "2026-01-01T00:00:00.000Z", + }, + models: [sdkParameterizedModel], + }); + }), + NodeServices.layer, + ), + ), + ), + ); + + expect(provider).toMatchObject({ + status: "ready", + auth: { + status: "authenticated", + type: "api-key", + label: "Cursor API key (test-key)", + email: "cursor@example.com", + }, + models: [ + { slug: "claude-opus-4-8", isCustom: false }, + { slug: "internal/cursor-model", isCustom: true }, + ], + }); + }); + + it("surfaces SDK authentication failures without invoking the CLI", async () => { + const provider = await Effect.runPromise( + checkCursorProviderStatus(baseCursorSettings, { + CURSOR_API_KEY: "invalid-test-key", + }).pipe( + Effect.provide( + Layer.mergeAll( + makeCursorSdkCatalogTestLayer(() => + Effect.fail( + new CursorSdkCatalogError({ + authenticationFailure: true, + cause: new Error("unauthorized"), + }), + ), + ), + NodeServices.layer, + ), + ), + ), + ); + + expect(provider).toMatchObject({ + status: "error", + auth: { status: "unauthenticated" }, + message: "Cursor SDK authentication failed. Check CURSOR_API_KEY.", + }); + }); + it("passes the injected environment to ACP model discovery", async () => { const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture()); @@ -424,9 +584,19 @@ describe("checkCursorProviderStatus", () => { }, { ...process.env, + CURSOR_API_KEY: "", T3_ACP_REQUEST_LOG_PATH: requestLogPath, }, - ).pipe(Effect.provide(NodeServices.layer)), + ).pipe( + Effect.provide( + Layer.mergeAll( + makeCursorSdkCatalogTestLayer(() => + Effect.die("SDK catalog must not be used without CURSOR_API_KEY"), + ), + NodeServices.layer, + ), + ), + ), ); expect(provider.models.map((model) => model.slug)).toEqual([ @@ -440,43 +610,40 @@ describe("checkCursorProviderStatus", () => { }); describe("discoverCursorModelsViaAcp", () => { - it("keeps the ACP probe runtime alive long enough to discover models", async () => { - const wrapperPath = await runNode(makeMockAgentWrapper()); - - const models = await Effect.runPromise( - discoverCursorModelsViaAcp({ + it.effect("keeps the ACP probe runtime alive long enough to discover models", () => + Effect.gen(function* () { + const wrapperPath = yield* makeMockAgentWrapper(); + const models = yield* discoverCursorModelsViaAcp({ enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], - }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), - ); - - expect(models.map((model) => model.slug)).toEqual([ - "default", - "composer-2", - "gpt-5.4", - "claude-opus-4-6", - ]); - }); - - it("closes the ACP probe runtime after discovery completes", async () => { - const { exitLogPath, wrapperPath } = await runNode( - makeExitLogFixture("cursor-provider-exit-log-"), - ); - - await Effect.runPromise( - discoverCursorModelsViaAcp({ + }); + + expect(models.map((model) => model.slug)).toEqual([ + "default", + "composer-2", + "gpt-5.4", + "claude-opus-4-6", + ]); + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), + ); + + it.effect("closes the ACP probe runtime after discovery completes", () => + Effect.gen(function* () { + const { exitLogPath, wrapperPath } = yield* makeExitLogFixture("cursor-provider-exit-log-"); + + yield* discoverCursorModelsViaAcp({ enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], - }).pipe(Effect.provide(NodeServices.layer)), - ); + }); - const exitLog = await runNode(waitForFileContent(exitLogPath)); - expect(exitLog).toContain("SIGTERM"); - }); + const exitLog = yield* waitForFileContent(exitLogPath); + expect(exitLog).toContain("SIGTERM"); + }).pipe(Effect.provide(NodeServices.layer)), + ); }); describe("parseCursorAboutOutput", () => { diff --git a/apps/server/src/provider/Layers/CursorProvider.ts b/apps/server/src/provider/Layers/CursorProvider.ts index ff96ece9349..37044cad302 100644 --- a/apps/server/src/provider/Layers/CursorProvider.ts +++ b/apps/server/src/provider/Layers/CursorProvider.ts @@ -1,7 +1,9 @@ +import type { SDKModel, SDKUser } from "@cursor/sdk"; import * as NodeOS from "node:os"; import type { CursorSettings, ModelCapabilities, + ProviderOptionDescriptor, ProviderOptionSelection, ServerProvider, ServerProviderAuth, @@ -44,8 +46,10 @@ import { enrichProviderSnapshotWithVersionAdvisory, type ProviderMaintenanceCapabilities, } from "../providerMaintenance.ts"; +import { cursorSdkParameterPriority, cursorSdkProviderOptionId } from "../cursorSdkModel.ts"; import * as AcpSessionRuntime from "../acp/AcpSessionRuntime.ts"; import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts"; +import { CursorSdkCatalog } from "./CursorSdkCatalog.ts"; const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect( CursorListAvailableModelsResponse, @@ -61,6 +65,7 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ }); const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000; +const CURSOR_SDK_CATALOG_TIMEOUT_MS = 15_000; const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08; export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = { _meta: { @@ -372,6 +377,112 @@ function buildCursorDiscoveredModels( }); } +function cursorSdkDefaultParameterValue(model: SDKModel, parameterId: string): string | undefined { + return model.variants + ?.find((variant) => variant.isDefault) + ?.params.find((parameter) => parameter.id === parameterId)?.value; +} + +export function buildCursorCapabilitiesFromSdkModel(model: SDKModel): ModelCapabilities { + const seen = new Set(); + const optionDescriptors: Array = []; + const parameters = (model.parameters ?? []) + .map((parameter, index) => ({ parameter, index })) + .toSorted( + (left, right) => + cursorSdkParameterPriority(left.parameter.id) - + cursorSdkParameterPriority(right.parameter.id) || left.index - right.index, + ); + for (const { parameter } of parameters) { + const nativeId = parameter.id.trim(); + const id = cursorSdkProviderOptionId(nativeId); + if (!nativeId || !id || seen.has(id)) { + continue; + } + seen.add(id); + + const values = parameter.values.flatMap((entry) => { + const value = entry.value.trim(); + if (!value) { + return []; + } + return [ + { + value, + label: entry.displayName?.trim() || value, + }, + ]; + }); + if (values.length === 0) { + continue; + } + + const label = parameter.displayName?.trim() || toTitleCaseWords(id); + const defaultValue = cursorSdkDefaultParameterValue(model, nativeId); + const normalizedValues = new Set(values.map((entry) => entry.value.toLowerCase())); + if (values.length === 2 && normalizedValues.has("true") && normalizedValues.has("false")) { + if (defaultValue === "true" || defaultValue === "false") { + optionDescriptors.push( + buildBooleanOptionDescriptor({ + id, + label, + currentValue: defaultValue === "true", + }), + ); + } else { + optionDescriptors.push(buildBooleanOptionDescriptor({ id, label })); + } + continue; + } + + optionDescriptors.push( + buildSelectOptionDescriptor({ + id, + label, + options: values.map((entry) => ({ + ...entry, + ...(entry.value === defaultValue ? { isDefault: true } : {}), + })), + }), + ); + } + + return createModelCapabilities({ optionDescriptors }); +} + +export function buildCursorDiscoveredModelsFromSdk( + models: ReadonlyArray, +): ReadonlyArray { + const seen = new Set(); + return models.flatMap((model) => { + const slug = model.id.trim(); + const name = model.displayName.trim(); + if (!slug || !name || seen.has(slug)) { + return []; + } + seen.add(slug); + return [ + { + slug, + name, + isCustom: false, + capabilities: buildCursorCapabilitiesFromSdkModel(model), + } satisfies ServerProviderModel, + ]; + }); +} + +function cursorSdkAuth(user: SDKUser): ServerProviderAuth { + const email = user.userEmail?.trim(); + const apiKeyName = user.apiKeyName.trim(); + return { + status: "authenticated", + type: "api-key", + label: apiKeyName ? `Cursor API key (${apiKeyName})` : "Cursor API key", + ...(email ? { email } : {}), + }; +} + function buildCursorDiscoveredModelsFromAvailableModelsResponse( response: typeof CursorListAvailableModelsResponse.Type, ): ReadonlyArray { @@ -977,7 +1088,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( ): Effect.fn.Return< ServerProviderDraft, never, - ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path + ChildProcessSpawner.ChildProcessSpawner | CursorSdkCatalog | FileSystem.FileSystem | Path.Path > { const checkedAt = DateTime.formatIso(yield* DateTime.now); const fallbackModels = getCursorFallbackModels(cursorSettings); @@ -998,6 +1109,68 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( }); } + const sdkApiKey = environment?.CURSOR_API_KEY?.trim(); + if (sdkApiKey) { + const sdkCatalog = yield* CursorSdkCatalog; + const catalogResult = yield* sdkCatalog + .read(sdkApiKey) + .pipe(Effect.timeoutOption(CURSOR_SDK_CATALOG_TIMEOUT_MS), Effect.result); + + if (Result.isFailure(catalogResult)) { + yield* Effect.logWarning("Cursor SDK catalog probe failed", { + cause: catalogResult.failure.cause, + }); + const authenticationFailure = catalogResult.failure.authenticationFailure; + return buildServerProvider({ + presentation: CURSOR_PRESENTATION, + enabled: cursorSettings.enabled, + checkedAt, + models: fallbackModels, + probe: { + installed: true, + version: null, + status: "error", + auth: { status: authenticationFailure ? "unauthenticated" : "unknown" }, + message: authenticationFailure + ? "Cursor SDK authentication failed. Check CURSOR_API_KEY." + : "Cursor SDK catalog request failed. Check server logs for details.", + }, + }); + } + + if (Option.isNone(catalogResult.success)) { + return buildServerProvider({ + presentation: CURSOR_PRESENTATION, + enabled: cursorSettings.enabled, + checkedAt, + models: fallbackModels, + probe: { + installed: true, + version: null, + status: "error", + auth: { status: "unknown" }, + message: `Cursor SDK catalog request timed out after ${CURSOR_SDK_CATALOG_TIMEOUT_MS}ms.`, + }, + }); + } + + const snapshot = catalogResult.success.value; + const discoveredModels = buildCursorDiscoveredModelsFromSdk(snapshot.models); + return buildCursorProviderSnapshot({ + checkedAt, + cursorSettings, + parsed: { + version: null, + status: "ready", + auth: cursorSdkAuth(snapshot.user), + }, + discoveredModels, + ...(discoveredModels.length === 0 + ? { discoveryWarning: "Cursor SDK model discovery returned no built-in models." } + : {}), + }); + } + // Single `agent about` probe: returns version + auth status in one call. const aboutProbe = yield* runCursorAboutCommand(cursorSettings, environment).pipe( Effect.timeoutOption(ABOUT_TIMEOUT_MS), @@ -1105,8 +1278,9 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( * * Used by `CursorDriver` as the `makeManagedServerProvider.enrichSnapshot` * hook: republishes update/version advisory metadata without performing any - * model or capability discovery. Cursor model data comes exclusively from - * `cursor/list_available_models` during provider status checks. + * model or capability discovery. Provider status checks source Cursor model + * data from the SDK when an API key is configured, with ACP discovery as the + * compatibility fallback. */ export const enrichCursorSnapshot = (input: { readonly settings: CursorSettings; diff --git a/apps/server/src/provider/Layers/CursorSdkCatalog.ts b/apps/server/src/provider/Layers/CursorSdkCatalog.ts new file mode 100644 index 00000000000..1b9a987e773 --- /dev/null +++ b/apps/server/src/provider/Layers/CursorSdkCatalog.ts @@ -0,0 +1,73 @@ +import { + AuthenticationError, + Cursor, + CursorSdkError, + type SDKModel, + type SDKUser, +} from "@cursor/sdk"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; + +export interface CursorSdkCatalogSnapshot { + readonly user: SDKUser; + readonly models: ReadonlyArray; +} + +export class CursorSdkCatalogError extends Schema.TaggedErrorClass()( + "CursorSdkCatalogError", + { + authenticationFailure: Schema.Boolean, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return this.authenticationFailure + ? "Cursor SDK authentication failed." + : "Cursor SDK catalog request failed."; + } +} + +export interface CursorSdkCatalogShape { + readonly read: (apiKey: string) => Effect.Effect; +} + +export class CursorSdkCatalog extends Context.Service()( + "t3/provider/Layers/CursorSdkCatalog", +) {} + +function isAuthenticationFailure(cause: unknown): boolean { + return ( + cause instanceof AuthenticationError || + (cause instanceof CursorSdkError && cause.status === 401) + ); +} + +const readCursorSdkCatalog = Effect.fn("CursorSdkCatalog.read")(function* (apiKey: string) { + return yield* Effect.tryPromise({ + try: async () => { + const [user, models] = await Promise.all([ + Cursor.me({ apiKey }), + Cursor.models.list({ apiKey }), + ]); + return { user, models } satisfies CursorSdkCatalogSnapshot; + }, + catch: (cause) => + new CursorSdkCatalogError({ + authenticationFailure: isAuthenticationFailure(cause), + cause, + }), + }); +}); + +export const CursorSdkCatalogLive = Layer.succeed( + CursorSdkCatalog, + CursorSdkCatalog.of({ read: readCursorSdkCatalog }), +); + +export function makeCursorSdkCatalogTestLayer( + read: CursorSdkCatalogShape["read"], +): Layer.Layer { + return Layer.succeed(CursorSdkCatalog, CursorSdkCatalog.of({ read })); +} diff --git a/apps/server/src/provider/Layers/GrokAdapter.test.ts b/apps/server/src/provider/Layers/GrokAdapter.test.ts deleted file mode 100644 index c871e3c2fc4..00000000000 --- a/apps/server/src/provider/Layers/GrokAdapter.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodePath from "node:path"; -import * as NodeOS from "node:os"; -import * as NodeFSP from "node:fs/promises"; -import * as NodeURL from "node:url"; - -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { assert, it } from "@effect/vitest"; -import * as Deferred from "effect/Deferred"; -import * as Effect from "effect/Effect"; -import * as Fiber from "effect/Fiber"; -import * as Layer from "effect/Layer"; -import * as Schema from "effect/Schema"; -import * as Stream from "effect/Stream"; - -import { - ApprovalRequestId, - GrokSettings, - ProviderDriverKind, - ThreadId, - ProviderInstanceId, - type ProviderRuntimeEvent, -} from "@t3tools/contracts"; - -import { ServerConfig } from "../../config.ts"; -import { makeGrokAdapter } from "./GrokAdapter.ts"; -const decodeGrokSettings = Schema.decodeSync(GrokSettings); - -const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); -const mockAgentPath = NodePath.join(__dirname, "../../../scripts/acp-mock-agent.ts"); -const mockAgentCommand = process.execPath; - -async function makeMockGrokWrapper(extraEnv?: Record) { - const dir = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-acp-mock-")); - const wrapperPath = NodePath.join(dir, "fake-grok.sh"); - const envExports = Object.entries(extraEnv ?? {}) - .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) - .join("\n"); - const script = `#!/bin/sh -${envExports} -exec ${JSON.stringify(mockAgentCommand)} ${JSON.stringify(mockAgentPath)} "$@" -`; - await NodeFSP.writeFile(wrapperPath, script, "utf8"); - await NodeFSP.chmod(wrapperPath, 0o755); - return wrapperPath; -} - -function waitForFileContent(filePath: string, attempts = 40): Effect.Effect { - const readAttempt = (remainingAttempts: number): Effect.Effect => - Effect.gen(function* () { - if (remainingAttempts <= 0) { - return yield* Effect.die(new Error(`Timed out waiting for file content at ${filePath}`)); - } - const raw = yield* Effect.tryPromise(() => NodeFSP.readFile(filePath, "utf8")).pipe( - Effect.orElseSucceed(() => ""), - ); - if (raw.trim().length > 0) { - return raw; - } - yield* Effect.sleep("25 millis"); - return yield* readAttempt(remainingAttempts - 1); - }); - return readAttempt(attempts); -} - -async function readJsonLines(filePath: string) { - const raw = await NodeFSP.readFile(filePath, "utf8"); - return raw - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.length > 0) - .map((line) => JSON.parse(line) as Record); -} - -const grokAdapterTestLayer = ServerConfig.layerTest(process.cwd(), { - prefix: "t3code-grok-adapter-test-", -}).pipe(Layer.provideMerge(NodeServices.layer)); - -const makeTestAdapter = (binaryPath: string, options?: Parameters[1]) => - makeGrokAdapter(decodeGrokSettings({ binaryPath }), options).pipe(Effect.orDie); - -it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => { - it.effect("starts a session and maps mock ACP prompt flow to runtime events", () => - Effect.gen(function* () { - const threadId = ThreadId.make("grok-mock-thread"); - const wrapperPath = yield* Effect.promise(() => makeMockGrokWrapper()); - const adapter = yield* makeTestAdapter(wrapperPath); - - const runtimeEvents: ProviderRuntimeEvent[] = []; - const turnCompleted = yield* Deferred.make(); - const runtimeEventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => - Effect.sync(() => { - runtimeEvents.push(event); - }).pipe( - Effect.andThen( - event.type === "turn.completed" - ? Deferred.succeed(turnCompleted, undefined) - : Effect.void, - ), - ), - ).pipe(Effect.forkChild); - - const session = yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("grok"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("grok"), model: "grok-mock-alt" }, - }); - - assert.equal(session.provider, "grok"); - assert.equal(session.model, "grok-mock-alt"); - assert.deepStrictEqual(session.resumeCursor, { - schemaVersion: 1, - sessionId: "mock-session-1", - }); - - yield* adapter.sendTurn({ - threadId, - input: "hello grok", - attachments: [], - }); - - yield* Deferred.await(turnCompleted); - yield* Fiber.interrupt(runtimeEventsFiber); - const types = runtimeEvents.map((e) => e.type); - - assert.includeMembers(types, [ - "session.started", - "session.state.changed", - "thread.started", - "turn.started", - "item.started", - "content.delta", - "turn.completed", - ] as const); - - const delta = runtimeEvents.find((e) => e.type === "content.delta"); - assert.isDefined(delta); - if (delta?.type === "content.delta") { - assert.equal(delta.payload.delta, "hello from mock"); - } - - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("closes the ACP child process when a session stops", () => - Effect.gen(function* () { - const threadId = ThreadId.make("grok-stop-session-close"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-adapter-exit-log-")), - ); - const exitLogPath = NodePath.join(tempDir, "exit.log"); - - const wrapperPath = yield* Effect.promise(() => - makeMockGrokWrapper({ - T3_ACP_EXIT_LOG_PATH: exitLogPath, - }), - ); - const adapter = yield* makeTestAdapter(wrapperPath); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("grok"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("grok"), model: "grok-build" }, - }); - - yield* adapter.stopSession(threadId); - - const exitLog = yield* waitForFileContent(exitLogPath); - assert.include(exitLog, "SIGTERM"); - }), - ); - - it.effect("rejects startSession when provider mismatches", () => - Effect.gen(function* () { - const wrapperPath = yield* Effect.promise(() => makeMockGrokWrapper()); - const adapter = yield* makeTestAdapter(wrapperPath); - const threadId = ThreadId.make("grok-provider-mismatch"); - - const error = yield* Effect.flip( - adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("cursor"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("grok"), model: "grok-build" }, - }), - ); - - assert.equal(error._tag, "ProviderAdapterValidationError"); - }), - ); - - it.effect("rejects sendTurn with empty input and no attachments", () => - Effect.gen(function* () { - const threadId = ThreadId.make("grok-empty-turn"); - - const wrapperPath = yield* Effect.promise(() => makeMockGrokWrapper()); - const adapter = yield* makeTestAdapter(wrapperPath); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("grok"), - cwd: process.cwd(), - runtimeMode: "full-access", - modelSelection: { instanceId: ProviderInstanceId.make("grok"), model: "grok-build" }, - }); - - const error = yield* Effect.flip( - adapter.sendTurn({ - threadId, - input: " ", - attachments: [], - }), - ); - - assert.equal(error._tag, "ProviderAdapterValidationError"); - - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("responds to ACP approvals using provider-supplied option ids", () => - Effect.gen(function* () { - const threadId = ThreadId.make("grok-custom-approval-option-id"); - const tempDir = yield* Effect.promise(() => - NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-acp-")), - ); - const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); - const wrapperPath = yield* Effect.promise(() => - makeMockGrokWrapper({ - T3_ACP_REQUEST_LOG_PATH: requestLogPath, - T3_ACP_EMIT_TOOL_CALLS: "1", - T3_ACP_ALLOW_ONCE_OPTION_ID: "agent-defined-approval-id", - }), - ); - const adapter = yield* makeTestAdapter(wrapperPath); - const eventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => - event.type === "request.opened" - ? adapter.respondToRequest( - threadId, - ApprovalRequestId.make(String(event.requestId)), - "accept", - ) - : Effect.void, - ).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("grok"), - cwd: process.cwd(), - runtimeMode: "approval-required", - }); - yield* adapter.sendTurn({ threadId, input: "approve this", attachments: [] }); - - const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); - assert.isTrue( - requests.some( - (entry) => - !("method" in entry) && - typeof entry.result === "object" && - entry.result !== null && - "outcome" in entry.result && - typeof entry.result.outcome === "object" && - entry.result.outcome !== null && - "optionId" in entry.result.outcome && - entry.result.outcome.optionId === "agent-defined-approval-id", - ), - ); - - yield* Fiber.interrupt(eventsFiber); - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("handles xAI ask_user_question extension requests", () => - Effect.gen(function* () { - const threadId = ThreadId.make("grok-xai-ask-user-question"); - const wrapperPath = yield* Effect.promise(() => - makeMockGrokWrapper({ T3_ACP_EMIT_XAI_ASK_USER_QUESTION: "1" }), - ); - const adapter = yield* makeTestAdapter(wrapperPath); - const requested = - yield* Deferred.make>(); - const resolved = - yield* Deferred.make>(); - - const eventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => { - if (String(event.threadId) !== String(threadId)) { - return Effect.void; - } - if (event.type === "user-input.requested") { - return Deferred.succeed(requested, event).pipe(Effect.ignore); - } - if (event.type === "user-input.resolved") { - return Deferred.succeed(resolved, event).pipe(Effect.ignore); - } - return Effect.void; - }).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("grok"), - cwd: process.cwd(), - runtimeMode: "full-access", - }); - - const sendTurnFiber = yield* adapter - .sendTurn({ threadId, input: "ask before continuing", attachments: [] }) - .pipe(Effect.forkChild); - - const requestedEvent = yield* Deferred.await(requested); - assert.equal(requestedEvent.payload.questions.length, 1); - assert.equal(requestedEvent.payload.questions[0]?.id, "Which scope should Grok use?"); - assert.equal(requestedEvent.payload.questions[0]?.question, "Which scope should Grok use?"); - assert.equal(requestedEvent.raw?.method, "_x.ai/ask_user_question"); - - yield* adapter.respondToUserInput( - threadId, - ApprovalRequestId.make(String(requestedEvent.requestId)), - { - "Which scope should Grok use?": "Workspace", - }, - ); - - const resolvedEvent = yield* Deferred.await(resolved); - assert.deepEqual(resolvedEvent.payload.answers, { - "Which scope should Grok use?": "Workspace", - }); - yield* Fiber.join(sendTurnFiber); - - yield* Fiber.interrupt(eventsFiber); - yield* adapter.stopSession(threadId); - }), - ); - - it.effect("continues streaming events when native notification logging fails", () => - Effect.gen(function* () { - const threadId = ThreadId.make("grok-native-log-failure"); - const wrapperPath = yield* Effect.promise(() => makeMockGrokWrapper()); - const adapter = yield* makeTestAdapter(wrapperPath, { - nativeEventLogger: { - filePath: "memory://grok-native-events", - write: (record: unknown) => - typeof record === "object" && - record !== null && - "event" in record && - typeof record.event === "object" && - record.event !== null && - "kind" in record.event && - record.event.kind === "notification" - ? Effect.die(new Error("native log write failed")) - : Effect.void, - close: () => Effect.void, - }, - }); - const contentDelta = yield* Deferred.make(); - const eventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => - event.type === "content.delta" ? Deferred.succeed(contentDelta, undefined) : Effect.void, - ).pipe(Effect.forkChild); - - yield* adapter.startSession({ - threadId, - provider: ProviderDriverKind.make("grok"), - cwd: process.cwd(), - runtimeMode: "full-access", - }); - yield* adapter.sendTurn({ threadId, input: "keep streaming", attachments: [] }); - yield* Deferred.await(contentDelta); - - yield* Fiber.interrupt(eventsFiber); - yield* adapter.stopSession(threadId); - }), - ); -}); diff --git a/apps/server/src/provider/Layers/GrokAdapter.ts b/apps/server/src/provider/Layers/GrokAdapter.ts deleted file mode 100644 index 40f425cbaa1..00000000000 --- a/apps/server/src/provider/Layers/GrokAdapter.ts +++ /dev/null @@ -1,1007 +0,0 @@ -import { - ApprovalRequestId, - type GrokSettings, - EventId, - type ProviderApprovalDecision, - type ProviderRuntimeEvent, - type ProviderSession, - type ProviderUserInputAnswers, - ProviderDriverKind, - ProviderInstanceId, - RuntimeRequestId, - type ThreadId, - TurnId, -} from "@t3tools/contracts"; -import * as Crypto from "effect/Crypto"; -import * as DateTime from "effect/DateTime"; -import * as Deferred from "effect/Deferred"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Fiber from "effect/Fiber"; -import * as FileSystem from "effect/FileSystem"; -import * as Option from "effect/Option"; -import * as Path from "effect/Path"; -import * as PubSub from "effect/PubSub"; -import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; -import * as Semaphore from "effect/Semaphore"; -import * as Stream from "effect/Stream"; -import * as SynchronizedRef from "effect/SynchronizedRef"; -import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; -import * as EffectAcpErrors from "effect-acp/errors"; -import type * as EffectAcpSchema from "effect-acp/schema"; - -import { resolveAttachmentPath } from "../../attachmentStore.ts"; -import { ServerConfig } from "../../config.ts"; -import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; -import { - ProviderAdapterProcessError, - ProviderAdapterRequestError, - ProviderAdapterSessionNotFoundError, - ProviderAdapterValidationError, -} from "../Errors.ts"; -import { mapAcpToAdapterError } from "../acp/AcpAdapterSupport.ts"; -import type * as AcpSessionRuntime from "../acp/AcpSessionRuntime.ts"; -import { - makeAcpAssistantItemEvent, - makeAcpContentDeltaEvent, - makeAcpPlanUpdatedEvent, - makeAcpRequestOpenedEvent, - makeAcpRequestResolvedEvent, - makeAcpToolCallEvent, -} from "../acp/AcpCoreRuntimeEvents.ts"; -import { parsePermissionRequest } from "../acp/AcpRuntimeModel.ts"; -import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts"; -import { - applyGrokAcpModelSelection, - currentGrokModelIdFromSessionSetup, - makeGrokAcpRuntime, - resolveGrokAcpBaseModelId, -} from "../acp/GrokAcpSupport.ts"; -import { - extractXAiAskUserQuestions, - makeXAiAskUserQuestionCancelledResponse, - makeXAiAskUserQuestionResponse, - XAiAskUserQuestionRequest, -} from "../acp/XAiAcpExtension.ts"; -import { type GrokAdapterShape } from "../Services/GrokAdapter.ts"; -import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; - -const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); - -const PROVIDER = ProviderDriverKind.make("grok"); -const GROK_RESUME_VERSION = 1 as const; - -function encodeJsonStringForDiagnostics(input: unknown): string | undefined { - const result = encodeUnknownJsonStringExit(input); - return Exit.isSuccess(result) ? result.value : undefined; -} - -export interface GrokAdapterLiveOptions { - readonly environment?: NodeJS.ProcessEnv; - readonly nativeEventLogPath?: string; - readonly nativeEventLogger?: EventNdjsonLogger; - readonly instanceId?: ProviderInstanceId; -} - -interface PendingApproval { - readonly decision: Deferred.Deferred; -} - -type PendingUserInputResolution = - | { readonly _tag: "answered"; readonly answers: ProviderUserInputAnswers } - | { readonly _tag: "cancelled" }; - -interface PendingUserInput { - readonly resolution: Deferred.Deferred; -} - -interface GrokSessionContext { - readonly threadId: ThreadId; - readonly acpSessionId: string; - session: ProviderSession; - readonly scope: Scope.Closeable; - readonly acp: AcpSessionRuntime.AcpSessionRuntime["Service"]; - notificationFiber: Fiber.Fiber | undefined; - readonly pendingApprovals: Map; - readonly pendingUserInputs: Map; - turns: Array<{ id: TurnId; items: Array }>; - lastPlanFingerprint: string | undefined; - activeTurnId: TurnId | undefined; - /** Number of sendTurn prompts currently in flight or being prepared. - * >0 means a turn is actively running, so a new sendTurn is a steer that - * continues it, and only the last remaining prompt settles the turn. */ - promptsInFlight: number; - currentModelId: string | undefined; - stopped: boolean; -} - -function settlePendingApprovalsAsCancelled( - pendingApprovals: ReadonlyMap, -): Effect.Effect { - return Effect.forEach( - Array.from(pendingApprovals.values()), - (pending) => Deferred.succeed(pending.decision, "cancel").pipe(Effect.ignore), - { discard: true }, - ); -} - -function settlePendingUserInputsAsCancelled( - pendingUserInputs: ReadonlyMap, -): Effect.Effect { - return Effect.forEach( - Array.from(pendingUserInputs.values()), - (pending) => Deferred.succeed(pending.resolution, { _tag: "cancelled" }).pipe(Effect.ignore), - { discard: true }, - ); -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function parseGrokResume(raw: unknown): { sessionId: string } | undefined { - if (!isRecord(raw)) return undefined; - if (raw.schemaVersion !== GROK_RESUME_VERSION) return undefined; - if (typeof raw.sessionId !== "string" || !raw.sessionId.trim()) return undefined; - return { sessionId: raw.sessionId.trim() }; -} - -function selectPermissionOptionId( - request: EffectAcpSchema.RequestPermissionRequest, - decision: Exclude, -): string | undefined { - const kind = - decision === "acceptForSession" - ? "allow_always" - : decision === "accept" - ? "allow_once" - : "reject_once"; - const option = request.options.find((entry) => entry.kind === kind); - return option?.optionId.trim() || undefined; -} - -function selectAutoApprovedPermissionOption( - request: EffectAcpSchema.RequestPermissionRequest, -): string | undefined { - return ( - selectPermissionOptionId(request, "acceptForSession") ?? - selectPermissionOptionId(request, "accept") - ); -} - -export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapterLiveOptions) { - return Effect.gen(function* () { - const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("grok"); - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const serverConfig = yield* Effect.service(ServerConfig); - const crypto = yield* Crypto.Crypto; - const nativeEventLogger = - options?.nativeEventLogger ?? - (options?.nativeEventLogPath !== undefined - ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { stream: "native" }) - : undefined); - const managedNativeEventLogger = - options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; - const makeAcpNativeLoggers = yield* makeAcpNativeLoggerFactory(); - - const sessions = new Map(); - const threadLocksRef = yield* SynchronizedRef.make(new Map()); - const runtimeEventPubSub = yield* PubSub.unbounded(); - - const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - const randomUUIDv4 = crypto.randomUUIDv4.pipe( - Effect.mapError( - (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "crypto/randomUUIDv4", - detail: "Failed to generate Grok runtime identifier.", - cause, - }), - ), - ); - const nextEventId = Effect.map(randomUUIDv4, (id) => EventId.make(id)); - const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); - const mapAcpCallbackFailure = (effect: Effect.Effect) => - effect.pipe( - Effect.mapError( - (cause) => - new EffectAcpErrors.AcpTransportError({ - detail: "Failed to process Grok ACP callback.", - cause, - }), - ), - ); - - const offerRuntimeEvent = (event: ProviderRuntimeEvent) => - PubSub.publish(runtimeEventPubSub, event).pipe(Effect.asVoid); - - const getThreadSemaphore = (threadId: string) => - SynchronizedRef.modifyEffect(threadLocksRef, (current) => { - const existing: Option.Option = Option.fromNullishOr( - current.get(threadId), - ); - return Option.match(existing, { - onNone: () => - Semaphore.make(1).pipe( - Effect.map((semaphore) => { - const next = new Map(current); - next.set(threadId, semaphore); - return [semaphore, next] as const; - }), - ), - onSome: (semaphore) => Effect.succeed([semaphore, current] as const), - }); - }); - - const withThreadLock = (threadId: string, effect: Effect.Effect) => - Effect.flatMap(getThreadSemaphore(threadId), (semaphore) => semaphore.withPermit(effect)); - - const logNative = (threadId: ThreadId, method: string, payload: unknown) => - Effect.gen(function* () { - if (!nativeEventLogger) return; - const observedAt = yield* nowIso; - yield* nativeEventLogger.write( - { - observedAt, - event: { - id: yield* randomUUIDv4, - kind: "notification", - provider: PROVIDER, - createdAt: observedAt, - method, - threadId, - payload, - }, - }, - threadId, - ); - }).pipe( - Effect.catchCause((cause) => - Effect.logWarning("Failed to write native Grok notification log.", { - cause, - threadId, - method, - }), - ), - ); - - const emitPlanUpdate = ( - ctx: GrokSessionContext, - payload: { - readonly explanation?: string | null; - readonly plan: ReadonlyArray<{ - readonly step: string; - readonly status: "pending" | "inProgress" | "completed"; - }>; - }, - rawPayload: unknown, - method: string, - ) => - Effect.gen(function* () { - const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${encodeJsonStringForDiagnostics(payload) ?? "[unserializable payload]"}`; - if (ctx.lastPlanFingerprint === fingerprint) { - return; - } - ctx.lastPlanFingerprint = fingerprint; - yield* offerRuntimeEvent( - makeAcpPlanUpdatedEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - payload, - source: "acp.jsonrpc", - method, - rawPayload, - }), - ); - }); - - const requireSession = ( - threadId: ThreadId, - ): Effect.Effect => { - const ctx = sessions.get(threadId); - if (!ctx || ctx.stopped) { - return Effect.fail( - new ProviderAdapterSessionNotFoundError({ provider: PROVIDER, threadId }), - ); - } - return Effect.succeed(ctx); - }; - - const stopSessionInternal = (ctx: GrokSessionContext) => - Effect.gen(function* () { - if (ctx.stopped) return; - ctx.stopped = true; - yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals); - yield* settlePendingUserInputsAsCancelled(ctx.pendingUserInputs); - if (ctx.notificationFiber) { - yield* Fiber.interrupt(ctx.notificationFiber); - } - yield* Effect.ignore(Scope.close(ctx.scope, Exit.void)); - sessions.delete(ctx.threadId); - yield* offerRuntimeEvent({ - type: "session.exited", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: ctx.threadId, - payload: { exitKind: "graceful" }, - }); - }); - - const startSession: GrokAdapterShape["startSession"] = (input) => - withThreadLock( - input.threadId, - Effect.gen(function* () { - if (input.provider !== undefined && input.provider !== PROVIDER) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "startSession", - issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, - }); - } - if (!input.cwd?.trim()) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "startSession", - issue: "cwd is required and must be non-empty.", - }); - } - - const cwd = path.resolve(input.cwd.trim()); - const grokModelSelection = - input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; - const existing = sessions.get(input.threadId); - if (existing && !existing.stopped) { - yield* stopSessionInternal(existing); - } - - const pendingApprovals = new Map(); - const pendingUserInputs = new Map(); - const sessionScope = yield* Scope.make("sequential"); - let sessionScopeTransferred = false; - yield* Effect.addFinalizer(() => - sessionScopeTransferred ? Effect.void : Scope.close(sessionScope, Exit.void), - ); - - const resumeSessionId = parseGrokResume(input.resumeCursor)?.sessionId; - const acpNativeLoggers = makeAcpNativeLoggers({ - nativeEventLogger, - provider: PROVIDER, - threadId: input.threadId, - }); - - const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); - const acp = yield* makeGrokAcpRuntime({ - grokSettings, - ...(options?.environment ? { environment: options.environment } : {}), - childProcessSpawner, - cwd, - ...(resumeSessionId ? { resumeSessionId } : {}), - clientInfo: { name: "t3-code", version: "0.0.0" }, - ...(mcpSession - ? { - mcpServers: [ - { - type: "http" as const, - name: "t3-code", - url: mcpSession.endpoint, - headers: [ - { - name: "Authorization", - value: mcpSession.authorizationHeader, - }, - ], - }, - ], - } - : {}), - ...acpNativeLoggers, - }).pipe( - Effect.provideService(Scope.Scope, sessionScope), - Effect.mapError( - (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId: input.threadId, - detail: cause.message, - cause, - }), - ), - ); - const started = yield* Effect.gen(function* () { - yield* Effect.forEach( - ["x.ai/ask_user_question", "_x.ai/ask_user_question"] as const, - (method) => - acp.handleExtRequest(method, XAiAskUserQuestionRequest, (params) => - mapAcpCallbackFailure( - Effect.gen(function* () { - yield* logNative(input.threadId, method, params); - const requestId = ApprovalRequestId.make(yield* randomUUIDv4); - const runtimeRequestId = RuntimeRequestId.make(requestId); - const resolution = yield* Deferred.make(); - pendingUserInputs.set(requestId, { resolution }); - yield* offerRuntimeEvent({ - type: "user-input.requested", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId: sessions.get(input.threadId)?.activeTurnId, - requestId: runtimeRequestId, - payload: { questions: extractXAiAskUserQuestions(params) }, - raw: { - source: "acp.grok.extension", - method, - payload: params, - }, - }); - const resolved = yield* Deferred.await(resolution); - pendingUserInputs.delete(requestId); - const resolvedAnswers = resolved._tag === "answered" ? resolved.answers : {}; - yield* offerRuntimeEvent({ - type: "user-input.resolved", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId: sessions.get(input.threadId)?.activeTurnId, - requestId: runtimeRequestId, - payload: { answers: resolvedAnswers }, - raw: { - source: "acp.grok.extension", - method, - payload: params, - }, - }); - switch (resolved._tag) { - case "answered": - return makeXAiAskUserQuestionResponse(params, resolved.answers); - case "cancelled": - return makeXAiAskUserQuestionCancelledResponse(); - } - }), - ), - ), - { discard: true }, - ); - yield* acp.handleRequestPermission((params) => - mapAcpCallbackFailure( - Effect.gen(function* () { - yield* logNative(input.threadId, "session/request_permission", params); - if (input.runtimeMode === "full-access") { - const autoApprovedOptionId = selectAutoApprovedPermissionOption(params); - if (autoApprovedOptionId !== undefined) { - return { - outcome: { - outcome: "selected" as const, - optionId: autoApprovedOptionId, - }, - }; - } - } - const permissionRequest = parsePermissionRequest(params); - const requestId = ApprovalRequestId.make(yield* randomUUIDv4); - const runtimeRequestId = RuntimeRequestId.make(requestId); - const decision = yield* Deferred.make(); - pendingApprovals.set(requestId, { decision }); - yield* offerRuntimeEvent( - makeAcpRequestOpenedEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: input.threadId, - turnId: sessions.get(input.threadId)?.activeTurnId, - requestId: runtimeRequestId, - permissionRequest, - detail: - permissionRequest.detail ?? - encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ?? - "[unserializable params]", - args: params, - source: "acp.jsonrpc", - method: "session/request_permission", - rawPayload: params, - }), - ); - const resolved = yield* Deferred.await(decision); - pendingApprovals.delete(requestId); - yield* offerRuntimeEvent( - makeAcpRequestResolvedEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: input.threadId, - turnId: sessions.get(input.threadId)?.activeTurnId, - requestId: runtimeRequestId, - permissionRequest, - decision: resolved, - }), - ); - const selectedOptionId = - resolved === "cancel" ? undefined : selectPermissionOptionId(params, resolved); - return { - outcome: selectedOptionId - ? { - outcome: "selected" as const, - optionId: selectedOptionId, - } - : ({ outcome: "cancelled" } as const), - }; - }), - ), - ); - return yield* acp.start(); - }).pipe( - Effect.mapError((error) => - mapAcpToAdapterError(PROVIDER, input.threadId, "session/start", error), - ), - ); - - const requestedStartModelId = grokModelSelection?.model - ? resolveGrokAcpBaseModelId(grokModelSelection.model) - : undefined; - const boundModelId = yield* applyGrokAcpModelSelection({ - runtime: acp, - currentModelId: currentGrokModelIdFromSessionSetup(started.sessionSetupResult), - requestedModelId: requestedStartModelId, - mapError: (cause) => - mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause), - }); - - const now = yield* nowIso; - const session: ProviderSession = { - provider: PROVIDER, - providerInstanceId: boundInstanceId, - status: "ready", - runtimeMode: input.runtimeMode, - cwd, - ...(boundModelId ? { model: resolveGrokAcpBaseModelId(boundModelId) } : {}), - threadId: input.threadId, - resumeCursor: { - schemaVersion: GROK_RESUME_VERSION, - sessionId: started.sessionId, - }, - createdAt: now, - updatedAt: now, - }; - - const ctx: GrokSessionContext = { - threadId: input.threadId, - acpSessionId: started.sessionId, - session, - scope: sessionScope, - acp, - notificationFiber: undefined, - pendingApprovals, - pendingUserInputs, - turns: [], - lastPlanFingerprint: undefined, - activeTurnId: undefined, - promptsInFlight: 0, - currentModelId: boundModelId, - stopped: false, - }; - - const nf = yield* Stream.runDrain( - Stream.mapEffect(acp.getEvents(), (event) => - Effect.gen(function* () { - switch (event._tag) { - case "AssistantItemStarted": - yield* offerRuntimeEvent( - makeAcpAssistantItemEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - itemId: event.itemId, - lifecycle: "item.started", - }), - ); - return; - case "AssistantItemCompleted": - yield* offerRuntimeEvent( - makeAcpAssistantItemEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - itemId: event.itemId, - lifecycle: "item.completed", - }), - ); - return; - case "PlanUpdated": - yield* logNative(ctx.threadId, "session/update", event.rawPayload); - yield* emitPlanUpdate(ctx, event.payload, event.rawPayload, "session/update"); - return; - case "ToolCallUpdated": - yield* logNative(ctx.threadId, "session/update", event.rawPayload); - yield* offerRuntimeEvent( - makeAcpToolCallEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - toolCall: event.toolCall, - rawPayload: event.rawPayload, - }), - ); - return; - case "ContentDelta": - yield* logNative(ctx.threadId, "session/update", event.rawPayload); - yield* offerRuntimeEvent( - makeAcpContentDeltaEvent({ - stamp: yield* makeEventStamp(), - provider: PROVIDER, - threadId: ctx.threadId, - turnId: ctx.activeTurnId, - ...(event.itemId ? { itemId: event.itemId } : {}), - text: event.text, - rawPayload: event.rawPayload, - }), - ); - return; - } - }), - ), - ).pipe( - Effect.catch((cause) => - Effect.logError("Failed to process Grok runtime notification.", { cause }), - ), - Effect.forkChild, - ); - - ctx.notificationFiber = nf; - sessions.set(input.threadId, ctx); - sessionScopeTransferred = true; - - yield* offerRuntimeEvent({ - type: "session.started", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - payload: { resume: started.initializeResult }, - }); - yield* offerRuntimeEvent({ - type: "session.state.changed", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - payload: { state: "ready", reason: "Grok ACP session ready" }, - }); - yield* offerRuntimeEvent({ - type: "thread.started", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - payload: { providerThreadId: started.sessionId }, - }); - - return session; - }).pipe(Effect.scoped), - ); - - const sendTurn: GrokAdapterShape["sendTurn"] = (input) => - Effect.gen(function* () { - const prepared = yield* withThreadLock( - input.threadId, - Effect.gen(function* () { - const ctx = yield* requireSession(input.threadId); - // A sendTurn while a prompt is in flight is a steer: the agent - // folds the new prompt into the ongoing work, so the active turn - // id is reused instead of opening a new turn. - const steeringTurnId = ctx.promptsInFlight > 0 ? ctx.activeTurnId : undefined; - const turnId = steeringTurnId ?? TurnId.make(yield* randomUUIDv4); - // Count this prompt immediately so a superseded in-flight prompt - // resolving from here on does not settle the turn; decremented on - // preparation failure here, and after the prompt below otherwise. - ctx.promptsInFlight += 1; - - return yield* Effect.gen(function* () { - const turnModelSelection = - input.modelSelection?.instanceId === boundInstanceId - ? input.modelSelection - : undefined; - const requestedTurnModelId = turnModelSelection?.model - ? resolveGrokAcpBaseModelId(turnModelSelection.model) - : undefined; - const currentModelId = yield* applyGrokAcpModelSelection({ - runtime: ctx.acp, - currentModelId: ctx.currentModelId, - requestedModelId: requestedTurnModelId, - mapError: (cause) => - mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause), - }); - - const text = input.input?.trim(); - const imagePromptParts = yield* Effect.forEach( - input.attachments ?? [], - (attachment) => - Effect.gen(function* () { - const attachmentPath = resolveAttachmentPath({ - attachmentsDir: serverConfig.attachmentsDir, - attachment, - }); - if (!attachmentPath) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "session/prompt", - detail: `Invalid attachment id '${attachment.id}'.`, - }); - } - const bytes = yield* fileSystem.readFile(attachmentPath).pipe( - Effect.mapError( - (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "session/prompt", - detail: cause.message, - cause, - }), - ), - ); - return { - type: "image", - data: Buffer.from(bytes).toString("base64"), - mimeType: attachment.mimeType, - } satisfies EffectAcpSchema.ContentBlock; - }), - ); - const promptParts: Array = [ - ...(text ? [{ type: "text" as const, text }] : []), - ...imagePromptParts, - ]; - - if (promptParts.length === 0) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "sendTurn", - issue: "Turn requires non-empty text or attachments.", - }); - } - - ctx.currentModelId = currentModelId; - const displayModel = currentModelId - ? resolveGrokAcpBaseModelId(currentModelId) - : undefined; - ctx.activeTurnId = turnId; - if (steeringTurnId === undefined) { - ctx.lastPlanFingerprint = undefined; - } - ctx.session = { - ...ctx.session, - activeTurnId: turnId, - updatedAt: yield* nowIso, - ...(displayModel ? { model: displayModel } : {}), - }; - - if (steeringTurnId === undefined) { - yield* offerRuntimeEvent({ - type: "turn.started", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId, - payload: displayModel ? { model: displayModel } : {}, - }); - } - - return { - acp: ctx.acp, - acpSessionId: ctx.acpSessionId, - displayModel, - promptParts, - turnId, - }; - }).pipe( - Effect.tapCause(() => - Effect.sync(() => { - ctx.promptsInFlight = Math.max(0, ctx.promptsInFlight - 1); - }), - ), - ); - }), - ); - - return yield* Effect.gen(function* () { - const result = yield* prepared.acp - .prompt({ - prompt: prepared.promptParts, - }) - .pipe( - Effect.mapError((error) => - mapAcpToAdapterError(PROVIDER, input.threadId, "session/prompt", error), - ), - ); - - return yield* withThreadLock( - input.threadId, - Effect.gen(function* () { - const ctx = yield* requireSession(input.threadId); - if (ctx.acpSessionId !== prepared.acpSessionId) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "session/prompt", - detail: "Grok session changed before the turn completed.", - }); - } - - const existingTurnRecord = ctx.turns.find((turn) => turn.id === prepared.turnId); - ctx.turns = existingTurnRecord - ? ctx.turns.map((turn) => - turn.id === prepared.turnId - ? { - ...turn, - items: [...turn.items, { prompt: prepared.promptParts, result }], - } - : turn, - ) - : [ - ...ctx.turns, - { id: prepared.turnId, items: [{ prompt: prepared.promptParts, result }] }, - ]; - ctx.session = { - ...ctx.session, - activeTurnId: prepared.turnId, - updatedAt: yield* nowIso, - ...(prepared.displayModel ? { model: prepared.displayModel } : {}), - }; - - // Only the last remaining prompt settles the turn — a steer- - // superseded prompt resolving (usually cancelled) while another - // is in flight or pending must leave the merged turn running. - if (ctx.promptsInFlight === 1) { - yield* offerRuntimeEvent({ - type: "turn.completed", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId: prepared.turnId, - payload: { - state: result.stopReason === "cancelled" ? "cancelled" : "completed", - stopReason: result.stopReason ?? null, - }, - }); - } - - return { - threadId: input.threadId, - turnId: prepared.turnId, - resumeCursor: ctx.session.resumeCursor, - }; - }), - ); - }).pipe( - Effect.ensuring( - Effect.sync(() => { - const liveCtx = sessions.get(input.threadId); - if (liveCtx) { - liveCtx.promptsInFlight = Math.max(0, liveCtx.promptsInFlight - 1); - } - }), - ), - ); - }); - - const interruptTurn: GrokAdapterShape["interruptTurn"] = (threadId) => - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals); - yield* settlePendingUserInputsAsCancelled(ctx.pendingUserInputs); - yield* Effect.ignore( - ctx.acp.cancel.pipe( - Effect.mapError((error) => - mapAcpToAdapterError(PROVIDER, threadId, "session/cancel", error), - ), - ), - ); - }); - - const respondToRequest: GrokAdapterShape["respondToRequest"] = ( - threadId, - requestId, - decision, - ) => - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - const pending = ctx.pendingApprovals.get(requestId); - if (!pending) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "session/request_permission", - detail: `Unknown pending approval request: ${requestId}`, - }); - } - yield* Deferred.succeed(pending.decision, decision); - }); - - const respondToUserInput: GrokAdapterShape["respondToUserInput"] = ( - threadId, - requestId, - answers, - ) => - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - const pending = ctx.pendingUserInputs.get(requestId); - if (!pending) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "_x.ai/ask_user_question", - detail: `Unknown pending user-input request: ${requestId}`, - }); - } - yield* Deferred.succeed(pending.resolution, { _tag: "answered", answers }); - }); - - const readThread: GrokAdapterShape["readThread"] = (threadId) => - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - return { threadId, turns: ctx.turns }; - }); - - const rollbackThread: GrokAdapterShape["rollbackThread"] = (threadId, numTurns) => - Effect.gen(function* () { - yield* requireSession(threadId); - if (!Number.isInteger(numTurns) || numTurns < 1) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "rollbackThread", - issue: "numTurns must be an integer >= 1.", - }); - } - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "thread/rollback", - detail: "Grok ACP sessions do not support provider-side rollback yet.", - }); - }); - - const stopSession: GrokAdapterShape["stopSession"] = (threadId) => - withThreadLock( - threadId, - Effect.gen(function* () { - const ctx = yield* requireSession(threadId); - yield* stopSessionInternal(ctx); - }), - ); - - const listSessions: GrokAdapterShape["listSessions"] = () => - Effect.sync(() => Array.from(sessions.values(), (c) => ({ ...c.session }))); - - const hasSession: GrokAdapterShape["hasSession"] = (threadId) => - Effect.sync(() => { - const c = sessions.get(threadId); - return c !== undefined && !c.stopped; - }); - - const stopAll: GrokAdapterShape["stopAll"] = () => - Effect.forEach(Array.from(sessions.values()), stopSessionInternal, { discard: true }); - - yield* Effect.addFinalizer(() => - Effect.ignore(stopAll()).pipe( - Effect.tap(() => PubSub.shutdown(runtimeEventPubSub)), - Effect.tap(() => managedNativeEventLogger?.close() ?? Effect.void), - ), - ); - - const streamEvents = Stream.fromPubSub(runtimeEventPubSub); - - return { - provider: PROVIDER, - capabilities: { sessionModelSwitch: "in-session" }, - startSession, - sendTurn, - interruptTurn, - readThread, - rollbackThread, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - stopAll, - streamEvents, - } satisfies GrokAdapterShape; - }); -} diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts deleted file mode 100644 index d0475e25284..00000000000 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts +++ /dev/null @@ -1,919 +0,0 @@ -import * as NodeAssert from "node:assert/strict"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { it } from "@effect/vitest"; -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Fiber from "effect/Fiber"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import * as TestClock from "effect/testing/TestClock"; -import { beforeEach } from "vite-plus/test"; - -import { - OpenCodeSettings, - ProviderDriverKind, - ProviderInstanceId, - ThreadId, -} from "@t3tools/contracts"; -import { createModelSelection } from "@t3tools/shared/model"; -import { ServerConfig } from "../../config.ts"; -import { ServerSettingsService } from "../../serverSettings.ts"; -import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; -import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; -import { - OpenCodeRuntime, - OpenCodeRuntimeError, - type OpenCodeRuntimeShape, -} from "../opencodeRuntime.ts"; -import { - appendOpenCodeAssistantTextDelta, - makeOpenCodeAdapter, - mergeOpenCodeAssistantText, -} from "./OpenCodeAdapter.ts"; - -// Test-local service tag so the rest of the file can keep using `yield* OpenCodeAdapter`. -class OpenCodeAdapter extends Context.Service()( - "t3/provider/Layers/OpenCodeAdapter.test/OpenCodeAdapter", -) {} - -const asThreadId = (value: string): ThreadId => ThreadId.make(value); - -type MessageEntry = { - info: { - id: string; - role: "user" | "assistant"; - }; - parts: Array; -}; - -const runtimeMock = { - state: { - startCalls: [] as string[], - sessionCreateUrls: [] as string[], - authHeaders: [] as Array, - abortCalls: [] as string[], - closeCalls: [] as string[], - revertCalls: [] as Array<{ sessionID: string; messageID?: string }>, - promptCalls: [] as Array, - promptAsyncError: null as Error | null, - closeError: null as Error | null, - messages: [] as MessageEntry[], - subscribedEvents: [] as unknown[], - }, - reset() { - this.state.startCalls.length = 0; - this.state.sessionCreateUrls.length = 0; - this.state.authHeaders.length = 0; - this.state.abortCalls.length = 0; - this.state.closeCalls.length = 0; - this.state.revertCalls.length = 0; - this.state.promptCalls.length = 0; - this.state.promptAsyncError = null; - this.state.closeError = null; - this.state.messages = []; - this.state.subscribedEvents = []; - }, -}; - -const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { - startOpenCodeServerProcess: ({ binaryPath }) => - Effect.gen(function* () { - runtimeMock.state.startCalls.push(binaryPath); - const url = "http://127.0.0.1:4301"; - yield* Effect.addFinalizer(() => - Effect.sync(() => { - runtimeMock.state.closeCalls.push(url); - if (runtimeMock.state.closeError) { - throw runtimeMock.state.closeError; - } - }), - ); - return { - url, - exitCode: Effect.never, - }; - }), - connectToOpenCodeServer: ({ serverUrl }) => - Effect.gen(function* () { - const url = serverUrl ?? "http://127.0.0.1:4301"; - // Unconditionally register a scope finalizer for test observability — - // preserves the `closeCalls` / `closeError` probes that the existing - // suites rely on. Production code never attaches a finalizer to an - // external server (it simply returns `Effect.succeed(...)`). - yield* Effect.addFinalizer(() => - Effect.sync(() => { - runtimeMock.state.closeCalls.push(url); - if (runtimeMock.state.closeError) { - throw runtimeMock.state.closeError; - } - }), - ); - return { - url, - exitCode: null, - external: Boolean(serverUrl), - }; - }), - runOpenCodeCommand: () => Effect.succeed({ stdout: "", stderr: "", code: 0 }), - createOpenCodeSdkClient: ({ baseUrl, serverPassword }) => - ({ - session: { - create: async () => { - runtimeMock.state.sessionCreateUrls.push(baseUrl); - runtimeMock.state.authHeaders.push( - serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null, - ); - return { data: { id: `${baseUrl}/session` } }; - }, - abort: async ({ sessionID }: { sessionID: string }) => { - runtimeMock.state.abortCalls.push(sessionID); - }, - promptAsync: async (input: unknown) => { - runtimeMock.state.promptCalls.push(input); - if (runtimeMock.state.promptAsyncError) { - throw runtimeMock.state.promptAsyncError; - } - }, - messages: async () => ({ data: runtimeMock.state.messages }), - revert: async ({ sessionID, messageID }: { sessionID: string; messageID?: string }) => { - runtimeMock.state.revertCalls.push({ - sessionID, - ...(messageID ? { messageID } : {}), - }); - if (!messageID) { - runtimeMock.state.messages = []; - return; - } - - const targetIndex = runtimeMock.state.messages.findIndex( - (entry) => entry.info.id === messageID, - ); - runtimeMock.state.messages = - targetIndex >= 0 - ? runtimeMock.state.messages.slice(0, targetIndex + 1) - : runtimeMock.state.messages; - }, - }, - event: { - subscribe: async () => ({ - stream: (async function* () { - for (const event of runtimeMock.state.subscribedEvents) { - yield event; - } - })(), - }), - }, - }) as unknown as ReturnType, - loadOpenCodeInventory: () => - Effect.fail( - new OpenCodeRuntimeError({ - operation: "loadOpenCodeInventory", - detail: "OpenCodeRuntimeTestDouble.loadOpenCodeInventory not used in this test", - cause: null, - }), - ), -}; - -const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory, { - upsert: () => Effect.void, - getProvider: () => - Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")), - getBinding: () => Effect.succeed(Option.none()), - listThreadIds: () => Effect.succeed([]), - listBindings: () => Effect.succeed([]), -}); - -// The adapter now receives its settings as a plain argument (the old design -// read from `ServerSettingsService` internally). The test-only -// `ServerSettingsService` below is still kept because other dependencies in -// the layer graph reach for it — but the routing values the assertions -// probe (serverUrl, serverPassword) must be threaded directly through the -// decoded `OpenCodeSettings`. -const openCodeAdapterTestSettings = Schema.decodeSync(OpenCodeSettings)({ - binaryPath: "fake-opencode", - serverUrl: "http://127.0.0.1:9999", - serverPassword: "secret-password", -}); - -const OpenCodeAdapterTestLayer = Layer.effect( - OpenCodeAdapter, - makeOpenCodeAdapter(openCodeAdapterTestSettings), -).pipe( - Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge( - ServerSettingsService.layerTest({ - providers: { - opencode: { - binaryPath: "fake-opencode", - serverUrl: "http://127.0.0.1:9999", - serverPassword: "secret-password", - }, - }, - }), - ), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), -); - -beforeEach(() => { - runtimeMock.reset(); -}); - -const advanceTestClock = (ms: number) => - TestClock.adjust(`${ms} millis`).pipe(Effect.andThen(Effect.yieldNow)); - -it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { - it.effect("reuses a configured OpenCode server URL instead of spawning a local server", () => - Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - - const session = yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId: asThreadId("thread-opencode"), - runtimeMode: "full-access", - }); - - NodeAssert.equal(session.provider, "opencode"); - NodeAssert.equal(session.threadId, "thread-opencode"); - NodeAssert.deepEqual(runtimeMock.state.startCalls, []); - NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); - NodeAssert.deepEqual(runtimeMock.state.authHeaders, [ - `Basic ${btoa("opencode:secret-password")}`, - ]); - }), - ); - - it.effect("stops a configured-server session without trying to own server lifecycle", () => - Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId: asThreadId("thread-opencode"), - runtimeMode: "full-access", - }); - - yield* adapter.stopSession(asThreadId("thread-opencode")); - - NodeAssert.deepEqual(runtimeMock.state.startCalls, []); - NodeAssert.deepEqual( - runtimeMock.state.abortCalls.includes("http://127.0.0.1:9999/session"), - true, - ); - }), - ); - - it.effect("emits one session.exited event when stopping a session", () => - Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - const threadId = asThreadId("thread-opencode-stop-event"); - const eventsFiber = yield* adapter.streamEvents.pipe( - Stream.filter((event) => event.threadId === threadId), - Stream.take(3), - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId, - runtimeMode: "full-access", - }); - yield* adapter.stopSession(threadId); - - const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second"))); - NodeAssert.deepEqual( - events.map((event) => event.type), - ["session.started", "thread.started", "session.exited"], - ); - }), - ); - - it.effect("clears session state even when cleanup finalizers throw", () => - Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId: asThreadId("thread-stop-all-a"), - runtimeMode: "full-access", - }); - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId: asThreadId("thread-stop-all-b"), - runtimeMode: "full-access", - }); - - runtimeMock.state.closeError = new Error("close failed"); - // `stopAll` relies on `stopOpenCodeContext`, which is typed as - // never-failing. A throwing finalizer surfaces as a defect — `Effect.exit` - // captures it so the assertions can still run. The key invariant we're - // validating is "the sessions map and close-call probes reflect cleanup - // attempts regardless of finalizer outcome". - yield* Effect.exit(adapter.stopAll()); - const sessions = yield* adapter.listSessions(); - - NodeAssert.deepEqual(runtimeMock.state.closeCalls, [ - "http://127.0.0.1:9999", - "http://127.0.0.1:9999", - ]); - NodeAssert.deepEqual(sessions, []); - }), - ); - - it.effect("completes streamEvents when the adapter scope closes", () => - Effect.gen(function* () { - const scope = yield* Scope.make("sequential"); - let scopeClosed = false; - - try { - const adapterLayer = Layer.effect( - OpenCodeAdapter, - makeOpenCodeAdapter(openCodeAdapterTestSettings), - ).pipe( - Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ); - const context = yield* Layer.buildWithScope(adapterLayer, scope); - const adapter = yield* Effect.service(OpenCodeAdapter).pipe(Effect.provide(context)); - const eventsFiber = yield* adapter.streamEvents.pipe(Stream.runCollect, Effect.forkChild); - - yield* Scope.close(scope, Exit.void); - scopeClosed = true; - - const exit = yield* Fiber.await(eventsFiber).pipe(Effect.timeout("1 second")); - NodeAssert.equal(Exit.hasInterrupts(exit), true); - } finally { - if (!scopeClosed) { - yield* Scope.close(scope, Exit.void).pipe(Effect.ignore); - } - } - }), - ); - - it.effect("rolls back session state when sendTurn fails before OpenCode accepts the prompt", () => - Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId: asThreadId("thread-send-turn-failure"), - runtimeMode: "full-access", - }); - - runtimeMock.state.promptAsyncError = new Error("prompt failed"); - const error = yield* adapter - .sendTurn({ - threadId: asThreadId("thread-send-turn-failure"), - input: "Fix it", - modelSelection: { - instanceId: ProviderInstanceId.make("opencode"), - model: "openai/gpt-5", - }, - }) - .pipe(Effect.flip); - const sessions = yield* adapter.listSessions(); - - NodeAssert.equal(error._tag, "ProviderAdapterRequestError"); - if (error._tag !== "ProviderAdapterRequestError") { - throw new Error("Unexpected error type"); - } - NodeAssert.equal(error.detail, "prompt failed"); - NodeAssert.equal( - error.message, - "Provider adapter request failed (opencode) for session.promptAsync: prompt failed", - ); - NodeAssert.equal(sessions.length, 1); - NodeAssert.equal(sessions[0]?.status, "ready"); - NodeAssert.equal(sessions[0]?.activeTurnId, undefined); - NodeAssert.equal(sessions[0]?.lastError, "prompt failed"); - }), - ); - - it.effect("steers a running turn instead of opening a new one on mid-turn sendTurn", () => - Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - const threadId = asThreadId("thread-steer"); - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId, - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId, - input: "run 5 commands", - modelSelection: { - instanceId: ProviderInstanceId.make("opencode"), - model: "openai/gpt-5", - }, - }); - - // Steer: OpenCode queues the prompt into the busy session, so the - // active turn id is reused instead of opening a new turn. - const steeredTurn = yield* adapter.sendTurn({ - threadId, - input: "actually run 15", - modelSelection: { - instanceId: ProviderInstanceId.make("opencode"), - model: "openai/gpt-5", - }, - }); - NodeAssert.equal(String(steeredTurn.turnId), String(turn.turnId)); - - const sessions = yield* adapter.listSessions(); - const session = sessions.find((entry) => entry.threadId === threadId); - NodeAssert.equal(session?.status, "running"); - NodeAssert.equal(String(session?.activeTurnId), String(turn.turnId)); - NodeAssert.equal(runtimeMock.state.promptCalls.length, 2); - }), - ); - - it.effect("keeps the running turn when a steer prompt fails", () => - Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - const threadId = asThreadId("thread-steer-failure"); - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId, - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId, - input: "run 5 commands", - modelSelection: { - instanceId: ProviderInstanceId.make("opencode"), - model: "openai/gpt-5", - }, - }); - - runtimeMock.state.promptAsyncError = new Error("steer failed"); - const error = yield* adapter - .sendTurn({ - threadId, - input: "actually run 15", - modelSelection: { - instanceId: ProviderInstanceId.make("opencode"), - model: "openai/gpt-5", - }, - }) - .pipe(Effect.flip); - - // The original turn keeps running — only the steer prompt failed. - NodeAssert.equal(error._tag, "ProviderAdapterRequestError"); - const sessions = yield* adapter.listSessions(); - const session = sessions.find((entry) => entry.threadId === threadId); - NodeAssert.equal(session?.status, "running"); - NodeAssert.equal(String(session?.activeTurnId), String(turn.turnId)); - }), - ); - - it.effect("passes agent and variant options for the adapter's bound custom instance id", () => { - const instanceId = ProviderInstanceId.make("opencode_zen"); - const adapterLayer = Layer.effect( - OpenCodeAdapter, - makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }), - ).pipe( - Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ); - - return Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId: asThreadId("thread-custom-instance"), - runtimeMode: "full-access", - }); - - yield* adapter.sendTurn({ - threadId: asThreadId("thread-custom-instance"), - input: "Fix it", - modelSelection: createModelSelection( - ProviderInstanceId.make("opencode_zen"), - "anthropic/claude-sonnet-4-5", - [ - { id: "agent", value: "github-copilot" }, - { id: "variant", value: "high" }, - ], - ), - }); - - NodeAssert.deepEqual(runtimeMock.state.promptCalls.at(-1), { - sessionID: "http://127.0.0.1:9999/session", - model: { - providerID: "anthropic", - modelID: "claude-sonnet-4-5", - }, - agent: "github-copilot", - variant: "high", - parts: [{ type: "text", text: "Fix it" }], - }); - }).pipe(Effect.provide(adapterLayer)); - }); - - it.effect("uses the bound custom instance id for fallback sendTurn model selection", () => { - const instanceId = ProviderInstanceId.make("opencode_zen"); - const adapterLayer = Layer.effect( - OpenCodeAdapter, - makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }), - ).pipe( - Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ); - - return Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - const threadId = asThreadId("thread-custom-instance-fallback-model"); - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId, - runtimeMode: "full-access", - modelSelection: createModelSelection( - ProviderInstanceId.make("opencode_zen"), - "anthropic/claude-sonnet-4-5", - ), - }); - - yield* adapter.sendTurn({ - threadId, - input: "Fix it", - }); - - NodeAssert.deepEqual(runtimeMock.state.promptCalls.at(-1), { - sessionID: "http://127.0.0.1:9999/session", - model: { - providerID: "anthropic", - modelID: "claude-sonnet-4-5", - }, - parts: [{ type: "text", text: "Fix it" }], - }); - }).pipe(Effect.provide(adapterLayer)); - }); - - it.effect("rejects sendTurn model selections for another instance id", () => { - const instanceId = ProviderInstanceId.make("opencode_zen"); - const adapterLayer = Layer.effect( - OpenCodeAdapter, - makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }), - ).pipe( - Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge(ServerSettingsService.layerTest()), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ); - - return Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - const threadId = asThreadId("thread-custom-instance-wrong-selection"); - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId, - runtimeMode: "full-access", - }); - - const error = yield* adapter - .sendTurn({ - threadId, - input: "Fix it", - modelSelection: createModelSelection( - ProviderInstanceId.make("opencode"), - "anthropic/claude-sonnet-4-5", - ), - }) - .pipe(Effect.flip); - - NodeAssert.equal(error._tag, "ProviderAdapterValidationError"); - if (error._tag !== "ProviderAdapterValidationError") { - throw new Error("Unexpected error type"); - } - NodeAssert.equal( - error.issue, - "OpenCode model selection is bound to instance 'opencode', expected 'opencode_zen'.", - ); - NodeAssert.deepEqual(runtimeMock.state.promptCalls, []); - }).pipe(Effect.provide(adapterLayer)); - }); - - it.effect("reverts the full thread when rollback removes every assistant turn", () => - Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - const threadId = asThreadId("thread-rollback-all"); - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId, - runtimeMode: "full-access", - }); - - runtimeMock.state.messages = [ - { - info: { id: "assistant-1", role: "assistant" }, - parts: [], - }, - { - info: { id: "assistant-2", role: "assistant" }, - parts: [], - }, - ]; - - const snapshot = yield* adapter.rollbackThread(threadId, 2); - - NodeAssert.deepEqual(runtimeMock.state.revertCalls, [ - { sessionID: "http://127.0.0.1:9999/session" }, - ]); - NodeAssert.deepEqual(snapshot.turns, []); - }), - ); - - it.effect("appends raw assistant text deltas and reconciles part update snapshots", () => - Effect.sync(() => { - const firstUpdate = mergeOpenCodeAssistantText(undefined, "Hello"); - const overlapDelta = appendOpenCodeAssistantTextDelta(firstUpdate.latestText, "lo world"); - const secondUpdate = mergeOpenCodeAssistantText(overlapDelta.nextText, "Hellolo world"); - - NodeAssert.deepEqual( - [firstUpdate.deltaToEmit, overlapDelta.deltaToEmit, secondUpdate.deltaToEmit], - ["Hello", "lo world", ""], - ); - NodeAssert.equal(secondUpdate.latestText, "Hellolo world"); - }), - ); - - it.effect("does not strip coincidental prefix overlap from OpenCode part deltas", () => - Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - const threadId = asThreadId("thread-opencode-raw-delta"); - const part = { - id: "part-raw-delta", - sessionID: "http://127.0.0.1:9999/session", - messageID: "msg-raw-delta", - type: "text", - text: "A B", - time: { start: 1 }, - }; - runtimeMock.state.subscribedEvents = [ - { - type: "message.updated", - properties: { - sessionID: "http://127.0.0.1:9999/session", - info: { - id: "msg-raw-delta", - role: "assistant", - }, - }, - }, - { - type: "message.part.updated", - properties: { - sessionID: "http://127.0.0.1:9999/session", - part, - time: 1, - }, - }, - { - type: "message.part.delta", - properties: { - sessionID: "http://127.0.0.1:9999/session", - messageID: "msg-raw-delta", - partID: "part-raw-delta", - field: "text", - delta: "Bonus", - }, - }, - { - type: "message.part.updated", - properties: { - sessionID: "http://127.0.0.1:9999/session", - part: { - ...part, - text: "A BBonus", - time: { start: 1, end: 2 }, - }, - time: 2, - }, - }, - ]; - const eventsFiber = yield* adapter.streamEvents.pipe( - Stream.filter((event) => event.threadId === threadId), - Stream.take(5), - Stream.runCollect, - Effect.forkChild, - ); - - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId, - runtimeMode: "full-access", - }); - - const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second"))); - const deltas = events.filter((event) => event.type === "content.delta"); - NodeAssert.deepEqual( - deltas.map((event) => (event.type === "content.delta" ? event.payload.delta : "")), - ["A B", "Bonus"], - ); - NodeAssert.equal(events.at(-1)?.type, "item.completed"); - const completed = events.at(-1); - if (completed?.type === "item.completed") { - NodeAssert.equal(completed.payload.detail, "A BBonus"); - } - }), - ); - - it.effect("writes provider-native observability records using the session thread id", () => - Effect.gen(function* () { - const nativeEvents: Array<{ - readonly event?: { - readonly provider?: string; - readonly threadId?: string; - readonly providerThreadId?: string; - readonly type?: string; - }; - }> = []; - const nativeThreadIds: Array = []; - runtimeMock.state.subscribedEvents = [ - { - type: "message.updated", - properties: { - info: { - id: "msg-missing-session", - role: "assistant", - }, - }, - }, - { - type: "message.updated", - properties: { - sessionID: "http://127.0.0.1:9999/other-session", - info: { - id: "msg-other-session", - role: "assistant", - }, - }, - }, - { - type: "message.updated", - properties: { - sessionID: "http://127.0.0.1:9999/session", - info: { - id: "msg-native-log", - role: "assistant", - }, - }, - }, - ]; - - const nativeEventLogger = { - filePath: "memory://opencode-native-events", - write: (event: unknown, threadId: ThreadId | null) => { - nativeEvents.push(event as (typeof nativeEvents)[number]); - nativeThreadIds.push(threadId ?? null); - return Effect.void; - }, - close: () => Effect.void, - }; - - const adapterLayer = Layer.effect( - OpenCodeAdapter, - makeOpenCodeAdapter(openCodeAdapterTestSettings, { - nativeEventLogger, - }), - ).pipe( - Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge( - ServerSettingsService.layerTest({ - providers: { - opencode: { - binaryPath: "fake-opencode", - serverUrl: "http://127.0.0.1:9999", - serverPassword: "secret-password", - }, - }, - }), - ), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ); - - const session = yield* Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - const started = yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId: asThreadId("thread-native-log"), - runtimeMode: "full-access", - }); - yield* advanceTestClock(10); - return started; - }).pipe(Effect.provide(adapterLayer)); - - NodeAssert.equal(session.threadId, "thread-native-log"); - NodeAssert.equal(nativeEvents.length, 1); - NodeAssert.equal( - nativeEvents.some((record) => record.event?.provider === "opencode"), - true, - ); - NodeAssert.equal( - nativeEvents.some( - (record) => record.event?.providerThreadId === "http://127.0.0.1:9999/session", - ), - true, - ); - NodeAssert.equal( - nativeEvents.some((record) => record.event?.threadId === "thread-native-log"), - true, - ); - NodeAssert.equal( - nativeEvents.some((record) => record.event?.type === "message.updated"), - true, - ); - NodeAssert.equal( - nativeThreadIds.every((threadId) => threadId === "thread-native-log"), - true, - ); - }), - ); - - it.effect("keeps the event pump alive when native event logging fails", () => - Effect.gen(function* () { - runtimeMock.state.subscribedEvents = [ - { - type: "message.updated", - properties: { - sessionID: "http://127.0.0.1:9999/session", - info: { - id: "msg-native-log-failure", - role: "assistant", - }, - }, - }, - ]; - - const nativeEventLogger = { - filePath: "memory://opencode-native-events", - write: () => Effect.die(new Error("native log write failed")), - close: () => Effect.void, - }; - - const adapterLayer = Layer.effect( - OpenCodeAdapter, - makeOpenCodeAdapter(openCodeAdapterTestSettings, { - nativeEventLogger, - }), - ).pipe( - Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)), - Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), - Layer.provideMerge( - ServerSettingsService.layerTest({ - providers: { - opencode: { - binaryPath: "fake-opencode", - serverUrl: "http://127.0.0.1:9999", - serverPassword: "secret-password", - }, - }, - }), - ), - Layer.provideMerge(providerSessionDirectoryTestLayer), - Layer.provideMerge(NodeServices.layer), - ); - - // Capture closeCalls *inside* the provided layer scope: the adapter's - // layer finalizer now tears down any live sessions when the layer - // closes (which is exactly what we want for leak prevention), so - // inspecting closeCalls after `Effect.provide` completes would observe - // the teardown — not the behavior under test. We care that the event - // pump kept the session alive while logging was failing. - const { sessions, closeCallsDuringRun } = yield* Effect.gen(function* () { - const adapter = yield* OpenCodeAdapter; - yield* adapter.startSession({ - provider: ProviderDriverKind.make("opencode"), - threadId: asThreadId("thread-native-log-failure"), - runtimeMode: "full-access", - }); - yield* advanceTestClock(10); - return { - sessions: yield* adapter.listSessions(), - closeCallsDuringRun: [...runtimeMock.state.closeCalls], - }; - }).pipe(Effect.provide(adapterLayer)); - - NodeAssert.equal(sessions.length, 1); - NodeAssert.equal(sessions[0]?.threadId, "thread-native-log-failure"); - NodeAssert.deepEqual(closeCallsDuringRun, []); - }), - ); -}); diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.ts deleted file mode 100644 index 1eb6e47bc19..00000000000 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.ts +++ /dev/null @@ -1,1473 +0,0 @@ -import { - EventId, - type OpenCodeSettings, - ProviderDriverKind, - ProviderInstanceId, - type ProviderRuntimeEvent, - type ProviderSession, - RuntimeItemId, - RuntimeRequestId, - ThreadId, - type ToolLifecycleItemType, - TurnId, - type UserInputQuestion, -} from "@t3tools/contracts"; -import * as Cause from "effect/Cause"; -import * as Crypto from "effect/Crypto"; -import * as DateTime from "effect/DateTime"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Queue from "effect/Queue"; -import * as Ref from "effect/Ref"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import type { OpencodeClient, Part, PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2"; -import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; - -import { resolveAttachmentPath } from "../../attachmentStore.ts"; -import { ServerConfig } from "../../config.ts"; -import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; -import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; -import { - ProviderAdapterProcessError, - ProviderAdapterRequestError, - ProviderAdapterSessionClosedError, - ProviderAdapterSessionNotFoundError, - ProviderAdapterValidationError, -} from "../Errors.ts"; -import { type OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; -import { - buildOpenCodePermissionRules, - OpenCodeRuntime, - OpenCodeRuntimeError, - openCodeQuestionId, - openCodeRuntimeErrorDetail, - parseOpenCodeModelSlug, - runOpenCodeSdk, - toOpenCodeFileParts, - toOpenCodePermissionReply, - toOpenCodeQuestionAnswers, - type OpenCodeServerConnection, -} from "../opencodeRuntime.ts"; -import * as Option from "effect/Option"; - -const PROVIDER = ProviderDriverKind.make("opencode"); - -interface OpenCodeTurnSnapshot { - readonly id: TurnId; - readonly items: Array; -} - -type OpenCodeSubscribedEvent = - Awaited> extends { - readonly stream: AsyncIterable; - } - ? TEvent - : never; - -interface OpenCodeSessionContext { - session: ProviderSession; - readonly client: OpencodeClient; - readonly server: OpenCodeServerConnection; - readonly directory: string; - readonly openCodeSessionId: string; - readonly pendingPermissions: Map; - readonly pendingQuestions: Map; - readonly messageRoleById: Map; - readonly partById: Map; - readonly emittedTextByPartId: Map; - readonly completedAssistantPartIds: Set; - readonly turns: Array; - activeTurnId: TurnId | undefined; - activeAgent: string | undefined; - activeVariant: string | undefined; - /** - * One-shot guard flipped by `stopOpenCodeContext` / `emitUnexpectedExit`. - * The session lifecycle is owned by `sessionScope`; this Ref exists only - * so concurrent callers can race the transition safely via `getAndSet`. - */ - readonly stopped: Ref.Ref; - /** - * Sole lifecycle handle for the session. Closing this scope: - * - aborts the `AbortController` registered as a finalizer - * (cancels the in-flight `event.subscribe` fetch), - * - interrupts the event-pump and server-exit fibers forked - * via `Effect.forkIn(sessionScope)`, - * - tears down the OpenCode server process for scope-owned servers. - */ - readonly sessionScope: Scope.Closeable; -} - -export interface OpenCodeAdapterLiveOptions { - readonly instanceId?: ProviderInstanceId; - readonly environment?: NodeJS.ProcessEnv; - readonly nativeEventLogPath?: string; - readonly nativeEventLogger?: EventNdjsonLogger; -} - -const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - -/** - * Map a tagged OpenCodeRuntimeError produced by {@link runOpenCodeSdk} into - * the adapter-boundary `ProviderAdapterRequestError`. SDK-method-level call - * sites pipe through this in `Effect.mapError` so they never build the error - * shape by hand. - */ -const toRequestError = (cause: OpenCodeRuntimeError): ProviderAdapterRequestError => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: cause.operation, - detail: cause.detail, - cause: cause.cause, - }); - -/** - * Map a `Cause.squash`-ed failure into a `ProviderAdapterProcessError`. The - * typed cause is usually an `OpenCodeRuntimeError` (from {@link runOpenCodeSdk}), - * in which case we preserve its `detail`; otherwise we fall back to - * {@link openCodeRuntimeErrorDetail} for unknown causes (defects, etc.). - */ -const toProcessError = (threadId: ThreadId, cause: unknown): ProviderAdapterProcessError => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId, - detail: OpenCodeRuntimeError.is(cause) ? cause.detail : openCodeRuntimeErrorDetail(cause), - cause, - }); - -type EventBaseInput = { - readonly threadId: ThreadId; - readonly turnId?: TurnId | undefined; - readonly itemId?: string | undefined; - readonly requestId?: string | undefined; - readonly createdAt?: string | undefined; - readonly raw?: unknown; -}; - -function toToolLifecycleItemType(toolName: string): ToolLifecycleItemType { - const normalized = toolName.toLowerCase(); - if (normalized.includes("bash") || normalized.includes("command")) { - return "command_execution"; - } - if ( - normalized.includes("edit") || - normalized.includes("write") || - normalized.includes("patch") || - normalized.includes("multiedit") - ) { - return "file_change"; - } - if (normalized.includes("web")) { - return "web_search"; - } - if (normalized.includes("mcp")) { - return "mcp_tool_call"; - } - if (normalized.includes("image")) { - return "image_view"; - } - if ( - normalized.includes("task") || - normalized.includes("agent") || - normalized.includes("subtask") - ) { - return "collab_agent_tool_call"; - } - return "dynamic_tool_call"; -} - -function mapPermissionToRequestType( - permission: string, -): "command_execution_approval" | "file_read_approval" | "file_change_approval" | "unknown" { - switch (permission) { - case "bash": - return "command_execution_approval"; - case "read": - return "file_read_approval"; - case "edit": - return "file_change_approval"; - default: - return "unknown"; - } -} - -function mapPermissionDecision(reply: "once" | "always" | "reject"): string { - switch (reply) { - case "once": - return "accept"; - case "always": - return "acceptForSession"; - case "reject": - default: - return "decline"; - } -} - -function resolveTurnSnapshot( - context: OpenCodeSessionContext, - turnId: TurnId, -): OpenCodeTurnSnapshot { - const existing = context.turns.find((turn) => turn.id === turnId); - if (existing) { - return existing; - } - - const created: OpenCodeTurnSnapshot = { id: turnId, items: [] }; - context.turns.push(created); - return created; -} - -function appendTurnItem( - context: OpenCodeSessionContext, - turnId: TurnId | undefined, - item: unknown, -): void { - if (!turnId) { - return; - } - resolveTurnSnapshot(context, turnId).items.push(item); -} - -function ensureSessionContext( - sessions: ReadonlyMap, - threadId: ThreadId, -): OpenCodeSessionContext { - const session = sessions.get(threadId); - if (!session) { - throw new ProviderAdapterSessionNotFoundError({ - provider: PROVIDER, - threadId, - }); - } - // `ensureSessionContext` is a sync gate used from both sync helpers and - // Effect bodies. `Ref.getUnsafe` is an atomic read of the backing cell — - // no fiber suspension required, which keeps this callable everywhere. - if (Ref.getUnsafe(session.stopped)) { - throw new ProviderAdapterSessionClosedError({ - provider: PROVIDER, - threadId, - }); - } - return session; -} - -function normalizeQuestionRequest(request: QuestionRequest): ReadonlyArray { - return request.questions.map((question, index) => ({ - id: openCodeQuestionId(index, question), - header: question.header, - question: question.question, - options: question.options.map((option) => ({ - label: option.label, - description: option.description, - })), - ...(question.multiple ? { multiSelect: true } : {}), - })); -} - -function resolveTextStreamKind(part: Part | undefined): "assistant_text" | "reasoning_text" { - return part?.type === "reasoning" ? "reasoning_text" : "assistant_text"; -} - -function textFromPart(part: Part): string | undefined { - switch (part.type) { - case "text": - case "reasoning": - return part.text; - default: - return undefined; - } -} - -function commonPrefixLength(left: string, right: string): number { - let index = 0; - while (index < left.length && index < right.length && left[index] === right[index]) { - index += 1; - } - return index; -} - -function resolveLatestAssistantText(previousText: string | undefined, nextText: string): string { - if (previousText && previousText.length > nextText.length && previousText.startsWith(nextText)) { - return previousText; - } - return nextText; -} - -export function mergeOpenCodeAssistantText( - previousText: string | undefined, - nextText: string, -): { - readonly latestText: string; - readonly deltaToEmit: string; -} { - const latestText = resolveLatestAssistantText(previousText, nextText); - return { - latestText, - deltaToEmit: latestText.slice(commonPrefixLength(previousText ?? "", latestText)), - }; -} - -export function appendOpenCodeAssistantTextDelta( - previousText: string, - delta: string, -): { - readonly nextText: string; - readonly deltaToEmit: string; -} { - return { - nextText: previousText + delta, - deltaToEmit: delta, - }; -} - -const isoFromEpochMs = (value: number) => - DateTime.make(value).pipe( - Option.match({ - onNone: () => undefined, - onSome: DateTime.formatIso, - }), - ); - -function messageRoleForPart( - context: OpenCodeSessionContext, - part: Pick, -): "assistant" | "user" | undefined { - const known = context.messageRoleById.get(part.messageID); - if (known) { - return known; - } - return part.type === "tool" ? "assistant" : undefined; -} - -function detailFromToolPart(part: Extract): string | undefined { - switch (part.state.status) { - case "completed": - return part.state.output; - case "error": - return part.state.error; - case "running": - return part.state.title; - default: - return undefined; - } -} - -function toolStateCreatedAt(part: Extract): string | undefined { - switch (part.state.status) { - case "running": - return isoFromEpochMs(part.state.time.start); - case "completed": - case "error": - return isoFromEpochMs(part.state.time.end); - default: - return undefined; - } -} - -function sessionErrorMessage(error: unknown): string { - if (!error || typeof error !== "object") { - return "OpenCode session failed."; - } - const data = "data" in error && error.data && typeof error.data === "object" ? error.data : null; - const message = data && "message" in data ? data.message : null; - return typeof message === "string" && message.trim().length > 0 - ? message - : "OpenCode session failed."; -} - -function updateProviderSession( - context: OpenCodeSessionContext, - patch: Partial, - options?: { - readonly clearActiveTurnId?: boolean; - readonly clearLastError?: boolean; - }, -): Effect.Effect { - return Effect.gen(function* () { - const updatedAt = yield* nowIso; - const nextSession = { - ...context.session, - ...patch, - updatedAt, - } as ProviderSession & Record; - const mutableSession = nextSession as Record; - if (options?.clearActiveTurnId) { - delete mutableSession.activeTurnId; - } - if (options?.clearLastError) { - delete mutableSession.lastError; - } - context.session = nextSession; - return nextSession; - }); -} - -const stopOpenCodeContext = Effect.fn("stopOpenCodeContext")(function* ( - context: OpenCodeSessionContext, -) { - // Race-safe one-shot: first caller flips the flag, everyone else no-ops. - if (yield* Ref.getAndSet(context.stopped, true)) { - return false; - } - - // Best-effort remote abort. The scope close below tears down the local - // handles (event-pump fiber, server-exit fiber, event-subscribe fetch), - // but we still want to tell OpenCode that this session is done. - yield* runOpenCodeSdk("session.abort", () => - context.client.session.abort({ sessionID: context.openCodeSessionId }), - ).pipe(Effect.ignore({ log: true })); - - // Closing the session scope interrupts every fiber forked into it and - // runs each finalizer we registered — the `AbortController.abort()` call, - // the child-process termination, etc. - yield* Scope.close(context.sessionScope, Exit.void); - return true; -}); - -export function makeOpenCodeAdapter( - openCodeSettings: OpenCodeSettings, - options?: OpenCodeAdapterLiveOptions, -) { - return Effect.gen(function* () { - const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("opencode"); - const serverConfig = yield* ServerConfig; - const openCodeRuntime = yield* OpenCodeRuntime; - const crypto = yield* Crypto.Crypto; - const nativeEventLogger = - options?.nativeEventLogger ?? - (options?.nativeEventLogPath !== undefined - ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { - stream: "native", - }) - : undefined); - // Only close loggers we created. If the caller passed one in via - // `options.nativeEventLogger`, they own its lifecycle. - const managedNativeEventLogger = - options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; - const runtimeEvents = yield* Queue.unbounded(); - const sessions = new Map(); - const randomUUIDv4 = crypto.randomUUIDv4.pipe( - Effect.mapError( - (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "crypto/randomUUIDv4", - detail: "Failed to generate OpenCode runtime identifier.", - cause, - }), - ), - ); - const buildEventBase = (input: EventBaseInput) => - Effect.all({ - eventId: randomUUIDv4.pipe(Effect.map(EventId.make)), - createdAt: input.createdAt === undefined ? nowIso : Effect.succeed(input.createdAt), - }).pipe( - Effect.map(({ eventId, createdAt }) => ({ - eventId, - provider: PROVIDER, - threadId: input.threadId, - createdAt, - ...(input.turnId ? { turnId: input.turnId } : {}), - ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}), - ...(input.requestId ? { requestId: RuntimeRequestId.make(input.requestId) } : {}), - ...(input.raw !== undefined - ? { - raw: { - source: "opencode.sdk.event" as const, - payload: input.raw, - }, - } - : {}), - })), - ); - - // Layer-level finalizer: when the adapter layer shuts down, stop every - // session. Each session's `Scope.close` tears down its spawned OpenCode - // server (via the `ChildProcessSpawner` finalizer installed in - // `startOpenCodeServerProcess`) and interrupts the forked event/exit - // fibers. Consumers that can't reason about Effect scopes therefore - // cannot leak OpenCode child processes by forgetting to call `stopAll`. - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - const contexts = [...sessions.values()]; - sessions.clear(); - // `ignoreCause` swallows both typed failures (none here) and defects - // from throwing scope finalizers so a sibling's death can't interrupt - // the remaining cleanups. - yield* Effect.forEach( - contexts, - (context) => Effect.ignoreCause(stopOpenCodeContext(context)), - { concurrency: "unbounded", discard: true }, - ); - // Close the logger AFTER session teardown so any final lifecycle - // events emitted during shutdown still get written. `close` flushes - // the `Logger.batched` window and closes each per-thread - // `RotatingFileSink` handle owned by the logger's internal scope. - if (managedNativeEventLogger !== undefined) { - yield* managedNativeEventLogger.close(); - } - }).pipe(Effect.ensuring(Queue.shutdown(runtimeEvents))), - ); - - const emit = (event: ProviderRuntimeEvent) => - Queue.offer(runtimeEvents, event).pipe(Effect.asVoid); - const writeNativeEvent = ( - threadId: ThreadId, - event: { - readonly observedAt: string; - readonly event: Record; - }, - ) => (nativeEventLogger ? nativeEventLogger.write(event, threadId) : Effect.void); - const writeNativeEventBestEffort = ( - threadId: ThreadId, - event: { - readonly observedAt: string; - readonly event: Record; - }, - ) => writeNativeEvent(threadId, event).pipe(Effect.catchCause(() => Effect.void)); - - const emitUnexpectedExit = Effect.fn("emitUnexpectedExit")(function* ( - context: OpenCodeSessionContext, - message: string, - ) { - // Atomic one-shot: two fibers can race here (the event-pump on stream - // failure and the server-exit watcher). `getAndSet` flips the flag in - // a single step so the loser observes `true` and returns; a plain - // `Ref.get` would let both racers slip past and emit duplicates. - if (yield* Ref.getAndSet(context.stopped, true)) { - return; - } - const turnId = context.activeTurnId; - sessions.delete(context.session.threadId); - // Emit lifecycle events BEFORE tearing down the scope. Both call sites - // run this inside a fiber forked via `Effect.forkIn(context.sessionScope)`; - // closing that scope triggers the fiber-interrupt finalizer, so any - // subsequent yield point would unwind and silently drop these emits. - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - })), - type: "runtime.error", - payload: { - message, - class: "transport_error", - }, - }).pipe(Effect.ignore); - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - })), - type: "session.exited", - payload: { - reason: message, - recoverable: false, - exitKind: "error", - }, - }).pipe(Effect.ignore); - // Inline the teardown that `stopOpenCodeContext` would do; we can't - // delegate to it because our `getAndSet` above already flipped the - // one-shot guard, so the call would no-op. - yield* runOpenCodeSdk("session.abort", () => - context.client.session.abort({ sessionID: context.openCodeSessionId }), - ).pipe(Effect.ignore({ log: true })); - yield* Scope.close(context.sessionScope, Exit.void); - }); - - /** Emit content.delta and item.completed events for an assistant text part. */ - const emitAssistantTextDelta = Effect.fn("emitAssistantTextDelta")(function* ( - context: OpenCodeSessionContext, - part: Part, - turnId: TurnId | undefined, - raw: unknown, - ) { - const text = textFromPart(part); - if (text === undefined) { - return; - } - const previousText = context.emittedTextByPartId.get(part.id); - const { latestText, deltaToEmit } = mergeOpenCodeAssistantText(previousText, text); - context.emittedTextByPartId.set(part.id, latestText); - if (latestText !== text) { - context.partById.set( - part.id, - (part.type === "text" || part.type === "reasoning" - ? { ...part, text: latestText } - : part) satisfies Part, - ); - } - if (deltaToEmit.length > 0) { - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - itemId: part.id, - createdAt: - (part.type === "text" || part.type === "reasoning") && part.time !== undefined - ? isoFromEpochMs(part.time.start) - : undefined, - raw, - })), - type: "content.delta", - payload: { - streamKind: resolveTextStreamKind(part), - delta: deltaToEmit, - }, - }); - } - - if ( - part.type === "text" && - part.time?.end !== undefined && - !context.completedAssistantPartIds.has(part.id) - ) { - context.completedAssistantPartIds.add(part.id); - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - itemId: part.id, - createdAt: isoFromEpochMs(part.time.end), - raw, - })), - type: "item.completed", - payload: { - itemType: "assistant_message", - status: "completed", - title: "Assistant message", - ...(latestText.length > 0 ? { detail: latestText } : {}), - }, - }); - } - }); - - const handleSubscribedEvent = Effect.fn("handleSubscribedEvent")(function* ( - context: OpenCodeSessionContext, - event: OpenCodeSubscribedEvent, - ) { - const payloadSessionId = - "properties" in event ? (event.properties as { sessionID?: unknown }).sessionID : undefined; - if (payloadSessionId !== context.openCodeSessionId) { - return; - } - - const turnId = context.activeTurnId; - yield* writeNativeEventBestEffort(context.session.threadId, { - observedAt: yield* nowIso, - event: { - provider: PROVIDER, - threadId: context.session.threadId, - providerThreadId: context.openCodeSessionId, - type: event.type, - ...(turnId ? { turnId } : {}), - payload: event, - }, - }); - - switch (event.type) { - case "message.updated": { - context.messageRoleById.set(event.properties.info.id, event.properties.info.role); - if (event.properties.info.role === "assistant") { - for (const part of context.partById.values()) { - if (part.messageID !== event.properties.info.id) { - continue; - } - yield* emitAssistantTextDelta(context, part, turnId, event); - } - } - break; - } - - case "message.removed": { - context.messageRoleById.delete(event.properties.messageID); - break; - } - - case "message.part.delta": { - const existingPart = context.partById.get(event.properties.partID); - if (!existingPart) { - break; - } - const role = messageRoleForPart(context, existingPart); - if (role !== "assistant") { - break; - } - const streamKind = resolveTextStreamKind(existingPart); - const delta = event.properties.delta; - if (delta.length === 0) { - break; - } - const previousText = - context.emittedTextByPartId.get(event.properties.partID) ?? - textFromPart(existingPart) ?? - ""; - const { nextText, deltaToEmit } = appendOpenCodeAssistantTextDelta(previousText, delta); - if (deltaToEmit.length === 0) { - break; - } - context.emittedTextByPartId.set(event.properties.partID, nextText); - if (existingPart.type === "text" || existingPart.type === "reasoning") { - context.partById.set(event.properties.partID, { - ...existingPart, - text: nextText, - }); - } - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - itemId: event.properties.partID, - raw: event, - })), - type: "content.delta", - payload: { - streamKind, - delta: deltaToEmit, - }, - }); - break; - } - - case "message.part.updated": { - const part = event.properties.part; - context.partById.set(part.id, part); - const messageRole = messageRoleForPart(context, part); - - if (messageRole === "assistant") { - yield* emitAssistantTextDelta(context, part, turnId, event); - } - - if (part.type === "tool") { - const itemType = toToolLifecycleItemType(part.tool); - const title = - part.state.status === "running" ? (part.state.title ?? part.tool) : part.tool; - const detail = detailFromToolPart(part); - const payload = { - itemType, - ...(part.state.status === "error" - ? { status: "failed" as const } - : part.state.status === "completed" - ? { status: "completed" as const } - : { status: "inProgress" as const }), - ...(title ? { title } : {}), - ...(detail ? { detail } : {}), - data: { - tool: part.tool, - state: part.state, - }, - }; - const runtimeEvent: ProviderRuntimeEvent = { - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - itemId: part.callID, - createdAt: toolStateCreatedAt(part), - raw: event, - })), - type: - part.state.status === "pending" - ? "item.started" - : part.state.status === "completed" || part.state.status === "error" - ? "item.completed" - : "item.updated", - payload, - }; - appendTurnItem(context, turnId, part); - yield* emit(runtimeEvent); - } - break; - } - - case "permission.asked": { - context.pendingPermissions.set(event.properties.id, event.properties); - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - requestId: event.properties.id, - raw: event, - })), - type: "request.opened", - payload: { - requestType: mapPermissionToRequestType(event.properties.permission), - detail: - event.properties.patterns.length > 0 - ? event.properties.patterns.join("\n") - : event.properties.permission, - args: event.properties.metadata, - }, - }); - break; - } - - case "permission.replied": { - context.pendingPermissions.delete(event.properties.requestID); - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - requestId: event.properties.requestID, - raw: event, - })), - type: "request.resolved", - payload: { - requestType: "unknown", - decision: mapPermissionDecision(event.properties.reply), - }, - }); - break; - } - - case "question.asked": { - context.pendingQuestions.set(event.properties.id, event.properties); - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - requestId: event.properties.id, - raw: event, - })), - type: "user-input.requested", - payload: { - questions: normalizeQuestionRequest(event.properties), - }, - }); - break; - } - - case "question.replied": { - const request = context.pendingQuestions.get(event.properties.requestID); - context.pendingQuestions.delete(event.properties.requestID); - const answers = Object.fromEntries( - (request?.questions ?? []).map((question, index) => [ - openCodeQuestionId(index, question), - event.properties.answers[index]?.join(", ") ?? "", - ]), - ); - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - requestId: event.properties.requestID, - raw: event, - })), - type: "user-input.resolved", - payload: { answers }, - }); - break; - } - - case "question.rejected": { - context.pendingQuestions.delete(event.properties.requestID); - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - requestId: event.properties.requestID, - raw: event, - })), - type: "user-input.resolved", - payload: { answers: {} }, - }); - break; - } - - case "session.status": { - if (event.properties.status.type === "busy") { - yield* updateProviderSession(context, { - status: "running", - activeTurnId: turnId, - }); - } - - if (event.properties.status.type === "retry") { - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - raw: event, - })), - type: "runtime.warning", - payload: { - message: event.properties.status.message, - detail: event.properties.status, - }, - }); - break; - } - - if (event.properties.status.type === "idle" && turnId) { - context.activeTurnId = undefined; - yield* updateProviderSession(context, { status: "ready" }, { clearActiveTurnId: true }); - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId, - raw: event, - })), - type: "turn.completed", - payload: { - state: "completed", - }, - }); - } - break; - } - - case "session.error": { - const message = sessionErrorMessage(event.properties.error); - const activeTurnId = context.activeTurnId; - context.activeTurnId = undefined; - yield* updateProviderSession( - context, - { - status: "error", - lastError: message, - }, - { clearActiveTurnId: true }, - ); - if (activeTurnId) { - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - turnId: activeTurnId, - raw: event, - })), - type: "turn.completed", - payload: { - state: "failed", - errorMessage: message, - }, - }); - } - yield* emit({ - ...(yield* buildEventBase({ - threadId: context.session.threadId, - raw: event, - })), - type: "runtime.error", - payload: { - message, - class: "provider_error", - detail: event.properties.error, - }, - }); - break; - } - - default: - break; - } - }); - - const startEventPump = Effect.fn("startEventPump")(function* (context: OpenCodeSessionContext) { - // One AbortController per session scope. The finalizer fires when - // the scope closes (explicit stop, unexpected exit, or layer - // shutdown) and cancels the in-flight `event.subscribe` fetch so - // the async iterable unwinds cleanly. - const eventsAbortController = new AbortController(); - yield* Scope.addFinalizer( - context.sessionScope, - Effect.sync(() => eventsAbortController.abort()), - ); - - // Fibers forked into `context.sessionScope` are interrupted - // automatically when the scope closes — no bookkeeping required. - yield* Effect.flatMap( - runOpenCodeSdk("event.subscribe", () => - context.client.event.subscribe(undefined, { - signal: eventsAbortController.signal, - }), - ), - (subscription) => - Stream.fromAsyncIterable( - subscription.stream, - (cause) => - new OpenCodeRuntimeError({ - operation: "event.subscribe", - detail: openCodeRuntimeErrorDetail(cause), - cause, - }), - ).pipe(Stream.runForEach((event) => handleSubscribedEvent(context, event))), - ).pipe( - Effect.exit, - Effect.flatMap((exit) => - Effect.gen(function* () { - // Expected paths: caller aborted the fetch or the session - // has already been marked stopped. Treat as a clean exit. - if (eventsAbortController.signal.aborted || (yield* Ref.get(context.stopped))) { - return; - } - if (Exit.isFailure(exit)) { - yield* emitUnexpectedExit( - context, - openCodeRuntimeErrorDetail(Cause.squash(exit.cause)), - ); - } - }), - ), - Effect.forkIn(context.sessionScope), - ); - - if (!context.server.external && context.server.exitCode !== null) { - yield* context.server.exitCode.pipe( - Effect.flatMap((code) => - Effect.gen(function* () { - if (yield* Ref.get(context.stopped)) { - return; - } - yield* emitUnexpectedExit(context, `OpenCode server exited unexpectedly (${code}).`); - }), - ), - Effect.forkIn(context.sessionScope), - ); - } - }); - - const startSession: OpenCodeAdapterShape["startSession"] = Effect.fn("startSession")( - function* (input) { - const binaryPath = openCodeSettings.binaryPath; - const serverUrl = openCodeSettings.serverUrl; - const serverPassword = openCodeSettings.serverPassword; - const directory = input.cwd ?? serverConfig.cwd; - const existing = sessions.get(input.threadId); - if (existing) { - yield* stopOpenCodeContext(existing); - sessions.delete(input.threadId); - } - - const started = yield* Effect.gen(function* () { - const sessionScope = yield* Scope.make(); - const startedExit = yield* Effect.exit( - Effect.gen(function* () { - // The runtime binds the server's lifetime to the Scope.Scope - // we provide below — closing `sessionScope` kills the child - // process automatically. No manual `server.close()` needed. - const server = yield* openCodeRuntime.connectToOpenCodeServer({ - binaryPath, - serverUrl, - ...(options?.environment ? { environment: options.environment } : {}), - }); - const client = openCodeRuntime.createOpenCodeSdkClient({ - baseUrl: server.url, - directory, - ...(server.external && serverPassword ? { serverPassword } : {}), - }); - const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); - if (mcpSession && !server.external) { - yield* runOpenCodeSdk("mcp.add", () => - client.mcp.add({ - name: "t3-code", - config: { - type: "remote", - url: mcpSession.endpoint, - headers: { - Authorization: mcpSession.authorizationHeader, - }, - oauth: false, - }, - }), - ); - } - const openCodeSession = yield* runOpenCodeSdk("session.create", () => - client.session.create({ - title: `T3 Code ${input.threadId}`, - permission: buildOpenCodePermissionRules(input.runtimeMode), - }), - ); - if (!openCodeSession.data) { - return yield* new OpenCodeRuntimeError({ - operation: "session.create", - detail: "OpenCode session.create returned no session payload.", - }); - } - return { - sessionScope, - server, - client, - openCodeSession: openCodeSession.data, - }; - }).pipe(Effect.provideService(Scope.Scope, sessionScope)), - ); - if (Exit.isFailure(startedExit)) { - yield* Scope.close(sessionScope, Exit.void).pipe(Effect.ignore); - return yield* toProcessError(input.threadId, Cause.squash(startedExit.cause)); - } - return startedExit.value; - }); - - // Guard against a concurrent startSession call that may have raced - // and already inserted a session while we were awaiting async work. - const raceWinner = sessions.get(input.threadId); - if (raceWinner) { - // Another call won the race – clean up the session we just created - // (including the remote SDK session) and return the existing one. - yield* runOpenCodeSdk("session.abort", () => - started.client.session.abort({ - sessionID: started.openCodeSession.id, - }), - ).pipe(Effect.ignore); - yield* Scope.close(started.sessionScope, Exit.void).pipe(Effect.ignore); - return raceWinner.session; - } - - const createdAt = yield* nowIso; - const session: ProviderSession = { - provider: PROVIDER, - providerInstanceId: boundInstanceId, - status: "ready", - runtimeMode: input.runtimeMode, - cwd: directory, - ...(input.modelSelection ? { model: input.modelSelection.model } : {}), - threadId: input.threadId, - createdAt, - updatedAt: createdAt, - }; - - const context: OpenCodeSessionContext = { - session, - client: started.client, - server: started.server, - directory, - openCodeSessionId: started.openCodeSession.id, - pendingPermissions: new Map(), - pendingQuestions: new Map(), - partById: new Map(), - emittedTextByPartId: new Map(), - messageRoleById: new Map(), - completedAssistantPartIds: new Set(), - turns: [], - activeTurnId: undefined, - activeAgent: undefined, - activeVariant: undefined, - stopped: yield* Ref.make(false), - sessionScope: started.sessionScope, - }; - sessions.set(input.threadId, context); - yield* startEventPump(context); - - yield* emit({ - ...(yield* buildEventBase({ threadId: input.threadId })), - type: "session.started", - payload: { - message: "OpenCode session started", - }, - }); - yield* emit({ - ...(yield* buildEventBase({ threadId: input.threadId })), - type: "thread.started", - payload: { - providerThreadId: started.openCodeSession.id, - }, - }); - - return session; - }, - ); - - const sendTurn: OpenCodeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { - const context = ensureSessionContext(sessions, input.threadId); - // A sendTurn while a turn is active is a steer: OpenCode queues the - // prompt into the busy session and the work continues as one turn, so - // the active turn id is reused instead of opening a new turn. - const steeringTurnId = context.activeTurnId; - const turnId = steeringTurnId ?? TurnId.make(`opencode-turn-${yield* randomUUIDv4}`); - const modelSelection = - input.modelSelection ?? - (context.session.model - ? { instanceId: boundInstanceId, model: context.session.model } - : undefined); - if (modelSelection !== undefined && modelSelection.instanceId !== boundInstanceId) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "sendTurn", - issue: `OpenCode model selection is bound to instance '${modelSelection?.instanceId}', expected '${boundInstanceId}'.`, - }); - } - const parsedModel = parseOpenCodeModelSlug(modelSelection?.model); - if (!parsedModel) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "sendTurn", - issue: "OpenCode model selection must use the 'provider/model' format.", - }); - } - - const text = input.input?.trim(); - const fileParts = toOpenCodeFileParts({ - attachments: input.attachments, - resolveAttachmentPath: (attachment) => - resolveAttachmentPath({ - attachmentsDir: serverConfig.attachmentsDir, - attachment, - }), - }); - if ((!text || text.length === 0) && fileParts.length === 0) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "sendTurn", - issue: "OpenCode turns require text input or at least one attachment.", - }); - } - - const agent = getModelSelectionStringOptionValue(modelSelection, "agent"); - const variant = getModelSelectionStringOptionValue(modelSelection, "variant"); - - context.activeTurnId = turnId; - context.activeAgent = agent ?? (input.interactionMode === "plan" ? "plan" : undefined); - context.activeVariant = variant; - yield* updateProviderSession( - context, - { - status: "running", - activeTurnId: turnId, - model: modelSelection?.model ?? context.session.model, - }, - { clearLastError: true }, - ); - - if (steeringTurnId === undefined) { - yield* emit({ - ...(yield* buildEventBase({ threadId: input.threadId, turnId })), - type: "turn.started", - payload: { - model: modelSelection?.model ?? context.session.model, - ...(variant ? { effort: variant } : {}), - }, - }); - } - - yield* runOpenCodeSdk("session.promptAsync", () => - context.client.session.promptAsync({ - sessionID: context.openCodeSessionId, - model: parsedModel, - ...(context.activeAgent ? { agent: context.activeAgent } : {}), - ...(context.activeVariant ? { variant: context.activeVariant } : {}), - parts: [...(text ? [{ type: "text" as const, text }] : []), ...fileParts], - }), - ).pipe( - Effect.mapError(toRequestError), - // On failure of a fresh turn: clear active-turn state, flip the - // session back to ready with lastError set, emit turn.aborted, then - // let the typed error propagate. We don't need to rebuild the error - // here — `toRequestError` already produced the right shape. A failed - // steer leaves the still-running original turn untouched. - Effect.tapError((requestError) => - steeringTurnId !== undefined - ? Effect.void - : Effect.gen(function* () { - context.activeTurnId = undefined; - context.activeAgent = undefined; - context.activeVariant = undefined; - yield* updateProviderSession( - context, - { - status: "ready", - model: modelSelection?.model ?? context.session.model, - lastError: requestError.detail, - }, - { clearActiveTurnId: true }, - ); - yield* emit({ - ...(yield* buildEventBase({ - threadId: input.threadId, - turnId, - })), - type: "turn.aborted", - payload: { - reason: requestError.detail, - }, - }); - }), - ), - ); - - return { - threadId: input.threadId, - turnId, - }; - }); - - const interruptTurn: OpenCodeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")( - function* (threadId, turnId) { - const context = ensureSessionContext(sessions, threadId); - yield* runOpenCodeSdk("session.abort", () => - context.client.session.abort({ sessionID: context.openCodeSessionId }), - ).pipe(Effect.mapError(toRequestError)); - if (turnId ?? context.activeTurnId) { - yield* emit({ - ...(yield* buildEventBase({ - threadId, - turnId: turnId ?? context.activeTurnId, - })), - type: "turn.aborted", - payload: { - reason: "Interrupted by user.", - }, - }); - } - }, - ); - - const respondToRequest: OpenCodeAdapterShape["respondToRequest"] = Effect.fn( - "respondToRequest", - )(function* (threadId, requestId, decision) { - const context = ensureSessionContext(sessions, threadId); - if (!context.pendingPermissions.has(requestId)) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "permission.reply", - detail: `Unknown pending permission request: ${requestId}`, - }); - } - - yield* runOpenCodeSdk("permission.reply", () => - context.client.permission.reply({ - requestID: requestId, - reply: toOpenCodePermissionReply(decision), - }), - ).pipe(Effect.mapError(toRequestError)); - }); - - const respondToUserInput: OpenCodeAdapterShape["respondToUserInput"] = Effect.fn( - "respondToUserInput", - )(function* (threadId, requestId, answers) { - const context = ensureSessionContext(sessions, threadId); - const request = context.pendingQuestions.get(requestId); - if (!request) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "question.reply", - detail: `Unknown pending user-input request: ${requestId}`, - }); - } - - yield* runOpenCodeSdk("question.reply", () => - context.client.question.reply({ - requestID: requestId, - answers: toOpenCodeQuestionAnswers(request, answers), - }), - ).pipe(Effect.mapError(toRequestError)); - }); - - const stopSession: OpenCodeAdapterShape["stopSession"] = Effect.fn("stopSession")( - function* (threadId) { - const context = sessions.get(threadId); - if (!context) { - throw new ProviderAdapterSessionNotFoundError({ - provider: PROVIDER, - threadId, - }); - } - const stopped = yield* stopOpenCodeContext(context); - sessions.delete(threadId); - if (!stopped) { - return; - } - yield* emit({ - ...(yield* buildEventBase({ threadId })), - type: "session.exited", - payload: { - reason: "Session stopped.", - recoverable: false, - exitKind: "graceful", - }, - }); - }, - ); - - const listSessions: OpenCodeAdapterShape["listSessions"] = () => - Effect.sync(() => [...sessions.values()].map((context) => context.session)); - - const hasSession: OpenCodeAdapterShape["hasSession"] = (threadId) => - Effect.sync(() => sessions.has(threadId)); - - const readThread: OpenCodeAdapterShape["readThread"] = Effect.fn("readThread")( - function* (threadId) { - const context = ensureSessionContext(sessions, threadId); - const messages = yield* runOpenCodeSdk("session.messages", () => - context.client.session.messages({ - sessionID: context.openCodeSessionId, - }), - ).pipe(Effect.mapError(toRequestError)); - - const turns: Array = []; - for (const entry of messages.data ?? []) { - if (entry.info.role === "assistant") { - turns.push({ - id: TurnId.make(entry.info.id), - items: [entry.info, ...entry.parts], - }); - } - } - - return { - threadId, - turns, - }; - }, - ); - - const rollbackThread: OpenCodeAdapterShape["rollbackThread"] = Effect.fn("rollbackThread")( - function* (threadId, numTurns) { - const context = ensureSessionContext(sessions, threadId); - const messages = yield* runOpenCodeSdk("session.messages", () => - context.client.session.messages({ - sessionID: context.openCodeSessionId, - }), - ).pipe(Effect.mapError(toRequestError)); - - const assistantMessages = (messages.data ?? []).filter( - (entry) => entry.info.role === "assistant", - ); - const targetIndex = assistantMessages.length - numTurns - 1; - const target = targetIndex >= 0 ? assistantMessages[targetIndex] : null; - yield* runOpenCodeSdk("session.revert", () => - context.client.session.revert({ - sessionID: context.openCodeSessionId, - ...(target ? { messageID: target.info.id } : {}), - }), - ).pipe(Effect.mapError(toRequestError)); - - return yield* readThread(threadId); - }, - ); - - const stopAll: OpenCodeAdapterShape["stopAll"] = () => - Effect.gen(function* () { - const contexts = [...sessions.values()]; - sessions.clear(); - // `stopOpenCodeContext` is typed as never-failing — SDK aborts are - // already `Effect.ignore`'d inside it. `ignoreCause` here also - // swallows defects from throwing finalizers so one bad close can't - // interrupt the sibling fibers. Same pattern as the layer finalizer. - yield* Effect.forEach( - contexts, - (context) => Effect.ignoreCause(stopOpenCodeContext(context)), - { concurrency: "unbounded", discard: true }, - ); - }); - - return { - provider: PROVIDER, - capabilities: { - sessionModelSwitch: "in-session", - }, - startSession, - sendTurn, - interruptTurn, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - readThread, - rollbackThread, - stopAll, - get streamEvents() { - return Stream.fromQueue(runtimeEvents); - }, - } satisfies OpenCodeAdapterShape; - }); -} diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts deleted file mode 100644 index c4145ecf1a0..00000000000 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { - defaultInstanceIdForDriver, - ProviderDriverKind, - type ServerProvider, -} from "@t3tools/contracts"; -import { it, assert, vi } from "@effect/vitest"; - -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as PubSub from "effect/PubSub"; -import * as Stream from "effect/Stream"; - -import type * as ClaudeAdapter from "../Services/ClaudeAdapter.ts"; -import type * as CodexAdapter from "../Services/CodexAdapter.ts"; -import type * as CursorAdapter from "../Services/CursorAdapter.ts"; -import type * as OpenCodeAdapter from "../Services/OpenCodeAdapter.ts"; -import * as ProviderAdapterRegistry from "../Services/ProviderAdapterRegistry.ts"; -import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts"; -import type { ProviderInstance } from "../ProviderDriver.ts"; -import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts"; -import type * as TextGeneration from "../../textGeneration/TextGeneration.ts"; -import * as ProviderAdapterRegistryLayer from "./ProviderAdapterRegistry.ts"; -import * as NodeServices from "@effect/platform-node/NodeServices"; - -const CODEX_DRIVER = ProviderDriverKind.make("codex"); -const CLAUDE_AGENT_DRIVER = ProviderDriverKind.make("claudeAgent"); -const OPENCODE_DRIVER = ProviderDriverKind.make("opencode"); -const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); - -const fakeCodexAdapter: CodexAdapter.CodexAdapterShape = { - provider: CODEX_DRIVER, - capabilities: { sessionModelSwitch: "in-session" }, - startSession: vi.fn(), - sendTurn: vi.fn(), - interruptTurn: vi.fn(), - respondToRequest: vi.fn(), - respondToUserInput: vi.fn(), - stopSession: vi.fn(), - listSessions: vi.fn(), - hasSession: vi.fn(), - readThread: vi.fn(), - rollbackThread: vi.fn(), - stopAll: vi.fn(), - streamEvents: Stream.empty, -}; - -const fakeClaudeAdapter: ClaudeAdapter.ClaudeAdapterShape = { - provider: CLAUDE_AGENT_DRIVER, - capabilities: { sessionModelSwitch: "in-session" }, - startSession: vi.fn(), - sendTurn: vi.fn(), - interruptTurn: vi.fn(), - respondToRequest: vi.fn(), - respondToUserInput: vi.fn(), - stopSession: vi.fn(), - listSessions: vi.fn(), - hasSession: vi.fn(), - readThread: vi.fn(), - rollbackThread: vi.fn(), - stopAll: vi.fn(), - streamEvents: Stream.empty, -}; - -const fakeOpenCodeAdapter: OpenCodeAdapter.OpenCodeAdapterShape = { - provider: OPENCODE_DRIVER, - capabilities: { sessionModelSwitch: "in-session" }, - startSession: vi.fn(), - sendTurn: vi.fn(), - interruptTurn: vi.fn(), - respondToRequest: vi.fn(), - respondToUserInput: vi.fn(), - stopSession: vi.fn(), - listSessions: vi.fn(), - hasSession: vi.fn(), - readThread: vi.fn(), - rollbackThread: vi.fn(), - stopAll: vi.fn(), - streamEvents: Stream.empty, -}; - -const fakeCursorAdapter: CursorAdapter.CursorAdapterShape = { - provider: CURSOR_DRIVER, - capabilities: { sessionModelSwitch: "in-session" }, - startSession: vi.fn(), - sendTurn: vi.fn(), - interruptTurn: vi.fn(), - respondToRequest: vi.fn(), - respondToUserInput: vi.fn(), - stopSession: vi.fn(), - listSessions: vi.fn(), - hasSession: vi.fn(), - readThread: vi.fn(), - rollbackThread: vi.fn(), - stopAll: vi.fn(), - streamEvents: Stream.empty, -}; - -// ProviderAdapterRegistryLive is now a facade over ProviderInstanceRegistry — -// it walks `listInstances` once at boot and surfaces the default-instance -// adapter keyed by its driver kind. To test the facade we supply four fake -// instances whose `instanceId === defaultInstanceIdForDriver(driverKind)` so -// they pass the default-instance filter. -const makeFakeInstance = ( - driverKindString: "codex" | "claudeAgent" | "cursor" | "opencode", - adapter: ProviderInstance["adapter"], -): ProviderInstance => { - const driverKind = ProviderDriverKind.make(driverKindString); - return { - instanceId: defaultInstanceIdForDriver(driverKind), - driverKind, - continuationIdentity: { - driverKind, - continuationKey: `${driverKind}:instance:${defaultInstanceIdForDriver(driverKind)}`, - }, - displayName: undefined, - enabled: true, - snapshot: { - maintenanceCapabilities: makeManualOnlyProviderMaintenanceCapabilities({ - provider: driverKind, - packageName: null, - }), - getSnapshot: Effect.succeed({} as unknown as ServerProvider), - refresh: Effect.succeed({} as unknown as ServerProvider), - streamChanges: Stream.empty, - }, - adapter, - textGeneration: {} as unknown as TextGeneration.TextGeneration["Service"], - }; -}; - -const fakeInstances: ReadonlyArray = [ - makeFakeInstance("codex", fakeCodexAdapter), - makeFakeInstance("claudeAgent", fakeClaudeAdapter), - makeFakeInstance("opencode", fakeOpenCodeAdapter), - makeFakeInstance("cursor", fakeCursorAdapter), -]; - -const fakeInstanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, { - getInstance: (instanceId) => - Effect.succeed(fakeInstances.find((instance) => instance.instanceId === instanceId)), - listInstances: Effect.succeed(fakeInstances), - listUnavailable: Effect.succeed([]), - streamChanges: Stream.empty, - // Tests never drive changes through this fake; acquire a throwaway - // subscription on an unused PubSub so the shape is satisfied. - subscribeChanges: Effect.flatMap(PubSub.unbounded(), (pubsub) => PubSub.subscribe(pubsub)), -}); - -const layer = Layer.mergeAll( - Layer.provide( - ProviderAdapterRegistryLayer.ProviderAdapterRegistryLive, - fakeInstanceRegistryLayer, - ), - NodeServices.layer, -); - -it.layer(layer)("ProviderAdapterRegistryLive", (it) => { - it("resolves adapters and routing metadata from provider instances", () => - Effect.gen(function* () { - const registry = yield* ProviderAdapterRegistry.ProviderAdapterRegistry; - const claudeInstanceId = defaultInstanceIdForDriver(CLAUDE_AGENT_DRIVER); - - const adapter = yield* registry.getByInstance(claudeInstanceId); - assert.strictEqual(adapter, fakeClaudeAdapter); - - const info = yield* registry.getInstanceInfo(claudeInstanceId); - assert.deepStrictEqual(info, { - instanceId: claudeInstanceId, - driverKind: CLAUDE_AGENT_DRIVER, - displayName: undefined, - accentColor: undefined, - enabled: true, - continuationIdentity: { - driverKind: CLAUDE_AGENT_DRIVER, - continuationKey: "claudeAgent:instance:claudeAgent", - }, - }); - - const instances = yield* registry.listInstances(); - assert.deepStrictEqual(instances, [ - defaultInstanceIdForDriver(CODEX_DRIVER), - claudeInstanceId, - defaultInstanceIdForDriver(OPENCODE_DRIVER), - defaultInstanceIdForDriver(CURSOR_DRIVER), - ]); - - const providers = yield* registry.listProviders(); - assert.deepStrictEqual(providers, [ - CODEX_DRIVER, - CLAUDE_AGENT_DRIVER, - OPENCODE_DRIVER, - CURSOR_DRIVER, - ]); - })); -}); diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts deleted file mode 100644 index b492399b10b..00000000000 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * ProviderAdapterRegistryLive — facade over `ProviderInstanceRegistry`. - * - * `ProviderAdapterRegistry` historically mapped one `ProviderDriverKind` to one - * adapter via the four `AdapterLive` singleton Layers. The per-instance - * refactor moved adapter construction inside each `ProviderDriver.create()`: - * adapters are now bundled on the `ProviderInstance` that the - * `ProviderInstanceRegistry` owns. - * - * This facade fulfills the `ProviderAdapterRegistryShape` contract by doing - * dynamic look-ups against `ProviderInstanceRegistry` on every call. That - * means settings-driven hot-reload shows up here automatically — adding a - * new instance via settings makes `getByInstance` resolve immediately - * without rebuilding the facade. - * - * @module ProviderAdapterRegistryLive - */ -import { - defaultInstanceIdForDriver, - ProviderInstanceId, - type ProviderDriverKind, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; - -import { ProviderUnsupportedError } from "../Errors.ts"; -import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; -import { - ProviderAdapterRegistry, - type ProviderAdapterRegistryShape, -} from "../Services/ProviderAdapterRegistry.ts"; - -const makeProviderAdapterRegistry = Effect.fn("makeProviderAdapterRegistry")(function* () { - const registry = yield* ProviderInstanceRegistry; - - const getByInstance: ProviderAdapterRegistryShape["getByInstance"] = (instanceId) => - registry.getInstance(instanceId).pipe( - Effect.flatMap((instance) => - instance === undefined - ? Effect.fail( - new ProviderUnsupportedError({ - provider: instanceId, - }), - ) - : Effect.succeed(instance.adapter), - ), - ); - - const getInstanceInfo: ProviderAdapterRegistryShape["getInstanceInfo"] = (instanceId) => - registry.getInstance(instanceId).pipe( - Effect.flatMap((instance) => - instance === undefined - ? Effect.fail( - new ProviderUnsupportedError({ - provider: instanceId, - }), - ) - : Effect.succeed({ - instanceId: instance.instanceId, - driverKind: instance.driverKind, - displayName: instance.displayName, - accentColor: instance.accentColor, - enabled: instance.enabled, - continuationIdentity: instance.continuationIdentity, - }), - ), - ); - - const listInstances: ProviderAdapterRegistryShape["listInstances"] = () => - registry.listInstances.pipe( - Effect.map((instances) => instances.map((instance) => instance.instanceId)), - ); - - const listProviders: ProviderAdapterRegistryShape["listProviders"] = () => - registry.listInstances.pipe( - Effect.map((instances) => { - const kinds = new Set(); - for (const instance of instances) { - const defaultId = defaultInstanceIdForDriver(instance.driverKind); - if (instance.instanceId === defaultId) { - // Only the default-instance rows show up through the legacy - // shim — custom instances like `codex_personal` have no - // `ProviderDriverKind` equivalent. - kinds.add(instance.driverKind); - } - } - return Array.from(kinds); - }), - ); - - return { - getByInstance, - getInstanceInfo, - listInstances, - listProviders, - // Proxy directly — the facade has no state of its own; the instance - // registry already coalesces adds/removes/rebuilds into one emission. - streamChanges: registry.streamChanges, - subscribeChanges: registry.subscribeChanges, - } satisfies ProviderAdapterRegistryShape; -}); - -export const ProviderAdapterRegistryLive = Layer.effect( - ProviderAdapterRegistry, - makeProviderAdapterRegistry(), -); - -// Exposed for tests that want to build a facade over a pre-assembled -// `ProviderInstanceRegistry` without pulling in the whole boot graph. -export { makeProviderAdapterRegistry }; - -// Re-export for consumers that need the accessor shape. The service tag -// itself lives in `Services/ProviderAdapterRegistry.ts`. -export { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; -// Re-export for consumers (including tests) that construct a -// `ProviderInstanceId` before calling `getByInstance`. -export { ProviderInstanceId }; diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts index 0fd88b4262a..0b28cd1bbe3 100644 --- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts +++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts @@ -56,6 +56,14 @@ import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "../builtInDrivers.ts"; import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; import { ProviderInstanceRegistryMutator } from "../Services/ProviderInstanceRegistryMutator.ts"; import { ProviderInstanceRegistryMutableLayer } from "./ProviderInstanceRegistryLive.ts"; +import { + type ProviderOrchestrationAdapterInfrastructure, + ProviderOrchestrationAdapterInfrastructureLive, +} from "./ProviderOrchestrationAdapterInfrastructure.ts"; + +type ProviderInstanceRegistryHydrationEnv = + | Exclude + | ServerSettingsService; /** * Synthesize a `ProviderInstanceConfigMap` from a `ServerSettings` snapshot. @@ -152,7 +160,7 @@ const SettingsWatcherLive = Layer.effectDiscard( export const ProviderInstanceRegistryHydrationLive: Layer.Layer< ProviderInstanceRegistry, never, - BuiltInDriversEnv | ServerSettingsService + ProviderInstanceRegistryHydrationEnv > = Layer.unwrap( Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; @@ -167,8 +175,8 @@ export const ProviderInstanceRegistryHydrationLive: Layer.Layer< const mutableLayer = ProviderInstanceRegistryMutableLayer({ drivers: BUILT_IN_DRIVERS, configMap: initialConfigMap, - }); + }).pipe(Layer.provide(ProviderOrchestrationAdapterInfrastructureLive)); return SettingsWatcherLive.pipe(Layer.provideMerge(mutableLayer)); }), -) as Layer.Layer; +) as Layer.Layer; diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts index dbfa7faffea..06b11ca072d 100644 --- a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts +++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts @@ -40,14 +40,16 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; +import type { BuiltInDriversEnv } from "../builtInDrivers.ts"; import { ClaudeDriver } from "../Drivers/ClaudeDriver.ts"; -import { CodexDriver } from "../Drivers/CodexDriver.ts"; +import { CodexDriver, type CodexDriverEnv } from "../Drivers/CodexDriver.ts"; import { CursorDriver } from "../Drivers/CursorDriver.ts"; import { GrokDriver } from "../Drivers/GrokDriver.ts"; import { OpenCodeDriver } from "../Drivers/OpenCodeDriver.ts"; import { OpenCodeRuntimeLive } from "../opencodeRuntime.ts"; import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts"; import { makeProviderInstanceRegistry } from "./ProviderInstanceRegistryLive.ts"; +import { ProviderOrchestrationAdapterInfrastructureLive } from "./ProviderOrchestrationAdapterInfrastructure.ts"; const TestHttpClientLive = Layer.succeed( HttpClient.HttpClient, @@ -104,7 +106,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => { // `NodeServices.layer` through `Layer.provideMerge` to satisfy that // dependency while still surfacing NodeServices to the test body (the // codex driver's `create` yields `ChildProcessSpawner` directly). - const testLayer = ServerConfig.layerTest(process.cwd(), { + const baseLayer = ServerConfig.layerTest(process.cwd(), { prefix: "provider-instance-registry-test", }).pipe( Layer.provideMerge(NodeServices.layer), @@ -112,6 +114,9 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => { Layer.provideMerge(TestHttpClientLive), Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)), ); + const testLayer = ProviderOrchestrationAdapterInfrastructureLive.pipe( + Layer.provideMerge(baseLayer), + ); it.live("boots two independent codex instances from a ProviderInstanceConfigMap", () => Effect.gen(function* () { @@ -142,7 +147,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => { }, }; - const { registry } = yield* makeProviderInstanceRegistry({ + const { registry } = yield* makeProviderInstanceRegistry({ drivers: [CodexDriver], configMap, }); @@ -161,7 +166,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => { const work = yield* registry.getInstance(workId); expect(personal).toBeDefined(); expect(work).toBeDefined(); - expect(personal!.adapter).not.toBe(work!.adapter); + expect(personal!.orchestrationAdapter).not.toBe(work!.orchestrationAdapter); expect(personal!.textGeneration).not.toBe(work!.textGeneration); expect(personal!.snapshot).not.toBe(work!.snapshot); @@ -208,7 +213,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => { }, }; - const { registry } = yield* makeProviderInstanceRegistry({ + const { registry } = yield* makeProviderInstanceRegistry({ drivers: [CodexDriver], configMap, }); @@ -242,7 +247,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => { // surfaced; that merged layer then provides `ServerConfig.layerTest`'s // `FileSystem` dep while keeping everything else surfaced to the test. const infraLayer = OpenCodeRuntimeLive.pipe(Layer.provideMerge(NodeServices.layer)); - const testLayer = ServerConfig.layerTest(process.cwd(), { + const baseLayer = ServerConfig.layerTest(process.cwd(), { prefix: "provider-instance-registry-all-drivers-test", }).pipe( Layer.provideMerge(infraLayer), @@ -250,6 +255,9 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => { Layer.provideMerge(TestHttpClientLive), Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)), ); + const testLayer = ProviderOrchestrationAdapterInfrastructureLive.pipe( + Layer.provideMerge(baseLayer), + ); it.live("boots one instance of every shipped driver from a single config map", () => Effect.gen(function* () { @@ -301,7 +309,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => { }, }; - const { registry } = yield* makeProviderInstanceRegistry({ + const { registry } = yield* makeProviderInstanceRegistry({ drivers: [CodexDriver, ClaudeDriver, CursorDriver, GrokDriver, OpenCodeDriver], configMap, }); @@ -337,16 +345,16 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => { expect(openCode?.displayName).toBe("OpenCode"); // Every instance owns its own set of closures — no sharing across - // drivers. `adapter` / `textGeneration` / `snapshot` are all + // drivers. `orchestrationAdapter` / `textGeneration` / `snapshot` are all // distinct references even when two instances happen to share a // trait (e.g. Cursor + others all use a stub-or-real // `textGeneration`; they must still be different object values). const adapters = [ - codex!.adapter, - claude!.adapter, - cursor!.adapter, - grok!.adapter, - openCode!.adapter, + codex!.orchestrationAdapter, + claude!.orchestrationAdapter, + cursor!.orchestrationAdapter, + grok!.orchestrationAdapter, + openCode!.orchestrationAdapter, ]; expect(new Set(adapters).size).toBe(adapters.length); const textGenerations = [ diff --git a/apps/server/src/provider/Layers/ProviderOrchestrationAdapterInfrastructure.ts b/apps/server/src/provider/Layers/ProviderOrchestrationAdapterInfrastructure.ts new file mode 100644 index 00000000000..bf34ea30657 --- /dev/null +++ b/apps/server/src/provider/Layers/ProviderOrchestrationAdapterInfrastructure.ts @@ -0,0 +1,29 @@ +import * as Layer from "effect/Layer"; + +import { + ClaudeAgentSdkQueryRunner, + claudeAgentSdkQueryRunnerLiveLayer, +} from "../../orchestration-v2/Adapters/ClaudeAdapterV2.ts"; +import { + CodexAppServerClientFactory, + codexAppServerClientFactoryFromSettingsLayer, +} from "../../orchestration-v2/Adapters/CodexAdapterV2.ts"; +import { + CursorAgentSdkRunner, + cursorAgentSdkRunnerLiveLayer, +} from "../../orchestration-v2/Adapters/CursorAgentSdk.ts"; +import { IdAllocatorV2, layer as idAllocatorLayer } from "../../orchestration-v2/IdAllocator.ts"; + +export type ProviderOrchestrationAdapterInfrastructure = + | ClaudeAgentSdkQueryRunner + | CodexAppServerClientFactory + | CursorAgentSdkRunner + | IdAllocatorV2; + +/** Infrastructure shared by the V2 adapters materialized inside provider instances. */ +export const ProviderOrchestrationAdapterInfrastructureLive = Layer.mergeAll( + claudeAgentSdkQueryRunnerLiveLayer, + codexAppServerClientFactoryFromSettingsLayer, + cursorAgentSdkRunnerLiveLayer, + idAllocatorLayer, +); diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index b3ab1145495..842bb4f9629 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -633,7 +633,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te ), streamChanges: Stream.empty, }, - adapter: {} as ProviderInstance["adapter"], + orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"], textGeneration: {} as ProviderInstance["textGeneration"], } satisfies ProviderInstance; const instanceRegistryLayer = Layer.succeed( @@ -786,7 +786,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te refresh: Effect.succeed(refreshedProvider), streamChanges: Stream.fromPubSub(changes), }, - adapter: {} as ProviderInstance["adapter"], + orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"], textGeneration: {} as ProviderInstance["textGeneration"], } satisfies ProviderInstance; const instanceRegistryLayer = Layer.succeed( @@ -883,7 +883,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te refresh: Effect.die(new Error("simulated refresh failure")), streamChanges: Stream.empty, }, - adapter: {} as ProviderInstance["adapter"], + orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"], textGeneration: {} as ProviderInstance["textGeneration"], } satisfies ProviderInstance; const instanceRegistryLayer = Layer.succeed( @@ -975,7 +975,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te refresh: Effect.succeed(provider), streamChanges: Stream.empty, }, - adapter: {} as ProviderInstance["adapter"], + orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"], textGeneration: {} as ProviderInstance["textGeneration"], }); const codexInstance = makeInstance(codexProvider); diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts deleted file mode 100644 index ccbbce1759f..00000000000 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ /dev/null @@ -1,1894 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; - -import type { - ProviderApprovalDecision, - ProviderRuntimeEvent, - ProviderSendTurnInput, - ProviderSession, - ProviderTurnStartResult, -} from "@t3tools/contracts"; -import { - ApprovalRequestId, - EventId, - ProviderDriverKind, - ProviderInstanceId, - ProviderSessionStartInput, - ThreadId, - TurnId, -} from "@t3tools/contracts"; -import { createModelSelection } from "@t3tools/shared/model"; -import { it, assert, vi } from "@effect/vitest"; - -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Fiber from "effect/Fiber"; -import * as Layer from "effect/Layer"; -import * as Metric from "effect/Metric"; -import * as Option from "effect/Option"; -import * as PubSub from "effect/PubSub"; -import * as Ref from "effect/Ref"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import * as TestClock from "effect/testing/TestClock"; -import * as SqlClient from "effect/unstable/sql/SqlClient"; - -import { - ProviderAdapterRequestError, - ProviderAdapterSessionNotFoundError, - ProviderUnsupportedError, - ProviderValidationError, - type ProviderAdapterError, -} from "../Errors.ts"; -import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts"; -import * as ProviderAdapterRegistry from "../Services/ProviderAdapterRegistry.ts"; -import * as ProviderService from "../Services/ProviderService.ts"; -import * as ProviderSessionDirectory from "../Services/ProviderSessionDirectory.ts"; -import { makeProviderServiceLive } from "./ProviderService.ts"; -import * as ProviderEventLoggers from "./ProviderEventLoggers.ts"; -import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; -import { - makeSqlitePersistenceLive, - SqlitePersistenceMemory, -} from "../../persistence/Layers/Sqlite.ts"; -import * as ServerSettings from "../../serverSettings.ts"; -import * as AnalyticsService from "../../telemetry/AnalyticsService.ts"; -import { makeAdapterRegistryMock } from "../testUtils/providerAdapterRegistryMock.ts"; - -const defaultServerSettingsLayer = ServerSettings.ServerSettingsService.layerTest(); - -const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make(value); -const asEventId = (value: string): EventId => EventId.make(value); -const asThreadId = (value: string): ThreadId => ThreadId.make(value); -const asTurnId = (value: string): TurnId => TurnId.make(value); -const codexInstanceId = ProviderInstanceId.make("codex"); -const claudeAgentInstanceId = ProviderInstanceId.make("claudeAgent"); -const CODEX_DRIVER = ProviderDriverKind.make("codex"); -const CLAUDE_AGENT_DRIVER = ProviderDriverKind.make("claudeAgent"); -const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); - -type LegacyProviderRuntimeEvent = { - readonly type: string; - readonly eventId: EventId; - readonly provider: ProviderDriverKind; - readonly createdAt: string; - readonly threadId: ThreadId; - readonly turnId?: string | undefined; - readonly itemId?: string | undefined; - readonly requestId?: string | undefined; - readonly payload?: unknown | undefined; - readonly [key: string]: unknown; -}; - -function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) { - const sessions = new Map(); - const runtimeEventPubSub = Effect.runSync(PubSub.unbounded()); - - const startSession = vi.fn((input: ProviderSessionStartInput) => - Effect.sync(() => { - const now = "2026-01-01T00:00:00.000Z"; - const session: ProviderSession = { - provider, - ...(input.providerInstanceId !== undefined - ? { providerInstanceId: input.providerInstanceId } - : {}), - status: "ready", - runtimeMode: input.runtimeMode, - threadId: input.threadId, - resumeCursor: input.resumeCursor ?? { - opaque: `resume-${String(input.threadId)}`, - }, - cwd: input.cwd ?? process.cwd(), - createdAt: now, - updatedAt: now, - }; - sessions.set(session.threadId, session); - return session; - }), - ); - - const sendTurn = vi.fn( - ( - input: ProviderSendTurnInput, - ): Effect.Effect => { - if (!sessions.has(input.threadId)) { - return Effect.fail( - new ProviderAdapterSessionNotFoundError({ - provider, - threadId: input.threadId, - }), - ); - } - - return Effect.succeed({ - threadId: input.threadId, - turnId: TurnId.make(`turn-${String(input.threadId)}`), - }); - }, - ); - - const interruptTurn = vi.fn( - (_threadId: ThreadId, _turnId?: TurnId): Effect.Effect => - Effect.void, - ); - - const respondToRequest = vi.fn( - ( - _threadId: ThreadId, - _requestId: string, - _decision: ProviderApprovalDecision, - ): Effect.Effect => Effect.void, - ); - - const respondToUserInput = vi.fn( - ( - _threadId: ThreadId, - _requestId: string, - _answers: Record, - ): Effect.Effect => Effect.void, - ); - - const stopSession = vi.fn( - (threadId: ThreadId): Effect.Effect => - Effect.sync(() => { - sessions.delete(threadId); - }), - ); - - const listSessions = vi.fn( - (): Effect.Effect> => - Effect.sync(() => Array.from(sessions.values())), - ); - - const hasSession = vi.fn( - (threadId: ThreadId): Effect.Effect => Effect.succeed(sessions.has(threadId)), - ); - - const readThread = vi.fn( - ( - threadId: ThreadId, - ): Effect.Effect< - { - threadId: ThreadId; - turns: ReadonlyArray<{ id: TurnId; items: readonly [] }>; - }, - ProviderAdapterError - > => - Effect.succeed({ - threadId, - turns: [{ id: asTurnId("turn-1"), items: [] }], - }), - ); - - const rollbackThread = vi.fn( - ( - threadId: ThreadId, - _numTurns: number, - ): Effect.Effect<{ threadId: ThreadId; turns: readonly [] }, ProviderAdapterError> => - Effect.succeed({ threadId, turns: [] }), - ); - - const stopAll = vi.fn( - (): Effect.Effect => - Effect.sync(() => { - sessions.clear(); - }), - ); - - const adapter: ProviderAdapterShape = { - provider, - capabilities: { - sessionModelSwitch: "in-session", - }, - startSession, - sendTurn, - interruptTurn, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - readThread, - rollbackThread, - stopAll, - get streamEvents() { - return Stream.fromPubSub(runtimeEventPubSub); - }, - }; - - const emit = (event: LegacyProviderRuntimeEvent): void => { - Effect.runSync(PubSub.publish(runtimeEventPubSub, event as unknown as ProviderRuntimeEvent)); - }; - - const updateSession = ( - threadId: ThreadId, - update: (session: ProviderSession) => ProviderSession, - ): void => { - const existing = sessions.get(threadId); - if (!existing) { - return; - } - sessions.set(threadId, update(existing)); - }; - - return { - adapter, - emit, - updateSession, - startSession, - sendTurn, - interruptTurn, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - readThread, - rollbackThread, - stopAll, - }; -} - -const advanceTestClock = (ms: number) => - TestClock.adjust(`${ms} millis`).pipe(Effect.andThen(Effect.yieldNow)); - -const hasMetricSnapshot = ( - snapshots: ReadonlyArray, - id: string, - attributes: Readonly>, -) => - snapshots.some( - (snapshot) => - snapshot.id === id && - Object.entries(attributes).every(([key, value]) => snapshot.attributes?.[key] === value), - ); - -function makeProviderServiceLayer() { - const codex = makeFakeCodexAdapter(); - const claude = makeFakeCodexAdapter(CLAUDE_AGENT_DRIVER); - const cursor = makeFakeCodexAdapter(CURSOR_DRIVER); - const registry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("codex")]: codex.adapter, - [ProviderDriverKind.make("claudeAgent")]: claude.adapter, - [ProviderDriverKind.make("cursor")]: cursor.adapter, - }); - - const providerAdapterLayer = Layer.succeed( - ProviderAdapterRegistry.ProviderAdapterRegistry, - registry, - ); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(SqlitePersistenceMemory), - ); - const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); - - const layer = it.layer( - Layer.mergeAll( - makeProviderServiceLive().pipe( - Layer.provide(providerAdapterLayer), - Layer.provide(directoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provideMerge(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ), - directoryLayer, - - runtimeRepositoryLayer, - NodeServices.layer, - ), - ); - - return { - codex, - claude, - cursor, - layer, - }; -} - -it.effect("ProviderServiceLive catches stopAll failures during shutdown", () => - Effect.gen(function* () { - const codex = makeFakeCodexAdapter(); - codex.stopAll.mockImplementation(() => - Effect.fail( - new ProviderAdapterRequestError({ - provider: String(CODEX_DRIVER), - method: "stopAll", - detail: "simulated stopAll failure", - }), - ), - ); - const registry = makeAdapterRegistryMock({ - [CODEX_DRIVER]: codex.adapter, - }); - const providerAdapterLayer = Layer.succeed( - ProviderAdapterRegistry.ProviderAdapterRegistry, - registry, - ); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(SqlitePersistenceMemory), - ); - const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); - const providerLayer = Layer.mergeAll( - makeProviderServiceLive().pipe( - Layer.provide(providerAdapterLayer), - Layer.provide(directoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provideMerge(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ), - directoryLayer, - runtimeRepositoryLayer, - NodeServices.layer, - ); - const scope = yield* Scope.make(); - const runtimeServices = yield* Layer.build(providerLayer).pipe(Scope.provide(scope)); - - yield* ProviderService.ProviderService.pipe(Effect.provide(runtimeServices)); - const closeExit = yield* Scope.close(scope, Exit.void).pipe(Effect.exit); - - assert.equal(Exit.isSuccess(closeExit), true); - assert.equal(codex.stopAll.mock.calls.length, 1); - }), -); - -it.effect("ProviderServiceLive rejects new sessions for disabled providers", () => - Effect.gen(function* () { - const codex = makeFakeCodexAdapter(); - const claude = makeFakeCodexAdapter(CLAUDE_AGENT_DRIVER); - const registryBase = makeAdapterRegistryMock({ - [CODEX_DRIVER]: codex.adapter, - [CLAUDE_AGENT_DRIVER]: claude.adapter, - }); - const registry: ProviderAdapterRegistry.ProviderAdapterRegistry["Service"] = { - ...registryBase, - getInstanceInfo: (instanceId) => - instanceId === claudeAgentInstanceId - ? Effect.succeed({ - instanceId, - driverKind: CLAUDE_AGENT_DRIVER, - displayName: undefined, - enabled: false, - continuationIdentity: { - driverKind: CLAUDE_AGENT_DRIVER, - continuationKey: "claudeAgent:instance:claudeAgent", - }, - }) - : registryBase.getInstanceInfo(instanceId), - }; - const providerAdapterLayer = Layer.succeed( - ProviderAdapterRegistry.ProviderAdapterRegistry, - registry, - ); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(SqlitePersistenceMemory), - ); - const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); - const providerLayer = makeProviderServiceLive().pipe( - Layer.provide(providerAdapterLayer), - Layer.provide(directoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - const failure = yield* Effect.flip( - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - return yield* provider.startSession(asThreadId("thread-disabled"), { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId: asThreadId("thread-disabled"), - runtimeMode: "full-access", - }); - }).pipe(Effect.provide(providerLayer)), - ); - - assert.instanceOf(failure, ProviderValidationError); - assert.include(failure.issue, "Provider instance 'claudeAgent' is disabled"); - assert.equal(claude.startSession.mock.calls.length, 0); - }).pipe(Effect.provide(NodeServices.layer)), -); - -it.effect( - "ProviderServiceLive allows enabled custom instances when legacy driver is disabled", - () => - Effect.gen(function* () { - const instanceId = ProviderInstanceId.make("codex_personal"); - const driverKind = CODEX_DRIVER; - const codex = makeFakeCodexAdapter(); - const unsupported = () => - new ProviderUnsupportedError({ - provider: driverKind, - }); - const registry: ProviderAdapterRegistry.ProviderAdapterRegistry["Service"] = { - getByInstance: (requestedInstanceId) => - requestedInstanceId === instanceId - ? Effect.succeed(codex.adapter) - : Effect.fail(unsupported()), - getInstanceInfo: (requestedInstanceId) => - requestedInstanceId === instanceId - ? Effect.succeed({ - instanceId, - driverKind, - displayName: "Codex Personal", - enabled: true, - continuationIdentity: { - driverKind, - continuationKey: "codex:/Users/example/.codex", - }, - }) - : Effect.fail(unsupported()), - listInstances: () => Effect.succeed([instanceId]), - listProviders: () => Effect.succeed([driverKind] as const), - streamChanges: Stream.empty, - subscribeChanges: Effect.flatMap(PubSub.unbounded(), (pubsub) => - PubSub.subscribe(pubsub), - ), - }; - const providerAdapterLayer = Layer.succeed( - ProviderAdapterRegistry.ProviderAdapterRegistry, - registry, - ); - const serverSettingsLayer = ServerSettings.ServerSettingsService.layerTest({ - providers: { - codex: { - enabled: false, - }, - }, - }); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(SqlitePersistenceMemory), - ); - const directoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(runtimeRepositoryLayer), - ); - const providerLayer = makeProviderServiceLive().pipe( - Layer.provide(providerAdapterLayer), - Layer.provide(directoryLayer), - Layer.provide(serverSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - const session = yield* Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - return yield* provider.startSession(asThreadId("thread-enabled-custom"), { - provider: driverKind, - providerInstanceId: instanceId, - threadId: asThreadId("thread-enabled-custom"), - runtimeMode: "full-access", - }); - }).pipe(Effect.provide(providerLayer)); - - assert.equal(session.providerInstanceId, instanceId); - assert.equal(codex.startSession.mock.calls.length, 1); - }).pipe(Effect.provide(NodeServices.layer)), -); - -it.effect("ProviderServiceLive rejects new sessions for disabled custom instances", () => - Effect.gen(function* () { - const instanceId = ProviderInstanceId.make("codex_personal"); - const driverKind = ProviderDriverKind.make("codex"); - const codex = makeFakeCodexAdapter(); - const unsupported = () => - new ProviderUnsupportedError({ - provider: ProviderDriverKind.make("codex"), - }); - const registry: ProviderAdapterRegistry.ProviderAdapterRegistry["Service"] = { - getByInstance: (requestedInstanceId) => - requestedInstanceId === instanceId - ? Effect.succeed(codex.adapter) - : Effect.fail(unsupported()), - getInstanceInfo: (requestedInstanceId) => - requestedInstanceId === instanceId - ? Effect.succeed({ - instanceId, - driverKind, - displayName: "Codex Personal", - enabled: false, - continuationIdentity: { - driverKind, - continuationKey: "codex:/Users/example/.codex", - }, - }) - : Effect.fail(unsupported()), - listInstances: () => Effect.succeed([instanceId]), - listProviders: () => Effect.succeed([CODEX_DRIVER] as const), - streamChanges: Stream.empty, - subscribeChanges: Effect.flatMap(PubSub.unbounded(), (pubsub) => - PubSub.subscribe(pubsub), - ), - }; - const providerAdapterLayer = Layer.succeed( - ProviderAdapterRegistry.ProviderAdapterRegistry, - registry, - ); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(SqlitePersistenceMemory), - ); - const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); - const providerLayer = makeProviderServiceLive().pipe( - Layer.provide(providerAdapterLayer), - Layer.provide(directoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - const failure = yield* Effect.flip( - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - return yield* provider.startSession(asThreadId("thread-disabled-instance"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: instanceId, - threadId: asThreadId("thread-disabled-instance"), - runtimeMode: "full-access", - }); - }).pipe(Effect.provide(providerLayer)), - ); - - assert.instanceOf(failure, ProviderValidationError); - assert.include(failure.issue, "Provider instance 'codex_personal' is disabled"); - assert.equal(codex.startSession.mock.calls.length, 0); - }).pipe(Effect.provide(NodeServices.layer)), -); - -const routing = makeProviderServiceLayer(); - -it.effect("ProviderServiceLive writes canonical events to the emitting thread segment", () => - Effect.gen(function* () { - const codex = makeFakeCodexAdapter(); - const canonicalEvents: ProviderRuntimeEvent[] = []; - const canonicalThreadIds: Array = []; - const registry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("codex")]: codex.adapter, - }); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(SqlitePersistenceMemory), - ); - const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); - const providerLayer = makeProviderServiceLive({ - canonicalEventLogger: { - filePath: "memory://provider-canonical-events", - write: (event, threadId) => { - canonicalEvents.push(event as ProviderRuntimeEvent); - canonicalThreadIds.push(threadId ?? null); - return Effect.void; - }, - close: () => Effect.void, - }, - }).pipe( - Layer.provide(Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, registry)), - Layer.provide(directoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - yield* Effect.gen(function* () { - yield* ProviderService.ProviderService; - yield* advanceTestClock(10); - codex.emit({ - eventId: asEventId("evt-canonical-thread-segment"), - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-canonical-thread-segment"), - createdAt: "2026-01-01T00:00:00.000Z", - type: "turn.completed", - payload: { - state: "completed", - }, - }); - yield* advanceTestClock(20); - }).pipe(Effect.provide(providerLayer)); - - assert.equal(canonicalEvents.length, 1); - assert.equal(canonicalEvents[0]?.threadId, "thread-canonical-thread-segment"); - assert.deepEqual(canonicalThreadIds, ["thread-canonical-thread-segment"]); - }).pipe(Effect.provide(NodeServices.layer)), -); - -it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", () => - Effect.gen(function* () { - const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-provider-service-")); - const dbPath = NodePath.join(tempDir, "orchestration.sqlite"); - - const codex = makeFakeCodexAdapter(); - const registry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("codex")]: codex.adapter, - }); - - const persistenceLayer = makeSqlitePersistenceLive(dbPath); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(persistenceLayer), - ); - const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); - - yield* Effect.gen(function* () { - const directory = yield* ProviderSessionDirectory.ProviderSessionDirectory; - yield* directory.upsert({ - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: ThreadId.make("thread-stale"), - }); - }).pipe(Effect.provide(directoryLayer)); - - const providerLayer = makeProviderServiceLive().pipe( - Layer.provide(Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, registry)), - Layer.provide(directoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - yield* ProviderService.ProviderService.pipe(Effect.provide(providerLayer)); - - const persistedProvider = yield* Effect.gen(function* () { - const directory = yield* ProviderSessionDirectory.ProviderSessionDirectory; - return yield* directory.getProvider(asThreadId("thread-stale")); - }).pipe(Effect.provide(directoryLayer)); - assert.equal(persistedProvider, "codex"); - - const runtime = yield* Effect.gen(function* () { - const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - return yield* repository.getByThreadId({ - threadId: asThreadId("thread-stale"), - }); - }).pipe(Effect.provide(runtimeRepositoryLayer)); - assert.equal(Option.isSome(runtime), true); - - const legacyTableRows = yield* Effect.gen(function* () { - const sql = yield* SqlClient.SqlClient; - return yield* sql<{ readonly name: string }>` - SELECT name - FROM sqlite_master - WHERE type = 'table' AND name = 'provider_sessions' - `; - }).pipe(Effect.provide(persistenceLayer)); - assert.equal(legacyTableRows.length, 0); - - NodeFS.rmSync(tempDir, { recursive: true, force: true }); - }).pipe(Effect.provide(NodeServices.layer)), -); - -it.effect( - "ProviderServiceLive restores rollback routing after restart using persisted thread mapping", - () => - Effect.gen(function* () { - const tempDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-provider-service-restart-"), - ); - const dbPath = NodePath.join(tempDir, "orchestration.sqlite"); - const persistenceLayer = makeSqlitePersistenceLive(dbPath); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(persistenceLayer), - ); - - const firstCodex = makeFakeCodexAdapter(); - const firstRegistry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("codex")]: firstCodex.adapter, - }); - - const firstDirectoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(runtimeRepositoryLayer), - ); - const firstProviderLayer = makeProviderServiceLive().pipe( - Layer.provide( - Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, firstRegistry), - ), - Layer.provide(firstDirectoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - const updatedResumeCursor = { - threadId: asThreadId("thread-1"), - resume: "resume-session-1", - resumeSessionAt: "assistant-message-1", - turnCount: 1, - }; - - const startedSession = yield* Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - const threadId = asThreadId("thread-1"); - const session = yield* provider.startSession(threadId, { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - cwd: "/tmp/project", - runtimeMode: "full-access", - threadId, - }); - firstCodex.updateSession(threadId, (existing) => ({ - ...existing, - status: "ready", - resumeCursor: updatedResumeCursor, - updatedAt: "2026-01-01T00:00:01.000Z", - })); - return session; - }).pipe(Effect.provide(firstProviderLayer)); - - const persistedAfterStopAll = yield* Effect.gen(function* () { - const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - return yield* repository.getByThreadId({ - threadId: startedSession.threadId, - }); - }).pipe(Effect.provide(runtimeRepositoryLayer)); - assert.equal(Option.isSome(persistedAfterStopAll), true); - if (Option.isSome(persistedAfterStopAll)) { - assert.equal(persistedAfterStopAll.value.status, "stopped"); - assert.deepEqual(persistedAfterStopAll.value.resumeCursor, updatedResumeCursor); - } - - const secondCodex = makeFakeCodexAdapter(); - const secondRegistry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("codex")]: secondCodex.adapter, - }); - const secondDirectoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(runtimeRepositoryLayer), - ); - const secondProviderLayer = makeProviderServiceLive().pipe( - Layer.provide( - Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, secondRegistry), - ), - Layer.provide(secondDirectoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - secondCodex.startSession.mockClear(); - secondCodex.rollbackThread.mockClear(); - - yield* Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - yield* provider.rollbackConversation({ - threadId: startedSession.threadId, - numTurns: 1, - }); - }).pipe(Effect.provide(secondProviderLayer)); - - assert.equal(secondCodex.startSession.mock.calls.length, 1); - const resumedStartInput = secondCodex.startSession.mock.calls[0]?.[0]; - assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); - if (resumedStartInput && typeof resumedStartInput === "object") { - const startPayload = resumedStartInput as { - provider?: string; - cwd?: string; - resumeCursor?: unknown; - threadId?: string; - }; - assert.equal(startPayload.provider, "codex"); - assert.equal(startPayload.cwd, "/tmp/project"); - assert.deepEqual(startPayload.resumeCursor, updatedResumeCursor); - assert.equal(startPayload.threadId, startedSession.threadId); - } - assert.equal(secondCodex.rollbackThread.mock.calls.length, 1); - const rollbackCall = secondCodex.rollbackThread.mock.calls[0]; - assert.equal(typeof rollbackCall?.[0], "string"); - assert.equal(rollbackCall?.[1], 1); - - NodeFS.rmSync(tempDir, { recursive: true, force: true }); - }).pipe(Effect.provide(NodeServices.layer)), -); - -routing.layer("ProviderServiceLive routing", (it) => { - it.effect("routes provider operations and rollback conversation", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - const session = yield* provider.startSession(asThreadId("thread-1"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-1"), - cwd: "/tmp/project", - runtimeMode: "full-access", - }); - assert.equal(session.provider, "codex"); - - const sessions = yield* provider.listSessions(); - assert.equal(sessions.length, 1); - - yield* provider.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - assert.equal(routing.codex.sendTurn.mock.calls.length, 1); - - yield* provider.interruptTurn({ threadId: session.threadId }); - assert.deepEqual(routing.codex.interruptTurn.mock.calls, [[session.threadId, undefined]]); - - yield* provider.respondToRequest({ - threadId: session.threadId, - requestId: asRequestId("req-1"), - decision: "accept", - }); - assert.deepEqual(routing.codex.respondToRequest.mock.calls, [ - [session.threadId, asRequestId("req-1"), "accept"], - ]); - - yield* provider.respondToUserInput({ - threadId: session.threadId, - requestId: asRequestId("req-user-input-1"), - answers: { - sandbox_mode: "workspace-write", - }, - }); - assert.deepEqual(routing.codex.respondToUserInput.mock.calls, [ - [ - session.threadId, - asRequestId("req-user-input-1"), - { - sandbox_mode: "workspace-write", - }, - ], - ]); - - yield* provider.rollbackConversation({ - threadId: session.threadId, - numTurns: 0, - }); - - yield* provider.stopSession({ threadId: session.threadId }); - routing.codex.startSession.mockClear(); - routing.codex.sendTurn.mockClear(); - - yield* provider.sendTurn({ - threadId: session.threadId, - input: "after-stop", - attachments: [], - }); - - assert.equal(routing.codex.startSession.mock.calls.length, 1); - const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0]; - assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); - if (resumedStartInput && typeof resumedStartInput === "object") { - const startPayload = resumedStartInput as { - provider?: string; - cwd?: string; - resumeCursor?: unknown; - threadId?: string; - }; - assert.equal(startPayload.provider, "codex"); - assert.equal(startPayload.cwd, "/tmp/project"); - assert.deepEqual(startPayload.resumeCursor, session.resumeCursor); - assert.equal(startPayload.threadId, session.threadId); - } - assert.equal(routing.codex.sendTurn.mock.calls.length, 1); - }), - ); - - it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - const initial = yield* provider.startSession(asThreadId("thread-1"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-1"), - cwd: "/tmp/project", - runtimeMode: "full-access", - }); - yield* routing.codex.stopSession(initial.threadId); - routing.codex.startSession.mockClear(); - routing.codex.rollbackThread.mockClear(); - - yield* provider.rollbackConversation({ - threadId: initial.threadId, - numTurns: 1, - }); - - assert.equal(routing.codex.startSession.mock.calls.length, 1); - const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0]; - assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); - if (resumedStartInput && typeof resumedStartInput === "object") { - const startPayload = resumedStartInput as { - provider?: string; - cwd?: string; - resumeCursor?: unknown; - threadId?: string; - }; - assert.equal(startPayload.provider, "codex"); - assert.equal(startPayload.cwd, "/tmp/project"); - assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor); - assert.equal(startPayload.threadId, initial.threadId); - } - assert.equal(routing.codex.rollbackThread.mock.calls.length, 1); - const rollbackCall = routing.codex.rollbackThread.mock.calls[0]; - assert.equal(rollbackCall?.[1], 1); - }), - ); - - it.effect("preserves the persisted binding when stopping a session", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - - const initial = yield* provider.startSession(asThreadId("thread-reap-preserve"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-reap-preserve"), - cwd: "/tmp/project-reap-preserve", - runtimeMode: "full-access", - }); - - yield* provider.stopSession({ threadId: initial.threadId }); - - const persistedAfterStop = yield* runtimeRepository.getByThreadId({ - threadId: initial.threadId, - }); - assert.equal(Option.isSome(persistedAfterStop), true); - if (Option.isSome(persistedAfterStop)) { - assert.equal(persistedAfterStop.value.status, "stopped"); - assert.deepEqual(persistedAfterStop.value.resumeCursor, initial.resumeCursor); - } - - routing.codex.startSession.mockClear(); - routing.codex.sendTurn.mockClear(); - - yield* provider.sendTurn({ - threadId: initial.threadId, - input: "resume after reap", - attachments: [], - }); - - assert.equal(routing.codex.startSession.mock.calls.length, 1); - const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0]; - assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); - if (resumedStartInput && typeof resumedStartInput === "object") { - const startPayload = resumedStartInput as { - provider?: string; - cwd?: string; - resumeCursor?: unknown; - threadId?: string; - }; - assert.equal(startPayload.provider, "codex"); - assert.equal(startPayload.cwd, "/tmp/project-reap-preserve"); - assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor); - assert.equal(startPayload.threadId, initial.threadId); - } - assert.equal(routing.codex.sendTurn.mock.calls.length, 1); - }), - ); - - it.effect("routes explicit claudeAgent provider session starts to the claude adapter", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - const session = yield* provider.startSession(asThreadId("thread-claude"), { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId: asThreadId("thread-claude"), - cwd: "/tmp/project-claude", - runtimeMode: "full-access", - }); - - assert.equal(session.provider, "claudeAgent"); - assert.equal(routing.claude.startSession.mock.calls.length, 1); - const startInput = routing.claude.startSession.mock.calls[0]?.[0]; - assert.equal(typeof startInput === "object" && startInput !== null, true); - if (startInput && typeof startInput === "object") { - const startPayload = startInput as { - provider?: string; - providerInstanceId?: ProviderInstanceId; - cwd?: string; - }; - assert.equal(startPayload.provider, "claudeAgent"); - assert.equal(startPayload.providerInstanceId, claudeAgentInstanceId); - assert.equal(startPayload.cwd, "/tmp/project-claude"); - } - }), - ); - - it.effect("dies when an active session conflicts with its persisted binding", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - const directory = yield* ProviderSessionDirectory.ProviderSessionDirectory; - const threadId = asThreadId("thread-binding-mismatch"); - - yield* provider.startSession(threadId, { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId, - cwd: "/tmp/project-binding-mismatch", - runtimeMode: "full-access", - }); - yield* directory.upsert({ - threadId, - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - runtimeMode: "full-access", - }); - - const exit = yield* Effect.exit(provider.listSessions()); - assert.equal(Exit.hasDies(exit), true); - yield* directory.upsert({ - threadId, - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - runtimeMode: "full-access", - }); - }), - ); - - it.effect("stops stale sessions in other providers after a successful replacement start", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - const threadId = asThreadId("thread-provider-replacement"); - - const codexSession = yield* provider.startSession(threadId, { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId, - cwd: "/tmp/project-provider-replacement", - runtimeMode: "full-access", - }); - - routing.codex.stopSession.mockClear(); - routing.claude.stopSession.mockClear(); - - const claudeSession = yield* provider.startSession(threadId, { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId, - cwd: "/tmp/project-provider-replacement", - runtimeMode: "full-access", - }); - - assert.equal(codexSession.provider, "codex"); - assert.equal(claudeSession.provider, "claudeAgent"); - assert.deepEqual(routing.codex.stopSession.mock.calls, [[threadId]]); - assert.equal(routing.claude.stopSession.mock.calls.length, 0); - - const sessions = yield* provider.listSessions(); - assert.deepEqual( - sessions - .filter((session) => session.threadId === threadId) - .map((session) => session.provider), - ["claudeAgent"], - ); - }), - ); - - it.effect("recovers stale sessions for sendTurn using persisted cwd", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - const initial = yield* provider.startSession(asThreadId("thread-1"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-1"), - cwd: "/tmp/project-send-turn", - runtimeMode: "full-access", - }); - - yield* routing.codex.stopAll(); - routing.codex.startSession.mockClear(); - routing.codex.sendTurn.mockClear(); - - yield* provider.sendTurn({ - threadId: initial.threadId, - input: "resume", - attachments: [], - }); - - assert.equal(routing.codex.startSession.mock.calls.length, 1); - const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0]; - assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); - if (resumedStartInput && typeof resumedStartInput === "object") { - const startPayload = resumedStartInput as { - provider?: string; - cwd?: string; - resumeCursor?: unknown; - threadId?: string; - }; - assert.equal(startPayload.provider, "codex"); - assert.equal(startPayload.cwd, "/tmp/project-send-turn"); - assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor); - assert.equal(startPayload.threadId, initial.threadId); - } - assert.equal(routing.codex.sendTurn.mock.calls.length, 1); - }), - ); - - it.effect("recovers stale claudeAgent sessions for sendTurn using persisted cwd", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - const initial = yield* provider.startSession(asThreadId("thread-claude-send-turn"), { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId: asThreadId("thread-claude-send-turn"), - cwd: "/tmp/project-claude-send-turn", - modelSelection: createModelSelection( - ProviderInstanceId.make("claudeAgent"), - "claude-opus-4-6", - [{ id: "effort", value: "max" }], - ), - runtimeMode: "full-access", - }); - - yield* routing.claude.stopAll(); - routing.claude.startSession.mockClear(); - routing.claude.sendTurn.mockClear(); - - yield* provider.sendTurn({ - threadId: initial.threadId, - input: "resume with claude", - attachments: [], - }); - - assert.equal(routing.claude.startSession.mock.calls.length, 1); - const resumedStartInput = routing.claude.startSession.mock.calls[0]?.[0]; - assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); - if (resumedStartInput && typeof resumedStartInput === "object") { - const startPayload = resumedStartInput as { - provider?: string; - cwd?: string; - modelSelection?: unknown; - resumeCursor?: unknown; - threadId?: string; - }; - assert.equal(startPayload.provider, "claudeAgent"); - assert.equal(startPayload.cwd, "/tmp/project-claude-send-turn"); - assert.deepEqual( - startPayload.modelSelection, - createModelSelection(ProviderInstanceId.make("claudeAgent"), "claude-opus-4-6", [ - { id: "effort", value: "max" }, - ]), - ); - assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor); - assert.equal(startPayload.threadId, initial.threadId); - } - assert.equal(routing.claude.sendTurn.mock.calls.length, 1); - }), - ); - - it.effect("lists no sessions after adapter runtime clears", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - yield* provider.startSession(asThreadId("thread-1"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-1"), - runtimeMode: "full-access", - }); - yield* provider.startSession(asThreadId("thread-2"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-2"), - runtimeMode: "full-access", - }); - - yield* routing.codex.stopAll(); - yield* routing.claude.stopAll(); - - const remaining = yield* provider.listSessions(); - assert.equal(remaining.length, 0); - }), - ); - - it.effect("persists runtime status transitions in provider_session_runtime", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - - const threadId = asThreadId("thread-runtime-status"); - const session = yield* provider.startSession(threadId, { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId, - runtimeMode: "full-access", - }); - yield* provider.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - const runningRuntime = yield* runtimeRepository.getByThreadId({ - threadId: session.threadId, - }); - assert.equal(Option.isSome(runningRuntime), true); - if (Option.isSome(runningRuntime)) { - assert.equal(runningRuntime.value.status, "running"); - assert.deepEqual(runningRuntime.value.resumeCursor, session.resumeCursor); - const payload = runningRuntime.value.runtimePayload; - assert.equal(payload !== null && typeof payload === "object", true); - if (payload !== null && typeof payload === "object" && !Array.isArray(payload)) { - const runtimePayload = payload as { - cwd: string; - model: string | null; - activeTurnId: string | null; - lastError: string | null; - lastRuntimeEvent: string | null; - }; - assert.equal(runtimePayload.cwd, session.cwd); - assert.equal(runtimePayload.model, null); - assert.equal(runtimePayload.activeTurnId, `turn-${String(session.threadId)}`); - assert.equal(runtimePayload.lastError, null); - assert.equal(runtimePayload.lastRuntimeEvent, "provider.sendTurn"); - } - } - }), - ); - - it.effect("reuses persisted resume cursor when startSession is called after a restart", () => - Effect.gen(function* () { - const tempDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-provider-service-start-"), - ); - const dbPath = NodePath.join(tempDir, "orchestration.sqlite"); - const persistenceLayer = makeSqlitePersistenceLive(dbPath); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(persistenceLayer), - ); - - const firstClaude = makeFakeCodexAdapter(CLAUDE_AGENT_DRIVER); - const firstRegistry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("claudeAgent")]: firstClaude.adapter, - }); - const firstDirectoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(runtimeRepositoryLayer), - ); - const firstProviderLayer = makeProviderServiceLive().pipe( - Layer.provide( - Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, firstRegistry), - ), - Layer.provide(firstDirectoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - const initial = yield* Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - return yield* provider.startSession(asThreadId("thread-claude-start"), { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId: asThreadId("thread-claude-start"), - cwd: "/tmp/project-claude-start", - runtimeMode: "full-access", - }); - }).pipe(Effect.provide(firstProviderLayer)); - - yield* Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - yield* provider.listSessions(); - }).pipe(Effect.provide(firstProviderLayer)); - - const secondClaude = makeFakeCodexAdapter(CLAUDE_AGENT_DRIVER); - const secondRegistry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("claudeAgent")]: secondClaude.adapter, - }); - const secondDirectoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(runtimeRepositoryLayer), - ); - const secondProviderLayer = makeProviderServiceLive().pipe( - Layer.provide( - Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, secondRegistry), - ), - Layer.provide(secondDirectoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - secondClaude.startSession.mockClear(); - - yield* Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - yield* provider.startSession(initial.threadId, { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId: initial.threadId, - cwd: "/tmp/project-claude-start", - runtimeMode: "full-access", - }); - }).pipe(Effect.provide(secondProviderLayer)); - - assert.equal(secondClaude.startSession.mock.calls.length, 1); - const resumedStartInput = secondClaude.startSession.mock.calls[0]?.[0]; - assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); - if (resumedStartInput && typeof resumedStartInput === "object") { - const startPayload = resumedStartInput as { - provider?: string; - cwd?: string; - resumeCursor?: unknown; - threadId?: string; - }; - assert.equal(startPayload.provider, "claudeAgent"); - assert.equal(startPayload.cwd, "/tmp/project-claude-start"); - assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor); - assert.equal(startPayload.threadId, initial.threadId); - } - - NodeFS.rmSync(tempDir, { recursive: true, force: true }); - }).pipe(Effect.provide(NodeServices.layer)), - ); - - it.effect( - "reuses persisted cwd when startSession resumes a claude session without cwd input", - () => - Effect.gen(function* () { - const tempDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-provider-service-cwd-"), - ); - const dbPath = NodePath.join(tempDir, "orchestration.sqlite"); - const persistenceLayer = makeSqlitePersistenceLive(dbPath); - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(persistenceLayer), - ); - - const firstClaude = makeFakeCodexAdapter(CLAUDE_AGENT_DRIVER); - const firstRegistry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("claudeAgent")]: firstClaude.adapter, - }); - const firstDirectoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(runtimeRepositoryLayer), - ); - const firstProviderLayer = makeProviderServiceLive().pipe( - Layer.provide( - Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, firstRegistry), - ), - Layer.provide(firstDirectoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - const initial = yield* Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - return yield* provider.startSession(asThreadId("thread-claude-cwd"), { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId: asThreadId("thread-claude-cwd"), - cwd: "/tmp/project-claude-cwd", - runtimeMode: "full-access", - }); - }).pipe(Effect.provide(firstProviderLayer)); - - const secondClaude = makeFakeCodexAdapter(CLAUDE_AGENT_DRIVER); - const secondRegistry = makeAdapterRegistryMock({ - [ProviderDriverKind.make("claudeAgent")]: secondClaude.adapter, - }); - const secondDirectoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(runtimeRepositoryLayer), - ); - const secondProviderLayer = makeProviderServiceLive().pipe( - Layer.provide( - Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, secondRegistry), - ), - Layer.provide(secondDirectoryLayer), - Layer.provide(defaultServerSettingsLayer), - Layer.provide(AnalyticsService.layerTest), - Layer.provide( - Layer.succeed( - ProviderEventLoggers.ProviderEventLoggers, - ProviderEventLoggers.NoOpProviderEventLoggers, - ), - ), - ); - - secondClaude.startSession.mockClear(); - - yield* Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - yield* provider.startSession(initial.threadId, { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId: initial.threadId, - runtimeMode: "full-access", - }); - }).pipe(Effect.provide(secondProviderLayer)); - - assert.equal(secondClaude.startSession.mock.calls.length, 1); - const resumedStartInput = secondClaude.startSession.mock.calls[0]?.[0]; - assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); - if (resumedStartInput && typeof resumedStartInput === "object") { - const startPayload = resumedStartInput as { - provider?: string; - cwd?: string; - resumeCursor?: unknown; - threadId?: string; - }; - assert.equal(startPayload.provider, "claudeAgent"); - assert.equal(startPayload.cwd, "/tmp/project-claude-cwd"); - assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor); - assert.equal(startPayload.threadId, initial.threadId); - } - - NodeFS.rmSync(tempDir, { recursive: true, force: true }); - }).pipe(Effect.provide(NodeServices.layer)), - ); -}); - -const fanout = makeProviderServiceLayer(); -fanout.layer("ProviderServiceLive fanout", (it) => { - it.effect("fans out adapter turn completion events", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - const session = yield* provider.startSession(asThreadId("thread-1"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-1"), - runtimeMode: "full-access", - }); - - const eventsRef = yield* Ref.make>([]); - const consumer = yield* Stream.runForEach(provider.streamEvents, (event) => - Ref.update(eventsRef, (current) => [...current, event]), - ).pipe(Effect.forkChild); - yield* advanceTestClock(50); - - const completedEvent: LegacyProviderRuntimeEvent = { - type: "turn.completed", - eventId: asEventId("evt-1"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: session.threadId, - turnId: asTurnId("turn-1"), - status: "completed", - }; - - fanout.codex.emit(completedEvent); - yield* advanceTestClock(50); - - const events = yield* Ref.get(eventsRef); - yield* Fiber.interrupt(consumer); - - assert.equal( - events.some((entry) => entry.type === "turn.completed"), - true, - ); - assert.equal( - events.some( - (entry) => - entry.type === "turn.completed" && entry.providerInstanceId === codexInstanceId, - ), - true, - ); - }), - ); - - it.effect("fans out canonical runtime events in emission order", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - const session = yield* provider.startSession(asThreadId("thread-seq"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-seq"), - runtimeMode: "full-access", - }); - - const receivedRef = yield* Ref.make>([]); - const consumer = yield* Stream.take(provider.streamEvents, 3).pipe( - Stream.runForEach((event) => Ref.update(receivedRef, (current) => [...current, event])), - Effect.forkChild, - ); - yield* advanceTestClock(50); - - fanout.codex.emit({ - type: "tool.started", - eventId: asEventId("evt-seq-1"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: session.threadId, - turnId: asTurnId("turn-1"), - toolKind: "command", - title: "Ran command", - }); - fanout.codex.emit({ - type: "tool.completed", - eventId: asEventId("evt-seq-2"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: session.threadId, - turnId: asTurnId("turn-1"), - toolKind: "command", - title: "Ran command", - }); - fanout.codex.emit({ - type: "turn.completed", - eventId: asEventId("evt-seq-3"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: session.threadId, - turnId: asTurnId("turn-1"), - status: "completed", - }); - - yield* Fiber.join(consumer); - const received = yield* Ref.get(receivedRef); - assert.deepEqual( - received.map((event) => event.eventId), - [asEventId("evt-seq-1"), asEventId("evt-seq-2"), asEventId("evt-seq-3")], - ); - }), - ); - - it.effect("keeps subscriber delivery ordered and isolates failing subscribers", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - const session = yield* provider.startSession(asThreadId("thread-1"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-1"), - runtimeMode: "full-access", - }); - - const receivedByHealthy: string[] = []; - const expectedEventIds = new Set(["evt-ordered-1", "evt-ordered-2", "evt-ordered-3"]); - const healthyFiber = yield* Stream.take(provider.streamEvents, 3).pipe( - Stream.runForEach((event) => - Effect.sync(() => { - receivedByHealthy.push(event.eventId); - }), - ), - Effect.forkChild, - ); - const failingFiber = yield* Stream.take(provider.streamEvents, 1).pipe( - Stream.runForEach(() => Effect.fail("listener crash")), - Effect.forkChild, - ); - yield* advanceTestClock(50); - - const events: ReadonlyArray = [ - { - type: "tool.completed", - eventId: asEventId("evt-ordered-1"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: session.threadId, - turnId: asTurnId("turn-1"), - toolKind: "command", - title: "Ran command", - detail: "echo one", - }, - { - type: "message.delta", - eventId: asEventId("evt-ordered-2"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: session.threadId, - turnId: asTurnId("turn-1"), - delta: "hello", - }, - { - type: "turn.completed", - eventId: asEventId("evt-ordered-3"), - provider: ProviderDriverKind.make("codex"), - createdAt: "2026-01-01T00:00:00.000Z", - threadId: session.threadId, - turnId: asTurnId("turn-1"), - status: "completed", - }, - ]; - - for (const event of events) { - fanout.codex.emit(event); - } - const failingResult = yield* Effect.result(Fiber.join(failingFiber)); - assert.equal(failingResult._tag, "Failure"); - yield* Fiber.join(healthyFiber); - - assert.deepEqual( - receivedByHealthy.filter((eventId) => expectedEventIds.has(eventId)).slice(0, 3), - ["evt-ordered-1", "evt-ordered-2", "evt-ordered-3"], - ); - }), - ); - - it.effect("records provider metrics with the routed provider label", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - const session = yield* provider.startSession(asThreadId("thread-metrics"), { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId: asThreadId("thread-metrics"), - cwd: "/tmp/project", - runtimeMode: "full-access", - }); - - yield* provider.interruptTurn({ threadId: session.threadId }); - yield* provider.respondToRequest({ - threadId: session.threadId, - requestId: asRequestId("req-metrics-1"), - decision: "accept", - }); - yield* provider.respondToUserInput({ - threadId: session.threadId, - requestId: asRequestId("req-metrics-2"), - answers: { - sandbox_mode: "workspace-write", - }, - }); - yield* provider.rollbackConversation({ - threadId: session.threadId, - numTurns: 1, - }); - yield* provider.stopSession({ threadId: session.threadId }); - - const snapshots = yield* Metric.snapshot; - - assert.equal( - hasMetricSnapshot(snapshots, "t3_provider_turns_total", { - provider: ProviderDriverKind.make("claudeAgent"), - operation: "interrupt", - outcome: "success", - }), - true, - ); - assert.equal( - hasMetricSnapshot(snapshots, "t3_provider_turns_total", { - provider: ProviderDriverKind.make("claudeAgent"), - operation: "approval-response", - outcome: "success", - }), - true, - ); - assert.equal( - hasMetricSnapshot(snapshots, "t3_provider_turns_total", { - provider: ProviderDriverKind.make("claudeAgent"), - operation: "user-input-response", - outcome: "success", - }), - true, - ); - assert.equal( - hasMetricSnapshot(snapshots, "t3_provider_turns_total", { - provider: ProviderDriverKind.make("claudeAgent"), - operation: "rollback", - outcome: "success", - }), - true, - ); - assert.equal( - hasMetricSnapshot(snapshots, "t3_provider_sessions_total", { - provider: ProviderDriverKind.make("claudeAgent"), - operation: "stop", - outcome: "success", - }), - true, - ); - }), - ); - - it.effect( - "records sendTurn metrics with the resolved provider when modelSelection is omitted", - () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - const session = yield* provider.startSession(asThreadId("thread-send-metrics"), { - provider: ProviderDriverKind.make("claudeAgent"), - providerInstanceId: claudeAgentInstanceId, - threadId: asThreadId("thread-send-metrics"), - cwd: "/tmp/project-send-metrics", - runtimeMode: "full-access", - }); - - yield* provider.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - const snapshots = yield* Metric.snapshot; - - assert.equal( - hasMetricSnapshot(snapshots, "t3_provider_turns_total", { - provider: ProviderDriverKind.make("claudeAgent"), - operation: "send", - outcome: "success", - }), - true, - ); - assert.equal( - hasMetricSnapshot(snapshots, "t3_provider_turn_duration", { - provider: ProviderDriverKind.make("claudeAgent"), - operation: "send", - }), - true, - ); - }), - ); -}); - -const validation = makeProviderServiceLayer(); -validation.layer("ProviderServiceLive validation", (it) => { - it.effect("rejects session starts without an explicit provider instance id", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - validation.codex.startSession.mockClear(); - const failure = yield* Effect.flip( - provider.startSession(asThreadId("thread-missing-instance-id"), { - provider: ProviderDriverKind.make("codex"), - threadId: asThreadId("thread-missing-instance-id"), - runtimeMode: "full-access", - }), - ); - - assert.instanceOf(failure, ProviderValidationError); - assert.include(failure.issue, "Provider instance id is required for provider 'codex'."); - assert.equal(validation.codex.startSession.mock.calls.length, 0); - }), - ); - - it.effect("rejects mismatched provider kind and provider instance id", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - validation.codex.startSession.mockClear(); - validation.claude.startSession.mockClear(); - const failure = yield* Effect.flip( - provider.startSession(asThreadId("thread-instance-mismatch"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: claudeAgentInstanceId, - threadId: asThreadId("thread-instance-mismatch"), - runtimeMode: "full-access", - }), - ); - - assert.instanceOf(failure, ProviderValidationError); - assert.include( - failure.issue, - "Provider instance 'claudeAgent' belongs to driver 'claudeAgent', not 'codex'.", - ); - assert.equal(validation.codex.startSession.mock.calls.length, 0); - assert.equal(validation.claude.startSession.mock.calls.length, 0); - }), - ); - - it.effect("returns ProviderValidationError for invalid input payloads", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - - const failure = yield* Effect.result( - provider.startSession(asThreadId("thread-validation"), { - threadId: asThreadId("thread-validation"), - provider: "invalid-provider", - runtimeMode: "full-access", - } as never), - ); - - assert.equal(failure._tag, "Failure"); - if (failure._tag !== "Failure") { - return; - } - assert.equal(failure.failure._tag, "ProviderValidationError"); - if (failure.failure._tag !== "ProviderValidationError") { - return; - } - assert.equal(failure.failure.operation, "ProviderService.startSession"); - assert.equal(failure.failure.issue.includes("invalid-provider"), true); - }), - ); - - it.effect("accepts startSession when adapter has not emitted provider thread id yet", () => - Effect.gen(function* () { - const provider = yield* ProviderService.ProviderService; - const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - - validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) => - Effect.sync(() => { - const now = "2026-01-01T00:00:00.000Z"; - return { - provider: ProviderDriverKind.make("codex"), - status: "ready", - threadId: input.threadId, - runtimeMode: input.runtimeMode, - cwd: input.cwd ?? process.cwd(), - createdAt: now, - updatedAt: now, - } satisfies ProviderSession; - }), - ); - - const session = yield* provider.startSession(asThreadId("thread-missing"), { - provider: ProviderDriverKind.make("codex"), - providerInstanceId: codexInstanceId, - threadId: asThreadId("thread-missing"), - cwd: "/tmp/project", - runtimeMode: "full-access", - }); - - assert.equal(session.threadId, asThreadId("thread-missing")); - - const runtime = yield* runtimeRepository.getByThreadId({ - threadId: session.threadId, - }); - assert.equal(Option.isSome(runtime), true); - if (Option.isSome(runtime)) { - assert.equal(runtime.value.threadId, session.threadId); - } - }), - ); -}); diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts deleted file mode 100644 index 2eaaeb8ce3c..00000000000 --- a/apps/server/src/provider/Layers/ProviderService.ts +++ /dev/null @@ -1,1098 +0,0 @@ -/** - * ProviderServiceLive - Cross-provider orchestration layer. - * - * Routes validated transport/API calls to provider adapters through - * `ProviderAdapterRegistry` and `ProviderSessionDirectory`, and exposes a - * unified provider event stream for subscribers. - * - * It does not implement provider protocol details (adapter concern). - * - * @module ProviderServiceLive - */ -import { - ModelSelection, - NonNegativeInt, - ThreadId, - ProviderInterruptTurnInput, - ProviderRespondToRequestInput, - ProviderRespondToUserInputInput, - ProviderSendTurnInput, - ProviderSessionStartInput, - ProviderStopSessionInput, - type ProviderInstanceId, - type ProviderDriverKind, - type ProviderRuntimeEvent, - type ProviderSession, -} from "@t3tools/contracts"; -import { causeErrorTag } from "@t3tools/shared/observability"; -import * as DateTime from "effect/DateTime"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as PubSub from "effect/PubSub"; -import * as Ref from "effect/Ref"; -import * as Schema from "effect/Schema"; -import * as SchemaIssue from "effect/SchemaIssue"; -import * as Stream from "effect/Stream"; - -import { - increment, - providerMetricAttributes, - providerRuntimeEventsTotal, - providerSessionsTotal, - providerTurnDuration, - providerTurnsTotal, - providerTurnMetricAttributes, - withMetrics, -} from "../../observability/Metrics.ts"; -import { type ProviderAdapterError, ProviderValidationError } from "../Errors.ts"; -import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts"; -import * as ProviderAdapterRegistry from "../Services/ProviderAdapterRegistry.ts"; -import * as ProviderService from "../Services/ProviderService.ts"; -import * as ProviderSessionDirectory from "../Services/ProviderSessionDirectory.ts"; -import { type EventNdjsonLogger } from "./EventNdjsonLogger.ts"; -import * as ProviderEventLoggers from "./ProviderEventLoggers.ts"; -import * as AnalyticsService from "../../telemetry/AnalyticsService.ts"; -import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; -import * as McpSessionRegistry from "../../mcp/McpSessionRegistry.ts"; -const isModelSelection = Schema.is(ModelSelection); - -/** - * Hook for tests that want to override the canonical event logger pulled - * from `ProviderEventLoggers`. Production wiring leaves this undefined and - * reads the logger off the tag. - */ -export interface ProviderServiceLiveOptions { - readonly canonicalEventLogger?: EventNdjsonLogger; -} - -type ProviderServiceMethod = - ProviderService.ProviderService["Service"][Name]; - -const ProviderRollbackConversationInput = Schema.Struct({ - threadId: ThreadId, - numTurns: NonNegativeInt, -}); - -function toValidationError( - operation: string, - issue: string, - cause?: unknown, -): ProviderValidationError { - return new ProviderValidationError({ - operation, - issue, - ...(cause !== undefined ? { cause } : {}), - }); -} - -const decodeInputOrValidationError = (input: { - readonly operation: string; - readonly schema: S; - readonly payload: unknown; -}) => { - const decodeProviderRequestInput = Schema.decodeUnknownEffect(input.schema); - return decodeProviderRequestInput(input.payload).pipe( - Effect.mapError( - (schemaError) => - new ProviderValidationError({ - operation: input.operation, - issue: SchemaIssue.makeFormatterDefault()(schemaError.issue), - cause: schemaError, - }), - ), - ); -}; - -function toRuntimeStatus(session: ProviderSession): "starting" | "running" | "stopped" | "error" { - switch (session.status) { - case "connecting": - return "starting"; - case "error": - return "error"; - case "closed": - return "stopped"; - case "ready": - case "running": - default: - return "running"; - } -} - -function toRuntimePayloadFromSession( - session: ProviderSession, - extra?: { - readonly modelSelection?: unknown; - readonly lastRuntimeEvent?: string; - readonly lastRuntimeEventAt?: string; - }, -): Record { - return { - cwd: session.cwd ?? null, - model: session.model ?? null, - activeTurnId: session.activeTurnId ?? null, - lastError: session.lastError ?? null, - ...(extra?.modelSelection !== undefined ? { modelSelection: extra.modelSelection } : {}), - ...(extra?.lastRuntimeEvent !== undefined ? { lastRuntimeEvent: extra.lastRuntimeEvent } : {}), - ...(extra?.lastRuntimeEventAt !== undefined - ? { lastRuntimeEventAt: extra.lastRuntimeEventAt } - : {}), - }; -} - -function readPersistedModelSelection( - runtimePayload: ProviderSessionDirectory.ProviderRuntimeBinding["runtimePayload"], -): ModelSelection | undefined { - if (!runtimePayload || typeof runtimePayload !== "object" || Array.isArray(runtimePayload)) { - return undefined; - } - const raw = "modelSelection" in runtimePayload ? runtimePayload.modelSelection : undefined; - return isModelSelection(raw) ? raw : undefined; -} - -function readPersistedCwd( - runtimePayload: ProviderSessionDirectory.ProviderRuntimeBinding["runtimePayload"], -): string | undefined { - if (!runtimePayload || typeof runtimePayload !== "object" || Array.isArray(runtimePayload)) { - return undefined; - } - const rawCwd = "cwd" in runtimePayload ? runtimePayload.cwd : undefined; - if (typeof rawCwd !== "string") return undefined; - const trimmed = rawCwd.trim(); - return trimmed.length > 0 ? trimmed : undefined; -} - -const dieOnMissingBindingInstanceId = ( - operation: string, - payload: { - readonly providerInstanceId?: ProviderInstanceId | undefined; - readonly provider?: ProviderDriverKind | undefined; - }, -): ProviderInstanceId => { - if (payload.providerInstanceId !== undefined) { - return payload.providerInstanceId; - } - throw new Error( - payload.provider - ? `${operation}: provider instance id is required for provider '${payload.provider}'.` - : `${operation}: provider instance id is required.`, - ); -}; - -const correlateRuntimeEventWithInstance = ( - source: { - readonly instanceId: ProviderInstanceId; - readonly provider: ProviderDriverKind; - }, - event: ProviderRuntimeEvent, -): ProviderRuntimeEvent => { - if (event.provider !== source.provider) { - throw new Error( - `ProviderService.streamEvents: provider instance '${source.instanceId}' is backed by driver '${source.provider}' but emitted driver '${event.provider}'.`, - ); - } - if (event.providerInstanceId !== undefined && event.providerInstanceId !== source.instanceId) { - throw new Error( - `ProviderService.streamEvents: provider instance '${source.instanceId}' emitted event for instance '${event.providerInstanceId}'.`, - ); - } - return { ...event, providerInstanceId: source.instanceId }; -}; - -const makeProviderService = Effect.fn("makeProviderService")(function* ( - options?: ProviderServiceLiveOptions, -) { - const analytics = yield* Effect.service(AnalyticsService.AnalyticsService); - const eventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers; - // Options-provided logger wins (test overrides); otherwise we take whatever - // the `ProviderEventLoggers` tag exposes — `undefined` means "no canonical - // log writer is attached", which downstream code already handles as a - // no-op. - const canonicalEventLogger = options?.canonicalEventLogger ?? eventLoggers.canonical; - - const registry = yield* ProviderAdapterRegistry.ProviderAdapterRegistry; - const directory = yield* ProviderSessionDirectory.ProviderSessionDirectory; - const runtimeEventPubSub = yield* PubSub.unbounded(); - const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - const prepareMcpSession = (threadId: ThreadId, providerInstanceId: ProviderInstanceId) => - McpSessionRegistry.issueActiveMcpCredential({ threadId, providerInstanceId }).pipe( - Effect.tap((credential) => - credential - ? Effect.sync(() => McpProviderSession.setMcpProviderSession(credential.config)) - : Effect.void, - ), - ); - const clearMcpSession = (threadId: ThreadId) => - McpSessionRegistry.revokeActiveMcpThread(threadId).pipe( - Effect.tap(() => Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId))), - ); - - const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect => - Effect.succeed(event).pipe( - Effect.tap((canonicalEvent) => - canonicalEventLogger - ? canonicalEventLogger.write(canonicalEvent, canonicalEvent.threadId) - : Effect.void, - ), - Effect.flatMap((canonicalEvent) => PubSub.publish(runtimeEventPubSub, canonicalEvent)), - Effect.asVoid, - ); - - const requireBindingInstanceId = ( - operation: string, - payload: { - readonly providerInstanceId?: ProviderInstanceId | undefined; - readonly provider?: ProviderDriverKind | undefined; - }, - ): Effect.Effect => - payload.providerInstanceId !== undefined - ? Effect.succeed(payload.providerInstanceId) - : Effect.fail( - toValidationError( - operation, - payload.provider - ? `Provider instance id is required for provider '${payload.provider}'.` - : "Provider instance id is required.", - ), - ); - - const upsertSessionBinding = ( - session: ProviderSession, - threadId: ThreadId, - extra?: { - readonly modelSelection?: unknown; - readonly lastRuntimeEvent?: string; - readonly lastRuntimeEventAt?: string; - }, - ) => - Effect.gen(function* () { - const providerInstanceId = yield* requireBindingInstanceId( - "ProviderService.upsertSessionBinding", - session, - ); - yield* directory.upsert({ - threadId, - provider: session.provider, - providerInstanceId, - runtimeMode: session.runtimeMode, - status: toRuntimeStatus(session), - ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}), - runtimePayload: toRuntimePayloadFromSession(session, extra), - }); - }); - - const processRuntimeEvent = ( - source: { - readonly instanceId: ProviderInstanceId; - readonly provider: ProviderDriverKind; - }, - event: ProviderRuntimeEvent, - ): Effect.Effect => - Effect.sync(() => correlateRuntimeEventWithInstance(source, event)).pipe( - Effect.flatMap((canonicalEvent) => - increment(providerRuntimeEventsTotal, { - provider: canonicalEvent.provider, - eventType: canonicalEvent.type, - }).pipe(Effect.andThen(publishRuntimeEvent(canonicalEvent))), - ), - ); - - // `subscribedAdapters` is our source-of-truth for "which instance adapters - // are currently wired into the runtime event bus". It both tracks the set - // of live subscriptions (so `reconcileInstanceSubscriptions` can diff and - // fork only the *new* or *rebuilt* ones) and serves as the dynamic adapter - // list consumed by `stopStaleSessionsForThread`, `listSessions`, and - // `runStopAll` — replacing the pre-Slice-D startup snapshot so hot-added - // instances become visible to those call sites as soon as settings edits - // land. - const subscribedAdapters = yield* Ref.make( - new Map>(), - ); - - const getAdapterEntries = Ref.get(subscribedAdapters).pipe( - Effect.map((map) => Array.from(map.entries())), - ); - - // Rebuild the map of id → adapter from the registry and fork a new event - // subscription for every instance that is either brand new or whose adapter - // identity changed (indicating the underlying `ProviderInstance` was torn - // down and rebuilt by `ProviderInstanceRegistry.reconcile`). Orphaned - // fibers for removed/replaced instances exit on their own because their - // adapter's `streamEvents` source terminates when the old scope closes. - const reconcileInstanceSubscriptions = Effect.gen(function* () { - const previous = yield* Ref.get(subscribedAdapters); - const currentIds = yield* registry.listInstances(); - const next = new Map>(); - for (const id of currentIds) { - const adapterOption = yield* registry - .getByInstance(id) - .pipe(Effect.tapError(Effect.logWarning), Effect.option); - if (Option.isNone(adapterOption)) continue; - const adapter = adapterOption.value; - next.set(id, adapter); - if (previous.get(id) !== adapter) { - yield* Stream.runForEach(adapter.streamEvents, (event) => - processRuntimeEvent( - { - instanceId: id, - provider: adapter.provider, - }, - event, - ), - ).pipe(Effect.forkScoped); - } - } - yield* Ref.set(subscribedAdapters, next); - }); - - const instanceChanges = yield* registry.subscribeChanges; - yield* reconcileInstanceSubscriptions; - yield* Stream.runForEach( - Stream.fromSubscription(instanceChanges), - () => reconcileInstanceSubscriptions, - ).pipe(Effect.forkScoped); - - const recoverSessionForThread = Effect.fn("recoverSessionForThread")(function* (input: { - readonly binding: ProviderSessionDirectory.ProviderRuntimeBinding; - readonly operation: string; - }) { - const bindingInstanceId = yield* requireBindingInstanceId(input.operation, input.binding); - yield* Effect.annotateCurrentSpan({ - "provider.operation": "recover-session", - "provider.kind": input.binding.provider, - "provider.instance_id": bindingInstanceId, - "provider.thread_id": input.binding.threadId, - }); - return yield* Effect.gen(function* () { - const adapter = yield* registry.getByInstance(bindingInstanceId); - const hasResumeCursor = - input.binding.resumeCursor !== null && input.binding.resumeCursor !== undefined; - const hasActiveSession = yield* adapter.hasSession(input.binding.threadId); - if (hasActiveSession) { - const activeSessions = yield* adapter.listSessions(); - const existing = activeSessions.find( - (session) => session.threadId === input.binding.threadId, - ); - if (existing) { - yield* upsertSessionBinding( - { ...existing, providerInstanceId: bindingInstanceId }, - input.binding.threadId, - ); - yield* analytics.record("provider.session.recovered", { - provider: existing.provider, - strategy: "adopt-existing", - hasResumeCursor: existing.resumeCursor !== undefined, - }); - return { adapter, session: existing } as const; - } - } - - if (!hasResumeCursor) { - return yield* toValidationError( - input.operation, - `Cannot recover thread '${input.binding.threadId}' because no provider resume state is persisted.`, - ); - } - - const persistedCwd = readPersistedCwd(input.binding.runtimePayload); - const persistedModelSelection = readPersistedModelSelection(input.binding.runtimePayload); - - yield* prepareMcpSession(input.binding.threadId, bindingInstanceId); - const resumed = yield* adapter - .startSession({ - threadId: input.binding.threadId, - provider: input.binding.provider, - providerInstanceId: bindingInstanceId, - ...(persistedCwd ? { cwd: persistedCwd } : {}), - ...(persistedModelSelection ? { modelSelection: persistedModelSelection } : {}), - ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}), - runtimeMode: input.binding.runtimeMode ?? "full-access", - }) - .pipe(Effect.onError(() => clearMcpSession(input.binding.threadId))); - if (resumed.provider !== adapter.provider) { - yield* clearMcpSession(input.binding.threadId); - return yield* toValidationError( - input.operation, - `Adapter/provider mismatch while recovering thread '${input.binding.threadId}'. Expected '${adapter.provider}', received '${resumed.provider}'.`, - ); - } - - yield* upsertSessionBinding( - { ...resumed, providerInstanceId: bindingInstanceId }, - input.binding.threadId, - ); - yield* analytics.record("provider.session.recovered", { - provider: resumed.provider, - strategy: "resume-thread", - hasResumeCursor: resumed.resumeCursor !== undefined, - }); - return { adapter, session: resumed } as const; - }).pipe( - withMetrics({ - counter: providerSessionsTotal, - attributes: providerMetricAttributes(input.binding.provider, { - operation: "recover", - }), - }), - ); - }); - - const resolveRoutableSession = Effect.fn("resolveRoutableSession")(function* (input: { - readonly threadId: ThreadId; - readonly operation: string; - readonly allowRecovery: boolean; - }) { - const bindingOption = yield* directory.getBinding(input.threadId); - const binding = Option.getOrUndefined(bindingOption); - if (!binding) { - return yield* toValidationError( - input.operation, - `Cannot route thread '${input.threadId}' because no persisted provider binding exists.`, - ); - } - const instanceId = yield* requireBindingInstanceId(input.operation, binding); - const adapter = yield* registry.getByInstance(instanceId); - - const hasRequestedSession = yield* adapter.hasSession(input.threadId); - if (hasRequestedSession) { - return { - adapter, - instanceId, - threadId: input.threadId, - isActive: true, - } as const; - } - - if (!input.allowRecovery) { - return { - adapter, - instanceId, - threadId: input.threadId, - isActive: false, - } as const; - } - - const recovered = yield* recoverSessionForThread({ - binding, - operation: input.operation, - }); - return { - adapter: recovered.adapter, - instanceId, - threadId: input.threadId, - isActive: true, - } as const; - }); - - const stopStaleSessionsForThread = Effect.fn("stopStaleSessionsForThread")(function* (input: { - readonly threadId: ThreadId; - readonly currentInstanceId: ProviderInstanceId; - }) { - const currentAdapters = yield* getAdapterEntries; - yield* Effect.forEach( - currentAdapters, - ([instanceId, adapter]) => - instanceId === input.currentInstanceId - ? Effect.void - : Effect.gen(function* () { - const hasSession = yield* adapter.hasSession(input.threadId); - if (!hasSession) { - return; - } - - yield* adapter.stopSession(input.threadId).pipe( - Effect.tap(() => - analytics.record("provider.session.stopped", { - provider: adapter.provider, - }), - ), - Effect.catchCause((cause) => - Effect.logWarning("provider.session.stop-stale-failed", { - threadId: input.threadId, - provider: adapter.provider, - cause, - }), - ), - ); - }), - { discard: true }, - ); - }); - - const startSession: ProviderServiceMethod<"startSession"> = Effect.fn("startSession")( - function* (threadId, rawInput) { - const parsed = yield* decodeInputOrValidationError({ - operation: "ProviderService.startSession", - schema: ProviderSessionStartInput, - payload: rawInput, - }); - - const resolvedInstanceId = yield* requireBindingInstanceId( - "ProviderService.startSession", - parsed, - ); - let metricProvider = parsed.provider ?? String(resolvedInstanceId); - yield* Effect.annotateCurrentSpan({ - "provider.operation": "start-session", - "provider.instance_id": resolvedInstanceId, - "provider.thread_id": threadId, - "provider.runtime_mode": parsed.runtimeMode, - }); - return yield* Effect.gen(function* () { - const instanceInfo = yield* registry.getInstanceInfo(resolvedInstanceId); - const resolvedProvider = instanceInfo.driverKind; - metricProvider = resolvedProvider; - if (parsed.provider !== undefined && parsed.provider !== resolvedProvider) { - return yield* toValidationError( - "ProviderService.startSession", - `Provider instance '${resolvedInstanceId}' belongs to driver '${resolvedProvider}', not '${parsed.provider}'.`, - ); - } - const input = { - ...parsed, - threadId, - provider: resolvedProvider, - }; - if (!instanceInfo.enabled) { - return yield* toValidationError( - "ProviderService.startSession", - `Provider instance '${resolvedInstanceId}' is disabled in T3 Code settings.`, - ); - } - const persistedBinding = Option.getOrUndefined(yield* directory.getBinding(threadId)); - const effectiveResumeCursor = - input.resumeCursor ?? - (persistedBinding?.providerInstanceId === resolvedInstanceId - ? persistedBinding.resumeCursor - : undefined); - const effectiveCwd = - input.cwd ?? - (persistedBinding?.providerInstanceId === resolvedInstanceId - ? readPersistedCwd(persistedBinding.runtimePayload) - : undefined); - yield* Effect.annotateCurrentSpan({ - "provider.kind": resolvedProvider, - "provider.resume_cursor.source": - input.resumeCursor !== undefined - ? "request" - : effectiveResumeCursor !== undefined && - persistedBinding?.providerInstanceId === resolvedInstanceId - ? "persisted" - : "none", - "provider.resume_cursor.present": effectiveResumeCursor !== undefined, - "provider.cwd.source": - input.cwd !== undefined - ? "request" - : effectiveCwd !== undefined && - persistedBinding?.providerInstanceId === resolvedInstanceId - ? "persisted" - : "none", - "provider.cwd.effective": effectiveCwd ?? "", - }); - const adapter = yield* registry.getByInstance(resolvedInstanceId); - yield* prepareMcpSession(threadId, resolvedInstanceId); - const session = yield* adapter - .startSession({ - ...input, - providerInstanceId: resolvedInstanceId, - ...(effectiveCwd !== undefined ? { cwd: effectiveCwd } : {}), - ...(effectiveResumeCursor !== undefined ? { resumeCursor: effectiveResumeCursor } : {}), - }) - .pipe(Effect.onError(() => clearMcpSession(threadId))); - - if (session.provider !== adapter.provider) { - yield* clearMcpSession(threadId); - return yield* toValidationError( - "ProviderService.startSession", - `Adapter/provider mismatch: requested '${adapter.provider}', received '${session.provider}'.`, - ); - } - const sessionWithInstance = { - ...session, - providerInstanceId: resolvedInstanceId, - }; - - yield* stopStaleSessionsForThread({ - threadId, - currentInstanceId: resolvedInstanceId, - }); - yield* upsertSessionBinding(sessionWithInstance, threadId, { - modelSelection: input.modelSelection, - }); - yield* analytics.record("provider.session.started", { - provider: sessionWithInstance.provider, - runtimeMode: input.runtimeMode, - hasResumeCursor: sessionWithInstance.resumeCursor !== undefined, - hasCwd: typeof effectiveCwd === "string" && effectiveCwd.trim().length > 0, - hasModel: - typeof input.modelSelection?.model === "string" && - input.modelSelection.model.trim().length > 0, - }); - - return sessionWithInstance; - }).pipe( - withMetrics({ - counter: providerSessionsTotal, - attributes: () => - providerMetricAttributes(metricProvider, { - operation: "start", - }), - }), - ); - }, - ); - - const sendTurn: ProviderServiceMethod<"sendTurn"> = Effect.fn("sendTurn")(function* (rawInput) { - const parsed = yield* decodeInputOrValidationError({ - operation: "ProviderService.sendTurn", - schema: ProviderSendTurnInput, - payload: rawInput, - }); - - const input = { - ...parsed, - attachments: parsed.attachments ?? [], - }; - if (!input.input && input.attachments.length === 0) { - return yield* toValidationError( - "ProviderService.sendTurn", - "Either input text or at least one attachment is required", - ); - } - yield* Effect.annotateCurrentSpan({ - "provider.operation": "send-turn", - "provider.thread_id": input.threadId, - "provider.interaction_mode": input.interactionMode, - "provider.attachment_count": input.attachments.length, - }); - let metricProvider = "unknown"; - let metricModel = input.modelSelection?.model; - return yield* Effect.gen(function* () { - const routed = yield* resolveRoutableSession({ - threadId: input.threadId, - operation: "ProviderService.sendTurn", - allowRecovery: true, - }); - metricProvider = routed.adapter.provider; - metricModel = input.modelSelection?.model; - yield* Effect.annotateCurrentSpan({ - "provider.kind": routed.adapter.provider, - ...(input.modelSelection?.model ? { "provider.model": input.modelSelection.model } : {}), - }); - const turn = yield* routed.adapter.sendTurn(input); - yield* directory.upsert({ - threadId: input.threadId, - provider: routed.adapter.provider, - providerInstanceId: routed.instanceId, - status: "running", - ...(turn.resumeCursor !== undefined ? { resumeCursor: turn.resumeCursor } : {}), - runtimePayload: { - ...(input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}), - activeTurnId: turn.turnId, - lastRuntimeEvent: "provider.sendTurn", - lastRuntimeEventAt: yield* nowIso, - }, - }); - yield* analytics.record("provider.turn.sent", { - provider: routed.adapter.provider, - model: input.modelSelection?.model, - interactionMode: input.interactionMode, - attachmentCount: input.attachments.length, - hasInput: typeof input.input === "string" && input.input.trim().length > 0, - }); - return turn; - }).pipe( - withMetrics({ - counter: providerTurnsTotal, - timer: providerTurnDuration, - attributes: () => - providerTurnMetricAttributes({ - provider: metricProvider, - model: metricModel, - extra: { - operation: "send", - }, - }), - }), - ); - }); - - const interruptTurn: ProviderServiceMethod<"interruptTurn"> = Effect.fn("interruptTurn")( - function* (rawInput) { - const input = yield* decodeInputOrValidationError({ - operation: "ProviderService.interruptTurn", - schema: ProviderInterruptTurnInput, - payload: rawInput, - }); - let metricProvider = "unknown"; - return yield* Effect.gen(function* () { - const routed = yield* resolveRoutableSession({ - threadId: input.threadId, - operation: "ProviderService.interruptTurn", - allowRecovery: true, - }); - metricProvider = routed.adapter.provider; - yield* Effect.annotateCurrentSpan({ - "provider.operation": "interrupt-turn", - "provider.kind": routed.adapter.provider, - "provider.thread_id": input.threadId, - "provider.turn_id": input.turnId, - }); - yield* routed.adapter.interruptTurn(routed.threadId, input.turnId); - yield* analytics.record("provider.turn.interrupted", { - provider: routed.adapter.provider, - }); - }).pipe( - withMetrics({ - counter: providerTurnsTotal, - outcomeAttributes: () => - providerMetricAttributes(metricProvider, { - operation: "interrupt", - }), - }), - ); - }, - ); - - const respondToRequest: ProviderServiceMethod<"respondToRequest"> = Effect.fn("respondToRequest")( - function* (rawInput) { - const input = yield* decodeInputOrValidationError({ - operation: "ProviderService.respondToRequest", - schema: ProviderRespondToRequestInput, - payload: rawInput, - }); - let metricProvider = "unknown"; - return yield* Effect.gen(function* () { - const routed = yield* resolveRoutableSession({ - threadId: input.threadId, - operation: "ProviderService.respondToRequest", - allowRecovery: true, - }); - metricProvider = routed.adapter.provider; - yield* Effect.annotateCurrentSpan({ - "provider.operation": "respond-to-request", - "provider.kind": routed.adapter.provider, - "provider.thread_id": input.threadId, - "provider.request_id": input.requestId, - }); - yield* routed.adapter.respondToRequest(routed.threadId, input.requestId, input.decision); - yield* analytics.record("provider.request.responded", { - provider: routed.adapter.provider, - decision: input.decision, - }); - }).pipe( - withMetrics({ - counter: providerTurnsTotal, - outcomeAttributes: () => - providerMetricAttributes(metricProvider, { - operation: "approval-response", - }), - }), - ); - }, - ); - - const respondToUserInput: ProviderServiceMethod<"respondToUserInput"> = Effect.fn( - "respondToUserInput", - )(function* (rawInput) { - const input = yield* decodeInputOrValidationError({ - operation: "ProviderService.respondToUserInput", - schema: ProviderRespondToUserInputInput, - payload: rawInput, - }); - let metricProvider = "unknown"; - return yield* Effect.gen(function* () { - const routed = yield* resolveRoutableSession({ - threadId: input.threadId, - operation: "ProviderService.respondToUserInput", - allowRecovery: true, - }); - metricProvider = routed.adapter.provider; - yield* Effect.annotateCurrentSpan({ - "provider.operation": "respond-to-user-input", - "provider.kind": routed.adapter.provider, - "provider.thread_id": input.threadId, - "provider.request_id": input.requestId, - }); - yield* routed.adapter.respondToUserInput(routed.threadId, input.requestId, input.answers); - }).pipe( - withMetrics({ - counter: providerTurnsTotal, - outcomeAttributes: () => - providerMetricAttributes(metricProvider, { - operation: "user-input-response", - }), - }), - ); - }); - - const stopSession: ProviderServiceMethod<"stopSession"> = Effect.fn("stopSession")( - function* (rawInput) { - const input = yield* decodeInputOrValidationError({ - operation: "ProviderService.stopSession", - schema: ProviderStopSessionInput, - payload: rawInput, - }); - let metricProvider = "unknown"; - return yield* Effect.gen(function* () { - const routed = yield* resolveRoutableSession({ - threadId: input.threadId, - operation: "ProviderService.stopSession", - allowRecovery: false, - }); - metricProvider = routed.adapter.provider; - yield* Effect.annotateCurrentSpan({ - "provider.operation": "stop-session", - "provider.kind": routed.adapter.provider, - "provider.thread_id": input.threadId, - }); - if (routed.isActive) { - yield* routed.adapter.stopSession(routed.threadId); - } - yield* clearMcpSession(input.threadId); - yield* directory.upsert({ - threadId: input.threadId, - provider: routed.adapter.provider, - providerInstanceId: routed.instanceId, - status: "stopped", - runtimePayload: { - activeTurnId: null, - }, - }); - yield* analytics.record("provider.session.stopped", { - provider: routed.adapter.provider, - }); - }).pipe( - withMetrics({ - counter: providerSessionsTotal, - outcomeAttributes: () => - providerMetricAttributes(metricProvider, { - operation: "stop", - }), - }), - ); - }, - ); - - const listSessions: ProviderServiceMethod<"listSessions"> = Effect.fn("listSessions")( - function* () { - const currentAdapters = yield* getAdapterEntries; - const sessionsByProvider = yield* Effect.forEach(currentAdapters, ([instanceId, adapter]) => - adapter.listSessions().pipe( - Effect.map((sessions) => - sessions.map((session) => ({ - ...session, - providerInstanceId: instanceId, - })), - ), - ), - ); - const activeSessions = sessionsByProvider.flatMap((sessions) => sessions); - const persistedBindings = yield* directory.listThreadIds().pipe( - Effect.flatMap((threadIds) => - Effect.forEach( - threadIds, - (threadId) => - directory - .getBinding(threadId) - .pipe( - Effect.orElseSucceed(() => - Option.none(), - ), - ), - { concurrency: "unbounded" }, - ), - ), - Effect.orElseSucceed( - () => [] as Array>, - ), - ); - const bindingsByThreadId = new Map< - ThreadId, - ProviderSessionDirectory.ProviderRuntimeBinding - >(); - for (const bindingOption of persistedBindings) { - const binding = Option.getOrUndefined(bindingOption); - if (binding) { - bindingsByThreadId.set(binding.threadId, binding); - } - } - - const sessions: ProviderSession[] = []; - for (const session of activeSessions) { - const binding = bindingsByThreadId.get(session.threadId); - if (!binding) { - sessions.push(session); - continue; - } - - const overrides: { - resumeCursor?: ProviderSession["resumeCursor"]; - runtimeMode?: ProviderSession["runtimeMode"]; - providerInstanceId?: ProviderSession["providerInstanceId"]; - } = {}; - overrides.providerInstanceId = dieOnMissingBindingInstanceId( - "ProviderService.listSessions", - binding, - ); - if (binding.provider !== session.provider) { - return yield* Effect.die( - new Error( - `ProviderService.listSessions: thread '${session.threadId}' is active on provider '${session.provider}' but persisted binding names provider '${binding.provider}'.`, - ), - ); - } - if (overrides.providerInstanceId !== session.providerInstanceId) { - return yield* Effect.die( - new Error( - `ProviderService.listSessions: thread '${session.threadId}' is active on provider instance '${session.providerInstanceId}' but persisted binding names '${overrides.providerInstanceId}'.`, - ), - ); - } - if (session.resumeCursor === undefined && binding.resumeCursor !== undefined) { - overrides.resumeCursor = binding.resumeCursor; - } - if (binding.runtimeMode !== undefined) { - overrides.runtimeMode = binding.runtimeMode; - } - sessions.push(Object.assign({}, session, overrides)); - } - return sessions; - }, - ); - - const getCapabilities: ProviderServiceMethod<"getCapabilities"> = (instanceId) => - registry.getByInstance(instanceId).pipe(Effect.map((adapter) => adapter.capabilities)); - - const getInstanceInfo: ProviderServiceMethod<"getInstanceInfo"> = (instanceId) => - registry.getInstanceInfo(instanceId); - - const rollbackConversation: ProviderServiceMethod<"rollbackConversation"> = Effect.fn( - "rollbackConversation", - )(function* (rawInput) { - const input = yield* decodeInputOrValidationError({ - operation: "ProviderService.rollbackConversation", - schema: ProviderRollbackConversationInput, - payload: rawInput, - }); - if (input.numTurns === 0) { - return; - } - let metricProvider = "unknown"; - return yield* Effect.gen(function* () { - const routed = yield* resolveRoutableSession({ - threadId: input.threadId, - operation: "ProviderService.rollbackConversation", - allowRecovery: true, - }); - metricProvider = routed.adapter.provider; - yield* Effect.annotateCurrentSpan({ - "provider.operation": "rollback-conversation", - "provider.kind": routed.adapter.provider, - "provider.thread_id": input.threadId, - "provider.rollback_turns": input.numTurns, - }); - yield* routed.adapter.rollbackThread(routed.threadId, input.numTurns); - yield* analytics.record("provider.conversation.rolled_back", { - provider: routed.adapter.provider, - turns: input.numTurns, - }); - }).pipe( - withMetrics({ - counter: providerTurnsTotal, - outcomeAttributes: () => - providerMetricAttributes(metricProvider, { - operation: "rollback", - }), - }), - ); - }); - - const runStopAll = Effect.fn("runStopAll")(function* () { - const threadIds = yield* directory.listThreadIds(); - const currentAdapters = yield* getAdapterEntries; - const activeSessions = yield* Effect.forEach(currentAdapters, ([instanceId, adapter]) => - adapter.listSessions().pipe( - Effect.map((sessions) => - sessions.map((session) => ({ - ...session, - providerInstanceId: instanceId, - })), - ), - ), - ).pipe(Effect.map((sessionsByAdapter) => sessionsByAdapter.flatMap((sessions) => sessions))); - yield* Effect.forEach(activeSessions, (session) => - Effect.flatMap(nowIso, (lastRuntimeEventAt) => - upsertSessionBinding(session, session.threadId, { - lastRuntimeEvent: "provider.stopAll", - lastRuntimeEventAt, - }), - ), - ).pipe(Effect.asVoid); - yield* Effect.forEach(currentAdapters, ([, adapter]) => adapter.stopAll()).pipe(Effect.asVoid); - yield* McpSessionRegistry.revokeAllActiveMcpCredentials(); - McpProviderSession.clearAllMcpProviderSessions(); - const bindings = yield* directory.listBindings().pipe(Effect.orElseSucceed(() => [])); - yield* Effect.forEach(bindings, (binding) => - Effect.gen(function* () { - const providerInstanceId = dieOnMissingBindingInstanceId( - "ProviderService.stopAll", - binding, - ); - return yield* directory.upsert({ - threadId: binding.threadId, - provider: binding.provider, - providerInstanceId, - status: "stopped", - runtimePayload: { - activeTurnId: null, - lastRuntimeEvent: "provider.stopAll", - lastRuntimeEventAt: yield* nowIso, - }, - }); - }), - ).pipe(Effect.asVoid); - yield* analytics.record("provider.sessions.stopped_all", { - sessionCount: threadIds.length, - }); - yield* analytics.flush; - }); - - yield* Effect.addFinalizer(() => - runStopAll().pipe( - Effect.catchCause((cause) => - Effect.logWarning("failed to stop provider service", { - errorTag: causeErrorTag(cause), - }), - ), - ), - ); - - return { - startSession, - sendTurn, - interruptTurn, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - getCapabilities, - getInstanceInfo, - rollbackConversation, - // Each access creates a fresh PubSub subscription so that multiple - // consumers (ProviderRuntimeIngestion, CheckpointReactor, etc.) each - // independently receive all runtime events. - get streamEvents(): ProviderServiceMethod<"streamEvents"> { - return Stream.fromPubSub(runtimeEventPubSub); - }, - } satisfies ProviderService.ProviderService["Service"]; -}); - -export const ProviderServiceLive = Layer.effect( - ProviderService.ProviderService, - makeProviderService(), -); - -export function makeProviderServiceLive(options?: ProviderServiceLiveOptions) { - return Layer.effect(ProviderService.ProviderService, makeProviderService(options)); -} diff --git a/apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts b/apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts deleted file mode 100644 index 079b7f10ebf..00000000000 --- a/apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off -import * as NodeFS from "node:fs"; -import * as NodeOS from "node:os"; -import * as NodePath from "node:path"; - -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { ProviderDriverKind, ThreadId } from "@t3tools/contracts"; -import { it, assert } from "@effect/vitest"; -import { assertSome } from "@effect/vitest/utils"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as SqlClient from "effect/unstable/sql/SqlClient"; - -import { - makeSqlitePersistenceLive, - SqlitePersistenceMemory, -} from "../../persistence/Layers/Sqlite.ts"; -import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; -import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; -import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; - -function makeDirectoryLayer(persistenceLayer: Layer.Layer) { - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe(Layer.provide(persistenceLayer)); - return Layer.mergeAll( - runtimeRepositoryLayer, - ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)), - NodeServices.layer, - ); -} - -it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryLive", (it) => { - it("upserts and reads thread bindings", () => - Effect.gen(function* () { - const directory = yield* ProviderSessionDirectory; - const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - - const initialThreadId = ThreadId.make("thread-1"); - - yield* directory.upsert({ - provider: ProviderDriverKind.make("codex"), - threadId: initialThreadId, - }); - - const provider = yield* directory.getProvider(initialThreadId); - assert.equal(provider, "codex"); - const resolvedBinding = yield* directory.getBinding(initialThreadId); - assertSome(resolvedBinding, { - threadId: initialThreadId, - provider: ProviderDriverKind.make("codex"), - }); - if (Option.isSome(resolvedBinding)) { - assert.equal(resolvedBinding.value.threadId, initialThreadId); - } - - const nextThreadId = ThreadId.make("thread-2"); - - yield* directory.upsert({ - provider: ProviderDriverKind.make("codex"), - threadId: nextThreadId, - }); - const updatedBinding = yield* directory.getBinding(nextThreadId); - assert.equal(Option.isSome(updatedBinding), true); - if (Option.isSome(updatedBinding)) { - assert.equal(updatedBinding.value.threadId, nextThreadId); - } - - const runtime = yield* runtimeRepository.getByThreadId({ threadId: nextThreadId }); - assert.equal(Option.isSome(runtime), true); - if (Option.isSome(runtime)) { - assert.equal(runtime.value.threadId, nextThreadId); - assert.equal(runtime.value.status, "running"); - assert.equal(runtime.value.providerName, "codex"); - } - - const threadIds = yield* directory.listThreadIds(); - assert.deepEqual(threadIds, [nextThreadId]); - })); - - it("persists runtime fields and merges payload updates", () => - Effect.gen(function* () { - const directory = yield* ProviderSessionDirectory; - const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - - const threadId = ThreadId.make("thread-runtime"); - - yield* directory.upsert({ - provider: ProviderDriverKind.make("codex"), - threadId, - status: "starting", - resumeCursor: { - threadId: "provider-thread-runtime", - }, - runtimePayload: { - cwd: "/tmp/project", - model: "gpt-5-codex", - }, - }); - - yield* directory.upsert({ - provider: ProviderDriverKind.make("codex"), - threadId, - status: "running", - runtimePayload: { - activeTurnId: "turn-1", - }, - }); - - const runtime = yield* runtimeRepository.getByThreadId({ threadId }); - assert.equal(Option.isSome(runtime), true); - if (Option.isSome(runtime)) { - assert.equal(runtime.value.threadId, threadId); - assert.equal(runtime.value.status, "running"); - assert.deepEqual(runtime.value.resumeCursor, { - threadId: "provider-thread-runtime", - }); - assert.deepEqual(runtime.value.runtimePayload, { - cwd: "/tmp/project", - model: "gpt-5-codex", - activeTurnId: "turn-1", - }); - } - })); - - it("lists persisted bindings with metadata in oldest-first order", () => - Effect.gen(function* () { - const directory = yield* ProviderSessionDirectory; - const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - - const olderThreadId = ThreadId.make("thread-runtime-older"); - const newerThreadId = ThreadId.make("thread-runtime-newer"); - - yield* runtimeRepository.upsert({ - threadId: newerThreadId, - providerName: "codex", - providerInstanceId: null, - adapterKey: "codex", - runtimeMode: "full-access", - status: "running", - lastSeenAt: "2026-04-14T12:05:00.000Z", - resumeCursor: { - opaque: "resume-newer", - }, - runtimePayload: { - cwd: "/tmp/newer", - }, - }); - - yield* runtimeRepository.upsert({ - threadId: olderThreadId, - providerName: "claudeAgent", - providerInstanceId: null, - adapterKey: "claudeAgent", - runtimeMode: "approval-required", - status: "starting", - lastSeenAt: "2026-04-14T12:00:00.000Z", - resumeCursor: { - opaque: "resume-older", - }, - runtimePayload: { - cwd: "/tmp/older", - }, - }); - - const bindings = yield* directory.listBindings(); - - assert.deepEqual(bindings, [ - { - threadId: olderThreadId, - provider: ProviderDriverKind.make("claudeAgent"), - adapterKey: "claudeAgent", - runtimeMode: "approval-required", - status: "starting", - lastSeenAt: "2026-04-14T12:00:00.000Z", - resumeCursor: { - opaque: "resume-older", - }, - runtimePayload: { - cwd: "/tmp/older", - }, - }, - { - threadId: newerThreadId, - provider: ProviderDriverKind.make("codex"), - adapterKey: "codex", - runtimeMode: "full-access", - status: "running", - lastSeenAt: "2026-04-14T12:05:00.000Z", - resumeCursor: { - opaque: "resume-newer", - }, - runtimePayload: { - cwd: "/tmp/newer", - }, - }, - ]); - })); - - it("resets adapterKey to the new provider when provider changes without an explicit adapter key", () => - Effect.gen(function* () { - const directory = yield* ProviderSessionDirectory; - const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - const threadId = ThreadId.make("thread-provider-change"); - - yield* runtimeRepository.upsert({ - threadId, - providerName: "claudeAgent", - providerInstanceId: null, - adapterKey: "claudeAgent", - runtimeMode: "full-access", - status: "running", - lastSeenAt: "2026-01-01T00:00:00.000Z", - resumeCursor: null, - runtimePayload: null, - }); - - yield* directory.upsert({ - provider: ProviderDriverKind.make("codex"), - threadId, - }); - - const runtime = yield* runtimeRepository.getByThreadId({ threadId }); - assert.equal(Option.isSome(runtime), true); - if (Option.isSome(runtime)) { - assert.equal(runtime.value.providerName, "codex"); - assert.equal(runtime.value.adapterKey, "codex"); - } - })); - - it("rehydrates persisted mappings across layer restart", () => - Effect.gen(function* () { - const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-provider-directory-")); - const dbPath = NodePath.join(tempDir, "orchestration.sqlite"); - const directoryLayer = makeDirectoryLayer(makeSqlitePersistenceLive(dbPath)); - - const threadId = ThreadId.make("thread-restart"); - - yield* Effect.gen(function* () { - const directory = yield* ProviderSessionDirectory; - yield* directory.upsert({ - provider: ProviderDriverKind.make("codex"), - threadId, - }); - }).pipe(Effect.provide(directoryLayer)); - - yield* Effect.gen(function* () { - const directory = yield* ProviderSessionDirectory; - const sql = yield* SqlClient.SqlClient; - const provider = yield* directory.getProvider(threadId); - assert.equal(provider, "codex"); - - const resolvedBinding = yield* directory.getBinding(threadId); - assertSome(resolvedBinding, { - threadId, - provider: ProviderDriverKind.make("codex"), - }); - if (Option.isSome(resolvedBinding)) { - assert.equal(resolvedBinding.value.threadId, threadId); - } - - const legacyTableRows = yield* sql<{ readonly name: string }>` - SELECT name - FROM sqlite_master - WHERE type = 'table' AND name = 'provider_sessions' - `; - assert.equal(legacyTableRows.length, 0); - }).pipe(Effect.provide(directoryLayer)); - - NodeFS.rmSync(tempDir, { recursive: true, force: true }); - })); -}); diff --git a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts b/apps/server/src/provider/Layers/ProviderSessionDirectory.ts deleted file mode 100644 index 23075bd9a06..00000000000 --- a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { defaultInstanceIdForDriver, ProviderDriverKind, type ThreadId } from "@t3tools/contracts"; -import * as DateTime from "effect/DateTime"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Schema from "effect/Schema"; - -import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; -import { ProviderSessionDirectoryPersistenceError, ProviderValidationError } from "../Errors.ts"; -import { - ProviderSessionDirectory, - type ProviderRuntimeBinding, - type ProviderRuntimeBindingWithMetadata, - type ProviderSessionDirectoryShape, -} from "../Services/ProviderSessionDirectory.ts"; -const decodeProviderDriverKindValue = Schema.decodeUnknownEffect(ProviderDriverKind); - -function toPersistenceError(operation: string) { - return (cause: unknown) => - new ProviderSessionDirectoryPersistenceError({ - operation, - detail: `Failed to execute ${operation}.`, - cause, - }); -} - -function decodeProviderDriverKind( - providerName: string, - operation: string, -): Effect.Effect { - return decodeProviderDriverKindValue(providerName).pipe( - Effect.mapError( - (cause) => - new ProviderSessionDirectoryPersistenceError({ - operation, - detail: `Unknown persisted provider '${providerName}'.`, - cause, - }), - ), - ); -} - -function isRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -function mergeRuntimePayload( - existing: unknown | null, - next: unknown | null | undefined, -): unknown | null { - if (next === undefined) { - return existing ?? null; - } - if (isRecord(existing) && isRecord(next)) { - return { ...existing, ...next }; - } - return next; -} - -function toRuntimeBinding( - runtime: ProviderSessionRuntime.ProviderSessionRuntime, - operation: string, -): Effect.Effect { - return decodeProviderDriverKind(runtime.providerName, operation).pipe( - Effect.map( - (provider) => - ({ - threadId: runtime.threadId, - provider, - // Migration boundary only: rows written before the instance split - // have a null provider_instance_id. Promote them as they leave - // persistence so hot routing code never has to infer an instance - // from a driver kind. - providerInstanceId: runtime.providerInstanceId ?? defaultInstanceIdForDriver(provider), - adapterKey: runtime.adapterKey, - runtimeMode: runtime.runtimeMode, - status: runtime.status, - resumeCursor: runtime.resumeCursor, - runtimePayload: runtime.runtimePayload, - lastSeenAt: runtime.lastSeenAt, - }) satisfies ProviderRuntimeBindingWithMetadata, - ), - ); -} - -const makeProviderSessionDirectory = Effect.gen(function* () { - const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; - - const getBinding = (threadId: ThreadId) => - repository.getByThreadId({ threadId }).pipe( - Effect.mapError(toPersistenceError("ProviderSessionDirectory.getBinding:getByThreadId")), - Effect.flatMap((runtime) => - Option.match(runtime, { - onNone: () => Effect.succeed(Option.none()), - onSome: (value) => - toRuntimeBinding(value, "ProviderSessionDirectory.getBinding").pipe( - Effect.map((binding) => Option.some(binding)), - ), - }), - ), - ); - - const upsert: ProviderSessionDirectoryShape["upsert"] = Effect.fn(function* (binding) { - const existing = yield* repository - .getByThreadId({ threadId: binding.threadId }) - .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:getByThreadId"))); - - const existingRuntime = Option.getOrUndefined(existing); - const resolvedThreadId = binding.threadId ?? existingRuntime?.threadId; - if (!resolvedThreadId) { - return yield* new ProviderValidationError({ - operation: "ProviderSessionDirectory.upsert", - issue: "threadId must be a non-empty string.", - }); - } - - const now = DateTime.formatIso(yield* DateTime.now); - const providerChanged = - existingRuntime !== undefined && existingRuntime.providerName !== binding.provider; - const providerInstanceId = - binding.providerInstanceId ?? (!providerChanged ? existingRuntime?.providerInstanceId : null); - if (providerInstanceId === null || providerInstanceId === undefined) { - return yield* new ProviderValidationError({ - operation: "ProviderSessionDirectory.upsert", - issue: "providerInstanceId is required for provider session runtime bindings.", - }); - } - yield* repository - .upsert({ - threadId: resolvedThreadId, - providerName: binding.provider, - providerInstanceId, - adapterKey: - binding.adapterKey ?? - (providerChanged ? binding.provider : (existingRuntime?.adapterKey ?? binding.provider)), - runtimeMode: binding.runtimeMode ?? existingRuntime?.runtimeMode ?? "full-access", - status: binding.status ?? existingRuntime?.status ?? "running", - lastSeenAt: now, - resumeCursor: - binding.resumeCursor !== undefined - ? binding.resumeCursor - : (existingRuntime?.resumeCursor ?? null), - runtimePayload: mergeRuntimePayload( - existingRuntime?.runtimePayload ?? null, - binding.runtimePayload, - ), - }) - .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:upsert"))); - }); - - const getProvider: ProviderSessionDirectoryShape["getProvider"] = (threadId) => - getBinding(threadId).pipe( - Effect.flatMap((binding) => - Option.match(binding, { - onSome: (value) => Effect.succeed(value.provider), - onNone: () => - Effect.fail( - new ProviderSessionDirectoryPersistenceError({ - operation: "ProviderSessionDirectory.getProvider", - detail: `No persisted provider binding found for thread '${threadId}'.`, - }), - ), - }), - ), - ); - - const listThreadIds: ProviderSessionDirectoryShape["listThreadIds"] = () => - repository.list().pipe( - Effect.mapError(toPersistenceError("ProviderSessionDirectory.listThreadIds:list")), - Effect.map((rows) => rows.map((row) => row.threadId)), - ); - - const listBindings: ProviderSessionDirectoryShape["listBindings"] = () => - repository.list().pipe( - Effect.mapError(toPersistenceError("ProviderSessionDirectory.listBindings:list")), - Effect.flatMap((rows) => - Effect.forEach( - rows, - (row) => toRuntimeBinding(row, "ProviderSessionDirectory.listBindings"), - { concurrency: "unbounded" }, - ), - ), - ); - - return { - upsert, - getProvider, - getBinding, - listThreadIds, - listBindings, - } satisfies ProviderSessionDirectoryShape; -}); - -export const ProviderSessionDirectoryLive = Layer.effect( - ProviderSessionDirectory, - makeProviderSessionDirectory, -); - -export function makeProviderSessionDirectoryLive() { - return Layer.effect(ProviderSessionDirectory, makeProviderSessionDirectory); -} diff --git a/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts b/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts deleted file mode 100644 index e976c183a43..00000000000 --- a/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts +++ /dev/null @@ -1,588 +0,0 @@ -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { - ProjectId, - ThreadId, - TurnId, - ProviderDriverKind, - ProviderInstanceId, -} from "@t3tools/contracts"; -import * as Clock from "effect/Clock"; -import * as DateTime from "effect/DateTime"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Layer from "effect/Layer"; -import * as ManagedRuntime from "effect/ManagedRuntime"; -import * as Option from "effect/Option"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import { afterEach, describe, expect, it, vi } from "vite-plus/test"; - -import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; -import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; -import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; -import { ProviderValidationError } from "../Errors.ts"; -import { ProviderSessionReaper } from "../Services/ProviderSessionReaper.ts"; -import { ProviderService, type ProviderServiceShape } from "../Services/ProviderService.ts"; -import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; -import { makeProviderSessionReaperLive } from "./ProviderSessionReaper.ts"; - -const defaultModelSelection = { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", -} as const; - -async function waitFor( - predicate: () => boolean | Promise, - timeoutMs = 2_000, -): Promise { - const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs; - const poll = async (): Promise => { - if (await predicate()) { - return; - } - if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) { - throw new Error("Timed out waiting for expectation."); - } - await Effect.runPromise(Effect.yieldNow); - return poll(); - }; - - return poll(); -} - -const drainFibers = Effect.forEach(Array.from({ length: 10 }), () => Effect.yieldNow, { - discard: true, -}); - -const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never; - -function makeReadModel( - threads: ReadonlyArray<{ - readonly id: ThreadId; - readonly session: { - readonly threadId: ThreadId; - readonly status: "starting" | "running" | "ready" | "interrupted" | "stopped" | "error"; - readonly providerName: "codex" | "claudeAgent"; - readonly runtimeMode: "approval-required" | "full-access" | "auto-accept-edits"; - readonly activeTurnId: TurnId | null; - readonly lastError: string | null; - readonly updatedAt: string; - } | null; - }>, -) { - const now = "2026-01-01T00:00:00.000Z"; - const projectId = ProjectId.make("project-provider-session-reaper"); - - return { - snapshotSequence: 0, - updatedAt: now, - projects: [ - { - id: projectId, - title: "Provider Reaper Project", - workspaceRoot: "/tmp/provider-reaper-project", - defaultModelSelection, - scripts: [], - createdAt: now, - updatedAt: now, - deletedAt: null, - }, - ], - threads: threads.map((thread) => ({ - id: thread.id, - projectId, - title: `Thread ${thread.id}`, - modelSelection: defaultModelSelection, - interactionMode: "default" as const, - runtimeMode: "full-access" as const, - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - archivedAt: null, - latestUserMessageAt: null, - hasPendingApprovals: false, - hasPendingUserInput: false, - hasActionableProposedPlan: false, - latestTurn: null, - messages: [], - session: thread.session, - activities: [], - proposedPlans: [], - checkpoints: [], - deletedAt: null, - })), - }; -} - -describe("ProviderSessionReaper", () => { - let runtime: ManagedRuntime.ManagedRuntime< - ProviderSessionReaper | ProviderSessionRuntime.ProviderSessionRuntimeRepository, - unknown - > | null = null; - let scope: Scope.Closeable | null = null; - - afterEach(async () => { - if (scope) { - await Effect.runPromise(Scope.close(scope, Exit.void)); - } - scope = null; - if (runtime) { - await runtime.dispose(); - } - runtime = null; - }); - - async function createHarness(input: { - readonly readModel: ReturnType; - readonly stopSessionImplementation?: (input: { - readonly threadId: ThreadId; - }) => ReturnType; - }) { - const stoppedThreadIds = new Set(); - const stopSession = vi.fn( - (request) => - (input.stopSessionImplementation - ? input.stopSessionImplementation(request) - : Effect.sync(() => { - stoppedThreadIds.add(request.threadId); - })) as ReturnType, - ); - - const providerService: ProviderServiceShape = { - startSession: () => unsupported(), - sendTurn: () => unsupported(), - interruptTurn: () => unsupported(), - respondToRequest: () => unsupported(), - respondToUserInput: () => unsupported(), - stopSession, - listSessions: () => Effect.succeed([]), - getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }), - getInstanceInfo: (instanceId) => { - const driverKind = ProviderDriverKind.make(String(instanceId)); - return Effect.succeed({ - instanceId, - driverKind, - displayName: undefined, - enabled: true, - continuationIdentity: { - driverKind, - continuationKey: `${driverKind}:instance:${instanceId}`, - }, - }); - }, - rollbackConversation: () => unsupported(), - streamEvents: Stream.empty, - }; - - const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( - Layer.provide(SqlitePersistenceMemory), - ); - const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( - Layer.provide(runtimeRepositoryLayer), - ); - const layer = makeProviderSessionReaperLive({ - inactivityThresholdMs: 1_000, - sweepIntervalMs: 60_000, - }).pipe( - Layer.provideMerge(providerSessionDirectoryLayer), - Layer.provideMerge(runtimeRepositoryLayer), - Layer.provideMerge(Layer.succeed(ProviderService, providerService)), - Layer.provideMerge( - Layer.succeed(ProjectionSnapshotQuery, { - getCommandReadModel: () => Effect.die("unused"), - getSnapshot: () => Effect.die("unused"), - getShellSnapshot: () => Effect.die("unused"), - getArchivedShellSnapshot: () => Effect.die("unused"), - getSnapshotSequence: () => - Effect.succeed({ snapshotSequence: input.readModel.snapshotSequence }), - getCounts: () => Effect.die("unused"), - getActiveProjectByWorkspaceRoot: () => Effect.die("unused"), - getProjectShellById: () => Effect.die("unused"), - getFirstActiveThreadIdByProjectId: () => Effect.die("unused"), - getThreadCheckpointContext: () => Effect.die("unused"), - getFullThreadDiffContext: () => Effect.die("unused"), - getThreadShellById: (threadId) => - Effect.succeed( - input.readModel.threads.find((thread) => thread.id === threadId) - ? Option.some(input.readModel.threads.find((thread) => thread.id === threadId)!) - : Option.none(), - ), - getThreadDetailById: () => Effect.die("unused"), - }), - ), - Layer.provideMerge(NodeServices.layer), - ); - - runtime = ManagedRuntime.make(layer); - return { stopSession, stoppedThreadIds }; - } - - it("reaps stale persisted sessions without active turns", async () => { - const threadId = ThreadId.make("thread-reaper-stale"); - const now = "2026-01-01T00:00:00.000Z"; - const harness = await createHarness({ - readModel: makeReadModel([ - { - id: threadId, - session: { - threadId, - status: "ready", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }, - ]), - }); - const repository = await runtime!.runPromise( - Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), - ); - - await runtime!.runPromise( - repository.upsert({ - threadId, - providerName: "claudeAgent", - providerInstanceId: null, - adapterKey: "claudeAgent", - runtimeMode: "full-access", - status: "running", - lastSeenAt: "2026-04-14T00:00:00.000Z", - resumeCursor: { - opaque: "resume-stale", - }, - runtimePayload: null, - }), - ); - - const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper)); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reaper.start().pipe(Scope.provide(scope))); - - await waitFor(() => harness.stopSession.mock.calls.length === 1); - - expect(harness.stopSession.mock.calls[0]?.[0]).toEqual({ threadId }); - expect(harness.stoppedThreadIds.has(threadId)).toBe(true); - }); - - it("skips stale sessions when the thread still has an active turn", async () => { - const threadId = ThreadId.make("thread-reaper-active-turn"); - const turnId = TurnId.make("turn-reaper-active"); - const now = "2026-01-01T00:00:00.000Z"; - const harness = await createHarness({ - readModel: makeReadModel([ - { - id: threadId, - session: { - threadId, - status: "running", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: turnId, - lastError: null, - updatedAt: now, - }, - }, - ]), - }); - const repository = await runtime!.runPromise( - Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), - ); - - await runtime!.runPromise( - repository.upsert({ - threadId, - providerName: "claudeAgent", - providerInstanceId: null, - adapterKey: "claudeAgent", - runtimeMode: "full-access", - status: "running", - lastSeenAt: "2026-04-14T00:00:00.000Z", - resumeCursor: { - opaque: "resume-active-turn", - }, - runtimePayload: null, - }), - ); - - const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper)); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reaper.start().pipe(Scope.provide(scope))); - await Effect.runPromise(drainFibers); - - expect(harness.stopSession).not.toHaveBeenCalled(); - const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId })); - expect(Option.isSome(remaining)).toBe(true); - }); - - it("does not reap sessions that are still within the inactivity threshold", async () => { - const threadId = ThreadId.make("thread-reaper-fresh"); - const now = DateTime.formatIso(await Effect.runPromise(DateTime.now)); - const harness = await createHarness({ - readModel: makeReadModel([ - { - id: threadId, - session: { - threadId, - status: "ready", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }, - ]), - }); - const repository = await runtime!.runPromise( - Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), - ); - - await runtime!.runPromise( - repository.upsert({ - threadId, - providerName: "claudeAgent", - providerInstanceId: null, - adapterKey: "claudeAgent", - runtimeMode: "full-access", - status: "running", - lastSeenAt: now, - resumeCursor: { - opaque: "resume-fresh", - }, - runtimePayload: null, - }), - ); - - const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper)); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reaper.start().pipe(Scope.provide(scope))); - await Effect.runPromise(drainFibers); - - expect(harness.stopSession).not.toHaveBeenCalled(); - const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId })); - expect(Option.isSome(remaining)).toBe(true); - }); - - it("skips persisted sessions that are already marked stopped", async () => { - const threadId = ThreadId.make("thread-reaper-stopped"); - const now = "2026-01-01T00:00:00.000Z"; - const harness = await createHarness({ - readModel: makeReadModel([ - { - id: threadId, - session: { - threadId, - status: "stopped", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }, - ]), - }); - const repository = await runtime!.runPromise( - Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), - ); - - await runtime!.runPromise( - repository.upsert({ - threadId, - providerName: "claudeAgent", - providerInstanceId: null, - adapterKey: "claudeAgent", - runtimeMode: "full-access", - status: "stopped", - lastSeenAt: "2026-04-14T00:00:00.000Z", - resumeCursor: { - opaque: "resume-stopped", - }, - runtimePayload: null, - }), - ); - - const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper)); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reaper.start().pipe(Scope.provide(scope))); - await Effect.runPromise(drainFibers); - - expect(harness.stopSession).not.toHaveBeenCalled(); - const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId })); - expect(Option.isSome(remaining)).toBe(true); - }); - - it("continues reaping other sessions when one stop attempt fails", async () => { - const failedThreadId = ThreadId.make("thread-reaper-stop-failure"); - const reapedThreadId = ThreadId.make("thread-reaper-stop-success"); - const now = "2026-01-01T00:00:00.000Z"; - const harness = await createHarness({ - readModel: makeReadModel([ - { - id: failedThreadId, - session: { - threadId: failedThreadId, - status: "ready", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }, - { - id: reapedThreadId, - session: { - threadId: reapedThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }, - ]), - stopSessionImplementation: (request) => - request.threadId === failedThreadId - ? Effect.fail( - new ProviderValidationError({ - operation: "ProviderSessionReaper.test", - issue: "simulated stop failure", - }), - ) - : Effect.void, - }); - const repository = await runtime!.runPromise( - Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), - ); - - await runtime!.runPromise( - repository.upsert({ - threadId: failedThreadId, - providerName: "claudeAgent", - providerInstanceId: null, - adapterKey: "claudeAgent", - runtimeMode: "full-access", - status: "running", - lastSeenAt: "2026-04-14T00:00:00.000Z", - resumeCursor: { - opaque: "resume-failure", - }, - runtimePayload: null, - }), - ); - await runtime!.runPromise( - repository.upsert({ - threadId: reapedThreadId, - providerName: "codex", - providerInstanceId: null, - adapterKey: "codex", - runtimeMode: "full-access", - status: "running", - lastSeenAt: "2026-04-14T00:01:00.000Z", - resumeCursor: { - opaque: "resume-success", - }, - runtimePayload: null, - }), - ); - - const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper)); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reaper.start().pipe(Scope.provide(scope))); - - await waitFor(() => harness.stopSession.mock.calls.length === 2); - - expect(harness.stopSession.mock.calls.map(([request]) => request.threadId)).toEqual([ - failedThreadId, - reapedThreadId, - ]); - }); - - it("continues reaping other sessions when one stop attempt defects", async () => { - const defectThreadId = ThreadId.make("thread-reaper-stop-defect"); - const reapedThreadId = ThreadId.make("thread-reaper-stop-after-defect"); - const now = "2026-01-01T00:00:00.000Z"; - const harness = await createHarness({ - readModel: makeReadModel([ - { - id: defectThreadId, - session: { - threadId: defectThreadId, - status: "ready", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }, - { - id: reapedThreadId, - session: { - threadId: reapedThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }, - ]), - stopSessionImplementation: (request) => - request.threadId === defectThreadId - ? Effect.die(new Error("simulated stop defect")) - : Effect.void, - }); - const repository = await runtime!.runPromise( - Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), - ); - - await runtime!.runPromise( - repository.upsert({ - threadId: defectThreadId, - providerName: "claudeAgent", - providerInstanceId: null, - adapterKey: "claudeAgent", - runtimeMode: "full-access", - status: "running", - lastSeenAt: "2026-04-14T00:00:00.000Z", - resumeCursor: { - opaque: "resume-defect", - }, - runtimePayload: null, - }), - ); - await runtime!.runPromise( - repository.upsert({ - threadId: reapedThreadId, - providerName: "codex", - providerInstanceId: null, - adapterKey: "codex", - runtimeMode: "full-access", - status: "running", - lastSeenAt: "2026-04-14T00:01:00.000Z", - resumeCursor: { - opaque: "resume-after-defect", - }, - runtimePayload: null, - }), - ); - - const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper)); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reaper.start().pipe(Scope.provide(scope))); - - await waitFor(() => harness.stopSession.mock.calls.length === 2); - - expect(harness.stopSession.mock.calls.map(([request]) => request.threadId)).toEqual([ - defectThreadId, - reapedThreadId, - ]); - }); -}); diff --git a/apps/server/src/provider/Layers/ProviderSessionReaper.ts b/apps/server/src/provider/Layers/ProviderSessionReaper.ts deleted file mode 100644 index ca396b40596..00000000000 --- a/apps/server/src/provider/Layers/ProviderSessionReaper.ts +++ /dev/null @@ -1,138 +0,0 @@ -import * as Clock from "effect/Clock"; -import * as Duration from "effect/Duration"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Schedule from "effect/Schedule"; - -import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; -import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; -import { - ProviderSessionReaper, - type ProviderSessionReaperShape, -} from "../Services/ProviderSessionReaper.ts"; -import { ProviderService } from "../Services/ProviderService.ts"; - -const DEFAULT_INACTIVITY_THRESHOLD_MS = 30 * 60 * 1000; -const DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1000; - -export interface ProviderSessionReaperLiveOptions { - readonly inactivityThresholdMs?: number; - readonly sweepIntervalMs?: number; -} - -const makeProviderSessionReaper = (options?: ProviderSessionReaperLiveOptions) => - Effect.gen(function* () { - const providerService = yield* ProviderService; - const directory = yield* ProviderSessionDirectory; - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; - - const inactivityThresholdMs = Math.max( - 1, - options?.inactivityThresholdMs ?? DEFAULT_INACTIVITY_THRESHOLD_MS, - ); - const sweepIntervalMs = Math.max(1, options?.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS); - - const sweep = Effect.gen(function* () { - const bindings = yield* directory.listBindings(); - const now = yield* Clock.currentTimeMillis; - let reapedCount = 0; - - for (const binding of bindings) { - if (binding.status === "stopped") { - continue; - } - - const lastSeenMs = Date.parse(binding.lastSeenAt); - if (Number.isNaN(lastSeenMs)) { - yield* Effect.logWarning("provider.session.reaper.invalid-last-seen", { - threadId: binding.threadId, - provider: binding.provider, - lastSeenAt: binding.lastSeenAt, - }); - continue; - } - - const idleDurationMs = now - lastSeenMs; - if (idleDurationMs < inactivityThresholdMs) { - continue; - } - - const thread = yield* projectionSnapshotQuery - .getThreadShellById(binding.threadId) - .pipe(Effect.map(Option.getOrUndefined)); - if (thread?.session?.activeTurnId != null) { - yield* Effect.logDebug("provider.session.reaper.skipped-active-turn", { - threadId: binding.threadId, - activeTurnId: thread.session.activeTurnId, - idleDurationMs, - }); - continue; - } - - const reaped = yield* providerService.stopSession({ threadId: binding.threadId }).pipe( - Effect.tap(() => - Effect.logInfo("provider.session.reaped", { - threadId: binding.threadId, - provider: binding.provider, - idleDurationMs, - reason: "inactivity_threshold", - }), - ), - Effect.as(true), - Effect.catchCause((cause) => - Effect.logWarning("provider.session.reaper.stop-failed", { - threadId: binding.threadId, - provider: binding.provider, - idleDurationMs, - cause, - }).pipe(Effect.as(false)), - ), - ); - - if (reaped) { - reapedCount += 1; - } - } - - if (reapedCount > 0) { - yield* Effect.logInfo("provider.session.reaper.sweep-complete", { - reapedCount, - totalBindings: bindings.length, - }); - } - }); - - const start: ProviderSessionReaperShape["start"] = () => - Effect.gen(function* () { - yield* Effect.forkScoped( - sweep.pipe( - Effect.catch((error: unknown) => - Effect.logWarning("provider.session.reaper.sweep-failed", { - error, - }), - ), - Effect.catchDefect((defect: unknown) => - Effect.logWarning("provider.session.reaper.sweep-defect", { - defect, - }), - ), - Effect.repeat(Schedule.spaced(Duration.millis(sweepIntervalMs))), - ), - ); - - yield* Effect.logInfo("provider.session.reaper.started", { - inactivityThresholdMs, - sweepIntervalMs, - }); - }); - - return { - start, - } satisfies ProviderSessionReaperShape; - }); - -export const makeProviderSessionReaperLive = (options?: ProviderSessionReaperLiveOptions) => - Layer.effect(ProviderSessionReaper, makeProviderSessionReaper(options)); - -export const ProviderSessionReaperLive = makeProviderSessionReaperLive(); diff --git a/apps/server/src/provider/ProviderDriver.ts b/apps/server/src/provider/ProviderDriver.ts index c738882c23a..f50d5396122 100644 --- a/apps/server/src/provider/ProviderDriver.ts +++ b/apps/server/src/provider/ProviderDriver.ts @@ -2,8 +2,8 @@ * ProviderDriver / ProviderInstance — driver SPI as plain values. * * `ProviderDriver` is a record, not a Context.Service. The thing it produces - * (`ProviderInstance`) is also a record — three captured closures - * (`snapshot`, `adapter`, `textGeneration`), an id, and a driver kind. There + * (`ProviderInstance`) is also a record of captured closures + * (`snapshot`, `orchestrationAdapter`, `textGeneration`), an id, and a driver kind. There * are intentionally no per-driver Context tags because tags are * singleton-per-runtime and we need many instances of the same driver. * @@ -30,9 +30,9 @@ import type * as Effect from "effect/Effect"; import type * as Schema from "effect/Schema"; import type * as Scope from "effect/Scope"; +import type { ProviderAdapterV2Shape } from "../orchestration-v2/ProviderAdapter.ts"; import type * as TextGeneration from "../textGeneration/TextGeneration.ts"; -import type { ProviderAdapterError, ProviderDriverError } from "./Errors.ts"; -import type { ProviderAdapterShape } from "./Services/ProviderAdapter.ts"; +import type { ProviderDriverError } from "./Errors.ts"; import type { ServerProviderShape } from "./Services/ServerProvider.ts"; /** @@ -69,7 +69,7 @@ export interface ProviderInstance { readonly accentColor?: string | undefined; readonly enabled: boolean; readonly snapshot: ServerProviderShape; - readonly adapter: ProviderAdapterShape; + readonly orchestrationAdapter: ProviderAdapterV2Shape; readonly textGeneration: TextGeneration.TextGeneration["Service"]; } diff --git a/apps/server/src/provider/Services/ClaudeAdapter.ts b/apps/server/src/provider/Services/ClaudeAdapter.ts deleted file mode 100644 index ed9bd7081bc..00000000000 --- a/apps/server/src/provider/Services/ClaudeAdapter.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * ClaudeAdapter — shape type for the Claude provider adapter. - * - * Historically this module exposed a `Context.Service` tag so consumers - * could inject the adapter through the Effect layer graph. The driver - * model ({@link ../Drivers/ClaudeDriver}) bundles one adapter per - * instance as a captured closure instead, so the tag is gone — we only - * retain the shape interface as a naming anchor for the driver bundle. - * - * @module ClaudeAdapter - */ -import type { ProviderAdapterError } from "../Errors.ts"; -import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; - -/** - * ClaudeAdapterShape — per-instance Claude adapter contract. Carries - * a branded driver kind as the nominal discriminant. - */ -export interface ClaudeAdapterShape extends ProviderAdapterShape {} diff --git a/apps/server/src/provider/Services/CodexAdapter.ts b/apps/server/src/provider/Services/CodexAdapter.ts deleted file mode 100644 index 33fe0fa12be..00000000000 --- a/apps/server/src/provider/Services/CodexAdapter.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * CodexAdapter — shape type for the Codex provider adapter. - * - * Historically this module exposed a `Context.Service` tag so consumers - * could inject the adapter through the Effect layer graph. The driver - * model ({@link ../Drivers/CodexDriver}) bundles one adapter per - * instance as a captured closure instead, so the tag is gone — we only - * retain the shape interface as a naming anchor for the driver bundle. - * - * @module CodexAdapter - */ -import type { ProviderAdapterError } from "../Errors.ts"; -import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; - -/** - * CodexAdapterShape — per-instance Codex adapter contract. Carries - * a branded driver kind as the nominal discriminant. - */ -export interface CodexAdapterShape extends ProviderAdapterShape {} diff --git a/apps/server/src/provider/Services/CursorAdapter.ts b/apps/server/src/provider/Services/CursorAdapter.ts deleted file mode 100644 index 83581f0a454..00000000000 --- a/apps/server/src/provider/Services/CursorAdapter.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * CursorAdapter — shape type for the Cursor provider adapter. - * - * Historically this module exposed a `Context.Service` tag so consumers - * could inject the adapter through the Effect layer graph. The driver - * model ({@link ../Drivers/CursorDriver}) bundles one adapter per - * instance as a captured closure instead, so the tag is gone — we only - * retain the shape interface as a naming anchor for the driver bundle. - * - * @module CursorAdapter - */ -import type { ProviderAdapterError } from "../Errors.ts"; -import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; - -/** - * CursorAdapterShape — per-instance Cursor adapter contract. Carries - * a branded driver kind as the nominal discriminant. - */ -export interface CursorAdapterShape extends ProviderAdapterShape {} diff --git a/apps/server/src/provider/Services/GrokAdapter.ts b/apps/server/src/provider/Services/GrokAdapter.ts deleted file mode 100644 index 73254cefe39..00000000000 --- a/apps/server/src/provider/Services/GrokAdapter.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * GrokAdapter — shape type for the Grok provider adapter. - * - * The driver model ({@link ../Drivers/GrokDriver}) bundles one adapter per - * instance as a captured closure, so this module only retains the shape - * interface as a naming anchor for the driver bundle. - * - * @module GrokAdapter - */ -import type { ProviderAdapterError } from "../Errors.ts"; -import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; - -/** - * GrokAdapterShape — per-instance Grok adapter contract. - */ -export interface GrokAdapterShape extends ProviderAdapterShape {} diff --git a/apps/server/src/provider/Services/OpenCodeAdapter.ts b/apps/server/src/provider/Services/OpenCodeAdapter.ts deleted file mode 100644 index e3ad97904d1..00000000000 --- a/apps/server/src/provider/Services/OpenCodeAdapter.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * OpenCodeAdapter — shape type for the OpenCode provider adapter. - * - * Historically this module exposed a `Context.Service` tag so consumers - * could inject the adapter through the Effect layer graph. The driver - * model ({@link ../Drivers/OpenCodeDriver}) bundles one adapter per - * instance as a captured closure instead, so the tag is gone — we only - * retain the shape interface as a naming anchor for the driver bundle. - * - * @module OpenCodeAdapter - */ -import type { ProviderAdapterError } from "../Errors.ts"; -import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; - -/** - * OpenCodeAdapterShape — per-instance OpenCode adapter contract. Carries - * a branded driver kind as the nominal discriminant. - */ -export interface OpenCodeAdapterShape extends ProviderAdapterShape {} diff --git a/apps/server/src/provider/Services/ProviderAdapter.ts b/apps/server/src/provider/Services/ProviderAdapter.ts deleted file mode 100644 index 01eeae7b7bd..00000000000 --- a/apps/server/src/provider/Services/ProviderAdapter.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * ProviderAdapter - Provider-specific runtime adapter contract. - * - * Defines the provider-native session/protocol operations that `ProviderService` - * routes to after resolving the target provider. Implementations should focus - * on provider behavior only and avoid cross-provider orchestration concerns. - * - * @module ProviderAdapter - */ -import type { - ApprovalRequestId, - ProviderApprovalDecision, - ProviderDriverKind, - ProviderUserInputAnswers, - ProviderRuntimeEvent, - ProviderSendTurnInput, - ProviderSession, - ProviderSessionStartInput, - ThreadId, - ProviderTurnStartResult, - TurnId, -} from "@t3tools/contracts"; -import type * as Effect from "effect/Effect"; -import type * as Stream from "effect/Stream"; - -export type ProviderSessionModelSwitchMode = "in-session" | "unsupported"; - -export interface ProviderAdapterCapabilities { - /** - * Declares whether changing the model on an existing session is supported. - */ - readonly sessionModelSwitch: ProviderSessionModelSwitchMode; -} - -export interface ProviderThreadTurnSnapshot { - readonly id: TurnId; - readonly items: ReadonlyArray; -} - -export interface ProviderThreadSnapshot { - readonly threadId: ThreadId; - readonly turns: ReadonlyArray; -} - -export interface ProviderAdapterShape { - /** - * Provider kind implemented by this adapter. - */ - readonly provider: ProviderDriverKind; - readonly capabilities: ProviderAdapterCapabilities; - - /** - * Start a provider-backed session. - */ - readonly startSession: ( - input: ProviderSessionStartInput, - ) => Effect.Effect; - - /** - * Send a turn to an active provider session. - */ - readonly sendTurn: ( - input: ProviderSendTurnInput, - ) => Effect.Effect; - - /** - * Interrupt an active turn. - */ - readonly interruptTurn: (threadId: ThreadId, turnId?: TurnId) => Effect.Effect; - - /** - * Respond to an interactive approval request. - */ - readonly respondToRequest: ( - threadId: ThreadId, - requestId: ApprovalRequestId, - decision: ProviderApprovalDecision, - ) => Effect.Effect; - - /** - * Respond to a structured user-input request. - */ - readonly respondToUserInput: ( - threadId: ThreadId, - requestId: ApprovalRequestId, - answers: ProviderUserInputAnswers, - ) => Effect.Effect; - - /** - * Stop one provider session. - */ - readonly stopSession: (threadId: ThreadId) => Effect.Effect; - - /** - * List currently active provider sessions for this adapter. - */ - readonly listSessions: () => Effect.Effect>; - - /** - * Check whether this adapter owns an active session id. - */ - readonly hasSession: (threadId: ThreadId) => Effect.Effect; - - /** - * Read a provider thread snapshot. - */ - readonly readThread: (threadId: ThreadId) => Effect.Effect; - - /** - * Roll back a provider thread by N turns. - */ - readonly rollbackThread: ( - threadId: ThreadId, - numTurns: number, - ) => Effect.Effect; - - /** - * Stop all sessions owned by this adapter. - */ - readonly stopAll: () => Effect.Effect; - - /** - * Canonical runtime event stream emitted by this adapter. - */ - readonly streamEvents: Stream.Stream; -} diff --git a/apps/server/src/provider/Services/ProviderAdapterRegistry.ts b/apps/server/src/provider/Services/ProviderAdapterRegistry.ts deleted file mode 100644 index 5b755c42eed..00000000000 --- a/apps/server/src/provider/Services/ProviderAdapterRegistry.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * ProviderAdapterRegistry - Lookup boundary for provider adapter implementations. - * - * Maps a `ProviderInstanceId` (the new per-instance routing key) or a - * `ProviderDriverKind` (legacy single-instance-per-driver key) to the concrete - * adapter service (Codex, Claude, etc). It does not own session lifecycle - * or routing rules; `ProviderService` uses this registry together with - * `ProviderSessionDirectory`. - * - * During the driver/instance migration this tag exposes both flavours: - * - * - `getByInstance` / `listInstances` — new per-instance routing. Callers - * that already know an `instanceId` (threads, sessions, events) - * should prefer these. - * (`defaultInstanceIdForDriver(kind) === kind`), matching the pre-Slice-D - * behaviour. New code should not grow additional callers of the kind-keyed - * methods; they exist so the settings UI, WS refresh RPC, and a handful - * of legacy persisted rows can still be routed during the rollout. - * - * @module ProviderAdapterRegistry - */ -import type { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts"; -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; -import type * as PubSub from "effect/PubSub"; -import type * as Scope from "effect/Scope"; -import type * as Stream from "effect/Stream"; - -import type { ProviderAdapterError, ProviderUnsupportedError } from "../Errors.ts"; -import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; -import type { ProviderContinuationIdentity } from "../ProviderDriver.ts"; - -export interface ProviderInstanceRoutingInfo { - readonly instanceId: ProviderInstanceId; - readonly driverKind: ProviderDriverKind; - readonly displayName: string | undefined; - readonly accentColor?: string | undefined; - readonly enabled: boolean; - readonly continuationIdentity: ProviderContinuationIdentity; -} - -/** - * ProviderAdapterRegistryShape - Service API for adapter lookup. - */ -export interface ProviderAdapterRegistryShape { - /** - * Resolve the adapter for a specific instance id. Returns - * `ProviderUnsupportedError` if no such instance is currently registered - * (which covers "never configured" *and* "configured but the driver is - * unavailable in this build" — both surface the same failure to callers - * that expect a working adapter). - */ - readonly getByInstance: ( - instanceId: ProviderInstanceId, - ) => Effect.Effect, ProviderUnsupportedError>; - - readonly getInstanceInfo: ( - instanceId: ProviderInstanceId, - ) => Effect.Effect; - - /** - * List all live instance ids. Excludes unavailable/shadow instances — - * callers of this method want something they can pass to `getByInstance`. - */ - readonly listInstances: () => Effect.Effect>; - - /** - * Legacy: list provider kinds whose default instance is currently - * registered. - * - * @deprecated Prefer `listInstances`. Retained for migration-era call - * sites that iterate providers to build UI/metrics. - */ - readonly listProviders: () => Effect.Effect>; - - /** - * Change notification stream mirroring `ProviderInstanceRegistry.streamChanges`. - * Emits one `void` tick whenever the set of live instances changes - * (instance added, removed, or rebuilt after a settings edit). Consumers - * that fan out `adapter.streamEvents` per instance — e.g. `ProviderService`'s - * runtime event bus — re-pull `listInstances` on each tick and fork new - * subscriptions for instances they haven't seen yet. - */ - readonly streamChanges: Stream.Stream; - - /** - * Acquire a change subscription synchronously in the caller's current fiber. - * Consumers that must avoid missing a publish between initial reconciliation - * and watcher startup should use this, then fork `Stream.fromSubscription`. - */ - readonly subscribeChanges: Effect.Effect, never, Scope.Scope>; -} - -/** - * ProviderAdapterRegistry - Service tag for provider adapter lookup. - */ -export class ProviderAdapterRegistry extends Context.Service< - ProviderAdapterRegistry, - ProviderAdapterRegistryShape ->()("t3/provider/Services/ProviderAdapterRegistry") {} diff --git a/apps/server/src/provider/Services/ProviderService.ts b/apps/server/src/provider/Services/ProviderService.ts deleted file mode 100644 index 4d4cb4fa01a..00000000000 --- a/apps/server/src/provider/Services/ProviderService.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * ProviderService - Service interface for provider sessions, turns, and checkpoints. - * - * Acts as the cross-provider facade used by transports (WebSocket/RPC). It - * resolves provider adapters through `ProviderAdapterRegistry`, routes - * session-scoped calls via `ProviderSessionDirectory`, and exposes one unified - * provider event stream to callers. - * - * Uses Effect `Context.Service` for dependency injection and returns typed - * domain errors for validation, session, codex, and checkpoint workflows. - * - * @module ProviderService - */ -import type { - ProviderInterruptTurnInput, - ProviderInstanceId, - ProviderRespondToRequestInput, - ProviderRespondToUserInputInput, - ProviderRuntimeEvent, - ProviderSendTurnInput, - ProviderSession, - ProviderSessionStartInput, - ProviderStopSessionInput, - ThreadId, - ProviderTurnStartResult, -} from "@t3tools/contracts"; -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; -import type * as Stream from "effect/Stream"; - -import type { ProviderServiceError } from "../Errors.ts"; -import type { ProviderAdapterCapabilities } from "./ProviderAdapter.ts"; -import type { ProviderInstanceRoutingInfo } from "./ProviderAdapterRegistry.ts"; - -/** - * ProviderServiceShape - Service API for provider session and turn orchestration. - */ -export interface ProviderServiceShape { - /** - * Start a provider session. - */ - readonly startSession: ( - threadId: ThreadId, - input: ProviderSessionStartInput, - ) => Effect.Effect; - - /** - * Send a provider turn. - */ - readonly sendTurn: ( - input: ProviderSendTurnInput, - ) => Effect.Effect; - - /** - * Interrupt a running provider turn. - */ - readonly interruptTurn: ( - input: ProviderInterruptTurnInput, - ) => Effect.Effect; - - /** - * Respond to a provider approval request. - */ - readonly respondToRequest: ( - input: ProviderRespondToRequestInput, - ) => Effect.Effect; - - /** - * Respond to a provider structured user-input request. - */ - readonly respondToUserInput: ( - input: ProviderRespondToUserInputInput, - ) => Effect.Effect; - - /** - * Stop a provider session. - */ - readonly stopSession: ( - input: ProviderStopSessionInput, - ) => Effect.Effect; - - /** - * List active provider sessions. - * - * Aggregates runtime session lists from all registered adapters. - */ - readonly listSessions: () => Effect.Effect>; - - /** - * Read capabilities for the adapter bound to a configured provider instance. - */ - readonly getCapabilities: ( - instanceId: ProviderInstanceId, - ) => Effect.Effect; - - readonly getInstanceInfo: ( - instanceId: ProviderInstanceId, - ) => Effect.Effect; - - /** - * Roll back provider conversation state by a number of turns. - */ - readonly rollbackConversation: (input: { - readonly threadId: ThreadId; - readonly numTurns: number; - }) => Effect.Effect; - - /** - * Canonical provider runtime event stream. - * - * Fan-out is owned by ProviderService (not by a standalone event-bus service). - */ - readonly streamEvents: Stream.Stream; -} - -/** - * ProviderService - Service tag for provider orchestration. - */ -export class ProviderService extends Context.Service()( - "t3/provider/Services/ProviderService", -) {} diff --git a/apps/server/src/provider/Services/ProviderSessionDirectory.ts b/apps/server/src/provider/Services/ProviderSessionDirectory.ts deleted file mode 100644 index f2dd4323f7a..00000000000 --- a/apps/server/src/provider/Services/ProviderSessionDirectory.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { - ProviderInstanceId, - ProviderDriverKind, - ProviderSessionRuntimeStatus, - RuntimeMode, - ThreadId, -} from "@t3tools/contracts"; -import * as Option from "effect/Option"; -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; - -import type { - ProviderSessionDirectoryPersistenceError, - ProviderValidationError, -} from "../Errors.ts"; - -export interface ProviderRuntimeBinding { - readonly threadId: ThreadId; - readonly provider: ProviderDriverKind; - /** - * Routing key for the configured provider instance that owns this - * session. The persistence layer promotes legacy null rows before - * exposing bindings; runtime callers must not infer this from `provider`. - */ - readonly providerInstanceId?: ProviderInstanceId; - readonly adapterKey?: string; - readonly status?: ProviderSessionRuntimeStatus; - readonly resumeCursor?: unknown | null; - readonly runtimePayload?: unknown | null; - readonly runtimeMode?: RuntimeMode; -} - -export interface ProviderRuntimeBindingWithMetadata extends ProviderRuntimeBinding { - readonly lastSeenAt: string; -} - -export type ProviderSessionDirectoryReadError = ProviderSessionDirectoryPersistenceError; - -export type ProviderSessionDirectoryWriteError = - | ProviderValidationError - | ProviderSessionDirectoryPersistenceError; - -export interface ProviderSessionDirectoryShape { - readonly upsert: ( - binding: ProviderRuntimeBinding, - ) => Effect.Effect; - - readonly getProvider: ( - threadId: ThreadId, - ) => Effect.Effect; - - readonly getBinding: ( - threadId: ThreadId, - ) => Effect.Effect, ProviderSessionDirectoryReadError>; - - readonly listThreadIds: () => Effect.Effect< - ReadonlyArray, - ProviderSessionDirectoryPersistenceError - >; - - readonly listBindings: () => Effect.Effect< - ReadonlyArray, - ProviderSessionDirectoryPersistenceError - >; -} - -export class ProviderSessionDirectory extends Context.Service< - ProviderSessionDirectory, - ProviderSessionDirectoryShape ->()("t3/provider/Services/ProviderSessionDirectory") {} diff --git a/apps/server/src/provider/Services/ProviderSessionReaper.ts b/apps/server/src/provider/Services/ProviderSessionReaper.ts deleted file mode 100644 index 7c4627eca89..00000000000 --- a/apps/server/src/provider/Services/ProviderSessionReaper.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as Context from "effect/Context"; -import type * as Effect from "effect/Effect"; -import type * as Scope from "effect/Scope"; - -export interface ProviderSessionReaperShape { - /** - * Start the background provider session reaper within the provided scope. - */ - readonly start: () => Effect.Effect; -} - -export class ProviderSessionReaper extends Context.Service< - ProviderSessionReaper, - ProviderSessionReaperShape ->()("t3/provider/Services/ProviderSessionReaper") {} diff --git a/apps/server/src/provider/acp/AcpAdapterSupport.test.ts b/apps/server/src/provider/acp/AcpAdapterSupport.test.ts deleted file mode 100644 index 0aebe0ca6d8..00000000000 --- a/apps/server/src/provider/acp/AcpAdapterSupport.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from "vite-plus/test"; -import * as EffectAcpErrors from "effect-acp/errors"; -import { ProviderDriverKind } from "@t3tools/contracts"; - -import { acpPermissionOutcome, mapAcpToAdapterError } from "./AcpAdapterSupport.ts"; - -describe("AcpAdapterSupport", () => { - it("maps ACP approval decisions to permission outcomes", () => { - expect(acpPermissionOutcome("accept")).toBe("allow-once"); - expect(acpPermissionOutcome("acceptForSession")).toBe("allow-always"); - expect(acpPermissionOutcome("decline")).toBe("reject-once"); - }); - - it("maps ACP request errors to provider adapter request errors", () => { - const error = mapAcpToAdapterError( - ProviderDriverKind.make("cursor"), - "thread-1" as never, - "session/prompt", - new EffectAcpErrors.AcpRequestError({ - code: -32602, - errorMessage: "Invalid params", - }), - ); - - expect(error._tag).toBe("ProviderAdapterRequestError"); - expect(error.message).toContain("Invalid params"); - }); -}); diff --git a/apps/server/src/provider/acp/AcpAdapterSupport.ts b/apps/server/src/provider/acp/AcpAdapterSupport.ts deleted file mode 100644 index cde110e6dd9..00000000000 --- a/apps/server/src/provider/acp/AcpAdapterSupport.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - type ProviderApprovalDecision, - type ProviderDriverKind, - type ThreadId, -} from "@t3tools/contracts"; -import * as Schema from "effect/Schema"; -import * as EffectAcpErrors from "effect-acp/errors"; - -import { - ProviderAdapterRequestError, - ProviderAdapterSessionClosedError, - type ProviderAdapterError, -} from "../Errors.ts"; -const isAcpProcessExitedError = Schema.is(EffectAcpErrors.AcpProcessExitedError); -const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError); - -export function mapAcpToAdapterError( - provider: ProviderDriverKind, - threadId: ThreadId, - method: string, - error: EffectAcpErrors.AcpError, -): ProviderAdapterError { - if (isAcpProcessExitedError(error)) { - return new ProviderAdapterSessionClosedError({ - provider, - threadId, - cause: error, - }); - } - if (isAcpRequestError(error)) { - return new ProviderAdapterRequestError({ - provider, - method, - detail: error.message, - cause: error, - }); - } - return new ProviderAdapterRequestError({ - provider, - method, - detail: error.message, - cause: error, - }); -} - -export function acpPermissionOutcome(decision: ProviderApprovalDecision): string { - switch (decision) { - case "acceptForSession": - return "allow-always"; - case "accept": - return "allow-once"; - case "decline": - default: - return "reject-once"; - } -} diff --git a/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts b/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts index 5533a04bc83..674ec7a4797 100644 --- a/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts +++ b/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts @@ -19,6 +19,17 @@ const mockAgentCommand = "node"; const mockAgentArgs = [mockAgentPath]; describe("AcpSessionRuntime", () => { + it("selects explicit or agent-managed authentication without choosing terminal auth", () => { + const methods = [ + { id: "browser", name: "Browser", type: "terminal" as const }, + { id: "api-key", name: "API key" }, + ]; + + expect(AcpSessionRuntime.selectAcpAgentAuthMethod(methods)?.id).toBe("api-key"); + expect(AcpSessionRuntime.selectAcpAgentAuthMethod(methods, "browser")?.id).toBe("browser"); + expect(AcpSessionRuntime.selectAcpAgentAuthMethod(methods, "missing")).toBeUndefined(); + }); + it.effect("merges custom initialize client capabilities into the ACP handshake", () => { const requestEvents: Array = []; return Effect.gen(function* () { @@ -62,6 +73,82 @@ describe("AcpSessionRuntime", () => { ); }); + it.effect("does not authenticate when an advertised method is not required", () => { + const requestEvents: Array = []; + return Effect.gen(function* () { + const runtime = yield* AcpSessionRuntime.AcpSessionRuntime; + yield* runtime.start(); + + expect( + requestEvents.filter((event) => event.status === "started").map((event) => event.method), + ).toEqual(["initialize", "session/new"]); + }).pipe( + Effect.provide( + AcpSessionRuntime.layer({ + authMethodId: "test", + spawn: { + command: mockAgentCommand, + args: mockAgentArgs, + env: { T3_ACP_AUTH_METHOD_ID: "test" }, + }, + cwd: process.cwd(), + clientInfo: { name: "t3-test", version: "0.0.0" }, + requestLogger: (event) => + Effect.sync(() => { + requestEvents.push(event); + }), + }), + ), + Effect.scoped, + Effect.provide(NodeServices.layer), + ); + }); + + it.effect("authenticates and retries once after session creation returns auth_required", () => { + const requestEvents: Array = []; + return Effect.gen(function* () { + const runtime = yield* AcpSessionRuntime.AcpSessionRuntime; + const started = yield* runtime.start(); + + expect(started.sessionId).toBe("mock-session-1"); + expect( + requestEvents.filter((event) => event.status === "started").map((event) => event.method), + ).toEqual(["initialize", "session/new", "authenticate", "session/new"]); + expect( + requestEvents.filter( + (event) => event.method === "session/new" && event.status === "failed", + ), + ).toHaveLength(1); + expect( + requestEvents.filter( + (event) => event.method === "session/new" && event.status === "succeeded", + ), + ).toHaveLength(1); + }).pipe( + Effect.provide( + AcpSessionRuntime.layer({ + authMethodId: "test", + spawn: { + command: mockAgentCommand, + args: mockAgentArgs, + env: { + T3_ACP_AUTH_METHOD_ID: "test", + T3_ACP_REQUIRE_AUTH: "1", + }, + }, + cwd: process.cwd(), + clientInfo: { name: "t3-test", version: "0.0.0" }, + requestLogger: (event) => + Effect.sync(() => { + requestEvents.push(event); + }), + }), + ), + Effect.scoped, + Effect.provide(NodeServices.layer), + ); + }); + it.effect("starts a session, prompts, and emits normalized events against the mock agent", () => Effect.gen(function* () { const runtime = yield* AcpSessionRuntime.AcpSessionRuntime; @@ -264,6 +351,63 @@ describe("AcpSessionRuntime", () => { ); }); + it.effect("supports negotiated ACP session list, fork, resume, and close methods", () => { + const requestEvents: Array = []; + return Effect.gen(function* () { + const runtime = yield* AcpSessionRuntime.AcpSessionRuntime; + const started = yield* runtime.start(); + expect(started.initializeResult.agentCapabilities?.sessionCapabilities).toMatchObject({ + list: {}, + fork: {}, + resume: {}, + close: {}, + }); + + const listed = yield* runtime.listSessions(); + expect(listed.sessions.map((session) => session.sessionId)).toEqual(["mock-session-1"]); + + const forked = yield* runtime.forkSession(started.sessionId); + expect(forked.sessionId).toBe("mock-session-1-fork"); + yield* runtime.prompt({ prompt: [{ type: "text", text: "on fork" }] }); + + const resumed = yield* runtime.resumeSession(started.sessionId); + expect(resumed.sessionId).toBe("mock-session-1"); + yield* runtime.closeSession(); + + expect( + requestEvents.find( + (event) => event.method === "session/prompt" && event.status === "started", + )?.payload, + ).toMatchObject({ sessionId: "mock-session-1-fork" }); + expect( + requestEvents.filter((event) => event.status === "started").map((event) => event.method), + ).toEqual( + expect.arrayContaining(["session/list", "session/fork", "session/resume", "session/close"]), + ); + }).pipe( + Effect.provide( + AcpSessionRuntime.layer({ + authMethodId: "test", + spawn: { + command: mockAgentCommand, + args: mockAgentArgs, + env: { + T3_ACP_SESSION_LIFECYCLE: "1", + }, + }, + cwd: process.cwd(), + clientInfo: { name: "t3-test", version: "0.0.0" }, + requestLogger: (event) => + Effect.sync(() => { + requestEvents.push(event); + }), + }), + ), + Effect.scoped, + Effect.provide(NodeServices.layer), + ); + }); + it.effect("skips no-op session config writes when the requested value is already active", () => { const requestEvents: Array = []; return Effect.gen(function* () { diff --git a/apps/server/src/provider/acp/AcpRegistrySupport.test.ts b/apps/server/src/provider/acp/AcpRegistrySupport.test.ts new file mode 100644 index 00000000000..b4363dda715 --- /dev/null +++ b/apps/server/src/provider/acp/AcpRegistrySupport.test.ts @@ -0,0 +1,263 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { describe, expect, it } from "@effect/vitest"; +import { AcpRegistrySettings } from "@t3tools/contracts"; +import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import { HttpClient, HttpClientResponse } from "effect/unstable/http"; + +import { + makeAcpRegistryResolver, + resolveAcpRegistryDistribution, + resolveAcpRegistryPlatformTarget, + type AcpRegistryAgent, +} from "./AcpRegistrySupport.ts"; + +const registryUrl = "https://registry.test/registry.json"; +const archiveUrl = "https://registry.test/example-agent.bin"; +const decodeAcpRegistrySettings = Schema.decodeSync(AcpRegistrySettings); + +function makeAgent(distribution: AcpRegistryAgent["distribution"]): AcpRegistryAgent { + return { + id: "example-agent", + name: "Example Agent", + version: "1.2.3", + description: "ACP Registry test agent", + distribution, + }; +} + +function makeRegistry(agent: AcpRegistryAgent): string { + return JSON.stringify({ version: "1.0.0", agents: [agent] }); +} + +function settings(input: Partial = {}): AcpRegistrySettings { + return decodeAcpRegistrySettings({ + agentId: "example-agent", + ...input, + }); +} + +function resolverLayer(execute: Parameters[0]) { + return Layer.mergeAll( + NodeServices.layer, + Layer.succeed(HostProcessPlatform, "linux"), + Layer.succeed(HostProcessArchitecture, "x64"), + Layer.succeed(HttpClient.HttpClient, HttpClient.make(execute)), + ); +} + +describe("AcpRegistrySupport", () => { + it("maps supported Node platforms to ACP Registry target keys", () => { + expect(resolveAcpRegistryPlatformTarget("darwin", "arm64")).toBe("darwin-aarch64"); + expect(resolveAcpRegistryPlatformTarget("linux", "x64")).toBe("linux-x86_64"); + expect(resolveAcpRegistryPlatformTarget("win32", "arm64")).toBe("windows-aarch64"); + expect(resolveAcpRegistryPlatformTarget("freebsd", "x64")).toBeUndefined(); + expect(resolveAcpRegistryPlatformTarget("linux", "ia32")).toBeUndefined(); + }); + + it("selects the preferred compatible distribution", () => { + const agent = makeAgent({ + binary: { + "linux-x86_64": { + archive: archiveUrl, + cmd: "./bin/example-agent", + args: ["acp"], + }, + }, + npx: { + package: "@example/acp@1.2.3", + args: ["--stdio"], + }, + }); + + expect( + resolveAcpRegistryDistribution({ + agent, + preference: "auto", + platformTarget: "linux-x86_64", + }), + ).toMatchObject({ kind: "binary", args: ["acp"] }); + expect( + resolveAcpRegistryDistribution({ + agent, + preference: "npx", + platformTarget: "linux-x86_64", + }), + ).toEqual({ + kind: "npx", + packageName: "@example/acp@1.2.3", + args: ["--stdio"], + env: {}, + }); + expect( + resolveAcpRegistryDistribution({ + agent, + preference: "binary", + platformTarget: "darwin-aarch64", + }), + ).toBeUndefined(); + }); + + it.effect("resolves command overrides while preserving registry args and environment", () => { + const agent = makeAgent({ + binary: { + "linux-x86_64": { + archive: archiveUrl, + cmd: "./bin/example-agent", + args: ["acp", "--stdio"], + env: { REGISTRY_VALUE: "registry", OVERRIDE_ME: "registry" }, + }, + }, + }); + const requests: Array = []; + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-registry-override-", + }); + const resolver = yield* makeAcpRegistryResolver({ cacheDir, registryUrl }); + const resolved = yield* resolver.resolve( + settings({ commandPath: "/opt/example-agent" }), + "/workspace", + { HOST_VALUE: "host", OVERRIDE_ME: "host" }, + ); + + expect(resolved.distribution).toBe("binary"); + expect(resolved.spawn).toEqual({ + command: "/opt/example-agent", + args: ["acp", "--stdio"], + cwd: "/workspace", + env: { + HOST_VALUE: "host", + OVERRIDE_ME: "registry", + REGISTRY_VALUE: "registry", + }, + }); + expect(requests).toEqual([registryUrl]); + }).pipe( + Effect.scoped, + Effect.provide( + resolverLayer((request) => { + requests.push(request.url); + return Effect.succeed( + HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))), + ); + }), + ), + ); + }); + + it.effect("installs and reuses a registry binary in the managed cache", () => { + const agent = makeAgent({ + binary: { + "linux-x86_64": { + archive: archiveUrl, + cmd: "./bin/example-agent", + args: ["acp"], + }, + }, + }); + const binaryBytes = new TextEncoder().encode("#!/bin/sh\necho example\n"); + const requests: Array = []; + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-registry-install-", + }); + const resolver = yield* makeAcpRegistryResolver({ cacheDir, registryUrl }); + const first = yield* resolver.resolve(settings(), "/workspace"); + const second = yield* resolver.resolve(settings(), "/workspace"); + + expect(first.spawn.command).toBe(second.spawn.command); + expect(first.spawn.command).toContain( + "/acp-registry/agents/example-agent/1.2.3/linux-x86_64/bin/example-agent", + ); + expect(yield* fileSystem.readFileString(first.spawn.command)).toBe( + "#!/bin/sh\necho example\n", + ); + expect(requests).toEqual([registryUrl, archiveUrl]); + }).pipe( + Effect.scoped, + Effect.provide( + resolverLayer((request) => { + requests.push(request.url); + const response = + request.url === registryUrl + ? new Response(makeRegistry(agent)) + : new Response(binaryBytes.buffer as ArrayBuffer); + return Effect.succeed(HttpClientResponse.fromWeb(request, response)); + }), + ), + ); + }); + + it.effect("falls back to a valid cached registry index when refresh fails", () => { + const agent = makeAgent({ + npx: { + package: "@example/acp@1.2.3", + args: ["--stdio"], + }, + }); + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-registry-cache-", + }); + const registryDirectory = `${cacheDir}/acp-registry`; + yield* fileSystem.makeDirectory(registryDirectory, { recursive: true }); + yield* fileSystem.writeFileString(`${registryDirectory}/registry.json`, makeRegistry(agent)); + const resolver = yield* makeAcpRegistryResolver({ cacheDir, registryUrl }); + const resolved = yield* resolver.resolve(settings(), "/workspace"); + + expect(resolved.spawn).toMatchObject({ + command: "npx", + args: ["--yes", "@example/acp@1.2.3", "--stdio"], + }); + }).pipe( + Effect.scoped, + Effect.provide( + resolverLayer((request) => + Effect.succeed( + HttpClientResponse.fromWeb(request, new Response("unavailable", { status: 503 })), + ), + ), + ), + ); + }); + + it.effect("rejects unsafe command paths before downloading an archive", () => { + const agent = makeAgent({ + binary: { + "linux-x86_64": { + archive: archiveUrl, + cmd: "../outside", + }, + }, + }); + const requests: Array = []; + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-registry-invalid-", + }); + const resolver = yield* makeAcpRegistryResolver({ cacheDir, registryUrl }); + const error = yield* resolver.resolve(settings(), "/workspace").pipe(Effect.flip); + + expect(error.reason).toBe("archive_invalid"); + expect(requests).toEqual([registryUrl]); + }).pipe( + Effect.scoped, + Effect.provide( + resolverLayer((request) => { + requests.push(request.url); + return Effect.succeed( + HttpClientResponse.fromWeb(request, new Response(makeRegistry(agent))), + ); + }), + ), + ); + }); +}); diff --git a/apps/server/src/provider/acp/AcpRegistrySupport.ts b/apps/server/src/provider/acp/AcpRegistrySupport.ts new file mode 100644 index 00000000000..a0107725c00 --- /dev/null +++ b/apps/server/src/provider/acp/AcpRegistrySupport.ts @@ -0,0 +1,633 @@ +import { + type AcpRegistryDistributionPreference, + type AcpRegistrySettings, +} from "@t3tools/contracts"; +import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import * as Clock from "effect/Clock"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Semaphore from "effect/Semaphore"; +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; + +import { collectUint8StreamText } from "../../stream/collectUint8StreamText.ts"; +import type { AcpSpawnInput } from "./AcpSessionRuntime.ts"; + +export const ACP_REGISTRY_URL = + "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json"; + +const AcpRegistryPackageDistribution = Schema.Struct({ + package: Schema.String, + args: Schema.optionalKey(Schema.Array(Schema.String)), + env: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)), +}); + +const AcpRegistryBinaryTarget = Schema.Struct({ + archive: Schema.String, + cmd: Schema.String, + args: Schema.optionalKey(Schema.Array(Schema.String)), + env: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)), +}); + +const AcpRegistryAgent = Schema.Struct({ + id: Schema.String, + name: Schema.String, + version: Schema.String, + description: Schema.String, + distribution: Schema.Struct({ + binary: Schema.optionalKey(Schema.Record(Schema.String, AcpRegistryBinaryTarget)), + npx: Schema.optionalKey(AcpRegistryPackageDistribution), + uvx: Schema.optionalKey(AcpRegistryPackageDistribution), + }), +}); +export type AcpRegistryAgent = typeof AcpRegistryAgent.Type; + +const AcpRegistryIndex = Schema.Struct({ + version: Schema.String, + agents: Schema.Array(AcpRegistryAgent), +}); +export type AcpRegistryIndex = typeof AcpRegistryIndex.Type; + +const decodeRegistryIndex = Schema.decodeUnknownEffect(AcpRegistryIndex); +const decodeJson = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString); + +export const AcpRegistryErrorReason = Schema.Literals([ + "agent_not_configured", + "agent_not_found", + "archive_invalid", + "download_failed", + "install_failed", + "registry_unavailable", + "unsupported_distribution", + "unsupported_platform", +]); +export type AcpRegistryErrorReason = typeof AcpRegistryErrorReason.Type; + +export class AcpRegistryError extends Schema.TaggedErrorClass()( + "AcpRegistryError", + { + reason: AcpRegistryErrorReason, + detail: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return this.detail; + } +} + +const isAcpRegistryError = Schema.is(AcpRegistryError); + +export type AcpRegistryPlatformTarget = + | "darwin-aarch64" + | "darwin-x86_64" + | "linux-aarch64" + | "linux-x86_64" + | "windows-aarch64" + | "windows-x86_64"; + +export function resolveAcpRegistryPlatformTarget( + platform: NodeJS.Platform, + architecture: NodeJS.Architecture, +): AcpRegistryPlatformTarget | undefined { + const os = + platform === "darwin" + ? "darwin" + : platform === "linux" + ? "linux" + : platform === "win32" + ? "windows" + : undefined; + const arch = architecture === "arm64" ? "aarch64" : architecture === "x64" ? "x86_64" : undefined; + return os && arch ? (`${os}-${arch}` as AcpRegistryPlatformTarget) : undefined; +} + +export type AcpRegistryDistributionKind = "binary" | "npx" | "uvx"; + +export interface ResolvedAcpRegistryDistribution { + readonly kind: AcpRegistryDistributionKind; + readonly args: ReadonlyArray; + readonly env: Readonly>; + readonly binaryTarget?: typeof AcpRegistryBinaryTarget.Type; + readonly packageName?: string; +} + +export function resolveAcpRegistryDistribution(input: { + readonly agent: AcpRegistryAgent; + readonly preference: AcpRegistryDistributionPreference; + readonly platformTarget: AcpRegistryPlatformTarget | undefined; +}): ResolvedAcpRegistryDistribution | undefined { + const { agent, platformTarget } = input; + const binary = platformTarget ? agent.distribution.binary?.[platformTarget] : undefined; + const candidates: ReadonlyArray = + input.preference === "auto" ? ["binary", "npx", "uvx"] : [input.preference]; + + for (const kind of candidates) { + if (kind === "binary" && binary) { + return { + kind, + args: binary.args ?? [], + env: binary.env ?? {}, + binaryTarget: binary, + }; + } + if (kind === "npx" && agent.distribution.npx) { + return { + kind, + args: agent.distribution.npx.args ?? [], + env: agent.distribution.npx.env ?? {}, + packageName: agent.distribution.npx.package, + }; + } + if (kind === "uvx" && agent.distribution.uvx) { + return { + kind, + args: agent.distribution.uvx.args ?? [], + env: agent.distribution.uvx.env ?? {}, + packageName: agent.distribution.uvx.package, + }; + } + } + return undefined; +} + +export interface ResolvedAcpRegistryAgent { + readonly agent: AcpRegistryAgent; + readonly distribution: AcpRegistryDistributionKind; + readonly spawn: AcpSpawnInput; +} + +export interface AcpRegistryResolverShape { + readonly resolve: ( + settings: AcpRegistrySettings, + cwd: string, + environment?: NodeJS.ProcessEnv, + ) => Effect.Effect; +} + +const INSTALL_LOCK_RETRY_COUNT = 300; +const INSTALL_LOCK_RETRY_DELAY = "100 millis"; +const INSTALL_LOCK_STALE_MS = 5 * 60 * 1_000; +const MAX_ARCHIVE_BYTES = 512 * 1024 * 1024; + +function isAlreadyExists(error: PlatformError.PlatformError): boolean { + return error.reason._tag === "AlreadyExists"; +} + +function normalizeRegistryCommandPath(command: string): ReadonlyArray | undefined { + const normalized = command.trim().replaceAll("\\", "/").replace(/^\.\//u, ""); + const segments = normalized.split("/").filter((segment) => segment.length > 0); + if ( + segments.length === 0 || + normalized.startsWith("/") || + /^[a-zA-Z]:/u.test(normalized) || + segments.some((segment) => segment === "." || segment === "..") + ) { + return undefined; + } + return segments; +} + +function validateArchiveEntries(output: string): boolean { + return output + .split(/\r?\n/u) + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) + .every((entry) => normalizeRegistryCommandPath(entry) !== undefined); +} + +type ArchiveKind = "raw" | "tar_bz2" | "tar_gz" | "zip"; + +function archiveKind(url: string): ArchiveKind { + const pathname = new URL(url).pathname.toLowerCase(); + if (pathname.endsWith(".tar.gz") || pathname.endsWith(".tgz")) return "tar_gz"; + if (pathname.endsWith(".tar.bz2") || pathname.endsWith(".tbz2")) return "tar_bz2"; + if (pathname.endsWith(".zip")) return "zip"; + return "raw"; +} + +function archiveFileName(kind: ArchiveKind): string { + switch (kind) { + case "tar_gz": + return "agent.tar.gz"; + case "tar_bz2": + return "agent.tar.bz2"; + case "zip": + return "agent.zip"; + case "raw": + return "agent.bin"; + } +} + +export const makeAcpRegistryResolver = Effect.fn("AcpRegistryResolver.make")(function* (input: { + readonly cacheDir: string; + readonly registryUrl?: string; +}): Effect.fn.Return< + AcpRegistryResolverShape, + never, + | ChildProcessSpawner.ChildProcessSpawner + | FileSystem.FileSystem + | HttpClient.HttpClient + | Path.Path +> { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const httpClient = yield* HttpClient.HttpClient; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const platform = yield* HostProcessPlatform; + const architecture = yield* HostProcessArchitecture; + const platformTarget = resolveAcpRegistryPlatformTarget(platform, architecture); + const registryUrl = input.registryUrl ?? ACP_REGISTRY_URL; + const registryDirectory = path.join(input.cacheDir, "acp-registry"); + const registryCachePath = path.join(registryDirectory, "registry.json"); + const installsDirectory = path.join(registryDirectory, "agents"); + const registryRef = yield* Ref.make(undefined); + const registrySemaphore = yield* Semaphore.make(1); + const installSemaphore = yield* Semaphore.make(1); + + const decodeRegistryText = Effect.fn("AcpRegistryResolver.decodeRegistryText")(function* ( + text: string, + ) { + const decoded = yield* decodeJson(text).pipe( + Effect.mapError( + (cause) => + new AcpRegistryError({ + reason: "registry_unavailable", + detail: "ACP Registry returned invalid JSON.", + cause, + }), + ), + ); + return yield* decodeRegistryIndex(decoded).pipe( + Effect.mapError( + (cause) => + new AcpRegistryError({ + reason: "registry_unavailable", + detail: "ACP Registry returned an invalid index.", + cause, + }), + ), + ); + }); + + const readCachedRegistry = fileSystem + .readFileString(registryCachePath) + .pipe(Effect.flatMap(decodeRegistryText), Effect.option); + + const writeRegistryCache = (text: string) => + fileSystem + .makeDirectory(registryDirectory, { recursive: true }) + .pipe( + Effect.andThen(fileSystem.writeFileString(`${registryCachePath}.${process.pid}.tmp`, text)), + Effect.andThen( + fileSystem.rename(`${registryCachePath}.${process.pid}.tmp`, registryCachePath), + ), + Effect.ignore, + ); + + const fetchRegistry = Effect.gen(function* () { + const response = yield* httpClient.execute(HttpClientRequest.get(registryUrl)).pipe( + Effect.flatMap(HttpClientResponse.filterStatusOk), + Effect.mapError( + (cause) => + new AcpRegistryError({ + reason: "registry_unavailable", + detail: `Could not fetch ACP Registry index from ${registryUrl}.`, + cause, + }), + ), + ); + const text = yield* response.text.pipe( + Effect.mapError( + (cause) => + new AcpRegistryError({ + reason: "registry_unavailable", + detail: "Could not read ACP Registry response body.", + cause, + }), + ), + ); + const registry = yield* decodeRegistryText(text); + yield* writeRegistryCache(text); + return registry; + }); + + const loadRegistry = registrySemaphore.withPermits(1)( + Effect.gen(function* () { + const memoized = yield* Ref.get(registryRef); + if (memoized !== undefined) return memoized; + const registry = yield* fetchRegistry.pipe( + Effect.catch((networkError) => + readCachedRegistry.pipe( + Effect.flatMap( + Option.match({ + onNone: () => Effect.fail(networkError), + onSome: Effect.succeed, + }), + ), + ), + ), + ); + yield* Ref.set(registryRef, registry); + return registry; + }), + ); + + const runCommand = Effect.fn("AcpRegistryResolver.runCommand")(function* ( + command: string, + args: ReadonlyArray, + cwd?: string, + ) { + const child = yield* spawner.spawn( + ChildProcess.make(command, args, { + ...(cwd ? { cwd } : {}), + shell: false, + }), + ); + const [stdout, stderr, exitCode] = yield* Effect.all( + [ + collectUint8StreamText({ stream: child.stdout }), + collectUint8StreamText({ stream: child.stderr }), + child.exitCode, + ], + { concurrency: "unbounded" }, + ); + if (Number(exitCode) !== 0) { + return yield* new AcpRegistryError({ + reason: "install_failed", + detail: `ACP Registry install command '${command}' exited with code ${Number(exitCode)}: ${stderr.text.trim()}`, + }); + } + return stdout.text; + }); + + const acquireInstallLock = Effect.fn("AcpRegistryResolver.acquireInstallLock")(function* ( + lockPath: string, + ) { + for (let attempt = 0; attempt < INSTALL_LOCK_RETRY_COUNT; attempt += 1) { + const acquired = yield* fileSystem.writeFileString(lockPath, "", { flag: "wx" }).pipe( + Effect.as(true), + Effect.catch((error) => + isAlreadyExists(error) ? Effect.succeed(false) : Effect.fail(error), + ), + ); + if (acquired) return; + const now = yield* Clock.currentTimeMillis; + const lockInfo = yield* fileSystem.stat(lockPath).pipe(Effect.option); + const mtime = Option.flatMap(lockInfo, (info) => info.mtime); + if (Option.isSome(mtime) && now - mtime.value.getTime() > INSTALL_LOCK_STALE_MS) { + yield* fileSystem.remove(lockPath, { force: true }); + continue; + } + yield* Effect.sleep(INSTALL_LOCK_RETRY_DELAY); + } + return yield* new AcpRegistryError({ + reason: "install_failed", + detail: `Timed out waiting for ACP Registry install lock ${lockPath}.`, + }); + }); + + const assertExecutableInRoot = Effect.fn("AcpRegistryResolver.assertExecutableInRoot")(function* ( + root: string, + executablePath: string, + ) { + const rootRealPath = yield* fileSystem.realPath(root); + const executableRealPath = yield* fileSystem.realPath(executablePath); + const relative = path.relative(rootRealPath, executableRealPath); + if (relative.startsWith("..") || path.isAbsolute(relative)) { + return yield* new AcpRegistryError({ + reason: "archive_invalid", + detail: "ACP Registry archive command resolves outside its installation directory.", + }); + } + return executableRealPath; + }); + + const installBinary = Effect.fn("AcpRegistryResolver.installBinary")(function* ( + agent: AcpRegistryAgent, + target: typeof AcpRegistryBinaryTarget.Type, + ) { + if (platformTarget === undefined) { + return yield* new AcpRegistryError({ + reason: "unsupported_platform", + detail: `ACP Registry does not support platform ${platform}-${architecture}.`, + }); + } + const commandSegments = normalizeRegistryCommandPath(target.cmd); + if (commandSegments === undefined) { + return yield* new AcpRegistryError({ + reason: "archive_invalid", + detail: `ACP Registry agent ${agent.id} declares an unsafe command path '${target.cmd}'.`, + }); + } + const installRoot = path.join(installsDirectory, agent.id, agent.version, platformTarget); + const executablePath = path.join(installRoot, ...commandSegments); + if (yield* fileSystem.exists(executablePath).pipe(Effect.orElseSucceed(() => false))) { + return yield* assertExecutableInRoot(installRoot, executablePath).pipe( + Effect.mapError((cause) => + isAcpRegistryError(cause) + ? cause + : new AcpRegistryError({ + reason: "install_failed", + detail: `Could not validate cached ACP Registry agent ${agent.id}.`, + cause, + }), + ), + ); + } + + yield* fileSystem.makeDirectory(path.dirname(installRoot), { recursive: true }).pipe( + Effect.mapError( + (cause) => + new AcpRegistryError({ + reason: "install_failed", + detail: `Could not create ACP Registry cache for ${agent.id}.`, + cause, + }), + ), + ); + const lockPath = `${installRoot}.lock`; + yield* acquireInstallLock(lockPath).pipe( + Effect.mapError((cause) => + isAcpRegistryError(cause) + ? cause + : new AcpRegistryError({ + reason: "install_failed", + detail: `Could not acquire install lock for ACP Registry agent ${agent.id}.`, + cause, + }), + ), + ); + + return yield* Effect.gen(function* () { + if (yield* fileSystem.exists(executablePath).pipe(Effect.orElseSucceed(() => false))) { + return yield* assertExecutableInRoot(installRoot, executablePath); + } + const temporaryRoot = yield* fileSystem.makeTempDirectoryScoped({ + directory: path.dirname(installRoot), + prefix: `.${agent.id}-install-`, + }); + const extractionRoot = path.join(temporaryRoot, "extracted"); + yield* fileSystem.makeDirectory(extractionRoot, { recursive: true }); + const kind = archiveKind(target.archive); + const archivePath = path.join(temporaryRoot, archiveFileName(kind)); + const response = yield* httpClient.execute(HttpClientRequest.get(target.archive)).pipe( + Effect.flatMap(HttpClientResponse.filterStatusOk), + Effect.mapError( + (cause) => + new AcpRegistryError({ + reason: "download_failed", + detail: `Could not download ACP Registry agent ${agent.id} ${agent.version}.`, + cause, + }), + ), + ); + const bytes = new Uint8Array( + yield* response.arrayBuffer.pipe( + Effect.mapError( + (cause) => + new AcpRegistryError({ + reason: "download_failed", + detail: `Could not read ACP Registry agent ${agent.id} download.`, + cause, + }), + ), + ), + ); + if (bytes.byteLength > MAX_ARCHIVE_BYTES) { + return yield* new AcpRegistryError({ + reason: "archive_invalid", + detail: `ACP Registry agent ${agent.id} archive exceeds ${MAX_ARCHIVE_BYTES} bytes.`, + }); + } + yield* fileSystem.writeFile(archivePath, bytes); + + if (kind === "raw") { + const targetPath = path.join(extractionRoot, ...commandSegments); + yield* fileSystem.makeDirectory(path.dirname(targetPath), { recursive: true }); + yield* fileSystem.rename(archivePath, targetPath); + } else { + const listArgs = + kind === "tar_gz" + ? ["-tzf", archivePath] + : kind === "tar_bz2" + ? ["-tjf", archivePath] + : platform === "win32" + ? ["-tf", archivePath] + : undefined; + const entries = + listArgs === undefined + ? yield* runCommand("unzip", ["-Z1", archivePath]) + : yield* runCommand("tar", listArgs); + if (!validateArchiveEntries(entries)) { + return yield* new AcpRegistryError({ + reason: "archive_invalid", + detail: `ACP Registry agent ${agent.id} archive contains an unsafe path.`, + }); + } + if (kind === "zip" && platform !== "win32") { + yield* runCommand("unzip", ["-q", archivePath, "-d", extractionRoot]); + } else { + const extractFlag = kind === "tar_gz" ? "-xzf" : kind === "tar_bz2" ? "-xjf" : "-xf"; + yield* runCommand("tar", [extractFlag, archivePath, "-C", extractionRoot]); + } + } + + const stagedExecutable = path.join(extractionRoot, ...commandSegments); + if (!(yield* fileSystem.exists(stagedExecutable).pipe(Effect.orElseSucceed(() => false)))) { + return yield* new AcpRegistryError({ + reason: "archive_invalid", + detail: `ACP Registry archive for ${agent.id} did not contain '${target.cmd}'.`, + }); + } + if (platform !== "win32") yield* fileSystem.chmod(stagedExecutable, 0o755); + yield* assertExecutableInRoot(extractionRoot, stagedExecutable); + yield* fileSystem.remove(installRoot, { recursive: true, force: true }); + yield* fileSystem.rename(extractionRoot, installRoot); + return yield* assertExecutableInRoot(installRoot, executablePath); + }).pipe( + Effect.scoped, + Effect.ensuring(fileSystem.remove(lockPath, { force: true }).pipe(Effect.ignore)), + Effect.mapError((cause) => + isAcpRegistryError(cause) + ? cause + : new AcpRegistryError({ + reason: "install_failed", + detail: `Could not install ACP Registry agent ${agent.id}.`, + cause, + }), + ), + ); + }); + + const resolve: AcpRegistryResolverShape["resolve"] = (settings, cwd, environment) => + Effect.gen(function* () { + const agentId = settings.agentId.trim(); + if (agentId.length === 0) { + return yield* new AcpRegistryError({ + reason: "agent_not_configured", + detail: "ACP Registry provider requires a registry agent ID.", + }); + } + const registry = yield* loadRegistry; + const agent = registry.agents.find((candidate) => candidate.id === agentId); + if (agent === undefined) { + return yield* new AcpRegistryError({ + reason: "agent_not_found", + detail: `ACP Registry does not contain agent '${agentId}'.`, + }); + } + const distribution = resolveAcpRegistryDistribution({ + agent, + preference: settings.distribution, + platformTarget, + }); + if (distribution === undefined) { + return yield* new AcpRegistryError({ + reason: + platformTarget === undefined ? "unsupported_platform" : "unsupported_distribution", + detail: `ACP Registry agent ${agent.id} has no ${settings.distribution === "auto" ? "compatible" : settings.distribution} distribution for ${platform}-${architecture}.`, + }); + } + + let command: string; + let args: ReadonlyArray; + const commandOverride = settings.commandPath.trim(); + if (commandOverride.length > 0) { + command = commandOverride; + args = distribution.args; + } else if (distribution.kind === "npx") { + command = "npx"; + args = ["--yes", distribution.packageName!, ...distribution.args]; + } else if (distribution.kind === "uvx") { + command = "uvx"; + args = [distribution.packageName!, ...distribution.args]; + } else { + command = yield* installSemaphore.withPermits(1)( + installBinary(agent, distribution.binaryTarget!), + ); + args = distribution.args; + } + + return { + agent, + distribution: distribution.kind, + spawn: { + command, + args, + cwd, + env: { + ...environment, + ...distribution.env, + }, + }, + } satisfies ResolvedAcpRegistryAgent; + }); + + return { resolve }; +}); diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.ts b/apps/server/src/provider/acp/AcpRuntimeModel.ts index 3587d703dbd..48a3c8b0ea7 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.ts @@ -72,6 +72,7 @@ export type AcpParsedSessionEvent = }; type AcpSessionSetupResponse = + | EffectAcpSchema.ForkSessionResponse | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse; diff --git a/apps/server/src/provider/acp/AcpSessionRuntime.ts b/apps/server/src/provider/acp/AcpSessionRuntime.ts index 4fc2c443e11..d59dca034ca 100644 --- a/apps/server/src/provider/acp/AcpSessionRuntime.ts +++ b/apps/server/src/provider/acp/AcpSessionRuntime.ts @@ -48,7 +48,7 @@ export interface AcpSessionRuntimeOptions { readonly name: string; readonly version: string; }; - readonly authMethodId: string; + readonly authMethodId?: string; readonly mcpServers?: ReadonlyArray; readonly requestLogger?: (event: AcpSessionRequestLogEvent) => Effect.Effect; readonly protocolLogging?: { @@ -66,10 +66,26 @@ export interface AcpSessionRequestLogEvent { readonly cause?: Cause.Cause; } +export function selectAcpAgentAuthMethod( + authMethods: ReadonlyArray | undefined, + preferredMethodId?: string, +): EffectAcpSchema.AuthMethod | undefined { + const preferred = preferredMethodId?.trim(); + if (preferred) { + return authMethods?.find((method) => method.id === preferred); + } + return authMethods?.find((method) => !("type" in method)); +} + +function isAcpAuthenticationRequired(error: EffectAcpErrors.AcpError): boolean { + return error._tag === "AcpRequestError" && error.code === -32000; +} + export interface AcpSessionRuntimeStartResult { readonly sessionId: string; readonly initializeResult: EffectAcpSchema.InitializeResponse; readonly sessionSetupResult: + | EffectAcpSchema.ForkSessionResponse | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse; @@ -165,6 +181,26 @@ export class AcpSessionRuntime extends Context.Service< readonly getModeState: Effect.Effect; /** Latest configuration options observed from session setup and configuration writes. */ readonly getConfigOptions: Effect.Effect>; + /** Loads a persisted ACP session into the active runtime. */ + readonly loadSession: ( + sessionId: string, + ) => Effect.Effect; + /** Resumes a persisted ACP session in the active runtime. */ + readonly resumeSession: ( + sessionId: string, + ) => Effect.Effect; + /** Forks a persisted ACP session and makes the fork active. */ + readonly forkSession: ( + sessionId: string, + ) => Effect.Effect; + /** Lists persisted sessions when the negotiated ACP capability is available. */ + readonly listSessions: ( + cursor?: string, + ) => Effect.Effect; + /** Closes the requested or currently active ACP session. */ + readonly closeSession: ( + sessionId?: string, + ) => Effect.Effect; /** * Sends a prompt turn to the active session. * @see https://agentclientprotocol.com/protocol/schema#session/prompt @@ -417,6 +453,7 @@ export const make = ( const updateConfigOptions = ( response: | EffectAcpSchema.SetSessionConfigOptionResponse + | EffectAcpSchema.ForkSessionResponse | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse, @@ -427,6 +464,30 @@ export const make = ( current ? { ...current, currentModeId: modeId } : current, ); + const adoptSession = ( + sessionId: string, + sessionSetupResult: + | EffectAcpSchema.ForkSessionResponse + | EffectAcpSchema.LoadSessionResponse + | EffectAcpSchema.NewSessionResponse + | EffectAcpSchema.ResumeSessionResponse, + ): Effect.Effect => + Effect.gen(function* () { + const current = yield* getStartedState; + const nextState = { + sessionId, + initializeResult: current.initializeResult, + sessionSetupResult, + modelConfigId: extractModelConfigId(sessionSetupResult), + } satisfies AcpStartedState; + yield* Ref.set(modeStateRef, parseSessionModeState(sessionSetupResult)); + yield* Ref.set(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult)); + yield* Ref.set(toolCallsRef, new Map()); + yield* Ref.set(assistantSegmentRef, { nextSegmentIndex: 0 }); + yield* Ref.set(startStateRef, { _tag: "Started", result: nextState }); + return nextState; + }); + const setConfigOption = ( configId: string, value: string | boolean, @@ -478,35 +539,80 @@ export const make = ( acp.agent.initialize(initializePayload), ); - const authenticatePayload = { - methodId: options.authMethodId, - } satisfies EffectAcpSchema.AuthenticateRequest; + const authenticateAfterRequired = ( + authRequiredError: EffectAcpErrors.AcpError, + ): Effect.Effect => + Effect.gen(function* () { + const configuredAuthMethodId = options.authMethodId?.trim(); + const authMethod = selectAcpAgentAuthMethod( + initializeResult.authMethods, + configuredAuthMethodId, + ); + if ( + configuredAuthMethodId && + initializeResult.authMethods !== undefined && + authMethod === undefined + ) { + return yield* new EffectAcpErrors.AcpTransportError({ + detail: `ACP agent did not advertise configured authentication method "${configuredAuthMethodId}"`, + cause: { configuredAuthMethodId, authMethods: initializeResult.authMethods }, + }); + } + if (authMethod !== undefined && "type" in authMethod) { + return yield* new EffectAcpErrors.AcpTransportError({ + detail: `ACP authentication method "${authMethod.id}" requires ${authMethod.type} authentication, which cannot run inside a headless provider session`, + cause: authMethod, + }); + } - yield* runLoggedRequest( - "authenticate", - authenticatePayload, - acp.agent.authenticate(authenticatePayload), - ); + const authMethodId = authMethod?.id ?? configuredAuthMethodId; + if (!authMethodId) { + return yield* authRequiredError; + } - let sessionId: string; - let sessionSetupResult: - | EffectAcpSchema.LoadSessionResponse - | EffectAcpSchema.NewSessionResponse - | EffectAcpSchema.ResumeSessionResponse; - if (options.resumeSessionId) { - const loadPayload = { - sessionId: options.resumeSessionId, - cwd: options.cwd, - mcpServers: options.mcpServers ?? [], - } satisfies EffectAcpSchema.LoadSessionRequest; - const resumed = yield* runLoggedRequest( - "session/load", - loadPayload, - acp.agent.loadSession(loadPayload), - ).pipe(Effect.exit); - if (Exit.isSuccess(resumed)) { - sessionId = options.resumeSessionId; - sessionSetupResult = resumed.value; + const authenticatePayload = { + methodId: authMethodId, + } satisfies EffectAcpSchema.AuthenticateRequest; + yield* runLoggedRequest( + "authenticate", + authenticatePayload, + acp.agent.authenticate(authenticatePayload), + ); + }); + + const setupSession = Effect.gen(function* () { + let sessionId: string; + let sessionSetupResult: + | EffectAcpSchema.LoadSessionResponse + | EffectAcpSchema.NewSessionResponse + | EffectAcpSchema.ResumeSessionResponse; + if (options.resumeSessionId) { + const loadPayload = { + sessionId: options.resumeSessionId, + cwd: options.cwd, + mcpServers: options.mcpServers ?? [], + } satisfies EffectAcpSchema.LoadSessionRequest; + const resumed = yield* runLoggedRequest( + "session/load", + loadPayload, + acp.agent.loadSession(loadPayload), + ).pipe(Effect.exit); + if (Exit.isSuccess(resumed)) { + sessionId = options.resumeSessionId; + sessionSetupResult = resumed.value; + } else { + const createPayload = { + cwd: options.cwd, + mcpServers: options.mcpServers ?? [], + } satisfies EffectAcpSchema.NewSessionRequest; + const created = yield* runLoggedRequest( + "session/new", + createPayload, + acp.agent.createSession(createPayload), + ); + sessionId = created.sessionId; + sessionSetupResult = created; + } } else { const createPayload = { cwd: options.cwd, @@ -520,19 +626,17 @@ export const make = ( sessionId = created.sessionId; sessionSetupResult = created; } - } else { - const createPayload = { - cwd: options.cwd, - mcpServers: options.mcpServers ?? [], - } satisfies EffectAcpSchema.NewSessionRequest; - const created = yield* runLoggedRequest( - "session/new", - createPayload, - acp.agent.createSession(createPayload), - ); - sessionId = created.sessionId; - sessionSetupResult = created; - } + + return { sessionId, sessionSetupResult }; + }); + + const { sessionId, sessionSetupResult } = yield* setupSession.pipe( + Effect.catch((error) => + isAcpAuthenticationRequired(error) + ? authenticateAfterRequired(error).pipe(Effect.andThen(setupSession)) + : Effect.fail(error), + ), + ); yield* Ref.set(modeStateRef, parseSessionModeState(sessionSetupResult)); yield* Ref.set(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult)); @@ -598,6 +702,82 @@ export const make = ( getEvents: () => Stream.fromQueue(eventQueue), getModeState: Ref.get(modeStateRef), getConfigOptions: Ref.get(configOptionsRef), + loadSession: (sessionId) => + start.pipe( + Effect.flatMap(() => { + const requestPayload = { + sessionId, + cwd: options.cwd, + mcpServers: options.mcpServers ?? [], + } satisfies EffectAcpSchema.LoadSessionRequest; + return runLoggedRequest( + "session/load", + requestPayload, + acp.agent.loadSession(requestPayload), + ); + }), + Effect.flatMap((response) => adoptSession(sessionId, response)), + ), + resumeSession: (sessionId) => + start.pipe( + Effect.flatMap(() => { + const requestPayload = { + sessionId, + cwd: options.cwd, + mcpServers: options.mcpServers ?? [], + } satisfies EffectAcpSchema.ResumeSessionRequest; + return runLoggedRequest( + "session/resume", + requestPayload, + acp.agent.resumeSession(requestPayload), + ); + }), + Effect.flatMap((response) => adoptSession(sessionId, response)), + ), + forkSession: (sessionId) => + start.pipe( + Effect.flatMap(() => { + const requestPayload = { + sessionId, + cwd: options.cwd, + mcpServers: options.mcpServers ?? [], + } satisfies EffectAcpSchema.ForkSessionRequest; + return runLoggedRequest( + "session/fork", + requestPayload, + acp.agent.forkSession(requestPayload), + ); + }), + Effect.flatMap((response) => adoptSession(response.sessionId, response)), + ), + listSessions: (cursor) => { + const requestPayload = { + cwd: options.cwd, + ...(cursor === undefined ? {} : { cursor }), + } satisfies EffectAcpSchema.ListSessionsRequest; + return start.pipe( + Effect.andThen( + runLoggedRequest( + "session/list", + requestPayload, + acp.agent.listSessions(requestPayload), + ), + ), + ); + }, + closeSession: (sessionId) => + start.pipe( + Effect.flatMap((started) => { + const requestPayload = { + sessionId: sessionId ?? started.sessionId, + } satisfies EffectAcpSchema.CloseSessionRequest; + return runLoggedRequest( + "session/close", + requestPayload, + acp.agent.closeSession(requestPayload), + ); + }), + ), prompt: (payload) => getStartedState.pipe( Effect.flatMap((started) => { diff --git a/apps/server/src/provider/acp/GrokAcpCliProbe.test.ts b/apps/server/src/provider/acp/GrokAcpCliProbe.test.ts index 222fc4a12d5..7ca3d72a12f 100644 --- a/apps/server/src/provider/acp/GrokAcpCliProbe.test.ts +++ b/apps/server/src/provider/acp/GrokAcpCliProbe.test.ts @@ -66,4 +66,23 @@ describe.runIf(process.env.T3_GROK_ACP_PROBE === "1")("Grok ACP CLI probe", () = yield* runtime.setSessionModel(currentModelId); }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ); + + it.effect("switches to another advertised model and completes a prompt", () => + Effect.gen(function* () { + const runtime = yield* makeProbeRuntime; + const started = yield* runtime.start(); + const models = started.sessionSetupResult.models; + const targetModel = models?.availableModels.find( + ({ modelId }) => modelId !== models.currentModelId, + )?.modelId; + expect(targetModel).toBeDefined(); + if (!targetModel) return; + + yield* runtime.setSessionModel(targetModel).pipe(Effect.timeout("20 seconds")); + const result = yield* runtime + .prompt({ prompt: [{ type: "text", text: "Respond with exactly: grok switch ok" }] }) + .pipe(Effect.timeout("60 seconds")); + expect(result.stopReason).toBe("end_turn"); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); }); diff --git a/apps/server/src/provider/acp/XAiAcpExtension.ts b/apps/server/src/provider/acp/XAiAcpExtension.ts index 6c774c7f8d5..cc7669fd3ca 100644 --- a/apps/server/src/provider/acp/XAiAcpExtension.ts +++ b/apps/server/src/provider/acp/XAiAcpExtension.ts @@ -64,6 +64,17 @@ export function extractXAiAskUserQuestions( })); } +export function extractXAiAskUserQuestionIdentity(params: XAiAskUserQuestionRequest): { + readonly sessionId: string; + readonly toolCallId: string; +} { + const unwrapped = unwrapAskUserQuestionParams(params); + return { + sessionId: unwrapped.sessionId, + toolCallId: unwrapped.toolCallId, + }; +} + interface XAiAskUserQuestionAnnotation { readonly preview?: string; readonly notes?: string; diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 791a96e1da3..bbff99705d2 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -20,6 +20,7 @@ * * @module provider/builtInDrivers */ +import { AcpRegistryDriver, type AcpRegistryDriverEnv } from "./Drivers/AcpRegistryDriver.ts"; import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts"; import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts"; import { CursorDriver, type CursorDriverEnv } from "./Drivers/CursorDriver.ts"; @@ -33,6 +34,7 @@ import type { AnyProviderDriver } from "./ProviderDriver.ts"; * layer must provide every service in this union. */ export type BuiltInDriversEnv = + | AcpRegistryDriverEnv | ClaudeDriverEnv | CodexDriverEnv | CursorDriverEnv @@ -50,4 +52,5 @@ export const BUILT_IN_DRIVERS: ReadonlyArray> = { + context: "contextWindow", + fast: "fastMode", +}; + +const PROVIDER_OPTION_TO_CURSOR_SDK_PARAMETER: Readonly> = { + contextWindow: "context", + fastMode: "fast", +}; + +export function cursorSdkProviderOptionId(parameterId: string): string { + return CURSOR_SDK_PARAMETER_TO_PROVIDER_OPTION[parameterId] ?? parameterId; +} + +export function cursorSdkParameterId(providerOptionId: string): string { + return PROVIDER_OPTION_TO_CURSOR_SDK_PARAMETER[providerOptionId] ?? providerOptionId; +} + +export function cursorSdkParameterPriority(parameterId: string): number { + switch (parameterId) { + case "effort": + case "reasoning": + return 0; + case "context": + return 1; + case "fast": + return 2; + case "thinking": + return 3; + default: + return 4; + } +} diff --git a/apps/server/src/provider/testUtils/providerAdapterRegistryMock.ts b/apps/server/src/provider/testUtils/providerAdapterRegistryMock.ts deleted file mode 100644 index c696a51c37e..00000000000 --- a/apps/server/src/provider/testUtils/providerAdapterRegistryMock.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Test helpers for constructing a `ProviderAdapterRegistryShape` mock from a - * kind-keyed adapter map. - * - * Tests historically assembled a `registry` object with only `getByProvider` - * + `listProviders` populated. Slice D grew the shape with `getByInstance` - * and `listInstances`; this helper fills both in from a single kind-keyed - * input so individual fixtures can stay concise. - * - * Non-default instance ids (e.g. `codex_personal`) are not addressable via - * the shim returned here — the legacy test fixtures only ever had - * single-instance-per-driver data anyway. - * - * @module provider/testUtils/providerAdapterRegistryMock - */ -import { - defaultInstanceIdForDriver, - ProviderDriverKind, - type ProviderInstanceId, -} from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import * as PubSub from "effect/PubSub"; -import * as Record from "effect/Record"; -import * as Result from "effect/Result"; -import * as Stream from "effect/Stream"; - -import { ProviderUnsupportedError, type ProviderAdapterError } from "../Errors.ts"; -import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts"; -import type { ProviderAdapterRegistryShape } from "../Services/ProviderAdapterRegistry.ts"; - -export type KindAdapterMap = Partial< - Record> ->; - -/** - * Build a `ProviderAdapterRegistryShape` from a kind-keyed adapter map. - * Every adapter present in the map is addressable via both the legacy - * `getByProvider(kind)` path and the new `getByInstance(id)` path (where - * `id = defaultInstanceIdForDriver(kind)`). - */ -export const makeAdapterRegistryMock = (adapters: KindAdapterMap): ProviderAdapterRegistryShape => { - const byInstanceId = new Map>(); - for (const [kind, adapter] of Object.entries(adapters)) { - if (!adapter) continue; - const driverKind = ProviderDriverKind.make(kind); - byInstanceId.set(defaultInstanceIdForDriver(driverKind), adapter); - } - - const getByInstance: ProviderAdapterRegistryShape["getByInstance"] = (instanceId) => { - const adapter = byInstanceId.get(instanceId); - return adapter - ? Effect.succeed(adapter) - : Effect.fail( - new ProviderUnsupportedError({ - provider: ProviderDriverKind.make(instanceId), - }), - ); - }; - - return { - getByInstance, - getInstanceInfo: (instanceId) => { - const adapter = byInstanceId.get(instanceId); - if (!adapter) { - return Effect.fail( - new ProviderUnsupportedError({ - provider: ProviderDriverKind.make(instanceId), - }), - ); - } - return Effect.succeed({ - instanceId, - driverKind: ProviderDriverKind.make(adapter.provider), - displayName: undefined, - enabled: true, - continuationIdentity: { - driverKind: ProviderDriverKind.make(adapter.provider), - continuationKey: `${adapter.provider}:instance:${instanceId}`, - }, - }); - }, - listInstances: () => Effect.succeed(Array.from(byInstanceId.keys())), - listProviders: () => - Effect.succeed( - Record.keys( - Record.filterMap(adapters, (adapter, kind) => - adapter !== undefined ? Result.succeed(kind) : Result.failVoid, - ), - ), - ), - // Static test fixtures don't reload; an empty stream is enough to - // satisfy the shape. Tests exercising hot-reload build their own - // stream via the real `ProviderInstanceRegistry`. - streamChanges: Stream.empty, - subscribeChanges: Effect.flatMap(PubSub.unbounded(), (pubsub) => - PubSub.subscribe(pubsub), - ), - }; -}; diff --git a/apps/server/src/relay/AgentAwarenessRelay.test.ts b/apps/server/src/relay/AgentAwarenessRelay.test.ts deleted file mode 100644 index 40ed694723d..00000000000 --- a/apps/server/src/relay/AgentAwarenessRelay.test.ts +++ /dev/null @@ -1,708 +0,0 @@ -import * as NodeCrypto from "node:crypto"; -import * as NodeServices from "@effect/platform-node/NodeServices"; - -import type { - EnvironmentId, - ExecutionEnvironmentDescriptor, - OrchestrationEvent, - OrchestrationProjectShell, - OrchestrationShellSnapshot, - OrchestrationThreadShell, - ProjectId, - ThreadId, - TurnId, -} from "@t3tools/contracts"; -import type { - RelayAgentActivityPublishProofPayload, - RelayAgentActivityState, -} from "@t3tools/contracts/relay"; -import { CommandId, ProviderInstanceId } from "@t3tools/contracts"; -import { RelayClientTracer } from "@t3tools/shared/relayTracing"; -import { RELAY_ACTIVITY_PUBLISH_TYP, verifyRelayJwt } from "@t3tools/shared/relayJwt"; -import { describe, expect, it } from "@effect/vitest"; -import * as Deferred from "effect/Deferred"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Queue from "effect/Queue"; -import * as Stream from "effect/Stream"; -import * as Tracer from "effect/Tracer"; - -import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; -import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; -import { - OrchestrationEngineService, - type OrchestrationEngineShape, -} from "../orchestration/Services/OrchestrationEngine.ts"; -import { - ProjectionSnapshotQuery, - type ProjectionSnapshotQueryShape, -} from "../orchestration/Services/ProjectionSnapshotQuery.ts"; -import { - RELAY_ENVIRONMENT_CREDENTIAL_SECRET, - RELAY_ISSUER_SECRET, - RELAY_URL_SECRET, - PUBLISH_AGENT_ACTIVITY_SECRET, -} from "../cloud/config.ts"; -import * as AgentAwarenessRelay from "./AgentAwarenessRelay.ts"; - -const state: RelayAgentActivityState = { - environmentId: "env" as RelayAgentActivityState["environmentId"], - threadId: "thread" as RelayAgentActivityState["threadId"], - projectTitle: "Project", - threadTitle: "Thread", - modelTitle: "gpt-5.4", - phase: "running", - headline: "Running", - updatedAt: "2026-05-25T00:00:00.000Z", - deepLink: "/threads/env/thread", -}; - -const encodeSecret = (value: string): Uint8Array => new TextEncoder().encode(value); - -function makeMemorySecretStore() { - const values = new Map(); - const store = { - get: ((name) => - Effect.sync(() => { - const value = values.get(name); - return value === undefined ? Option.none() : Option.some(Uint8Array.from(value)); - })) satisfies ServerSecretStore.ServerSecretStore["Service"]["get"], - set: ((name, value) => - Effect.sync(() => { - values.set(name, Uint8Array.from(value)); - })) satisfies ServerSecretStore.ServerSecretStore["Service"]["set"], - create: ((name, value) => - Effect.sync(() => { - values.set(name, Uint8Array.from(value)); - })) satisfies ServerSecretStore.ServerSecretStore["Service"]["create"], - getOrCreateRandom: ((name, bytes) => - Effect.sync(() => { - const existing = values.get(name); - if (existing) { - return existing; - } - const generated = new Uint8Array(bytes); - values.set(name, generated); - return generated; - })) satisfies ServerSecretStore.ServerSecretStore["Service"]["getOrCreateRandom"], - remove: ((name) => - Effect.sync(() => { - values.delete(name); - })) satisfies ServerSecretStore.ServerSecretStore["Service"]["remove"], - } satisfies ServerSecretStore.ServerSecretStore["Service"]; - return { - store, - setString: (name: string, value: string) => store.set(name, encodeSecret(value)), - }; -} - -describe.sequential("signRelayAgentActivityPublishProof", () => { - it("distinguishes pending link credentials from disabled publication", () => { - expect( - AgentAwarenessRelay.resolveAgentActivityPublishingStartupState({ - relayConfigured: false, - publishEnabled: false, - }), - ).toBe("waiting-for-link"); - expect( - AgentAwarenessRelay.resolveAgentActivityPublishingStartupState({ - relayConfigured: true, - publishEnabled: false, - }), - ).toBe("disabled"); - expect( - AgentAwarenessRelay.resolveAgentActivityPublishingStartupState({ - relayConfigured: true, - publishEnabled: true, - }), - ).toBe("enabled"); - }); - - it("derives the thread id from the aggregate id for thread events without payload thread ids", () => { - const threadId = "thread-aggregate-1" as ThreadId; - const now = "2026-05-25T00:00:00.000Z"; - const event = { - type: "thread.activity-appended", - sequence: 1, - eventId: "evt-aggregate-1", - commandId: CommandId.make("cmd-1"), - aggregateKind: "thread", - aggregateId: threadId, - actor: { kind: "server" }, - payload: {}, - occurredAt: now, - } as unknown as OrchestrationEvent; - - expect(AgentAwarenessRelay.eventThreadId(event)).toBe(threadId); - }); - - it("does not publish streaming content or non-awareness activity events", () => { - const now = "2026-05-25T00:00:00.000Z"; - const base = { - sequence: 1, - eventId: "evt-1", - commandId: CommandId.make("cmd-1"), - aggregateKind: "thread", - aggregateId: "thread-1" as ThreadId, - occurredAt: now, - }; - - expect( - AgentAwarenessRelay.shouldPublishAgentAwarenessEvent({ - ...base, - type: "thread.message-sent", - payload: { - threadId: "thread-1" as ThreadId, - streaming: true, - }, - } as unknown as OrchestrationEvent), - ).toBe(false); - expect( - AgentAwarenessRelay.shouldPublishAgentAwarenessEvent({ - ...base, - type: "thread.activity-appended", - payload: { - threadId: "thread-1" as ThreadId, - activity: { - kind: "task.progress", - }, - }, - } as unknown as OrchestrationEvent), - ).toBe(false); - expect( - AgentAwarenessRelay.shouldPublishAgentAwarenessEvent({ - ...base, - type: "thread.activity-appended", - payload: { - threadId: "thread-1" as ThreadId, - activity: { - kind: "approval.requested", - }, - }, - } as unknown as OrchestrationEvent), - ).toBe(true); - expect( - AgentAwarenessRelay.shouldPublishAgentAwarenessEvent({ - ...base, - type: "thread.message-sent", - payload: { - threadId: "thread-1" as ThreadId, - streaming: false, - }, - } as unknown as OrchestrationEvent), - ).toBe(true); - }); - - it("deduplicates awareness state updates whose only change is their event timestamp", () => { - expect(AgentAwarenessRelay.agentAwarenessPublishIdentity(state)).toBe( - AgentAwarenessRelay.agentAwarenessPublishIdentity({ - ...state, - updatedAt: "2026-05-25T00:10:00.000Z", - }), - ); - expect(AgentAwarenessRelay.agentAwarenessPublishIdentity(state)).not.toBe( - AgentAwarenessRelay.agentAwarenessPublishIdentity({ - ...state, - phase: "completed", - headline: "Agent finished", - }), - ); - }); - - it("requires an explicit opt-in before publishing agent activity", () => { - expect(AgentAwarenessRelay.isAgentActivityPublishingEnabled(null)).toBe(false); - expect(AgentAwarenessRelay.isAgentActivityPublishingEnabled("false")).toBe(false); - expect(AgentAwarenessRelay.isAgentActivityPublishingEnabled("true")).toBe(true); - }); - - it("redacts failed activity details and caps other relay detail", () => { - expect( - AgentAwarenessRelay.sanitizeRelayAgentActivityState({ - ...state, - phase: "failed", - detail: "Provider process exited with secret token.", - }), - ).toMatchObject({ - phase: "failed", - detail: "The agent run failed.", - }); - expect( - AgentAwarenessRelay.sanitizeRelayAgentActivityState({ - ...state, - detail: "x".repeat(200), - })?.detail, - ).toHaveLength(160); - }); - - it("resolves a null publish state when a thread or project snapshot disappeared", () => { - const environmentId = "env-1" as EnvironmentId; - const threadId = "thread-1" as ThreadId; - const thread = { - id: threadId, - projectId: "project-1" as ProjectId, - title: "Deleted thread", - modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, - session: null, - latestTurn: null, - updatedAt: "2026-05-25T00:00:00.000Z", - hasPendingApprovals: false, - hasPendingUserInput: false, - } as OrchestrationThreadShell; - - expect( - AgentAwarenessRelay.resolveAgentAwarenessRelayPublishSnapshot({ - environmentId, - threadId, - thread: Option.none(), - project: Option.none(), - }), - ).toEqual({ - projectId: null, - state: null, - reason: "thread-not-found", - }); - - expect( - AgentAwarenessRelay.resolveAgentAwarenessRelayPublishSnapshot({ - environmentId, - threadId, - thread: Option.some(thread), - project: Option.none(), - }), - ).toEqual({ - projectId: "project-1", - state: null, - reason: "project-not-found", - }); - }); - - it("selects only active shell snapshot threads for startup catch-up", () => { - const now = "2026-05-25T00:00:00.000Z"; - const environmentId = "env-1" as EnvironmentId; - const projectId = "project-1" as ProjectId; - const activeThreadId = "thread-active" as ThreadId; - const idleThreadId = "thread-idle" as ThreadId; - - const baseThread = { - projectId, - title: "Run remote agent", - modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - latestTurn: null, - createdAt: now, - updatedAt: now, - archivedAt: null, - session: null, - latestUserMessageAt: null, - hasPendingApprovals: false, - hasPendingUserInput: false, - hasActionableProposedPlan: false, - } satisfies Omit; - - expect( - AgentAwarenessRelay.resolveAgentAwarenessRelayActiveThreadIds({ - environmentId, - projects: [ - { - id: projectId, - title: "T3 Code", - }, - ], - threads: [ - { - ...baseThread, - id: activeThreadId, - latestTurn: { - turnId: "turn-1" as TurnId, - state: "running", - requestedAt: now, - startedAt: now, - completedAt: null, - assistantMessageId: null, - }, - }, - { - ...baseThread, - id: idleThreadId, - }, - { - ...baseThread, - id: "thread-missing-project" as ThreadId, - projectId: "missing-project" as ProjectId, - latestTurn: { - turnId: "turn-2" as TurnId, - state: "running", - requestedAt: now, - startedAt: now, - completedAt: null, - assistantMessageId: null, - }, - }, - ], - }), - ).toEqual([activeThreadId]); - }); - - it("signs the activity publish JWT and rejects tampering", async () => { - const keyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const payload = { - iss: "t3-env:env", - aud: "https://relay.example.test", - sub: "env", - jti: "nonce-1", - iat: 100, - exp: 200, - environmentId: state.environmentId, - threadId: state.threadId, - state, - } satisfies RelayAgentActivityPublishProofPayload; - const proof = await Effect.runPromise( - AgentAwarenessRelay.signRelayAgentActivityPublishProof({ - privateKey: keyPair.privateKey, - payload, - }), - ); - - await expect( - Effect.runPromise( - verifyRelayJwt({ - publicKey: keyPair.publicKey, - token: proof, - typ: RELAY_ACTIVITY_PUBLISH_TYP, - issuer: "t3-env:env", - audience: "https://relay.example.test", - nowEpochSeconds: 150, - }), - ), - ).resolves.toMatchObject({ jti: "nonce-1", state }); - await expect( - Effect.runPromise( - verifyRelayJwt({ - publicKey: keyPair.publicKey, - token: (() => { - const [header, body, signature = ""] = proof.split("."); - const corruptedSignature = `${signature.startsWith("a") ? "b" : "a"}${signature.slice(1)}`; - return `${header}.${body}.${corruptedSignature}`; - })(), - typ: RELAY_ACTIVITY_PUBLISH_TYP, - issuer: "t3-env:env", - audience: "https://relay.example.test", - nowEpochSeconds: 150, - }), - ), - ).rejects.toBeDefined(); - }); - - it.effect("keeps the orchestration listener armed until relay config is installed", () => - Effect.scoped( - Effect.gen(function* () { - const events = yield* Queue.unbounded(); - const threadShellRequested = yield* Deferred.make(); - const secrets = makeMemorySecretStore(); - const now = "2026-05-25T00:00:00.000Z"; - const projectId = "project-1" as ProjectId; - const threadId = "thread-1" as ThreadId; - const environmentId = "env-1" as EnvironmentId; - - const project = { - id: projectId, - title: "T3 Code", - workspaceRoot: "/workspace", - repositoryIdentity: null, - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - } satisfies OrchestrationProjectShell; - - const thread = { - id: threadId, - projectId, - title: "Run remote agent", - modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - latestTurn: { - turnId: "turn-1" as TurnId, - state: "running", - requestedAt: now, - startedAt: now, - completedAt: null, - assistantMessageId: null, - }, - createdAt: now, - updatedAt: now, - archivedAt: null, - session: { - threadId, - status: "running", - providerName: "Codex", - runtimeMode: "full-access", - activeTurnId: "turn-1" as TurnId, - lastError: null, - updatedAt: now, - }, - latestUserMessageAt: now, - hasPendingApprovals: false, - hasPendingUserInput: false, - hasActionableProposedPlan: false, - } satisfies OrchestrationThreadShell; - - const orchestrationEngine = { - readEvents: () => Stream.empty, - dispatch: () => Effect.succeed({ sequence: 1 }), - streamDomainEvents: Stream.fromQueue(events), - } satisfies OrchestrationEngineShape; - - const snapshotQuery = { - getShellSnapshot: () => - Effect.succeed({ - snapshotSequence: 1, - projects: [project], - threads: [thread], - updatedAt: now, - } satisfies OrchestrationShellSnapshot), - getThreadShellById: () => - Deferred.succeed(threadShellRequested, undefined).pipe( - Effect.ignore, - Effect.as(Option.some(thread)), - ), - getProjectShellById: () => Effect.succeed(Option.some(project)), - } as unknown as ProjectionSnapshotQueryShape; - - const descriptor = { - environmentId, - label: "Test Desktop", - platform: { - os: "darwin", - arch: "arm64", - }, - serverVersion: "0.0.0-test", - capabilities: { - repositoryIdentity: true, - }, - } satisfies ExecutionEnvironmentDescriptor; - - const layer = Layer.mergeAll( - Layer.succeed(ServerSecretStore.ServerSecretStore, secrets.store), - Layer.succeed(ServerEnvironment.ServerEnvironment, { - getEnvironmentId: Effect.succeed(environmentId), - getDescriptor: Effect.succeed(descriptor), - }), - Layer.succeed(OrchestrationEngineService, orchestrationEngine), - Layer.succeed(ProjectionSnapshotQuery, snapshotQuery), - ); - - yield* Effect.gen(function* () { - const relay = yield* AgentAwarenessRelay.AgentAwarenessRelay; - yield* relay.start(); - yield* secrets.setString(RELAY_URL_SECRET, "http://127.0.0.1:1"); - yield* secrets.setString(RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "relay-credential"); - yield* secrets.setString(PUBLISH_AGENT_ACTIVITY_SECRET, "true"); - yield* Queue.offer(events, { - type: "thread.activity-appended", - sequence: 1, - eventId: "evt-1", - commandId: CommandId.make("cmd-1"), - aggregateKind: "thread", - aggregateId: threadId, - actor: { kind: "server" }, - payload: { - threadId, - activity: { - kind: "approval.requested", - }, - }, - occurredAt: now, - } as unknown as OrchestrationEvent); - - yield* Deferred.await(threadShellRequested).pipe(Effect.timeout("2 seconds")); - }).pipe( - Effect.provide( - AgentAwarenessRelay.layer.pipe( - Layer.provide(layer), - Layer.provideMerge(NodeServices.layer), - ), - ), - ); - }), - ), - ); - - it.effect("publishes agent activity to the relay transport URL, not the relay issuer", () => - Effect.scoped( - Effect.gen(function* () { - const originalFetch = globalThis.fetch; - const context = yield* Effect.context(); - const runFork = Effect.runForkWith(context); - const events = yield* Queue.unbounded(); - const fetchSeen = yield* Deferred.make(); - const userSpans: Array = []; - const productSpans: Array = []; - const collectingTracer = (spans: Array) => - Tracer.make({ - span: (options) => { - const span = new Tracer.NativeSpan(options); - const end = span.end.bind(span); - span.end = (endTime, exit) => { - end(endTime, exit); - spans.push(span.name); - }; - return span; - }, - }); - const secrets = makeMemorySecretStore(); - const now = "2026-05-25T00:00:00.000Z"; - const projectId = "project-1" as ProjectId; - const threadId = "thread-1" as ThreadId; - const environmentId = "env-1" as EnvironmentId; - - const project = { - id: projectId, - title: "T3 Code", - workspaceRoot: "/workspace", - repositoryIdentity: null, - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - } satisfies OrchestrationProjectShell; - - const thread = { - id: threadId, - projectId, - title: "Run remote agent", - modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - latestTurn: { - turnId: "turn-1" as TurnId, - state: "running", - requestedAt: now, - startedAt: now, - completedAt: null, - assistantMessageId: null, - }, - createdAt: now, - updatedAt: now, - archivedAt: null, - session: { - threadId, - status: "running", - providerName: "Codex", - runtimeMode: "full-access", - activeTurnId: "turn-1" as TurnId, - lastError: null, - updatedAt: now, - }, - latestUserMessageAt: now, - hasPendingApprovals: false, - hasPendingUserInput: false, - hasActionableProposedPlan: false, - } satisfies OrchestrationThreadShell; - - const descriptor = { - environmentId, - label: "Test Desktop", - platform: { - os: "darwin", - arch: "arm64", - }, - serverVersion: "0.0.0-test", - capabilities: { - repositoryIdentity: true, - }, - } satisfies ExecutionEnvironmentDescriptor; - - globalThis.fetch = ((input: Parameters[0]) => { - const url = new URL( - typeof input === "string" || input instanceof URL - ? input - : (input as unknown as { readonly url: string }).url, - ); - runFork(Deferred.succeed(fetchSeen, url)); - return Promise.resolve(Response.json({ ok: true, deliveries: [] })); - }) as unknown as typeof fetch; - yield* Effect.addFinalizer(() => - Effect.sync(() => { - globalThis.fetch = originalFetch; - }), - ); - - const layer = Layer.mergeAll( - Layer.succeed(ServerSecretStore.ServerSecretStore, secrets.store), - Layer.succeed(ServerEnvironment.ServerEnvironment, { - getEnvironmentId: Effect.succeed(environmentId), - getDescriptor: Effect.succeed(descriptor), - }), - Layer.succeed(OrchestrationEngineService, { - readEvents: () => Stream.empty, - dispatch: () => Effect.succeed({ sequence: 1 }), - streamDomainEvents: Stream.fromQueue(events), - } satisfies OrchestrationEngineShape), - Layer.succeed(ProjectionSnapshotQuery, { - getShellSnapshot: () => - Effect.succeed({ - snapshotSequence: 1, - projects: [project], - threads: [thread], - updatedAt: now, - } satisfies OrchestrationShellSnapshot), - getThreadShellById: () => Effect.succeed(Option.some(thread)), - getProjectShellById: () => Effect.succeed(Option.some(project)), - } as unknown as ProjectionSnapshotQueryShape), - ); - - yield* Effect.gen(function* () { - const relay = yield* AgentAwarenessRelay.AgentAwarenessRelay; - yield* secrets.setString(RELAY_URL_SECRET, "https://transport.example.test"); - yield* secrets.setString(RELAY_ISSUER_SECRET, "https://issuer.example.test"); - yield* secrets.setString(RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "relay-credential"); - yield* secrets.setString(PUBLISH_AGENT_ACTIVITY_SECRET, "true"); - yield* relay.start(); - yield* Queue.offer(events, { - type: "thread.activity-appended", - sequence: 1, - eventId: "evt-1", - commandId: CommandId.make("cmd-1"), - aggregateKind: "thread", - aggregateId: threadId, - actor: { kind: "server" }, - payload: { - threadId, - activity: { - kind: "approval.requested", - }, - }, - occurredAt: now, - } as unknown as OrchestrationEvent); - - const url = yield* Deferred.await(fetchSeen).pipe(Effect.timeout("2 seconds")); - expect(url.origin).toBe("https://transport.example.test"); - expect(productSpans).toContain("makePublishProof"); - expect(userSpans).not.toContain("makePublishProof"); - }).pipe( - Effect.provide( - AgentAwarenessRelay.layer.pipe( - Layer.provide(layer), - Layer.provideMerge(NodeServices.layer), - ), - ), - Effect.provideService(RelayClientTracer, Option.some(collectingTracer(productSpans))), - Effect.withTracer(collectingTracer(userSpans)), - ); - }), - ), - ); -}); diff --git a/apps/server/src/relay/AgentAwarenessRelay.ts b/apps/server/src/relay/AgentAwarenessRelay.ts index 4e036e3ea0e..9b04cf57cfb 100644 --- a/apps/server/src/relay/AgentAwarenessRelay.ts +++ b/apps/server/src/relay/AgentAwarenessRelay.ts @@ -1,8 +1,8 @@ import type { EnvironmentId, - OrchestrationEvent, - OrchestrationProjectShell, - OrchestrationThreadShell, + OrchestrationV2DomainEvent, + OrchestrationV2ThreadShell, + Project, ThreadId, } from "@t3tools/contracts"; import { @@ -10,7 +10,7 @@ import { type RelayAgentActivityPublishProofPayload, type RelayAgentActivityState, } from "@t3tools/contracts/relay"; -import { projectThreadAwareness } from "@t3tools/shared/agentAwareness"; +import { projectThreadAwarenessV2 } from "@t3tools/shared/agentAwareness"; import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import { @@ -42,8 +42,8 @@ import { } from "../cloud/config.ts"; import { getOrCreateEnvironmentKeyPairFromSecretStore } from "../cloud/environmentKeys.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; -import * as OrchestrationEngine from "../orchestration/Services/OrchestrationEngine.ts"; -import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; +import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts"; +import * as ProjectService from "../project/ProjectService.ts"; export class AgentAwarenessRelay extends Context.Service< AgentAwarenessRelay, @@ -53,37 +53,15 @@ export class AgentAwarenessRelay extends Context.Service< } >()("t3/relay/AgentAwarenessRelay") {} -export function eventThreadId(event: OrchestrationEvent): ThreadId | null { - const payload = event.payload as { readonly threadId?: unknown }; - if (typeof payload.threadId === "string") { - return payload.threadId as ThreadId; - } - if (event.aggregateKind === "thread" && typeof event.aggregateId === "string") { - return event.aggregateId as ThreadId; - } - return null; +export function eventThreadId(event: OrchestrationV2DomainEvent): ThreadId { + return event.threadId; } -export function shouldPublishAgentAwarenessEvent(event: OrchestrationEvent): boolean { - switch (event.type) { - case "thread.message-sent": - return !event.payload.streaming; - case "thread.proposed-plan-upserted": - case "thread.runtime-mode-set": - case "thread.interaction-mode-set": - return false; - case "thread.activity-appended": - return ( - event.payload.activity.kind === "approval.requested" || - event.payload.activity.kind === "approval.resolved" || - event.payload.activity.kind === "provider.approval.respond.failed" || - event.payload.activity.kind === "user-input.requested" || - event.payload.activity.kind === "user-input.resolved" || - event.payload.activity.kind === "runtime.error" - ); - default: - return true; - } +export function shouldPublishAgentAwarenessEvent(_event: OrchestrationV2DomainEvent): boolean { + // Publishing is identity-deduplicated below. Watching every V2 event keeps + // this consumer correct as the shell projection evolves without duplicating + // projection-specific relevance rules here. + return true; } export function agentAwarenessPublishIdentity(state: RelayAgentActivityState | null): string { @@ -206,8 +184,8 @@ const makePublishProof = Effect.fn("makePublishProof")(function* (input: { export function resolveAgentAwarenessRelayPublishSnapshot(input: { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; - readonly thread: Option.Option; - readonly project: Option.Option; + readonly thread: Option.Option; + readonly project: Option.Option; }): { readonly projectId: string | null; readonly state: RelayAgentActivityState | null; @@ -230,7 +208,7 @@ export function resolveAgentAwarenessRelayPublishSnapshot(input: { return { projectId: input.thread.value.projectId, state: sanitizeRelayAgentActivityState( - projectThreadAwareness({ + projectThreadAwarenessV2({ environmentId: input.environmentId, project: input.project.value, thread: input.thread.value, @@ -242,8 +220,8 @@ export function resolveAgentAwarenessRelayPublishSnapshot(input: { export function resolveAgentAwarenessRelayActiveThreadIds(input: { readonly environmentId: EnvironmentId; - readonly projects: ReadonlyArray>; - readonly threads: ReadonlyArray; + readonly projects: ReadonlyArray>; + readonly threads: ReadonlyArray; }): ReadonlyArray { const projectById = new Map(input.projects.map((project) => [project.id, project])); return input.threads @@ -253,7 +231,7 @@ export function resolveAgentAwarenessRelayActiveThreadIds(input: { return false; } return ( - projectThreadAwareness({ + projectThreadAwarenessV2({ environmentId: input.environmentId, project, thread, @@ -266,8 +244,8 @@ export function resolveAgentAwarenessRelayActiveThreadIds(input: { export const make = Effect.gen(function* () { const secrets = yield* ServerSecretStore.ServerSecretStore; const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; - const snapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; - const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService; + const threads = yield* ThreadManagement.ThreadManagementService; + const projects = yield* ProjectService.ProjectService; const crypto = yield* Crypto.Crypto; const cloudLinkKeyPair = yield* getOrCreateEnvironmentKeyPairFromSecretStore(secrets); const activeSnapshotPublishedRef = yield* Ref.make(false); @@ -369,10 +347,12 @@ export const make = Effect.gen(function* () { }); }); - const thread = yield* snapshotQuery.getThreadShellById(threadId); + const shell = yield* threads.getShellSnapshot(); + const threadValue = shell.threads.find((candidate) => candidate.id === threadId); + const thread = threadValue === undefined ? Option.none() : Option.some(threadValue); const project = Option.isSome(thread) - ? yield* snapshotQuery.getProjectShellById(thread.value.projectId) - : Option.none(); + ? yield* projects.getById(thread.value.projectId) + : Option.none(); const snapshot = resolveAgentAwarenessRelayPublishSnapshot({ environmentId, threadId, @@ -441,11 +421,14 @@ export const make = Effect.gen(function* () { return false; } const environmentId = yield* serverEnvironment.getEnvironmentId; - const snapshot = yield* snapshotQuery.getShellSnapshot(); + const [projectSnapshot, shellSnapshot] = yield* Effect.all([ + projects.snapshot, + threads.getShellSnapshot(), + ]); const activeThreadIds = resolveAgentAwarenessRelayActiveThreadIds({ environmentId, - projects: snapshot.projects, - threads: snapshot.threads, + projects: projectSnapshot.projects, + threads: shellSnapshot.threads, }); if (activeThreadIds.length === 0) { yield* Effect.logDebug("agent activity snapshot has no publishable threads"); @@ -509,13 +492,8 @@ export const make = Effect.gen(function* () { ), ); yield* Effect.forkScoped( - Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { + Stream.runForEach(threads.streamDomainEvents, (event) => { const threadId = eventThreadId(event); - if (threadId === null) { - return Effect.logDebug("agent activity publishing ignored event without thread id", { - eventType: event.type, - }); - } if (!shouldPublishAgentAwarenessEvent(event)) { return Effect.logDebug( "agent activity publishing ignored event without activity changes", diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts deleted file mode 100644 index e1daf20ed57..00000000000 --- a/apps/server/src/server.test.ts +++ /dev/null @@ -1,6639 +0,0 @@ -import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; -import * as NodeSocket from "@effect/platform-node/NodeSocket"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import * as NodeCrypto from "node:crypto"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; - -import { - AuthAccessTokenType, - AuthEnvironmentBootstrapTokenType, - AuthTokenExchangeGrantType, - CommandId, - DEFAULT_SERVER_SETTINGS, - EnvironmentId, - EventId, - GitCommandError, - KeybindingRule, - MessageId, - ExternalLauncherCommandNotFoundError, - type OrchestrationThreadShell, - TerminalNotRunningError, - type OrchestrationCommand, - type OrchestrationEvent, - ORCHESTRATION_WS_METHODS, - type PreviewEvent, - ProjectId, - ProviderDriverKind, - ProviderInstanceId, - ResolvedKeybindingRule, - ThreadId, - WS_METHODS, - WsRpcGroup, - EditorId, -} from "@t3tools/contracts"; -import { - computeDpopAccessTokenHash, - computeDpopJwkThumbprint, - type DpopPublicJwk, -} from "@t3tools/shared/dpop"; -import { RELAY_HEALTH_REQUEST_TYP, RELAY_MINT_REQUEST_TYP } from "@t3tools/shared/relayJwt"; -import * as RelayClient from "@t3tools/shared/relayClient"; -import { assert, it } from "@effect/vitest"; -import { assertFailure, assertInclude, assertTrue } from "@effect/vitest/utils"; -import * as Clock from "effect/Clock"; -import * as Deferred from "effect/Deferred"; -import * as DateTime from "effect/DateTime"; -import * as Duration from "effect/Duration"; -import * as Effect from "effect/Effect"; -import * as FileSystem from "effect/FileSystem"; -import * as Layer from "effect/Layer"; -import * as ManagedRuntime from "effect/ManagedRuntime"; -import * as Option from "effect/Option"; -import * as Path from "effect/Path"; -import * as PubSub from "effect/PubSub"; -import * as Stream from "effect/Stream"; -import * as TestClock from "effect/testing/TestClock"; -import { ChildProcessSpawner } from "effect/unstable/process"; -import { - FetchHttpClient, - HttpBody, - HttpClient, - HttpClientRequest, - HttpClientResponse, - HttpRouter, - HttpServer, -} from "effect/unstable/http"; -import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; -import { RpcClient, RpcSerialization } from "effect/unstable/rpc"; -import * as Socket from "effect/unstable/socket/Socket"; -import { vi } from "vite-plus/test"; - -const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); - -import * as ServerConfig from "./config.ts"; -import { makeRoutesLayer } from "./server.ts"; -import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; -import * as GitManager from "./git/GitManager.ts"; -import * as Keybindings from "./keybindings.ts"; -import * as ExternalLauncher from "./process/externalLauncher.ts"; -import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; -import { OrchestrationListenerCallbackError } from "./orchestration/Errors.ts"; -import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; -import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts"; -import { PersistenceSqlError } from "./persistence/Errors.ts"; -import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; -import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts"; -import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; -import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; -import * as ServerSettings from "./serverSettings.ts"; -import * as TerminalManager from "./terminal/Manager.ts"; -import * as PreviewManager from "./preview/Manager.ts"; -import * as PortScanner from "./preview/PortScanner.ts"; -import * as BrowserTraceCollector from "./observability/BrowserTraceCollector.ts"; -import * as ProjectFaviconResolver from "./project/ProjectFaviconResolver.ts"; -import * as ProjectSetupScriptRunner from "./project/ProjectSetupScriptRunner.ts"; -import * as RepositoryIdentityResolver from "./project/RepositoryIdentityResolver.ts"; -import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; -import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts"; -import * as WorkspaceFileSystem from "./workspace/WorkspaceFileSystem.ts"; -import * as WorkspacePaths from "./workspace/WorkspacePaths.ts"; -import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; -import * as VcsDriver from "./vcs/VcsDriver.ts"; -import * as VcsStatusBroadcaster from "./vcs/VcsStatusBroadcaster.ts"; -import * as VcsDriverRegistry from "./vcs/VcsDriverRegistry.ts"; -import * as VcsProvisioningService from "./vcs/VcsProvisioningService.ts"; -import * as GitWorkflowService from "./git/GitWorkflowService.ts"; -import * as ReviewService from "./review/ReviewService.ts"; -import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; -import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; -import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; -import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts"; -import * as CloudCliTokenManager from "./cloud/CliTokenManager.ts"; -import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; -import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts"; -import * as TraceDiagnostics from "./diagnostics/TraceDiagnostics.ts"; -import * as Data from "effect/Data"; - -const defaultProjectId = ProjectId.make("project-default"); -const defaultThreadId = ThreadId.make("thread-default"); -const defaultDesktopBootstrapToken = "test-desktop-bootstrap-token"; -const defaultModelSelection = { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", -} as const; -const testEnvironmentDescriptor = { - environmentId: EnvironmentId.make("environment-test"), - label: "Test environment", - platform: { - os: "darwin" as const, - arch: "arm64" as const, - }, - serverVersion: "0.0.0-test", - capabilities: { - repositoryIdentity: true, - }, -}; -const makeDefaultOrchestrationReadModel = () => { - const now = "2026-01-01T00:00:00.000Z"; - return { - snapshotSequence: 0, - updatedAt: now, - projects: [ - { - id: defaultProjectId, - title: "Default Project", - workspaceRoot: "/tmp/default-project", - defaultModelSelection, - scripts: [], - createdAt: now, - updatedAt: now, - deletedAt: null, - }, - ], - threads: [ - { - id: defaultThreadId, - projectId: defaultProjectId, - title: "Default Thread", - modelSelection: defaultModelSelection, - interactionMode: "default" as const, - runtimeMode: "full-access" as const, - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - archivedAt: null, - latestTurn: null, - messages: [], - session: null, - activities: [], - proposedPlans: [], - checkpoints: [], - deletedAt: null, - }, - ], - }; -}; - -const makeDefaultOrchestrationThreadShell = ( - overrides: Partial = {}, -): OrchestrationThreadShell => { - const now = "2026-01-01T00:00:00.000Z"; - return { - id: defaultThreadId, - projectId: defaultProjectId, - title: "Default Thread", - modelSelection: defaultModelSelection, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - latestTurn: null, - createdAt: now, - updatedAt: now, - archivedAt: null, - session: null, - latestUserMessageAt: null, - hasPendingApprovals: false, - hasPendingUserInput: false, - hasActionableProposedPlan: false, - ...overrides, - }; -}; - -const browserOtlpTracingLayer = Layer.mergeAll( - FetchHttpClient.layer, - OtlpSerialization.layerJson, - Layer.succeed(HttpClient.TracerDisabledWhen, () => true), -); - -const makeAuthTestLayer = () => - EnvironmentAuth.layer.pipe( - Layer.provide(SqlitePersistenceMemory), - Layer.provide(ServerSecretStore.layer), - ); - -const makeBrowserOtlpPayload = (spanName: string) => - Effect.gen(function* () { - const collector = yield* Effect.acquireRelease( - Effect.promise(async () => { - const NodeHttp = await import("node:http"); - - return await new Promise<{ - readonly close: () => Promise; - readonly firstRequest: Promise<{ - readonly body: string; - readonly contentType: string | null; - }>; - readonly url: string; - }>((resolve, reject) => { - let resolveFirstRequest: - | ((request: { readonly body: string; readonly contentType: string | null }) => void) - | undefined; - const firstRequest = new Promise<{ - readonly body: string; - readonly contentType: string | null; - }>((resolveRequest) => { - resolveFirstRequest = resolveRequest; - }); - - const server = NodeHttp.createServer((request, response) => { - const chunks: Buffer[] = []; - request.on("data", (chunk) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - request.on("end", () => { - resolveFirstRequest?.({ - body: Buffer.concat(chunks).toString("utf8"), - contentType: request.headers["content-type"] ?? null, - }); - resolveFirstRequest = undefined; - response.statusCode = 204; - response.end(); - }); - }); - - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (!address || typeof address === "string") { - reject(new Error("Expected TCP collector address")); - return; - } - - resolve({ - url: `http://127.0.0.1:${address.port}/v1/traces`, - firstRequest, - close: () => - new Promise((resolveClose, rejectClose) => { - server.close((error) => { - if (error) { - rejectClose(error); - return; - } - resolveClose(); - }); - }), - }); - }); - }); - }), - ({ close }) => Effect.promise(close), - ); - - const runtime = ManagedRuntime.make( - OtlpTracer.layer({ - url: collector.url, - exportInterval: "10 millis", - resource: { - serviceName: "t3-web", - attributes: { - "service.runtime": "t3-web", - "service.mode": "browser", - "service.version": "test", - }, - }, - }).pipe(Layer.provide(browserOtlpTracingLayer)), - ); - - try { - yield* Effect.promise(() => runtime.runPromise(Effect.void.pipe(Effect.withSpan(spanName)))); - } finally { - yield* Effect.promise(() => runtime.dispose()); - } - - const request = yield* Effect.raceFirst( - Effect.promise(() => collector.firstRequest).pipe(Effect.orDie), - Effect.sleep(Duration.seconds(1)).pipe( - Effect.andThen(Effect.die(new Error("Timed out waiting for OTLP trace export"))), - ), - ); - // @effect-diagnostics-next-line preferSchemaOverJson:off - return JSON.parse(request.body) as OtlpTracer.TraceData; - }); - -const buildAppUnderTest = (options?: { - config?: Partial; - layers?: { - keybindings?: Partial; - providerRegistry?: Partial; - serverSettings?: Partial; - externalLauncher?: Partial; - vcsDriver?: Partial; - vcsDriverRegistry?: Partial; - gitVcsDriver?: Partial; - gitManager?: Partial; - sourceControlRepositoryService?: Partial< - SourceControlRepositoryService.SourceControlRepositoryService["Service"] - >; - reviewService?: Partial; - vcsStatusBroadcaster?: Partial; - projectSetupScriptRunner?: Partial< - ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"] - >; - terminalManager?: Partial; - orchestrationEngine?: Partial; - projectionSnapshotQuery?: Partial; - checkpointDiffQuery?: Partial; - browserTraceCollector?: Partial; - serverLifecycleEvents?: Partial; - serverRuntimeStartup?: Partial; - serverEnvironment?: Partial; - repositoryIdentityResolver?: Partial< - RepositoryIdentityResolver.RepositoryIdentityResolver["Service"] - >; - cloudManagedEndpointRuntime?: Partial< - CloudManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"] - >; - relayClient?: Partial; - cloudCliTokenManager?: Partial; - }; -}) => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" }); - const baseDir = options?.config?.baseDir ?? tempBaseDir; - const devUrl = options?.config?.devUrl; - const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl); - const config: ServerConfig.ServerConfig["Service"] = { - logLevel: "Info", - traceMinLevel: "Info", - traceTimingEnabled: true, - traceBatchWindowMs: 200, - traceMaxBytes: 10 * 1024 * 1024, - traceMaxFiles: 10, - otlpTracesUrl: undefined, - otlpMetricsUrl: undefined, - otlpExportIntervalMs: 10_000, - otlpServiceName: "t3-server", - mode: "desktop", - port: 0, - host: "127.0.0.1", - cwd: process.cwd(), - baseDir, - ...derivedPaths, - staticDir: undefined, - devUrl, - noBrowser: true, - startupPresentation: "browser", - desktopBootstrapToken: defaultDesktopBootstrapToken, - autoBootstrapProjectFromCwd: false, - logWebSocketEvents: false, - tailscaleServeEnabled: false, - tailscaleServePort: 443, - ...options?.config, - }; - const layerConfig = ServerConfig.layer(config); - const defaultVcsDriver: VcsDriver.VcsDriver["Service"] = { - capabilities: { - kind: "git", - supportsWorktrees: true, - supportsBookmarks: false, - supportsAtomicSnapshot: false, - supportsPushDefaultRemote: true, - ignoreClassifier: "native", - }, - execute: () => - Effect.succeed({ - exitCode: ChildProcessSpawner.ExitCode(0), - stdout: "", - stderr: "", - stdoutTruncated: false, - stderrTruncated: false, - }), - detectRepository: () => Effect.succeed(null), - isInsideWorkTree: () => Effect.succeed(false), - listWorkspaceFiles: () => - Effect.succeed({ - paths: [], - truncated: false, - freshness: { - source: "live-local", - observedAt: TEST_EPOCH, - expiresAt: Option.none(), - }, - }), - listRemotes: () => - Effect.succeed({ - remotes: [], - freshness: { - source: "live-local", - observedAt: TEST_EPOCH, - expiresAt: Option.none(), - }, - }), - filterIgnoredPaths: (_cwd, relativePaths) => Effect.succeed(relativePaths), - initRepository: () => Effect.void, - ...options?.layers?.vcsDriver, - }; - const vcsDriverRegistryLayer = Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({ - get: () => Effect.succeed(defaultVcsDriver), - detect: (input) => - defaultVcsDriver.detectRepository(input.cwd).pipe( - Effect.flatMap((repository) => - repository - ? Effect.succeed(repository) - : defaultVcsDriver.isInsideWorkTree(input.cwd).pipe( - Effect.map((isInsideWorkTree) => - isInsideWorkTree - ? { - kind: "git" as const, - rootPath: input.cwd, - metadataPath: null, - freshness: { - source: "live-local" as const, - observedAt: TEST_EPOCH, - expiresAt: Option.none(), - }, - } - : null, - ), - ), - ), - Effect.map((repository) => - repository - ? ({ - kind: repository.kind, - repository, - driver: defaultVcsDriver, - } satisfies VcsDriverRegistry.VcsDriverHandle) - : null, - ), - ), - resolve: (input) => - Effect.succeed({ - kind: - input.requestedKind === "auto" || !input.requestedKind ? "git" : input.requestedKind, - repository: { - kind: - input.requestedKind === "auto" || !input.requestedKind ? "git" : input.requestedKind, - rootPath: input.cwd, - metadataPath: null, - freshness: { - source: "live-local", - observedAt: TEST_EPOCH, - expiresAt: Option.none(), - }, - }, - driver: defaultVcsDriver, - }), - ...options?.layers?.vcsDriverRegistry, - }); - const gitVcsDriverLayer = Layer.mock(GitVcsDriver.GitVcsDriver)({ - ...options?.layers?.gitVcsDriver, - }); - const gitManagerLayer = Layer.mock(GitManager.GitManager)({ - ...options?.layers?.gitManager, - }); - const workspaceEntriesLayer = WorkspaceEntries.layer.pipe( - Layer.provide(WorkspacePaths.layer), - Layer.provideMerge(vcsDriverRegistryLayer), - ); - const workspaceAndProjectServicesLayer = Layer.mergeAll( - WorkspacePaths.layer, - workspaceEntriesLayer, - WorkspaceFileSystem.layer.pipe( - Layer.provide(WorkspacePaths.layer), - Layer.provide(workspaceEntriesLayer), - ), - ProjectFaviconResolver.layer.pipe(Layer.provide(WorkspacePaths.layer)), - ); - const gitWorkflowLayer = GitWorkflowService.layer.pipe( - Layer.provideMerge(vcsDriverRegistryLayer), - Layer.provideMerge(gitVcsDriverLayer), - Layer.provideMerge(gitManagerLayer), - ); - const vcsProvisioningLayer = VcsProvisioningService.layer.pipe( - Layer.provide(vcsDriverRegistryLayer), - ); - const reviewLayer = options?.layers?.reviewService - ? Layer.mock(ReviewService.ReviewService)({ - ...options.layers.reviewService, - }) - : ReviewService.layer.pipe( - Layer.provideMerge(gitVcsDriverLayer), - Layer.provide(vcsDriverRegistryLayer), - ); - const vcsStatusBroadcasterLayer = options?.layers?.vcsStatusBroadcaster - ? Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({ - ...options.layers.vcsStatusBroadcaster, - }) - : VcsStatusBroadcaster.layer.pipe(Layer.provide(gitWorkflowLayer)); - - const servedRoutesLayer = HttpRouter.serve(makeRoutesLayer, { - disableListenLog: true, - disableLogger: true, - }).pipe( - Layer.provide( - Layer.mock(Keybindings.Keybindings)({ - loadConfigState: Effect.succeed({ - keybindings: [], - issues: [], - }), - streamChanges: Stream.empty, - ...options?.layers?.keybindings, - }), - ), - Layer.provide( - Layer.mock(ProviderRegistry.ProviderRegistry)({ - getProviders: Effect.succeed([]), - refresh: () => Effect.succeed([]), - refreshInstance: () => Effect.succeed([]), - getProviderMaintenanceCapabilitiesForInstance: (_instanceId, provider) => - Effect.succeed( - makeManualOnlyProviderMaintenanceCapabilities({ provider, packageName: null }), - ), - setProviderMaintenanceActionState: () => Effect.succeed([]), - streamChanges: Stream.empty, - ...options?.layers?.providerRegistry, - }), - ), - Layer.provide( - Layer.mock(ServerSettings.ServerSettingsService)({ - start: Effect.void, - ready: Effect.void, - getSettings: Effect.succeed(DEFAULT_SERVER_SETTINGS), - updateSettings: () => Effect.succeed(DEFAULT_SERVER_SETTINGS), - streamChanges: Stream.empty, - ...options?.layers?.serverSettings, - }), - ), - Layer.provide( - Layer.mock(ExternalLauncher.ExternalLauncher)({ - resolveAvailableEditors: () => Effect.succeed([]), - ...options?.layers?.externalLauncher, - }), - ), - Layer.provide( - Layer.mock(ProcessDiagnostics.ProcessDiagnostics)({ - read: Effect.succeed({ - serverPid: process.pid, - readAt: TEST_EPOCH, - processCount: 0, - totalRssBytes: 0, - totalCpuPercent: 0, - processes: [], - error: Option.none(), - }), - signal: (input) => - Effect.succeed({ - pid: input.pid, - signal: input.signal, - signaled: true, - message: Option.none(), - }), - }), - ), - Layer.provide( - Layer.mock(ProcessResourceMonitor.ProcessResourceMonitor)({ - readHistory: (input) => - Effect.succeed({ - readAt: TEST_EPOCH, - windowMs: input.windowMs, - bucketMs: input.bucketMs, - sampleIntervalMs: 5_000, - retainedSampleCount: 0, - totalCpuSecondsApprox: 0, - buckets: [], - topProcesses: [], - error: Option.none(), - }), - }), - ), - Layer.provide( - Layer.mock(TraceDiagnostics.TraceDiagnostics)({ - read: () => - Effect.succeed({ - traceFilePath: "", - scannedFilePaths: [], - readAt: TEST_EPOCH, - recordCount: 0, - parseErrorCount: 0, - firstSpanAt: Option.none(), - lastSpanAt: Option.none(), - failureCount: 0, - interruptionCount: 0, - slowSpanThresholdMs: 1_000, - slowSpanCount: 0, - logLevelCounts: {}, - topSpansByCount: [], - slowestSpans: [], - commonFailures: [], - latestFailures: [], - latestWarningAndErrorLogs: [], - partialFailure: Option.none(), - error: Option.none(), - }), - }), - ), - Layer.provide(gitManagerLayer), - Layer.provide(gitVcsDriverLayer), - Layer.provide(gitWorkflowLayer), - Layer.provide(reviewLayer), - Layer.provide(vcsProvisioningLayer), - Layer.provide( - Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({ - ...options?.layers?.sourceControlRepositoryService, - }), - ), - Layer.provideMerge(vcsStatusBroadcasterLayer), - Layer.provide( - Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({ - runForThread: () => Effect.succeed({ status: "no-script" as const }), - ...options?.layers?.projectSetupScriptRunner, - }), - ), - Layer.provide( - Layer.mock(TerminalManager.TerminalManager)({ - ...options?.layers?.terminalManager, - }), - ), - Layer.provide( - Layer.mergeAll( - Layer.mock(PreviewManager.PreviewManager)({ - open: () => Effect.die("PreviewManager not stubbed in this test"), - navigate: () => Effect.die("PreviewManager not stubbed in this test"), - reportStatus: () => Effect.void, - refresh: () => Effect.void, - close: () => Effect.void, - list: () => Effect.succeed({ sessions: [] }), - events: Stream.empty, - subscribeEvents: Effect.flatMap(PubSub.unbounded(), (pubsub) => - PubSub.subscribe(pubsub), - ), - }), - Layer.mock(PortScanner.PortDiscovery)({ - scan: () => Effect.succeed([]), - subscribe: () => Effect.void, - retain: Effect.void, - registerTerminalProcesses: () => Effect.void, - unregisterTerminal: () => Effect.void, - }), - ), - ), - Layer.provide( - Layer.mock(OrchestrationEngine.OrchestrationEngineService)({ - readEvents: () => Stream.empty, - dispatch: () => Effect.succeed({ sequence: 0 }), - streamDomainEvents: Stream.empty, - ...options?.layers?.orchestrationEngine, - }), - ), - Layer.provide( - Layer.mock(ProjectionSnapshotQuery.ProjectionSnapshotQuery)({ - getCommandReadModel: () => Effect.succeed(makeDefaultOrchestrationReadModel()), - getSnapshot: () => Effect.succeed(makeDefaultOrchestrationReadModel()), - getShellSnapshot: () => - Effect.succeed({ - snapshotSequence: 0, - projects: [], - threads: [], - updatedAt: "1970-01-01T00:00:00.000Z", - }), - getArchivedShellSnapshot: () => - Effect.succeed({ - snapshotSequence: 0, - projects: [], - threads: [], - updatedAt: "1970-01-01T00:00:00.000Z", - }), - getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), - getProjectShellById: () => Effect.succeed(Option.none()), - getThreadShellById: () => Effect.succeed(Option.none()), - getThreadDetailById: () => Effect.succeed(Option.none()), - getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => Effect.succeed(Option.none()), - ...options?.layers?.projectionSnapshotQuery, - }), - ), - Layer.provide( - Layer.mock(CheckpointDiffQuery.CheckpointDiffQuery)({ - getTurnDiff: () => - Effect.succeed({ - threadId: defaultThreadId, - fromTurnCount: 0, - toTurnCount: 0, - diff: "", - }), - getFullThreadDiff: () => - Effect.succeed({ - threadId: defaultThreadId, - fromTurnCount: 0, - toTurnCount: 0, - diff: "", - }), - ...options?.layers?.checkpointDiffQuery, - }), - ), - ); - - const appLayer = servedRoutesLayer.pipe( - Layer.provide( - Layer.mock(BrowserTraceCollector.BrowserTraceCollector)({ - record: () => Effect.void, - ...options?.layers?.browserTraceCollector, - }), - ), - Layer.provide( - Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({ - publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }), - snapshot: Effect.succeed({ sequence: 0, events: [] }), - stream: Stream.empty, - ...options?.layers?.serverLifecycleEvents, - }), - ), - Layer.provide( - Layer.mock(ServerRuntimeStartup.ServerRuntimeStartup)({ - awaitCommandReady: Effect.void, - markHttpListening: Effect.void, - enqueueCommand: (effect) => effect, - ...options?.layers?.serverRuntimeStartup, - }), - ), - Layer.provide( - Layer.mock(ServerEnvironment.ServerEnvironment)({ - getEnvironmentId: Effect.succeed(testEnvironmentDescriptor.environmentId), - getDescriptor: Effect.succeed(testEnvironmentDescriptor), - ...options?.layers?.serverEnvironment, - }), - ), - Layer.provide( - Layer.mock(RepositoryIdentityResolver.RepositoryIdentityResolver)({ - resolve: () => Effect.succeed(null), - ...options?.layers?.repositoryIdentityResolver, - }), - ), - Layer.provide( - Layer.succeed( - CloudManagedEndpointRuntime.CloudManagedEndpointRuntime, - CloudManagedEndpointRuntime.CloudManagedEndpointRuntime.of({ - applyConfig: () => Effect.succeed({ status: "disabled" }), - ...options?.layers?.cloudManagedEndpointRuntime, - }), - ), - ), - Layer.provide( - Layer.succeed( - RelayClient.RelayClient, - RelayClient.RelayClient.of({ - resolve: Effect.succeed({ - status: "missing", - version: RelayClient.CLOUDFLARED_VERSION, - }), - install: Effect.die("unused relay-client install"), - installWithProgress: () => Effect.die("unused relay-client install"), - ...options?.layers?.relayClient, - }), - ), - ), - Layer.provide( - Layer.mock(CloudCliTokenManager.CloudCliTokenManager)({ - get: Effect.die(new Error("Unexpected T3 Connect CLI authorization request.")), - getExisting: Effect.succeed(Option.none()), - hasCredential: Effect.succeed(false), - clear: Effect.void, - ...options?.layers?.cloudCliTokenManager, - }), - ), - Layer.provideMerge(makeAuthTestLayer()), - Layer.provideMerge(ServerSecretStore.layer), - Layer.provide(workspaceAndProjectServicesLayer), - Layer.provideMerge(FetchHttpClient.layer), - Layer.provide(layerConfig), - ); - - yield* Layer.build(appLayer); - return config; - }); - -const parseSessionCookieFromWsUrl = ( - wsUrl: string, -): { readonly cookie: string | null; readonly url: string } => { - const next = new URL(wsUrl); - const cookie = next.hash.startsWith("#cookie=") - ? decodeURIComponent(next.hash.slice("#cookie=".length)) - : null; - next.hash = ""; - return { - cookie, - url: next.toString(), - }; -}; - -const wsRpcProtocolLayer = (wsUrl: string) => { - const { cookie, url } = parseSessionCookieFromWsUrl(wsUrl); - const webSocketConstructorLayer = Layer.succeed( - Socket.WebSocketConstructor, - (socketUrl, protocols) => - new NodeSocket.NodeWS.WebSocket( - socketUrl, - protocols, - cookie ? { headers: { cookie } } : undefined, - ) as unknown as globalThis.WebSocket, - ); - - return RpcClient.layerProtocolSocket().pipe( - Layer.provide(Socket.layerWebSocket(url).pipe(Layer.provide(webSocketConstructorLayer))), - Layer.provide(RpcSerialization.layerJson), - ); -}; - -const makeWsRpcClient = RpcClient.make(WsRpcGroup); -type WsRpcClient = - typeof makeWsRpcClient extends Effect.Effect ? Client : never; - -const withWsRpcClient = ( - wsUrl: string, - f: (client: WsRpcClient) => Effect.Effect, -) => makeWsRpcClient.pipe(Effect.flatMap(f), Effect.provide(wsRpcProtocolLayer(wsUrl))); - -const appendSessionCookieToWsUrl = (url: string, sessionCookieHeader: string) => { - const isAbsoluteUrl = /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(url); - const next = new URL(url, "http://localhost"); - next.hash = `cookie=${encodeURIComponent(sessionCookieHeader)}`; - return isAbsoluteUrl ? next.toString() : `${next.pathname}${next.search}${next.hash}`; -}; - -const getHttpServerUrl = (pathname = "") => - Effect.gen(function* () { - const server = yield* HttpServer.HttpServer; - const address = server.address as HttpServer.TcpAddress; - return `http://127.0.0.1:${address.port}${pathname}`; - }); - -const bootstrapBrowserSession = ( - credential = defaultDesktopBootstrapToken, - options?: { - readonly headers?: Record; - }, -) => - Effect.gen(function* () { - const bootstrapUrl = yield* getHttpServerUrl("/api/auth/browser-session"); - const response = yield* fetchEffect(bootstrapUrl, { - method: "POST", - headers: { - "content-type": "application/json", - ...options?.headers, - }, - body: jsonRequestBody({ - credential, - }), - }); - const body = yield* responseJsonEffect<{ - readonly authenticated: boolean; - readonly sessionMethod: string; - readonly expiresAt: string; - }>(response); - return { - response, - body, - cookie: response.headers["set-cookie"], - }; - }); - -const exchangeAccessToken = ( - credential = defaultDesktopBootstrapToken, - options?: { - readonly headers?: Record; - readonly scope?: string; - readonly clientMetadata?: { - readonly label?: string; - readonly deviceType?: string; - readonly os?: string; - }; - }, -) => - Effect.gen(function* () { - const tokenUrl = yield* getHttpServerUrl("/oauth/token"); - const response = yield* fetchEffect(tokenUrl, { - method: "POST", - headers: { - "content-type": "application/x-www-form-urlencoded", - ...options?.headers, - }, - body: new URLSearchParams({ - grant_type: AuthTokenExchangeGrantType, - subject_token: credential, - subject_token_type: AuthEnvironmentBootstrapTokenType, - requested_token_type: AuthAccessTokenType, - scope: - options?.scope ?? - "orchestration:read orchestration:operate terminal:operate review:write relay:read access:read access:write relay:write", - ...(options?.clientMetadata?.label ? { client_label: options.clientMetadata.label } : {}), - ...(options?.clientMetadata?.deviceType - ? { client_device_type: options.clientMetadata.deviceType } - : {}), - ...(options?.clientMetadata?.os ? { client_os: options.clientMetadata.os } : {}), - }).toString(), - }); - const body = yield* responseJsonEffect<{ - readonly access_token?: string; - readonly issued_token_type?: string; - readonly token_type?: string; - readonly expires_in?: number; - readonly scope?: string; - readonly _tag?: string; - readonly code?: string; - readonly reason?: string; - readonly traceId?: string; - }>(response); - return { - response, - body, - }; - }); - -const makeDpopProof = (input: { - readonly method: string; - readonly url: string; - readonly iat: number; - readonly accessToken?: string; - readonly jti?: string; - readonly privateKey?: NodeCrypto.KeyObject; - readonly publicJwk?: DpopPublicJwk; -}) => { - const keyPair = - input.privateKey && input.publicJwk - ? { privateKey: input.privateKey, publicJwk: input.publicJwk } - : (() => { - const { privateKey, publicKey } = NodeCrypto.generateKeyPairSync("ec", { - namedCurve: "P-256", - }); - return { privateKey, publicJwk: publicKey.export({ format: "jwk" }) as DpopPublicJwk }; - })(); - const header = Buffer.from( - JSON.stringify({ - typ: "dpop+jwt", - alg: "ES256", - jwk: keyPair.publicJwk, - }), - ).toString("base64url"); - const payload = Buffer.from( - JSON.stringify({ - htm: input.method, - htu: input.url, - jti: input.jti ?? "proof-1", - iat: input.iat, - ...(input.accessToken ? { ath: computeDpopAccessTokenHash(input.accessToken) } : {}), - }), - ).toString("base64url"); - const signature = NodeCrypto.sign("sha256", Buffer.from(`${header}.${payload}`), { - key: keyPair.privateKey, - dsaEncoding: "ieee-p1363", - }).toString("base64url"); - return { - proof: `${header}.${payload}.${signature}`, - thumbprint: computeDpopJwkThumbprint(keyPair.publicJwk), - privateKey: keyPair.privateKey, - publicJwk: keyPair.publicJwk, - }; -}; - -const makeCloudMintCredentialRequest = (input: { - readonly privateKey: string; - readonly environmentId: EnvironmentId; - readonly clientProofKeyThumbprint: string; - readonly issuer?: string; - readonly audience?: string; - readonly subject?: string; - readonly jti?: string; - readonly nonce: string; - readonly issuedAt: string; - readonly expiresAt: string; - readonly scope?: ReadonlyArray<"environment:connect">; -}) => { - const payload = { - iss: input.issuer ?? "https://relay.example.test", - aud: input.audience ?? `t3-env:${input.environmentId}`, - sub: input.subject ?? "user_123", - jti: input.jti ?? "cloud-mint-jti-1", - environmentId: input.environmentId, - clientProofKeyThumbprint: input.clientProofKeyThumbprint, - cnf: { - jkt: input.clientProofKeyThumbprint, - }, - nonce: input.nonce, - iat: Math.floor(DateTime.makeUnsafe(input.issuedAt).epochMilliseconds / 1_000), - exp: Math.floor(DateTime.makeUnsafe(input.expiresAt).epochMilliseconds / 1_000), - scope: input.scope ?? ["environment:connect"], - } as const; - const header = Buffer.from( - JSON.stringify({ alg: "EdDSA", typ: RELAY_MINT_REQUEST_TYP }), - ).toString("base64url"); - const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url"); - const signingInput = `${header}.${encodedPayload}`; - return { - proof: `${signingInput}.${NodeCrypto.sign(null, Buffer.from(signingInput), input.privateKey).toString("base64url")}`, - }; -}; - -const makeCloudEnvironmentHealthRequest = (input: { - readonly privateKey: string; - readonly environmentId: EnvironmentId; - readonly issuer?: string; - readonly audience?: string; - readonly subject?: string; - readonly jti?: string; - readonly nonce: string; - readonly issuedAt: string; - readonly expiresAt: string; - readonly scope?: ReadonlyArray<"environment:status">; -}) => { - const payload = { - iss: input.issuer ?? "https://relay.example.test", - aud: input.audience ?? `t3-env:${input.environmentId}`, - sub: input.subject ?? "user_123", - jti: input.jti ?? "cloud-health-jti-1", - environmentId: input.environmentId, - nonce: input.nonce, - iat: Math.floor(DateTime.makeUnsafe(input.issuedAt).epochMilliseconds / 1_000), - exp: Math.floor(DateTime.makeUnsafe(input.expiresAt).epochMilliseconds / 1_000), - scope: input.scope ?? ["environment:status"], - } as const; - const header = Buffer.from( - JSON.stringify({ alg: "EdDSA", typ: RELAY_HEALTH_REQUEST_TYP }), - ).toString("base64url"); - const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url"); - const signingInput = `${header}.${encodedPayload}`; - return { - proof: `${signingInput}.${NodeCrypto.sign(null, Buffer.from(signingInput), input.privateKey).toString("base64url")}`, - }; -}; - -const decodeCompactJwtPayload = (token: string): A => { - const encodedPayload = token.split(".")[1]; - if (!encodedPayload) { - throw new Error("JWT does not contain a payload."); - } - return JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8")) as A; -}; - -class AuthenticationGetterError extends Data.TaggedError("AuthenticationGetterError")<{ - readonly message: string; -}> {} - -class TestHttpRequestError extends Data.TaggedError("TestHttpRequestError")<{ - readonly cause: unknown; -}> {} - -const testRequestUrl = (input: Parameters[0]): string => { - const value = input.toString(); - if (!/^https?:\/\//i.test(value)) { - return value; - } - const url = new URL(value); - return `${url.pathname}${url.search}`; -}; - -const fetchEffect = (input: Parameters[0], init?: RequestInit) => { - const request = HttpClientRequest.make((init?.method ?? "GET") as "GET" | "POST")( - testRequestUrl(input), - { - headers: init?.headers as Record | undefined, - }, - ).pipe( - typeof init?.body === "string" - ? HttpClientRequest.bodyText( - init.body, - (init.headers as Record | undefined)?.["content-type"] ?? - "application/json", - ) - : (request) => request, - ); - const effect = HttpClient.execute(request); - return ( - init?.redirect === "manual" - ? effect.pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" })) - : effect - ).pipe(Effect.mapError((cause) => new TestHttpRequestError({ cause }))); -}; - -const jsonRequestBody = (value: unknown): string => { - return JSON.stringify(value); -}; - -const responseJsonEffect = (response: HttpClientResponse.HttpClientResponse) => - response.json.pipe( - Effect.map((json) => json as A), - Effect.mapError((cause) => new TestHttpRequestError({ cause })), - ); - -const responseOk = (response: HttpClientResponse.HttpClientResponse) => - response.status >= 200 && response.status < 300; - -const getAuthenticatedSessionCookieHeader = (credential = defaultDesktopBootstrapToken) => - Effect.gen(function* () { - const { response, cookie } = yield* bootstrapBrowserSession(credential); - if (!responseOk(response)) { - return yield* new AuthenticationGetterError({ - message: `Expected bootstrap session response to succeed, got ${response.status}`, - }); - } - - if (!cookie) { - return yield* new AuthenticationGetterError({ - message: "Expected bootstrap session response to set a cookie.", - }); - } - - return cookie.split(";")[0] ?? cookie; - }); - -const getAuthenticatedBearerSessionToken = (credential = defaultDesktopBootstrapToken) => - Effect.gen(function* () { - const { response, body } = yield* exchangeAccessToken(credential); - if (!responseOk(response)) { - return yield* new AuthenticationGetterError({ - message: `Expected bearer bootstrap response to succeed, got ${response.status}`, - }); - } - - if (!body.access_token) { - return yield* new AuthenticationGetterError({ - message: "Expected token exchange response to include an access token.", - }); - } - - return body.access_token; - }); - -const extractSessionTokenFromSetCookie = (cookieHeader: string): string => { - const [nameValue] = cookieHeader.split(";", 1); - const token = nameValue?.split("=", 2)[1]; - if (!token) { - throw new Error("Expected session cookie header to contain a token value."); - } - return token; -}; - -const splitHeaderTokens = (value: string | null | undefined) => - (value ?? "") - .split(",") - .map((token) => token.trim()) - .filter((token) => token.length > 0) - .toSorted(); - -const assertBrowserApiCorsResponseHeaders = ( - headers: Readonly>, - options?: { - readonly origin?: string; - readonly credentials?: boolean; - }, -) => { - assert.equal(headers["access-control-allow-origin"], options?.origin ?? "*"); - assert.equal( - headers["access-control-allow-credentials"], - options?.credentials ? "true" : undefined, - ); -}; - -const assertBrowserApiCorsPreflightHeaders = ( - headers: Readonly>, - options?: { - readonly origin?: string; - readonly credentials?: boolean; - }, -) => { - assertBrowserApiCorsResponseHeaders(headers, options); - assert.deepEqual(splitHeaderTokens(headers["access-control-allow-methods"] ?? null), [ - "GET", - "OPTIONS", - "POST", - ]); - assert.deepEqual(splitHeaderTokens(headers["access-control-allow-headers"]), [ - "authorization", - "b3", - "content-type", - "dpop", - "traceparent", - ]); -}; -const crossOriginClientOrigin = "http://remote-client.test:3773"; - -const getWsServerUrl = ( - pathname = "", - options?: { authenticated?: boolean; credential?: string }, -) => - Effect.gen(function* () { - const server = yield* HttpServer.HttpServer; - const address = server.address as HttpServer.TcpAddress; - const baseUrl = `ws://127.0.0.1:${address.port}${pathname}`; - if (options?.authenticated === false) { - return baseUrl; - } - return appendSessionCookieToWsUrl( - baseUrl, - yield* getAuthenticatedSessionCookieHeader(options?.credential), - ); - }); - -it.layer(NodeServices.layer)("server router seam", (it) => { - it.effect("serves static index content for GET / when staticDir is configured", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-static-" }); - const indexPath = path.join(staticDir, "index.html"); - yield* fileSystem.writeFileString(indexPath, "router-static-ok"); - - yield* buildAppUnderTest({ config: { staticDir } }); - - const response = yield* HttpClient.get("/"); - assert.equal(response.status, 200); - assert.include(yield* response.text, "router-static-ok"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("redirects to dev URL when configured", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - config: { devUrl: new URL("http://127.0.0.1:5173") }, - }); - - const url = yield* getHttpServerUrl("/foo/bar?token=test-token"); - const response = yield* fetchEffect(url, { redirect: "manual" }); - - assert.equal(response.status, 302); - assert.equal(response.headers.location, "http://127.0.0.1:5173/foo/bar?token=test-token"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("serves the public environment descriptor without requiring auth", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const url = yield* getHttpServerUrl("/.well-known/t3/environment"); - const response = yield* fetchEffect(url); - const body = yield* responseJsonEffect(response); - - assert.equal(response.status, 200); - assert.deepEqual(body, testEnvironmentDescriptor); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("includes CORS headers on public environment descriptor responses", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const url = yield* getHttpServerUrl("/.well-known/t3/environment"); - const response = yield* fetchEffect(url, { - headers: { - origin: crossOriginClientOrigin, - }, - }); - const body = yield* responseJsonEffect(response); - - assert.equal(response.status, 200); - assertBrowserApiCorsResponseHeaders(response.headers); - assert.deepEqual(body, testEnvironmentDescriptor); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("reports unauthenticated session state without requiring auth", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const url = yield* getHttpServerUrl("/api/auth/session"); - const response = yield* fetchEffect(url); - const body = yield* responseJsonEffect<{ - readonly authenticated: boolean; - readonly auth: { - readonly policy: string; - readonly bootstrapMethods: ReadonlyArray; - readonly sessionMethods: ReadonlyArray; - readonly sessionCookieName: string; - }; - }>(response); - - assert.equal(response.status, 200); - assert.equal(body.authenticated, false); - assert.equal(body.auth.policy, "desktop-managed-local"); - assert.deepEqual(body.auth.bootstrapMethods, ["desktop-bootstrap"]); - assert.deepEqual(body.auth.sessionMethods, [ - "browser-session-cookie", - "bearer-access-token", - "dpop-access-token", - ]); - assert.isTrue(body.auth.sessionCookieName.startsWith("t3_session_")); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("bootstraps a browser session and authenticates the session endpoint via cookie", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const { - response: bootstrapResponse, - body: bootstrapBody, - cookie: setCookie, - } = yield* bootstrapBrowserSession(); - - assert.equal(bootstrapResponse.status, 200); - assert.equal(bootstrapBody.authenticated, true); - assert.equal(bootstrapBody.sessionMethod, "browser-session-cookie"); - assert.isUndefined((bootstrapBody as { readonly sessionToken?: string }).sessionToken); - assert.isDefined(setCookie); - - const sessionUrl = yield* getHttpServerUrl("/api/auth/session"); - const sessionResponse = yield* fetchEffect(sessionUrl, { - headers: { - cookie: setCookie?.split(";")[0] ?? "", - }, - }); - const sessionBody = yield* responseJsonEffect<{ - readonly authenticated: boolean; - readonly sessionMethod?: string; - }>(sessionResponse); - - assert.equal(sessionResponse.status, 200); - assert.equal(sessionBody.authenticated, true); - assert.equal(sessionBody.sessionMethod, "browser-session-cookie"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("exchanges a bootstrap grant for a scoped bearer access token", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const { response: tokenResponse, body: tokenBody } = yield* exchangeAccessToken(); - - assert.equal(tokenResponse.status, 200); - assert.equal(tokenBody.issued_token_type, AuthAccessTokenType); - assert.equal(tokenBody.token_type, "Bearer"); - assert.equal( - tokenBody.scope, - "orchestration:read orchestration:operate terminal:operate review:write relay:read access:read access:write relay:write", - ); - assert.equal(typeof tokenBody.access_token, "string"); - - const sessionUrl = yield* getHttpServerUrl("/api/auth/session"); - const sessionResponse = yield* fetchEffect(sessionUrl, { - headers: { - authorization: `Bearer ${tokenBody.access_token ?? ""}`, - }, - }); - const sessionBody = yield* responseJsonEffect<{ - readonly authenticated: boolean; - readonly sessionMethod?: string; - readonly scopes?: ReadonlyArray; - }>(sessionResponse); - - assert.equal(sessionResponse.status, 200); - assert.equal(sessionBody.authenticated, true); - assert.equal(sessionBody.sessionMethod, "bearer-access-token"); - assert.deepEqual(sessionBody.scopes, [ - "orchestration:read", - "orchestration:operate", - "terminal:operate", - "review:write", - "relay:read", - "access:read", - "access:write", - "relay:write", - ]); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("persists token exchange client display metadata for authorized-client listings", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - config: { - host: "0.0.0.0", - }, - }); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const pairingResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: ownerCookie, - }, - body: yield* HttpBody.json({}), - }); - const pairingBody = (yield* pairingResponse.json) as { - readonly credential: string; - }; - - const { response } = yield* exchangeAccessToken(pairingBody.credential, { - headers: { - "user-agent": "undici", - }, - scope: "orchestration:read orchestration:operate terminal:operate review:write", - clientMetadata: { - label: "T3 Code Mobile", - deviceType: "mobile", - os: "iOS", - }, - }); - - const clientsResponse = yield* HttpClient.get("/api/auth/clients", { - headers: { - cookie: ownerCookie, - }, - }); - const clients = (yield* clientsResponse.json) as ReadonlyArray<{ - readonly current: boolean; - readonly client: { - readonly label?: string; - readonly deviceType: string; - readonly ipAddress?: string; - readonly os?: string; - readonly userAgent?: string; - }; - }>; - const mobileClient = clients.find((client) => !client.current); - - assert.equal(pairingResponse.status, 200); - assert.equal(response.status, 200); - assert.equal(clientsResponse.status, 200); - assert.deepInclude(mobileClient?.client, { - label: "T3 Code Mobile", - deviceType: "mobile", - os: "iOS", - ipAddress: "127.0.0.1", - userAgent: "undici", - }); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "exchanges a bootstrap credential for a DPoP-bound access token without bearer downgrade", - () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const credentialResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { cookie: ownerCookie }, - body: yield* HttpBody.json({}), - }); - const credential = (yield* credentialResponse.json) as { readonly credential: string }; - const tokenUrl = yield* getHttpServerUrl("/oauth/token"); - const now = yield* DateTime.now; - const tokenProof = makeDpopProof({ - method: "POST", - url: tokenUrl, - iat: Math.floor(now.epochMilliseconds / 1_000), - jti: "token-exchange-proof", - }); - const tokenResponse = yield* fetchEffect(tokenUrl, { - method: "POST", - headers: { - "content-type": "application/x-www-form-urlencoded", - dpop: tokenProof.proof, - }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", - subject_token: credential.credential, - subject_token_type: "urn:t3:params:oauth:token-type:environment-bootstrap", - requested_token_type: "urn:ietf:params:oauth:token-type:access_token", - scope: "orchestration:read orchestration:operate terminal:operate review:write", - }).toString(), - }); - const token = yield* responseJsonEffect<{ - readonly access_token: string; - readonly token_type: string; - }>(tokenResponse); - - assert.equal(tokenResponse.status, 200); - assert.equal(tokenResponse.headers["cache-control"], "no-store"); - assert.equal(token.token_type, "DPoP"); - - const sessionUrl = yield* getHttpServerUrl("/api/auth/session"); - const bearerResponse = yield* fetchEffect(sessionUrl, { - headers: { authorization: `Bearer ${token.access_token}` }, - }); - const bearerState = yield* responseJsonEffect<{ readonly authenticated: boolean }>( - bearerResponse, - ); - assert.equal(bearerState.authenticated, false); - - const sessionProof = makeDpopProof({ - method: "GET", - url: sessionUrl, - iat: Math.floor(now.epochMilliseconds / 1_000), - jti: "session-proof", - accessToken: token.access_token, - privateKey: tokenProof.privateKey, - publicJwk: tokenProof.publicJwk, - }); - const dpopResponse = yield* fetchEffect(sessionUrl, { - headers: { - authorization: `DPoP ${token.access_token}`, - dpop: sessionProof.proof, - }, - }); - const dpopState = yield* responseJsonEffect<{ - readonly authenticated: boolean; - readonly sessionMethod?: string; - }>(dpopResponse); - assert.equal(dpopState.authenticated, true); - assert.equal(dpopState.sessionMethod, "dpop-access-token"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects replayed DPoP proofs across token exchanges", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const firstCredentialResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: ownerCookie, - }, - body: yield* HttpBody.json({}), - }); - const firstCredential = (yield* firstCredentialResponse.json) as { - readonly credential: string; - }; - const secondCredentialResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: ownerCookie, - }, - body: yield* HttpBody.json({}), - }); - const secondCredential = (yield* secondCredentialResponse.json) as { - readonly credential: string; - }; - const tokenUrl = yield* getHttpServerUrl("/oauth/token"); - const now = yield* DateTime.now; - const dpop = makeDpopProof({ - method: "POST", - url: tokenUrl, - iat: Math.floor(now.epochMilliseconds / 1_000), - }); - - const firstBootstrap = yield* exchangeAccessToken(firstCredential.credential, { - headers: { - dpop: dpop.proof, - }, - scope: "orchestration:read orchestration:operate terminal:operate review:write", - }); - const replayBootstrap = yield* exchangeAccessToken(secondCredential.credential, { - headers: { - dpop: dpop.proof, - }, - scope: "orchestration:read orchestration:operate terminal:operate review:write", - }); - - assert.equal(firstBootstrap.response.status, 200); - assert.equal(replayBootstrap.response.status, 401); - assert.equal(replayBootstrap.body._tag, "EnvironmentAuthInvalidError"); - assert.equal(replayBootstrap.body.code, "auth_invalid"); - assert.equal(replayBootstrap.body.reason, "invalid_credential"); - assert.equal(typeof replayBootstrap.body.traceId, "string"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("ignores forwarded host headers when validating token exchange DPoP URLs", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const credentialResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: ownerCookie, - }, - body: yield* HttpBody.json({}), - }); - const credential = (yield* credentialResponse.json) as { - readonly credential: string; - }; - const tokenUrl = yield* getHttpServerUrl("/oauth/token"); - const now = yield* DateTime.now; - const dpop = makeDpopProof({ - method: "POST", - url: tokenUrl, - iat: Math.floor(now.epochMilliseconds / 1_000), - }); - - const bootstrap = yield* exchangeAccessToken(credential.credential, { - headers: { - dpop: dpop.proof, - "x-forwarded-host": "environment.example.test", - }, - scope: "orchestration:read orchestration:operate terminal:operate review:write", - }); - - assert.equal(bootstrap.response.status, 200); - assert.equal(bootstrap.body.token_type, "DPoP"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects token exchange DPoP proofs bound to spoofed forwarded hosts", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const credentialResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: ownerCookie, - }, - body: yield* HttpBody.json({}), - }); - const credential = (yield* credentialResponse.json) as { - readonly credential: string; - }; - const tokenUrl = yield* getHttpServerUrl("/oauth/token"); - const spoofedUrl = new URL(tokenUrl); - spoofedUrl.hostname = "environment.example.test"; - const now = yield* DateTime.now; - const dpop = makeDpopProof({ - method: "POST", - url: spoofedUrl.href, - iat: Math.floor(now.epochMilliseconds / 1_000), - }); - - const bootstrap = yield* exchangeAccessToken(credential.credential, { - headers: { - dpop: dpop.proof, - "x-forwarded-host": spoofedUrl.host, - }, - scope: "orchestration:read orchestration:operate terminal:operate review:write", - }); - - assert.equal(bootstrap.response.status, 401); - assert.equal(bootstrap.body._tag, "EnvironmentAuthInvalidError"); - assert.equal(bootstrap.body.code, "auth_invalid"); - assert.equal(bootstrap.body.reason, "invalid_credential"); - assert.equal(typeof bootstrap.body.traceId, "string"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects cloud link proofs for non-loopback managed endpoint origins", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const linkProofUrl = yield* getHttpServerUrl("/api/connect/link-proof"); - const linkProofResponse = yield* fetchEffect(linkProofUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - challenge: "relay-link-challenge", - relayIssuer: "https://relay.example.test", - endpoint: { - httpBaseUrl: "https://environment.example.test/", - wsBaseUrl: "wss://environment.example.test/ws", - providerKind: "manual", - }, - origin: { - localHttpHost: "192.168.1.42", - localHttpPort: 3773, - }, - }), - }); - const body = yield* responseJsonEffect<{ - readonly _tag?: string; - readonly message?: string; - }>(linkProofResponse); - - assert.equal(linkProofResponse.status, 400); - assert.equal(body._tag, "EnvironmentHttpBadRequestError"); - assert.equal(body.message, "Invalid managed endpoint origin."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects managed cloud link proofs for manual endpoint providers", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const linkProofUrl = yield* getHttpServerUrl("/api/connect/link-proof"); - const serverPort = Number(new URL(linkProofUrl).port); - const linkProofResponse = yield* fetchEffect(linkProofUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - challenge: "relay-link-challenge", - relayIssuer: "https://relay.example.test", - endpoint: { - httpBaseUrl: linkProofUrl.replace("/api/connect/link-proof", ""), - wsBaseUrl: linkProofUrl - .replace("http://", "ws://") - .replace("/api/connect/link-proof", "/ws"), - providerKind: "manual", - }, - origin: { - localHttpHost: "127.0.0.1", - localHttpPort: serverPort, - }, - }), - }); - const body = yield* responseJsonEffect<{ - readonly _tag?: string; - readonly message?: string; - }>(linkProofResponse); - - assert.equal(linkProofResponse.status, 400); - assert.equal(body._tag, "EnvironmentHttpBadRequestError"); - assert.equal(body.message, "Invalid managed endpoint origin."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects cloud link proofs requested through a public managed endpoint", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const linkProofUrl = yield* getHttpServerUrl("/api/connect/link-proof"); - const serverPort = Number(new URL(linkProofUrl).port); - const linkProofResponse = yield* HttpClient.post("/api/connect/link-proof", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - "content-type": "application/json", - host: "environment.example.test", - "x-forwarded-host": "environment.example.test", - "x-forwarded-proto": "https", - }, - body: HttpBody.text( - jsonRequestBody({ - challenge: "relay-link-challenge", - relayIssuer: "https://relay.example.test", - endpoint: { - httpBaseUrl: "https://environment.example.test/", - wsBaseUrl: "wss://environment.example.test/ws", - providerKind: "manual", - }, - origin: { - localHttpHost: "127.0.0.1", - localHttpPort: serverPort, - }, - }), - "application/json", - ), - }); - const body = (yield* linkProofResponse.json) as { - readonly _tag?: string; - readonly message?: string; - }; - - assert.equal(linkProofResponse.status, 400); - assert.equal(body._tag, "EnvironmentHttpBadRequestError"); - assert.equal(body.message, "Invalid managed endpoint origin."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "rejects cloud link proofs when a public request spoofs loopback forwarded headers", - () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const linkProofUrl = yield* getHttpServerUrl("/api/connect/link-proof"); - const serverPort = Number(new URL(linkProofUrl).port); - const linkProofResponse = yield* HttpClient.post("/api/connect/link-proof", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - "content-type": "application/json", - host: "environment.example.test", - "x-forwarded-host": `127.0.0.1:${serverPort}`, - "x-forwarded-proto": "http", - }, - body: HttpBody.text( - jsonRequestBody({ - challenge: "relay-link-challenge", - relayIssuer: "https://relay.example.test", - endpoint: { - httpBaseUrl: "https://environment.example.test/", - wsBaseUrl: "wss://environment.example.test/ws", - providerKind: "manual", - }, - origin: { - localHttpHost: "127.0.0.1", - localHttpPort: serverPort, - }, - }), - "application/json", - ), - }); - const body = (yield* linkProofResponse.json) as { - readonly _tag?: string; - readonly message?: string; - }; - - assert.equal(linkProofResponse.status, 400); - assert.equal(body._tag, "EnvironmentHttpBadRequestError"); - assert.equal(body.message, "Invalid managed endpoint origin."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects cloud link proofs with malformed forwarded request hosts", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const linkProofUrl = yield* getHttpServerUrl("/api/connect/link-proof"); - const serverPort = Number(new URL(linkProofUrl).port); - const linkProofResponse = yield* HttpClient.post("/api/connect/link-proof", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - "content-type": "application/json", - host: "bad host", - "x-forwarded-host": "bad host", - "x-forwarded-proto": "https", - }, - body: HttpBody.text( - jsonRequestBody({ - challenge: "relay-link-challenge", - relayIssuer: "https://relay.example.test", - endpoint: { - httpBaseUrl: "https://environment.example.test/", - wsBaseUrl: "wss://environment.example.test/ws", - providerKind: "manual", - }, - origin: { - localHttpHost: "127.0.0.1", - localHttpPort: serverPort, - }, - }), - "application/json", - ), - }); - const body = (yield* linkProofResponse.json) as { - readonly _tag?: string; - readonly message?: string; - }; - - assert.equal(linkProofResponse.status, 400); - assert.equal(body._tag, "EnvironmentHttpBadRequestError"); - assert.equal(body.message, "Invalid managed endpoint origin."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects local cloud link proofs for a different loopback port", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const linkProofUrl = yield* getHttpServerUrl("/api/connect/link-proof"); - const serverPort = Number(new URL(linkProofUrl).port); - const linkProofResponse = yield* fetchEffect(linkProofUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - challenge: "relay-link-challenge", - relayIssuer: "https://relay.example.test", - endpoint: { - httpBaseUrl: "https://environment.example.test/", - wsBaseUrl: "wss://environment.example.test/ws", - providerKind: "manual", - }, - origin: { - localHttpHost: "127.0.0.1", - localHttpPort: serverPort === 65_535 ? serverPort - 1 : serverPort + 1, - }, - }), - }); - const body = yield* responseJsonEffect<{ - readonly _tag?: string; - readonly message?: string; - }>(linkProofResponse); - - assert.equal(linkProofResponse.status, 400); - assert.equal(body._tag, "EnvironmentHttpBadRequestError"); - assert.equal(body.message, "Invalid managed endpoint origin."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("allows standard clients to read managed relay configuration state", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const credentialResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { cookie: ownerCookie }, - body: yield* HttpBody.json({}), - }); - const credential = (yield* credentialResponse.json) as { readonly credential: string }; - const pairedCookie = yield* getAuthenticatedSessionCookieHeader(credential.credential); - const linkStateUrl = yield* getHttpServerUrl("/api/connect/link-state"); - const response = yield* fetchEffect(linkStateUrl, { - headers: { cookie: pairedCookie }, - }); - const body = yield* responseJsonEffect<{ - readonly linked?: boolean; - readonly publishAgentActivity?: boolean; - }>(response); - - assert.equal(response.status, 200); - assert.equal(body.linked, false); - assert.equal(body.publishAgentActivity, false); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "reports relay client status and streams installation progress over environment RPC", - () => - Effect.gen(function* () { - const installedRelayClient = { - status: "available" as const, - executablePath: "/tmp/t3/tools/cloudflared", - source: "managed" as const, - version: RelayClient.CLOUDFLARED_VERSION, - }; - yield* buildAppUnderTest({ - layers: { - relayClient: { - resolve: Effect.succeed({ - status: "missing", - version: RelayClient.CLOUDFLARED_VERSION, - }), - install: Effect.succeed(installedRelayClient), - installWithProgress: (report) => - report({ type: "progress", stage: "checking" }).pipe( - Effect.andThen(report({ type: "progress", stage: "downloading" })), - Effect.as(installedRelayClient), - ), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const status = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => client[WS_METHODS.cloudGetRelayClientStatus]({})), - ); - const installEvents = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.cloudInstallRelayClient]({}).pipe(Stream.runCollect), - ), - ); - - assert.equal(status.status, "missing"); - assert.deepEqual(Array.from(installEvents), [ - { type: "progress", stage: "checking" }, - { type: "progress", stage: "downloading" }, - { type: "complete", status: installedRelayClient }, - ]); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("requires relay write scope to update agent activity publication", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const preferencesUrl = yield* getHttpServerUrl("/api/connect/preferences"); - const ownerResponse = yield* fetchEffect(preferencesUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ publishAgentActivity: true }), - }); - const ownerBody = yield* responseJsonEffect<{ - readonly publishAgentActivity?: boolean; - }>(ownerResponse); - assert.equal(ownerResponse.status, 200); - assert.equal(ownerBody.publishAgentActivity, true); - - const credentialResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { cookie: ownerCookie }, - body: yield* HttpBody.json({}), - }); - const credential = (yield* credentialResponse.json) as { readonly credential: string }; - const pairedCookie = yield* getAuthenticatedSessionCookieHeader(credential.credential); - const pairedResponse = yield* fetchEffect(preferencesUrl, { - method: "POST", - headers: { - cookie: pairedCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ publishAgentActivity: false }), - }); - const pairedBody = yield* responseJsonEffect<{ - readonly _tag?: string; - readonly requiredScope?: string; - }>(pairedResponse); - assert.equal(pairedResponse.status, 403); - assert.equal(pairedBody._tag, "EnvironmentScopeRequiredError"); - assert.equal(pairedBody.requiredScope, "relay:write"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects relay config with an invalid cloud mint public key", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: "not-a-public-key", - endpointRuntime: null, - }), - }); - const body = yield* responseJsonEffect<{ - readonly _tag?: string; - readonly message?: string; - }>(relayConfigResponse); - - assert.equal(relayConfigResponse.status, 400); - assert.equal(body._tag, "EnvironmentHttpBadRequestError"); - assert.equal(body.message, "Cloud mint public key must be a valid Ed25519 public key."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects relay config with insecure relay metadata or empty credentials", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const postRelayConfig = (body: { - readonly relayUrl: string; - readonly relayIssuer?: string; - readonly cloudUserId: string; - readonly environmentCredential: string; - }) => - fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - ...body, - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - - const insecureRelayUrl = yield* postRelayConfig({ - relayUrl: "http://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - }); - const insecureRelayIssuer = yield* postRelayConfig({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - relayIssuer: "http://relay.example.test", - environmentCredential: "t3env_test_credential", - }); - const nonOriginRelayUrl = yield* postRelayConfig({ - relayUrl: "https://relay.example.test/path", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - }); - const emptyCredential = yield* postRelayConfig({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: " ", - }); - const insecureRelayUrlBody = yield* responseJsonEffect<{ readonly message?: string }>( - insecureRelayUrl, - ); - const insecureRelayIssuerBody = yield* responseJsonEffect<{ readonly message?: string }>( - insecureRelayIssuer, - ); - const nonOriginRelayUrlBody = yield* responseJsonEffect<{ readonly message?: string }>( - nonOriginRelayUrl, - ); - const emptyCredentialBody = yield* responseJsonEffect<{ readonly message?: string }>( - emptyCredential, - ); - - assert.equal(insecureRelayUrl.status, 400); - assert.equal(insecureRelayUrlBody.message, "Relay URL must be a secure absolute HTTPS URL."); - assert.equal(insecureRelayIssuer.status, 400); - assert.equal( - insecureRelayIssuerBody.message, - "Relay issuer must be a secure absolute HTTPS URL.", - ); - assert.equal(nonOriginRelayUrl.status, 400); - assert.equal(nonOriginRelayUrlBody.message, "Relay URL must be a secure absolute HTTPS URL."); - assert.equal(emptyCredential.status, 400); - assert.equal(emptyCredentialBody.message, "Relay environment credential is required."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects relay config replacement from a different cloud account", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const postRelayConfig = (cloudUserId: string, environmentCredential: string) => - fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId, - environmentCredential, - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - - const firstResponse = yield* postRelayConfig("user_123", "t3env_first_credential"); - const replacementResponse = yield* postRelayConfig("user_456", "t3env_second_credential"); - const replacementBody = yield* responseJsonEffect<{ - readonly _tag?: string; - readonly message?: string; - }>(replacementResponse); - - assert.equal(firstResponse.status, 200); - assert.equal(replacementResponse.status, 409); - assert.equal(replacementBody._tag, "EnvironmentHttpConflictError"); - assert.equal( - replacementBody.message, - "This environment is already linked to a different cloud account. Unlink it before switching accounts.", - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("reports local cloud link state from persisted relay config", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const linkStateUrl = yield* getHttpServerUrl("/api/connect/link-state"); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - - const initialResponse = yield* fetchEffect(linkStateUrl, { - headers: { - cookie: ownerCookie, - }, - }); - const initialBody = yield* responseJsonEffect<{ - readonly linked?: boolean; - readonly cloudUserId?: string | null; - }>(initialResponse); - assert.equal(initialResponse.status, 200); - assert.equal(initialBody.linked, false); - assert.equal(initialBody.cloudUserId, null); - - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://transport.example.test", - relayIssuer: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const linkedResponse = yield* fetchEffect(linkStateUrl, { - headers: { - cookie: ownerCookie, - }, - }); - const linkedBody = yield* responseJsonEffect<{ - readonly linked?: boolean; - readonly cloudUserId?: string | null; - readonly relayUrl?: string | null; - readonly relayIssuer?: string | null; - }>(linkedResponse); - - assert.equal(linkedResponse.status, 200); - assert.equal(linkedBody.linked, true); - assert.equal(linkedBody.cloudUserId, "user_123"); - assert.equal(linkedBody.relayUrl, "https://transport.example.test"); - assert.equal(linkedBody.relayIssuer, "https://relay.example.test"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("does not expose internal cloud reconciliation over HTTP", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const reconcileUrl = yield* getHttpServerUrl("/api/connect/reconcile"); - const response = yield* fetchEffect(reconcileUrl, { - method: "POST", - }); - - assert.equal(response.status, 404); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("unlinks local cloud state and disables the managed endpoint runtime", () => - Effect.gen(function* () { - const appliedRuntimeConfigs: Array = []; - yield* buildAppUnderTest({ - layers: { - cloudManagedEndpointRuntime: { - applyConfig: (config) => { - appliedRuntimeConfigs.push(config); - if (!config) { - return Effect.succeed({ status: "disabled" }); - } - return Effect.succeed({ - status: "running", - providerKind: "cloudflare_tunnel", - pid: 123, - ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}), - ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}), - }); - }, - }, - }, - }); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const unlinkUrl = yield* getHttpServerUrl("/api/connect/unlink"); - const linkStateUrl = yield* getHttpServerUrl("/api/connect/link-state"); - - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://transport.example.test", - relayIssuer: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: { - providerKind: "cloudflare_tunnel", - connectorToken: "connector-token", - tunnelId: "tunnel-id", - tunnelName: "tunnel-name", - }, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const unlinkResponse = yield* fetchEffect(unlinkUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - }, - }); - const unlinkBody = yield* responseJsonEffect<{ - readonly ok?: boolean; - readonly endpointRuntimeStatus?: { readonly status?: string }; - }>(unlinkResponse); - assert.equal(unlinkResponse.status, 200); - assert.equal(unlinkBody.ok, true); - assert.equal(unlinkBody.endpointRuntimeStatus?.status, "disabled"); - - const linkStateResponse = yield* fetchEffect(linkStateUrl, { - headers: { - cookie: ownerCookie, - }, - }); - const linkStateBody = yield* responseJsonEffect<{ - readonly linked?: boolean; - readonly cloudUserId?: string | null; - readonly relayUrl?: string | null; - readonly relayIssuer?: string | null; - }>(linkStateResponse); - assert.equal(linkStateResponse.status, 200); - assert.equal(linkStateBody.linked, false); - assert.equal(linkStateBody.cloudUserId, null); - assert.equal(linkStateBody.relayUrl, null); - assert.equal(linkStateBody.relayIssuer, null); - assert.deepEqual(appliedRuntimeConfigs, [ - { - providerKind: "cloudflare_tunnel", - connectorToken: "connector-token", - tunnelId: "tunnel-id", - tunnelName: "tunnel-name", - }, - null, - ]); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects replayed cloud mint requests atomically", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const request = makeCloudMintCredentialRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - clientProofKeyThumbprint: "client-proof-key-thumbprint", - nonce: "cloud-mint-nonce-1", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }); - const mintUrl = yield* getHttpServerUrl("/api/connect/mint-credential"); - const postMint = () => - fetchEffect(mintUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody(request), - }); - - const firstResponse = yield* postMint(); - const replayResponse = yield* postMint(); - const replayBody = yield* responseJsonEffect<{ - readonly _tag?: string; - readonly message?: string; - }>(replayResponse); - - assert.equal(firstResponse.status, 200); - assert.equal(replayResponse.status, 409); - assert.equal(replayBody._tag, "EnvironmentHttpConflictError"); - assert.equal(replayBody.message, "Cloud mint request was already consumed."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("serves the documented T3 Connect mint credential endpoint", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const request = makeCloudMintCredentialRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - clientProofKeyThumbprint: "client-proof-key-thumbprint", - jti: "cloud-mint-jti-documented-endpoint", - nonce: "cloud-mint-nonce-documented-endpoint", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }); - const mintUrl = yield* getHttpServerUrl("/api/t3-connect/mint-credential"); - const response = yield* fetchEffect(mintUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody(request), - }); - - assert.equal(response.status, 200); - const body = yield* responseJsonEffect<{ - readonly credential?: string; - readonly proof?: string; - }>(response); - assert.equal(typeof body.credential, "string"); - assert.equal(typeof body.proof, "string"); - assert.equal( - decodeCompactJwtPayload<{ readonly requestNonce?: string }>(body.proof!).requestNonce, - "cloud-mint-nonce-documented-endpoint", - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("serves signed T3 Connect environment health checks", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const request = makeCloudEnvironmentHealthRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - jti: "cloud-health-jti-documented-endpoint", - nonce: "cloud-health-nonce-documented-endpoint", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }); - const healthUrl = yield* getHttpServerUrl("/api/t3-connect/health"); - const response = yield* fetchEffect(healthUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody(request), - }); - - assert.equal(response.status, 200); - const body = yield* responseJsonEffect<{ - readonly status?: string; - readonly descriptor?: { readonly environmentId?: string }; - readonly proof?: string; - }>(response); - assert.equal(body.status, "online"); - assert.equal(body.descriptor?.environmentId, testEnvironmentDescriptor.environmentId); - assert.equal(typeof body.proof, "string"); - assert.equal( - decodeCompactJwtPayload<{ readonly requestNonce?: string }>(body.proof!).requestNonce, - "cloud-health-nonce-documented-endpoint", - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects replayed cloud health requests atomically", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const request = makeCloudEnvironmentHealthRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - jti: "cloud-health-jti-replay", - nonce: "cloud-health-nonce-replay", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }); - const healthUrl = yield* getHttpServerUrl("/api/t3-connect/health"); - const postHealth = () => - fetchEffect(healthUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody(request), - }); - - const firstResponse = yield* postHealth(); - const replayResponse = yield* postHealth(); - const replayBody = yield* responseJsonEffect<{ - readonly _tag?: string; - readonly message?: string; - }>(replayResponse); - - assert.equal(firstResponse.status, 200); - assert.equal(replayResponse.status, 409); - assert.equal(replayBody._tag, "EnvironmentHttpConflictError"); - assert.equal(replayBody.message, "Cloud health request was already consumed."); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "validates cloud proofs against the configured relay issuer, not the transport URL", - () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://transport.example.test", - cloudUserId: "user_123", - relayIssuer: "https://relay.example.test", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const mintUrl = yield* getHttpServerUrl("/api/t3-connect/mint-credential"); - const postMint = (request: ReturnType) => - fetchEffect(mintUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody(request), - }); - - const acceptedResponse = yield* postMint( - makeCloudMintCredentialRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - clientProofKeyThumbprint: "client-proof-key-thumbprint", - issuer: "https://relay.example.test", - jti: "cloud-mint-jti-explicit-relay-issuer", - nonce: "cloud-mint-nonce-explicit-relay-issuer", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }), - ); - const rejectedResponse = yield* postMint( - makeCloudMintCredentialRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - clientProofKeyThumbprint: "client-proof-key-thumbprint", - issuer: "https://transport.example.test", - jti: "cloud-mint-jti-transport-url", - nonce: "cloud-mint-nonce-transport-url", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }), - ); - - assert.equal(acceptedResponse.status, 200); - assert.equal(rejectedResponse.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("fails relay config when the managed endpoint connector cannot start", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - layers: { - cloudManagedEndpointRuntime: { - applyConfig: () => - Effect.succeed({ - status: "failed", - providerKind: "cloudflare_tunnel", - reason: "cloudflared missing", - tunnelId: "tunnel-1", - }), - }, - }, - }); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: { - providerKind: "cloudflare_tunnel", - connectorToken: "connector-token", - tunnelId: "tunnel-1", - }, - }), - }); - - assert.equal(relayConfigResponse.status, 503); - const relayConfigBody = yield* responseJsonEffect<{ - _tag?: string; - message?: string; - endpointRuntimeStatus?: { status?: string; reason?: string }; - }>(relayConfigResponse); - assert.equal(relayConfigBody._tag, "EnvironmentCloudEndpointUnavailableError"); - assert.equal(relayConfigBody.message, "Managed endpoint runtime could not be started."); - assert.equal(relayConfigBody.endpointRuntimeStatus?.status, "failed"); - assert.equal(relayConfigBody.endpointRuntimeStatus?.reason, "cloudflared missing"); - - const now = yield* DateTime.now; - const healthRequest = makeCloudEnvironmentHealthRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - nonce: "cloud-health-after-failed-runtime", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }); - const healthUrl = yield* getHttpServerUrl("/api/t3-connect/health"); - const healthResponse = yield* fetchEffect(healthUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody(healthRequest), - }); - const healthBody = yield* responseJsonEffect<{ - _tag?: string; - message?: string; - }>(healthResponse); - assert.equal(healthResponse.status, 500); - assert.equal(healthBody._tag, "EnvironmentHttpInternalServerError"); - assert.equal( - healthBody.message, - "Cloud mint public key is not installed for this environment.", - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects cloud mint requests with the wrong issuer or audience", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test/", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const mintUrl = yield* getHttpServerUrl("/api/connect/mint-credential"); - const postMint = (request: ReturnType) => - fetchEffect(mintUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody(request), - }); - - const wrongIssuer = yield* postMint( - makeCloudMintCredentialRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - clientProofKeyThumbprint: "client-proof-key-thumbprint", - issuer: "https://attacker.example.test", - jti: "cloud-mint-jti-wrong-issuer", - nonce: "cloud-mint-nonce-wrong-issuer", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }), - ); - const wrongAudience = yield* postMint( - makeCloudMintCredentialRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - clientProofKeyThumbprint: "client-proof-key-thumbprint", - audience: "t3-env:other-environment", - jti: "cloud-mint-jti-wrong-audience", - nonce: "cloud-mint-nonce-wrong-audience", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }), - ); - - assert.equal(wrongIssuer.status, 401); - assert.equal(wrongAudience.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects cloud mint requests for a cloud subject other than the linked user", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test/", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const mintUrl = yield* getHttpServerUrl("/api/t3-connect/mint-credential"); - const response = yield* fetchEffect(mintUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody( - makeCloudMintCredentialRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - clientProofKeyThumbprint: "client-proof-key-thumbprint", - subject: "user_other", - jti: "cloud-mint-jti-wrong-subject", - nonce: "cloud-mint-nonce-wrong-subject", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }), - ), - }); - - assert.equal(response.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects cloud mint requests without the exact connect scope", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test/", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const mintUrl = yield* getHttpServerUrl("/api/t3-connect/mint-credential"); - const response = yield* fetchEffect(mintUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody( - makeCloudMintCredentialRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - clientProofKeyThumbprint: "client-proof-key-thumbprint", - jti: "cloud-mint-jti-duplicate-scope", - nonce: "cloud-mint-nonce-duplicate-scope", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - scope: ["environment:connect", "environment:connect"], - }), - ), - }); - - assert.equal(response.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects cloud health requests with the wrong issuer or audience", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test/", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const healthUrl = yield* getHttpServerUrl("/api/t3-connect/health"); - const postHealth = (request: ReturnType) => - fetchEffect(healthUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody(request), - }); - - const wrongIssuer = yield* postHealth( - makeCloudEnvironmentHealthRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - issuer: "https://attacker.example.test", - jti: "cloud-health-jti-wrong-issuer", - nonce: "cloud-health-nonce-wrong-issuer", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }), - ); - const wrongAudience = yield* postHealth( - makeCloudEnvironmentHealthRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - audience: "t3-env:other-environment", - jti: "cloud-health-jti-wrong-audience", - nonce: "cloud-health-nonce-wrong-audience", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }), - ); - - assert.equal(wrongIssuer.status, 401); - assert.equal(wrongAudience.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects cloud health requests for a cloud subject other than the linked user", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test/", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const healthUrl = yield* getHttpServerUrl("/api/t3-connect/health"); - const response = yield* fetchEffect(healthUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody( - makeCloudEnvironmentHealthRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - subject: "user_other", - jti: "cloud-health-jti-wrong-subject", - nonce: "cloud-health-nonce-wrong-subject", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - }), - ), - }); - - assert.equal(response.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects cloud health requests without the exact status scope", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", { - privateKeyEncoding: { format: "pem", type: "pkcs8" }, - publicKeyEncoding: { format: "pem", type: "spki" }, - }); - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const relayConfigUrl = yield* getHttpServerUrl("/api/connect/relay-config"); - const relayConfigResponse = yield* fetchEffect(relayConfigUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - relayUrl: "https://relay.example.test/", - cloudUserId: "user_123", - environmentCredential: "t3env_test_credential", - cloudMintPublicKey: cloudKeyPair.publicKey, - endpointRuntime: null, - }), - }); - assert.equal(relayConfigResponse.status, 200); - - const now = yield* DateTime.now; - const healthUrl = yield* getHttpServerUrl("/api/t3-connect/health"); - const response = yield* fetchEffect(healthUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: jsonRequestBody( - makeCloudEnvironmentHealthRequest({ - privateKey: cloudKeyPair.privateKey, - environmentId: testEnvironmentDescriptor.environmentId, - jti: "cloud-health-jti-duplicate-scope", - nonce: "cloud-health-nonce-duplicate-scope", - issuedAt: DateTime.formatIso(now), - expiresAt: DateTime.formatIso(DateTime.add(now, { minutes: 5 })), - scope: ["environment:status", "environment:status"], - }), - ), - }); - - assert.equal(response.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("issues short-lived websocket tickets for authenticated bearer sessions", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const bearerToken = yield* getAuthenticatedBearerSessionToken(); - const wsTicketUrl = yield* getHttpServerUrl("/api/auth/websocket-ticket"); - const wsTicketResponse = yield* fetchEffect(wsTicketUrl, { - method: "POST", - headers: { - authorization: `Bearer ${bearerToken}`, - }, - }); - const wsTicketBody = yield* responseJsonEffect<{ - readonly ticket: string; - readonly expiresAt: string; - }>(wsTicketResponse); - - assert.equal(wsTicketResponse.status, 200); - assert.equal(typeof wsTicketBody.ticket, "string"); - assert.isTrue(wsTicketBody.ticket.length > 0); - assert.equal(typeof wsTicketBody.expiresAt, "string"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("does not allow management-only access tokens to operate the environment", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const { response: exchangeResponse, body: tokenBody } = yield* exchangeAccessToken( - defaultDesktopBootstrapToken, - { scope: "access:write" }, - ); - assert.equal(exchangeResponse.status, 200); - assert.equal(tokenBody.scope, "access:write"); - assert.isDefined(tokenBody.access_token); - - const overbroadPairingResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - authorization: `Bearer ${tokenBody.access_token ?? ""}`, - }, - body: yield* HttpBody.json({}), - }); - const overbroadPairingBody = (yield* overbroadPairingResponse.json) as { - readonly requiredScope: string; - }; - const pairingResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - authorization: `Bearer ${tokenBody.access_token ?? ""}`, - }, - body: yield* HttpBody.json({ scopes: ["access:write"] }), - }); - const wsTicketResponse = yield* HttpClient.post("/api/auth/websocket-ticket", { - headers: { - authorization: `Bearer ${tokenBody.access_token ?? ""}`, - }, - }); - const wsTicketBody = (yield* wsTicketResponse.json) as { readonly ticket: string }; - assert.equal(overbroadPairingResponse.status, 403); - assert.equal(overbroadPairingBody.requiredScope, "orchestration:read"); - assert.equal(pairingResponse.status, 200); - assert.equal(wsTicketResponse.status, 200); - const wsUrl = `${yield* getWsServerUrl("/ws", { authenticated: false })}?wsTicket=${encodeURIComponent(wsTicketBody.ticket)}`; - const rpcError = yield* Effect.flip( - Effect.scoped(withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverGetConfig]({}))), - ); - assert.equal(rpcError._tag, "EnvironmentAuthorizationError"); - if (rpcError._tag === "EnvironmentAuthorizationError") { - assert.equal(rpcError.requiredScope, "orchestration:read"); - } - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("includes CORS headers on remote auth success responses", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const origin = crossOriginClientOrigin; - const { response: tokenResponse, body: tokenBody } = yield* exchangeAccessToken( - defaultDesktopBootstrapToken, - { - headers: { origin }, - }, - ); - - assert.equal(tokenResponse.status, 200); - assertBrowserApiCorsResponseHeaders(tokenResponse.headers); - assert.equal(tokenBody.token_type, "Bearer"); - assert.equal(typeof tokenBody.access_token, "string"); - - const sessionUrl = yield* getHttpServerUrl("/api/auth/session"); - const sessionResponse = yield* fetchEffect(sessionUrl, { - headers: { - authorization: `Bearer ${tokenBody.access_token ?? ""}`, - origin, - }, - }); - const sessionBody = yield* responseJsonEffect<{ - readonly authenticated: boolean; - readonly sessionMethod?: string; - }>(sessionResponse); - - assert.equal(sessionResponse.status, 200); - assertBrowserApiCorsResponseHeaders(sessionResponse.headers); - assert.equal(sessionBody.authenticated, true); - assert.equal(sessionBody.sessionMethod, "bearer-access-token"); - - const wsTicketUrl = yield* getHttpServerUrl("/api/auth/websocket-ticket"); - const wsTicketResponse = yield* fetchEffect(wsTicketUrl, { - method: "POST", - headers: { - authorization: `Bearer ${tokenBody.access_token ?? ""}`, - origin, - }, - }); - const wsTicketBody = yield* responseJsonEffect<{ - readonly ticket: string; - }>(wsTicketResponse); - - assert.equal(wsTicketResponse.status, 200); - assertBrowserApiCorsResponseHeaders(wsTicketResponse.headers); - assert.equal(typeof wsTicketBody.ticket, "string"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "responds to remote auth websocket-ticket preflight requests with authorization CORS headers", - () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const wsTicketUrl = yield* getHttpServerUrl("/api/auth/websocket-ticket"); - const response = yield* fetchEffect(wsTicketUrl, { - method: "OPTIONS", - headers: { - origin: crossOriginClientOrigin, - "access-control-request-method": "POST", - "access-control-request-headers": "authorization", - }, - }); - - assert.equal(response.status, 204); - assertBrowserApiCorsPreflightHeaders(response.headers); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("allows credentialed cloud link proof preflights from the configured dev UI", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - config: { devUrl: new URL(crossOriginClientOrigin) }, - }); - - const linkProofUrl = yield* getHttpServerUrl("/api/connect/link-proof"); - const response = yield* fetchEffect(linkProofUrl, { - method: "OPTIONS", - headers: { - origin: crossOriginClientOrigin, - "access-control-request-method": "POST", - "access-control-request-headers": "content-type", - }, - }); - - assert.equal(response.status, 204); - assertBrowserApiCorsPreflightHeaders(response.headers, { - origin: crossOriginClientOrigin, - credentials: true, - }); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("includes CORS headers on remote websocket-ticket auth failures", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const wsTicketUrl = yield* getHttpServerUrl("/api/auth/websocket-ticket"); - const response = yield* fetchEffect(wsTicketUrl, { - method: "POST", - headers: { - origin: crossOriginClientOrigin, - }, - }); - const body = yield* responseJsonEffect<{ - readonly _tag?: string; - readonly code?: string; - readonly reason?: string; - readonly traceId?: string; - }>(response); - - assert.equal(response.status, 401); - assertBrowserApiCorsResponseHeaders(response.headers); - assert.equal(body._tag, "EnvironmentAuthInvalidError"); - assert.equal(body.code, "auth_invalid"); - assert.equal(body.reason, "missing_credential"); - assert.equal(typeof body.traceId, "string"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("issues authenticated one-time pairing credentials for additional clients", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const response = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - }, - body: yield* HttpBody.json({}), - }); - const body = (yield* response.json) as { - readonly credential: string; - readonly expiresAt: string; - }; - - assert.equal(response.status, 200); - assert.equal(typeof body.credential, "string"); - assert.isTrue(body.credential.length > 0); - assert.equal(typeof body.expiresAt, "string"); - - const bootstrapResult = yield* bootstrapBrowserSession(body.credential); - assert.equal(bootstrapResult.response.status, 200); - - const reusedResult = yield* bootstrapBrowserSession(body.credential); - assert.equal(reusedResult.response.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("issues pairing credentials for bearer sessions with access management scope", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const bearerToken = yield* getAuthenticatedBearerSessionToken(); - const response = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - authorization: `Bearer ${bearerToken}`, - }, - body: yield* HttpBody.json({ label: "Hosted web" }), - }); - const body = (yield* response.json) as { - readonly credential: string; - readonly label?: string; - }; - - assert.equal(response.status, 200); - assert.isTrue(body.credential.length > 0); - assert.equal(body.label, "Hosted web"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects pairing credentials with an empty scope grant", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const response = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - }, - body: yield* HttpBody.json({ scopes: [] }), - }); - const body = (yield* response.json) as { - readonly code: string; - readonly reason: string; - }; - - assert.equal(response.status, 400); - assert.equal(body.code, "invalid_request"); - assert.equal(body.reason, "invalid_scope"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects unauthenticated pairing credential requests", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const response = yield* HttpClient.post("/api/auth/pairing-token", { - body: yield* HttpBody.json({}), - }); - assert.equal(response.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("lists and revokes pairing links for access management sessions", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - config: { - host: "0.0.0.0", - }, - }); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const createdResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: ownerCookie, - }, - body: yield* HttpBody.json({}), - }); - const createdBody = (yield* createdResponse.json) as { - readonly id: string; - readonly credential: string; - }; - - const listResponse = yield* HttpClient.get("/api/auth/pairing-links", { - headers: { - cookie: ownerCookie, - }, - }); - const listedLinks = (yield* listResponse.json) as ReadonlyArray<{ - readonly id: string; - readonly credential: string; - }>; - - const revokeResponse = yield* HttpClient.post("/api/auth/pairing-links/revoke", { - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: HttpBody.text(jsonRequestBody({ id: createdBody.id }), "application/json"), - }); - const revokedBootstrap = yield* bootstrapBrowserSession(createdBody.credential); - - assert.equal(createdResponse.status, 200); - assert.equal(listResponse.status, 200); - assert.isTrue(listedLinks.some((entry) => entry.id === createdBody.id)); - assert.equal(revokeResponse.status, 200); - assert.equal(revokedBootstrap.response.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects pairing credential requests without access management scope", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - config: { - host: "0.0.0.0", - }, - }); - - const ownerResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - }, - body: yield* HttpBody.json({}), - }); - const ownerBody = (yield* ownerResponse.json) as { - readonly credential: string; - }; - assert.equal(ownerResponse.status, 200); - - const pairedSessionCookie = yield* getAuthenticatedSessionCookieHeader(ownerBody.credential); - const pairedResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: pairedSessionCookie, - }, - body: yield* HttpBody.json({}), - }); - const pairedBody = (yield* pairedResponse.json) as { - readonly _tag: string; - readonly code: string; - readonly requiredScope: string; - readonly traceId: string; - }; - - assert.equal(pairedResponse.status, 403); - assert.equal(pairedBody._tag, "EnvironmentScopeRequiredError"); - assert.equal(pairedBody.code, "insufficient_scope"); - assert.equal(pairedBody.requiredScope, "access:write"); - assert.equal(typeof pairedBody.traceId, "string"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("lists paired clients and revokes other sessions while keeping the administrator", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - config: { - host: "0.0.0.0", - }, - }); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const pairingTokenUrl = yield* getHttpServerUrl("/api/auth/pairing-token"); - const ownerPairingResponse = yield* fetchEffect(pairingTokenUrl, { - method: "POST", - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: jsonRequestBody({ - label: "Julius iPhone", - }), - }); - const ownerPairingBody = yield* responseJsonEffect<{ - readonly credential: string; - readonly label?: string; - }>(ownerPairingResponse); - assert.equal(ownerPairingResponse.status, 200); - const pairedSessionBootstrap = yield* bootstrapBrowserSession(ownerPairingBody.credential, { - headers: { - "user-agent": - "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", - }, - }); - const pairedSessionCookie = pairedSessionBootstrap.cookie?.split(";")[0]; - assert.isDefined(pairedSessionCookie); - - const pairedSessionCookieHeader = pairedSessionCookie ?? ""; - const listBeforeResponse = yield* HttpClient.get("/api/auth/clients", { - headers: { - cookie: ownerCookie, - }, - }); - const clientsBefore = (yield* listBeforeResponse.json) as ReadonlyArray<{ - readonly sessionId: string; - readonly current: boolean; - readonly client: { - readonly label?: string; - readonly deviceType: string; - readonly ipAddress?: string; - readonly os?: string; - readonly browser?: string; - }; - }>; - const pairedClientBefore = clientsBefore.find((entry) => !entry.current); - const pairedSessionId = clientsBefore.find((entry) => !entry.current)?.sessionId; - - const revokeOthersResponse = yield* HttpClient.post("/api/auth/clients/revoke-others", { - headers: { - cookie: ownerCookie, - }, - }); - const revokeOthersBody = (yield* revokeOthersResponse.json) as { - readonly revokedCount: number; - }; - - const listAfterResponse = yield* HttpClient.get("/api/auth/clients", { - headers: { - cookie: ownerCookie, - }, - }); - const clientsAfter = (yield* listAfterResponse.json) as ReadonlyArray<{ - readonly sessionId: string; - readonly current: boolean; - }>; - - const pairedClientPairingResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: pairedSessionCookieHeader, - }, - body: yield* HttpBody.json({}), - }); - const pairedClientPairingBody = (yield* pairedClientPairingResponse.json) as { - readonly _tag: string; - readonly code: string; - readonly reason: string; - readonly traceId: string; - }; - - assert.equal(listBeforeResponse.status, 200); - assert.equal(ownerPairingBody.label, "Julius iPhone"); - assert.lengthOf(clientsBefore, 2); - assert.isDefined(pairedSessionId); - assert.isDefined(pairedClientBefore); - assert.deepInclude(pairedClientBefore?.client, { - label: "Julius iPhone", - deviceType: "mobile", - os: "iOS", - browser: "Safari", - ipAddress: "127.0.0.1", - }); - assert.equal(revokeOthersResponse.status, 200); - assert.equal(revokeOthersBody.revokedCount, 1); - assert.equal(listAfterResponse.status, 200); - assert.lengthOf(clientsAfter, 1); - assert.equal(clientsAfter[0]?.current, true); - assert.equal(pairedClientPairingResponse.status, 401); - assert.equal(pairedClientPairingBody._tag, "EnvironmentAuthInvalidError"); - assert.equal(pairedClientPairingBody.code, "auth_invalid"); - assert.equal(pairedClientPairingBody.reason, "invalid_credential"); - assert.equal(typeof pairedClientPairingBody.traceId, "string"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("separates access inventory reads from credential management writes", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - config: { - host: "0.0.0.0", - }, - }); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const issueScopedSession = Effect.fnUntraced(function* ( - scope: "access:read" | "access:write", - ) { - const pairingResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: ownerCookie, - }, - body: yield* HttpBody.json({ scopes: [scope] }), - }); - assert.equal(pairingResponse.status, 200); - const pairingBody = (yield* pairingResponse.json) as { - readonly credential: string; - }; - return yield* getAuthenticatedSessionCookieHeader(pairingBody.credential); - }); - - const readCookie = yield* issueScopedSession("access:read"); - const readListResponse = yield* HttpClient.get("/api/auth/clients", { - headers: { - cookie: readCookie, - }, - }); - const readWriteResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: readCookie, - }, - body: yield* HttpBody.json({}), - }); - const readWriteBody = (yield* readWriteResponse.json) as { - readonly requiredScope: string; - }; - - const writeCookie = yield* issueScopedSession("access:write"); - const writeListResponse = yield* HttpClient.get("/api/auth/clients", { - headers: { - cookie: writeCookie, - }, - }); - const writeListBody = (yield* writeListResponse.json) as { - readonly requiredScope: string; - }; - - assert.equal(readListResponse.status, 200); - assert.equal(readWriteResponse.status, 403); - assert.equal(readWriteBody.requiredScope, "access:write"); - assert.equal(writeListResponse.status, 403); - assert.equal(writeListBody.requiredScope, "access:read"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("revokes an individual paired client session", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - config: { - host: "0.0.0.0", - }, - }); - - const ownerCookie = yield* getAuthenticatedSessionCookieHeader(); - const pairingResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: ownerCookie, - }, - body: yield* HttpBody.json({}), - }); - const pairingBody = (yield* pairingResponse.json) as { - readonly credential: string; - }; - const pairedSessionCookie = yield* getAuthenticatedSessionCookieHeader( - pairingBody.credential, - ); - - const clientsResponse = yield* HttpClient.get("/api/auth/clients", { - headers: { - cookie: ownerCookie, - }, - }); - const clients = (yield* clientsResponse.json) as ReadonlyArray<{ - readonly sessionId: string; - readonly current: boolean; - }>; - const pairedSessionId = clients.find((entry) => !entry.current)?.sessionId; - assert.isDefined(pairedSessionId); - - const revokeResponse = yield* HttpClient.post("/api/auth/clients/revoke", { - headers: { - cookie: ownerCookie, - "content-type": "application/json", - }, - body: HttpBody.text(jsonRequestBody({ sessionId: pairedSessionId }), "application/json"), - }); - const pairedClientPairingResponse = yield* HttpClient.post("/api/auth/pairing-token", { - headers: { - cookie: pairedSessionCookie, - }, - body: yield* HttpBody.json({}), - }); - - assert.equal(revokeResponse.status, 200); - assert.equal(pairedClientPairingResponse.status, 401); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects reusing the same bootstrap credential after it has been exchanged", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const first = yield* bootstrapBrowserSession(); - const second = yield* bootstrapBrowserSession(); - - assert.equal(first.response.status, 200); - assert.equal(second.response.status, 401); - assert.equal((second.body as { readonly _tag?: string })._tag, "EnvironmentAuthInvalidError"); - assert.equal((second.body as { readonly code?: string }).code, "auth_invalid"); - assert.equal((second.body as { readonly reason?: string }).reason, "invalid_credential"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("accepts websocket rpc handshake with a bootstrapped browser session cookie", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const { response: bootstrapResponse, cookie } = yield* bootstrapBrowserSession(); - - assert.equal(bootstrapResponse.status, 200); - assert.isDefined(cookie); - - const wsUrl = appendSessionCookieToWsUrl( - yield* getWsServerUrl("/ws", { authenticated: false }), - cookie?.split(";")[0] ?? "", - ); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverGetConfig]({})), - ); - - assert.equal(response.environment.environmentId, testEnvironmentDescriptor.environmentId); - assert.equal(response.auth.policy, "desktop-managed-local"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "rejects websocket rpc handshake when a session token is only provided via query string", - () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const { cookie } = yield* bootstrapBrowserSession(); - assert.isDefined(cookie); - const sessionToken = extractSessionTokenFromSetCookie(cookie ?? ""); - const wsUrl = `${yield* getWsServerUrl("/ws", { authenticated: false })}?token=${encodeURIComponent(sessionToken)}`; - - const error = yield* Effect.flip( - Effect.scoped(withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverGetConfig]({}))), - ); - - assert.equal(error._tag, "RpcClientError"); - assertInclude(String(error), "SocketOpenError"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "accepts websocket rpc handshake with a dedicated websocket ticket in the query string", - () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const bearerToken = yield* getAuthenticatedBearerSessionToken(); - const wsTicketUrl = yield* getHttpServerUrl("/api/auth/websocket-ticket"); - const wsTicketResponse = yield* fetchEffect(wsTicketUrl, { - method: "POST", - headers: { - authorization: `Bearer ${bearerToken}`, - }, - }); - const wsTicketBody = yield* responseJsonEffect<{ - readonly ticket: string; - }>(wsTicketResponse); - const wsUrl = `${yield* getWsServerUrl("/ws", { authenticated: false })}?wsTicket=${encodeURIComponent(wsTicketBody.ticket)}`; - - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverGetConfig]({})), - ); - - assert.equal(response.environment.environmentId, testEnvironmentDescriptor.environmentId); - assert.equal(response.auth.policy, "desktop-managed-local"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("proxies browser OTLP trace exports through the server", () => - Effect.gen(function* () { - const upstreamRequests: Array<{ - readonly body: string; - readonly contentType: string | null; - }> = []; - const localTraceRecords: Array = []; - const payload = { - resourceSpans: [ - { - resource: { - attributes: [ - { - key: "service.name", - value: { stringValue: "t3-web" }, - }, - ], - }, - scopeSpans: [ - { - scope: { - name: "effect", - version: "4.0.0-beta.43", - }, - spans: [ - { - traceId: "11111111111111111111111111111111", - spanId: "2222222222222222", - parentSpanId: "3333333333333333", - name: "RpcClient.server.getSettings", - kind: 3, - startTimeUnixNano: "1000000", - endTimeUnixNano: "2000000", - attributes: [ - { - key: "rpc.method", - value: { stringValue: "server.getSettings" }, - }, - ], - events: [ - { - name: "http.request", - timeUnixNano: "1500000", - attributes: [ - { - key: "http.status_code", - value: { intValue: "200" }, - }, - ], - }, - ], - links: [], - status: { - code: "STATUS_CODE_OK", - }, - flags: 1, - }, - ], - }, - ], - }, - ], - }; - - const collector = yield* Effect.acquireRelease( - Effect.promise(async () => { - const NodeHttp = await import("node:http"); - - return await new Promise<{ - readonly close: () => Promise; - readonly url: string; - }>((resolve, reject) => { - const server = NodeHttp.createServer((request, response) => { - const chunks: Buffer[] = []; - request.on("data", (chunk) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - request.on("end", () => { - upstreamRequests.push({ - body: Buffer.concat(chunks).toString("utf8"), - contentType: request.headers["content-type"] ?? null, - }); - response.statusCode = 204; - response.end(); - }); - }); - - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (!address || typeof address === "string") { - reject(new Error("Expected TCP collector address")); - return; - } - - resolve({ - url: `http://127.0.0.1:${address.port}/v1/traces`, - close: () => - new Promise((resolveClose, rejectClose) => { - server.close((error) => { - if (error) { - rejectClose(error); - return; - } - resolveClose(); - }); - }), - }); - }); - }); - }), - ({ close }) => Effect.promise(close), - ); - - yield* buildAppUnderTest({ - config: { - otlpTracesUrl: collector.url, - }, - layers: { - browserTraceCollector: { - record: (records) => - Effect.sync(() => { - localTraceRecords.push(...records); - }), - }, - }, - }); - - const response = yield* HttpClient.post("/api/observability/v1/traces", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - "content-type": "application/json", - origin: "http://localhost:5733", - }, - // @effect-diagnostics-next-line preferSchemaOverJson:off - body: HttpBody.text(JSON.stringify(payload), "application/json"), - }); - - assert.equal(response.status, 204); - assert.equal(response.headers["access-control-allow-origin"], "*"); - assert.deepEqual(localTraceRecords, [ - { - type: "otlp-span", - name: "RpcClient.server.getSettings", - traceId: "11111111111111111111111111111111", - spanId: "2222222222222222", - parentSpanId: "3333333333333333", - sampled: true, - kind: "client", - startTimeUnixNano: "1000000", - endTimeUnixNano: "2000000", - durationMs: 1, - attributes: { - "rpc.method": "server.getSettings", - }, - resourceAttributes: { - "service.name": "t3-web", - }, - scope: { - name: "effect", - version: "4.0.0-beta.43", - attributes: {}, - }, - events: [ - { - name: "http.request", - timeUnixNano: "1500000", - attributes: { - "http.status_code": "200", - }, - }, - ], - links: [], - status: { - code: "STATUS_CODE_OK", - }, - }, - ]); - assert.deepEqual(upstreamRequests, [ - { - body: jsonRequestBody(payload), - contentType: "application/json", - }, - ]); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("responds to browser OTLP trace preflight requests with CORS headers", () => - Effect.gen(function* () { - yield* buildAppUnderTest(); - - const response = yield* HttpClient.options("/api/observability/v1/traces", { - headers: { - origin: "http://localhost:5733", - "access-control-request-method": "POST", - "access-control-request-headers": "content-type", - }, - }); - - assert.equal(response.status, 204); - assert.equal(response.headers["access-control-allow-origin"], "*"); - assert.deepEqual(splitHeaderTokens(response.headers["access-control-allow-methods"]), [ - "GET", - "OPTIONS", - "POST", - ]); - assert.deepEqual(splitHeaderTokens(response.headers["access-control-allow-headers"]), [ - "authorization", - "b3", - "content-type", - "dpop", - "traceparent", - ]); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "stores browser OTLP trace exports locally when no upstream collector is configured", - () => - Effect.gen(function* () { - const localTraceRecords: Array = []; - const payload = yield* makeBrowserOtlpPayload("client.test"); - const resourceSpan = payload.resourceSpans[0]; - const scopeSpan = resourceSpan?.scopeSpans[0]; - const span = scopeSpan?.spans[0]; - - assert.notEqual(resourceSpan, undefined); - assert.notEqual(scopeSpan, undefined); - assert.notEqual(span, undefined); - if (!resourceSpan || !scopeSpan || !span) { - return; - } - - yield* buildAppUnderTest({ - layers: { - browserTraceCollector: { - record: (records) => - Effect.sync(() => { - localTraceRecords.push(...records); - }), - }, - }, - }); - - const response = yield* HttpClient.post("/api/observability/v1/traces", { - headers: { - cookie: yield* getAuthenticatedSessionCookieHeader(), - "content-type": "application/json", - }, - // @effect-diagnostics-next-line preferSchemaOverJson:off - body: HttpBody.text(JSON.stringify(payload), "application/json"), - }); - - assert.equal(response.status, 204); - assert.equal(localTraceRecords.length, 1); - const record = localTraceRecords[0] as { - readonly type: string; - readonly name: string; - readonly traceId: string; - readonly spanId: string; - readonly kind: string; - readonly attributes: Readonly>; - readonly events: ReadonlyArray; - readonly links: ReadonlyArray; - readonly scope: { - readonly name?: string; - readonly attributes: Readonly>; - }; - readonly resourceAttributes: Readonly>; - readonly status?: { - readonly code?: string; - }; - }; - - assert.equal(record.type, "otlp-span"); - assert.equal(record.name, span.name); - assert.equal(record.traceId, span.traceId); - assert.equal(record.spanId, span.spanId); - assert.equal(record.kind, "internal"); - assert.deepEqual(record.attributes, {}); - assert.deepEqual(record.events, []); - assert.deepEqual(record.links, []); - assert.equal(record.scope.name, scopeSpan.scope.name); - assert.deepEqual(record.scope.attributes, {}); - assert.equal(record.resourceAttributes["service.name"], "t3-web"); - assert.equal(record.status?.code, String(span.status.code)); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc server.upsertKeybinding", () => - Effect.gen(function* () { - const rule: KeybindingRule = { - command: "terminal.toggle", - key: "ctrl+k", - }; - const resolved: ResolvedKeybindingRule = { - command: "terminal.toggle", - shortcut: { - key: "k", - metaKey: false, - ctrlKey: true, - shiftKey: false, - altKey: false, - modKey: true, - }, - }; - - yield* buildAppUnderTest({ - layers: { - keybindings: { - upsertKeybindingRule: () => Effect.succeed([resolved]), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverUpsertKeybinding](rule)), - ); - - assert.deepEqual(response.issues, []); - assert.deepEqual(response.keybindings, [resolved]); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc server.removeKeybinding", () => - Effect.gen(function* () { - const rule: KeybindingRule = { - command: "terminal.toggle", - key: "ctrl+k", - }; - const resolved: ResolvedKeybindingRule = { - command: "terminal.toggle", - shortcut: { - key: "j", - metaKey: false, - ctrlKey: false, - shiftKey: false, - altKey: false, - modKey: true, - }, - }; - - yield* buildAppUnderTest({ - layers: { - keybindings: { - removeKeybindingRule: () => Effect.succeed([resolved]), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverRemoveKeybinding](rule)), - ); - - assert.deepEqual(response.issues, []); - assert.deepEqual(response.keybindings, [resolved]); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("rejects websocket rpc handshake when session authentication is missing", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-auth-required-" }); - yield* fs.writeFileString( - path.join(workspaceDir, "needle-file.ts"), - "export const needle = 1;", - ); - - yield* buildAppUnderTest(); - - const wsUrl = yield* getWsServerUrl("/ws", { authenticated: false }); - const result = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.projectsSearchEntries]({ - cwd: workspaceDir, - query: "needle", - limit: 10, - }), - ).pipe(Effect.result), - ); - - assertTrue(result._tag === "Failure"); - const failureMessage = String(result.failure); - assertTrue( - failureMessage.includes("SocketOpenError") || failureMessage.includes("SocketCloseError"), - ); - assertTrue( - failureMessage.includes("Unauthorized") || - failureMessage.includes("An error occurred during Open"), - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc subscribeServerConfig streams snapshot then update", () => - Effect.gen(function* () { - const providers = [ - { - instanceId: ProviderInstanceId.make("codex"), - driver: ProviderDriverKind.make("codex"), - enabled: true, - installed: true, - version: "1.0.0", - status: "ready" as const, - auth: { status: "authenticated" as const }, - checkedAt: "2026-04-11T00:00:00.000Z", - models: [], - slashCommands: [], - skills: [], - }, - ] as const; - const changeEvent = { - keybindings: [], - issues: [], - } as const; - - yield* buildAppUnderTest({ - config: { - otlpTracesUrl: "http://localhost:4318/v1/traces", - otlpMetricsUrl: "http://localhost:4318/v1/metrics", - }, - layers: { - keybindings: { - loadConfigState: Effect.succeed({ - keybindings: [], - issues: [], - }), - streamChanges: Stream.succeed(changeEvent), - }, - providerRegistry: { - getProviders: Effect.succeed(providers), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const events = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.subscribeServerConfig]({}).pipe(Stream.take(2), Stream.runCollect), - ), - ); - - const [first, second] = Array.from(events); - assert.equal(first?.type, "snapshot"); - if (first?.type === "snapshot") { - assert.equal(first.version, 1); - assert.deepEqual(first.config.keybindings, []); - assert.deepEqual(first.config.issues, []); - assert.deepEqual(first.config.providers, providers); - assert.equal(first.config.observability.logsDirectoryPath.endsWith("/logs"), true); - assert.equal(first.config.observability.localTracingEnabled, true); - assert.equal(first.config.observability.otlpTracesUrl, "http://localhost:4318/v1/traces"); - assert.equal(first.config.observability.otlpTracesEnabled, true); - assert.equal(first.config.observability.otlpMetricsUrl, "http://localhost:4318/v1/metrics"); - assert.equal(first.config.observability.otlpMetricsEnabled, true); - assert.deepEqual(first.config.settings, DEFAULT_SERVER_SETTINGS); - } - assert.deepEqual(second, { - version: 1, - type: "keybindingsUpdated", - payload: { keybindings: [], issues: [] }, - }); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc subscribeServerConfig emits provider status updates", () => - Effect.gen(function* () { - const nextProviders = [ - { - instanceId: ProviderInstanceId.make("codex"), - driver: ProviderDriverKind.make("codex"), - enabled: true, - installed: true, - version: "1.0.0", - status: "ready" as const, - auth: { status: "authenticated" as const }, - checkedAt: "2026-04-11T00:00:00.000Z", - models: [], - slashCommands: [], - skills: [], - }, - ] as const; - - yield* buildAppUnderTest({ - layers: { - keybindings: { - loadConfigState: Effect.succeed({ - keybindings: [], - issues: [], - }), - streamChanges: Stream.empty, - }, - providerRegistry: { - getProviders: Effect.succeed([]), - streamChanges: Stream.succeed(nextProviders), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const events = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.subscribeServerConfig]({}).pipe(Stream.take(2), Stream.runCollect), - ), - ); - - const [first, second] = Array.from(events); - assert.equal(first?.type, "snapshot"); - if (first?.type === "snapshot") { - assert.deepEqual(first.config.providers, []); - } - assert.deepEqual(second, { - version: 1, - type: "providerStatuses", - payload: { providers: nextProviders }, - }); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "routes websocket rpc subscribeServerLifecycle replays snapshot and streams updates", - () => - Effect.gen(function* () { - const lifecycleEvents = [ - { - version: 1 as const, - sequence: 1, - type: "welcome" as const, - payload: { - environment: testEnvironmentDescriptor, - cwd: "/tmp/project", - projectName: "project", - }, - }, - ] as const; - const liveEvents = Stream.make({ - version: 1 as const, - sequence: 2, - type: "ready" as const, - payload: { at: "2026-01-01T00:00:00.000Z", environment: testEnvironmentDescriptor }, - }); - - yield* buildAppUnderTest({ - layers: { - serverLifecycleEvents: { - snapshot: Effect.succeed({ - sequence: 1, - events: lifecycleEvents, - }), - stream: liveEvents, - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const events = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.subscribeServerLifecycle]({}).pipe(Stream.take(2), Stream.runCollect), - ), - ); - - const [first, second] = Array.from(events); - assert.equal(first?.type, "welcome"); - assert.equal(first?.sequence, 1); - assert.equal(second?.type, "ready"); - assert.equal(second?.sequence, 2); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc projects.searchEntries", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-project-search-" }); - yield* fs.writeFileString( - path.join(workspaceDir, "needle-file.ts"), - "export const needle = 1;", - ); - - yield* buildAppUnderTest(); - - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.projectsSearchEntries]({ - cwd: workspaceDir, - query: "needle", - limit: 10, - }), - ), - ); - - assert.isAtLeast(response.entries.length, 1); - assert.isTrue(response.entries.some((entry) => entry.path === "needle-file.ts")); - assert.equal(response.truncated, false); - }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive), - ); - - it.effect("routes websocket rpc projects.listEntries and projects.readFile", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-project-files-" }); - yield* fs.makeDirectory(path.join(workspaceDir, "src"), { recursive: true }); - yield* fs.writeFileString( - path.join(workspaceDir, "src", "index.ts"), - "export const answer = 42;\n", - ); - - yield* buildAppUnderTest(); - - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - Effect.all({ - listing: client[WS_METHODS.projectsListEntries]({ cwd: workspaceDir }), - file: client[WS_METHODS.projectsReadFile]({ - cwd: workspaceDir, - relativePath: "src/index.ts", - }), - }), - ), - ); - - assert.isTrue(response.listing.entries.some((entry) => entry.path === "src/index.ts")); - assert.deepEqual(response.file, { - relativePath: "src/index.ts", - contents: "export const answer = 42;\n", - byteLength: 26, - truncated: false, - }); - }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive), - ); - - it.effect("routes websocket rpc projects.searchEntries excludes gitignored files", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspaceDir = yield* fs.makeTempDirectoryScoped({ - prefix: "t3-ws-project-search-gitignored-", - }); - yield* fs.writeFileString(path.join(workspaceDir, ".gitignore"), ".venv/\n"); - yield* fs.makeDirectory(path.join(workspaceDir, ".venv", "lib"), { recursive: true }); - yield* fs.writeFileString( - path.join(workspaceDir, ".venv", "lib", "ignored-search-target.ts"), - "export const ignored = true;", - ); - yield* fs.makeDirectory(path.join(workspaceDir, "src"), { recursive: true }); - yield* fs.writeFileString( - path.join(workspaceDir, "src", "tracked.ts"), - "export const ok = 1;", - ); - - yield* buildAppUnderTest({ - layers: { - vcsDriver: { - isInsideWorkTree: () => Effect.succeed(true), - listWorkspaceFiles: () => - Effect.succeed({ - paths: ["src/tracked.ts"], - truncated: false, - freshness: { - source: "live-local", - observedAt: TEST_EPOCH, - expiresAt: Option.none(), - }, - }), - filterIgnoredPaths: (_cwd, relativePaths) => - Effect.succeed( - relativePaths.filter((relativePath) => !relativePath.startsWith(".venv/")), - ), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.projectsSearchEntries]({ - cwd: workspaceDir, - query: "ignored-search-target", - limit: 10, - }), - ), - ); - - assert.equal(response.entries.length, 0); - assert.equal(response.truncated, false); - }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive), - ); - - it.effect("preserves structured workspace rpc failures", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspaceDir = yield* fs.makeTempDirectoryScoped({ - prefix: "t3-ws-workspace-errors-", - }); - const outsideDir = yield* fs.makeTempDirectoryScoped({ - prefix: "t3-ws-workspace-errors-outside-", - }); - const outsideFile = path.join(outsideDir, "outside.txt"); - yield* fs.writeFileString(outsideFile, "outside\n"); - yield* fs.symlink(outsideFile, path.join(workspaceDir, "linked-outside.txt")); - const resolvedOutsideFile = yield* fs.realPath(outsideFile); - - yield* buildAppUnderTest(); - - const invalidWorkspace = path.join(workspaceDir, "missing-workspace"); - const missingBrowseParent = path.join(workspaceDir, "missing-browse"); - const sensitiveQuery = "authorization: Bearer secret-token"; - const wsUrl = yield* getWsServerUrl("/ws"); - const results = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - Effect.all({ - search: client[WS_METHODS.projectsSearchEntries]({ - cwd: invalidWorkspace, - query: sensitiveQuery, - limit: 10, - }).pipe(Effect.result), - list: client[WS_METHODS.projectsListEntries]({ cwd: invalidWorkspace }).pipe( - Effect.result, - ), - read: client[WS_METHODS.projectsReadFile]({ - cwd: workspaceDir, - relativePath: "linked-outside.txt", - }).pipe(Effect.result), - browse: client[WS_METHODS.filesystemBrowse]({ - cwd: workspaceDir, - partialPath: "./missing-browse/child", - }).pipe(Effect.result), - }), - ), - ); - - if ( - results.search._tag !== "Failure" || - results.search.failure._tag !== "ProjectSearchEntriesError" - ) { - assert.fail("Expected a ProjectSearchEntriesError"); - } - const searchError = results.search.failure; - assert.equal( - searchError.message, - `Failed to search workspace entries in '${invalidWorkspace}'.`, - ); - assert.equal(searchError.cwd, invalidWorkspace); - assert.equal(searchError.queryLength, sensitiveQuery.length); - assert.notProperty(searchError, "query"); - assert.notInclude(searchError.message, "Bearer"); - assert.notInclude(searchError.message, "secret-token"); - assert.equal(searchError.limit, 10); - assert.equal(searchError.failure, "workspace_root_not_found"); - assert.equal(searchError.normalizedCwd, invalidWorkspace); - assert.isDefined(searchError.cause); - - if ( - results.list._tag !== "Failure" || - results.list.failure._tag !== "ProjectListEntriesError" - ) { - assert.fail("Expected a ProjectListEntriesError"); - } - const listError = results.list.failure; - assert.equal(listError.message, `Failed to list workspace entries in '${invalidWorkspace}'.`); - assert.equal(listError.cwd, invalidWorkspace); - assert.equal(listError.failure, "workspace_root_not_found"); - assert.equal(listError.normalizedCwd, invalidWorkspace); - assert.isDefined(listError.cause); - - if (results.read._tag !== "Failure" || results.read.failure._tag !== "ProjectReadFileError") { - assert.fail("Expected a ProjectReadFileError"); - } - const readError = results.read.failure; - assert.equal( - readError.message, - `Failed to read workspace file 'linked-outside.txt' in '${workspaceDir}'.`, - ); - assert.equal(readError.cwd, workspaceDir); - assert.equal(readError.relativePath, "linked-outside.txt"); - assert.equal(readError.failure, "resolved_path_outside_root"); - assert.equal(readError.resolvedPath, resolvedOutsideFile); - assert.isDefined(readError.cause); - - if ( - results.browse._tag !== "Failure" || - results.browse.failure._tag !== "FilesystemBrowseError" - ) { - assert.fail("Expected a FilesystemBrowseError"); - } - const browseError = results.browse.failure; - assert.equal( - browseError.message, - `Failed to browse filesystem path './missing-browse/child' from '${workspaceDir}'.`, - ); - assert.equal(browseError.cwd, workspaceDir); - assert.equal(browseError.partialPath, "./missing-browse/child"); - assert.equal(browseError.failure, "read_directory_failed"); - assert.equal(browseError.parentPath, missingBrowseParent); - assert.isDefined(browseError.cause); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("reports workspace root stat failures without relabeling them as missing", () => - Effect.gen(function* () { - if ((yield* HostProcessPlatform) === "win32") return; - - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const blockedRoot = yield* fs.makeTempDirectoryScoped({ - prefix: "t3-ws-workspace-stat-error-", - }); - const workspaceRoot = path.join(blockedRoot, "workspace"); - yield* fs.makeDirectory(workspaceRoot); - yield* fs.chmod(blockedRoot, 0o000); - - const result = yield* Effect.gen(function* () { - yield* buildAppUnderTest(); - const wsUrl = yield* getWsServerUrl("/ws"); - return yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.projectsListEntries]({ cwd: workspaceRoot }).pipe(Effect.result), - ), - ); - }).pipe(Effect.ensuring(fs.chmod(blockedRoot, 0o700).pipe(Effect.ignore))); - - if (result._tag !== "Failure" || result.failure._tag !== "ProjectListEntriesError") { - assert.fail("Expected a ProjectListEntriesError"); - } - const error = result.failure; - assert.equal(error.failure, "workspace_root_stat_failed"); - assert.equal(error.normalizedCwd, workspaceRoot); - assert.equal(error.detail, "validate-existing"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc projects.writeFile", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-project-write-" }); - - yield* buildAppUnderTest(); - - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.projectsWriteFile]({ - cwd: workspaceDir, - relativePath: "nested/created.txt", - contents: "written-by-rpc", - }), - ), - ); - - assert.equal(response.relativePath, "nested/created.txt"); - const persisted = yield* fs.readFileString(path.join(workspaceDir, "nested", "created.txt")); - assert.equal(persisted, "written-by-rpc"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("creates a missing workspace root during websocket project.create dispatch", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const parentDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-project-create-" }); - const missingWorkspaceRoot = path.join(parentDir, "nested", "new-project"); - - yield* buildAppUnderTest(); - - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "project.create", - commandId: CommandId.make("cmd-project-create-missing-root"), - projectId: ProjectId.make("project-create-missing-root"), - title: "New Project", - workspaceRoot: missingWorkspaceRoot, - createWorkspaceRootIfMissing: true, - defaultModelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - createdAt: "2026-01-01T00:00:00.000Z", - }), - ), - ); - const stat = yield* fs.stat(missingWorkspaceRoot); - - assert.isAtLeast(response.sequence, 0); - assert.equal(stat.type, "Directory"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc projects.writeFile errors", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const workspaceDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-ws-project-write-" }); - - yield* buildAppUnderTest(); - - const wsUrl = yield* getWsServerUrl("/ws"); - const result = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.projectsWriteFile]({ - cwd: workspaceDir, - relativePath: "../escape.txt", - contents: "nope", - }), - ).pipe(Effect.result), - ); - - if (result._tag !== "Failure" || result.failure._tag !== "ProjectWriteFileError") { - assert.fail("Expected a ProjectWriteFileError"); - } - const writeError = result.failure; - assert.equal( - writeError.message, - `Failed to write workspace file '../escape.txt' in '${workspaceDir}'.`, - ); - assert.equal(writeError.cwd, workspaceDir); - assert.equal(writeError.relativePath, "../escape.txt"); - assert.equal(writeError.failure, "workspace_path_outside_root"); - assert.isDefined(writeError.cause); - assert.notProperty(writeError, "contents"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc shell.openInEditor", () => - Effect.gen(function* () { - let openedInput: { cwd: string; editor: EditorId } | null = null; - yield* buildAppUnderTest({ - layers: { - externalLauncher: { - launchEditor: (input) => - Effect.sync(() => { - openedInput = input; - }), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.shellOpenInEditor]({ - cwd: "/tmp/project", - editor: "cursor", - }), - ), - ); - - assert.deepEqual(openedInput, { cwd: "/tmp/project", editor: "cursor" }); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc shell.openInEditor errors", () => - Effect.gen(function* () { - const externalLauncherError = new ExternalLauncherCommandNotFoundError({ - editor: "cursor", - command: "cursor", - }); - yield* buildAppUnderTest({ - layers: { - externalLauncher: { - launchEditor: () => Effect.fail(externalLauncherError), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const result = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.shellOpenInEditor]({ - cwd: "/tmp/project", - editor: "cursor", - }), - ).pipe(Effect.result), - ); - - assertFailure(result, externalLauncherError); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc git methods", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - config: { - cwd: "/tmp/repo", - }, - layers: { - vcsDriver: { - isInsideWorkTree: () => Effect.succeed(true), - }, - gitManager: { - invalidateLocalStatus: () => Effect.void, - invalidateRemoteStatus: () => Effect.void, - invalidateStatus: () => Effect.void, - localStatus: () => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: true, - refName: "main", - hasWorkingTreeChanges: false, - workingTree: { files: [], insertions: 0, deletions: 0 }, - }), - remoteStatus: () => - Effect.succeed({ - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }), - status: () => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: true, - refName: "main", - hasWorkingTreeChanges: false, - workingTree: { files: [], insertions: 0, deletions: 0 }, - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }), - runStackedAction: (input, options) => - Effect.gen(function* () { - const result = { - action: "commit" as const, - branch: { status: "skipped_not_requested" as const }, - commit: { - status: "created" as const, - commitSha: "abc123", - subject: "feat: demo", - }, - push: { status: "skipped_not_requested" as const }, - pr: { status: "skipped_not_requested" as const }, - toast: { - title: "Committed abc123", - description: "feat: demo", - cta: { - kind: "run_action" as const, - label: "Push", - action: { - kind: "push" as const, - }, - }, - }, - }; - - yield* ( - options?.progressReporter?.publish({ - actionId: options.actionId ?? input.actionId, - cwd: input.cwd, - action: input.action, - kind: "phase_started", - phase: "commit", - label: "Committing...", - }) ?? Effect.void - ); - - yield* ( - options?.progressReporter?.publish({ - actionId: options.actionId ?? input.actionId, - cwd: input.cwd, - action: input.action, - kind: "action_finished", - result, - }) ?? Effect.void - ); - - return result; - }), - resolvePullRequest: () => - Effect.succeed({ - pullRequest: { - number: 1, - title: "Demo PR", - url: "https://example.com/pr/1", - baseBranch: "main", - headBranch: "feature/demo", - state: "open", - }, - }), - preparePullRequestThread: () => - Effect.succeed({ - pullRequest: { - number: 1, - title: "Demo PR", - url: "https://example.com/pr/1", - baseBranch: "main", - headBranch: "feature/demo", - state: "open", - }, - branch: "feature/demo", - worktreePath: null, - }), - }, - gitVcsDriver: { - pullCurrentBranch: () => - Effect.succeed({ - status: "pulled", - refName: "main", - upstreamRef: "origin/main", - }), - listRefs: () => - Effect.succeed({ - refs: [ - { - name: "main", - current: true, - isDefault: true, - worktreePath: null, - }, - ], - isRepo: true, - hasPrimaryRemote: true, - nextCursor: null, - totalCount: 1, - }), - createWorktree: () => - Effect.succeed({ - worktree: { path: "/tmp/wt", refName: "feature/demo" }, - }), - removeWorktree: () => Effect.void, - createRef: (input) => Effect.succeed({ refName: input.refName }), - switchRef: (input) => Effect.succeed({ refName: input.refName }), - }, - vcsStatusBroadcaster: { - refreshStatus: () => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: true, - refName: "main", - hasWorkingTreeChanges: false, - workingTree: { files: [], insertions: 0, deletions: 0 }, - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }), - }, - reviewService: { - getDiffPreview: (input) => - Effect.succeed({ - cwd: input.cwd, - generatedAt: DateTime.nowUnsafe(), - sources: [ - { - id: "working-tree", - kind: "working-tree", - title: "Dirty worktree", - baseRef: "HEAD", - headRef: null, - diff: "dirty-diff", - diffHash: "hash-dirty", - truncated: false, - }, - { - id: "branch-range", - kind: "branch-range", - title: "Against main", - baseRef: "main", - headRef: "feature/demo", - diff: "base-diff", - diffHash: "hash-base", - truncated: false, - }, - ], - }), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - - const pull = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => client[WS_METHODS.vcsPull]({ cwd: "/tmp/repo" })), - ); - assert.equal(pull.status, "pulled"); - - const refreshedStatus = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.vcsRefreshStatus]({ cwd: "/tmp/repo" }), - ), - ); - assert.equal(refreshedStatus.isRepo, true); - - const stackedEvents = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.gitRunStackedAction]({ - actionId: "action-1", - cwd: "/tmp/repo", - action: "commit", - }).pipe( - Stream.runCollect, - Effect.map((events) => Array.from(events)), - ), - ), - ); - const lastStackedEvent = stackedEvents.at(-1); - assert.equal(lastStackedEvent?.kind, "action_finished"); - if (lastStackedEvent?.kind === "action_finished") { - assert.equal(lastStackedEvent.result.action, "commit"); - } - - const resolvedPr = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.gitResolvePullRequest]({ - cwd: "/tmp/repo", - reference: "1", - }), - ), - ); - assert.equal(resolvedPr.pullRequest.number, 1); - - const prepared = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.gitPreparePullRequestThread]({ - cwd: "/tmp/repo", - reference: "1", - mode: "local", - }), - ), - ); - assert.equal(prepared.branch, "feature/demo"); - - const refs = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => client[WS_METHODS.vcsListRefs]({ cwd: "/tmp/repo" })), - ); - assert.equal(refs.refs[0]?.name, "main"); - - const worktree = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.vcsCreateWorktree]({ - cwd: "/tmp/repo", - refName: "main", - path: null, - }), - ), - ); - assert.equal(worktree.worktree.refName, "feature/demo"); - - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.vcsRemoveWorktree]({ - cwd: "/tmp/repo", - path: "/tmp/wt", - }), - ), - ); - - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.vcsCreateRef]({ - cwd: "/tmp/repo", - refName: "feature/new", - }), - ), - ); - - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.vcsSwitchRef]({ - cwd: "/tmp/repo", - refName: "main", - }), - ), - ); - - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.vcsInit]({ - cwd: "/tmp/repo", - }), - ), - ); - - const diffPreview = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.reviewGetDiffPreview]({ cwd: "/tmp/repo" }), - ), - ); - assert.equal(diffPreview.sources[0]?.diff, "dirty-diff"); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc git.pull errors", () => - Effect.gen(function* () { - const gitError = new GitCommandError({ - operation: "pull", - command: "git pull --ff-only", - cwd: "/tmp/repo", - detail: "upstream missing", - }); - let invalidationCalls = 0; - let statusCalls = 0; - yield* buildAppUnderTest({ - layers: { - gitVcsDriver: { - pullCurrentBranch: () => Effect.fail(gitError), - }, - gitManager: { - invalidateLocalStatus: () => - Effect.sync(() => { - invalidationCalls += 1; - }), - invalidateRemoteStatus: () => - Effect.sync(() => { - invalidationCalls += 1; - }), - invalidateStatus: () => - Effect.sync(() => { - invalidationCalls += 1; - }), - localStatus: () => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: true, - refName: "main", - hasWorkingTreeChanges: true, - workingTree: { files: [], insertions: 0, deletions: 0 }, - }), - remoteStatus: () => - Effect.sync(() => { - statusCalls += 1; - return { - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }; - }), - status: () => - Effect.sync(() => { - statusCalls += 1; - return { - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: true, - refName: "main", - hasWorkingTreeChanges: true, - workingTree: { files: [], insertions: 0, deletions: 0 }, - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }; - }), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const result = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => client[WS_METHODS.vcsPull]({ cwd: "/tmp/repo" })).pipe( - Effect.result, - ), - ); - - assertFailure(result, gitError); - assert.equal(invalidationCalls, 0); - assert.equal(statusCalls, 0); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc git.runStackedAction errors after refreshing git status", () => - Effect.gen(function* () { - const gitError = new GitCommandError({ - operation: "commit", - command: "git commit", - cwd: "/tmp/repo", - detail: "nothing to commit", - }); - let invalidationCalls = 0; - let statusCalls = 0; - yield* buildAppUnderTest({ - layers: { - gitManager: { - invalidateLocalStatus: () => - Effect.sync(() => { - invalidationCalls += 1; - }), - invalidateRemoteStatus: () => - Effect.sync(() => { - invalidationCalls += 1; - }), - invalidateStatus: () => - Effect.sync(() => { - invalidationCalls += 1; - }), - localStatus: () => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: false, - refName: "feature/demo", - hasWorkingTreeChanges: true, - workingTree: { files: [], insertions: 0, deletions: 0 }, - }), - remoteStatus: () => - Effect.sync(() => { - statusCalls += 1; - return { - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }; - }), - status: () => - Effect.sync(() => { - statusCalls += 1; - return { - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: false, - refName: "feature/demo", - hasWorkingTreeChanges: true, - workingTree: { files: [], insertions: 0, deletions: 0 }, - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }; - }), - runStackedAction: () => Effect.fail(gitError), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const result = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.gitRunStackedAction]({ - actionId: "action-1", - cwd: "/tmp/repo", - action: "commit", - }).pipe(Stream.runCollect, Effect.result), - ), - ); - - assertFailure(result, gitError); - assert.equal(invalidationCalls, 0); - assert.equal(statusCalls, 0); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("completes websocket rpc git.pull before background git status refresh finishes", () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - layers: { - gitVcsDriver: { - pullCurrentBranch: () => - Effect.succeed({ - status: "pulled" as const, - refName: "main", - upstreamRef: "origin/main", - }), - }, - gitManager: { - invalidateLocalStatus: () => Effect.void, - invalidateRemoteStatus: () => Effect.void, - localStatus: () => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: true, - refName: "main", - hasWorkingTreeChanges: false, - workingTree: { files: [], insertions: 0, deletions: 0 }, - }), - remoteStatus: () => - Effect.sleep(Duration.seconds(2)).pipe( - Effect.as({ - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }), - ), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const startedAt = yield* Clock.currentTimeMillis; - const result = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => client[WS_METHODS.vcsPull]({ cwd: "/tmp/repo" })), - ); - const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt; - - assert.equal(result.status, "pulled"); - assertTrue(elapsedMs < 1_000); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "completes websocket rpc git.runStackedAction before background git status refresh finishes", - () => - Effect.gen(function* () { - yield* buildAppUnderTest({ - layers: { - vcsDriver: { - isInsideWorkTree: () => Effect.succeed(true), - }, - gitManager: { - invalidateLocalStatus: () => Effect.void, - invalidateRemoteStatus: () => Effect.void, - localStatus: () => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: false, - refName: "feature/demo", - hasWorkingTreeChanges: false, - workingTree: { files: [], insertions: 0, deletions: 0 }, - }), - remoteStatus: () => - Effect.sleep(Duration.seconds(2)).pipe( - Effect.as({ - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }), - ), - runStackedAction: () => - Effect.succeed({ - action: "commit" as const, - branch: { status: "skipped_not_requested" as const }, - commit: { - status: "created" as const, - commitSha: "abc123", - subject: "feat: demo", - }, - push: { status: "skipped_not_requested" as const }, - pr: { status: "skipped_not_requested" as const }, - toast: { - title: "Committed abc123", - description: "feat: demo", - cta: { - kind: "run_action" as const, - label: "Push", - action: { - kind: "push" as const, - }, - }, - }, - }), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const startedAt = yield* Clock.currentTimeMillis; - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.gitRunStackedAction]({ - actionId: "action-1", - cwd: "/tmp/repo", - action: "commit", - }).pipe(Stream.runCollect), - ), - ); - const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt; - - assertTrue(elapsedMs < 1_000); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "starts a background local git status refresh after a successful git.runStackedAction", - () => - Effect.gen(function* () { - const localRefreshStarted = yield* Deferred.make(); - - yield* buildAppUnderTest({ - layers: { - vcsDriver: { - isInsideWorkTree: () => Effect.succeed(true), - }, - gitManager: { - invalidateLocalStatus: () => Effect.void, - invalidateRemoteStatus: () => Effect.void, - localStatus: () => - Deferred.succeed(localRefreshStarted, undefined).pipe( - Effect.ignore, - Effect.andThen( - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: false, - refName: "feature/demo", - hasWorkingTreeChanges: false, - workingTree: { files: [], insertions: 0, deletions: 0 }, - }), - ), - ), - remoteStatus: () => - Effect.sleep(Duration.seconds(2)).pipe( - Effect.as({ - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }), - ), - runStackedAction: () => - Effect.succeed({ - action: "commit" as const, - branch: { status: "skipped_not_requested" as const }, - commit: { - status: "created" as const, - commitSha: "abc123", - subject: "feat: demo", - }, - push: { status: "skipped_not_requested" as const }, - pr: { status: "skipped_not_requested" as const }, - toast: { - title: "Committed abc123", - description: "feat: demo", - cta: { - kind: "run_action" as const, - label: "Push", - action: { - kind: "push" as const, - }, - }, - }, - }), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.gitRunStackedAction]({ - actionId: "action-1", - cwd: "/tmp/repo", - action: "commit", - }).pipe(Stream.runCollect), - ), - ); - - yield* Deferred.await(localRefreshStarted); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc orchestration methods", () => - Effect.gen(function* () { - const now = "2026-01-01T00:00:00.000Z"; - const snapshot = { - snapshotSequence: 1, - updatedAt: now, - projects: [ - { - id: ProjectId.make("project-a"), - title: "Project A", - workspaceRoot: "/tmp/project-a", - defaultModelSelection, - scripts: [], - createdAt: now, - updatedAt: now, - deletedAt: null, - }, - ], - threads: [ - { - id: ThreadId.make("thread-1"), - projectId: ProjectId.make("project-a"), - title: "Thread A", - modelSelection: defaultModelSelection, - interactionMode: "default" as const, - runtimeMode: "full-access" as const, - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - archivedAt: null, - latestTurn: null, - messages: [], - session: null, - activities: [], - proposedPlans: [], - checkpoints: [], - deletedAt: null, - }, - ], - }; - - yield* buildAppUnderTest({ - layers: { - projectionSnapshotQuery: { - getSnapshot: () => Effect.succeed(snapshot), - }, - orchestrationEngine: { - dispatch: () => Effect.succeed({ sequence: 7 }), - readEvents: () => Stream.empty, - }, - checkpointDiffQuery: { - getTurnDiff: () => - Effect.succeed({ - threadId: ThreadId.make("thread-1"), - fromTurnCount: 0, - toTurnCount: 1, - diff: "turn-diff", - }), - getFullThreadDiff: () => - Effect.succeed({ - threadId: ThreadId.make("thread-1"), - fromTurnCount: 0, - toTurnCount: 1, - diff: "full-diff", - }), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const dispatchResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.session.stop", - commandId: CommandId.make("cmd-1"), - threadId: ThreadId.make("thread-1"), - createdAt: now, - }), - ), - ); - assert.equal(dispatchResult.sequence, 7); - - const turnDiffResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.getTurnDiff]({ - threadId: ThreadId.make("thread-1"), - fromTurnCount: 0, - toTurnCount: 1, - }), - ), - ); - assert.equal(turnDiffResult.diff, "turn-diff"); - - const fullDiffResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.getFullThreadDiff]({ - threadId: ThreadId.make("thread-1"), - toTurnCount: 1, - }), - ), - ); - assert.equal(fullDiffResult.diff, "full-diff"); - - const replayResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.replayEvents]({ - fromSequenceExclusive: 0, - }), - ), - ); - assert.deepEqual(replayResult, []); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc orchestration shell snapshot errors", () => - Effect.gen(function* () { - const projectionError = new PersistenceSqlError({ - operation: "ProjectionSnapshotQuery.getShellSnapshot:test", - detail: "failed to read projection shell snapshot", - }); - yield* buildAppUnderTest({ - layers: { - projectionSnapshotQuery: { - getShellSnapshot: () => Effect.fail(projectionError), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const result = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.subscribeShell]({}).pipe(Stream.runCollect), - ).pipe(Effect.result), - ); - - assertTrue(result._tag === "Failure"); - assertTrue(result.failure._tag === "OrchestrationGetSnapshotError"); - assertTrue(result.failure.cause instanceof Error); - assert.include(result.failure.cause.message, projectionError.message); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("enriches replayed project events with repository identity metadata", () => - Effect.gen(function* () { - const repositoryIdentity = { - canonicalKey: "github.com/t3tools/t3code", - locator: { - source: "git-remote" as const, - remoteName: "origin", - remoteUrl: "git@github.com:T3Tools/t3code.git", - }, - displayName: "T3Tools/t3code", - provider: "github", - owner: "T3Tools", - name: "t3code", - }; - - yield* buildAppUnderTest({ - layers: { - orchestrationEngine: { - readEvents: (_fromSequenceExclusive) => - Stream.make({ - sequence: 1, - eventId: EventId.make("event-1"), - aggregateKind: "project", - aggregateId: defaultProjectId, - occurredAt: "2026-04-05T00:00:00.000Z", - commandId: null, - causationEventId: null, - correlationId: null, - metadata: {}, - type: "project.created", - payload: { - projectId: defaultProjectId, - title: "Default Project", - workspaceRoot: "/tmp/default-project", - defaultModelSelection, - scripts: [], - createdAt: "2026-04-05T00:00:00.000Z", - updatedAt: "2026-04-05T00:00:00.000Z", - }, - } satisfies Extract), - }, - repositoryIdentityResolver: { - resolve: () => Effect.succeed(repositoryIdentity), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const replayResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.replayEvents]({ - fromSequenceExclusive: 0, - }), - ), - ); - - const replayedEvent = replayResult[0]; - assert.equal(replayedEvent?.type, "project.created"); - assert.deepEqual( - replayedEvent && replayedEvent.type === "project.created" - ? replayedEvent.payload.repositoryIdentity - : null, - repositoryIdentity, - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("stops the provider session and closes thread terminals after archive", () => - Effect.gen(function* () { - const threadId = ThreadId.make("thread-archive"); - const effects: string[] = []; - const dispatchedCommands: Array = []; - const now = "2026-01-01T00:00:00.000Z"; - - yield* buildAppUnderTest({ - layers: { - terminalManager: { - close: (input) => - Effect.sync(() => { - effects.push(`terminal.close:${input.threadId}`); - }), - }, - orchestrationEngine: { - dispatch: (command) => - Effect.sync(() => { - dispatchedCommands.push(command); - effects.push(`dispatch:${command.type}`); - return { sequence: dispatchedCommands.length }; - }), - }, - projectionSnapshotQuery: { - getThreadShellById: () => - Effect.succeed( - Option.some( - makeDefaultOrchestrationThreadShell({ - id: threadId, - updatedAt: now, - session: { - threadId, - status: "ready", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }), - ), - ), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const dispatchResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.archive", - commandId: CommandId.make("cmd-thread-archive"), - threadId, - }), - ), - ); - - assert.equal(dispatchResult.sequence, 1); - assert.deepEqual(effects, [ - "dispatch:thread.archive", - "dispatch:thread.session.stop", - `terminal.close:${threadId}`, - ]); - const sessionStopCommand = dispatchedCommands[1]; - assert.equal(sessionStopCommand?.type, "thread.session.stop"); - if (sessionStopCommand?.type === "thread.session.stop") { - assert.equal(sessionStopCommand.threadId, threadId); - } - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("checks session status before archiving removes the thread from active lookups", () => - Effect.gen(function* () { - const threadId = ThreadId.make("thread-archive-precheck"); - const effects: string[] = []; - const dispatchedCommands: Array = []; - const now = "2026-01-01T00:00:00.000Z"; - let archived = false; - - yield* buildAppUnderTest({ - layers: { - terminalManager: { - close: (input) => - Effect.sync(() => { - effects.push(`terminal.close:${input.threadId}`); - }), - }, - orchestrationEngine: { - dispatch: (command) => - Effect.sync(() => { - dispatchedCommands.push(command); - effects.push(`dispatch:${command.type}`); - if (command.type === "thread.archive") { - archived = true; - } - return { sequence: dispatchedCommands.length }; - }), - }, - projectionSnapshotQuery: { - getThreadShellById: () => - Effect.sync(() => { - effects.push(`query:thread-shell:${archived ? "archived" : "active"}`); - return archived - ? Option.none() - : Option.some( - makeDefaultOrchestrationThreadShell({ - id: threadId, - updatedAt: now, - session: { - threadId, - status: "ready", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }), - ); - }), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const dispatchResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.archive", - commandId: CommandId.make("cmd-thread-archive-precheck"), - threadId, - }), - ), - ); - - assert.equal(dispatchResult.sequence, 1); - assert.deepEqual(effects, [ - "query:thread-shell:active", - "dispatch:thread.archive", - "dispatch:thread.session.stop", - `terminal.close:${threadId}`, - ]); - assert.deepEqual( - dispatchedCommands.map((command) => command.type), - ["thread.archive", "thread.session.stop"], - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("archives without dispatching session stop when the thread has no session", () => - Effect.gen(function* () { - const threadId = ThreadId.make("thread-archive-no-session"); - const effects: string[] = []; - const dispatchedCommands: Array = []; - - yield* buildAppUnderTest({ - layers: { - terminalManager: { - close: (input) => - Effect.sync(() => { - effects.push(`terminal.close:${input.threadId}`); - }), - }, - orchestrationEngine: { - dispatch: (command) => - Effect.sync(() => { - dispatchedCommands.push(command); - effects.push(`dispatch:${command.type}`); - return { sequence: dispatchedCommands.length }; - }), - }, - projectionSnapshotQuery: { - getThreadShellById: () => - Effect.succeed( - Option.some(makeDefaultOrchestrationThreadShell({ id: threadId, session: null })), - ), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const dispatchResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.archive", - commandId: CommandId.make("cmd-thread-archive-no-session"), - threadId, - }), - ), - ); - - assert.equal(dispatchResult.sequence, 1); - assert.deepEqual(effects, ["dispatch:thread.archive", `terminal.close:${threadId}`]); - assert.deepEqual( - dispatchedCommands.map((command) => command.type), - ["thread.archive"], - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "archives without dispatching session stop when the thread session is already stopped", - () => - Effect.gen(function* () { - const threadId = ThreadId.make("thread-archive-stopped-session"); - const effects: string[] = []; - const dispatchedCommands: Array = []; - const now = "2026-01-01T00:00:00.000Z"; - - yield* buildAppUnderTest({ - layers: { - terminalManager: { - close: (input) => - Effect.sync(() => { - effects.push(`terminal.close:${input.threadId}`); - }), - }, - orchestrationEngine: { - dispatch: (command) => - Effect.sync(() => { - dispatchedCommands.push(command); - effects.push(`dispatch:${command.type}`); - return { sequence: dispatchedCommands.length }; - }), - }, - projectionSnapshotQuery: { - getThreadShellById: () => - Effect.succeed( - Option.some( - makeDefaultOrchestrationThreadShell({ - id: threadId, - updatedAt: now, - session: { - threadId, - status: "stopped", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }), - ), - ), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const dispatchResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.archive", - commandId: CommandId.make("cmd-thread-archive-stopped-session"), - threadId, - }), - ), - ); - - assert.equal(dispatchResult.sequence, 1); - assert.deepEqual(effects, ["dispatch:thread.archive", `terminal.close:${threadId}`]); - assert.deepEqual( - dispatchedCommands.map((command) => command.type), - ["thread.archive"], - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("archives and still closes terminals when session stop fails", () => - Effect.gen(function* () { - const threadId = ThreadId.make("thread-archive-stop-failure"); - const effects: string[] = []; - const dispatchedCommands: Array = []; - const now = "2026-01-01T00:00:00.000Z"; - - yield* buildAppUnderTest({ - layers: { - terminalManager: { - close: (input) => - Effect.sync(() => { - effects.push(`terminal.close:${input.threadId}`); - }), - }, - orchestrationEngine: { - dispatch: (command) => { - dispatchedCommands.push(command); - effects.push(`dispatch:${command.type}`); - if (command.type === "thread.session.stop") { - return Effect.fail( - new OrchestrationListenerCallbackError({ - listener: "domain-event", - detail: "simulated archive stop failure", - }), - ); - } - return Effect.succeed({ sequence: dispatchedCommands.length }); - }, - }, - projectionSnapshotQuery: { - getThreadShellById: () => - Effect.succeed( - Option.some( - makeDefaultOrchestrationThreadShell({ - id: threadId, - updatedAt: now, - session: { - threadId, - status: "ready", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }), - ), - ), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const dispatchResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.archive", - commandId: CommandId.make("cmd-thread-archive-stop-failure"), - threadId, - }), - ), - ); - - assert.equal(dispatchResult.sequence, 1); - assert.deepEqual(effects, [ - "dispatch:thread.archive", - "dispatch:thread.session.stop", - `terminal.close:${threadId}`, - ]); - assert.deepEqual( - dispatchedCommands.map((command) => command.type), - ["thread.archive", "thread.session.stop"], - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("archives and still closes terminals when session stop defects", () => - Effect.gen(function* () { - const threadId = ThreadId.make("thread-archive-stop-defect"); - const effects: string[] = []; - const dispatchedCommands: Array = []; - const now = "2026-01-01T00:00:00.000Z"; - - yield* buildAppUnderTest({ - layers: { - terminalManager: { - close: (input) => - Effect.sync(() => { - effects.push(`terminal.close:${input.threadId}`); - }), - }, - orchestrationEngine: { - dispatch: (command) => { - dispatchedCommands.push(command); - effects.push(`dispatch:${command.type}`); - if (command.type === "thread.session.stop") { - return Effect.die(new Error("simulated archive stop defect")); - } - return Effect.succeed({ sequence: dispatchedCommands.length }); - }, - }, - projectionSnapshotQuery: { - getThreadShellById: () => - Effect.succeed( - Option.some( - makeDefaultOrchestrationThreadShell({ - id: threadId, - updatedAt: now, - session: { - threadId, - status: "ready", - providerName: "claudeAgent", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: now, - }, - }), - ), - ), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const dispatchResult = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.archive", - commandId: CommandId.make("cmd-thread-archive-stop-defect"), - threadId, - }), - ), - ); - - assert.equal(dispatchResult.sequence, 1); - assert.deepEqual(effects, [ - "dispatch:thread.archive", - "dispatch:thread.session.stop", - `terminal.close:${threadId}`, - ]); - assert.deepEqual( - dispatchedCommands.map((command) => command.type), - ["thread.archive", "thread.session.stop"], - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect( - "bootstraps first-send worktree turns on the server before dispatching turn start", - () => - Effect.gen(function* () { - const dispatchedCommands: Array = []; - const bootstrapGitOperations: string[] = []; - const refreshStatus = vi.fn((_: string) => - Effect.succeed({ - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: false, - refName: "t3code/bootstrap-refName", - hasWorkingTreeChanges: false, - workingTree: { - files: [], - insertions: 0, - deletions: 0, - }, - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - }), - ); - const fetchRemote = vi.fn( - (_: Parameters[0]) => - Effect.sync(() => { - bootstrapGitOperations.push("fetch"); - }), - ); - const fetchedOriginCommit = "0123456789abcdef0123456789abcdef01234567"; - const resolveRemoteTrackingCommit = vi.fn( - (_: Parameters[0]) => - Effect.sync(() => { - bootstrapGitOperations.push("resolve-remote-commit"); - return { - commitSha: fetchedOriginCommit, - remoteRefName: "origin/main", - }; - }), - ); - const createWorktree = vi.fn( - (_: Parameters[0]) => - Effect.sync(() => { - bootstrapGitOperations.push("create-worktree"); - return { - worktree: { - refName: "t3code/bootstrap-refName", - path: "/tmp/bootstrap-worktree", - }, - }; - }), - ); - const runForThread = vi.fn( - ( - _: Parameters< - ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"] - >[0], - ) => - Effect.succeed({ - status: "started" as const, - scriptId: "setup", - scriptName: "Setup", - terminalId: "setup-setup", - cwd: "/tmp/bootstrap-worktree", - }), - ); - - yield* buildAppUnderTest({ - layers: { - gitVcsDriver: { - fetchRemote, - resolveRemoteTrackingCommit, - createWorktree, - }, - vcsStatusBroadcaster: { - refreshStatus, - }, - orchestrationEngine: { - dispatch: (command) => - Effect.sync(() => { - dispatchedCommands.push(command); - return { sequence: dispatchedCommands.length }; - }), - readEvents: () => Stream.empty, - }, - projectSetupScriptRunner: { - runForThread, - }, - }, - }); - - const createdAt = "2026-01-01T00:00:00.000Z"; - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-bootstrap-turn-start"), - threadId: ThreadId.make("thread-bootstrap"), - message: { - messageId: MessageId.make("msg-bootstrap"), - role: "user", - text: "hello", - attachments: [], - }, - modelSelection: defaultModelSelection, - runtimeMode: "full-access", - interactionMode: "default", - bootstrap: { - createThread: { - projectId: defaultProjectId, - title: "Bootstrap Thread", - modelSelection: defaultModelSelection, - runtimeMode: "full-access", - interactionMode: "default", - branch: "main", - worktreePath: null, - createdAt, - }, - prepareWorktree: { - projectCwd: "/tmp/project", - baseBranch: "main", - branch: "t3code/bootstrap-refName", - startFromOrigin: true, - }, - runSetupScript: true, - }, - createdAt, - }), - ), - ); - - assert.equal(response.sequence, 5); - assert.deepEqual( - dispatchedCommands.map((command) => command.type), - [ - "thread.create", - "thread.meta.update", - "thread.activity.append", - "thread.activity.append", - "thread.turn.start", - ], - ); - assert.deepEqual(createWorktree.mock.calls[0]?.[0], { - cwd: "/tmp/project", - refName: fetchedOriginCommit, - newRefName: "t3code/bootstrap-refName", - baseRefName: "main", - path: null, - }); - assert.deepEqual(fetchRemote.mock.calls[0]?.[0], { - cwd: "/tmp/project", - remoteName: "origin", - }); - assert.deepEqual(resolveRemoteTrackingCommit.mock.calls[0]?.[0], { - cwd: "/tmp/project", - refName: "main", - fallbackRemoteName: "origin", - }); - assert.deepEqual(bootstrapGitOperations, [ - "fetch", - "resolve-remote-commit", - "create-worktree", - ]); - assert.deepEqual(runForThread.mock.calls[0]?.[0], { - threadId: ThreadId.make("thread-bootstrap"), - projectId: defaultProjectId, - projectCwd: "/tmp/project", - worktreePath: "/tmp/bootstrap-worktree", - }); - assert.deepEqual(refreshStatus.mock.calls[0]?.[0], "/tmp/bootstrap-worktree"); - - const setupActivities = dispatchedCommands.filter( - (command): command is Extract => - command.type === "thread.activity.append", - ); - assert.deepEqual( - setupActivities.map((command) => command.activity.kind), - ["setup-script.requested", "setup-script.started"], - ); - const finalCommand = dispatchedCommands[4]; - assertTrue(finalCommand?.type === "thread.turn.start"); - if (finalCommand?.type === "thread.turn.start") { - assert.equal(finalCommand.bootstrap, undefined); - } - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("records setup-script failures without aborting bootstrap turn start", () => - Effect.gen(function* () { - const dispatchedCommands: Array = []; - const createWorktree = vi.fn( - (_: Parameters[0]) => - Effect.succeed({ - worktree: { - refName: "t3code/bootstrap-refName", - path: "/tmp/bootstrap-worktree", - }, - }), - ); - const runForThread = vi.fn( - ( - input: Parameters< - ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"] - >[0], - ) => - Effect.fail( - new ProjectSetupScriptRunner.ProjectSetupScriptOperationError({ - threadId: input.threadId, - worktreePath: input.worktreePath, - operation: "openTerminal", - cause: { message: "pty unavailable" }, - }), - ), - ); - - yield* buildAppUnderTest({ - layers: { - gitVcsDriver: { - createWorktree, - }, - orchestrationEngine: { - dispatch: (command) => - Effect.sync(() => { - dispatchedCommands.push(command); - return { sequence: dispatchedCommands.length }; - }), - readEvents: () => Stream.empty, - }, - projectSetupScriptRunner: { - runForThread, - }, - }, - }); - - const createdAt = "2026-01-01T00:00:00.000Z"; - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-bootstrap-turn-start-setup-failure"), - threadId: ThreadId.make("thread-bootstrap-setup-failure"), - message: { - messageId: MessageId.make("msg-bootstrap-setup-failure"), - role: "user", - text: "hello", - attachments: [], - }, - modelSelection: defaultModelSelection, - runtimeMode: "full-access", - interactionMode: "default", - bootstrap: { - createThread: { - projectId: defaultProjectId, - title: "Bootstrap Thread", - modelSelection: defaultModelSelection, - runtimeMode: "full-access", - interactionMode: "default", - branch: "main", - worktreePath: null, - createdAt, - }, - prepareWorktree: { - projectCwd: "/tmp/project", - baseBranch: "main", - branch: "t3code/bootstrap-refName", - }, - runSetupScript: true, - }, - createdAt, - }), - ), - ); - - assert.equal(response.sequence, 4); - assert.deepEqual( - dispatchedCommands.map((command) => command.type), - ["thread.create", "thread.meta.update", "thread.activity.append", "thread.turn.start"], - ); - const setupFailureActivity = dispatchedCommands.find( - (command): command is Extract => - command.type === "thread.activity.append", - ); - assert.equal(setupFailureActivity?.activity.kind, "setup-script.failed"); - assert.deepEqual(setupFailureActivity?.activity.payload, { - detail: "pty unavailable", - worktreePath: "/tmp/bootstrap-worktree", - }); - assertTrue(dispatchedCommands.every((command) => command.type !== "thread.delete")); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("does not misattribute setup activity dispatch failures as setup launch failures", () => - Effect.gen(function* () { - const dispatchedCommands: Array = []; - const createWorktree = vi.fn( - (_: Parameters[0]) => - Effect.succeed({ - worktree: { - refName: "t3code/bootstrap-refName", - path: "/tmp/bootstrap-worktree", - }, - }), - ); - const runForThread = vi.fn( - ( - _: Parameters< - ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"] - >[0], - ) => - Effect.succeed({ - status: "started" as const, - scriptId: "setup", - scriptName: "Setup", - terminalId: "setup-setup", - cwd: "/tmp/bootstrap-worktree", - }), - ); - let setupActivityAppendAttempt = 0; - - yield* buildAppUnderTest({ - layers: { - gitVcsDriver: { - createWorktree, - }, - orchestrationEngine: { - dispatch: (command) => { - if ( - command.type === "thread.activity.append" && - command.activity.kind.startsWith("setup-script.") - ) { - setupActivityAppendAttempt += 1; - if (setupActivityAppendAttempt === 2) { - return Effect.fail( - new OrchestrationListenerCallbackError({ - listener: "domain-event", - detail: "failed to append setup-script.started activity", - }), - ); - } - } - - return Effect.sync(() => { - dispatchedCommands.push(command); - return { sequence: dispatchedCommands.length }; - }); - }, - readEvents: () => Stream.empty, - }, - projectSetupScriptRunner: { - runForThread, - }, - }, - }); - - const createdAt = "2026-01-01T00:00:00.000Z"; - const wsUrl = yield* getWsServerUrl("/ws"); - const response = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-bootstrap-turn-start-setup-activity-failure"), - threadId: ThreadId.make("thread-bootstrap-setup-activity-failure"), - message: { - messageId: MessageId.make("msg-bootstrap-setup-activity-failure"), - role: "user", - text: "hello", - attachments: [], - }, - modelSelection: defaultModelSelection, - runtimeMode: "full-access", - interactionMode: "default", - bootstrap: { - createThread: { - projectId: defaultProjectId, - title: "Bootstrap Thread", - modelSelection: defaultModelSelection, - runtimeMode: "full-access", - interactionMode: "default", - branch: "main", - worktreePath: null, - createdAt, - }, - prepareWorktree: { - projectCwd: "/tmp/project", - baseBranch: "main", - branch: "t3code/bootstrap-refName", - }, - runSetupScript: true, - }, - createdAt, - }), - ), - ); - - assert.equal(response.sequence, 4); - assert.deepEqual( - dispatchedCommands.map((command) => command.type), - ["thread.create", "thread.meta.update", "thread.activity.append", "thread.turn.start"], - ); - const setupActivities = dispatchedCommands.filter( - (command): command is Extract => - command.type === "thread.activity.append", - ); - assert.deepEqual( - setupActivities.map((command) => command.activity.kind), - ["setup-script.requested"], - ); - assertTrue( - setupActivities.every((command) => command.activity.kind !== "setup-script.failed"), - ); - assertTrue(dispatchedCommands.every((command) => command.type !== "thread.delete")); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("cleans up created bootstrap threads when worktree creation defects", () => - Effect.gen(function* () { - const dispatchedCommands: Array = []; - const createWorktree = vi.fn( - (_: Parameters[0]) => - Effect.die(new Error("worktree exploded")), - ); - - yield* buildAppUnderTest({ - layers: { - gitVcsDriver: { - createWorktree, - }, - orchestrationEngine: { - dispatch: (command) => - Effect.sync(() => { - dispatchedCommands.push(command); - return { sequence: dispatchedCommands.length }; - }), - readEvents: () => Stream.empty, - }, - }, - }); - - const createdAt = "2026-01-01T00:00:00.000Z"; - const wsUrl = yield* getWsServerUrl("/ws"); - const result = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[ORCHESTRATION_WS_METHODS.dispatchCommand]({ - type: "thread.turn.start", - commandId: CommandId.make("cmd-bootstrap-turn-start-defect"), - threadId: ThreadId.make("thread-bootstrap-defect"), - message: { - messageId: MessageId.make("msg-bootstrap-defect"), - role: "user", - text: "hello", - attachments: [], - }, - modelSelection: defaultModelSelection, - runtimeMode: "full-access", - interactionMode: "default", - bootstrap: { - createThread: { - projectId: defaultProjectId, - title: "Bootstrap Thread", - modelSelection: defaultModelSelection, - runtimeMode: "full-access", - interactionMode: "default", - branch: "main", - worktreePath: null, - createdAt, - }, - prepareWorktree: { - projectCwd: "/tmp/project", - baseBranch: "main", - branch: "t3code/bootstrap-refName", - }, - runSetupScript: false, - }, - createdAt, - }), - ).pipe(Effect.result), - ); - - assertTrue(result._tag === "Failure"); - assertTrue(result.failure._tag === "OrchestrationDispatchCommandError"); - assert.include(result.failure.message, "worktree exploded"); - assert.deepEqual( - dispatchedCommands.map((command) => command.type), - ["thread.create", "thread.delete"], - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc terminal methods", () => - Effect.gen(function* () { - const snapshot = { - threadId: "thread-1", - terminalId: "default", - cwd: "/tmp/project", - worktreePath: null, - status: "running" as const, - pid: 1234, - history: "", - exitCode: null, - exitSignal: null, - label: "Primary", - updatedAt: "2026-01-01T00:00:00.000Z", - }; - - yield* buildAppUnderTest({ - layers: { - terminalManager: { - open: () => Effect.succeed(snapshot), - write: () => Effect.void, - resize: () => Effect.void, - clear: () => Effect.void, - restart: () => Effect.succeed(snapshot), - close: () => Effect.void, - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - - const opened = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.terminalOpen]({ - threadId: "thread-1", - terminalId: "default", - cwd: "/tmp/project", - }), - ), - ); - assert.equal(opened.terminalId, "default"); - - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.terminalWrite]({ - threadId: "thread-1", - terminalId: "default", - data: "echo hi\n", - }), - ), - ); - - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.terminalResize]({ - threadId: "thread-1", - terminalId: "default", - cols: 120, - rows: 40, - }), - ), - ); - - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.terminalClear]({ - threadId: "thread-1", - terminalId: "default", - }), - ), - ); - - const restarted = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.terminalRestart]({ - threadId: "thread-1", - terminalId: "default", - cwd: "/tmp/project", - cols: 120, - rows: 40, - }), - ), - ); - assert.equal(restarted.terminalId, "default"); - - yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.terminalClose]({ - threadId: "thread-1", - terminalId: "default", - }), - ), - ); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); - - it.effect("routes websocket rpc terminal.write errors", () => - Effect.gen(function* () { - const terminalError = new TerminalNotRunningError({ - threadId: "thread-1", - terminalId: "default", - }); - yield* buildAppUnderTest({ - layers: { - terminalManager: { - write: () => Effect.fail(terminalError), - }, - }, - }); - - const wsUrl = yield* getWsServerUrl("/ws"); - const result = yield* Effect.scoped( - withWsRpcClient(wsUrl, (client) => - client[WS_METHODS.terminalWrite]({ - threadId: "thread-1", - terminalId: "default", - data: "echo fail\n", - }), - ).pipe(Effect.result), - ); - - assertFailure(result, terminalError); - }).pipe(Effect.provide(NodeHttpServer.layerTest)), - ); -}); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 81d0013b20c..0b0972b2272 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -18,12 +18,6 @@ import * as ExternalLauncher from "./process/externalLauncher.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; import * as AnalyticsService from "./telemetry/AnalyticsService.ts"; -import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts"; -import * as ProviderSessionRuntime from "./persistence/ProviderSessionRuntime.ts"; -import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts"; -import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts"; -import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts"; -import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts"; import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts"; import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; import * as CheckpointStore from "./checkpointing/CheckpointStore.ts"; @@ -42,12 +36,6 @@ import * as ProcessRunner from "./processRunner.ts"; import * as GitManager from "./git/GitManager.ts"; import * as Keybindings from "./keybindings.ts"; import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; -import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts"; -import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts"; -import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts"; -import { ProviderCommandReactorLive } from "./orchestration/Layers/ProviderCommandReactor.ts"; -import { CheckpointReactorLive } from "./orchestration/Layers/CheckpointReactor.ts"; -import { ThreadDeletionReactorLive } from "./orchestration/Layers/ThreadDeletionReactor.ts"; import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts"; import { hasCloudPublicConfig } from "./cloud/publicConfig.ts"; import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts"; @@ -67,7 +55,6 @@ import * as GitWorkflowService from "./git/GitWorkflowService.ts"; import * as ReviewService from "./review/ReviewService.ts"; import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; -import * as ProjectSetupScriptRunner from "./project/ProjectSetupScriptRunner.ts"; import { ObservabilityLive } from "./observability/Layers/Observability.ts"; import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; import { authHttpApiLayer, environmentAuthenticatedAuthLayer } from "./auth/http.ts"; @@ -81,13 +68,18 @@ import * as CloudCliState from "./cloud/CliState.ts"; import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts"; import * as TraceDiagnostics from "./diagnostics/TraceDiagnostics.ts"; -import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts"; +import { + OrchestrationV2ProductionLayerLive, + ProjectSetupScriptRunnerLayerLive, +} from "./orchestration-v2/runtimeLayer.ts"; +import * as ResourceCleanupService from "./orchestration-v2/ResourceCleanupService.ts"; +import * as RunFinalizationService from "./orchestration-v2/RunFinalizationService.ts"; import { clearPersistedServerRuntimeState, makePersistedServerRuntimeState, persistServerRuntimeState, } from "./serverRuntimeState.ts"; -import { orchestrationHttpApiLayer } from "./orchestration/http.ts"; +import { projectHttpApiLayer } from "./project/http.ts"; import * as NetService from "@t3tools/shared/Net"; import * as RelayClient from "@t3tools/shared/relayClient"; import { disableTailscaleServe, ensureTailscaleServe } from "@t3tools/tailscale"; @@ -155,31 +147,6 @@ const PlatformServicesLive = Layer.unwrap( }), ); -const ReactorLayerLive = Layer.empty.pipe( - Layer.provideMerge(OrchestrationReactorLive), - Layer.provideMerge(ProviderRuntimeIngestionLive), - Layer.provideMerge(ProviderCommandReactorLive), - Layer.provideMerge(CheckpointReactorLive), - Layer.provideMerge(ThreadDeletionReactorLive), - Layer.provideMerge(AgentAwarenessRelay.layer.pipe(Layer.provide(ServerSecretStore.layer))), - Layer.provideMerge(RuntimeReceiptBusLive), -); - -const ProviderSessionDirectoryLayerLive = ProviderSessionDirectoryLive.pipe( - Layer.provide(ProviderSessionRuntime.layer), -); - -// `ProviderAdapterRegistryLive` is now a facade that resolves kind → adapter -// by looking up the default `ProviderInstance` per driver in the instance -// registry. Adapter construction itself moved inside each driver's -// `create()`; `ProviderEventLoggersLive` owns the shared native/canonical -// NDJSON writers and is provided at the outer runtime layer so both -// `ProviderService` and the per-instance drivers read the same logger pair. -const ProviderLayerLive = ProviderServiceLive.pipe( - Layer.provide(ProviderAdapterRegistryLive), - Layer.provideMerge(ProviderSessionDirectoryLayerLive), -); - const PersistenceLayerLive = Layer.empty.pipe(Layer.provideMerge(SqlitePersistenceLayerLive)); const VcsDriverRegistryLayerLive = VcsDriverRegistry.layer.pipe( @@ -195,7 +162,7 @@ const SourceControlProviderRegistryLayerLive = SourceControlProviderRegistry.lay ); const GitManagerLayerLive = GitManager.layer.pipe( - Layer.provideMerge(ProjectSetupScriptRunner.layer), + Layer.provideMerge(ProjectSetupScriptRunnerLayerLive), Layer.provideMerge(GitVcsDriver.layer), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(TextGeneration.layer), @@ -231,9 +198,8 @@ const VcsLayerLive = Layer.empty.pipe( Layer.provideMerge(VcsStatusBroadcaster.layer.pipe(Layer.provide(GitWorkflowLayerLive))), ); -const CheckpointingLayerLive = Layer.empty.pipe( - Layer.provideMerge(CheckpointDiffQuery.layer), - Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistryLayerLive))), +const CheckpointStoreLayerLive = CheckpointStore.layer.pipe( + Layer.provide(VcsDriverRegistryLayerLive), ); const PortScannerLayerLive = PortScanner.layer.pipe(Layer.provide(ProcessRunner.layer)); @@ -278,18 +244,23 @@ const CloudManagedEndpointRuntimeLive = Layer.mergeAll( ), ); -const ProviderRuntimeLayerLive = ProviderSessionReaperLive.pipe( - Layer.provideMerge(ProviderLayerLive), - Layer.provideMerge(OrchestrationLayerLive), +const OrchestrationV2RuntimeLayerLive = OrchestrationV2ProductionLayerLive.pipe( + Layer.provide(CheckpointStoreLayerLive), + Layer.provide(ResourceCleanupService.live), + Layer.provide(RunFinalizationService.observerLive), +); + +const OrchestrationApplicationLayerLive = CheckpointDiffQuery.layer.pipe( + Layer.provideMerge(CheckpointStoreLayerLive), + Layer.provideMerge(OrchestrationV2RuntimeLayerLive), ); -const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( +const RuntimeCoreDependenciesBaseLive = AgentAwarenessRelay.layer.pipe( // Core Services - Layer.provideMerge(CheckpointingLayerLive), + Layer.provideMerge(OrchestrationApplicationLayerLive), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), - Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), Layer.provideMerge(Keybindings.layer), @@ -300,12 +271,9 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( // `providerInstances` hydration merges `settings.providers.` // with explicit `providerInstances` entries on boot. Layer.provideMerge(ProviderInstanceRegistryHydrationLive), - // Shared native/canonical NDJSON writers used by both the per-instance - // drivers (native stream, written from inside each `Adapter`) and - // `ProviderService` (canonical stream, written after event normalization). - // Provided once at the runtime level so every consumer sees the same - // logger instances. - Layer.provideMerge(ProviderEventLoggers.ProviderEventLoggersLive), +); + +const RuntimeCoreDependenciesLive = RuntimeCoreDependenciesBaseLive.pipe( // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but // the rewritten registry reads snapshots off the instance registry and @@ -347,7 +315,7 @@ export const makeRoutesLayer = Layer.mergeAll( HttpApiBuilder.layer(EnvironmentHttpApi).pipe( Layer.provide(authHttpApiLayer), Layer.provide(connectHttpApiLayer), - Layer.provide(orchestrationHttpApiLayer), + Layer.provide(projectHttpApiLayer), Layer.provide(serverEnvironmentHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer), ), @@ -356,7 +324,7 @@ export const makeRoutesLayer = Layer.mergeAll( staticAndDevRouteLayer, websocketRpcRouteLayer, ), - McpHttpServer.layer.pipe(Layer.provide(McpSessionRegistry.layer)), + McpHttpServer.layer, ).pipe(Layer.provide(browserApiCorsLayer)); export const makeServerLayer = Layer.unwrap( @@ -478,6 +446,7 @@ export const makeServerLayer = Layer.unwrap( return serverApplicationLayer.pipe( Layer.provideMerge(RuntimeServicesLive), + Layer.provideMerge(McpSessionRegistry.layer.pipe(Layer.provide(ServerEnvironment.layer))), Layer.provideMerge(serverRelayBrokerTracingLayer), Layer.provideMerge(HttpServerLive), Layer.provide(ObservabilityLive), diff --git a/apps/server/src/serverRuntimeStartup.test.ts b/apps/server/src/serverRuntimeStartup.test.ts index e331f0cd4d6..c3731023e39 100644 --- a/apps/server/src/serverRuntimeStartup.test.ts +++ b/apps/server/src/serverRuntimeStartup.test.ts @@ -1,270 +1,72 @@ -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { DEFAULT_MODEL, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; import { assert, it } from "@effect/vitest"; -import * as Crypto from "effect/Crypto"; -import * as Deferred from "effect/Deferred"; +import { DEFAULT_MODEL, ProviderInstanceId } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; -import * as Option from "effect/Option"; -import * as PlatformError from "effect/PlatformError"; import * as Ref from "effect/Ref"; -import * as Stream from "effect/Stream"; -import * as ServerConfig from "./config.ts"; -import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; -import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; -import * as AnalyticsService from "./telemetry/AnalyticsService.ts"; import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; -it("uses the canonical Codex default for auto-bootstrapped model selection", () => { - assert.deepStrictEqual(ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), { +it("uses the canonical Codex model for auto-bootstrap", () => { + assert.deepEqual(ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), { instanceId: ProviderInstanceId.make("codex"), model: DEFAULT_MODEL, }); }); -it.effect("enqueueCommand waits for readiness and then drains queued work", () => - Effect.scoped( - Effect.gen(function* () { - const executionCount = yield* Ref.make(0); - const commandGate = yield* ServerRuntimeStartup.makeCommandGate; - - const queuedCommandFiber = yield* commandGate - .enqueueCommand(Ref.updateAndGet(executionCount, (count) => count + 1)) - .pipe(Effect.forkScoped); - - yield* Effect.yieldNow; - assert.equal(yield* Ref.get(executionCount), 0); - - yield* commandGate.signalCommandReady; - - const result = yield* Fiber.join(queuedCommandFiber); - assert.equal(result, 1); - assert.equal(yield* Ref.get(executionCount), 1); - }), - ), -); - -it.effect("enqueueCommand fails queued work when readiness fails", () => - Effect.scoped( - Effect.gen(function* () { - const commandGate = yield* ServerRuntimeStartup.makeCommandGate; - const failure = yield* Deferred.make(); - - const queuedCommandFiber = yield* commandGate - .enqueueCommand(Deferred.await(failure).pipe(Effect.as("should-not-run"))) - .pipe(Effect.forkScoped); - - yield* commandGate.failCommandReady( - new ServerRuntimeStartup.ServerRuntimeStartupError({ - mode: "web", - host: "127.0.0.1", - port: 3773, - cause: new Error("test startup failure"), - }), - ); - - const error = yield* Effect.flip(Fiber.join(queuedCommandFiber)); - assert.equal(error.message, "Server runtime startup failed before command readiness."); - }), - ), -); - -it.effect("launchStartupHeartbeat does not block the caller while counts are loading", () => - Effect.scoped( - Effect.gen(function* () { - const releaseCounts = yield* Deferred.make(); - - yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe( - Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => Effect.die("unused"), - getSnapshot: () => Effect.die("unused"), - getShellSnapshot: () => Effect.die("unused"), - getArchivedShellSnapshot: () => Effect.die("unused"), - getSnapshotSequence: () => Effect.die("unused"), - getCounts: () => - Deferred.await(releaseCounts).pipe( - Effect.as({ - projectCount: 2, - threadCount: 3, - }), - ), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getProjectShellById: () => Effect.succeed(Option.none()), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => Effect.succeed(Option.none()), - getFullThreadDiffContext: () => Effect.succeed(Option.none()), - getThreadShellById: () => Effect.succeed(Option.none()), - getThreadDetailById: () => Effect.succeed(Option.none()), - }), - Effect.provideService(AnalyticsService.AnalyticsService, { - record: () => Effect.void, - flush: Effect.void, - }), - ); - }), - ), -); - -it.effect("resolveWelcomeBase derives cwd and project name from server config", () => +it.effect("runs projection repair, recovery, worker startup, and bootstrap in order", () => Effect.gen(function* () { - const welcome = yield* ServerRuntimeStartup.resolveWelcomeBase.pipe( - Effect.provideService(ServerConfig.ServerConfig, { - cwd: "/tmp/startup-project", - } as never), - ); - - assert.deepStrictEqual(welcome, { - cwd: "/tmp/startup-project", - projectName: "startup-project", + const calls = yield* Ref.make>([]); + const record = (label: string) => Ref.update(calls, (current) => [...current, label]); + + const result = yield* ServerRuntimeStartup.runOrderedV2StartupPhases({ + verify: record("verify").pipe(Effect.as({ valid: false })), + rebuild: record("rebuild").pipe(Effect.as({ valid: true })), + recover: record("recover").pipe(Effect.as({ resumedSessions: 2 })), + startEffectWorker: record("worker"), + autoBootstrap: record("bootstrap").pipe(Effect.as({ projectId: "project-1" })), }); - }), -); - -it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and thread ids", () => { - const bootstrapProjectId = ProjectId.make("project-startup-bootstrap"); - const bootstrapThreadId = ThreadId.make("thread-startup-bootstrap"); - return Effect.gen(function* () { - const dispatchCalls = yield* Ref.make>([]); - const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe( - Effect.provideService(ServerConfig.ServerConfig, { - cwd: "/tmp/startup-project", - autoBootstrapProjectFromCwd: true, - } as never), - Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => Effect.die("unused"), - getSnapshot: () => Effect.die("unused"), - getShellSnapshot: () => Effect.die("unused"), - getArchivedShellSnapshot: () => Effect.die("unused"), - getSnapshotSequence: () => Effect.die("unused"), - getCounts: () => Effect.die("unused"), - getActiveProjectByWorkspaceRoot: () => - Effect.succeed( - Option.some({ - id: bootstrapProjectId, - title: "Startup Project", - workspaceRoot: "/tmp/startup-project", - defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), - scripts: [], - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", - deletedAt: null, - }), - ), - getProjectShellById: () => Effect.die("unused"), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.some(bootstrapThreadId)), - getThreadCheckpointContext: () => Effect.succeed(Option.none()), - getFullThreadDiffContext: () => Effect.succeed(Option.none()), - getThreadShellById: () => Effect.die("unused"), - getThreadDetailById: () => Effect.die("unused"), - }), - Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { - readEvents: () => Stream.empty, - dispatch: (command) => - Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe( - Effect.as({ sequence: 1 }), - ), - streamDomainEvents: Stream.empty, - } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]), - Effect.provide(NodeServices.layer), - ); - - assert.deepStrictEqual(targets, { - bootstrapProjectId, - bootstrapThreadId, + assert.deepEqual(yield* Ref.get(calls), [ + "verify", + "rebuild", + "recover", + "worker", + "bootstrap", + ]); + assert.deepEqual(result, { + recovery: { resumedSessions: 2 }, + bootstrap: { projectId: "project-1" }, }); - assert.deepStrictEqual(yield* Ref.get(dispatchCalls), []); - }); -}); - -it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when missing", () => - Effect.gen(function* () { - const dispatchCalls = yield* Ref.make>([]); - const targets = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe( - Effect.provideService(ServerConfig.ServerConfig, { - cwd: "/tmp/startup-project", - autoBootstrapProjectFromCwd: true, - } as never), - Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => Effect.die("unused"), - getSnapshot: () => Effect.die("unused"), - getShellSnapshot: () => Effect.die("unused"), - getArchivedShellSnapshot: () => Effect.die("unused"), - getSnapshotSequence: () => Effect.die("unused"), - getCounts: () => Effect.die("unused"), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getProjectShellById: () => Effect.die("unused"), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => Effect.succeed(Option.none()), - getFullThreadDiffContext: () => Effect.succeed(Option.none()), - getThreadShellById: () => Effect.die("unused"), - getThreadDetailById: () => Effect.die("unused"), - }), - Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { - readEvents: () => Stream.empty, - dispatch: (command) => - Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe( - Effect.as({ sequence: 1 }), - ), - streamDomainEvents: Stream.empty, - } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]), - Effect.provide(NodeServices.layer), - ); - - assert.equal(typeof targets.bootstrapProjectId, "string"); - assert.equal(typeof targets.bootstrapThreadId, "string"); - assert.deepStrictEqual(yield* Ref.get(dispatchCalls), ["project.create", "thread.create"]); }), ); -it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation failures", () => +it.effect("does not rebuild valid projections", () => Effect.gen(function* () { - const crypto = yield* Crypto.Crypto; - const uuidError = PlatformError.systemError({ - _tag: "Unknown", - module: "Crypto", - method: "randomUUIDv4", - description: "UUID generation unavailable", + const rebuilt = yield* Ref.make(false); + yield* ServerRuntimeStartup.runOrderedV2StartupPhases({ + verify: Effect.succeed({ valid: true }), + rebuild: Ref.set(rebuilt, true).pipe(Effect.as({ valid: true })), + recover: Effect.void, + startEffectWorker: Effect.void, + autoBootstrap: Effect.void, }); - const dispatchCalls = yield* Ref.make>([]); + assert.isFalse(yield* Ref.get(rebuilt)); + }), +); - const error = yield* ServerRuntimeStartup.resolveAutoBootstrapWelcomeTargets.pipe( - Effect.provideService(ServerConfig.ServerConfig, { - cwd: "/tmp/startup-project", - autoBootstrapProjectFromCwd: true, - } as never), - Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { - getCommandReadModel: () => Effect.die("unused"), - getSnapshot: () => Effect.die("unused"), - getShellSnapshot: () => Effect.die("unused"), - getArchivedShellSnapshot: () => Effect.die("unused"), - getSnapshotSequence: () => Effect.die("unused"), - getCounts: () => Effect.die("unused"), - getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), - getProjectShellById: () => Effect.die("unused"), - getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), - getThreadCheckpointContext: () => Effect.succeed(Option.none()), - getFullThreadDiffContext: () => Effect.succeed(Option.none()), - getThreadShellById: () => Effect.die("unused"), - getThreadDetailById: () => Effect.die("unused"), - }), - Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { - readEvents: () => Stream.empty, - dispatch: (command) => - Ref.update(dispatchCalls, (calls) => [...calls, command.type]).pipe( - Effect.as({ sequence: 1 }), - ), - streamDomainEvents: Stream.empty, - } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]), - Effect.provideService(Crypto.Crypto, { - ...crypto, - randomUUIDv4: Effect.fail(uuidError), - }), - Effect.flip, - ); +it.effect("queues commands until startup signals readiness", () => + Effect.scoped( + Effect.gen(function* () { + const gate = yield* ServerRuntimeStartup.makeCommandGate; + const count = yield* Ref.make(0); + const queued = yield* gate + .enqueueCommand(Ref.updateAndGet(count, (value) => value + 1)) + .pipe(Effect.forkScoped); - assert.strictEqual(error, uuidError); - assert.deepStrictEqual(yield* Ref.get(dispatchCalls), []); - }).pipe(Effect.provide(NodeServices.layer)), + yield* Effect.yieldNow; + assert.equal(yield* Ref.get(count), 0); + yield* gate.signalCommandReady; + assert.equal(yield* Fiber.join(queued), 1); + }), + ), ); diff --git a/apps/server/src/serverRuntimeStartup.ts b/apps/server/src/serverRuntimeStartup.ts index b52b577c5b5..b7b39bc2d70 100644 --- a/apps/server/src/serverRuntimeStartup.ts +++ b/apps/server/src/serverRuntimeStartup.ts @@ -15,25 +15,26 @@ import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; import * as ServerConfig from "./config.ts"; import * as Keybindings from "./keybindings.ts"; import * as ExternalLauncher from "./process/externalLauncher.ts"; -import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; -import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; -import * as OrchestrationReactor from "./orchestration/Services/OrchestrationReactor.ts"; +import * as EffectWorker from "./orchestration-v2/EffectWorker.ts"; +import * as ProjectionMaintenance from "./orchestration-v2/ProjectionMaintenance.ts"; +import * as ProviderRuntimeRecovery from "./orchestration-v2/ProviderRuntimeRecoveryService.ts"; +import * as ThreadLaunch from "./orchestration-v2/ThreadLaunchService.ts"; +import * as ThreadManagement from "./orchestration-v2/ThreadManagementService.ts"; +import * as ProjectService from "./project/ProjectService.ts"; +import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts"; import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; import * as ServerSettings from "./serverSettings.ts"; import * as AnalyticsService from "./telemetry/AnalyticsService.ts"; import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; -import * as ProviderSessionReaper from "./provider/Services/ProviderSessionReaper.ts"; import { formatHeadlessServeOutput, formatHostForUrl, @@ -132,11 +133,19 @@ export const makeCommandGate = Effect.gen(function* () { export const recordStartupHeartbeat = Effect.gen(function* () { const analytics = yield* AnalyticsService.AnalyticsService; - const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; + const projects = yield* ProjectService.ProjectService; + const threads = yield* ThreadManagement.ThreadManagementService; - const { threadCount, projectCount } = yield* projectionSnapshotQuery.getCounts().pipe( + const { threadCount, projectCount } = yield* Effect.all({ + projects: projects.snapshot, + threads: threads.getShellSnapshot(), + }).pipe( + Effect.map(({ projects: projectSnapshot, threads: shellSnapshot }) => ({ + projectCount: projectSnapshot.projects.length, + threadCount: shellSnapshot.threads.length + shellSnapshot.archivedThreads.length, + })), Effect.catch((cause) => - Effect.logWarning("failed to gather startup projection counts for telemetry", { + Effect.logWarning("failed to gather V2 startup counts for telemetry", { cause, }).pipe( Effect.as({ @@ -166,6 +175,11 @@ export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({ model: DEFAULT_MODEL, }); +interface AutoBootstrapWelcomeTargets { + readonly bootstrapProjectId?: ProjectId; + readonly bootstrapThreadId?: ThreadId; +} + export const resolveWelcomeBase = Effect.gen(function* () { const serverConfig = yield* ServerConfig.ServerConfig; const segments = serverConfig.cwd.split(/[/\\]/).filter(Boolean); @@ -181,72 +195,52 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { const crypto = yield* Crypto.Crypto; const randomUUID = crypto.randomUUIDv4; const serverConfig = yield* ServerConfig.ServerConfig; - const projectionReadModelQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; - const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService; + const projects = yield* ProjectService.ProjectService; + const threads = yield* ThreadManagement.ThreadManagementService; + const threadLaunch = yield* ThreadLaunch.ThreadLaunchService; const path = yield* Path.Path; let bootstrapProjectId: ProjectId | undefined; let bootstrapThreadId: ThreadId | undefined; if (serverConfig.autoBootstrapProjectFromCwd) { - yield* Effect.gen(function* () { - const existingProject = yield* projectionReadModelQuery.getActiveProjectByWorkspaceRoot( - serverConfig.cwd, - ); - let nextProjectId: ProjectId; - let nextProjectDefaultModelSelection: ModelSelection; - - if (Option.isNone(existingProject)) { - const createdAt = DateTime.formatIso(yield* DateTime.now); - nextProjectId = ProjectId.make(yield* randomUUID); - const bootstrapProjectTitle = path.basename(serverConfig.cwd) || "project"; - nextProjectDefaultModelSelection = getAutoBootstrapDefaultModelSelection(); - yield* orchestrationEngine.dispatch({ - type: "project.create", - commandId: CommandId.make(yield* randomUUID), - projectId: nextProjectId, - title: bootstrapProjectTitle, - workspaceRoot: serverConfig.cwd, - defaultModelSelection: nextProjectDefaultModelSelection, - createdAt, - }); - } else { - nextProjectId = existingProject.value.id; - nextProjectDefaultModelSelection = - existingProject.value.defaultModelSelection ?? getAutoBootstrapDefaultModelSelection(); - } - - const existingThreadId = - yield* projectionReadModelQuery.getFirstActiveThreadIdByProjectId(nextProjectId); - if (Option.isNone(existingThreadId)) { - const createdAt = DateTime.formatIso(yield* DateTime.now); - const createdThreadId = ThreadId.make(yield* randomUUID); - yield* orchestrationEngine.dispatch({ - type: "thread.create", - commandId: CommandId.make(yield* randomUUID), - threadId: createdThreadId, - projectId: nextProjectId, - title: "New thread", - modelSelection: nextProjectDefaultModelSelection, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt, - }); - bootstrapProjectId = nextProjectId; - bootstrapThreadId = createdThreadId; - } else { - bootstrapProjectId = nextProjectId; - bootstrapThreadId = existingThreadId.value; - } + const defaultModelSelection = getAutoBootstrapDefaultModelSelection(); + const { project } = yield* projects.bootstrap({ + commandId: CommandId.make(yield* randomUUID), + projectId: ProjectId.make(yield* randomUUID), + title: path.basename(serverConfig.cwd) || "project", + workspaceRoot: serverConfig.cwd, + defaultModelSelection, }); + const shell = yield* threads.getShellSnapshot(); + const existingThread = shell.threads.find( + (thread) => + thread.projectId === project.id && thread.lineage.relationshipToParent !== "subagent", + ); + if (existingThread === undefined) { + const launched = yield* threadLaunch.launch({ + commandId: CommandId.make(yield* randomUUID), + projectId: project.id, + title: "New thread", + modelSelection: project.defaultModelSelection ?? defaultModelSelection, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "full-access", + workspaceStrategy: { type: "root" }, + createdBy: "system", + creationSource: "server", + }); + bootstrapProjectId = project.id; + bootstrapThreadId = launched.threadId; + } else { + bootstrapProjectId = project.id; + bootstrapThreadId = existingThread.id; + } } return { ...(bootstrapProjectId ? { bootstrapProjectId } : {}), ...(bootstrapThreadId ? { bootstrapThreadId } : {}), - } as const; + } satisfies AutoBootstrapWelcomeTargets; }); const resolveStartupBrowserTarget = Effect.gen(function* () { @@ -288,11 +282,51 @@ const runStartupPhase = (phase: string, effect: Effect.Effect) Effect.withSpan(`server.startup.${phase}`), ); +export function runOrderedV2StartupPhases< + Verification extends { readonly valid: boolean }, + RebuildVerification extends { readonly valid: boolean }, + Recovery, + Bootstrap, + VerifyError, + RebuildError, + RecoveryError, + WorkerError, + BootstrapError, + VerifyContext, + RebuildContext, + RecoveryContext, + WorkerContext, + BootstrapContext, +>(input: { + readonly verify: Effect.Effect; + readonly rebuild: Effect.Effect; + readonly recover: Effect.Effect; + readonly startEffectWorker: Effect.Effect; + readonly autoBootstrap: Effect.Effect; +}) { + return Effect.gen(function* () { + const verification = yield* input.verify; + if (!verification.valid) { + const rebuilt = yield* input.rebuild; + if (!rebuilt.valid) { + return yield* Effect.die( + new Error("V2 orchestration projection rebuild did not produce a valid projection."), + ); + } + } + const recovery = yield* input.recover; + yield* input.startEffectWorker; + const bootstrap = yield* input.autoBootstrap; + return { recovery, bootstrap } as const; + }); +} + export const make = Effect.gen(function* () { const serverConfig = yield* ServerConfig.ServerConfig; const keybindings = yield* Keybindings.Keybindings; - const orchestrationReactor = yield* OrchestrationReactor.OrchestrationReactor; - const providerSessionReaper = yield* ProviderSessionReaper.ProviderSessionReaper; + const projectionMaintenance = yield* ProjectionMaintenance.ProjectionMaintenanceV2; + const providerRuntimeRecovery = yield* ProviderRuntimeRecovery.ProviderRuntimeRecoveryService; + const agentAwarenessRelay = yield* AgentAwarenessRelay.AgentAwarenessRelay; const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents; const serverSettings = yield* ServerSettings.ServerSettingsService; const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; @@ -300,9 +334,6 @@ export const make = Effect.gen(function* () { const commandGate = yield* makeCommandGate; const httpListening = yield* Deferred.make(); - const reactorScope = yield* Scope.make("sequential"); - - yield* Effect.addFinalizer(() => Scope.close(reactorScope, Exit.void)); const startup = Effect.gen(function* () { yield* Effect.logDebug("startup phase: starting keybindings runtime"); @@ -337,22 +368,57 @@ export const make = Effect.gen(function* () { ), ); - yield* Effect.logDebug("startup phase: starting orchestration reactors"); - yield* runStartupPhase( - "reactors.start", - Effect.gen(function* () { - yield* orchestrationReactor.start().pipe(Scope.provide(reactorScope)); - yield* providerSessionReaper.start().pipe(Scope.provide(reactorScope)); - }), - ); - const welcomeBase = yield* resolveWelcomeBase; const environment = yield* serverEnvironment.getDescriptor; - yield* Effect.logDebug("startup phase: preparing welcome payload"); + const { recovery, bootstrap: bootstrapTargets } = yield* runOrderedV2StartupPhases({ + verify: runStartupPhase( + "orchestration-v2.projections.verify", + projectionMaintenance.verify.pipe( + Effect.tap((verification) => + verification.valid + ? Effect.void + : Effect.logWarning("V2 orchestration projections are stale; rebuilding", { + expectedSequence: verification.expectedSequence, + projectionSequence: verification.projectionSequence, + schemaVersion: verification.schemaVersion, + missingThreadCount: verification.missingThreadIds.length, + unexpectedThreadCount: verification.unexpectedThreadIds.length, + differingThreadCount: verification.differingThreadIds.length, + }), + ), + ), + ), + rebuild: runStartupPhase( + "orchestration-v2.projections.rebuild", + projectionMaintenance.rebuild, + ), + recover: runStartupPhase("orchestration-v2.recovery", providerRuntimeRecovery.recover), + startEffectWorker: runStartupPhase( + "orchestration-v2.effect-worker.start", + Effect.gen(function* () { + yield* EffectWorker.runDaemon.pipe(Effect.forkScoped); + yield* agentAwarenessRelay.start(); + }), + ), + autoBootstrap: (serverConfig.autoBootstrapProjectFromCwd + ? runStartupPhase( + "welcome.autobootstrap", + resolveAutoBootstrapWelcomeTargets.pipe(Effect.provideService(Crypto.Crypto, crypto)), + ) + : Effect.succeed({}) + ).pipe(Effect.map((targets): AutoBootstrapWelcomeTargets => targets)), + }); + yield* Effect.logInfo("V2 orchestration recovery completed", recovery); + + yield* Effect.logDebug("Accepting commands"); + yield* commandGate.signalCommandReady; + yield* Effect.logDebug("startup phase: publishing welcome event", { environmentId: environment.environmentId, cwd: welcomeBase.cwd, projectName: welcomeBase.projectName, + bootstrapProjectId: bootstrapTargets.bootstrapProjectId, + bootstrapThreadId: bootstrapTargets.bootstrapThreadId, }); yield* runStartupPhase( "welcome.publish", @@ -362,48 +428,10 @@ export const make = Effect.gen(function* () { payload: { environment, ...welcomeBase, + ...bootstrapTargets, }, }), ); - - if (serverConfig.autoBootstrapProjectFromCwd) { - yield* Effect.forkScoped( - runStartupPhase( - "welcome.autobootstrap", - Effect.gen(function* () { - const bootstrapTargets = yield* resolveAutoBootstrapWelcomeTargets.pipe( - Effect.provideService(Crypto.Crypto, crypto), - ); - if (!bootstrapTargets.bootstrapProjectId && !bootstrapTargets.bootstrapThreadId) { - return; - } - - yield* Effect.logDebug("startup phase: publishing bootstrapped welcome event", { - environmentId: environment.environmentId, - cwd: welcomeBase.cwd, - projectName: welcomeBase.projectName, - bootstrapProjectId: bootstrapTargets.bootstrapProjectId, - bootstrapThreadId: bootstrapTargets.bootstrapThreadId, - }); - yield* lifecycleEvents.publish({ - version: 1, - type: "welcome", - payload: { - environment, - ...welcomeBase, - ...bootstrapTargets, - }, - }); - }).pipe( - Effect.catch((cause) => - Effect.logWarning("startup auto-bootstrap welcome failed", { - cause, - }), - ), - ), - ), - ); - } }).pipe( Effect.annotateSpans({ "server.mode": serverConfig.mode, @@ -428,8 +456,6 @@ export const make = Effect.gen(function* () { return; } - yield* Effect.logDebug("Accepting commands"); - yield* commandGate.signalCommandReady; yield* Effect.logDebug("startup phase: waiting for http listener"); yield* runStartupPhase("http.wait", Deferred.await(httpListening)); yield* Effect.logDebug("startup phase: publishing ready event"); diff --git a/apps/server/src/textGeneration/TextGeneration.test.ts b/apps/server/src/textGeneration/TextGeneration.test.ts index 9bccb9c1fc5..9966c37ddad 100644 --- a/apps/server/src/textGeneration/TextGeneration.test.ts +++ b/apps/server/src/textGeneration/TextGeneration.test.ts @@ -38,7 +38,7 @@ const makeStubInstance = ( displayName: undefined, enabled: true, snapshot: {} as ProviderInstance["snapshot"], - adapter: {} as ProviderInstance["adapter"], + orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"], textGeneration, }) satisfies ProviderInstance; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 7ebc432038c..1f3f9e9d3b3 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -1,13 +1,14 @@ -import * as Cause from "effect/Cause"; -import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; +import * as Encoding from "effect/Encoding"; import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; +import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; -import * as Schema from "effect/Schema"; +import * as Result from "effect/Result"; import * as Stream from "effect/Stream"; import { DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL, @@ -23,30 +24,33 @@ import { AuthSessionId, CommandId, type DiscoveredLocalServerList, - EventId, - type OrchestrationCommand, type GitActionProgressEvent, type GitManagerServiceError, - OrchestrationDispatchCommandError, - type OrchestrationEvent, - type OrchestrationShellStreamEvent, + type MessageId, OrchestrationGetFullThreadDiffError, - OrchestrationGetSnapshotError, OrchestrationGetTurnDiffError, + ORCHESTRATION_V2_WS_METHODS, + OrchestrationV2DispatchCommandError, + OrchestrationV2GetShellSnapshotError, + OrchestrationV2GetThreadProjectionError, + OrchestrationV2ThreadLaunchError, ORCHESTRATION_WS_METHODS, type ProjectEntriesFailure, type ProjectFileFailure, type ProjectFileOperation, + type ProjectMutation, ProjectListEntriesError, ProjectReadFileError, ProjectSearchEntriesError, ProjectWriteFileError, + ProjectMutationError, RelayClientInstallFailedError, type RelayClientInstallProgressEvent, - OrchestrationReplayEventsError, type FilesystemBrowseFailure, FilesystemBrowseError, AssetAccessError, + ChatAttachmentId, + PersistChatAttachmentsError, EnvironmentAuthorizationError, ThreadId, type TerminalAttachStreamEvent, @@ -56,17 +60,23 @@ import { WS_METHODS, WsRpcGroup, } from "@t3tools/contracts"; -import { clamp } from "effect/Number"; import { HttpRouter, HttpServerRequest, HttpServerRespondable } from "effect/unstable/http"; import { RpcSerialization, RpcServer } from "effect/unstable/rpc"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; import * as ServerConfig from "./config.ts"; import * as Keybindings from "./keybindings.ts"; import * as ExternalLauncher from "./process/externalLauncher.ts"; -import { normalizeDispatchCommand } from "./orchestration/Normalizer.ts"; -import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; +import * as ThreadManagementService from "./orchestration-v2/ThreadManagementService.ts"; +import * as ThreadLaunchService from "./orchestration-v2/ThreadLaunchService.ts"; +import { + archivedShellStreamItemFromSnapshot, + shellStreamItemFromSnapshot, +} from "./orchestration-v2/ShellStream.ts"; import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; +import * as OrchestrationEventStore from "./persistence/Services/OrchestrationEventStore.ts"; +import { userFacingDispatchErrorMessage } from "./orchestration-v2/UserFacingErrors.ts"; import { observeRpcEffect as instrumentRpcEffect, observeRpcStream as instrumentRpcStream, @@ -81,6 +91,8 @@ import * as TerminalManager from "./terminal/Manager.ts"; import * as PreviewAutomationBroker from "./mcp/PreviewAutomationBroker.ts"; import * as PreviewManager from "./preview/Manager.ts"; import { issueAssetUrl } from "./assets/AssetAccess.ts"; +import { attachmentRelativePath, createDeterministicAttachmentId } from "./attachmentStore.ts"; +import { parseBase64DataUrl } from "./imageMime.ts"; import * as PortScanner from "./preview/PortScanner.ts"; import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts"; import * as WorkspaceFileSystem from "./workspace/WorkspaceFileSystem.ts"; @@ -89,8 +101,7 @@ import * as VcsStatusBroadcaster from "./vcs/VcsStatusBroadcaster.ts"; import * as VcsProvisioningService from "./vcs/VcsProvisioningService.ts"; import * as GitWorkflowService from "./git/GitWorkflowService.ts"; import * as ReviewService from "./review/ReviewService.ts"; -import * as ProjectSetupScriptRunner from "./project/ProjectSetupScriptRunner.ts"; -import * as RepositoryIdentityResolver from "./project/RepositoryIdentityResolver.ts"; +import * as ProjectService from "./project/ProjectService.ts"; import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; @@ -111,26 +122,77 @@ import * as PairingGrantStore from "./auth/PairingGrantStore.ts"; import * as SessionStore from "./auth/SessionStore.ts"; import { failEnvironmentAuthInvalid, failEnvironmentInternal } from "./auth/http.ts"; import * as RelayClient from "@t3tools/shared/relayClient"; -const isOrchestrationDispatchCommandError = Schema.is(OrchestrationDispatchCommandError); - -const nowIso = Effect.map(DateTime.now, DateTime.formatIso); function unexpectedCompatibilityError(error: never): never { throw new Error(`Unhandled compatibility error: ${String(error)}`); } -/** Preserve the setup runner's broader pre-refactor message normalization. */ -function legacySetupFailureDescription(cause: unknown): string { - if ( - typeof cause === "object" && - cause !== null && - "message" in cause && - typeof cause.message === "string" - ) { - return cause.message; - } - return String(cause); -} +const persistChatAttachments = Effect.fn("ws.assets.persistChatAttachments")(function* (input: { + readonly threadId: ThreadId; + readonly messageId: MessageId; + readonly attachments: ReadonlyArray<{ + readonly type: "image"; + readonly name: string; + readonly mimeType: string; + readonly sizeBytes: number; + readonly dataUrl: string; + }>; +}) { + const config = yield* ServerConfig.ServerConfig; + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + return yield* Effect.forEach( + input.attachments.map((attachment, index) => ({ attachment, index })), + Effect.fn("ws.assets.persistChatAttachment")(function* ({ attachment, index }) { + const parsed = parseBase64DataUrl(attachment.dataUrl); + if (parsed === null || parsed.mimeType !== attachment.mimeType.toLowerCase()) { + return yield* new PersistChatAttachmentsError({ + message: `Attachment ${attachment.name} has an invalid image payload.`, + }); + } + const bytes = yield* Effect.fromResult(Encoding.decodeBase64(parsed.base64)).pipe( + Effect.mapError( + (cause) => + new PersistChatAttachmentsError({ + message: `Attachment ${attachment.name} is not valid base64.`, + cause, + }), + ), + ); + if (bytes.byteLength !== attachment.sizeBytes) { + return yield* new PersistChatAttachmentsError({ + message: `Attachment ${attachment.name} size does not match its payload.`, + }); + } + const rawId = createDeterministicAttachmentId(input.threadId, `${input.messageId}:${index}`); + if (rawId === null) { + return yield* new PersistChatAttachmentsError({ + message: "Could not allocate an attachment identifier.", + }); + } + const persisted = { + type: "image" as const, + id: ChatAttachmentId.make(rawId), + name: attachment.name, + mimeType: attachment.mimeType, + sizeBytes: attachment.sizeBytes, + }; + yield* fileSystem + .writeFile(path.join(config.attachmentsDir, attachmentRelativePath(persisted)), bytes) + .pipe( + Effect.mapError( + (cause) => + new PersistChatAttachmentsError({ + message: `Could not persist attachment ${attachment.name}.`, + cause, + }), + ), + ); + return persisted; + }), + { concurrency: 2 }, + ); +}); function projectEntriesFailureContext(error: WorkspaceEntries.WorkspaceEntriesError): { readonly failure: ProjectEntriesFailure; @@ -236,51 +298,18 @@ function projectFileFailureContext( } } -function projectSetupScriptCompatibilityDetail( - error: ProjectSetupScriptRunner.ProjectSetupScriptRunnerError, -): string { - switch (error._tag) { - case "ProjectSetupScriptOperationError": - return legacySetupFailureDescription(error.cause); - case "ProjectSetupScriptProjectNotFoundError": - return "Project was not found for setup script execution."; - default: - return unexpectedCompatibilityError(error); - } -} - -function isThreadDetailEvent(event: OrchestrationEvent): event is Extract< - OrchestrationEvent, - { - type: - | "thread.message-sent" - | "thread.proposed-plan-upserted" - | "thread.activity-appended" - | "thread.turn-diff-completed" - | "thread.reverted" - | "thread.session-set"; - } -> { - return ( - event.type === "thread.message-sent" || - event.type === "thread.proposed-plan-upserted" || - event.type === "thread.activity-appended" || - event.type === "thread.turn-diff-completed" || - event.type === "thread.reverted" || - event.type === "thread.session-set" - ); -} - const PROVIDER_STATUS_DEBOUNCE_MS = 200; const RPC_REQUIRED_SCOPE = new Map([ - [ORCHESTRATION_WS_METHODS.dispatchCommand, AuthOrchestrationOperateScope], - [ORCHESTRATION_WS_METHODS.getTurnDiff, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.getFullThreadDiff, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.replayEvents, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.subscribeShell, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope], - [ORCHESTRATION_WS_METHODS.subscribeThread, AuthOrchestrationReadScope], + [ORCHESTRATION_V2_WS_METHODS.dispatchCommand, AuthOrchestrationOperateScope], + [ORCHESTRATION_V2_WS_METHODS.getTurnDiff, AuthOrchestrationReadScope], + [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff, AuthOrchestrationReadScope], + [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope], + [ORCHESTRATION_V2_WS_METHODS.getThreadProjection, AuthOrchestrationReadScope], + [ORCHESTRATION_V2_WS_METHODS.launchThread, AuthOrchestrationOperateScope], + [ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell, AuthOrchestrationReadScope], + [ORCHESTRATION_V2_WS_METHODS.subscribeShell, AuthOrchestrationReadScope], + [ORCHESTRATION_V2_WS_METHODS.subscribeThread, AuthOrchestrationReadScope], [WS_METHODS.serverGetConfig, AuthOrchestrationReadScope], [WS_METHODS.serverRefreshProviders, AuthOrchestrationOperateScope], [WS_METHODS.serverUpdateProvider, AuthOrchestrationOperateScope], @@ -302,9 +331,11 @@ const RPC_REQUIRED_SCOPE = new Map([ [WS_METHODS.projectsReadFile, AuthOrchestrationReadScope], [WS_METHODS.projectsSearchEntries, AuthOrchestrationReadScope], [WS_METHODS.projectsWriteFile, AuthOrchestrationOperateScope], + [WS_METHODS.projectsMutate, AuthOrchestrationOperateScope], [WS_METHODS.shellOpenInEditor, AuthOrchestrationOperateScope], [WS_METHODS.filesystemBrowse, AuthOrchestrationReadScope], [WS_METHODS.assetsCreateUrl, AuthOrchestrationReadScope], + [WS_METHODS.assetsPersistChatAttachments, AuthOrchestrationOperateScope], [WS_METHODS.subscribeVcsStatus, AuthOrchestrationReadScope], [WS_METHODS.vcsRefreshStatus, AuthOrchestrationReadScope], [WS_METHODS.vcsPull, AuthOrchestrationOperateScope], @@ -344,6 +375,16 @@ const RPC_REQUIRED_SCOPE = new Map([ [WS_METHODS.subscribeAuthAccess, AuthAccessReadScope], ]); +const ServerWsRpcGroup = WsRpcGroup.omit( + ORCHESTRATION_WS_METHODS.dispatchCommand, + ORCHESTRATION_WS_METHODS.getTurnDiff, + ORCHESTRATION_WS_METHODS.getFullThreadDiff, + ORCHESTRATION_WS_METHODS.replayEvents, + ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, + ORCHESTRATION_WS_METHODS.subscribeShell, + ORCHESTRATION_WS_METHODS.subscribeThread, +); + function toAuthAccessStreamEvent( change: PairingGrantStore.BootstrapCredentialChange | SessionStore.SessionCredentialChange, revision: number, @@ -385,12 +426,15 @@ function toAuthAccessStreamEvent( } const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => - WsRpcGroup.toLayer( + ServerWsRpcGroup.toLayer( Effect.gen(function* () { const currentSessionId = currentSession.sessionId; - const crypto = yield* Crypto.Crypto; + const sql = yield* SqlClient.SqlClient; + const threadManagement = yield* ThreadManagementService.ThreadManagementService; + const applicationEvents = yield* OrchestrationEventStore.OrchestrationEventStore; const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; - const orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService; + const threadLaunch = yield* ThreadLaunchService.ThreadLaunchService; + const projectService = yield* ProjectService.ProjectService; const checkpointDiffQuery = yield* CheckpointDiffQuery.CheckpointDiffQuery; const keybindings = yield* Keybindings.Keybindings; const externalLauncher = yield* ExternalLauncher.ExternalLauncher; @@ -410,9 +454,6 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup; const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; - const projectSetupScriptRunner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner; - const repositoryIdentityResolver = - yield* RepositoryIdentityResolver.RepositoryIdentityResolver; const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const sourceControlDiscovery = yield* SourceControlDiscovery.SourceControlDiscovery; @@ -491,22 +532,6 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => authorizeEffect(requiredScopeForMethod(method), effect), traceAttributes, ); - const toDispatchCommandError = (cause: unknown, fallbackMessage: string) => - isOrchestrationDispatchCommandError(cause) - ? cause - : new OrchestrationDispatchCommandError({ - message: cause instanceof Error ? cause.message : fallbackMessage, - cause, - }); - const randomUUID = crypto.randomUUIDv4.pipe( - Effect.mapError((cause) => - toDispatchCommandError(cause, "Failed to generate orchestration command identifier."), - ), - ); - const serverEventId = randomUUID.pipe(Effect.map(EventId.make)); - const serverCommandId = (tag: string) => - randomUUID.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`))); - const loadAuthAccessSnapshot = () => Effect.all({ pairingLinks: serverAuth.listPairingLinks(), @@ -520,387 +545,6 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => ), ); - const appendSetupScriptActivity = (input: { - readonly threadId: ThreadId; - readonly kind: "setup-script.requested" | "setup-script.started" | "setup-script.failed"; - readonly summary: string; - readonly createdAt: string; - readonly payload: Record; - readonly tone: "info" | "error"; - }) => - Effect.all({ - commandId: serverCommandId("setup-script-activity"), - activityId: serverEventId, - }).pipe( - Effect.flatMap(({ commandId, activityId }) => - orchestrationEngine.dispatch({ - type: "thread.activity.append", - commandId, - threadId: input.threadId, - activity: { - id: activityId, - tone: input.tone, - kind: input.kind, - summary: input.summary, - payload: input.payload, - turnId: null, - createdAt: input.createdAt, - }, - createdAt: input.createdAt, - }), - ), - ); - - const toBootstrapDispatchCommandCauseError = (cause: Cause.Cause) => { - const error = Cause.squash(cause); - return isOrchestrationDispatchCommandError(error) - ? error - : new OrchestrationDispatchCommandError({ - message: - error instanceof Error ? error.message : "Failed to bootstrap thread turn start.", - cause, - }); - }; - - const enrichProjectEvent = ( - event: OrchestrationEvent, - ): Effect.Effect => { - switch (event.type) { - case "project.created": - return repositoryIdentityResolver.resolve(event.payload.workspaceRoot).pipe( - Effect.map((repositoryIdentity) => ({ - ...event, - payload: { - ...event.payload, - repositoryIdentity, - }, - })), - ); - case "project.meta-updated": - return Effect.gen(function* () { - const workspaceRoot = - event.payload.workspaceRoot ?? - Option.match( - yield* projectionSnapshotQuery.getProjectShellById(event.payload.projectId), - { - onNone: () => null, - onSome: (project) => project.workspaceRoot, - }, - ) ?? - null; - if (workspaceRoot === null) { - return event; - } - - const repositoryIdentity = yield* repositoryIdentityResolver.resolve(workspaceRoot); - return { - ...event, - payload: { - ...event.payload, - repositoryIdentity, - }, - } satisfies OrchestrationEvent; - }).pipe(Effect.orElseSucceed(() => event)); - default: - return Effect.succeed(event); - } - }; - - const enrichOrchestrationEvents = (events: ReadonlyArray) => - Effect.forEach(events, enrichProjectEvent, { concurrency: 4 }); - - const toShellStreamEvent = ( - event: OrchestrationEvent, - ): Effect.Effect, never, never> => { - switch (event.type) { - case "project.created": - case "project.meta-updated": - return projectionSnapshotQuery.getProjectShellById(event.payload.projectId).pipe( - Effect.map((project) => - Option.map(project, (nextProject) => ({ - kind: "project-upserted" as const, - sequence: event.sequence, - project: nextProject, - })), - ), - Effect.orElseSucceed(() => Option.none()), - ); - case "project.deleted": - return Effect.succeed( - Option.some({ - kind: "project-removed" as const, - sequence: event.sequence, - projectId: event.payload.projectId, - }), - ); - case "thread.deleted": - case "thread.archived": - return Effect.succeed( - Option.some({ - kind: "thread-removed" as const, - sequence: event.sequence, - threadId: event.payload.threadId, - }), - ); - case "thread.unarchived": - return projectionSnapshotQuery.getThreadShellById(event.payload.threadId).pipe( - Effect.map((thread) => - Option.map(thread, (nextThread) => ({ - kind: "thread-upserted" as const, - sequence: event.sequence, - thread: nextThread, - })), - ), - Effect.orElseSucceed(() => Option.none()), - ); - default: - if (event.aggregateKind !== "thread") { - return Effect.succeed(Option.none()); - } - return projectionSnapshotQuery - .getThreadShellById(ThreadId.make(event.aggregateId)) - .pipe( - Effect.map((thread) => - Option.map(thread, (nextThread) => ({ - kind: "thread-upserted" as const, - sequence: event.sequence, - thread: nextThread, - })), - ), - Effect.orElseSucceed(() => Option.none()), - ); - } - }; - - const dispatchBootstrapTurnStart = ( - command: Extract, - ): Effect.Effect<{ readonly sequence: number }, OrchestrationDispatchCommandError> => - Effect.gen(function* () { - const bootstrap = command.bootstrap; - const { bootstrap: _bootstrap, ...finalTurnStartCommand } = command; - let createdThread = false; - let targetProjectId = bootstrap?.createThread?.projectId; - let targetProjectCwd = bootstrap?.prepareWorktree?.projectCwd; - let targetWorktreePath = bootstrap?.createThread?.worktreePath ?? null; - - const cleanupCreatedThread = () => - createdThread - ? serverCommandId("bootstrap-thread-delete").pipe( - Effect.flatMap((commandId) => - orchestrationEngine.dispatch({ - type: "thread.delete", - commandId, - threadId: command.threadId, - }), - ), - Effect.ignoreCause({ log: true }), - ) - : Effect.void; - - const recordSetupScriptLaunchFailure = (input: { - readonly error: ProjectSetupScriptRunner.ProjectSetupScriptRunnerError; - readonly requestedAt: string; - readonly worktreePath: string; - }) => { - const detail = projectSetupScriptCompatibilityDetail(input.error); - return appendSetupScriptActivity({ - threadId: command.threadId, - kind: "setup-script.failed", - summary: "Setup script failed to start", - createdAt: input.requestedAt, - payload: { - detail, - worktreePath: input.worktreePath, - }, - tone: "error", - }).pipe( - Effect.ignoreCause({ log: false }), - Effect.flatMap(() => - Effect.logWarning("bootstrap turn start failed to launch setup script", { - threadId: command.threadId, - worktreePath: input.worktreePath, - detail, - }), - ), - ); - }; - - const recordSetupScriptStarted = (input: { - readonly requestedAt: string; - readonly worktreePath: string; - readonly scriptId: string; - readonly scriptName: string; - readonly terminalId: string; - }) => - Effect.gen(function* () { - const startedAt = yield* nowIso; - const payload = { - scriptId: input.scriptId, - scriptName: input.scriptName, - terminalId: input.terminalId, - worktreePath: input.worktreePath, - }; - yield* Effect.all([ - appendSetupScriptActivity({ - threadId: command.threadId, - kind: "setup-script.requested", - summary: "Starting setup script", - createdAt: input.requestedAt, - payload, - tone: "info", - }), - appendSetupScriptActivity({ - threadId: command.threadId, - kind: "setup-script.started", - summary: "Setup script started", - createdAt: startedAt, - payload, - tone: "info", - }), - ]).pipe( - Effect.asVoid, - Effect.catch((error) => - Effect.logWarning( - "bootstrap turn start launched setup script but failed to record setup activity", - { - threadId: command.threadId, - worktreePath: input.worktreePath, - scriptId: input.scriptId, - terminalId: input.terminalId, - detail: error.message, - }, - ), - ), - ); - }); - - const runSetupProgram = () => - Effect.gen(function* () { - if (!bootstrap?.runSetupScript || !targetWorktreePath) { - return; - } - const worktreePath = targetWorktreePath; - const requestedAt = yield* nowIso; - yield* projectSetupScriptRunner - .runForThread({ - threadId: command.threadId, - ...(targetProjectId ? { projectId: targetProjectId } : {}), - ...(targetProjectCwd ? { projectCwd: targetProjectCwd } : {}), - worktreePath, - }) - .pipe( - Effect.matchEffect({ - onFailure: (error) => - recordSetupScriptLaunchFailure({ - error, - requestedAt, - worktreePath, - }), - onSuccess: (setupResult) => { - if (setupResult.status !== "started") { - return Effect.void; - } - return recordSetupScriptStarted({ - requestedAt, - worktreePath, - scriptId: setupResult.scriptId, - scriptName: setupResult.scriptName, - terminalId: setupResult.terminalId, - }); - }, - }), - ); - }); - - const bootstrapProgram = Effect.gen(function* () { - if (bootstrap?.createThread) { - yield* orchestrationEngine.dispatch({ - type: "thread.create", - commandId: yield* serverCommandId("bootstrap-thread-create"), - threadId: command.threadId, - projectId: bootstrap.createThread.projectId, - title: bootstrap.createThread.title, - modelSelection: bootstrap.createThread.modelSelection, - runtimeMode: bootstrap.createThread.runtimeMode, - interactionMode: bootstrap.createThread.interactionMode, - branch: bootstrap.createThread.branch, - worktreePath: bootstrap.createThread.worktreePath, - createdAt: bootstrap.createThread.createdAt, - }); - createdThread = true; - } - - if (bootstrap?.prepareWorktree) { - let worktreeBaseRef = bootstrap.prepareWorktree.baseBranch; - if (bootstrap.prepareWorktree.startFromOrigin) { - yield* gitWorkflow.fetchRemote({ - cwd: bootstrap.prepareWorktree.projectCwd, - remoteName: "origin", - }); - const resolvedRemoteBase = yield* gitWorkflow.resolveRemoteTrackingCommit({ - cwd: bootstrap.prepareWorktree.projectCwd, - refName: bootstrap.prepareWorktree.baseBranch, - fallbackRemoteName: "origin", - }); - worktreeBaseRef = resolvedRemoteBase.commitSha; - } - const worktree = yield* gitWorkflow.createWorktree({ - cwd: bootstrap.prepareWorktree.projectCwd, - refName: worktreeBaseRef, - newRefName: bootstrap.prepareWorktree.branch, - baseRefName: bootstrap.prepareWorktree.baseBranch, - path: null, - }); - targetWorktreePath = worktree.worktree.path; - yield* orchestrationEngine.dispatch({ - type: "thread.meta.update", - commandId: yield* serverCommandId("bootstrap-thread-meta-update"), - threadId: command.threadId, - branch: worktree.worktree.refName, - worktreePath: targetWorktreePath, - }); - yield* refreshGitStatus(targetWorktreePath); - } - - yield* runSetupProgram(); - - return yield* orchestrationEngine.dispatch(finalTurnStartCommand); - }); - - return yield* bootstrapProgram.pipe( - Effect.catchCause((cause) => { - const dispatchError = toBootstrapDispatchCommandCauseError(cause); - if (Cause.hasInterruptsOnly(cause)) { - return Effect.fail(dispatchError); - } - return cleanupCreatedThread().pipe(Effect.flatMap(() => Effect.fail(dispatchError))); - }), - ); - }); - - const dispatchNormalizedCommand = ( - normalizedCommand: OrchestrationCommand, - ): Effect.Effect<{ readonly sequence: number }, OrchestrationDispatchCommandError> => { - const dispatchEffect = - normalizedCommand.type === "thread.turn.start" && normalizedCommand.bootstrap - ? dispatchBootstrapTurnStart(normalizedCommand) - : orchestrationEngine - .dispatch(normalizedCommand) - .pipe( - Effect.mapError((cause) => - toDispatchCommandError(cause, "Failed to dispatch orchestration command"), - ), - ); - - return startup - .enqueueCommand(dispatchEffect) - .pipe( - Effect.mapError((cause) => - toDispatchCommandError(cause, "Failed to dispatch orchestration command"), - ), - ); - }; - const loadServerConfig = Effect.gen(function* () { const keybindingsConfig = yield* keybindings.loadConfigState; const providers = yield* providerRegistry.getProviders; @@ -938,76 +582,294 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => .refreshStatus(cwd) .pipe(Effect.ignoreCause({ log: true }), Effect.forkDetach, Effect.asVoid); - return WsRpcGroup.of({ - [ORCHESTRATION_WS_METHODS.dispatchCommand]: (command) => - observeRpcEffect( - ORCHESTRATION_WS_METHODS.dispatchCommand, - Effect.gen(function* () { - const normalizedCommand = yield* normalizeDispatchCommand(command); - const shouldStopSessionAfterArchive = - normalizedCommand.type === "thread.archive" - ? yield* projectionSnapshotQuery - .getThreadShellById(normalizedCommand.threadId) - .pipe( - Effect.map( - Option.match({ - onNone: () => false, - onSome: (thread) => - thread.session !== null && thread.session.status !== "stopped", - }), - ), - Effect.orElseSucceed(() => false), - ) - : false; - const result = yield* dispatchNormalizedCommand(normalizedCommand); - if (normalizedCommand.type === "thread.archive") { - if (shouldStopSessionAfterArchive) { - yield* Effect.gen(function* () { - const stopCommand = yield* normalizeDispatchCommand({ - type: "thread.session.stop", - commandId: CommandId.make( - `session-stop-for-archive:${normalizedCommand.commandId}`, - ), - threadId: normalizedCommand.threadId, - createdAt: yield* nowIso, - }); + const subscribeOrchestrationV2Thread = Effect.fn("ws.orchestrationV2.subscribeThread")( + function* (input: { readonly threadId: ThreadId }) { + yield* Effect.annotateCurrentSpan({ + "orchestration_v2.thread_id": input.threadId, + }); - yield* dispatchNormalizedCommand(stopCommand); - }).pipe( - Effect.catchCause((cause) => - Effect.logWarning("failed to stop provider session during archive", { - threadId: normalizedCommand.threadId, - cause, + const snapshot = yield* threadManagement.getThreadSnapshot(input.threadId).pipe( + Effect.mapError( + (cause) => + new OrchestrationV2GetThreadProjectionError({ + threadId: input.threadId, + message: `Failed to load orchestration V2 thread ${input.threadId}`, + cause, + }), + ), + ); + const { projection, snapshotSequence } = snapshot; + + const liveStream = threadManagement + .streamStoredEventsFrom({ + threadId: input.threadId, + afterSequence: snapshotSequence, + }) + .pipe( + Stream.map((stored) => ({ + kind: "event" as const, + sequence: stored.sequence, + event: stored.event, + })), + Stream.mapError( + (cause) => + new OrchestrationV2GetThreadProjectionError({ + threadId: input.threadId, + message: `Failed while streaming orchestration V2 thread ${input.threadId}`, + cause, + }), + ), + ); + + return Stream.concat( + Stream.make({ + kind: "snapshot" as const, + snapshotSequence, + projection, + }), + liveStream, + ); + }, + ); + + const subscribeOrchestrationV2Shell = Effect.fn("ws.orchestrationV2.subscribeShell")( + function* () { + const snapshot = yield* sql + .withTransaction( + Effect.gen(function* () { + const projects = yield* projectionSnapshotQuery.getShellSnapshot(); + const threads = yield* threadManagement.getShellSnapshot(); + return { + schemaVersion: threads.schemaVersion, + snapshotSequence: yield* applicationEvents.latestApplicationSequence, + projects: projects.projects, + threads: threads.threads, + archivedThreads: threads.archivedThreads, + } as const; + }), + ) + .pipe( + Effect.mapError( + (cause) => + new OrchestrationV2GetShellSnapshotError({ + message: "Failed to load the application shell snapshot", + cause, + }), + ), + ); + + const live = applicationEvents + .streamApplicationEvents({ afterSequence: snapshot.snapshotSequence }) + .pipe( + Stream.mapEffect((stored) => + Effect.gen(function* () { + if ("aggregateKind" in stored) { + if (stored.type === "project.deleted") { + return { + kind: "project.removed" as const, + sequence: stored.sequence, + projectId: stored.payload.projectId, + }; + } + const project = yield* projectionSnapshotQuery.getProjectShellById( + stored.payload.projectId, + ); + return Option.match(project, { + onNone: () => ({ + kind: "project.removed" as const, + sequence: stored.sequence, + projectId: stored.payload.projectId, }), - ), - ); - } + onSome: (value) => ({ + kind: "project.updated" as const, + sequence: stored.sequence, + project: value, + }), + }); + } + const nextSnapshot = yield* threadManagement.getShellSnapshot(); + return shellStreamItemFromSnapshot({ stored, snapshot: nextSnapshot }); + }), + ), + Stream.mapError( + (cause) => + new OrchestrationV2GetShellSnapshotError({ + message: "Failed while streaming the application shell", + cause, + }), + ), + ); - yield* terminalManager.close({ threadId: normalizedCommand.threadId }).pipe( - Effect.catch((error) => - Effect.logWarning("failed to close thread terminals after archive", { - threadId: normalizedCommand.threadId, - error: error.message, - }), - ), - ); - } - return result; - }).pipe( - Effect.mapError((cause) => - isOrchestrationDispatchCommandError(cause) - ? cause - : new OrchestrationDispatchCommandError({ - message: "Failed to dispatch orchestration command", + return Stream.concat(Stream.make({ kind: "snapshot" as const, snapshot }), live).pipe( + Stream.mapError( + (cause) => + new OrchestrationV2GetShellSnapshotError({ + message: "Failed while streaming the application shell", + cause, + }), + ), + ); + }, + ); + + const getOrchestrationV2ArchivedShellSnapshot = sql + .withTransaction( + Effect.gen(function* () { + const projects = yield* projectionSnapshotQuery.getShellSnapshot(); + const threads = yield* threadManagement.getShellSnapshot(); + return { + schemaVersion: threads.schemaVersion, + snapshotSequence: yield* applicationEvents.latestApplicationSequence, + projects: projects.projects, + threads: threads.archivedThreads, + } as const; + }), + ) + .pipe( + Effect.mapError( + (cause) => + new OrchestrationV2GetShellSnapshotError({ + message: "Failed to load archived thread snapshot", + cause, + }), + ), + ); + + const subscribeOrchestrationV2ArchivedShell = Effect.fn( + "ws.orchestrationV2.subscribeArchivedShell", + )(function* () { + const snapshot = yield* getOrchestrationV2ArchivedShellSnapshot; + const live = threadManagement + .streamStoredEventsFrom({ afterSequence: snapshot.snapshotSequence }) + .pipe( + Stream.mapEffect((stored) => + threadManagement.getShellSnapshot().pipe( + Effect.map((nextSnapshot) => + archivedShellStreamItemFromSnapshot({ stored, snapshot: nextSnapshot }), + ), + Effect.mapError( + (cause) => + new OrchestrationV2GetShellSnapshotError({ + message: "Failed while streaming archived threads", cause, }), + ), ), ), - { "rpc.aggregate": "orchestration" }, + Stream.filterMap((item) => (item === null ? Result.failVoid : Result.succeed(item))), + Stream.mapError( + (cause) => + new OrchestrationV2GetShellSnapshotError({ + message: "Failed while streaming archived threads", + cause, + }), + ), + ); + return Stream.concat(Stream.make({ kind: "snapshot" as const, snapshot }), live); + }); + + const mutateProject = Effect.fn("ws.projects.mutate")(function* (mutation: ProjectMutation) { + switch (mutation.type) { + case "project.create": + return yield* projectService.create({ + commandId: mutation.commandId, + projectId: mutation.projectId, + title: mutation.title, + workspaceRoot: mutation.workspaceRoot, + ...(mutation.createWorkspaceRootIfMissing === undefined + ? {} + : { createWorkspaceRootIfMissing: mutation.createWorkspaceRootIfMissing }), + ...(mutation.defaultModelSelection === undefined + ? {} + : { defaultModelSelection: mutation.defaultModelSelection }), + ...(mutation.scripts === undefined ? {} : { scripts: mutation.scripts }), + }); + case "project.update": + return yield* projectService.update({ + commandId: mutation.commandId, + projectId: mutation.projectId, + ...(mutation.title === undefined ? {} : { title: mutation.title }), + ...(mutation.workspaceRoot === undefined + ? {} + : { workspaceRoot: mutation.workspaceRoot }), + ...(mutation.defaultModelSelection === undefined + ? {} + : { defaultModelSelection: mutation.defaultModelSelection }), + ...(mutation.scripts === undefined ? {} : { scripts: mutation.scripts }), + }); + case "project.delete": { + const snapshot = yield* threadManagement.getShellSnapshot(); + const projectThreads = [...snapshot.threads, ...snapshot.archivedThreads].filter( + (thread) => thread.projectId === mutation.projectId, + ); + if (projectThreads.length > 0 && mutation.force !== true) { + return yield* new ProjectMutationError({ + commandId: mutation.commandId, + message: `Project ${mutation.projectId} is not empty.`, + }); + } + yield* Effect.forEach( + projectThreads, + (thread) => + threadManagement.dispatch({ + type: "thread.delete", + commandId: CommandId.make(`${mutation.commandId}:delete-thread:${thread.id}`), + threadId: thread.id, + }), + { concurrency: 1, discard: true }, + ); + return yield* projectService.delete({ + commandId: mutation.commandId, + projectId: mutation.projectId, + }); + } + } + }); + + const handlers = ServerWsRpcGroup.of({ + [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: (command) => + observeRpcEffect( + ORCHESTRATION_V2_WS_METHODS.dispatchCommand, + startup + .enqueueCommand( + threadManagement.dispatch( + ThreadManagementService.withCreationProvenance(command, { + createdBy: "user", + creationSource: "creationSource" in command ? command.creationSource : "web", + }), + ), + ) + .pipe( + Effect.map((result) => ({ sequence: result.sequence })), + Effect.mapError((cause) => { + const detail = userFacingDispatchErrorMessage(cause); + return new OrchestrationV2DispatchCommandError({ + commandId: command.commandId, + commandType: command.type, + message: detail ?? "Failed to dispatch orchestration V2 command", + ...(detail === undefined ? {} : { detail }), + cause, + }); + }), + ), + { + "rpc.aggregate": "orchestrationV2", + "orchestration_v2.command_id": command.commandId, + "orchestration_v2.command_type": command.type, + "orchestration_v2.thread_id": + command.type === "thread.fork" || command.type === "thread.merge_back" + ? command.targetThreadId + : command.type === "delegated_task.request" + ? command.parentThreadId + : command.threadId, + ...(command.type === "thread.fork" || command.type === "thread.merge_back" + ? { "orchestration_v2.source_thread_id": command.sourceThreadId } + : {}), + }, ), - [ORCHESTRATION_WS_METHODS.getTurnDiff]: (input) => + [ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: (input) => observeRpcEffect( - ORCHESTRATION_WS_METHODS.getTurnDiff, + ORCHESTRATION_V2_WS_METHODS.getTurnDiff, checkpointDiffQuery.getTurnDiff(input).pipe( Effect.mapError( (cause) => @@ -1019,9 +881,9 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => ), { "rpc.aggregate": "orchestration" }, ), - [ORCHESTRATION_WS_METHODS.getFullThreadDiff]: (input) => + [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: (input) => observeRpcEffect( - ORCHESTRATION_WS_METHODS.getFullThreadDiff, + ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff, checkpointDiffQuery.getFullThreadDiff(input).pipe( Effect.mapError( (cause) => @@ -1033,139 +895,102 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => ), { "rpc.aggregate": "orchestration" }, ), - [ORCHESTRATION_WS_METHODS.replayEvents]: (input) => + [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: (_input) => observeRpcEffect( - ORCHESTRATION_WS_METHODS.replayEvents, - Stream.runCollect( - orchestrationEngine.readEvents( - clamp(input.fromSequenceExclusive, { - maximum: Number.MAX_SAFE_INTEGER, - minimum: 0, - }), - ), - ).pipe( - Effect.map((events) => Array.from(events)), - Effect.flatMap(enrichOrchestrationEvents), + ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot, + getOrchestrationV2ArchivedShellSnapshot, + { "rpc.aggregate": "orchestration" }, + ), + [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: (input) => + observeRpcEffect( + ORCHESTRATION_V2_WS_METHODS.getThreadProjection, + threadManagement.getThreadProjection(input.threadId).pipe( Effect.mapError( (cause) => - new OrchestrationReplayEventsError({ - message: "Failed to replay orchestration events", + new OrchestrationV2GetThreadProjectionError({ + threadId: input.threadId, + message: `Failed to load orchestration V2 thread ${input.threadId}`, cause, }), ), ), - { "rpc.aggregate": "orchestration" }, + { + "rpc.aggregate": "orchestrationV2", + "orchestration_v2.thread_id": input.threadId, + }, ), - [ORCHESTRATION_WS_METHODS.subscribeShell]: (_input) => - observeRpcStreamEffect( - ORCHESTRATION_WS_METHODS.subscribeShell, - Effect.gen(function* () { - const snapshot = yield* projectionSnapshotQuery.getShellSnapshot().pipe( - Effect.tapError((cause) => - Effect.logError("orchestration shell snapshot load failed", { cause }), - ), + [ORCHESTRATION_V2_WS_METHODS.launchThread]: (input) => + observeRpcEffect( + ORCHESTRATION_V2_WS_METHODS.launchThread, + startup + .enqueueCommand( + threadLaunch.launch({ + commandId: input.commandId, + ...(input.threadId === undefined ? {} : { threadId: input.threadId }), + ...(input.reuseExistingThread === undefined + ? {} + : { reuseExistingThread: input.reuseExistingThread }), + projectId: input.projectId, + title: input.title, + modelSelection: input.modelSelection, + runtimeMode: input.runtimeMode, + interactionMode: input.interactionMode, + workspaceStrategy: input.workspaceStrategy, + ...(input.initialMessage === undefined + ? {} + : { + initialMessage: { + ...(input.initialMessage.messageId === undefined + ? {} + : { messageId: input.initialMessage.messageId }), + text: input.initialMessage.text, + attachments: input.initialMessage.attachments, + }, + }), + createdBy: "user", + creationSource: input.creationSource ?? "web", + }), + ) + .pipe( Effect.mapError( (cause) => - new OrchestrationGetSnapshotError({ - message: "Failed to load orchestration shell snapshot", + new OrchestrationV2ThreadLaunchError({ + commandId: input.commandId, + projectId: input.projectId, + message: "Failed to launch thread", cause, }), ), - ); - - const liveStream = orchestrationEngine.streamDomainEvents.pipe( - Stream.mapEffect(toShellStreamEvent), - Stream.flatMap((event) => - Option.isSome(event) ? Stream.succeed(event.value) : Stream.empty, - ), - ); - - return Stream.concat( - Stream.make({ - kind: "snapshot" as const, - snapshot, - }), - liveStream, - ); - }), - { "rpc.aggregate": "orchestration" }, - ), - [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot]: (_input) => - observeRpcEffect( - ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, - projectionSnapshotQuery.getArchivedShellSnapshot().pipe( - Effect.tapError((cause) => - Effect.logError("orchestration archived shell snapshot load failed", { cause }), - ), - Effect.mapError( - (cause) => - new OrchestrationGetSnapshotError({ - message: "Failed to load archived orchestration shell snapshot", - cause, - }), ), - ), - { "rpc.aggregate": "orchestration" }, + { + "rpc.aggregate": "orchestration", + "orchestration_v2.command_id": input.commandId, + "orchestration_v2.project_id": input.projectId, + }, ), - [ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => + [ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell]: (_input) => observeRpcStreamEffect( - ORCHESTRATION_WS_METHODS.subscribeThread, - Effect.gen(function* () { - const [threadDetail, snapshotSequence] = yield* Effect.all([ - projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe( - Effect.mapError( - (cause) => - new OrchestrationGetSnapshotError({ - message: `Failed to load thread ${input.threadId}`, - cause, - }), - ), - ), - projectionSnapshotQuery.getSnapshotSequence().pipe( - Effect.map(({ snapshotSequence }) => snapshotSequence), - Effect.mapError( - (cause) => - new OrchestrationGetSnapshotError({ - message: "Failed to load orchestration snapshot sequence", - cause, - }), - ), - ), - ]); - - if (Option.isNone(threadDetail)) { - return yield* new OrchestrationGetSnapshotError({ - message: `Thread ${input.threadId} was not found`, - cause: input.threadId, - }); - } - - const liveStream = orchestrationEngine.streamDomainEvents.pipe( - Stream.filter( - (event) => - event.aggregateKind === "thread" && - event.aggregateId === input.threadId && - isThreadDetailEvent(event), - ), - Stream.map((event) => ({ - kind: "event" as const, - event, - })), - ); - - return Stream.concat( - Stream.make({ - kind: "snapshot" as const, - snapshot: { - snapshotSequence, - thread: threadDetail.value, - }, - }), - liveStream, - ); - }), + ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell, + subscribeOrchestrationV2ArchivedShell(), { "rpc.aggregate": "orchestration" }, ), + [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: (_input) => + observeRpcStreamEffect( + ORCHESTRATION_V2_WS_METHODS.subscribeShell, + subscribeOrchestrationV2Shell(), + { + "rpc.aggregate": "orchestrationV2", + }, + ), + [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: (input) => + observeRpcStreamEffect( + ORCHESTRATION_V2_WS_METHODS.subscribeThread, + subscribeOrchestrationV2Thread(input), + { + "rpc.aggregate": "orchestrationV2", + "orchestration_v2.thread_id": input.threadId, + }, + ), [WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server", @@ -1382,6 +1207,22 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => ), { "rpc.aggregate": "workspace" }, ), + [WS_METHODS.projectsMutate]: (mutation) => + observeRpcEffect( + WS_METHODS.projectsMutate, + startup.enqueueCommand(mutateProject(mutation)).pipe( + Effect.mapError((cause) => + cause._tag === "ProjectMutationError" + ? cause + : new ProjectMutationError({ + commandId: mutation.commandId, + message: "Failed to mutate project.", + cause, + }), + ), + ), + { "rpc.aggregate": "orchestration" }, + ), [WS_METHODS.shellOpenInEditor]: (input) => observeRpcEffect(WS_METHODS.shellOpenInEditor, externalLauncher.launchEditor(input), { "rpc.aggregate": "workspace", @@ -1408,28 +1249,8 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => if (input.resource._tag !== "workspace-file") { return yield* issueAssetUrl({ resource: input.resource }); } - const thread = yield* projectionSnapshotQuery - .getThreadShellById(input.resource.threadId) - .pipe( - Effect.mapError( - (cause) => - new AssetAccessError({ - operation: "resolve-workspace-context", - resource: input.resource, - message: "Failed to resolve workspace context.", - cause, - }), - ), - ); - if (Option.isNone(thread)) { - return yield* new AssetAccessError({ - operation: "resolve-workspace-context", - resource: input.resource, - message: "Workspace context was not found.", - }); - } - const project = yield* projectionSnapshotQuery - .getProjectShellById(thread.value.projectId) + const thread = yield* threadManagement + .getThreadProjection(input.resource.threadId) .pipe( Effect.mapError( (cause) => @@ -1441,6 +1262,17 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => }), ), ); + const project = yield* projectService.getById(thread.thread.projectId).pipe( + Effect.mapError( + (cause) => + new AssetAccessError({ + operation: "resolve-workspace-context", + resource: input.resource, + message: "Failed to resolve workspace context.", + cause, + }), + ), + ); if (Option.isNone(project)) { return yield* new AssetAccessError({ operation: "resolve-workspace-context", @@ -1450,11 +1282,17 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => } return yield* issueAssetUrl({ resource: input.resource, - workspaceRoot: thread.value.worktreePath ?? project.value.workspaceRoot, + workspaceRoot: thread.thread.worktreePath ?? project.value.workspaceRoot, }); }), { "rpc.aggregate": "workspace" }, ), + [WS_METHODS.assetsPersistChatAttachments]: (input) => + observeRpcEffect( + WS_METHODS.assetsPersistChatAttachments, + persistChatAttachments(input).pipe(Effect.map((attachments) => ({ attachments }))), + { "rpc.aggregate": "orchestration" }, + ), [WS_METHODS.subscribeVcsStatus]: (input) => observeRpcStream( WS_METHODS.subscribeVcsStatus, @@ -1794,6 +1632,7 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => { "rpc.aggregate": "auth" }, ), }); + return handlers; }), ); @@ -1814,7 +1653,7 @@ export const websocketRpcRouteLayer = Layer.unwrap( failEnvironmentInternal("internal_error", error), ), ); - const rpcWebSocketHttpEffect = yield* RpcServer.toHttpEffectWebsocket(WsRpcGroup, { + const rpcWebSocketHttpEffect = yield* RpcServer.toHttpEffectWebsocket(ServerWsRpcGroup, { disableTracing: true, }).pipe( Effect.provide( diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 7798f38e43e..92e7f30d027 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -126,7 +126,7 @@ export function BranchToolbarBranchSelector({ [environmentId, threadId], ); const serverThread = useThread(threadRef); - const serverSession = serverThread?.session ?? null; + const serverSession = serverThread?.runtime ?? null; const draftThread = useComposerDraftStore((store) => draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef), ); diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 43ed895c0db..4ae4464cfd4 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -1,7 +1,8 @@ -import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId, TurnId } from "@t3tools/contracts"; +import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId, RunId } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import type { Thread } from "../types"; +import { makeThreadFixture } from "../test-fixtures"; import { MAX_HIDDEN_MOUNTED_PREVIEW_THREADS, MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, @@ -22,7 +23,7 @@ const threadId = ThreadId.make("thread-1"); const now = "2026-03-29T00:00:00.000Z"; function makeThread(overrides: Partial = {}): Thread { - return { + return makeThreadFixture({ id: threadId, environmentId, projectId, @@ -33,25 +34,23 @@ function makeThread(overrides: Partial = {}): Thread { }, runtimeMode: "full-access", interactionMode: "default", - session: null, + runtime: null, messages: [], proposedPlans: [], - activities: [], - checkpoints: [], createdAt: now, updatedAt: now, archivedAt: null, deletedAt: null, - latestTurn: null, + latestRun: null, branch: null, worktreePath: null, ...overrides, - }; + }); } const completedTurn = { - turnId: TurnId.make("turn-1"), - state: "completed" as const, + runId: RunId.make("turn-1"), + status: "completed" as const, requestedAt: now, startedAt: "2026-03-29T00:00:01.000Z", completedAt: "2026-03-29T00:00:10.000Z", @@ -59,12 +58,10 @@ const completedTurn = { }; const readySession = { - threadId, - status: "ready" as const, + status: "completed" as const, providerName: "codex", providerInstanceId: ProviderInstanceId.make("codex"), - runtimeMode: "full-access" as const, - activeTurnId: null, + activeRunId: null, lastError: null, updatedAt: "2026-03-29T00:00:10.000Z", }; @@ -326,15 +323,15 @@ describe("shouldWriteThreadErrorToCurrentServerThread", () => { describe("hasServerAcknowledgedLocalDispatch", () => { it("does not acknowledge unchanged server state", () => { const localDispatch = createLocalDispatchSnapshot( - makeThread({ latestTurn: completedTurn, session: readySession }), + makeThread({ latestRun: completedTurn, runtime: readySession }), ); expect( hasServerAcknowledgedLocalDispatch({ localDispatch, phase: "ready", - latestTurn: completedTurn, - session: readySession, + latestRun: completedTurn, + runtime: readySession, hasPendingApproval: false, hasPendingUserInput: false, threadError: null, @@ -344,11 +341,11 @@ describe("hasServerAcknowledgedLocalDispatch", () => { it("acknowledges a settled newer turn", () => { const localDispatch = createLocalDispatchSnapshot( - makeThread({ latestTurn: completedTurn, session: readySession }), + makeThread({ latestRun: completedTurn, runtime: readySession }), ); const newerTurn = { ...completedTurn, - turnId: TurnId.make("turn-2"), + runId: RunId.make("turn-2"), requestedAt: "2026-03-29T00:01:00.000Z", startedAt: "2026-03-29T00:01:01.000Z", completedAt: "2026-03-29T00:01:30.000Z", @@ -358,8 +355,8 @@ describe("hasServerAcknowledgedLocalDispatch", () => { hasServerAcknowledgedLocalDispatch({ localDispatch, phase: "ready", - latestTurn: newerTurn, - session: { ...readySession, updatedAt: newerTurn.completedAt }, + latestRun: newerTurn, + runtime: { ...readySession, updatedAt: newerTurn.completedAt }, hasPendingApproval: false, hasPendingUserInput: false, threadError: null, @@ -369,12 +366,12 @@ describe("hasServerAcknowledgedLocalDispatch", () => { it("waits for the matching running turn before acknowledging", () => { const localDispatch = createLocalDispatchSnapshot( - makeThread({ latestTurn: completedTurn, session: readySession }), + makeThread({ latestRun: completedTurn, runtime: readySession }), ); const runningTurn = { ...completedTurn, - turnId: TurnId.make("turn-2"), - state: "running" as const, + runId: RunId.make("turn-2"), + status: "running" as const, requestedAt: "2026-03-29T00:01:00.000Z", startedAt: "2026-03-29T00:01:01.000Z", completedAt: null, @@ -384,11 +381,11 @@ describe("hasServerAcknowledgedLocalDispatch", () => { hasServerAcknowledgedLocalDispatch({ localDispatch, phase: "running", - latestTurn: runningTurn, - session: { + latestRun: runningTurn, + runtime: { ...readySession, status: "running", - activeTurnId: TurnId.make("turn-other"), + activeRunId: RunId.make("turn-other"), }, hasPendingApproval: false, hasPendingUserInput: false, @@ -399,11 +396,11 @@ describe("hasServerAcknowledgedLocalDispatch", () => { hasServerAcknowledgedLocalDispatch({ localDispatch, phase: "running", - latestTurn: runningTurn, - session: { + latestRun: runningTurn, + runtime: { ...readySession, status: "running", - activeTurnId: runningTurn.turnId, + activeRunId: runningTurn.runId, }, hasPendingApproval: false, hasPendingUserInput: false, @@ -417,8 +414,8 @@ describe("hasServerAcknowledgedLocalDispatch", () => { const common = { localDispatch, phase: "ready" as const, - latestTurn: null, - session: null, + latestRun: null, + runtime: null, hasPendingApproval: false, hasPendingUserInput: false, threadError: null, diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 36947caae6f..d00b18151e1 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -7,8 +7,10 @@ import { type ServerProvider, type ScopedThreadRef, type ThreadId, - type TurnId, + type RunId, } from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import { presentThread } from "@t3tools/client-runtime/state/shell"; import { type ChatMessage, type SessionPhase, type Thread } from "../types"; import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore"; import * as Schema from "effect/Schema"; @@ -32,27 +34,46 @@ export function buildLocalDraftThread( draftThread: DraftThreadState, fallbackModelSelection: ModelSelection, ): Thread { - return { - id: threadId, - environmentId: draftThread.environmentId, - projectId: draftThread.projectId, - title: "New thread", - modelSelection: fallbackModelSelection, - runtimeMode: draftThread.runtimeMode, - interactionMode: draftThread.interactionMode, - session: null, + const timestamp = DateTime.makeUnsafe(draftThread.createdAt); + return presentThread(draftThread.environmentId, { + thread: { + id: threadId, + projectId: draftThread.projectId, + title: "New thread", + providerInstanceId: fallbackModelSelection.instanceId, + modelSelection: fallbackModelSelection, + runtimeMode: draftThread.runtimeMode, + interactionMode: draftThread.interactionMode, + branch: draftThread.branch, + worktreePath: draftThread.worktreePath, + activeProviderThreadId: null, + lineage: { rootThreadId: threadId, parentThreadId: null, relationshipToParent: null }, + forkedFrom: null, + createdBy: "user", + creationSource: "web", + createdAt: timestamp, + updatedAt: timestamp, + archivedAt: null, + deletedAt: null, + }, + runs: [], + attempts: [], + nodes: [], + subagents: [], + providerSessions: [], + providerThreads: [], + providerTurns: [], + runtimeRequests: [], messages: [], - createdAt: draftThread.createdAt, - updatedAt: draftThread.createdAt, - archivedAt: null, - deletedAt: null, - latestTurn: null, - branch: draftThread.branch, - worktreePath: draftThread.worktreePath, + plans: [], + turnItems: [], + checkpointScopes: [], checkpoints: [], - activities: [], - proposedPlans: [], - }; + contextHandoffs: [], + contextTransfers: [], + visibleTurnItems: [], + updatedAt: timestamp, + }); } export function shouldWriteThreadErrorToCurrentServerThread(input: { @@ -251,7 +272,7 @@ export function buildExpiredTerminalContextToastCopy( export function threadHasStarted(thread: Thread | null | undefined): boolean { return Boolean( - thread && (thread.latestTurn !== null || thread.messages.length > 0 || thread.session !== null), + thread && (thread.latestRun !== null || thread.messages.length > 0 || thread.runtime !== null), ); } @@ -275,7 +296,7 @@ export function deriveLockedProvider(input: { if (!threadHasStarted(input.thread)) { return null; } - const sessionProvider = input.thread?.session?.providerName ?? null; + const sessionProvider = input.thread?.runtime?.providerName ?? null; if (sessionProvider && isProviderDriverKind(sessionProvider)) { return sessionProvider; } @@ -376,37 +397,37 @@ export async function waitForStartedServerThread( export interface LocalDispatchSnapshot { startedAt: string; preparingWorktree: boolean; - latestTurnTurnId: TurnId | null; - latestTurnRequestedAt: string | null; - latestTurnStartedAt: string | null; - latestTurnCompletedAt: string | null; - sessionStatus: NonNullable["status"] | null; - sessionUpdatedAt: string | null; + latestRunId: RunId | null; + latestRunRequestedAt: string | null; + latestRunStartedAt: string | null; + latestRunCompletedAt: string | null; + runtimeStatus: NonNullable["status"] | null; + runtimeUpdatedAt: string | null; } export function createLocalDispatchSnapshot( activeThread: Thread | undefined, options?: { preparingWorktree?: boolean }, ): LocalDispatchSnapshot { - const latestTurn = activeThread?.latestTurn ?? null; - const session = activeThread?.session ?? null; + const latestRun = activeThread?.latestRun ?? null; + const runtime = activeThread?.runtime ?? null; return { startedAt: new Date().toISOString(), preparingWorktree: Boolean(options?.preparingWorktree), - latestTurnTurnId: latestTurn?.turnId ?? null, - latestTurnRequestedAt: latestTurn?.requestedAt ?? null, - latestTurnStartedAt: latestTurn?.startedAt ?? null, - latestTurnCompletedAt: latestTurn?.completedAt ?? null, - sessionStatus: session?.status ?? null, - sessionUpdatedAt: session?.updatedAt ?? null, + latestRunId: latestRun?.runId ?? null, + latestRunRequestedAt: latestRun?.requestedAt ?? null, + latestRunStartedAt: latestRun?.startedAt ?? null, + latestRunCompletedAt: latestRun?.completedAt ?? null, + runtimeStatus: runtime?.status ?? null, + runtimeUpdatedAt: runtime?.updatedAt ?? null, }; } export function hasServerAcknowledgedLocalDispatch(input: { localDispatch: LocalDispatchSnapshot | null; phase: SessionPhase; - latestTurn: Thread["latestTurn"] | null; - session: Thread["session"] | null; + latestRun: Thread["latestRun"] | null; + runtime: Thread["runtime"] | null; hasPendingApproval: boolean; hasPendingUserInput: boolean; threadError: string | null | undefined; @@ -418,25 +439,25 @@ export function hasServerAcknowledgedLocalDispatch(input: { return true; } - const latestTurn = input.latestTurn ?? null; - const session = input.session ?? null; - const latestTurnChanged = - input.localDispatch.latestTurnTurnId !== (latestTurn?.turnId ?? null) || - input.localDispatch.latestTurnRequestedAt !== (latestTurn?.requestedAt ?? null) || - input.localDispatch.latestTurnStartedAt !== (latestTurn?.startedAt ?? null) || - input.localDispatch.latestTurnCompletedAt !== (latestTurn?.completedAt ?? null); + const latestRun = input.latestRun ?? null; + const runtime = input.runtime ?? null; + const latestRunChanged = + input.localDispatch.latestRunId !== (latestRun?.runId ?? null) || + input.localDispatch.latestRunRequestedAt !== (latestRun?.requestedAt ?? null) || + input.localDispatch.latestRunStartedAt !== (latestRun?.startedAt ?? null) || + input.localDispatch.latestRunCompletedAt !== (latestRun?.completedAt ?? null); if (input.phase === "running") { - if (!latestTurnChanged) { + if (!latestRunChanged) { return false; } - if (latestTurn?.startedAt === null || latestTurn === null) { + if (latestRun?.startedAt === null || latestRun === null) { return false; } if ( - session?.activeTurnId !== null && - session?.activeTurnId !== undefined && - latestTurn?.turnId !== session.activeTurnId + runtime?.activeRunId !== null && + runtime?.activeRunId !== undefined && + latestRun?.runId !== runtime.activeRunId ) { return false; } @@ -444,8 +465,8 @@ export function hasServerAcknowledgedLocalDispatch(input: { } return ( - latestTurnChanged || - input.localDispatch.sessionStatus !== (session?.status ?? null) || - input.localDispatch.sessionUpdatedAt !== (session?.updatedAt ?? null) + latestRunChanged || + input.localDispatch.runtimeStatus !== (runtime?.status ?? null) || + input.localDispatch.runtimeUpdatedAt !== (runtime?.updatedAt ?? null) ); } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index cf5bb9de5e9..cf35b34a30d 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1,5 +1,4 @@ import { - type ApprovalRequestId, DEFAULT_MODEL, defaultInstanceIdForDriver, type EnvironmentId, @@ -13,14 +12,15 @@ import { type ResolvedKeybindingsConfig, type ScopedThreadRef, type ThreadId, - type TurnId, + type RunId, + type RuntimeRequestId, type KeybindingCommand, - OrchestrationThreadActivity, ProviderInteractionMode, ProviderDriverKind, RuntimeMode, TerminalOpenInput, } from "@t3tools/contracts"; +import type { ThreadWorkEntry } from "@t3tools/client-runtime/state/shell"; import { connectionStatusText, type EnvironmentConnectionPresentation, @@ -65,13 +65,14 @@ import { derivePendingUserInputs, derivePhase, deriveTimelineEntries, + deriveTimelineEntriesFromVisibleTurnItems, deriveActiveWorkStartedAt, deriveActivePlanState, findSidebarProposedPlan, findLatestProposedPlan, deriveWorkLogEntries, hasActionableProposedPlan, - isLatestTurnSettled, + isLatestRunSettled, } from "../session-logic"; import { type LegendListRef } from "@legendapp/list/react"; import { @@ -186,6 +187,7 @@ import { useThread, useThreadProposedPlans, useThreadRefs, + useThreadVisibleTurnItems, } from "../state/entities"; import { environmentShell } from "../state/shell"; import { ChatComposer, type ChatComposerHandle } from "./chat/ChatComposer"; @@ -239,7 +241,7 @@ import { useAssetUrls } from "../assets/assetUrls"; const IMAGE_ONLY_BOOTSTRAP_PROMPT = "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; -const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = []; +const EMPTY_WORK_ENTRIES: ThreadWorkEntry[] = []; const EMPTY_PROVIDERS: ServerProvider[] = []; const EMPTY_PROVIDER_SKILLS: ServerProvider["skills"] = []; const EMPTY_PENDING_USER_INPUT_ANSWERS: Record = {}; @@ -348,10 +350,10 @@ type PersistentTerminalLaunchContext = Pick(null); @@ -365,17 +367,17 @@ function useLocalDispatchState(input: { hasServerAcknowledgedLocalDispatch({ localDispatch, phase: input.phase, - latestTurn: input.activeLatestTurn, - session: input.activeThread?.session ?? null, + latestRun: input.activeLatestRun, + runtime: input.activeThread?.runtime ?? null, hasPendingApproval: input.activePendingApproval !== null, hasPendingUserInput: input.activePendingUserInput !== null, threadError: input.threadError, }), [ - input.activeLatestTurn, + input.activeLatestRun, input.activePendingApproval, input.activePendingUserInput, - input.activeThread?.session, + input.activeThread?.runtime, input.phase, input.threadError, localDispatch, @@ -1023,6 +1025,9 @@ function ChatViewContent(props: ChatViewProps) { const composerDraftTarget: ScopedThreadRef | DraftId = routeKind === "server" ? routeThreadRef : props.draftId; const serverThread = useThread(routeKind === "server" ? routeThreadRef : null); + const serverVisibleTurnItems = useThreadVisibleTurnItems( + routeKind === "server" ? routeThreadRef : null, + ); const markThreadVisited = useUiStateStore((store) => store.markThreadVisited); const activeThreadLastVisitedAt = useUiStateStore((store) => routeKind === "server" ? store.threadLastVisitedAtById[routeThreadKey] : undefined, @@ -1100,9 +1105,9 @@ function ChatViewContent(props: ChatViewProps) { const [maximizedRightPanelThreadKey, setMaximizedRightPanelThreadKey] = useState( null, ); - const [respondingRequestIds, setRespondingRequestIds] = useState([]); + const [respondingRequestIds, setRespondingRequestIds] = useState([]); const [respondingUserInputRequestIds, setRespondingUserInputRequestIds] = useState< - ApprovalRequestId[] + RuntimeRequestId[] >([]); const [pendingUserInputAnswersByRequestId, setPendingUserInputAnswersByRequestId] = useState< Record> @@ -1206,7 +1211,7 @@ function ChatViewContent(props: ChatViewProps) { const isServerThread = routeKind === "server" && serverThread !== null; const activeThread = isServerThread ? serverThread : localDraftThread; const threadError = isServerThread - ? (localServerError ?? serverThread?.session?.lastError ?? null) + ? (localServerError ?? serverThread?.runtime?.lastError ?? null) : localDraftError; const runtimeMode = composerRuntimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; const interactionMode = @@ -1295,14 +1300,14 @@ function ChatViewContent(props: ChatViewProps) { const existingThreadKeys = new Set([...serverThreadKeys, ...draftThreadKeys]); return openTerminalThreadKeys.filter((nextThreadKey) => existingThreadKeys.has(nextThreadKey)); }, [draftThreadKeys, openTerminalThreadKeys, serverThreadKeys]); - const activeLatestTurn = activeThread?.latestTurn ?? null; + const activeLatestRun = activeThread?.latestRun ?? null; const sourcePlanThreadRef = useMemo(() => { - const sourceThreadId = activeLatestTurn?.sourceProposedPlan?.threadId; + const sourceThreadId = activeLatestRun?.sourcePlanRef?.threadId; if (!activeThread || !sourceThreadId || sourceThreadId === activeThread.id) { return null; } return scopeThreadRef(activeThread.environmentId, sourceThreadId); - }, [activeLatestTurn?.sourceProposedPlan?.threadId, activeThread]); + }, [activeLatestRun?.sourcePlanRef?.threadId, activeThread]); const sourceThreadProposedPlans = useThreadProposedPlans(sourcePlanThreadRef); const threadPlanCatalog = useMemo(() => { if (!activeThread) { @@ -1334,7 +1339,7 @@ function ChatViewContent(props: ChatViewProps) { : nextThreadIds; }); }, [activeThreadKey, existingOpenTerminalThreadKeys, terminalUiState.terminalOpen]); - const latestTurnSettled = isLatestTurnSettled(activeLatestTurn, activeThread?.session ?? null); + const latestRunSettled = isLatestRunSettled(activeLatestRun, activeThread?.runtime ?? null); const activeProjectRef = activeThread ? scopeProjectRef(activeThread.environmentId, activeThread.projectId) : null; @@ -1682,16 +1687,19 @@ function ChatViewContent(props: ChatViewProps) { selectedProviderByThreadId ?? threadProvider ?? ProviderDriverKind.make("codex"), ); const selectedProvider: ProviderDriverKind = lockedProvider ?? unlockedSelectedProvider; - const phase = derivePhase(activeThread?.session ?? null); - const threadActivities = activeThread?.activities ?? EMPTY_ACTIVITIES; - const workLogEntries = useMemo(() => deriveWorkLogEntries(threadActivities), [threadActivities]); + const phase = derivePhase(activeThread?.runtime ?? null); + const threadWorkEntries = activeThread?.workEntries ?? EMPTY_WORK_ENTRIES; + const workLogEntries = useMemo( + () => deriveWorkLogEntries(threadWorkEntries), + [threadWorkEntries], + ); const pendingApprovals = useMemo( - () => derivePendingApprovals(threadActivities), - [threadActivities], + () => derivePendingApprovals(activeThread?.pendingApprovals ?? []), + [activeThread?.pendingApprovals], ); const pendingUserInputs = useMemo( - () => derivePendingUserInputs(threadActivities), - [threadActivities], + () => derivePendingUserInputs(activeThread?.pendingUserInputs ?? []), + [activeThread?.pendingUserInputs], ); const activePendingUserInput = pendingUserInputs[0] ?? null; const activePendingDraftAnswers = useMemo( @@ -1724,36 +1732,37 @@ function ChatViewContent(props: ChatViewProps) { [activePendingDraftAnswers, activePendingUserInput], ); const activePendingIsResponding = activePendingUserInput - ? respondingUserInputRequestIds.includes(activePendingUserInput.requestId) + ? activePendingUserInput.responseCapability !== "live" || + respondingUserInputRequestIds.includes(activePendingUserInput.requestId) : false; const activeProposedPlan = useMemo(() => { - if (!latestTurnSettled) { + if (!latestRunSettled) { return null; } return findLatestProposedPlan( activeThread?.proposedPlans ?? [], - activeLatestTurn?.turnId ?? null, + activeLatestRun?.runId ?? null, ); - }, [activeLatestTurn?.turnId, activeThread?.proposedPlans, latestTurnSettled]); + }, [activeLatestRun?.runId, activeThread?.proposedPlans, latestRunSettled]); const sidebarProposedPlan = useMemo( () => findSidebarProposedPlan({ threads: threadPlanCatalog, - latestTurn: activeLatestTurn, - latestTurnSettled, + latestRun: activeLatestRun, + latestRunSettled, threadId: activeThread?.id ?? null, }), - [activeLatestTurn, activeThread?.id, latestTurnSettled, threadPlanCatalog], + [activeLatestRun, activeThread?.id, latestRunSettled, threadPlanCatalog], ); const activePlan = useMemo( - () => deriveActivePlanState(threadActivities, activeLatestTurn?.turnId ?? undefined), - [activeLatestTurn?.turnId, threadActivities], + () => deriveActivePlanState(activeThread?.todoPlans ?? [], activeLatestRun?.runId ?? undefined), + [activeLatestRun?.runId, activeThread?.todoPlans], ); const planSidebarLabel = sidebarProposedPlan || interactionMode === "plan" ? "Plan" : "Tasks"; const showPlanFollowUpPrompt = pendingUserInputs.length === 0 && interactionMode === "plan" && - latestTurnSettled && + latestRunSettled && hasActionableProposedPlan(activeProposedPlan); const activePendingApproval = pendingApprovals[0] ?? null; const { @@ -1764,7 +1773,7 @@ function ChatViewContent(props: ChatViewProps) { isSendBusy, } = useLocalDispatchState({ activeThread, - activeLatestTurn, + activeLatestRun, phase, activePendingApproval: activePendingApproval?.requestId ?? null, activePendingUserInput: activePendingUserInput?.requestId ?? null, @@ -1772,8 +1781,8 @@ function ChatViewContent(props: ChatViewProps) { }); const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint; const activeWorkStartedAt = deriveActiveWorkStartedAt( - activeLatestTurn, - activeThread?.session ?? null, + activeLatestRun, + activeThread?.runtime ?? null, localDispatchStartedAt, ); useEffect(() => { @@ -1837,7 +1846,17 @@ function ChatViewContent(props: ChatViewProps) { }); }, []); const serverMessages = activeThread?.messages; - const serverAttachmentIds = useMemo(() => { + const committedServerAttachmentIds = useMemo(() => { + const attachmentIds = new Set(); + for (const row of serverVisibleTurnItems) { + if (row.item.type !== "user_message") continue; + for (const attachment of row.item.attachments) { + attachmentIds.add(attachment.id); + } + } + return [...attachmentIds]; + }, [serverVisibleTurnItems]); + const draftAttachmentIds = useMemo(() => { const attachmentIds = new Set(); for (const message of serverMessages ?? []) { for (const attachment of message.attachments ?? []) { @@ -1846,6 +1865,7 @@ function ChatViewContent(props: ChatViewProps) { } return [...attachmentIds]; }, [serverMessages]); + const serverAttachmentIds = isServerThread ? committedServerAttachmentIds : draftAttachmentIds; const serverAttachmentResources = useMemo( () => serverAttachmentIds.map((attachmentId) => ({ @@ -2017,12 +2037,22 @@ function ChatViewContent(props: ChatViewProps) { } return [...serverMessagesWithPreviewHandoff, ...pendingMessages]; }, [attachmentPreviewHandoffByMessageId, displayServerMessages, optimisticUserMessages]); - const timelineEntries = useMemo( + const serverTimelineEntries = useMemo( + () => + deriveTimelineEntriesFromVisibleTurnItems({ + visibleTurnItems: serverVisibleTurnItems, + optimisticMessages: optimisticUserMessages, + attachmentUrlById: serverAttachmentUrlById, + }), + [optimisticUserMessages, serverVisibleTurnItems, serverAttachmentUrlById], + ); + const draftTimelineEntries = useMemo( () => deriveTimelineEntries(timelineMessages, activeThread?.proposedPlans ?? [], workLogEntries), [activeThread?.proposedPlans, timelineMessages, workLogEntries], ); - const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = + const timelineEntries = isServerThread ? serverTimelineEntries : draftTimelineEntries; + const { turnDiffSummaries, inferredCheckpointTurnCountByRunId } = useTurnDiffSummaries(activeThread); const turnDiffSummaryByAssistantMessageId = useMemo(() => { const byMessageId = new Map(); @@ -2053,7 +2083,7 @@ function ChatViewContent(props: ChatViewProps) { continue; } const turnCount = - summary.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[summary.turnId]; + summary.checkpointTurnCount ?? inferredCheckpointTurnCountByRunId[summary.runId]; if (typeof turnCount !== "number") { break; } @@ -2063,7 +2093,7 @@ function ChatViewContent(props: ChatViewProps) { } return byUserMessageId; - }, [inferredCheckpointTurnCountByTurnId, timelineEntries, turnDiffSummaryByAssistantMessageId]); + }, [inferredCheckpointTurnCountByRunId, timelineEntries, turnDiffSummaryByAssistantMessageId]); const gitCwd = activeProject ? projectScriptCwd({ @@ -2090,7 +2120,7 @@ function ChatViewContent(props: ChatViewProps) { ?.instanceId ?? null; const activeProviderInstanceId = selectedProviderInstanceId ?? - activeThread?.session?.providerInstanceId ?? + activeThread?.runtime?.providerInstanceId ?? activeThread?.modelSelection.instanceId ?? activeProject?.defaultModelSelection?.instanceId ?? null; @@ -2149,9 +2179,7 @@ function ChatViewContent(props: ChatViewProps) { }, [activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen]); const envLocked = Boolean( - activeThread && - (activeThread.messages.length > 0 || - (activeThread.session !== null && activeThread.session.status !== "stopped")), + activeThread && (activeThread.messages.length > 0 || activeThread.runtime !== null), ); // Handle environment change for draft threads. When the user picks a @@ -2707,8 +2735,8 @@ function ChatViewContent(props: ChatViewProps) { }, [handleInteractionModeChange, interactionMode]); const dismissPlanSidebarForCurrentTurn = useCallback(() => { planSidebarDismissedForTurnRef.current = - activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; - }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); + activePlan?.runId ?? sidebarProposedPlan?.runId ?? "__dismissed__"; + }, [activePlan?.runId, sidebarProposedPlan?.runId]); const togglePlanSidebar = useCallback(() => { if (!activeThreadRef) return; if (planSidebarOpen) { @@ -3152,20 +3180,20 @@ function ChatViewContent(props: ChatViewProps) { if (!autoOpenPlanSidebar) return; if (!activePlan) return; if (planSidebarOpen) return; - const latestTurnId = activeLatestTurn?.turnId ?? null; - if (latestTurnId && activePlan.turnId !== latestTurnId) return; - const turnKey = activePlan.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; + const latestRunId = activeLatestRun?.runId ?? null; + if (latestRunId && activePlan.runId !== latestRunId) return; + const turnKey = activePlan.runId ?? sidebarProposedPlan?.runId ?? "__dismissed__"; if (planSidebarDismissedForTurnRef.current === turnKey) return; if (activeThreadRef) { useRightPanelStore.getState().open(activeThreadRef, "plan"); } }, [ activePlan, - activeLatestTurn?.turnId, + activeLatestRun?.runId, activeThreadRef, autoOpenPlanSidebar, planSidebarOpen, - sidebarProposedPlan?.turnId, + sidebarProposedPlan?.runId, ]); useEffect(() => { @@ -3704,7 +3732,7 @@ function ChatViewContent(props: ChatViewProps) { role: "user", text: outgoingMessageText, ...(optimisticAttachments.length > 0 ? { attachments: optimisticAttachments } : {}), - turnId: null, + runId: null, createdAt: messageCreatedAt, updatedAt: messageCreatedAt, streaming: false, @@ -3914,8 +3942,13 @@ function ChatViewContent(props: ChatViewProps) { }; const onRespondToApproval = useCallback( - async (requestId: ApprovalRequestId, decision: ProviderApprovalDecision) => { + async (requestId: RuntimeRequestId, decision: ProviderApprovalDecision) => { if (!activeThreadId) return; + if ( + pendingApprovals.find((approval) => approval.requestId === requestId) + ?.responseCapability !== "live" + ) + return; setRespondingRequestIds((existing) => existing.includes(requestId) ? existing : [...existing, requestId], @@ -3938,12 +3971,17 @@ function ChatViewContent(props: ChatViewProps) { setRespondingRequestIds((existing) => existing.filter((id) => id !== requestId)); return result; }, - [activeThreadId, environmentId, respondToThreadApproval, setThreadError], + [activeThreadId, environmentId, pendingApprovals, respondToThreadApproval, setThreadError], ); const onRespondToUserInput = useCallback( - async (requestId: ApprovalRequestId, answers: Record) => { + async (requestId: RuntimeRequestId, answers: Record) => { if (!activeThreadId) return; + if ( + pendingUserInputs.find((input) => input.requestId === requestId)?.responseCapability !== + "live" + ) + return; setRespondingUserInputRequestIds((existing) => existing.includes(requestId) ? existing : [...existing, requestId], @@ -3966,7 +4004,7 @@ function ChatViewContent(props: ChatViewProps) { setRespondingUserInputRequestIds((existing) => existing.filter((id) => id !== requestId)); return result; }, - [activeThreadId, environmentId, respondToThreadUserInput, setThreadError], + [activeThreadId, environmentId, pendingUserInputs, respondToThreadUserInput, setThreadError], ); const setActivePendingUserInputQuestionIndex = useCallback( @@ -4050,7 +4088,11 @@ function ChatViewContent(props: ChatViewProps) { ); const onAdvanceActivePendingUserInput = useCallback(() => { - if (!activePendingUserInput || !activePendingProgress) { + if ( + !activePendingUserInput || + activePendingUserInput.responseCapability !== "live" || + !activePendingProgress + ) { return; } if (activePendingProgress.isLastQuestion) { @@ -4137,7 +4179,7 @@ function ChatViewContent(props: ChatViewProps) { id: messageIdForSend, role: "user", text: outgoingMessageText, - turnId: null, + runId: null, createdAt: messageCreatedAt, updatedAt: messageCreatedAt, streaming: false, @@ -4403,9 +4445,9 @@ function ChatViewContent(props: ChatViewProps) { } const reason = getStartedThreadModelChangeBlockReason({ providers: providerStatuses, - hasStartedSession: activeThread.session !== null, + hasStartedSession: activeThread.runtime !== null, currentModelSelection: activeThread.modelSelection, - currentProviderInstanceId: activeThread.session?.providerInstanceId ?? null, + currentProviderInstanceId: activeThread.runtime?.providerInstanceId ?? null, nextModelSelection: { instanceId, model }, }); return reason ? `${reason.description} Start a new thread to use this model.` : null; @@ -4429,9 +4471,9 @@ function ChatViewContent(props: ChatViewProps) { scheduleComposerFocus(); return; } - if (lockedProvider !== null && activeThread.session?.providerInstanceId) { + if (lockedProvider !== null && activeThread.runtime?.providerInstanceId) { const currentEntry = providerStatuses.find( - (snapshot) => snapshot.instanceId === activeThread.session?.providerInstanceId, + (snapshot) => snapshot.instanceId === activeThread.runtime?.providerInstanceId, ); if ( currentEntry?.continuation?.groupKey && @@ -4458,9 +4500,9 @@ function ChatViewContent(props: ChatViewProps) { }; const modelChangeBlockReason = getStartedThreadModelChangeBlockReason({ providers: providerStatuses, - hasStartedSession: activeThread.session !== null, + hasStartedSession: activeThread.runtime !== null, currentModelSelection: activeThread.modelSelection, - currentProviderInstanceId: activeThread.session?.providerInstanceId ?? null, + currentProviderInstanceId: activeThread.runtime?.providerInstanceId ?? null, nextModelSelection, }); if (modelChangeBlockReason) { @@ -4540,9 +4582,9 @@ function ChatViewContent(props: ChatViewProps) { setExpandedImage(preview); }, []); const onOpenTurnDiff = useCallback( - (turnId: TurnId, filePath?: string) => { + (runId: RunId, filePath?: string) => { if (!isServerThread || !activeThreadRef) return; - useDiffPanelStore.getState().selectTurn(activeThreadRef, turnId, filePath); + useDiffPanelStore.getState().selectTurn(activeThreadRef, runId, filePath); useRightPanelStore.getState().open(activeThreadRef, "diff"); onDiffPanelOpen?.(); }, @@ -4727,11 +4769,11 @@ function ChatViewContent(props: ChatViewProps) { = {}): Thread { - return { + return makeThreadFixture({ id: ThreadId.make("thread-1"), environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, @@ -19,20 +20,18 @@ function makeThread(overrides: Partial = {}): Thread { modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5" }, runtimeMode: "full-access", interactionMode: "default", - session: null, + runtime: null, messages: [], proposedPlans: [], createdAt: "2026-03-01T00:00:00.000Z", archivedAt: null, deletedAt: null, updatedAt: "2026-03-01T00:00:00.000Z", - latestTurn: null, + latestRun: null, branch: null, worktreePath: null, - checkpoints: [], - activities: [], ...overrides, - }; + }); } describe("buildThreadActionItems", () => { diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index f39af581d5a..020b8b58405 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -5,7 +5,7 @@ import { squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; -import type { ScopedThreadRef, TurnId } from "@t3tools/contracts"; +import type { ScopedThreadRef, RunId } from "@t3tools/contracts"; import { ArrowRightIcon, CheckIcon, @@ -229,55 +229,55 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff : null, ); const isGitRepo = gitStatusQuery.data?.isRepo ?? true; - const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = + const { turnDiffSummaries, inferredCheckpointTurnCountByRunId } = useTurnDiffSummaries(activeThread); const orderedTurnDiffSummaries = useMemo( () => [...turnDiffSummaries].toSorted((left, right) => { const leftTurnCount = - left.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[left.turnId] ?? 0; + left.checkpointTurnCount ?? inferredCheckpointTurnCountByRunId[left.runId] ?? 0; const rightTurnCount = - right.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[right.turnId] ?? 0; + right.checkpointTurnCount ?? inferredCheckpointTurnCountByRunId[right.runId] ?? 0; if (leftTurnCount !== rightTurnCount) { return rightTurnCount - leftTurnCount; } return right.completedAt.localeCompare(left.completedAt); }), - [inferredCheckpointTurnCountByTurnId, turnDiffSummaries], + [inferredCheckpointTurnCountByRunId, turnDiffSummaries], ); useEffect(() => { if (!routeThreadRef || diffSelection.kind !== "turn") return; useDiffPanelStore.getState().reconcileTurnSelection( routeThreadRef, - orderedTurnDiffSummaries.map((summary) => summary.turnId), + orderedTurnDiffSummaries.map((summary) => summary.runId), ); }, [diffSelection, orderedTurnDiffSummaries, routeThreadRef]); - const selectedTurnId = diffSelection.kind === "turn" ? diffSelection.turnId : null; + const selectedRunId = diffSelection.kind === "turn" ? diffSelection.turnId : null; const selectedGitScope = diffSelection.kind === "unstaged" ? "unstaged" : "branch"; const selectedBaseRef = diffSelection.kind === "branch" ? diffSelection.baseRef : null; const selectedFilePath = diffSelection.kind === "turn" ? diffSelection.filePath : null; const selectedFileRevealRequestId = diffSelection.kind === "turn" ? diffSelection.revealRequestId : 0; const selectedTurn = - selectedTurnId === null + selectedRunId === null ? undefined - : (orderedTurnDiffSummaries.find((summary) => summary.turnId === selectedTurnId) ?? + : (orderedTurnDiffSummaries.find((summary) => summary.runId === selectedRunId) ?? orderedTurnDiffSummaries[0]); const selectedCheckpointTurnCount = selectedTurn && - (selectedTurn.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[selectedTurn.turnId]); + (selectedTurn.checkpointTurnCount ?? inferredCheckpointTurnCountByRunId[selectedTurn.runId]); const latestTurn = orderedTurnDiffSummaries[0]; const selectedScopeLabel = - selectedTurnId === null + selectedRunId === null ? selectedGitScope === "unstaged" ? "Working tree" : "Branch changes" - : selectedTurn?.turnId === latestTurn?.turnId + : selectedTurn?.runId === latestTurn?.runId ? "Latest turn" : `Turn ${selectedCheckpointTurnCount ?? "?"}`; - const reviewSectionId = selectedTurn ? `turn:${selectedTurn.turnId}` : selectedGitScope; + const reviewSectionId = selectedTurn ? `turn:${selectedTurn.runId}` : selectedGitScope; const collapseScopeKey = routeThreadRef ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}` : null; @@ -307,12 +307,12 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff fromTurnCount: selectedCheckpointRange?.fromTurnCount ?? null, toTurnCount: selectedCheckpointRange?.toTurnCount ?? null, ignoreWhitespace: diffIgnoreWhitespace, - cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : null, + cacheScope: selectedTurn ? `turn:${selectedTurn.runId}` : null, }, { enabled: isGitRepo && selectedTurn !== undefined }, ); const primaryBranchDiffPreview = useEnvironmentQuery( - selectedTurnId === null && activeThread && activeCwd + selectedRunId === null && activeThread && activeCwd ? reviewEnvironment.diffPreview({ environmentId: activeThread.environmentId, input: { @@ -324,7 +324,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff : null, ); const shouldRetryBranchDiffAtEnvironmentCwd = - selectedTurnId === null && + selectedRunId === null && primaryBranchDiffPreview.error?.includes("configured workspace root") === true && serverConfig?.cwd !== undefined && serverConfig.cwd !== activeCwd; @@ -347,7 +347,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff (source) => source.kind === (selectedGitScope === "unstaged" ? "working-tree" : "branch-range"), ); const localBranchRefs = useEnvironmentQuery( - selectedTurnId === null && + selectedRunId === null && selectedGitScope === "branch" && activeThread && branchDiffPreview.data?.cwd @@ -364,7 +364,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff : null, ); const remoteBranchRefs = useEnvironmentQuery( - selectedTurnId === null && + selectedRunId === null && selectedGitScope === "branch" && activeThread && branchDiffPreview.data?.cwd @@ -407,9 +407,9 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff const renderablePatch = useMemo( () => getRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}`, { - compactPartialHunkOffsets: selectedTurnId === null, + compactPartialHunkOffsets: selectedRunId === null, }), - [resolvedTheme, selectedPatch, selectedTurnId], + [resolvedTheme, selectedPatch, selectedRunId], ); const renderableFiles = useMemo(() => { if (!renderablePatch || renderablePatch.kind !== "files") { @@ -485,9 +485,9 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff [collapseScopeKey], ); - const selectTurn = (turnId: TurnId) => { + const selectTurn = (runId: RunId) => { if (!routeThreadRef) return; - useDiffPanelStore.getState().selectTurn(routeThreadRef, turnId); + useDiffPanelStore.getState().selectTurn(routeThreadRef, runId); }; const selectGitScope = (scope: "branch" | "unstaged") => { if (!routeThreadRef) return; @@ -512,23 +512,23 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff selectGitScope("unstaged")}> Working tree - {selectedTurnId === null && selectedGitScope === "unstaged" && ( + {selectedRunId === null && selectedGitScope === "unstaged" && ( )} selectGitScope("branch")}> Branch changes - {selectedTurnId === null && selectedGitScope === "branch" && ( + {selectedRunId === null && selectedGitScope === "branch" && ( )} { - if (latestTurn) selectTurn(latestTurn.turnId); + if (latestTurn) selectTurn(latestTurn.runId); }} > Latest turn - {selectedTurnId !== null && selectedTurn?.turnId === latestTurn?.turnId && ( + {selectedRunId !== null && selectedTurn?.runId === latestTurn?.runId && ( )} @@ -538,18 +538,15 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff {orderedTurnDiffSummaries.map((summary) => { const turnCount = summary.checkpointTurnCount ?? - inferredCheckpointTurnCountByTurnId[summary.turnId] ?? + inferredCheckpointTurnCountByRunId[summary.runId] ?? "?"; return ( - selectTurn(summary.turnId)} - > + selectTurn(summary.runId)}> Turn {turnCount} {formatShortTimestamp(summary.completedAt, settings.timestampFormat)} - {summary.turnId === selectedTurn?.turnId && } + {summary.runId === selectedTurn?.runId && } ); })} @@ -557,7 +554,7 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff - {selectedTurnId === null && selectedGitScope === "branch" && selectedGitSource?.baseRef && ( + {selectedRunId === null && selectedGitScope === "branch" && selectedGitSource?.baseRef && (
Turn diffs are unavailable because this project is not a git repository.
- ) : selectedTurnId !== null && orderedTurnDiffSummaries.length === 0 ? ( + ) : selectedRunId !== null && orderedTurnDiffSummaries.length === 0 ? (
No completed turns yet.
diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 574e33d4dab..b2b882ab30a 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -21,19 +21,14 @@ import { sortProjectsForSidebar, THREAD_JUMP_HINT_SHOW_DELAY_MS, } from "./Sidebar.logic"; -import { - EnvironmentId, - OrchestrationLatestTurn, - ProjectId, - ProviderInstanceId, - ThreadId, -} from "@t3tools/contracts"; +import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type Project, type Thread, } from "../types"; +import { makeThreadFixture } from "../test-fixtures"; const localEnvironmentId = EnvironmentId.make("environment-local"); @@ -75,13 +70,13 @@ describe("resolveSidebarStageBadgeLabel", () => { }); }); -function makeLatestTurn(overrides?: { +function makeLatestRun(overrides?: { completedAt?: string | null; startedAt?: string | null; -}): OrchestrationLatestTurn { +}): NonNullable { return { - turnId: "turn-1" as never, - state: "completed", + runId: "turn-1" as never, + status: "completed", assistantMessageId: null, requestedAt: "2026-03-09T10:00:00.000Z", startedAt: overrides?.startedAt ?? "2026-03-09T10:00:00.000Z", @@ -97,9 +92,9 @@ describe("hasUnseenCompletion", () => { hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "default", - latestTurn: makeLatestTurn(), + latestRun: makeLatestRun(), lastVisitedAt: "2026-03-09T10:04:00.000Z", - session: null, + runtime: null, }), ).toBe(true); }); @@ -111,9 +106,9 @@ describe("hasUnseenCompletion", () => { hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "default", - latestTurn: makeLatestTurn(), + latestRun: makeLatestRun(), lastVisitedAt: undefined, - session: null, + runtime: null, }), ).toBe(false); }); @@ -570,15 +565,13 @@ describe("resolveThreadStatusPill", () => { hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "plan" as const, - latestTurn: null, + latestRun: null, lastVisitedAt: undefined, - session: { - threadId: ThreadId.make("thread-1"), + runtime: { status: "running" as const, providerName: "Codex", providerInstanceId: ProviderInstanceId.make("codex"), - runtimeMode: DEFAULT_RUNTIME_MODE, - activeTurnId: "turn-1" as never, + activeRunId: "turn-1" as never, lastError: null, updatedAt: "2026-03-09T10:00:00.000Z", }, @@ -621,11 +614,11 @@ describe("resolveThreadStatusPill", () => { thread: { ...baseThread, hasActionableProposedPlan: true, - latestTurn: makeLatestTurn(), - session: { - ...baseThread.session, - status: "ready", - activeTurnId: null, + latestRun: makeLatestRun(), + runtime: { + ...baseThread.runtime, + status: "completed", + activeRunId: null, }, }, }), @@ -637,11 +630,11 @@ describe("resolveThreadStatusPill", () => { resolveThreadStatusPill({ thread: { ...baseThread, - latestTurn: makeLatestTurn(), - session: { - ...baseThread.session, - status: "ready", - activeTurnId: null, + latestRun: makeLatestRun(), + runtime: { + ...baseThread.runtime, + status: "completed", + activeRunId: null, }, }, }), @@ -654,12 +647,12 @@ describe("resolveThreadStatusPill", () => { thread: { ...baseThread, interactionMode: "default", - latestTurn: makeLatestTurn(), + latestRun: makeLatestRun(), lastVisitedAt: "2026-03-09T10:04:00.000Z", - session: { - ...baseThread.session, - status: "ready", - activeTurnId: null, + runtime: { + ...baseThread.runtime, + status: "completed", + activeRunId: null, }, }, }), @@ -813,7 +806,7 @@ function makeProject(overrides: Partial = {}): Project { } function makeThread(overrides: Partial = {}): Thread { - return { + return makeThreadFixture({ id: ThreadId.make("thread-1"), environmentId: localEnvironmentId, projectId: ProjectId.make("project-1"), @@ -825,20 +818,18 @@ function makeThread(overrides: Partial = {}): Thread { }, runtimeMode: DEFAULT_RUNTIME_MODE, interactionMode: DEFAULT_INTERACTION_MODE, - session: null, + runtime: null, messages: [], proposedPlans: [], createdAt: "2026-03-09T10:00:00.000Z", archivedAt: null, deletedAt: null, updatedAt: "2026-03-09T10:00:00.000Z", - latestTurn: null, + latestRun: null, branch: null, worktreePath: null, - checkpoints: [], - activities: [], ...overrides, - }; + }); } describe("getFallbackThreadIdAfterDelete", () => { @@ -922,7 +913,7 @@ describe("sortProjectsForSidebar", () => { id: "message-1" as never, role: "user", text: "older project user message", - turnId: null, + runId: null, createdAt: "2026-03-09T10:01:00.000Z", updatedAt: "2026-03-09T10:01:00.000Z", streaming: false, @@ -938,7 +929,7 @@ describe("sortProjectsForSidebar", () => { id: "message-2" as never, role: "user", text: "newer project user message", - turnId: null, + runId: null, createdAt: "2026-03-09T10:05:00.000Z", updatedAt: "2026-03-09T10:05:00.000Z", streaming: false, diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 4e7614ed551..9b2cb61c4e7 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -8,7 +8,7 @@ import { } from "../lib/threadSort"; import type { SidebarThreadSummary, Thread } from "../types"; import { cn } from "../lib/utils"; -import { isLatestTurnSettled } from "../session-logic"; +import { isLatestRunSettled } from "../session-logic"; import { resolveServerBackedAppStageLabel } from "../branding.logic"; export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]"; @@ -54,8 +54,8 @@ type ThreadStatusInput = Pick< | "hasPendingApprovals" | "hasPendingUserInput" | "interactionMode" - | "latestTurn" - | "session" + | "latestRun" + | "runtime" > & { lastVisitedAt?: string | undefined; }; @@ -153,8 +153,8 @@ export function useThreadJumpHintVisibility(): { } export function hasUnseenCompletion(thread: ThreadStatusInput): boolean { - if (!thread.latestTurn?.completedAt) return false; - const completedAt = Date.parse(thread.latestTurn.completedAt); + if (!thread.latestRun?.completedAt) return false; + const completedAt = Date.parse(thread.latestRun.completedAt); if (Number.isNaN(completedAt)) return false; if (!thread.lastVisitedAt) return false; @@ -380,7 +380,7 @@ export function resolveThreadStatusPill(input: { }; } - if (thread.session?.status === "running") { + if (thread.runtime?.status === "running" || thread.runtime?.status === "waiting") { return { label: "Working", colorClass: "text-sky-600 dark:text-sky-300/80", @@ -389,7 +389,7 @@ export function resolveThreadStatusPill(input: { }; } - if (thread.session?.status === "starting") { + if (thread.runtime?.status === "starting" || thread.runtime?.status === "queued") { return { label: "Connecting", colorClass: "text-sky-600 dark:text-sky-300/80", @@ -401,7 +401,7 @@ export function resolveThreadStatusPill(input: { const hasPlanReadyPrompt = !thread.hasPendingUserInput && thread.interactionMode === "plan" && - isLatestTurnSettled(thread.latestTurn, thread.session) && + isLatestRunSettled(thread.latestRun, thread.runtime) && thread.hasActionableProposedPlan; if (hasPlanReadyPrompt) { return { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index f3ed88bd3b9..b97b9c593bf 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -445,7 +445,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr [discoveredPorts, navigateToThread, openPreview, threadRef], ); const isThreadRunning = - thread.session?.status === "running" && thread.session.activeTurnId != null; + thread.runtime?.status === "running" && thread.runtime.activeRunId != null; const threadStatus = resolveThreadStatusPill({ thread: { ...thread, @@ -1771,7 +1771,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec if (clicked === "mark-unread") { for (const threadKey of threadKeys) { const thread = sidebarThreadByKeyRef.current.get(threadKey); - markThreadUnread(threadKey, thread?.latestTurn?.completedAt); + markThreadUnread(threadKey, thread?.latestRun?.completedAt); } clearSelection(); return; @@ -2127,7 +2127,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec } if (clicked === "mark-unread") { - markThreadUnread(threadKey, thread.latestTurn?.completedAt); + markThreadUnread(threadKey, thread.latestRun?.completedAt); return; } if (clicked === "copy-path") { diff --git a/apps/web/src/components/chat/ChangedFilesTree.test.tsx b/apps/web/src/components/chat/ChangedFilesTree.test.tsx index c371acdb362..b6e026b9e35 100644 --- a/apps/web/src/components/chat/ChangedFilesTree.test.tsx +++ b/apps/web/src/components/chat/ChangedFilesTree.test.tsx @@ -1,4 +1,4 @@ -import { TurnId } from "@t3tools/contracts"; +import { RunId } from "@t3tools/contracts"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vite-plus/test"; @@ -54,7 +54,7 @@ describe("ChangedFilesTree", () => { ({ files, visibleLabels, hiddenLabels }) => { const markup = renderToStaticMarkup( { ({ files, visibleLabels }) => { const markup = renderToStaticMarkup( = {}; export const ChangedFilesCard = memo(function ChangedFilesCard(props: { - turnId: TurnId; + runId: RunId; files: ReadonlyArray; allDirectoriesExpanded: boolean; resolvedTheme: "light" | "dark"; onToggleAllDirectories: () => void; - onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void; + onOpenTurnDiff: (runId: RunId, filePath?: string) => void; }) { const { - turnId, + runId, files, allDirectoriesExpanded, resolvedTheme, @@ -60,7 +60,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: { type="button" size="xs" variant="outline" - onClick={() => onOpenTurnDiff(turnId, files[0]?.path)} + onClick={() => onOpenTurnDiff(runId, files[0]?.path)} > View diff @@ -68,8 +68,8 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
; allDirectoriesExpanded: boolean; resolvedTheme: "light" | "dark"; - onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void; + onOpenTurnDiff: (runId: RunId, filePath?: string) => void; }) { - const { files, allDirectoriesExpanded, onOpenTurnDiff, resolvedTheme, turnId } = props; + const { files, allDirectoriesExpanded, onOpenTurnDiff, resolvedTheme, runId } = props; const treeNodes = useMemo(() => buildTurnDiffTree(files), [files]); const directoryPathsKey = useMemo( () => collectDirectoryPaths(treeNodes).join("\u0000"), @@ -172,7 +172,7 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: { type="button" className="group flex w-full items-center gap-1.5 rounded-xl py-1 pr-3 text-left transition-colors hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background" style={{ paddingLeft: `${leftPadding}px` }} - onClick={() => onOpenTurnDiff(turnId, node.path)} + onClick={() => onOpenTurnDiff(runId, node.path)} > {hasDirectoryNodes || depth > 0 ? (
@@ -2472,6 +2473,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) diff --git a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx index 64c3acc7bf7..33734c63c8b 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx @@ -1,12 +1,13 @@ -import { type ApprovalRequestId, type ProviderApprovalDecision } from "@t3tools/contracts"; +import { type RuntimeRequestId, type ProviderApprovalDecision } from "@t3tools/contracts"; import { memo } from "react"; import { Button } from "../ui/button"; interface ComposerPendingApprovalActionsProps { - requestId: ApprovalRequestId; + requestId: RuntimeRequestId; isResponding: boolean; + canRespond: boolean; onRespondToApproval: ( - requestId: ApprovalRequestId, + requestId: RuntimeRequestId, decision: ProviderApprovalDecision, ) => Promise; } @@ -14,6 +15,7 @@ interface ComposerPendingApprovalActionsProps { export const ComposerPendingApprovalActions = memo(function ComposerPendingApprovalActions({ requestId, isResponding, + canRespond, onRespondToApproval, }: ComposerPendingApprovalActionsProps) { return ( @@ -21,7 +23,7 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro @@ -905,15 +1075,15 @@ function AssistantChangedFilesSectionInner({ type="button" size="xs" variant="outline" - onClick={() => onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path)} + onClick={() => onOpenTurnDiff(turnSummary.runId, checkpointFiles[0]?.path)} > View diff 0 ? blocks.join("\n\n") : null; } function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName { - if ( - workEntry.sourceActivityKind === "user-input.requested" || - workEntry.sourceActivityKind === "user-input.resolved" - ) { + if (workEntry.itemType === "user_input_request") { return "message-circle"; } if (workEntry.requestKind === "command") return "terminal"; @@ -1509,13 +1690,12 @@ function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName { return "square-pen"; } if (workEntry.itemType === "web_search") return "globe"; - if (workEntry.itemType === "image_view") return "eye"; + if (workEntry.itemType === "file_search") return "eye"; switch (workEntry.itemType) { - case "mcp_tool_call": + case "dynamic_tool": return "wrench"; - case "dynamic_tool_call": - case "collab_agent_tool_call": + case "subagent": return "hammer"; } @@ -1547,7 +1727,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { const activity = use(TimelineRowActivityCtx); const [expanded, setExpanded] = useState(false); const iconConfig = workToneIcon(workEntry.tone); - const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning"; + const showWarningIndicator = false; const entryIconName = showWarningIndicator ? "x" : workEntryIconName(workEntry); const heading = toolWorkEntryHeading(workEntry); const rawPreview = workEntryPreview(workEntry, workspaceRoot); @@ -1561,9 +1741,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { const expandedBody = buildToolCallExpandedBody(workEntry, workspaceRoot); const canExpand = expandedBody !== null; const showFailedIndicator = workEntryIndicatesToolFailure(workEntry); - const showDestructiveRowStyle = - showFailedIndicator && - (workEntry.sourceActivityKind === "runtime.error" || !workLogEntryIsToolLike(workEntry)); + const showDestructiveRowStyle = showFailedIndicator && !workLogEntryIsToolLike(workEntry); const iconWrapperClass = cn( "flex size-5 shrink-0 items-center justify-center", showWarningIndicator @@ -1606,6 +1784,8 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { canExpand && "cursor-pointer hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70", )} + data-v2-item-type={workEntry.projectedItem?.item.type} + data-v2-item-visibility={workEntry.projectedItem?.visibility} {...rowToggleProps} >
@@ -1619,6 +1799,12 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {

{heading} + {workEntry.projectedItem?.visibility !== undefined && + workEntry.projectedItem.visibility !== "local" ? ( + + {workEntry.projectedItem.visibility === "inherited" ? "Inherited" : "Synthetic"} + + ) : null} {preview && ( {preview} )} diff --git a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx index 77c1813f110..674228dd63e 100644 --- a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx +++ b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx @@ -13,7 +13,7 @@ import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSet import { cn } from "../../lib/utils"; import { normalizeProviderAccentColor } from "../../providerInstances"; import { Button } from "../ui/button"; -import { ACPRegistryIcon, Gemini, GithubCopilotIcon, PiAgentIcon, type Icon } from "../Icons"; +import { Gemini, GithubCopilotIcon, PiAgentIcon, type Icon } from "../Icons"; import { Dialog, DialogDescription, @@ -81,11 +81,6 @@ const COMING_SOON_DRIVER_OPTIONS: readonly ComingSoonDriverOption[] = [ label: "Gemini", icon: Gemini, }, - { - value: ProviderDriverKind.make("acpRegistry"), - label: "ACP Registry", - icon: ACPRegistryIcon, - }, { value: ProviderDriverKind.make("piAgent"), label: "Pi Agent", diff --git a/apps/web/src/components/settings/ProviderSettingsForm.test.ts b/apps/web/src/components/settings/ProviderSettingsForm.test.ts index 33331c14901..fabf27d05c2 100644 --- a/apps/web/src/components/settings/ProviderSettingsForm.test.ts +++ b/apps/web/src/components/settings/ProviderSettingsForm.test.ts @@ -36,6 +36,18 @@ describe("ProviderSettingsForm helpers", () => { }); }); + it("exposes ACP Registry as an instance-only configurable driver", () => { + const acpRegistry = DRIVER_OPTION_BY_VALUE[ProviderDriverKind.make("acpRegistry")]; + + expect(acpRegistry).toBeDefined(); + expect(acpRegistry?.hasDefaultInstance).toBe(false); + expect(deriveProviderSettingsFields(acpRegistry!).map((field) => field.key)).toEqual([ + "agentId", + "commandPath", + "authMethodId", + ]); + }); + it("preserves unknown config keys while omitting empty configurable fields", () => { const opencode = DRIVER_OPTION_BY_VALUE[ProviderDriverKind.make("opencode")]; expect(opencode).toBeDefined(); diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 994cbb08f23..bb433ff92c0 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -12,6 +12,7 @@ import { type ScopedThreadRef, } from "@t3tools/contracts"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { presentThreadShell } from "@t3tools/client-runtime/state/shell"; import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; import { isAtomCommandInterrupted, @@ -130,6 +131,7 @@ function withoutProviderInstanceFavorites( const PROVIDER_SETTINGS = DRIVER_OPTIONS.map((definition) => ({ provider: definition.value, + hasDefaultInstance: definition.hasDefaultInstance !== false, })); function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null }) { @@ -1117,9 +1119,9 @@ export function ProviderSettingsPanel() { } const defaultSlotIdsBySource = new Set( - visibleProviderSettings.map((providerSettings) => - String(defaultInstanceIdForDriver(providerSettings.provider)), - ), + visibleProviderSettings + .filter((providerSettings) => providerSettings.hasDefaultInstance) + .map((providerSettings) => String(defaultInstanceIdForDriver(providerSettings.provider))), ); const rows: InstanceRow[] = []; @@ -1128,6 +1130,12 @@ export function ProviderSettingsPanel() { ); for (const providerSettings of visibleProviderSettings) { + if (!providerSettings.hasDefaultInstance) { + for (const [id, instance] of instancesByDriver.get(providerSettings.provider) ?? []) { + rows.push({ instanceId: id, instance, driver: instance.driver, isDefault: false }); + } + continue; + } type LegacyProviderSettings = (typeof settings.providers)[keyof typeof settings.providers]; const legacyProviders = settings.providers as Record; const defaultLegacyProviders = DEFAULT_UNIFIED_SETTINGS.providers as Record< @@ -1454,10 +1462,7 @@ export function ArchivedThreadsPanel() { ), ); const threads = archivedSnapshots.flatMap(({ environmentId, snapshot }) => - snapshot.threads.map((thread) => ({ - ...thread, - environmentId, - })), + snapshot.threads.map((thread) => presentThreadShell(environmentId, thread)), ); const archivedProjects = Array.from(projectsByEnvironmentAndId.values()); diff --git a/apps/web/src/components/settings/providerDriverMeta.ts b/apps/web/src/components/settings/providerDriverMeta.ts index bfee6a8d680..fd4f72e4c3d 100644 --- a/apps/web/src/components/settings/providerDriverMeta.ts +++ b/apps/web/src/components/settings/providerDriverMeta.ts @@ -1,4 +1,5 @@ import { + AcpRegistrySettings, ClaudeSettings, CodexSettings, CursorSettings, @@ -7,7 +8,15 @@ import { ProviderDriverKind, } from "@t3tools/contracts"; import type * as Schema from "effect/Schema"; -import { ClaudeAI, CursorIcon, GrokIcon, type Icon, OpenAI, OpenCodeIcon } from "../Icons"; +import { + ACPRegistryIcon, + ClaudeAI, + CursorIcon, + GrokIcon, + type Icon, + OpenAI, + OpenCodeIcon, +} from "../Icons"; type ProviderSettingsSchema = { readonly fields: Readonly>; @@ -24,6 +33,8 @@ export interface ProviderClientDefinition { readonly label: string; readonly icon: Icon; readonly settingsSchema: ProviderSettingsSchema; + /** Whether this driver has a built-in default instance backed by legacy settings. */ + readonly hasDefaultInstance?: boolean; /** * Optional short label rendered as a `variant="warning"` badge next to * the instance title. Used to flag drivers that still ship under an @@ -61,6 +72,14 @@ export const PROVIDER_CLIENT_DEFINITIONS: readonly ProviderClientDefinition[] = badgeLabel: "Early Access", settingsSchema: GrokSettings, }, + { + value: ProviderDriverKind.make("acpRegistry"), + label: "ACP Registry", + icon: ACPRegistryIcon, + badgeLabel: "V2 Preview", + settingsSchema: AcpRegistrySettings, + hasDefaultInstance: false, + }, { value: ProviderDriverKind.make("opencode"), label: "OpenCode", diff --git a/apps/web/src/connection/storage.ts b/apps/web/src/connection/storage.ts index d118a428ed7..b3f0633bb47 100644 --- a/apps/web/src/connection/storage.ts +++ b/apps/web/src/connection/storage.ts @@ -6,6 +6,10 @@ import { ConnectionTargetStore, EMPTY_CONNECTION_CATALOG_DOCUMENT, EnvironmentCacheStore, + ORCHESTRATION_CACHE_SCHEMA_VERSION, + StoredOrchestrationShellSnapshot, + StoredOrchestrationThreadSnapshot, + decodeOrDiscardOrchestrationCache, registerConnectionInCatalog, removeCatalogValue, removeConnectionFromCatalog, @@ -17,12 +21,7 @@ import { CredentialStore, ProfileStore, } from "@t3tools/client-runtime/connection"; -import { - EnvironmentId, - OrchestrationShellSnapshot, - OrchestrationThread, - ThreadId, -} from "@t3tools/contracts"; +import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -37,20 +36,9 @@ const CATALOG_STORE_NAME = "catalog"; const SHELL_STORE_NAME = "shell"; const THREAD_STORE_NAME = "thread"; const CATALOG_KEY = "document"; -const SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION = 1; - -const StoredShellSnapshot = Schema.Struct({ - schemaVersion: Schema.Literal(SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION), - environmentId: EnvironmentId, - snapshot: OrchestrationShellSnapshot, -}); +const StoredShellSnapshot = StoredOrchestrationShellSnapshot; const StoredShellSnapshotJson = Schema.fromJsonString(StoredShellSnapshot); -const StoredThreadSnapshot = Schema.Struct({ - schemaVersion: Schema.Literal(1), - environmentId: EnvironmentId, - threadId: ThreadId, - thread: OrchestrationThread, -}); +const StoredThreadSnapshot = StoredOrchestrationThreadSnapshot; const StoredThreadSnapshotJson = Schema.fromJsonString(StoredThreadSnapshot); const ConnectionCatalogDocumentJson = Schema.fromJsonString(ConnectionCatalogDocument); const decodeConnectionCatalogDocument = Schema.decodeUnknownEffect(ConnectionCatalogDocumentJson); @@ -429,25 +417,24 @@ export const connectionStorageLayer = Layer.effectContext( if (typeof raw !== "string") { return Effect.succeed(Option.none()); } - return decodeStoredShellSnapshot(raw).pipe( - Effect.mapError((cause) => persistenceError("load-shell", cause)), - Effect.map((stored) => - stored.environmentId === environmentId - ? Option.some(stored.snapshot) - : Option.none(), + return decodeOrDiscardOrchestrationCache( + decodeStoredShellSnapshot(raw).pipe( + Effect.mapError((cause) => persistenceError("load-shell", cause)), + Effect.map((stored) => + stored.environmentId === environmentId + ? Option.some(stored.snapshot) + : Option.none(), + ), ), + removeDatabaseValue(database, SHELL_STORE_NAME, environmentId), ); }), - Effect.mapError((cause) => - cause._tag === "ConnectionPersistenceError" - ? cause - : persistenceError("load-shell", cause), - ), + Effect.mapError((cause) => persistenceError("load-shell", cause)), ), saveShell: (environmentId, snapshot) => Effect.gen(function* () { const encoded = yield* encodeStoredShellSnapshot({ - schemaVersion: SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION, + schemaVersion: ORCHESTRATION_CACHE_SCHEMA_VERSION, environmentId, snapshot, }).pipe(Effect.mapError((cause) => persistenceError("save-shell", cause))); @@ -469,33 +456,36 @@ export const connectionStorageLayer = Layer.effectContext( if (typeof raw !== "string") { return Effect.succeed(Option.none()); } - return decodeStoredThreadSnapshot(raw).pipe( - Effect.mapError((cause) => persistenceError("load-thread", cause)), - Effect.map((stored) => - stored.environmentId === environmentId && stored.threadId === threadId - ? Option.some(stored.thread) - : Option.none(), + return decodeOrDiscardOrchestrationCache( + decodeStoredThreadSnapshot(raw).pipe( + Effect.mapError((cause) => persistenceError("load-thread", cause)), + Effect.map((stored) => + stored.environmentId === environmentId && stored.threadId === threadId + ? Option.some(stored.thread) + : Option.none(), + ), + ), + removeDatabaseValue( + database, + THREAD_STORE_NAME, + threadCacheKey(environmentId, threadId), ), ); }), - Effect.mapError((cause) => - cause._tag === "ConnectionPersistenceError" - ? cause - : persistenceError("load-thread", cause), - ), + Effect.mapError((cause) => persistenceError("load-thread", cause)), ), saveThread: (environmentId, thread) => Effect.gen(function* () { const encoded = yield* encodeStoredThreadSnapshot({ - schemaVersion: 1, + schemaVersion: ORCHESTRATION_CACHE_SCHEMA_VERSION, environmentId, - threadId: thread.id, + threadId: thread.thread.id, thread, }).pipe(Effect.mapError((cause) => persistenceError("save-thread", cause))); yield* writeDatabaseValue( database, THREAD_STORE_NAME, - threadCacheKey(environmentId, thread.id), + threadCacheKey(environmentId, thread.thread.id), encoded, ); }).pipe( diff --git a/apps/web/src/diffPanelStore.test.ts b/apps/web/src/diffPanelStore.test.ts index 64846e8e9f1..49b2dd5db54 100644 --- a/apps/web/src/diffPanelStore.test.ts +++ b/apps/web/src/diffPanelStore.test.ts @@ -1,5 +1,5 @@ import { scopeThreadRef } from "@t3tools/client-runtime/environment"; -import { EnvironmentId, ThreadId, TurnId } from "@t3tools/contracts"; +import { EnvironmentId, ThreadId, RunId } from "@t3tools/contracts"; import { beforeEach, describe, expect, it } from "vite-plus/test"; import { selectThreadDiffPanelSelection, useDiffPanelStore } from "./diffPanelStore"; @@ -17,7 +17,7 @@ describe("diffPanelStore", () => { it("clears incompatible selection fields when changing scopes", () => { const store = useDiffPanelStore.getState(); - store.selectTurn(THREAD_REF, TurnId.make("turn-1"), "src/app.ts"); + store.selectTurn(THREAD_REF, RunId.make("turn-1"), "src/app.ts"); store.selectGitScope(THREAD_REF, "unstaged"); expect( @@ -31,7 +31,7 @@ describe("diffPanelStore", () => { }); it("increments the reveal request when opening the same turn file again", () => { - const turnId = TurnId.make("turn-1"); + const turnId = RunId.make("turn-1"); useDiffPanelStore.getState().selectTurn(THREAD_REF, turnId, "src/app.ts"); useDiffPanelStore.getState().selectTurn(THREAD_REF, turnId, "src/app.ts"); @@ -51,8 +51,8 @@ describe("diffPanelStore", () => { }); it("reconciles a missing turn selection to the latest available turn", () => { - const missingTurnId = TurnId.make("turn-missing"); - const latestTurnId = TurnId.make("turn-latest"); + const missingTurnId = RunId.make("turn-missing"); + const latestTurnId = RunId.make("turn-latest"); useDiffPanelStore.getState().selectTurn(THREAD_REF, missingTurnId, "src/app.ts"); useDiffPanelStore.getState().reconcileTurnSelection(THREAD_REF, [latestTurnId]); diff --git a/apps/web/src/diffPanelStore.ts b/apps/web/src/diffPanelStore.ts index c946b286d1b..a4d5bd274f5 100644 --- a/apps/web/src/diffPanelStore.ts +++ b/apps/web/src/diffPanelStore.ts @@ -1,5 +1,5 @@ import { scopedThreadKey } from "@t3tools/client-runtime/environment"; -import type { ScopedThreadRef, TurnId } from "@t3tools/contracts"; +import type { RunId, ScopedThreadRef } from "@t3tools/contracts"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; @@ -8,7 +8,7 @@ import { resolveStorage } from "./lib/storage"; export type DiffPanelSelection = | { kind: "branch"; baseRef: string | null } | { kind: "unstaged" } - | { kind: "turn"; turnId: TurnId; filePath: string | null; revealRequestId: number }; + | { kind: "turn"; turnId: RunId; filePath: string | null; revealRequestId: number }; const DEFAULT_SELECTION: DiffPanelSelection = { kind: "branch", baseRef: null }; @@ -17,8 +17,8 @@ interface DiffPanelStoreState { branchBaseRefByThreadKey: Record; selectGitScope: (ref: ScopedThreadRef, scope: "branch" | "unstaged") => void; selectBranchBaseRef: (ref: ScopedThreadRef, baseRef: string | null) => void; - selectTurn: (ref: ScopedThreadRef, turnId: TurnId, filePath?: string) => void; - reconcileTurnSelection: (ref: ScopedThreadRef, availableTurnIds: ReadonlyArray) => void; + selectTurn: (ref: ScopedThreadRef, turnId: RunId, filePath?: string) => void; + reconcileTurnSelection: (ref: ScopedThreadRef, availableTurnIds: ReadonlyArray) => void; removeThread: (ref: ScopedThreadRef) => void; } @@ -118,7 +118,7 @@ export const useDiffPanelStore = create()( }), { name: "t3code:diff-panel-state:v1", - version: 1, + version: 2, storage: createJSONStorage(() => resolveStorage(typeof window !== "undefined" ? window.localStorage : undefined), ), diff --git a/apps/web/src/historyBootstrap.test.ts b/apps/web/src/historyBootstrap.test.ts index b4be13716ea..8c35fef269c 100644 --- a/apps/web/src/historyBootstrap.test.ts +++ b/apps/web/src/historyBootstrap.test.ts @@ -14,7 +14,7 @@ describe("buildBootstrapInput", () => { role: "user", text: "hello", createdAt: "2026-02-09T00:00:00.000Z", - turnId: null, + runId: null, updatedAt: "2026-02-09T00:00:00.000Z", streaming: false, }, @@ -23,7 +23,7 @@ describe("buildBootstrapInput", () => { role: "assistant", text: "world", createdAt: "2026-02-09T00:00:01.000Z", - turnId: null, + runId: null, updatedAt: "2026-02-09T00:00:01.000Z", streaming: false, }, @@ -49,7 +49,7 @@ describe("buildBootstrapInput", () => { role: "user", text: "first question with details", createdAt: "2026-02-09T00:00:00.000Z", - turnId: null, + runId: null, updatedAt: "2026-02-09T00:00:00.000Z", streaming: false, }, @@ -58,7 +58,7 @@ describe("buildBootstrapInput", () => { role: "assistant", text: "first answer with details", createdAt: "2026-02-09T00:00:01.000Z", - turnId: null, + runId: null, updatedAt: "2026-02-09T00:00:01.000Z", streaming: false, }, @@ -67,7 +67,7 @@ describe("buildBootstrapInput", () => { role: "user", text: "second question with details", createdAt: "2026-02-09T00:00:02.000Z", - turnId: null, + runId: null, updatedAt: "2026-02-09T00:00:02.000Z", streaming: false, }, @@ -92,7 +92,7 @@ describe("buildBootstrapInput", () => { role: "user", text: "old context", createdAt: "2026-02-09T00:00:00.000Z", - turnId: null, + runId: null, updatedAt: "2026-02-09T00:00:00.000Z", streaming: false, }, @@ -124,7 +124,7 @@ describe("buildBootstrapInput", () => { }, ], createdAt: "2026-02-09T00:00:00.000Z", - turnId: null, + runId: null, updatedAt: "2026-02-09T00:00:00.000Z", streaming: false, }, diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts index 35783348068..5d5c4de7cdf 100644 --- a/apps/web/src/hooks/useThreadActions.ts +++ b/apps/web/src/hooks/useThreadActions.ts @@ -85,7 +85,7 @@ export function useThreadActions() { const resolved = resolveThreadTarget(target); if (!resolved) return AsyncResult.success(undefined); const { thread, threadRef } = resolved; - if (thread.session?.status === "running" && thread.session.activeTurnId != null) { + if (thread.runtime?.status === "running" && thread.runtime.activeRunId != null) { return AsyncResult.failure( Cause.fail( new ThreadArchiveBlockedError({ @@ -201,7 +201,7 @@ export function useThreadActions() { shouldDeleteWorktree = confirmationResult.value; } - if (thread.session && thread.session.status !== "stopped") { + if (thread.runtime !== null) { await stopThreadSession({ environmentId: threadRef.environmentId, input: { threadId: threadRef.threadId }, diff --git a/apps/web/src/hooks/useTurnDiffSummaries.ts b/apps/web/src/hooks/useTurnDiffSummaries.ts index f51acc15cc0..21e28be7bbd 100644 --- a/apps/web/src/hooks/useTurnDiffSummaries.ts +++ b/apps/web/src/hooks/useTurnDiffSummaries.ts @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { inferCheckpointTurnCountByTurnId } from "../session-logic"; +import { inferCheckpointTurnCountByRunId } from "../session-logic"; import type { Thread, TurnDiffSummary } from "../types"; export function useTurnDiffSummaries(activeThread: Thread | null | undefined) { @@ -10,10 +10,10 @@ export function useTurnDiffSummaries(activeThread: Thread | null | undefined) { return activeThread.checkpoints; }, [activeThread]); - const inferredCheckpointTurnCountByTurnId = useMemo( - () => inferCheckpointTurnCountByTurnId(turnDiffSummaries), + const inferredCheckpointTurnCountByRunId = useMemo( + () => inferCheckpointTurnCountByRunId(turnDiffSummaries), [turnDiffSummaries], ); - return { turnDiffSummaries, inferredCheckpointTurnCountByTurnId }; + return { turnDiffSummaries, inferredCheckpointTurnCountByRunId }; } diff --git a/apps/web/src/lib/contextWindow.test.ts b/apps/web/src/lib/contextWindow.test.ts index c3226884a31..3a56d20f0d5 100644 --- a/apps/web/src/lib/contextWindow.test.ts +++ b/apps/web/src/lib/contextWindow.test.ts @@ -1,84 +1,44 @@ import { describe, expect, it } from "vite-plus/test"; -import { EventId, type OrchestrationThreadActivity, TurnId } from "@t3tools/contracts"; - import { deriveLatestContextWindowSnapshot, formatContextWindowTokens } from "./contextWindow"; -function makeActivity(id: string, kind: string, payload: unknown): OrchestrationThreadActivity { - return { - id: EventId.make(id), - tone: "info", - kind, - summary: kind, - payload, - turnId: TurnId.make("turn-1"), - createdAt: "2026-03-23T00:00:00.000Z", - }; -} - -describe("contextWindow", () => { - it("derives the latest valid context window snapshot", () => { - const snapshot = deriveLatestContextWindowSnapshot([ - makeActivity("activity-1", "context-window.updated", { - usedTokens: 1000, - }), - makeActivity("activity-2", "tool.started", {}), - makeActivity("activity-3", "context-window.updated", { - usedTokens: 14_000, - maxTokens: 258_000, - compactsAutomatically: true, - }), - ]); - - expect(snapshot).not.toBeNull(); - expect(snapshot?.usedTokens).toBe(14_000); - expect(snapshot?.totalProcessedTokens).toBeNull(); - expect(snapshot?.maxTokens).toBe(258_000); - expect(snapshot?.compactsAutomatically).toBe(true); - }); - - it("ignores malformed payloads", () => { - const snapshot = deriveLatestContextWindowSnapshot([ - makeActivity("activity-1", "context-window.updated", {}), - ]); - - expect(snapshot).toBeNull(); - }); - - it("keeps valid zero-usage snapshots", () => { +describe("V2 context window presentation", () => { + it("uses retained compaction token data when available", () => { const snapshot = deriveLatestContextWindowSnapshot([ - makeActivity("activity-1", "context-window.updated", { - usedTokens: 0, - maxTokens: 100_000, - }), + { + id: "compaction-1", + createdAt: "2026-06-20T00:00:00.000Z", + runId: null, + label: "Context compacted", + tone: "info", + itemType: "compaction", + toolLifecycleStatus: "completed", + structuredPayload: { + id: "compaction-1" as never, + threadId: "thread-1" as never, + runId: null, + nodeId: null, + providerThreadId: null, + providerTurnId: null, + nativeItemRef: null, + parentItemId: null, + ordinal: 1, + status: "completed", + title: null, + startedAt: null, + completedAt: null, + updatedAt: {} as never, + type: "compaction", + driver: null, + beforeTokenCount: 10_000, + afterTokenCount: 2_000, + }, + }, ]); - - expect(snapshot).toMatchObject({ - usedTokens: 0, - maxTokens: 100_000, - remainingTokens: 100_000, - usedPercentage: 0, - remainingPercentage: 100, - }); + expect(snapshot?.usedTokens).toBe(2_000); + expect(snapshot?.totalProcessedTokens).toBe(10_000); }); - it("formats compact token counts", () => { - expect(formatContextWindowTokens(999)).toBe("999"); - expect(formatContextWindowTokens(1400)).toBe("1.4k"); - expect(formatContextWindowTokens(14_000)).toBe("14k"); - expect(formatContextWindowTokens(258_000)).toBe("258k"); - }); - - it("includes total processed tokens when available", () => { - const snapshot = deriveLatestContextWindowSnapshot([ - makeActivity("activity-1", "context-window.updated", { - usedTokens: 81_659, - totalProcessedTokens: 748_126, - maxTokens: 258_400, - lastUsedTokens: 81_659, - }), - ]); - - expect(snapshot?.usedTokens).toBe(81_659); - expect(snapshot?.totalProcessedTokens).toBe(748_126); + it("formats compact token values", () => { + expect(formatContextWindowTokens(1_500)).toBe("1.5k"); }); }); diff --git a/apps/web/src/lib/contextWindow.ts b/apps/web/src/lib/contextWindow.ts index 80f7d31cf2f..d7254211c82 100644 --- a/apps/web/src/lib/contextWindow.ts +++ b/apps/web/src/lib/contextWindow.ts @@ -1,17 +1,10 @@ -import type { OrchestrationThreadActivity, ThreadTokenUsageSnapshot } from "@t3tools/contracts"; - -function asRecord(value: unknown): Record | null { - return value && typeof value === "object" ? (value as Record) : null; -} +import type { ThreadTokenUsageSnapshot } from "@t3tools/contracts"; +import type { ThreadWorkEntry } from "@t3tools/client-runtime/state/shell"; function asFiniteNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } -function asBoolean(value: unknown): boolean | null { - return typeof value === "boolean" ? value : null; -} - type NullableContextWindowUsage = { readonly [Key in keyof ThreadTokenUsageSnapshot]: undefined extends ThreadTokenUsageSnapshot[Key] ? Exclude | null @@ -48,21 +41,20 @@ export function formatProviderDisplayName(provider: string | null | undefined): } export function deriveLatestContextWindowSnapshot( - activities: ReadonlyArray, + entries: ReadonlyArray, ): ContextWindowSnapshot | null { - for (let index = activities.length - 1; index >= 0; index -= 1) { - const activity = activities[index]; - if (!activity || activity.kind !== "context-window.updated") { + for (let index = entries.length - 1; index >= 0; index -= 1) { + const entry = entries[index]; + if (!entry || entry.structuredPayload.type !== "compaction") { continue; } - - const payload = asRecord(activity.payload); - const usedTokens = asFiniteNumber(payload?.usedTokens); + const payload = entry.structuredPayload; + const usedTokens = asFiniteNumber(payload.afterTokenCount); if (usedTokens === null || usedTokens < 0) { continue; } - const maxTokens = asFiniteNumber(payload?.maxTokens); + const maxTokens = null; const usedPercentage = maxTokens !== null && maxTokens > 0 ? Math.min(100, (usedTokens / maxTokens) * 100) : null; const remainingTokens = @@ -71,24 +63,24 @@ export function deriveLatestContextWindowSnapshot( return { usedTokens, - totalProcessedTokens: asFiniteNumber(payload?.totalProcessedTokens), + totalProcessedTokens: asFiniteNumber(payload.beforeTokenCount), maxTokens, remainingTokens, usedPercentage, remainingPercentage, - inputTokens: asFiniteNumber(payload?.inputTokens), - cachedInputTokens: asFiniteNumber(payload?.cachedInputTokens), - outputTokens: asFiniteNumber(payload?.outputTokens), - reasoningOutputTokens: asFiniteNumber(payload?.reasoningOutputTokens), - lastUsedTokens: asFiniteNumber(payload?.lastUsedTokens), - lastInputTokens: asFiniteNumber(payload?.lastInputTokens), - lastCachedInputTokens: asFiniteNumber(payload?.lastCachedInputTokens), - lastOutputTokens: asFiniteNumber(payload?.lastOutputTokens), - lastReasoningOutputTokens: asFiniteNumber(payload?.lastReasoningOutputTokens), - toolUses: asFiniteNumber(payload?.toolUses), - durationMs: asFiniteNumber(payload?.durationMs), - compactsAutomatically: asBoolean(payload?.compactsAutomatically) ?? false, - updatedAt: activity.createdAt, + inputTokens: null, + cachedInputTokens: null, + outputTokens: null, + reasoningOutputTokens: null, + lastUsedTokens: null, + lastInputTokens: null, + lastCachedInputTokens: null, + lastOutputTokens: null, + lastReasoningOutputTokens: null, + toolUses: null, + durationMs: null, + compactsAutomatically: true, + updatedAt: entry.createdAt, }; } diff --git a/apps/web/src/lib/orchestrationV2Timeline.test.ts b/apps/web/src/lib/orchestrationV2Timeline.test.ts new file mode 100644 index 00000000000..c94a3816a8a --- /dev/null +++ b/apps/web/src/lib/orchestrationV2Timeline.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + removeAndRenumberTimelineItem, + upsertTimelineItemAtStablePosition, +} from "./orchestrationV2Timeline"; + +describe("removeAndRenumberTimelineItem", () => { + it("closes the position gap before a streamed item is reinserted", () => { + const remaining = removeAndRenumberTimelineItem( + [ + { position: 0, sourceItemId: "first" }, + { position: 1, sourceItemId: "streaming" }, + { position: 2, sourceItemId: "last" }, + ], + "streaming", + ); + + expect(remaining).toEqual([ + { position: 0, sourceItemId: "first" }, + { position: 1, sourceItemId: "last" }, + ]); + expect(new Set(remaining.map((row) => row.position)).size).toBe(remaining.length); + }); +}); + +describe("upsertTimelineItemAtStablePosition", () => { + it("replaces a streaming item without moving it behind newer items", () => { + const updated = upsertTimelineItemAtStablePosition( + [ + { position: 0, sourceItemId: "reasoning", text: "partial" }, + { position: 1, sourceItemId: "subagent", text: "running" }, + ], + { position: 2, sourceItemId: "reasoning", text: "completed" }, + ); + + expect(updated).toEqual([ + { position: 0, sourceItemId: "reasoning", text: "completed" }, + { position: 1, sourceItemId: "subagent", text: "running" }, + ]); + }); + + it("appends a genuinely new item at the next position", () => { + const updated = upsertTimelineItemAtStablePosition( + [{ position: 0, sourceItemId: "reasoning" }], + { position: 99, sourceItemId: "assistant" }, + ); + + expect(updated).toEqual([ + { position: 0, sourceItemId: "reasoning" }, + { position: 1, sourceItemId: "assistant" }, + ]); + }); +}); diff --git a/apps/web/src/lib/orchestrationV2Timeline.ts b/apps/web/src/lib/orchestrationV2Timeline.ts new file mode 100644 index 00000000000..8be8b77b8c9 --- /dev/null +++ b/apps/web/src/lib/orchestrationV2Timeline.ts @@ -0,0 +1,22 @@ +export function removeAndRenumberTimelineItem< + Id, + Row extends { readonly position: number; readonly sourceItemId: Id }, +>(rows: ReadonlyArray, sourceItemId: Id): Array { + return rows + .filter((row) => row.sourceItemId !== sourceItemId) + .map((row, position) => ({ ...row, position })); +} + +export function upsertTimelineItemAtStablePosition< + Id, + Row extends { readonly position: number; readonly sourceItemId: Id }, +>(rows: ReadonlyArray, item: Row): Array { + const existingIndex = rows.findIndex((row) => row.sourceItemId === item.sourceItemId); + if (existingIndex === -1) { + return [...rows, { ...item, position: rows.length }]; + } + + return rows.map((row, index) => + index === existingIndex ? { ...item, position: row.position } : row, + ); +} diff --git a/apps/web/src/lib/threadSort.test.ts b/apps/web/src/lib/threadSort.test.ts index b9981bc2e3e..2ae3c0c5240 100644 --- a/apps/web/src/lib/threadSort.test.ts +++ b/apps/web/src/lib/threadSort.test.ts @@ -7,13 +7,14 @@ import { ThreadId, } from "@t3tools/contracts"; import type { Thread } from "../types"; +import { makeThreadFixture } from "../test-fixtures"; import { getLatestThreadForProject, sortThreads } from "./threadSort"; const LOCAL_ENVIRONMENT_ID = EnvironmentId.make("environment-local"); const PROJECT_ID = ProjectId.make("project-1"); function makeThread(overrides: Partial = {}): Thread { - return { + return makeThreadFixture({ id: ThreadId.make("thread-1"), environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, @@ -21,20 +22,18 @@ function makeThread(overrides: Partial = {}): Thread { modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: DEFAULT_RUNTIME_MODE, interactionMode: "default", - session: null, + runtime: null, messages: [], proposedPlans: [], createdAt: "2026-03-09T10:00:00.000Z", archivedAt: null, deletedAt: null, updatedAt: "2026-03-09T10:00:00.000Z", - latestTurn: null, + latestRun: null, branch: null, worktreePath: null, - checkpoints: [], - activities: [], ...overrides, - }; + }); } describe("sortThreads", () => { @@ -49,7 +48,7 @@ describe("sortThreads", () => { id: "message-1" as never, role: "user", text: "older", - turnId: null, + runId: null, createdAt: "2026-03-09T10:01:00.000Z", updatedAt: "2026-03-09T10:01:00.000Z", streaming: false, @@ -65,7 +64,7 @@ describe("sortThreads", () => { id: "message-2" as never, role: "user", text: "newer", - turnId: null, + runId: null, createdAt: "2026-03-09T10:06:00.000Z", updatedAt: "2026-03-09T10:06:00.000Z", streaming: false, @@ -93,7 +92,7 @@ describe("sortThreads", () => { id: "message-1" as never, role: "assistant", text: "assistant only", - turnId: null, + runId: null, createdAt: "2026-03-09T10:02:00.000Z", updatedAt: "2026-03-09T10:02:00.000Z", streaming: false, diff --git a/apps/web/src/orchestrationV2DebugProviders.test.ts b/apps/web/src/orchestrationV2DebugProviders.test.ts new file mode 100644 index 00000000000..0df7d258b00 --- /dev/null +++ b/apps/web/src/orchestrationV2DebugProviders.test.ts @@ -0,0 +1,82 @@ +import { + ProviderDriverKind, + ProviderInstanceId, + type ProviderInstanceConfigMap, + type ServerProvider, +} from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { deriveOrchestrationV2DebugProviderSnapshots } from "./orchestrationV2DebugProviders"; + +const acpRegistry = ProviderDriverKind.make("acpRegistry"); +const instanceId = ProviderInstanceId.make("acpRegistry_example"); + +const unavailable: ServerProvider = { + instanceId, + driver: acpRegistry, + displayName: "Example ACP", + enabled: false, + installed: false, + version: null, + status: "error", + auth: { status: "unknown" }, + checkedAt: "2026-01-01T00:00:00.000Z", + availability: "unavailable", + unavailableReason: "Driver is not registered in the V1 runtime.", + models: [], + slashCommands: [], + skills: [], +}; + +describe("deriveOrchestrationV2DebugProviderSnapshots", () => { + it("replaces the V1 unavailable shadow for a configured registry agent", () => { + const providerInstances = { + [instanceId]: { + driver: acpRegistry, + enabled: true, + displayName: "Example ACP", + config: { + agentId: "example-agent", + customModels: ["model-one"], + }, + }, + } satisfies ProviderInstanceConfigMap; + + expect( + deriveOrchestrationV2DebugProviderSnapshots({ + providers: [unavailable], + providerInstances, + }), + ).toEqual([ + expect.objectContaining({ + instanceId, + driver: acpRegistry, + displayName: "Example ACP", + enabled: true, + installed: true, + status: "ready", + availability: "available", + models: [ + expect.objectContaining({ slug: "default", isCustom: false }), + expect.objectContaining({ slug: "model-one", isCustom: true }), + ], + }), + ]); + }); + + it("does not make an incomplete registry instance selectable", () => { + const providerInstances = { + [instanceId]: { + driver: acpRegistry, + config: { agentId: "" }, + }, + } satisfies ProviderInstanceConfigMap; + + expect( + deriveOrchestrationV2DebugProviderSnapshots({ + providers: [unavailable], + providerInstances, + }), + ).toEqual([unavailable]); + }); +}); diff --git a/apps/web/src/orchestrationV2DebugProviders.ts b/apps/web/src/orchestrationV2DebugProviders.ts new file mode 100644 index 00000000000..36994447dd9 --- /dev/null +++ b/apps/web/src/orchestrationV2DebugProviders.ts @@ -0,0 +1,88 @@ +import { + ProviderDriverKind, + type ProviderInstanceConfigMap, + type ProviderInstanceId, + type ServerProvider, + type ServerProviderModel, +} from "@t3tools/contracts"; + +const ACP_REGISTRY_DRIVER = ProviderDriverKind.make("acpRegistry"); +const DEBUG_CHECKED_AT = "1970-01-01T00:00:00.000Z"; + +function configRecord(config: unknown): Readonly> { + return config !== null && typeof config === "object" && !Array.isArray(config) + ? (config as Readonly>) + : {}; +} + +function configuredModels( + config: Readonly>, +): ReadonlyArray { + const customModels = Array.isArray(config.customModels) + ? config.customModels.filter( + (model): model is string => typeof model === "string" && model.trim().length > 0, + ) + : []; + const slugs = [...new Set(["default", ...customModels.map((model) => model.trim())])]; + return slugs.map((slug) => ({ + slug, + name: slug === "default" ? "Agent default" : slug, + shortName: slug === "default" ? "Default" : slug, + isCustom: slug !== "default", + capabilities: null, + })); +} + +/** + * Add V2-only ACP Registry instances to the orchestration debugger's provider catalog. + * The regular application provider catalog remains V1-owned and is intentionally untouched. + */ +export function deriveOrchestrationV2DebugProviderSnapshots(input: { + readonly providers: ReadonlyArray; + readonly providerInstances: ProviderInstanceConfigMap; +}): ReadonlyArray { + const snapshots = [...input.providers]; + const indexByInstanceId = new Map( + snapshots.map((provider, index) => [provider.instanceId, index]), + ); + + for (const [instanceId, instance] of Object.entries(input.providerInstances)) { + if (instance.driver !== ACP_REGISTRY_DRIVER) continue; + const config = configRecord(instance.config); + const agentId = typeof config.agentId === "string" ? config.agentId.trim() : ""; + if (agentId.length === 0) continue; + const id = instanceId as ProviderInstanceId; + const existingIndex = indexByInstanceId.get(id); + const existing = existingIndex === undefined ? undefined : snapshots[existingIndex]; + const enabled = + instance.enabled ?? (typeof config.enabled === "boolean" ? config.enabled : true); + const snapshot: ServerProvider = { + instanceId: id, + driver: ACP_REGISTRY_DRIVER, + displayName: instance.displayName?.trim() || `ACP: ${agentId}`, + ...(instance.accentColor ? { accentColor: instance.accentColor } : {}), + badgeLabel: "V2 Preview", + showInteractionModeToggle: true, + requiresNewThreadForModelChange: false, + enabled, + installed: true, + version: null, + status: enabled ? "ready" : "disabled", + auth: { status: "unknown" }, + checkedAt: existing?.checkedAt ?? DEBUG_CHECKED_AT, + message: `ACP Registry agent '${agentId}' is resolved when the V2 session starts.`, + availability: "available", + models: configuredModels(config), + slashCommands: [], + skills: [], + }; + if (existingIndex === undefined) { + indexByInstanceId.set(id, snapshots.length); + snapshots.push(snapshot); + } else { + snapshots[existingIndex] = snapshot; + } + } + + return snapshots; +} diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 3a9140e278c..017ba73ff9a 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -20,6 +20,7 @@ import { Route as SettingsGeneralRouteImport } from './routes/settings.general' import { Route as SettingsDiagnosticsRouteImport } from './routes/settings.diagnostics' import { Route as SettingsConnectionsRouteImport } from './routes/settings.connections' import { Route as SettingsArchivedRouteImport } from './routes/settings.archived' +import { Route as DebugOrchestrationV2RouteImport } from './routes/debug.orchestration-v2' import { Route as ChatDraftDraftIdRouteImport } from './routes/_chat.draft.$draftId' import { Route as ChatEnvironmentIdThreadIdRouteImport } from './routes/_chat.$environmentId.$threadId' @@ -77,6 +78,11 @@ const SettingsArchivedRoute = SettingsArchivedRouteImport.update({ path: '/archived', getParentRoute: () => SettingsRoute, } as any) +const DebugOrchestrationV2Route = DebugOrchestrationV2RouteImport.update({ + id: '/debug/orchestration-v2', + path: '/debug/orchestration-v2', + getParentRoute: () => rootRouteImport, +} as any) const ChatDraftDraftIdRoute = ChatDraftDraftIdRouteImport.update({ id: '/draft/$draftId', path: '/draft/$draftId', @@ -93,6 +99,7 @@ export interface FileRoutesByFullPath { '/': typeof ChatIndexRoute '/pair': typeof PairRoute '/settings': typeof SettingsRouteWithChildren + '/debug/orchestration-v2': typeof DebugOrchestrationV2Route '/settings/archived': typeof SettingsArchivedRoute '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute @@ -106,6 +113,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/pair': typeof PairRoute '/settings': typeof SettingsRouteWithChildren + '/debug/orchestration-v2': typeof DebugOrchestrationV2Route '/settings/archived': typeof SettingsArchivedRoute '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute @@ -122,6 +130,7 @@ export interface FileRoutesById { '/_chat': typeof ChatRouteWithChildren '/pair': typeof PairRoute '/settings': typeof SettingsRouteWithChildren + '/debug/orchestration-v2': typeof DebugOrchestrationV2Route '/settings/archived': typeof SettingsArchivedRoute '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute @@ -139,6 +148,7 @@ export interface FileRouteTypes { | '/' | '/pair' | '/settings' + | '/debug/orchestration-v2' | '/settings/archived' | '/settings/connections' | '/settings/diagnostics' @@ -152,6 +162,7 @@ export interface FileRouteTypes { to: | '/pair' | '/settings' + | '/debug/orchestration-v2' | '/settings/archived' | '/settings/connections' | '/settings/diagnostics' @@ -167,6 +178,7 @@ export interface FileRouteTypes { | '/_chat' | '/pair' | '/settings' + | '/debug/orchestration-v2' | '/settings/archived' | '/settings/connections' | '/settings/diagnostics' @@ -183,6 +195,7 @@ export interface RootRouteChildren { ChatRoute: typeof ChatRouteWithChildren PairRoute: typeof PairRoute SettingsRoute: typeof SettingsRouteWithChildren + DebugOrchestrationV2Route: typeof DebugOrchestrationV2Route } declare module '@tanstack/react-router' { @@ -264,6 +277,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsArchivedRouteImport parentRoute: typeof SettingsRoute } + '/debug/orchestration-v2': { + id: '/debug/orchestration-v2' + path: '/debug/orchestration-v2' + fullPath: '/debug/orchestration-v2' + preLoaderRoute: typeof DebugOrchestrationV2RouteImport + parentRoute: typeof rootRouteImport + } '/_chat/draft/$draftId': { id: '/_chat/draft/$draftId' path: '/draft/$draftId' @@ -323,6 +343,7 @@ const rootRouteChildren: RootRouteChildren = { ChatRoute: ChatRouteWithChildren, PairRoute: PairRoute, SettingsRoute: SettingsRouteWithChildren, + DebugOrchestrationV2Route: DebugOrchestrationV2Route, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/apps/web/src/routes/debug.orchestration-v2.tsx b/apps/web/src/routes/debug.orchestration-v2.tsx new file mode 100644 index 00000000000..7a923e33e90 --- /dev/null +++ b/apps/web/src/routes/debug.orchestration-v2.tsx @@ -0,0 +1,3891 @@ +import { useAtomValue } from "@effect/atom-react"; +import { applyOrchestrationV2ProjectionEvent } from "@t3tools/client-runtime/state/orchestration-v2-projection"; +import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; +import { + ProviderDriverKind, + ProviderInstanceId, + type CheckpointId, + type CheckpointScopeId, + type ModelSelection, + type OrchestrationV2Checkpoint, + type OrchestrationV2Command, + type OrchestrationV2PlanStep, + type OrchestrationV2Run, + type OrchestrationV2RunStatus, + type OrchestrationV2ThreadProjection, + type OrchestrationV2ThreadShell, + type OrchestrationV2ThreadStreamItem, + type OrchestrationV2TurnItem, + type OrchestrationV2TurnItemStatus, + type OrchestrationV2UserInputQuestion, + type RunId, + type RuntimeMode, + type ServerProvider, + type ThreadId, +} from "@t3tools/contracts"; +import { createModelSelection } from "@t3tools/shared/model"; +import { createFileRoute } from "@tanstack/react-router"; +import { GitMergeIcon } from "lucide-react"; +import type { CSSProperties, DragEvent, ReactNode } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { ProviderModelPicker } from "../components/chat/ProviderModelPicker"; +import { usePrimarySettings } from "../hooks/useSettings"; +import { newCommandId, newMessageId, newProjectId, newThreadId } from "../lib/utils"; +import { type AppModelOption, getAppModelOptionsForInstance } from "../modelSelection"; +import { deriveOrchestrationV2DebugProviderSnapshots } from "../orchestrationV2DebugProviders"; +import { + type ProviderInstanceEntry, + deriveProviderInstanceEntries, + sortProviderInstanceEntries, +} from "../providerInstances"; +import { usePrimaryEnvironmentId } from "../state/environments"; +import { orchestrationEnvironment } from "../state/orchestration"; +import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server"; +import { useAtomCommand } from "../state/use-atom-command"; +import { useAtomQueryRunner } from "../state/use-atom-query-runner"; +import { useEnvironmentQuery } from "../state/query"; + +export const Route = createFileRoute("/debug/orchestration-v2")({ + component: OrchestrationV2DebugRoute, +}); + +const DEFAULT_PROMPT = "Respond with the following text: fixture simple ok"; +const DEBUG_CODEX_DRIVER = ProviderDriverKind.make("codex"); +const DEBUG_CLAUDE_DRIVER = ProviderDriverKind.make("claudeAgent"); +const DEBUG_CODEX_INSTANCE_ID = ProviderInstanceId.make("codex"); +const DEBUG_CLAUDE_INSTANCE_ID = ProviderInstanceId.make("claudeAgent"); +const DEFAULT_MODEL_SELECTION = createModelSelection(DEBUG_CODEX_INSTANCE_ID, "gpt-5.4"); + +const DEBUG_PROVIDER_SNAPSHOTS: ReadonlyArray = [ + { + instanceId: DEBUG_CODEX_INSTANCE_ID, + driver: DEBUG_CODEX_DRIVER, + displayName: "Codex", + enabled: true, + installed: true, + version: null, + status: "ready", + auth: { status: "authenticated" }, + checkedAt: "2026-01-01T00:00:00.000Z", + models: [ + { + slug: "gpt-5.5", + name: "GPT-5.5", + shortName: "5.5", + isCustom: false, + capabilities: null, + }, + { + slug: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + shortName: "5.3", + isCustom: false, + capabilities: null, + }, + { + slug: "gpt-5.4", + name: "GPT-5.4", + shortName: "5.4", + isCustom: false, + capabilities: null, + }, + { + slug: "gpt-5.4-mini", + name: "GPT-5.4 Mini", + shortName: "5.4 Mini", + isCustom: false, + capabilities: null, + }, + { + slug: "gpt-5.3-codex-spark", + name: "GPT-5.3 Codex Spark", + shortName: "Spark", + isCustom: false, + capabilities: null, + }, + { + slug: "gpt-5.2", + name: "GPT-5.2", + shortName: "5.2", + isCustom: false, + capabilities: null, + }, + ], + slashCommands: [], + skills: [], + }, + { + instanceId: DEBUG_CLAUDE_INSTANCE_ID, + driver: DEBUG_CLAUDE_DRIVER, + displayName: "Claude", + enabled: true, + installed: true, + version: null, + status: "ready", + auth: { status: "authenticated" }, + checkedAt: "2026-01-01T00:00:00.000Z", + models: [ + { + slug: "claude-opus-4-7", + name: "Claude Opus 4.7", + shortName: "Opus 4.7", + isCustom: false, + capabilities: null, + }, + { + slug: "claude-opus-4-6", + name: "Claude Opus 4.6", + shortName: "Opus 4.6", + isCustom: false, + capabilities: null, + }, + { + slug: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + shortName: "Sonnet 4.6", + isCustom: false, + capabilities: null, + }, + { + slug: "claude-haiku-4-5", + name: "Claude Haiku 4.5", + shortName: "Haiku 4.5", + isCustom: false, + capabilities: null, + }, + ], + slashCommands: [], + skills: [], + }, +]; + +type LogEntry = + | { + readonly type: "command"; + readonly label: string; + readonly value: unknown; + } + | { + readonly type: "stream"; + readonly value: OrchestrationV2ThreadStreamItem; + } + | { + readonly type: "error"; + readonly message: string; + }; + +interface TimelineEntry { + readonly key: string; + readonly eyebrow: string; + readonly title: string; + readonly subtitle?: string | undefined; + readonly status?: string | undefined; + readonly body?: string | undefined; + readonly timestamp?: string | undefined; + readonly sequence?: number | undefined; + readonly raw: unknown; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function formatErrorMessage(error: unknown): string { + if (isRecord(error) && typeof error.detail === "string" && error.detail.trim().length > 0) { + return error.detail; + } + if (error instanceof Error && error.message.trim().length > 0) { + return error.message; + } + return String(error); +} + +function formatLabel(value: string): string { + return value + .split(/[._-]+/g) + .filter(Boolean) + .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`) + .join(" "); +} + +function compactId(value: unknown): string | undefined { + if (typeof value !== "string" || value.length === 0) { + return undefined; + } + if (value.length <= 18) { + return value; + } + return `${value.slice(0, 10)}...${value.slice(-6)}`; +} + +function stringifyShort(value: unknown): string | undefined { + if (typeof value === "string") { + return value; + } + if (value === null || value === undefined) { + return undefined; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function formatTimestamp(value: unknown): string | undefined { + if (value === null || value === undefined) { + return undefined; + } + if (value instanceof Date) { + return Number.isNaN(value.getTime()) ? undefined : value.toISOString(); + } + const raw = typeof value === "string" ? value : String(value); + const wrapped = /^DateTime\.(?:Utc|Zoned|Local)\((.+)\)$/.exec(raw); + const iso = wrapped?.[1] ?? raw; + const parsed = new Date(iso); + return Number.isNaN(parsed.getTime()) ? iso : parsed.toISOString(); +} + +function formatRelative(fromMs: number, nowMs: number): string { + const diff = nowMs - fromMs; + const abs = Math.abs(diff); + if (abs < 1000) return "just now"; + const future = diff < 0; + const sec = Math.round(abs / 1000); + if (sec < 60) return future ? `in ${sec}s` : `${sec}s ago`; + const min = Math.round(sec / 60); + if (min < 60) return future ? `in ${min}m` : `${min}m ago`; + const hr = Math.round(min / 60); + if (hr < 24) return future ? `in ${hr}h` : `${hr}h ago`; + const day = Math.round(hr / 24); + return future ? `in ${day}d` : `${day}d ago`; +} + +function useNow(intervalMs = 10_000): number { + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + const id = setInterval(() => { + setNow(Date.now()); + }, intervalMs); + return () => { + clearInterval(id); + }; + }, [intervalMs]); + return now; +} + +function selectDebugFallbackProviderInstance( + entries: ReadonlyArray, +): ProviderInstanceEntry | undefined { + return ( + entries.find((entry) => entry.enabled && entry.isAvailable && entry.status === "ready") ?? + entries.find((entry) => entry.enabled && entry.isAvailable) ?? + entries[0] + ); +} + +function resolveDebugModelSelection( + current: ModelSelection, + entries: ReadonlyArray, + modelOptionsByInstance: ReadonlyMap>, +): ModelSelection { + const currentEntry = entries.find((entry) => entry.instanceId === current.instanceId); + const entry = currentEntry ?? selectDebugFallbackProviderInstance(entries); + if (!entry) return current; + + const modelOptions = modelOptionsByInstance.get(entry.instanceId) ?? []; + const hasCurrentModel = + currentEntry !== undefined && modelOptions.some((option) => option.slug === current.model); + if (currentEntry !== undefined && (modelOptions.length === 0 || hasCurrentModel)) { + return current; + } + + const model = modelOptions[0]?.slug ?? current.model; + return createModelSelection(entry.instanceId, model); +} + +const IS_MAC = + typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent); + +function Timestamp(props: { readonly iso: string | undefined; readonly nowMs: number }) { + if (props.iso === undefined) return null; + const parsed = new Date(props.iso); + if (Number.isNaN(parsed.getTime())) { + return ( + + {props.iso} + + ); + } + const clock = parsed.toLocaleTimeString(undefined, { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + return ( + + ); +} + +function readText( + record: Record, + keys: ReadonlyArray, +): string | undefined { + for (const key of keys) { + const value = record[key]; + if (typeof value === "string" && value.trim().length > 0) { + return value; + } + } + return undefined; +} + +function buildProjectionTimeline( + projection: OrchestrationV2ThreadProjection | null, +): ReadonlyArray { + if (projection === null) { + return []; + } + + const entries: Array = [ + { + key: `thread:${projection.thread.id}`, + eyebrow: "Thread", + title: projection.thread.title, + subtitle: `${projection.thread.modelSelection.instanceId} / ${projection.thread.modelSelection.model}`, + status: projection.thread.archivedAt ? "archived" : "active", + timestamp: formatTimestamp(projection.thread.createdAt), + raw: projection.thread, + sort: 0, + }, + ]; + + projection.messages.forEach((message, index) => { + entries.push({ + key: `message:${message.id}`, + eyebrow: "Message", + title: formatLabel(message.role), + subtitle: compactId(message.id), + status: message.streaming ? "streaming" : "completed", + body: message.text, + timestamp: formatTimestamp(message.createdAt), + raw: message, + sort: 100 + index, + }); + }); + + projection.runs.forEach((run, index) => { + entries.push({ + key: `run:${run.id}`, + eyebrow: "Run", + title: `Run ${run.ordinal}`, + subtitle: `${run.modelSelection.instanceId} / ${run.modelSelection.model}`, + status: run.status, + timestamp: formatTimestamp(run.startedAt ?? run.requestedAt), + raw: run, + sort: 200 + index * 100, + }); + }); + + projection.turnItems.forEach((item) => { + const record = item as unknown as Record; + const body = + readText(record, ["text", "markdown", "detail", "explanation", "output"]) ?? + stringifyShort(record.input); + entries.push({ + key: `turn-item:${item.id}`, + eyebrow: "Turn Item", + title: formatLabel(item.type), + subtitle: compactId(item.providerTurnId), + status: item.status, + body, + timestamp: formatTimestamp(item.updatedAt ?? item.startedAt), + raw: item, + sort: 250 + item.ordinal, + }); + }); + + projection.runtimeRequests.forEach((request, index) => { + entries.push({ + key: `request:${request.id}`, + eyebrow: "Request", + title: formatLabel(request.kind), + subtitle: compactId(request.providerTurnId), + status: request.status, + timestamp: formatTimestamp(request.createdAt), + raw: request, + sort: 500 + index, + }); + }); + + projection.contextTransfers.forEach((transfer, index) => { + const resolution = + transfer.resolution === null + ? undefined + : `resolved by ${formatLabel(transfer.resolution.strategy)}`; + entries.push({ + key: `context-transfer:${transfer.id}`, + eyebrow: "Context Transfer", + title: formatLabel(transfer.type), + subtitle: compactId(transfer.targetThreadId), + status: transfer.status, + body: [resolution, transfer.error].filter(Boolean).join("\n") || undefined, + timestamp: formatTimestamp(transfer.updatedAt ?? transfer.createdAt), + raw: transfer, + sort: 650 + index, + }); + }); + + projection.checkpoints.forEach((checkpoint, index) => { + entries.push({ + key: `checkpoint:${checkpoint.id}`, + eyebrow: "Checkpoint", + title: compactId(checkpoint.ref) ?? compactId(checkpoint.id) ?? "Checkpoint", + subtitle: compactId(checkpoint.scopeId), + status: checkpoint.status, + timestamp: formatTimestamp(checkpoint.capturedAt), + raw: checkpoint, + sort: 700 + index, + }); + }); + + return entries.toSorted((left, right) => left.sort - right.sort); +} + +function buildStreamTimeline(logEntries: ReadonlyArray): ReadonlyArray { + return logEntries.map((entry, index) => { + if (entry.type === "error") { + return { + key: `log:${index}`, + eyebrow: "Error", + title: "Error", + status: "failed", + body: entry.message, + raw: entry, + }; + } + + if (entry.type === "command") { + return { + key: `log:${index}`, + eyebrow: "Command", + title: entry.label, + status: "sent", + raw: entry.value, + }; + } + + const value = entry.value; + if (value.kind === "snapshot") { + return { + key: `log:${index}`, + eyebrow: "Snapshot", + title: "Projection Snapshot", + subtitle: `sequence ${value.snapshotSequence}`, + sequence: value.snapshotSequence, + status: "received", + raw: value, + }; + } + + const event = value.event; + const payload: Record = isRecord(event.payload) ? event.payload : {}; + return { + key: `log:${index}`, + eyebrow: "Event", + title: event.type, + subtitle: compactId(event.threadId), + sequence: value.sequence, + status: stringifyShort(payload.status) ?? stringifyShort(event.driver) ?? "received", + body: readText(payload, ["text", "title", "detail", "markdown"]), + raw: value, + }; + }); +} + +function turnItemBody(item: OrchestrationV2TurnItem): string | undefined { + switch (item.type) { + case "user_message": + case "assistant_message": + case "reasoning": + return item.text; + case "proposed_plan": + return item.markdown; + case "todo_list": { + const steps = item.steps.map((step) => `${step.status}: ${step.text}`).join("\n"); + return [item.explanation, steps].filter(Boolean).join("\n\n") || undefined; + } + case "user_input_request": + return item.questions.map((question) => question.question).join("\n"); + case "file_change": + return [ + item.fileName, + item.additions === undefined && item.deletions === undefined + ? undefined + : `+${item.additions ?? 0} / -${item.deletions ?? 0}`, + item.diffStr, + ] + .filter(Boolean) + .join("\n"); + case "command_execution": + return [item.input, item.output].filter(Boolean).join("\n\n"); + case "file_search": + return [ + item.pattern, + item.results + ?.map((result) => + [result.fileName, result.line === undefined ? undefined : `:${result.line}`].join(""), + ) + .join("\n"), + ] + .filter(Boolean) + .join("\n"); + case "web_search": + return [ + item.patterns?.join(", "), + item.results?.map((result) => result.title ?? result.url ?? result.snippet).join("\n"), + ] + .filter(Boolean) + .join("\n"); + case "approval_request": + return item.prompt ?? formatLabel(item.requestKind); + case "checkpoint": + return item.files + .map((file) => `${file.path} +${file.additions} / -${file.deletions}`) + .join("\n"); + case "run_interrupt_request": + case "run_interrupt_result": + return item.message; + case "compaction": + return item.summary; + case "handoff": + return item.summary; + case "fork": + return `Target thread: ${compactId(item.targetThreadId) ?? item.targetThreadId}`; + case "subagent": + return [item.prompt, item.result].filter(Boolean).join("\n\n"); + case "dynamic_tool": + return [item.toolName, stringifyShort(item.output) ?? stringifyShort(item.input)] + .filter(Boolean) + .join("\n"); + } +} + +interface ItemTimelineItemRow { + readonly kind: "item"; + readonly item: OrchestrationV2TurnItem; + readonly entry: TimelineEntry; + readonly inheritedFromThreadId?: ThreadId | undefined; +} + +interface ItemTimelineForkMarkerRow { + readonly kind: "fork-marker"; + readonly entry: TimelineEntry; + readonly sourceThreadId: ThreadId; + readonly targetThreadId: ThreadId; +} + +type ItemTimelineRow = ItemTimelineItemRow | ItemTimelineForkMarkerRow; + +interface QueuedRunRow { + readonly run: OrchestrationV2Run; + readonly messageText: string; +} + +interface MergeBackCandidate { + readonly sourceThreadId: ThreadId; + readonly targetThreadId: ThreadId; + readonly latestCompletedRun: OrchestrationV2Run | null; +} + +type PendingMergeBackTransfer = OrchestrationV2ThreadProjection["contextTransfers"][number]; + +interface DebugThreadTreeNode { + readonly threadId: ThreadId; + readonly thread: OrchestrationV2ThreadShell; + readonly modelSelection: ModelSelection; + readonly children: ReadonlyArray; +} + +function buildItemTimeline(input: { + readonly projection: OrchestrationV2ThreadProjection | null; + readonly projectionsByThread: ReadonlyMap; + readonly logEntries: ReadonlyArray; +}): ReadonlyArray { + if (input.projection !== null && input.projection.visibleTurnItems.length > 0) { + return input.projection.visibleTurnItems.map((row): ItemTimelineRow => { + const item = row.item; + if (item.type === "fork" && row.visibility === "synthetic") { + return { + kind: "fork-marker", + sourceThreadId: row.sourceThreadId, + targetThreadId: item.targetThreadId, + entry: itemTimelineEntry(item, `visible:${input.projection?.thread.id}:${row.position}`), + }; + } + return { + kind: "item", + item, + inheritedFromThreadId: row.visibility === "inherited" ? row.sourceThreadId : undefined, + entry: itemTimelineEntry(item, `visible:${input.projection?.thread.id}:${row.position}`), + }; + }); + } + + const items = new Map< + string, + { + readonly item: OrchestrationV2TurnItem; + readonly sequence?: number | undefined; + } + >(); + + const upsert = (item: OrchestrationV2TurnItem, sequence?: number) => { + const existing = items.get(item.id); + items.set(item.id, { + item, + ...(sequence === undefined + ? existing?.sequence === undefined + ? {} + : { sequence: existing.sequence } + : { sequence }), + }); + }; + + for (const entry of input.logEntries) { + if (entry.type !== "stream") { + continue; + } + const value = entry.value; + if (value.kind === "snapshot") { + for (const item of value.projection.turnItems) { + upsert(item, value.snapshotSequence); + } + continue; + } + if (value.event.type === "turn-item.updated") { + upsert(value.event.payload, value.sequence); + } + } + + if (input.projection !== null) { + for (const item of input.projection.turnItems) { + upsert(item); + } + } + + const currentRows: ReadonlyArray = [...items.values()] + .toSorted((left, right) => left.item.ordinal - right.item.ordinal) + .map(({ item, sequence }) => ({ + kind: "item", + item, + entry: itemTimelineEntry(item, `item:${item.id}`, sequence), + })); + + const forkedFrom = input.projection?.thread.forkedFrom; + if (input.projection === null || forkedFrom?.type !== "run") { + return currentRows; + } + const targetProjection = input.projection; + + const sourceProjection = input.projectionsByThread.get(forkedFrom.threadId); + if (sourceProjection === undefined) { + return currentRows; + } + + const sourceRun = sourceProjection.runs.find((run) => run.id === forkedFrom.runId); + if (sourceRun === undefined) { + return currentRows; + } + + const runOrdinalById = new Map(sourceProjection.runs.map((run) => [run.id, run.ordinal])); + const inheritedRows = sourceProjection.turnItems + .filter((item) => { + if (item.runId === null) return false; + const ordinal = runOrdinalById.get(item.runId); + return ordinal !== undefined && ordinal <= sourceRun.ordinal; + }) + .toSorted((left, right) => left.ordinal - right.ordinal) + .map( + (item): ItemTimelineItemRow => ({ + kind: "item", + item, + inheritedFromThreadId: sourceProjection.thread.id, + entry: itemTimelineEntry(item, `inherited:${targetProjection.thread.id}:${item.id}`), + }), + ); + + const marker: ItemTimelineForkMarkerRow = { + kind: "fork-marker", + sourceThreadId: sourceProjection.thread.id, + targetThreadId: targetProjection.thread.id, + entry: { + key: `fork-marker:${targetProjection.thread.id}`, + eyebrow: "Fork", + title: "Forked from conversation", + subtitle: compactId(sourceProjection.thread.id), + status: "received", + timestamp: formatTimestamp(targetProjection.thread.createdAt), + raw: { + sourceThreadId: sourceProjection.thread.id, + sourceRunId: sourceRun.id, + targetThreadId: targetProjection.thread.id, + }, + }, + }; + + return [...inheritedRows, marker, ...currentRows]; +} + +function itemTimelineEntry( + item: OrchestrationV2TurnItem, + key = `item:${item.id}`, + sequence?: number, +): TimelineEntry { + return { + key, + eyebrow: `Item ${item.ordinal}`, + title: item.title?.trim() || formatLabel(item.type), + subtitle: `${formatLabel(item.type)} · ${compactId(item.id) ?? item.id}`, + status: item.status, + body: turnItemBody(item), + timestamp: formatTimestamp(item.updatedAt ?? item.completedAt ?? item.startedAt), + sequence, + raw: item, + }; +} + +function forkSourceThreadId(thread: OrchestrationV2ThreadShell): ThreadId | null { + const forkedFrom = thread.forkedFrom; + if (forkedFrom?.type === "run") return forkedFrom.threadId; + return thread.lineage.parentThreadId ?? null; +} + +function threadShellCreatedMs(thread: OrchestrationV2ThreadShell): number { + const iso = formatTimestamp(thread.createdAt); + if (iso === undefined) return 0; + const parsed = new Date(iso).getTime(); + return Number.isNaN(parsed) ? 0 : parsed; +} + +function buildThreadTree(input: { + readonly threads: ReadonlyMap; + readonly projectionsByThread: ReadonlyMap; +}): ReadonlyArray { + const childIds = new Map>(); + const rootIds: Array = []; + for (const [threadId, thread] of input.threads) { + const parentThreadId = forkSourceThreadId(thread); + if (parentThreadId !== null && input.threads.has(parentThreadId)) { + const children = childIds.get(parentThreadId) ?? []; + children.push(threadId); + childIds.set(parentThreadId, children); + } else { + rootIds.push(threadId); + } + } + + const sortThreadIds = (threadIds: Array) => + threadIds.toSorted((left, right) => { + const leftThread = input.threads.get(left); + const rightThread = input.threads.get(right); + return ( + (leftThread === undefined ? 0 : threadShellCreatedMs(leftThread)) - + (rightThread === undefined ? 0 : threadShellCreatedMs(rightThread)) || + String(left).localeCompare(String(right)) + ); + }); + + const buildNode = (threadId: ThreadId): DebugThreadTreeNode => { + const thread = input.threads.get(threadId); + if (thread === undefined) { + throw new Error(`Missing orchestration V2 shell thread ${threadId}`); + } + const latestRun = input.projectionsByThread.get(threadId)?.runs.at(-1); + const children = sortThreadIds(childIds.get(threadId) ?? []).map(buildNode); + return { + threadId, + thread, + modelSelection: latestRun?.modelSelection ?? thread.modelSelection, + children, + }; + }; + + return sortThreadIds(rootIds).map(buildNode); +} + +const PANEL_KEYS = ["tree", "projection", "item", "stream"] as const; +type PanelKey = (typeof PANEL_KEYS)[number]; +const ALL_PANEL_KEYS: ReadonlyArray = PANEL_KEYS; +const DEFAULT_VISIBLE_PANELS: ReadonlyArray = ["tree", "item", "stream"]; + +const PANEL_TITLES: Record = { + tree: "Thread Tree", + projection: "Projection Timeline", + item: "Item Timeline", + stream: "Stream Timeline", +}; + +const PANEL_DND_MIME = "application/x-t3-panel-key"; + +function readDraggedPanelKey(event: DragEvent): PanelKey | null { + const raw = event.dataTransfer.getData(PANEL_DND_MIME); + return ALL_PANEL_KEYS.includes(raw as PanelKey) ? (raw as PanelKey) : null; +} + +function computePanelDropSide(event: DragEvent): "before" | "after" { + const rect = event.currentTarget.getBoundingClientRect(); + return event.clientX - rect.left < rect.width / 2 ? "before" : "after"; +} + +function DragHandleIcon(props: { readonly className?: string }) { + return ( + + ); +} + +function CloseIcon(props: { readonly className?: string }) { + return ( + + ); +} + +function ForkIcon(props: { readonly className?: string }) { + return ( + + ); +} + +function RollbackIcon(props: { readonly className?: string }) { + return ( + + ); +} + +function PanelHost(props: { + readonly visiblePanels: ReadonlyArray; + readonly hiddenPanels: ReadonlyArray; + readonly renderPanel: (key: PanelKey) => ReactNode; + readonly onReorder: (source: PanelKey, target: PanelKey, side: "before" | "after") => void; + readonly onRestoreAtEnd: (source: PanelKey) => void; +}) { + const visibleCount = props.visiblePanels.length; + const minWidthRem = Math.max(28, 28 * visibleCount); + const gridStyle: CSSProperties = { + gridTemplateColumns: + visibleCount === 0 ? "minmax(0,1fr)" : `repeat(${visibleCount}, minmax(0, 1fr))`, + minWidth: `${minWidthRem}rem`, + }; + + const [draggingOverEnd, setDraggingOverEnd] = useState(false); + + const handleContainerDragOver = (event: DragEvent) => { + if (!event.dataTransfer.types.includes(PANEL_DND_MIME)) return; + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + setDraggingOverEnd(true); + }; + + const handleContainerDragLeave = (event: DragEvent) => { + if (event.currentTarget === event.target) { + setDraggingOverEnd(false); + } + }; + + const handleContainerDrop = (event: DragEvent) => { + const sourceKey = readDraggedPanelKey(event); + setDraggingOverEnd(false); + if (sourceKey === null) return; + event.preventDefault(); + props.onRestoreAtEnd(sourceKey); + }; + + if (visibleCount === 0) { + return ( +

+

+ No panels visible. Click a pill in the header to restore it, or drag one here. +

+
+ ); + } + + return ( +
+
+ {props.visiblePanels.map((panelKey) => ( + + {props.renderPanel(panelKey)} + + ))} +
+
+ ); +} + +function PanelDropSlot(props: { + readonly panelKey: PanelKey; + readonly onReorder: (source: PanelKey, target: PanelKey, side: "before" | "after") => void; + readonly children: ReactNode; +}) { + const [dropSide, setDropSide] = useState<"before" | "after" | null>(null); + + const handleDragOver = (event: DragEvent) => { + if (!event.dataTransfer.types.includes(PANEL_DND_MIME)) return; + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = "move"; + setDropSide(computePanelDropSide(event)); + }; + + const handleDragLeave = () => { + setDropSide(null); + }; + + const handleDrop = (event: DragEvent) => { + const sourceKey = readDraggedPanelKey(event); + const side = dropSide ?? computePanelDropSide(event); + setDropSide(null); + if (sourceKey === null) return; + event.preventDefault(); + event.stopPropagation(); + if (sourceKey === props.panelKey) return; + props.onReorder(sourceKey, props.panelKey, side); + }; + + return ( +
+ {dropSide === "before" ? ( +
+ ); +} + +function HiddenPanelPills(props: { + readonly hiddenPanels: ReadonlyArray; + readonly onRestore: (key: PanelKey) => void; +}) { + if (props.hiddenPanels.length === 0) return null; + return ( +
    + {props.hiddenPanels.map((key) => ( +
  • + +
  • + ))} +
+ ); +} + +function HiddenPanelPill(props: { + readonly panelKey: PanelKey; + readonly onRestore: (key: PanelKey) => void; +}) { + const handleDragStart = (event: DragEvent) => { + event.dataTransfer.setData(PANEL_DND_MIME, props.panelKey); + event.dataTransfer.effectAllowed = "move"; + }; + return ( + + ); +} + +function countThreadTreeNodes(nodes: ReadonlyArray): number { + return nodes.reduce((count, node) => count + 1 + countThreadTreeNodes(node.children), 0); +} + +function ThreadTreePanel(props: { + readonly title: string; + readonly nodes: ReadonlyArray; + readonly activeThreadId: ThreadId | null; + readonly disabled: boolean; + readonly panelKey?: PanelKey; + readonly onClose?: () => void; + readonly onCreateThread: () => void; + readonly onOpenThread: (threadId: ThreadId) => void; +}) { + const count = countThreadTreeNodes(props.nodes); + return ( +
+ + + + {count} + + + } + /> + {count === 0 ? ( +
+ No threads yet. +
+ ) : ( +
+
    + {props.nodes.map((node) => ( + + ))} +
+
+ )} +
+ ); +} + +function ThreadTreeRow(props: { + readonly node: DebugThreadTreeNode; + readonly depth: number; + readonly activeThreadId: ThreadId | null; + readonly onOpenThread: (threadId: ThreadId) => void; +}) { + const thread = props.node.thread; + const active = props.node.threadId === props.activeThreadId; + const relationship = thread.lineage.relationshipToParent ?? "source"; + const instanceId = props.node.modelSelection.instanceId; + const model = props.node.modelSelection.model; + const itemCount = thread.visibleItemCount; + const createdAt = formatTimestamp(thread.createdAt); + + return ( +
  • 0 ? true : undefined} + className="relative" + > + {props.depth === 0 ? null : ( +
  • + ); +} + +function QueueControls(props: { + readonly rows: ReadonlyArray; + readonly activeTurn: ActiveTurn | null; + readonly disabled: boolean; + readonly onPromote: (runId: RunId) => void; + readonly onReorder: (runId: RunId, beforeRunId: RunId | null) => void; +}) { + if (props.rows.length === 0) { + return null; + } + + return ( +
    +
    +

    Queue

    + + {props.rows.length} + +
    +
      + {props.rows.map((row, index) => { + const previous = props.rows[index - 1]; + const afterNext = props.rows[index + 2]; + const canPromote = props.activeTurn !== null; + return ( +
    1. + + {row.run.queuePosition ?? row.run.ordinal} + +
      +

      {row.messageText}

      +

      + Run {row.run.ordinal} · {compactId(row.run.id)} +

      +
      +
      + { + if (previous !== undefined) { + props.onReorder(row.run.id, previous.run.id); + } + }} + > + ↑ + + { + props.onReorder(row.run.id, afterNext?.run.id ?? null); + }} + > + ↓ + + +
      +
    2. + ); + })} +
    +
    + ); +} + +function MergeBackControls(props: { + readonly candidate: MergeBackCandidate | null; + readonly disabled: boolean; + readonly onMergeBack: () => void; +}) { + if (props.candidate === null) { + return null; + } + + const canMerge = props.candidate.latestCompletedRun !== null; + if (!canMerge) return null; + return ( + + ); +} + +function PendingMergeBackNotice(props: { readonly transfer: PendingMergeBackTransfer | null }) { + if (props.transfer === null) { + return null; + } + + return ( +
    + +

    + Merge back pending from{" "} + + {compactId(props.transfer.sourceThreadId) ?? props.transfer.sourceThreadId} + + . Your next message will include that fork context. +

    +
    + ); +} + +function QueueActionButton(props: { + readonly label: string; + readonly disabled: boolean; + readonly onClick: () => void; + readonly children: ReactNode; +}) { + return ( + + ); +} + +function PanelHeader(props: { + readonly title: string; + readonly panelKey?: PanelKey | undefined; + readonly onClose?: (() => void) | undefined; + readonly trailing?: ReactNode; +}) { + const { panelKey, onClose } = props; + const handleDragStart = useCallback( + (event: DragEvent) => { + if (panelKey === undefined) return; + event.dataTransfer.setData(PANEL_DND_MIME, panelKey); + event.dataTransfer.effectAllowed = "move"; + }, + [panelKey], + ); + + return ( +
    + {panelKey === undefined ? null : ( + + )} +

    {props.title}

    +
    + {props.trailing} + {onClose ? ( + + ) : null} +
    +
    + ); +} + +function OrchestrationV2DebugRoute() { + const [prompt, setPrompt] = useState(DEFAULT_PROMPT); + const [modelSelection, setModelSelection] = useState(DEFAULT_MODEL_SELECTION); + const [threadId, setThreadId] = useState(null); + const [projection, setProjection] = useState(null); + const [projectionError, setProjectionError] = useState(null); + const [logEntries, setLogEntries] = useState>([]); + const [shellThreadsById, setShellThreadsById] = useState< + ReadonlyMap + >(() => new Map()); + const [projectionByThread, setProjectionByThread] = useState< + ReadonlyMap + >(() => new Map()); + const [isBusy, setIsBusy] = useState(false); + const [visiblePanels, setVisiblePanels] = + useState>(DEFAULT_VISIBLE_PANELS); + + const environmentId = usePrimaryEnvironmentId(); + const serverProviders = useAtomValue(primaryServerProvidersAtom); + const settings = usePrimarySettings(); + const keybindings = useAtomValue(primaryServerKeybindingsAtom); + const dispatchCommandMutation = useAtomCommand(orchestrationEnvironment.v2.dispatchCommand, { + reportFailure: false, + }); + const getThreadProjectionQuery = useAtomQueryRunner( + orchestrationEnvironment.v2.threadProjection, + { reportFailure: false }, + ); + const shellSubscription = useEnvironmentQuery( + environmentId === null ? null : orchestrationEnvironment.v2.shell({ environmentId, input: {} }), + ); + const threadSubscription = useEnvironmentQuery( + environmentId === null || threadId === null + ? null + : orchestrationEnvironment.v2.thread({ + environmentId, + input: { threadId }, + }), + ); + const providerSnapshots = useMemo( + () => + deriveOrchestrationV2DebugProviderSnapshots({ + providers: serverProviders.length > 0 ? serverProviders : DEBUG_PROVIDER_SNAPSHOTS, + providerInstances: settings.providerInstances, + }), + [serverProviders, settings.providerInstances], + ); + const providerInstanceEntries = useMemo>( + () => sortProviderInstanceEntries(deriveProviderInstanceEntries(providerSnapshots)), + [providerSnapshots], + ); + const modelOptionsByInstance = useMemo< + ReadonlyMap> + >(() => { + const out = new Map>(); + for (const entry of providerInstanceEntries) { + out.set(entry.instanceId, getAppModelOptionsForInstance(settings, entry)); + } + return out; + }, [providerInstanceEntries, settings]); + + useEffect(() => { + setModelSelection((current) => { + const next = resolveDebugModelSelection( + current, + providerInstanceEntries, + modelOptionsByInstance, + ); + return current.instanceId === next.instanceId && current.model === next.model + ? current + : next; + }); + }, [modelOptionsByInstance, providerInstanceEntries]); + + const hiddenPanels = useMemo( + () => ALL_PANEL_KEYS.filter((key) => !visiblePanels.includes(key)), + [visiblePanels], + ); + + const restorePanel = useCallback( + (key: PanelKey, targetKey?: PanelKey, side: "before" | "after" = "before") => { + setVisiblePanels((prev) => { + const without = prev.filter((k) => k !== key); + if (targetKey === undefined) return [...without, key]; + const targetIdx = without.indexOf(targetKey); + if (targetIdx === -1) return [...without, key]; + const insertIdx = side === "before" ? targetIdx : targetIdx + 1; + return [...without.slice(0, insertIdx), key, ...without.slice(insertIdx)]; + }); + }, + [], + ); + + const closePanel = useCallback((key: PanelKey) => { + setVisiblePanels((prev) => prev.filter((k) => k !== key)); + }, []); + + const dispatchCommand = useCallback( + async (command: OrchestrationV2Command) => { + if (environmentId === null) { + throw new Error("The primary environment is unavailable."); + } + const result = await dispatchCommandMutation({ environmentId, input: command }); + if (result._tag === "Failure") { + throw squashAtomCommandFailure(result); + } + return result.value; + }, + [dispatchCommandMutation, environmentId], + ); + const projectionTimeline = useMemo(() => buildProjectionTimeline(projection), [projection]); + const streamTimeline = useMemo(() => buildStreamTimeline(logEntries), [logEntries]); + const itemTimeline = useMemo( + () => + buildItemTimeline({ + projection, + projectionsByThread: projectionByThread, + logEntries, + }), + [logEntries, projection, projectionByThread], + ); + const threadTree = useMemo( + () => buildThreadTree({ threads: shellThreadsById, projectionsByThread: projectionByThread }), + [projectionByThread, shellThreadsById], + ); + const activeTurn = useMemo(() => deriveActiveTurn(projection), [projection]); + const queuedRuns = useMemo(() => buildQueuedRunRows(projection), [projection]); + const mergeBackCandidate = useMemo(() => deriveMergeBackCandidate(projection), [projection]); + const pendingMergeBackTransfer = useMemo( + () => derivePendingMergeBackTransfer(projection), + [projection], + ); + + const appendLog = useCallback((entry: LogEntry) => { + setLogEntries((entries) => [...entries, entry]); + }, []); + + const cacheProjection = useCallback((nextProjection: OrchestrationV2ThreadProjection) => { + setProjectionByThread((current) => { + const next = new Map(current); + next.set(nextProjection.thread.id, nextProjection); + return next; + }); + }, []); + + useEffect(() => { + const item = shellSubscription.data; + if (item === null) return; + if (item.kind === "snapshot") { + setShellThreadsById(new Map(item.snapshot.threads.map((thread) => [thread.id, thread]))); + return; + } + + setShellThreadsById((current) => { + const next = new Map(current); + if (item.kind === "project.updated" || item.kind === "project.removed") { + return next; + } + if (item.kind === "thread.removed") { + next.delete(item.threadId); + return next; + } + next.set(item.thread.id, item.thread); + return next; + }); + }, [shellSubscription.data]); + + useEffect(() => { + const item = threadSubscription.data; + if (item === null) return; + appendLog({ type: "stream", value: item }); + if (item.kind === "snapshot") { + setProjectionError(null); + setProjection(item.projection); + cacheProjection(item.projection); + return; + } + + setProjection((current) => { + const nextProjection = applyOrchestrationV2ProjectionEvent(current, item.event); + if (nextProjection !== null) { + setProjectionError(null); + cacheProjection(nextProjection); + } + return nextProjection; + }); + }, [appendLog, cacheProjection, threadSubscription.data]); + + useEffect(() => { + const message = threadSubscription.error; + if (message === null) return; + setProjection(null); + setProjectionError(message); + appendLog({ type: "error", message }); + }, [appendLog, threadSubscription.error]); + + const refreshProjection = useCallback( + async (nextThreadId: ThreadId) => { + try { + if (environmentId === null) { + throw new Error("The primary environment is unavailable."); + } + const result = await getThreadProjectionQuery({ + environmentId, + input: { threadId: nextThreadId }, + }); + if (result._tag === "Failure") { + throw squashAtomCommandFailure(result); + } + const nextProjection = result.value; + setProjectionError(null); + setProjection(nextProjection); + cacheProjection(nextProjection); + return nextProjection; + } catch (error) { + const message = formatErrorMessage(error); + setProjection(null); + setProjectionError(message); + appendLog({ type: "error", message }); + return null; + } + }, + [appendLog, cacheProjection, environmentId, getThreadProjectionQuery], + ); + + const openThread = useCallback( + async (nextThreadId: ThreadId) => { + setLogEntries([]); + setThreadId(nextThreadId); + const nextProjection = await refreshProjection(nextThreadId); + if (nextProjection !== null) { + setModelSelection(nextProjection.thread.modelSelection); + } + }, + [refreshProjection], + ); + + const createDebugThread = useCallback(async () => { + const nextThreadId = newThreadId(); + const result = await dispatchCommand({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: newCommandId(), + threadId: nextThreadId, + projectId: newProjectId(), + title: "V2 debug thread", + modelSelection, + runtimeMode: "full-access" satisfies RuntimeMode, + interactionMode: "default", + branch: null, + worktreePath: null, + }); + + setLogEntries([]); + setThreadId(nextThreadId); + appendLog({ type: "command", label: "thread.create", value: result }); + await refreshProjection(nextThreadId); + return nextThreadId; + }, [appendLog, dispatchCommand, modelSelection, refreshProjection]); + + const ensureThread = useCallback(async () => { + if (threadId !== null) { + return threadId; + } + + return createDebugThread(); + }, [createDebugThread, threadId]); + + const createNewThread = useCallback(async () => { + if (isBusy) return; + setIsBusy(true); + try { + await createDebugThread(); + } catch (error) { + appendLog({ + type: "error", + message: error instanceof Error ? error.message : String(error), + }); + } finally { + setIsBusy(false); + } + }, [appendLog, createDebugThread, isBusy]); + + const sendPrompt = useCallback( + async (intent: "send" | "steer" = "send") => { + const trimmedPrompt = prompt.trim(); + if (trimmedPrompt.length === 0 || isBusy) { + return; + } + + setIsBusy(true); + try { + const activeThreadId = await ensureThread(); + const targetRunId = activeTurn?.targetRunId; + const result = await dispatchCommand({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: newCommandId(), + threadId: activeThreadId, + messageId: newMessageId(), + text: trimmedPrompt, + attachments: [], + modelSelection, + dispatchMode: + intent === "steer" && targetRunId !== undefined + ? { type: "steer_active", targetRunId } + : targetRunId !== undefined + ? { type: "queue_after_active" } + : { type: "start_immediately" }, + }); + appendLog({ + type: "command", + label: "message.dispatch", + value: result, + }); + await refreshProjection(activeThreadId); + } catch (error) { + appendLog({ + type: "error", + message: formatErrorMessage(error), + }); + } finally { + setIsBusy(false); + } + }, + [ + activeTurn?.targetRunId, + appendLog, + dispatchCommand, + ensureThread, + isBusy, + modelSelection, + prompt, + refreshProjection, + ], + ); + + const promoteQueuedRun = useCallback( + async (queuedRunId: RunId) => { + if (threadId === null || activeTurn === null || isBusy) return; + setIsBusy(true); + try { + const result = await dispatchCommand({ + type: "queued-message.promote-to-steer", + commandId: newCommandId(), + threadId, + queuedRunId, + targetRunId: activeTurn.targetRunId, + }); + appendLog({ + type: "command", + label: "queued-message.promote-to-steer", + value: result, + }); + await refreshProjection(threadId); + } catch (error) { + appendLog({ + type: "error", + message: formatErrorMessage(error), + }); + } finally { + setIsBusy(false); + } + }, + [activeTurn, appendLog, dispatchCommand, isBusy, refreshProjection, threadId], + ); + + const interruptActiveRun = useCallback(async () => { + if (threadId === null || activeTurn === null || isBusy) return; + setIsBusy(true); + try { + const result = await dispatchCommand({ + type: "run.interrupt", + commandId: newCommandId(), + threadId, + runId: activeTurn.targetRunId, + }); + appendLog({ type: "command", label: "run.interrupt", value: result }); + await refreshProjection(threadId); + } catch (error) { + appendLog({ + type: "error", + message: error instanceof Error ? error.message : String(error), + }); + } finally { + setIsBusy(false); + } + }, [activeTurn, appendLog, dispatchCommand, isBusy, refreshProjection, threadId]); + + const reorderQueuedRun = useCallback( + async (runId: RunId, beforeRunId: RunId | null) => { + if (threadId === null || isBusy) return; + setIsBusy(true); + try { + const result = await dispatchCommand({ + type: "queued-run.reorder", + commandId: newCommandId(), + threadId, + runId, + beforeRunId, + }); + appendLog({ + type: "command", + label: "queued-run.reorder", + value: result, + }); + await refreshProjection(threadId); + } catch (error) { + appendLog({ + type: "error", + message: formatErrorMessage(error), + }); + } finally { + setIsBusy(false); + } + }, + [appendLog, dispatchCommand, isBusy, refreshProjection, threadId], + ); + + const forkFromRun = useCallback( + async (input: { readonly threadId: ThreadId; readonly runId: RunId }) => { + const sourceProjection = projectionByThread.get(input.threadId); + if (sourceProjection === undefined || isBusy) return; + + setIsBusy(true); + try { + const targetThreadId = newThreadId(); + const result = await dispatchCommand({ + type: "thread.fork", + createdBy: "user", + creationSource: "web", + commandId: newCommandId(), + sourceThreadId: input.threadId, + targetThreadId, + sourcePoint: { type: "run", runId: input.runId }, + title: `${sourceProjection.thread.title} fork`, + }); + appendLog({ type: "command", label: "thread.fork", value: result }); + await openThread(targetThreadId); + } catch (error) { + appendLog({ + type: "error", + message: error instanceof Error ? error.message : String(error), + }); + } finally { + setIsBusy(false); + } + }, + [appendLog, dispatchCommand, isBusy, openThread, projectionByThread], + ); + + const mergeBackToSource = useCallback(async () => { + if (mergeBackCandidate === null || mergeBackCandidate.latestCompletedRun === null || isBusy) { + return; + } + + setIsBusy(true); + try { + const result = await dispatchCommand({ + type: "thread.merge_back", + createdBy: "user", + creationSource: "web", + commandId: newCommandId(), + sourceThreadId: mergeBackCandidate.sourceThreadId, + targetThreadId: mergeBackCandidate.targetThreadId, + sourcePoint: { + type: "run", + runId: mergeBackCandidate.latestCompletedRun.id, + }, + }); + appendLog({ type: "command", label: "thread.merge_back", value: result }); + await openThread(mergeBackCandidate.targetThreadId); + } catch (error) { + appendLog({ + type: "error", + message: error instanceof Error ? error.message : String(error), + }); + } finally { + setIsBusy(false); + } + }, [appendLog, dispatchCommand, isBusy, mergeBackCandidate, openThread]); + + const rollbackToCheckpoint = useCallback( + async (input: { readonly checkpointId: CheckpointId; readonly scopeId: CheckpointScopeId }) => { + if (threadId === null || isBusy) return; + setIsBusy(true); + try { + const result = await dispatchCommand({ + type: "checkpoint.rollback", + commandId: newCommandId(), + threadId, + scopeId: input.scopeId, + checkpointId: input.checkpointId, + }); + appendLog({ + type: "command", + label: "checkpoint.rollback", + value: result, + }); + await refreshProjection(threadId); + } catch (error) { + appendLog({ + type: "error", + message: formatErrorMessage(error), + }); + } finally { + setIsBusy(false); + } + }, + [appendLog, dispatchCommand, isBusy, refreshProjection, threadId], + ); + + const reset = useCallback(() => { + setThreadId(null); + setProjection(null); + setProjectionError(null); + setLogEntries([]); + setProjectionByThread(new Map()); + }, []); + + const renderPanel = (panelKey: PanelKey): ReactNode => { + const onClose = () => { + closePanel(panelKey); + }; + switch (panelKey) { + case "tree": + return ( + { + void createNewThread(); + }} + onOpenThread={(nextThreadId) => { + void openThread(nextThreadId); + }} + /> + ); + case "projection": + return ( + + ); + case "item": + return ( + { + void openThread(nextThreadId); + }} + onForkFromRun={(input) => { + void forkFromRun(input); + }} + onRollbackToCheckpoint={(input) => { + void rollbackToCheckpoint(input); + }} + /> + ); + case "stream": + return ( + + ); + } + }; + + return ( +
    +
    +
    + +
    +
    + { + setModelSelection(createModelSelection(instanceId, model)); + }} + /> + + +
    +
    + + { + restorePanel(key); + }} + /> + +
    +
    { + event.preventDefault(); + void sendPrompt("send"); + }} + > + +