SPEC.md
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.
| 1 | const ThreadId = Schema.String.pipe(Schema.brand("ThreadId")); |
| 2 | const ProjectId = Schema.String.pipe(Schema.brand("ProjectId")); |
| 3 | const CommandId = Schema.String.pipe(Schema.brand("CommandId")); |
| 4 | const EventId = Schema.String.pipe(Schema.brand("EventId")); |
| 5 | const MessageId = Schema.String.pipe(Schema.brand("MessageId")); |
| 6 | const TurnId = Schema.String.pipe(Schema.brand("TurnId")); |
| 7 | |
| 8 | const ProviderSessionId = Schema.String.pipe(Schema.brand("ProviderSessionId")); |
| 9 | const ProviderThreadId = Schema.String.pipe(Schema.brand("ProviderThreadId")); |
| 10 | const ProviderTurnId = Schema.String.pipe(Schema.brand("ProviderTurnId")); |
| 11 | const ProviderItemId = Schema.String.pipe(Schema.brand("ProviderItemId")); |
| 12 | const ApprovalRequestId = Schema.String.pipe(Schema.brand("ApprovalRequestId")); |
| 13 | |
| 14 | 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):
| 1 | type OrchestrationPersistedEvent = { |
| 2 | sequence: number; |
| 3 | eventId: EventId; |
| 4 | aggregateKind: "project" | "thread"; |
| 5 | streamId: ProjectId | ThreadId; |
| 6 | streamVersion: number; |
| 7 | eventType: OrchestrationEventType; |
| 8 | occurredAt: IsoDateTime; |
| 9 | commandId: CommandId | null; |
| 10 | causationEventId: EventId | null; |
| 11 | correlationId: CommandId | null; |
| 12 | actorKind: "client" | "server" | "provider"; |
| 13 | payload: OrchestrationEventPayload; |
| 14 | metadata: { |
| 15 | providerSessionId?: ProviderSessionId; |
| 16 | providerThreadId?: ProviderThreadId; |
| 17 | providerTurnId?: ProviderTurnId; |
| 18 | providerItemId?: ProviderItemId; |
| 19 | adapterKey?: string; |
| 20 | requestId?: ApprovalRequestId; |
| 21 | ingestedAt?: IsoDateTime; |
| 22 | }; |
| 23 | }; |
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 | nullcompletedAt: IsoDateTime- unique key:
(threadId, checkpointTurnCount)
projection_pending_approvals
requestId: ApprovalRequestId(primary key)threadId: ThreadIdturnId: TurnId | nullstatus: "pending" | "resolved"decision: "accept" | "acceptForSession" | "decline" | "cancel" | nullcreatedAt: IsoDateTimeresolvedAt: IsoDateTime | null
projection_state
projector: string(primary key; e.g.threads,messages,sessions)lastAppliedSequence: numberupdatedAt: IsoDateTime
Projection consistency rules:
- Every projector applies read-model row updates and
projection_state.lastAppliedSequencein the same database transaction. - Optional debug field on projection rows:
lastEventSequence: number(not required for correctness).
7.4 Snapshot and RPC Requirements
orchestration.getSnapshotis fully served from projection tables and returnssnapshotSequence: number.- Snapshot must include
projects[]fromprojection_projects. - Thread snapshot must include
checkpoints[]fromprojection_checkpoints:
turnId: TurnIdcompletedAt: IsoDateTimestatusfiles[](path,kind,additions,deletions)checkpointRef: CheckpointRefassistantMessageId?: MessageIdcheckpointTurnCount: number
- Client does not require
listCheckpointsRPC:
- checkpoint list comes from snapshot projections
- full diff text comes from
orchestration.getTurnDiffbacked bycheckpoint_diff_blobs
- Provider session identity is not a client routing key:
- client acts on
ThreadId - server resolves provider session internally via
projection_thread_sessions
snapshotSequenceis derived fromprojection_state:
- if snapshot depends on multiple projectors, use the minimum
lastAppliedSequenceacross those projectors.
- Event subscription handoff contract:
- client performs
getSnapshotand readssnapshotSequence - client subscribes/replays with
fromSequenceExclusive = snapshotSequence - server guarantees no gap between snapshot visibility and subsequent event stream from that sequence.
7.5 External Derived State (Non-Orchestration)
- Current git/worktree state is not projected from orchestration events.
- If persisted, it belongs in a separate git cache/read model (for example
git_state_cache) owned by a git service. - Orchestration may embed git metadata only when captured as a domain fact (for example checkpoint metadata at turn completion).
- Any RPC for current git status should be outside orchestration RPC (for example
git.getThreadState).
7.6 Existing Repository Placement
ProjectsRepository
- Fits as projection/query access on top of
projection_projectsplus orchestration command dispatch for writes. - Must not bypass command->event append path for mutations.
CheckpointsRepository
- Fits as projection/query access on top of
projection_checkpointspluscheckpoint_diff_blobs(or git-service on-demand diff implementation). - Represents the same concept as prior
turn_diff_summary; canonical naming ischeckpoint. - Must not be an independent source of truth outside orchestration events.
ProviderSessionsRepository
- Fits as server-internal runtime persistence on top of
provider_session_runtime, with domain-visible state projected intoprojection_thread_sessions. - Not part of client RPC/domain aggregate boundary.