Orchestration Interface Spec (Required State)
Status: Required target architecture (not a description of current behavior).
1. Identifier Schema (Branded)
All identifiers are branded and never plain string in schema definitions.
const ThreadId = Schema.String.pipe(Schema.brand("ThreadId"));
const ProjectId = Schema.String.pipe(Schema.brand("ProjectId"));
const CommandId = Schema.String.pipe(Schema.brand("CommandId"));
const EventId = Schema.String.pipe(Schema.brand("EventId"));
const MessageId = Schema.String.pipe(Schema.brand("MessageId"));
const TurnId = Schema.String.pipe(Schema.brand("TurnId"));
const ProviderSessionId = Schema.String.pipe(Schema.brand("ProviderSessionId"));
const ProviderThreadId = Schema.String.pipe(Schema.brand("ProviderThreadId"));
const ProviderTurnId = Schema.String.pipe(Schema.brand("ProviderTurnId"));
const ProviderItemId = Schema.String.pipe(Schema.brand("ProviderItemId"));
const ApprovalRequestId = Schema.String.pipe(Schema.brand("ApprovalRequestId"));
const CheckpointRef = Schema.String.pipe(Schema.brand("CheckpointRef"));
Rules:
ThreadIdis app/orchestration thread identity.ProviderThreadIdis provider runtime thread identity.- They are never assigned to each other.
ProviderSessionIdis internal server/provider routing identity; clients do not send it.- use e.g.
ThreadId.make(randomUUID())to create new entity ids at the source (e.g. client create thread id and send to server, provider adapter creates the provider thread id from the codex app server etc)
2. Client Commands (Domain Commands)
All commands are sent through one RPC: orchestration.dispatchCommand.
Output for every command: DispatchResultSchema = { sequence: number }.
2.1 Client-Dispatchable Commands
project.create
- Input:
commandId: CommandIdprojectId: ProjectIdtitle: stringworkspaceRoot: stringdefaultModel?: stringcreatedAt: IsoDateTime
- Output:
DispatchResult
project.meta.update
- Input:
commandId: CommandIdprojectId: ProjectId- optional
{ title, workspaceRoot, defaultModel, scripts }
- Output:
DispatchResult
project.delete
- Input:
commandId,projectId - Output:
DispatchResult
thread.create
- Input:
commandId: CommandIdthreadId: ThreadIdprojectId: ProjectIdtitle: stringmodel: stringbranch: string | nullworktreePath: string | nullcreatedAt: IsoDateTime
- Output:
DispatchResult
thread.delete
- Input:
commandId,threadId - Output:
DispatchResult
thread.meta.update
- Input:
commandId,threadId, optional{ title, model, branch, worktreePath } - Output:
DispatchResult
thread.turn.start
- Input:
commandIdthreadIdmessage:messageId: MessageIdrole: "user"text: stringattachments: ChatAttachment[](schema-shared)
model?: stringeffort?: stringcreatedAt
- Output:
DispatchResult
thread.turn.interrupt
- Input:
commandId,threadId, optionalturnId: TurnId,createdAt - Output:
DispatchResult
thread.approval.respond
- Input:
commandIdthreadIdrequestId: ApprovalRequestIddecision: "accept" | "acceptForSession" | "decline" | "cancel"createdAt
- Output:
DispatchResult
thread.checkpoint.revert
- Input:
commandId,threadId,turnCount: number,createdAt - Output:
DispatchResult
thread.session.stop
- Input:
commandId,threadId,createdAt - Output:
DispatchResult
2.2 Internal-Only Commands (Not Client Dispatchable)
thread.session.set
- Server-owned projection update for session lifecycle.
thread.message.assistant.delta
- Server-owned incremental assistant content append.
thread.message.assistant.complete
- Server-owned assistant message completion marker.
thread.turn.diff.complete
- Server-owned checkpoint diff summary write.
thread.activity.append
- Server-owned activity feed append.
3. Client RPC (Required)
These are the required client-facing RPC methods.
orchestration.getSnapshot
- Input:
{} - Output:
OrchestrationReadModel
orchestration.dispatchCommand
- Input:
ClientOrchestrationCommand(Schema.Unionof all client-dispatchable commands) - Output:
DispatchResult
orchestration.getTurnDiff
- Input:
threadId: ThreadIdfromTurnCount: numbertoTurnCount: number
- Output:
threadId: ThreadIdfromTurnCount: numbertoTurnCount: numberdiff: string
orchestration.replayEvents(optional operational/debug API)
- Input:
{ fromSequenceExclusive: number } - Output:
OrchestrationEvent[]
3.1 RPC To Remove
Remove from client boundary:
providers.startSessionproviders.sendTurnproviders.interruptTurnproviders.respondToRequestproviders.stopSessionproviders.listCheckpointsproviders.getCheckpointDiffproviders.revertToCheckpoint
Rationale:
- Session/turn/checkpoint control belongs to orchestrator command path.
- Snapshot already includes checkpoint summaries (
checkpoints); only full textual diff needs explicit query RPC.
4. Server Orchestrator Command <-> Event Schemas
Each command deterministically yields one or more domain events that are persisted to the event store.
project.create->project.createdproject.meta.update->project.meta-updatedproject.delete->project.deletedthread.create->thread.createdthread.delete->thread.deletedthread.meta.update->thread.meta-updatedthread.turn.start->
thread.message-sent(user message append)thread.turn-start-requested- then (async side effect success)
thread.session-set+ provider-driven message/turn events
thread.turn.interrupt->thread.turn-interrupt-requestedthread.approval.respond->thread.approval-response-requestedthread.checkpoint.revert->thread.checkpoint-revert-requestedthenthread.revertedthread.session.stop->thread.session-stop-requestedthenthread.session-set
Internal command/event path:
thread.session.set->thread.session-setthread.message.assistant.delta->thread.message-sent(assistant streaming)thread.message.assistant.complete->thread.message-sent(streaming false)thread.turn.diff.complete->thread.turn-diff-completedthread.activity.append->thread.activity-appended
5. Orchestration Events <-> Generic Provider Events <-> Raw Provider Events
Required mapping pipeline:
Raw Provider Event (adapter-specific)
-> GenericProviderRuntimeEvent (provider-agnostic schema)
-> Internal Orchestration Command
-> Orchestration Domain Event
5.1 Mapping Table for Codex Adapter
- Raw
thread/started
- Generic:
thread.started - Internal command:
thread.session.set(attachProviderThreadId, statusready|running) - Domain event:
thread.session-set
- Raw
turn/started
- Generic:
turn.started - Internal command:
thread.session.set(statusrunning, active turn) - Domain event:
thread.session-set
- Raw
item/agentMessage/delta
- Generic:
message.delta - Internal command:
thread.message.assistant.delta - Domain event:
thread.message-sent(assistant, streaming)
- Raw
item/completed(agent message)
- Generic:
message.completed - Internal command:
thread.message.assistant.complete - Domain event:
thread.message-sent(assistant, completed)
- Raw
turn/completed
- Generic:
turn.completed - Internal commands:
thread.session.set(ready/error)thread.activity.append(optional summary)
- Domain events:
thread.session-set,thread.activity-appended
- Raw approval request
- Generic:
approval.requested - Internal command:
thread.activity.append(typedrequestId,requestKind) - Domain event:
thread.activity-appended
- Raw approval decision ack
- Generic:
approval.resolved - Internal command:
thread.activity.append - Domain event:
thread.activity-appended
- Raw tool start/complete
- Generic:
tool.started/tool.completed - Internal command:
thread.activity.append - Domain event:
thread.activity-appended
- Checkpoint capture (server-produced)
- Generic:
checkpoint.captured - Internal command:
thread.turn.diff.complete - Domain event:
thread.turn-diff-completed
- Raw/runtime error
- Generic:
runtime.error - Internal commands:
thread.session.set(statuserror)thread.activity.append(tone=error)
- Domain events:
thread.session-set,thread.activity-appended
6. Schema Section (Boundary + Reuse)
6.1 Shared Schemas (contracts package)
Expose to both web and server:
OrchestrationReadModelSchemaClientOrchestrationCommandSchemaOrchestrationEventSchemaOrchestrationRpcSchemas(getSnapshot,dispatchCommand,getTurnDiff, optional replay)GenericProviderRuntimeEventSchema(used for diagnostics/ingestion tests)- Shared message/activity/checkpoint summary schemas used in snapshot
6.2 Server-Internal Schemas
Not exposed to web:
InternalOrchestrationCommandSchema- Adapter raw-event schemas and provider-specific payload schemas
- Provider session directory records and persistence-only row schemas
- Git service cache/query schemas (non-orchestration read model)
6.3 Reuse Requirements
- Snapshot checkpoint summary schema is source of truth for checkpoint list UI.
getTurnDiffresponse reusesThreadId/turn count brands and textualdiffonly.- Activity schemas are reused for approval/tool/runtime annotations; no separate UI-only structure.
- Session schema must include both
threadId: ThreadIdand optionalproviderThreadId: ProviderThreadId(explicitly distinct).
7. Persistence Model (Required)
This section defines required persisted tables, the canonical persisted event envelope, and required projected/read tables.
7.1 Write-Side Persisted Tables
orchestration_events(append-only event store)
sequence: number(global monotonic sequence, primary key)eventId: EventId(unique)aggregateKind: "project" | "thread"streamId: ProjectId | ThreadId(aggregate stream id)streamVersion: number(per-aggregate monotonic version)eventType: OrchestrationEventTypeoccurredAt: IsoDateTimecommandId: CommandId | nullcausationEventId: EventId | nullcorrelationId: CommandId | nullactorKind: "client" | "server" | "provider"payload: OrchestrationEventPayload(type-validated byeventType)metadata: OrchestrationEventMetadata
orchestration_command_receipts(idempotency + ack replay)
commandId: CommandId(primary key)aggregateKind: "project" | "thread"aggregateId: ProjectId | ThreadIdacceptedAt: IsoDateTimeresultSequence: numberstatus: "accepted" | "rejected"error: string | null
checkpoint_diff_blobs(large plaintext diffs; separate from summaries)
threadId: ThreadIdfromTurnCount: numbertoTurnCount: numberdiff: stringcreatedAt: IsoDateTime- unique key:
(threadId, fromTurnCount, toTurnCount)
provider_session_runtime(server-internal adapter resume state)
providerSessionId: ProviderSessionId(primary key)threadId: ThreadIdproviderName: stringadapterKey: stringproviderThreadId: ProviderThreadId | nullstatus: "starting" | "running" | "stopped" | "error"lastSeenAt: IsoDateTimeresumeCursor: JsonValue | null(adapter-specific opaque state)runtimePayload: JsonValue | null(adapter-specific opaque state)
7.2 Canonical Persisted Event Schema
OrchestrationPersistedEventSchema (full envelope):
type OrchestrationPersistedEvent = {
sequence: number;
eventId: EventId;
aggregateKind: "project" | "thread";
streamId: ProjectId | ThreadId;
streamVersion: number;
eventType: OrchestrationEventType;
occurredAt: IsoDateTime;
commandId: CommandId | null;
causationEventId: EventId | null;
correlationId: CommandId | null;
actorKind: "client" | "server" | "provider";
payload: OrchestrationEventPayload;
metadata: {
providerSessionId?: ProviderSessionId;
providerThreadId?: ProviderThreadId;
providerTurnId?: ProviderTurnId;
providerItemId?: ProviderItemId;
adapterKey?: string;
requestId?: ApprovalRequestId;
ingestedAt?: IsoDateTime;
};
};
Rules:
payloadmust be schema-discriminated byeventType.- provider identifiers only appear in
metadataand provider-specific payload sub-shapes; they do not replaceThreadId. streamVersionis concurrency guard for aggregate writes.
7.3 Required Projected Tables (Read Models)
projection_projects
projectId: ProjectId(primary key)title: stringworkspaceRoot: stringdefaultModel: string | nullscripts: ProjectScript[]createdAt: IsoDateTimeupdatedAt: IsoDateTimedeletedAt: IsoDateTime | null
projection_threads
threadId: ThreadId(primary key)projectId: ProjectIdtitle: stringmodel: stringbranch: string | nullworktreePath: string | nulllatestTurnId: TurnId | nullcreatedAt: IsoDateTimeupdatedAt: IsoDateTimedeletedAt: IsoDateTime | null
projection_thread_messages
messageId: MessageId(primary key)threadId: ThreadIdturnId: TurnId | nullrole: "user" | "assistant" | "system"text: stringisStreaming: booleancreatedAt: IsoDateTimeupdatedAt: IsoDateTime
projection_thread_activities
activityId: EventId(primary key; derived from source event)threadId: ThreadIdturnId: TurnId | nulltone: "info" | "tool" | "approval" | "error"kind: stringsummary: stringpayload: JsonValuecreatedAt: IsoDateTime
projection_thread_sessions
threadId: ThreadId(primary key)status: "idle" | "starting" | "running" | "ready" | "interrupted" | "stopped" | "error"providerName: string | nullproviderSessionId: ProviderSessionId | nullproviderThreadId: ProviderThreadId | nullactiveTurnId: TurnId | nulllastError: string | nullupdatedAt: IsoDateTime
projection_thread_turns
turnId: TurnId(primary key)threadId: ThreadIdturnCount: numberstatus: "running" | "completed" | "interrupted" | "error"userMessageId: MessageId | nullassistantMessageId: MessageId | nullstartedAt: IsoDateTimecompletedAt: IsoDateTime | null
projection_checkpoints
threadId: ThreadIdturnId: TurnIdcheckpointTurnCount: numbercheckpointRef: CheckpointRefstatus: "ready" | "missing" | "error"files: JsonArray(typed as file diff summary schema)assistantMessageId: MessageId | null
Lines 1–500 of 582. Load more
# Orchestration Interface Spec (Required State) Status: Required target architecture (not a description of current behavior). ## 1. Identifier Schema (Branded) All identifiers are branded and never plain `string` in schema definitions. ```ts const ThreadId = Schema.String.pipe(Schema.brand("ThreadId")); const ProjectId = Schema.String.pipe(Schema.brand("ProjectId")); const CommandId = Schema.String.pipe(Schema.brand("CommandId")); const EventId = Schema.String.pipe(Schema.brand("EventId")); const MessageId = Schema.String.pipe(Schema.brand("MessageId")); const TurnId = Schema.String.pipe(Schema.brand("TurnId")); const ProviderSessionId = Schema.String.pipe(Schema.brand("ProviderSessionId")); const ProviderThreadId = Schema.String.pipe(Schema.brand("ProviderThreadId")); const ProviderTurnId = Schema.String.pipe(Schema.brand("ProviderTurnId")); const ProviderItemId = Schema.String.pipe(Schema.brand("ProviderItemId")); const ApprovalRequestId = Schema.String.pipe(Schema.brand("ApprovalRequestId")); const CheckpointRef = Schema.String.pipe(Schema.brand("CheckpointRef")); ``` Rules: - `ThreadId` is app/orchestration thread identity. - `ProviderThreadId` is provider runtime thread identity. - They are never assigned to each other. - `ProviderSessionId` is internal server/provider routing identity; clients do not send it. - use e.g. `ThreadId.make(randomUUID())` to create new entity ids at the source (e.g. client create thread id and send to server, provider adapter creates the provider thread id from the codex app server etc) ## 2. Client Commands (Domain Commands) All commands are sent through one RPC: `orchestration.dispatchCommand`. Output for every command: `DispatchResultSchema = { sequence: number }`. ### 2.1 Client-Dispatchable Commands 1. `project.create` - Input: - `commandId: CommandId` - `projectId: ProjectId` - `title: string` - `workspaceRoot: string` - `defaultModel?: string` - `createdAt: IsoDateTime` - Output: `DispatchResult` 2. `project.meta.update` - Input: - `commandId: CommandId` - `projectId: ProjectId` - optional `{ title, workspaceRoot, defaultModel, scripts }` - Output: `DispatchResult` 3. `project.delete` - Input: `commandId`, `projectId` - Output: `DispatchResult` 4. `thread.create` - Input: - `commandId: CommandId` - `threadId: ThreadId` - `projectId: ProjectId` - `title: string` - `model: string` - `branch: string | null` - `worktreePath: string | null` - `createdAt: IsoDateTime` - Output: `DispatchResult` 5. `thread.delete` - Input: `commandId`, `threadId` - Output: `DispatchResult` 6. `thread.meta.update` - Input: `commandId`, `threadId`, optional `{ title, model, branch, worktreePath }` - Output: `DispatchResult` 7. `thread.turn.start` - Input: - `commandId` - `threadId` - `message`: - `messageId: MessageId` - `role: "user"` - `text: string` - `attachments: ChatAttachment[]` (schema-shared) - `model?: string` - `effort?: string` - `createdAt` - Output: `DispatchResult` 8. `thread.turn.interrupt` - Input: `commandId`, `threadId`, optional `turnId: TurnId`, `createdAt` - Output: `DispatchResult` 9. `thread.approval.respond` - Input: - `commandId` - `threadId` - `requestId: ApprovalRequestId` - `decision: "accept" | "acceptForSession" | "decline" | "cancel"` - `createdAt` - Output: `DispatchResult` 10. `thread.checkpoint.revert` - Input: `commandId`, `threadId`, `turnCount: number`, `createdAt` - Output: `DispatchResult` 11. `thread.session.stop` - Input: `commandId`, `threadId`, `createdAt` - Output: `DispatchResult` ### 2.2 Internal-Only Commands (Not Client Dispatchable) 1. `thread.session.set` - Server-owned projection update for session lifecycle. 2. `thread.message.assistant.delta` - Server-owned incremental assistant content append. 3. `thread.message.assistant.complete` - Server-owned assistant message completion marker. 4. `thread.turn.diff.complete` - Server-owned checkpoint diff summary write. 5. `thread.activity.append` - Server-owned activity feed append. ## 3. Client RPC (Required) These are the required client-facing RPC methods. 1. `orchestration.getSnapshot` - Input: `{}` - Output: `OrchestrationReadModel` 2. `orchestration.dispatchCommand` - Input: `ClientOrchestrationCommand` (`Schema.Union` of all client-dispatchable commands) - Output: `DispatchResult` 3. `orchestration.getTurnDiff` - Input: - `threadId: ThreadId` - `fromTurnCount: number` - `toTurnCount: number` - Output: - `threadId: ThreadId` - `fromTurnCount: number` - `toTurnCount: number` - `diff: string` 4. `orchestration.replayEvents` (optional operational/debug API) - Input: `{ fromSequenceExclusive: number }` - Output: `OrchestrationEvent[]` ### 3.1 RPC To Remove Remove from client boundary: - `providers.startSession` - `providers.sendTurn` - `providers.interruptTurn` - `providers.respondToRequest` - `providers.stopSession` - `providers.listCheckpoints` - `providers.getCheckpointDiff` - `providers.revertToCheckpoint` Rationale: - Session/turn/checkpoint control belongs to orchestrator command path. - Snapshot already includes checkpoint summaries (`checkpoints`); only full textual diff needs explicit query RPC. ## 4. Server Orchestrator Command <-> Event Schemas Each command deterministically yields one or more domain events that are persisted to the event store. 1. `project.create` -> `project.created` 2. `project.meta.update` -> `project.meta-updated` 3. `project.delete` -> `project.deleted` 4. `thread.create` -> `thread.created` 5. `thread.delete` -> `thread.deleted` 6. `thread.meta.update` -> `thread.meta-updated` 7. `thread.turn.start` -> - `thread.message-sent` (user message append) - `thread.turn-start-requested` - then (async side effect success) `thread.session-set` + provider-driven message/turn events 8. `thread.turn.interrupt` -> `thread.turn-interrupt-requested` 9. `thread.approval.respond` -> `thread.approval-response-requested` 10. `thread.checkpoint.revert` -> `thread.checkpoint-revert-requested` then `thread.reverted` 11. `thread.session.stop` -> `thread.session-stop-requested` then `thread.session-set` Internal command/event path: - `thread.session.set` -> `thread.session-set` - `thread.message.assistant.delta` -> `thread.message-sent` (assistant streaming) - `thread.message.assistant.complete` -> `thread.message-sent` (streaming false) - `thread.turn.diff.complete` -> `thread.turn-diff-completed` - `thread.activity.append` -> `thread.activity-appended` ## 5. Orchestration Events <-> Generic Provider Events <-> Raw Provider Events Required mapping pipeline: `Raw Provider Event (adapter-specific)` -> `GenericProviderRuntimeEvent` (provider-agnostic schema) -> `Internal Orchestration Command` -> `Orchestration Domain Event` ### 5.1 Mapping Table for Codex Adapter 1. Raw `thread/started` - Generic: `thread.started` - Internal command: `thread.session.set` (attach `ProviderThreadId`, status `ready|running`) - Domain event: `thread.session-set` 2. Raw `turn/started` - Generic: `turn.started` - Internal command: `thread.session.set` (status `running`, active turn) - Domain event: `thread.session-set` 3. Raw `item/agentMessage/delta` - Generic: `message.delta` - Internal command: `thread.message.assistant.delta` - Domain event: `thread.message-sent` (assistant, streaming) 4. Raw `item/completed` (agent message) - Generic: `message.completed` - Internal command: `thread.message.assistant.complete` - Domain event: `thread.message-sent` (assistant, completed) 5. Raw `turn/completed` - Generic: `turn.completed` - Internal commands: - `thread.session.set` (ready/error) - `thread.activity.append` (optional summary) - Domain events: `thread.session-set`, `thread.activity-appended` 6. Raw approval request - Generic: `approval.requested` - Internal command: `thread.activity.append` (typed `requestId`, `requestKind`) - Domain event: `thread.activity-appended` 7. Raw approval decision ack - Generic: `approval.resolved` - Internal command: `thread.activity.append` - Domain event: `thread.activity-appended` 8. Raw tool start/complete - Generic: `tool.started` / `tool.completed` - Internal command: `thread.activity.append` - Domain event: `thread.activity-appended` 9. Checkpoint capture (server-produced) - Generic: `checkpoint.captured` - Internal command: `thread.turn.diff.complete` - Domain event: `thread.turn-diff-completed` 10. Raw/runtime error - Generic: `runtime.error` - Internal commands: - `thread.session.set` (status `error`) - `thread.activity.append` (`tone=error`) - Domain events: `thread.session-set`, `thread.activity-appended` ## 6. Schema Section (Boundary + Reuse) ### 6.1 Shared Schemas (contracts package) Expose to both web and server: - `OrchestrationReadModelSchema` - `ClientOrchestrationCommandSchema` - `OrchestrationEventSchema` - `OrchestrationRpcSchemas` (`getSnapshot`, `dispatchCommand`, `getTurnDiff`, optional replay) - `GenericProviderRuntimeEventSchema` (used for diagnostics/ingestion tests) - Shared message/activity/checkpoint summary schemas used in snapshot ### 6.2 Server-Internal Schemas Not exposed to web: - `InternalOrchestrationCommandSchema` - Adapter raw-event schemas and provider-specific payload schemas - Provider session directory records and persistence-only row schemas - Git service cache/query schemas (non-orchestration read model) ### 6.3 Reuse Requirements - Snapshot checkpoint summary schema is source of truth for checkpoint list UI. - `getTurnDiff` response reuses `ThreadId`/turn count brands and textual `diff` only. - Activity schemas are reused for approval/tool/runtime annotations; no separate UI-only structure. - Session schema must include both `threadId: ThreadId` and optional `providerThreadId: ProviderThreadId` (explicitly distinct). ## 7. Persistence Model (Required) This section defines required persisted tables, the canonical persisted event envelope, and required projected/read tables. ### 7.1 Write-Side Persisted Tables 1. `orchestration_events` (append-only event store) - `sequence: number` (global monotonic sequence, primary key) - `eventId: EventId` (unique) - `aggregateKind: "project" | "thread"` - `streamId: ProjectId | ThreadId` (aggregate stream id) - `streamVersion: number` (per-aggregate monotonic version) - `eventType: OrchestrationEventType` - `occurredAt: IsoDateTime` - `commandId: CommandId | null` - `causationEventId: EventId | null` - `correlationId: CommandId | null` - `actorKind: "client" | "server" | "provider"` - `payload: OrchestrationEventPayload` (type-validated by `eventType`) - `metadata: OrchestrationEventMetadata` 2. `orchestration_command_receipts` (idempotency + ack replay) - `commandId: CommandId` (primary key) - `aggregateKind: "project" | "thread"` - `aggregateId: ProjectId | ThreadId` - `acceptedAt: IsoDateTime` - `resultSequence: number` - `status: "accepted" | "rejected"` - `error: string | null` 3. `checkpoint_diff_blobs` (large plaintext diffs; separate from summaries) - `threadId: ThreadId` - `fromTurnCount: number` - `toTurnCount: number` - `diff: string` - `createdAt: IsoDateTime` - unique key: `(threadId, fromTurnCount, toTurnCount)` 4. `provider_session_runtime` (server-internal adapter resume state) - `providerSessionId: ProviderSessionId` (primary key) - `threadId: ThreadId` - `providerName: string` - `adapterKey: string` - `providerThreadId: ProviderThreadId | null` - `status: "starting" | "running" | "stopped" | "error"` - `lastSeenAt: IsoDateTime` - `resumeCursor: JsonValue | null` (adapter-specific opaque state) - `runtimePayload: JsonValue | null` (adapter-specific opaque state) ### 7.2 Canonical Persisted Event Schema `OrchestrationPersistedEventSchema` (full envelope): ```ts type OrchestrationPersistedEvent = { sequence: number; eventId: EventId; aggregateKind: "project" | "thread"; streamId: ProjectId | ThreadId; streamVersion: number; eventType: OrchestrationEventType; occurredAt: IsoDateTime; commandId: CommandId | null; causationEventId: EventId | null; correlationId: CommandId | null; actorKind: "client" | "server" | "provider"; payload: OrchestrationEventPayload; metadata: { providerSessionId?: ProviderSessionId; providerThreadId?: ProviderThreadId; providerTurnId?: ProviderTurnId; providerItemId?: ProviderItemId; adapterKey?: string; requestId?: ApprovalRequestId; ingestedAt?: IsoDateTime; }; }; ``` Rules: - `payload` must be schema-discriminated by `eventType`. - provider identifiers only appear in `metadata` and provider-specific payload sub-shapes; they do not replace `ThreadId`. - `streamVersion` is concurrency guard for aggregate writes. ### 7.3 Required Projected Tables (Read Models) 1. `projection_projects` - `projectId: ProjectId` (primary key) - `title: string` - `workspaceRoot: string` - `defaultModel: string | null` - `scripts: ProjectScript[]` - `createdAt: IsoDateTime` - `updatedAt: IsoDateTime` - `deletedAt: IsoDateTime | null` 2. `projection_threads` - `threadId: ThreadId` (primary key) - `projectId: ProjectId` - `title: string` - `model: string` - `branch: string | null` - `worktreePath: string | null` - `latestTurnId: TurnId | null` - `createdAt: IsoDateTime` - `updatedAt: IsoDateTime` - `deletedAt: IsoDateTime | null` 3. `projection_thread_messages` - `messageId: MessageId` (primary key) - `threadId: ThreadId` - `turnId: TurnId | null` - `role: "user" | "assistant" | "system"` - `text: string` - `isStreaming: boolean` - `createdAt: IsoDateTime` - `updatedAt: IsoDateTime` 4. `projection_thread_activities` - `activityId: EventId` (primary key; derived from source event) - `threadId: ThreadId` - `turnId: TurnId | null` - `tone: "info" | "tool" | "approval" | "error"` - `kind: string` - `summary: string` - `payload: JsonValue` - `createdAt: IsoDateTime` 5. `projection_thread_sessions` - `threadId: ThreadId` (primary key) - `status: "idle" | "starting" | "running" | "ready" | "interrupted" | "stopped" | "error"` - `providerName: string | null` - `providerSessionId: ProviderSessionId | null` - `providerThreadId: ProviderThreadId | null` - `activeTurnId: TurnId | null` - `lastError: string | null` - `updatedAt: IsoDateTime` 6. `projection_thread_turns` - `turnId: TurnId` (primary key) - `threadId: ThreadId` - `turnCount: number` - `status: "running" | "completed" | "interrupted" | "error"` - `userMessageId: MessageId | null` - `assistantMessageId: MessageId | null` - `startedAt: IsoDateTime` - `completedAt: IsoDateTime | null` 7. `projection_checkpoints` - `threadId: ThreadId` - `turnId: TurnId` - `checkpointTurnCount: number` - `checkpointRef: CheckpointRef` - `status: "ready" | "missing" | "error"` - `files: JsonArray` (typed as file diff summary schema) - `assistantMessageId: MessageId | null`