runview

package
v1.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 61 Imported by: 0

Documentation

Overview

Package runview — agent-driven merge-conflict resolution.

The HTTP layer's `POST /merge/conflicts/resolve-with-agent` calls ResolveAllConflictsWithAgent below; it short-circuits to ErrAgentResolverNotWired when no LLM credential is reachable, and otherwise builds a claw direct-LLM call against the resolver prompt + schema, applies the returned resolutions via StageResolvedFile, and returns the refreshed conflict snapshot. The implementation lives in this file rather than service_control.go because the imports (model, detect) are heavy and would otherwise burden every service-control reader with the LLM dependency surface.

Package runview exposes a service-layer view of iterion runs for programmatic consumers — the HTTP server and the future "run console" UI. It contains the canonical Launch / Resume / Cancel / Snapshot implementations that the CLI also delegates to, along with a pure reducer that derives a per-execution snapshot from the persisted event stream.

Index

Constants

View Source
const IRWorkflowEndpointPath = "/api/runs/{id}/workflow"

IRWorkflowEndpointPath is exposed for symmetry with other runview helpers; the server wires the handler manually.

View Source
const MainBranch = "main"

MainBranch is the synthetic branch name used when an event carries no explicit branch_id (single-threaded execution before any fan-out).

View Source
const MaxEventsPerPage = runstream.MaxEventsPerPage

MaxEventsPerPage caps the number of events any single LoadEvents response materialises. The original 5000 was tuned for a world where tool I/O bodies (multi-MB Bash stdout, LLM thinking blocks) were inlined into events.jsonl, so a single page could easily exceed 100MB of allocation. The sidecar-blob migration moved those bodies out (preview ≤4KB stays inline; the rest lives in runs/<id>/tools/<tool_use_id>/{input,output}), bounding per-event size to a few KB regardless of payload size.

25000 keeps the worst-case per-page allocation in the low tens of MB on typical events while letting most full runs replay in a single round-trip (the WS subscriber + the /events HTTP endpoint both paginate, so this is a per-page knob, not a hard ceiling). Callers paginate by passing the next page's `from` as previous_last.Seq+1; len(out) == cap means "more available".

The canonical constant lives in runstream (the streaming seam batches replay pages at the same size); this alias keeps the historical runview.MaxEventsPerPage name working for the HTTP/WS layer.

View Source
const NoEventsSeq int64 = -1

SnapshotBuilder is a stateful incremental reducer: feed it events in sequence order via Apply, and read out the current RunSnapshot via Snapshot. The same builder is used for cold reads (replay every event from disk) and for live subscribers (replay history then accept new events as they arrive).

The reducer is deterministic: BuildSnapshot(run, events) always produces the same output for the same input, which lets the frontend derive the same per-seq snapshots locally to power the time-travel scrubber. NoEventsSeq is the sentinel value of RunSnapshot.LastSeq when no events have been applied yet. Distinguishing "empty stream" from "one event at seq 0" matters for WS catch-up dedup: we must not drop the first live event after subscribing to a fresh run.

Variables

View Source
var ErrAgentResolverNotWired = errors.New("agent resolver unavailable: no LLM credential detected (sign in via `claude` or `codex` and retry)")

ErrAgentResolverNotWired signals that no provider credential is reachable for the resolver. Detect with errors.Is so future generations of this code surface a stable "no creds" signal even if the message changes.

View Source
var ErrInteractionNotAsync = errors.New("runview: interaction is not an async question")

ErrInteractionNotAsync reports an answer targeting a non-async interaction (blocking pauses are answered via /answer-human). Maps to 409.

View Source
var ErrInteractionNotFound = errors.New("runview: interaction not found")

ErrInteractionNotFound reports an unknown interaction ID. Maps to 404.

View Source
var ErrNotAwaitingHuman = errors.New("runview: run is not awaiting human input")

ErrNotAwaitingHuman reports answer_human on a run that is not paused on a human gate. Maps to 409.

View Source
var ErrRunNotActive = errors.New("runview: run is not active in this process")

ErrRunNotActive is returned when a manager operation references a run ID that has no in-process handle (either it was never launched in this process or it has already terminated).

View Source
var ErrRunNotHeld = errors.New("runview: run is not held by this process")

ErrRunNotHeld reports a steering command for a run this process does not hold (a runner-pod or dispatcher-owned run, or simply not running). Maps to 409.

View Source
var ErrSteerPending = errors.New("runview: steering command queued; the run is busy in a long node and will apply it at its next boundary")

ErrSteerPending reports a command that was DELIVERED to the live engine but not acknowledged within steerAckTimeout (the run is busy inside a long node execution). The command stays queued and applies at the next safe boundary; it is not lost. Maps to 202-style handling upstream.

Functions

func AwaitSubbotTerminal added in v0.50.0

func AwaitSubbotTerminal(ctx context.Context, rs store.RunStore, childRunID string, logger *iterlog.Logger) (map[string]any, error)

AwaitSubbotTerminal parks a subbot node whose in-process child engine returned a pause (human gate / operator pause) until the child run reaches a terminal state, then reconstructs the child's terminal output from the store. The child is resumed EXTERNALLY — the pipeline-board sidebar answer or `iterion resume --run-id <child>` claims the paused run doc and drives a fresh engine to completion in another goroutine/process; this waiter only observes the store.

Terminal semantics: finished → output; failed / failed_resumable / cancelled → error (the parent branch fails; resuming the PARENT re-runs the subbot node with a fresh child). A child that pauses again after a resume (several human gates) simply keeps this waiter parked — each new pause surfaces on the pipeline board like the first.

Shared by the studio's in-process runner (subbotRunnerFor) and pkg/cli's subbotRunnerForCLI so both surfaces behave identically.

func BuildExecutor

func BuildExecutor(spec ExecutorSpec) (*model.ClawExecutor, error)

func CompileBundleWorkflow added in v0.39.0

func CompileBundleWorkflow(path string, b *bundle.Bundle) (*ir.Workflow, string, error)

CompileBundleWorkflow is CompileWorkflowWithHash specialised for bundle inputs: it merges the bundle's prompts/*.md into the AST File before compilation, so node-level prompt references resolve against bundle resources during static validation.

func CompileWorkflow

func CompileWorkflow(path string) (*ir.Workflow, error)

CompileWorkflow parses and compiles a workflow source file at path. It returns the compiled workflow or an error with the first parse / compile diagnostic encountered.

MCP server resolution is finalised against the file's directory so relative `command` paths in `mcp_server` blocks resolve correctly.

func CompileWorkflowFromSource added in v0.5.0

func CompileWorkflowFromSource(path, source string) (*ir.Workflow, string, error)

CompileWorkflowFromSource is the cloud-mode entry point: the workflow content is supplied verbatim (uploaded by the studio SPA). Path is retained as a logical label for diagnostics + MCP relative-path resolution; when empty, MCP resolution falls back to the current working directory.

func CompileWorkflowWithHash

func CompileWorkflowWithHash(path string) (*ir.Workflow, string, error)

CompileWorkflowWithHash is CompileWorkflow plus a SHA-256 hash of the raw source bytes. The hash is persisted in run.json so that resume can detect when the workflow file has changed under it (and require --force to proceed). Use this everywhere a workflow is loaded for execution; CompileWorkflow is for static-only callers (validate, diagram).

func MCPHealthCheck

func MCPHealthCheck(ctx context.Context, executor runtime.NodeExecutor, servers []string) error

MCPHealthCheck runs the executor's optional MCP health-check implementation. The `iterion run` and `iterion resume` paths invoke this just before eng.Run / eng.Resume so a misconfigured catalog surfaces an error before any node is dispatched.

func MakeExecutionID

func MakeExecutionID(branch, nodeID string, iteration int) string

MakeExecutionID composes a stable ID from (branch, node, iteration). The format is documented in the WS protocol; clients depend on it for tab/anchor URLs and for matching events to executions. Empty branch is normalised to MainBranch.

func MergeBundlePrompts added in v0.39.0

func MergeBundlePrompts(f *ast.File, b *bundle.Bundle) error

MergeBundlePrompts injects every `*.md` file from the bundle's `prompts/` directory into the AST File's Prompts slice, keyed by the file stem (e.g. `prompts/helper.md` → `helper`).

Workflow-declared prompts already in f.Prompts keep precedence — bundle files only fill in names the workflow author did not declare in source. This lets a bundle ship reusable instructions without bloating the .bot while still allowing the workflow to override any name locally.

Operating at the AST level (rather than on the compiled IR) means downstream validation sees the merged prompts and can resolve node-level `system:`/`user:` references against them.

Returns nil when bundle is nil or has no prompts/ directory.

func ParseExecutionID

func ParseExecutionID(id string) (branch, nodeID string, iteration int, err error)

ParseExecutionID is the inverse of MakeExecutionID. It returns the branch, node ID, and iteration. Returns an error if the input is not a well-formed exec ID.

func ResolveBundleFromFilePath added in v0.39.0

func ResolveBundleFromFilePath(filePath string) *bundle.Bundle

ResolveBundleFromFilePath inspects filePath and, when it looks like the canonical entrypoint of a directory bundle (named main.bot, in a parent dir that carries `skills/` or `manifest.yaml`), opens the parent as a bundle so the engine can mirror skills/, recipes/, attachments/ into the workspace at run time. Returns nil when filePath is empty, not the canonical name, the parent has no bundle markers, or OpenDir fails (best-effort).

Mirrors the auto-promotion the CLI does in pkg/cli/run.go (F-NEW-4). Without this, studio launches of `iterion run bots/whats-next/main.bot` silently produce empty `.claude/skills/` and prompts that reference `repo-survey.md` fail with `no such file or directory`.

Types

type AlertSettings added in v0.39.0

type AlertSettings struct {
	// WebhookURL targets a generic incoming webhook (Slack/Discord
	// shape). Empty disables webhook delivery. Treated as a secret.
	WebhookURL string
	// StallTimeout is the no-activity window for stall alerts. Zero or
	// negative disables stall detection; the caller resolves the default.
	StallTimeout time.Duration
	// BaseURL is the origin used to build /runs/<id> deep links.
	BaseURL string
	// DesktopSink, when non-nil, is added as an extra sink — the desktop
	// app injects a Wails EventsEmit sink here for in-window
	// notifications. Nil in headless server / browser-only mode.
	DesktopSink alert.Sink
}

AlertSettings configures the run-health alert Manager the service builds when WithAlerts is supplied. The webhook + desktop sinks are optional; browser delivery (broker → WS toast) is always wired so studio sessions get alerts regardless of these fields.

type AnswerInteractionResult added in v1.1.0

type AnswerInteractionResult struct {
	RunID         string `json:"run_id"`
	InteractionID string `json:"interaction_id"`
	// Queued is true when the answer was queued for mid-run delivery to
	// the asking node (the run keeps executing).
	Queued bool `json:"queued"`
	// Resumed is true when this answer completed the pending set of an
	// await-paused run and the run was auto-resumed.
	Resumed bool `json:"resumed"`
}

AnswerInteractionResult reports what happened to the answer.

type ArtifactSummary

type ArtifactSummary struct {
	Version   int       `json:"version"`
	WrittenAt time.Time `json:"written_at"`
}

ArtifactSummary is the lightweight shape returned by ListArtifacts — just enough for the UI to populate a version selector without reading every artifact body.

type BackendUsage added in v0.44.0

type BackendUsage struct {
	Backend   string `json:"backend"`
	Model     string `json:"model,omitempty"`
	NodeCount int    `json:"node_count"`
}

BackendUsage is one distinct (backend, model) pair a run's LLM / delegate nodes executed against. NodeCount is the number of distinct IR nodes that resolved to this pair, so loop iterations and resume re-executions of the same node do not inflate it. Model is empty when the backend did not report an effective model.

type BumpLoopRequest added in v0.50.0

type BumpLoopRequest struct {
	LoopName string `json:"loop_name"`
	Delta    int    `json:"delta"`
	// IssuedBy names the operator for the persisted run_steered event
	// (filled by the HTTP layer from the authenticated identity).
	IssuedBy string `json:"-"`
}

BumpLoopRequest grants extra iterations to a named loop of a LIVE run.

type BumpLoopResponse added in v0.50.0

type BumpLoopResponse struct {
	RunID        string `json:"run_id"`
	Loop         string `json:"loop"`
	Delta        int    `json:"delta,omitempty"`
	Extra        int    `json:"extra,omitempty"`
	EffectiveMax int    `json:"effective_max,omitempty"`
	Current      int    `json:"current_iteration"`
	Noop         bool   `json:"noop,omitempty"`
	NoopReason   string `json:"noop_reason,omitempty"`
	// Warning surfaces a non-fatal degradation (applied in-memory but
	// not persisted — a resume would lose the grant).
	Warning string `json:"warning,omitempty"`
}

BumpLoopResponse reports the applied grant and effective ceiling.

type BundleSkill added in v0.39.0

type BundleSkill struct {
	// Name is the skill identifier — the SKILL.md basename without
	// extension, OR the `name:` frontmatter field when present.
	Name string `json:"name"`
	// Description is the one-line summary parsed from the SKILL.md
	// frontmatter's `description:` field. Empty when the file has no
	// frontmatter (rare) or the field is absent.
	Description string `json:"description,omitempty"`
	// Path is the skill's path relative to the bundle's skills/
	// directory. For a top-level SKILL.md it's just the filename;
	// for skill directories it's the directory name (the SDK reads
	// `<dir>/SKILL.md` inside).
	Path string `json:"path"`
}

BundleSkill is one entry in a bundle's skill catalog. Mirrors the shape of a parsed SKILL.md frontmatter block plus the relative path inside the bundle's skills/ directory. The studio's chatbox skill picker consumes this list verbatim.

type CommitAndFinalizeResponse added in v0.39.0

type CommitAndFinalizeResponse struct {
	RunID         string              `json:"run_id"`
	FinalCommit   string              `json:"final_commit"`
	FinalBranch   string              `json:"final_branch"`
	MergeStatus   store.MergeStatus   `json:"merge_status"`
	MergedInto    string              `json:"merged_into,omitempty"`
	MergedCommit  string              `json:"merged_commit,omitempty"`
	MergeStrategy store.MergeStrategy `json:"merge_strategy,omitempty"`
	// SourceIssueID is set when the run was dispatcher-spawned. The HTTP
	// handler reads it to fire the merged-issue auto-transition when an
	// auto-FF landed the merge inside finalize (MergeStatus="merged"),
	// without a second LoadRun round-trip. Internal-only — omitted from
	// the JSON wire.
	SourceIssueID string `json:"-"`
}

CommitAndFinalizeResponse echoes the persisted state after a successful commit-and-finalize, so the studio can update its snapshot and pivot to the standard /merge UX without an extra GET.

type DeploymentReport added in v1.1.0

type DeploymentReport struct {
	// NodeID is the IR node that reported the delivery, so the operator
	// can jump to its output. Empty only for hand-built values.
	NodeID string `json:"node_id,omitempty"`
	// Deployed is the reporting node's claim that the deploy applied.
	Deployed bool `json:"deployed"`
	// Healthy is its claim that the deployed URL answers healthily.
	Healthy bool `json:"healthy"`
	// URL is the public address a human opens. Empty on a failed or
	// blocked deploy — a truthful blocker, never a guessed URL.
	URL string `json:"url,omitempty"`
	// ImageRef is the exact image reference running, e.g.
	// "ghcr.io/owner/repo:<sha>". Empty when none was reported.
	ImageRef string `json:"image_ref,omitempty"`
	// Commit is the source commit the delivery is anchored to (the one
	// the image is expected to name). Empty when the reporter did not
	// state it — the studio then shows no commit rather than borrowing
	// the run's final_commit, which can be a LATER commit than the one
	// deployed.
	Commit string `json:"commit,omitempty"`
	// Notes is the reporter's own prose: what was published, or the
	// concrete blocking error.
	Notes string `json:"notes,omitempty"`
	// Trace is the traceability verdict. Nil when no node ran a
	// traceability gate at all — a distinct fact from a gate that ran
	// and could not establish the facts (Trace.Verifiable == false).
	Trace *DeploymentTrace `json:"trace,omitempty"`
}

DeploymentReport is a run's delivery outcome, assembled from the deployment-report output contract (see recordDeployment). It is the run-level answer to "what did this ship, and where can I see it".

Deployed/Healthy are the deploying node's own claims; Trace carries the INDEPENDENT verdict on whether that claim is traceable back to the repository. The two are kept apart on purpose: a URL that answers 200 while nothing was pushed and nothing is reproducible is not a delivery, and the studio must never render the first without the second.

type DeploymentTrace added in v1.1.0

type DeploymentTrace struct {
	NodeID string `json:"node_id,omitempty"`
	// Verifiable reports whether the gate could establish the facts.
	// When false, the three booleans below carry no information.
	Verifiable bool `json:"verifiable"`
	// Pushed: the deployed commits are reachable from a remote branch.
	Pushed bool `json:"pushed"`
	// ImageFromRepo: the running image is published under this repo's
	// own registry path (not a stock base image).
	ImageFromRepo bool `json:"image_from_repo"`
	// BuiltFromHead: the image reference names the pushed commit, which
	// is what ties the running artifact to reviewable source.
	BuiltFromHead bool `json:"built_from_head"`
	// Log is the gate's own explanation — the remedy on a failure, the
	// environment fault when unverifiable.
	Log string `json:"log,omitempty"`
}

DeploymentTrace is the traceability verdict on a delivery: whether the running artifact can be traced back to reviewable source.

Verifiable is the meta-fact and gates the other three: false means the gate could not establish them at all (git unreachable, gate miswired). That is NOT a failed delivery — reading it as one rejects deliveries that are in fact correct — and the studio renders it as its own "unverified" state, never as a failure.

func (*DeploymentTrace) Traceable added in v1.1.0

func (t *DeploymentTrace) Traceable() bool

Traceable reports whether the delivery is fully traced back to the repository. It is only meaningful when Verifiable is true; callers must branch on Verifiable first.

type EventBroker

type EventBroker struct {
	// contains filtered or unexported fields
}

EventBroker is the in-process fan-out for persistent run events. The runtime publishes via runtime.WithEventObserver(broker.Publish); WS handlers subscribe via broker.Subscribe(runID).

The broker does NOT read from disk. Callers that need historical events (catch-up replay) should subscribe through the streaming seam — Service.StreamSource().SubscribeEvents — which does the paginated replay + live-splice dedup (service_stream.go). Direct broker subscriptions only see events published after Subscribe.

func NewEventBroker

func NewEventBroker() *EventBroker

NewEventBroker creates an empty broker.

func (*EventBroker) CloseRun

func (b *EventBroker) CloseRun(runID string)

CloseRun terminates every subscription for runID. Used at run completion to signal that no further events will arrive on this stream. After CloseRun the broker forgets about the run; new Subscribe calls would receive a fresh empty subscription that never fires (callers should use cold reads for terminated runs).

func (*EventBroker) Publish

func (b *EventBroker) Publish(evt store.Event)

Publish fans out an event to every active subscriber for evt.RunID. Safe for concurrent use. This is the function passed to runtime.WithEventObserver. Slow subscribers are dropped lossily — the publisher never blocks (see subscriberBufferSize for the rationale).

The read lock is held through the entire fan-out, including the non-blocking channel sends. This serialises against Cancel and CloseRun (which take the write lock to close subscriber channels), preventing the "send on closed channel" race that would otherwise occur if Cancel ran between Publish's slice copy and its send. Because every send is wrapped in a `select { default }`, the read lock is held only briefly even under buffer pressure.

Publish takes a value to match the runtime.WithEventObserver signature; we copy the pointer-shaped fields on the way out so downstream consumers receive a stable snapshot of the event even if the runtime mutated its local copy after emit.

func (*EventBroker) Subscribe

func (b *EventBroker) Subscribe(runID string) *EventSubscription

Subscribe registers a new subscriber for runID and returns its handle. The subscription only delivers events received AFTER this call returns; for catch-up + live replay use the streaming seam (Service.StreamSource().SubscribeEvents).

func (*EventBroker) SubscriberCount

func (b *EventBroker) SubscriberCount(runID string) int

SubscriberCount returns the live subscriber count for runID. Useful for tests and metrics.

type EventSubscription

type EventSubscription struct {
	C <-chan *store.Event
	// contains filtered or unexported fields
}

EventSubscription is the public handle returned to callers. Receive from C, call Cancel to unsubscribe (idempotent).

func (*EventSubscription) Cancel

func (s *EventSubscription) Cancel()

Cancel unregisters the subscription and closes the channel. Idempotent — safe to call multiple times.

func (*EventSubscription) Drops

func (s *EventSubscription) Drops() int

Drops reports the number of events that were dropped because this subscriber's buffer was full. The transport layer can surface this to the client so the UI knows to re-subscribe with from_seq.

type ExecStatus

type ExecStatus string

ExecStatus is the lifecycle state of a single execution (one branch × one loop iteration of an IR node).

const (
	ExecStatusRunning  ExecStatus = "running"
	ExecStatusFinished ExecStatus = "finished"
	ExecStatusFailed   ExecStatus = "failed"
	ExecStatusPaused   ExecStatus = "paused_waiting_human"
	ExecStatusSkipped  ExecStatus = "skipped"
)

type ExecutionState

type ExecutionState struct {
	ExecutionID         string     `json:"execution_id"`
	IRNodeID            string     `json:"ir_node_id"`
	BranchID            string     `json:"branch_id"`
	LoopIteration       int        `json:"loop_iteration"`
	Status              ExecStatus `json:"status"`
	Kind                string     `json:"kind,omitempty"` // node kind (Agent / Judge / Router / ...)
	StartedAt           *time.Time `json:"started_at,omitempty"`
	FinishedAt          *time.Time `json:"finished_at,omitempty"`
	LastArtifactVersion *int       `json:"last_artifact_version,omitempty"`
	CurrentEventSeq     int64      `json:"current_event_seq"`
	Error               string     `json:"error,omitempty"`
	// FirstSeq / LastSeq mark the persisted event range that produced
	// this execution row, allowing clients to scrub directly to the
	// segment of events.jsonl describing this execution.
	FirstSeq int64 `json:"first_seq"`
	LastSeq  int64 `json:"last_seq"`
}

ExecutionState is one rendered row in the dynamic execution graph: a concrete invocation of an IR node within a specific branch and loop iteration. The same IR node may appear N times across branches and loop iterations — each gets its own ExecutionState with a distinct ExecutionID.

type ExecutorSpec

type ExecutorSpec struct {
	// Ctx is captured by the store-backed event hooks for AppendEvent
	// calls during execution. Filesystem stores ignore it; cloud
	// (Mongo) stores honor it for cancellation/timeout.
	Ctx      context.Context
	Workflow *ir.Workflow
	Vars     map[string]string
	Store    model.EventEmitter // typically *store.RunStore
	RunID    string
	Logger   *iterlog.Logger
	StoreDir string
	// SourceIssueID is the ticket that owns this run (dispatcher / pipeline
	// launch). Empty for ad-hoc runs. Used to auto-stamp parent_id when the
	// bot creates child tickets via board.create.
	SourceIssueID string
	// ExtraHooks are merged into the store-backed event hooks. Pass
	// the prometheus exporter's hooks here (cli does this); the HTTP
	// service can pass nothing or a future broker-side hook chain.
	ExtraHooks []model.EventHooks
	// EventObservers (ADR-046) fire on every event the backend-hook
	// layer persists — the stall-heartbeat seam for high-frequency tool
	// events that bypass the runtime engine's WithEventObserver. The
	// dispatcher-via-service path sets these so it can observe the same
	// store-level event stream the direct path's heartbeatStore saw,
	// WITHOUT wrapping the store (which would shadow its optional
	// capabilities — PlanWriter / RunFilesStore / … — against the probes
	// below). Engine-level events reach the same observers through
	// runtime.WithEventObserver; the two event sets are disjoint.
	EventObservers []func(store.Event)
	// Inbox, when non-nil, wires the operator chatbox plumbing into
	// the claw backend so queued messages are delivered between
	// agent-loop iterations. Nil disables the inbox (CLI mode +
	// runs that opted out).
	Inbox model.InboxBinder
	// AsyncAsk, when non-nil, backs the ask_user_async / await_answers
	// tools of interaction: async nodes (ADR-081). Nil = the tools
	// error explicitly if a node resolves them.
	AsyncAsk model.AsyncAskBinder
	// Backend, when non-empty, takes precedence over the workflow's
	// `default_backend:` for this run only. Node-level explicit
	// `backend:` still wins (it's the most specific level in the
	// resolution chain). Used by the studio launch UI to A/B a
	// workflow against different backends without editing the .bot.
	Backend string
	// ModelOverrides are launch-time per-node/-group backend+model+provider
	// overrides (studio Launch dropdowns, CLI --model/--backend selector=spec).
	// Unlike Backend (a run-level default below node DSL), these target nodes
	// explicitly and win over the node's DSL backend:/model:, so a run can
	// re-target the bot per node-group without editing the .bot. Empty = no-op.
	ModelOverrides model.ModelOverrides
	// BotID is the stable bundle/bot identifier used to qualify
	// structured visibility=bot memory. Empty falls back to Workflow.Name.
	BotID string
	// MemoryStore overrides the workspace-memory backend. nil → the
	// local filesystem store. Cloud runners pass the Mongo store so
	// shared knowledge persists in the tenant's document store.
	MemoryStore knowledge.MemoryStore
	// BoardRegister mints a per-node board MCP run token (C082, server
	// path): it registers the node's board caps with the server's token
	// registry and returns the token. nil (CLI) leaves sandboxed
	// board-emit disabled.
	BoardRegister func(caps []string, sourceIssueID string) string
	// Compress is the run-level command-output-compression override ("",
	// "on", "ultra", "off"), forwarded to the executor as the highest-priority
	// input to rewrite.Resolve (above node/workflow DSL and ITERION_COMPRESS).
	Compress string

	// Permission is the run-level tool-permission-gate mode override
	// ("", "off", "ask", "deny"), highest-priority input to the gate's
	// mode precedence (above node/workflow DSL and ITERION_PERMISSION).
	// PermissionAllow/Ask/Deny are run-level rules, additive to the
	// workflow lists. See docs/permissions.md.
	Permission      string
	PermissionAllow []string
	PermissionAsk   []string
	PermissionDeny  []string

	// LocalSecrets + LocalSealer are the local (desktop / CLI / non-cloud
	// studio) secret store and its AES-GCM sealer. When both are set and the
	// ctx does not already carry resolved Credentials (the cloud runner path),
	// BuildExecutor resolves the workflow's declared `secrets:` names from the
	// store and stamps them into ctx via secrets.WithCredentials — the
	// in-process equivalent of the cloud runner's injectCredentials. Nil on
	// the cloud path (credentials arrive pre-resolved in ctx).
	LocalSecrets secrets.GenericSecretStore
	LocalSealer  secrets.Sealer
}

ExecutorSpec carries the inputs required to construct a default ClawExecutor. Splitting the args into a struct keeps cli/run.go and the HTTP service layer in sync as new options accrue (compactor callbacks, recipe overrides, etc.).

type ForkResult added in v0.39.0

type ForkResult struct {
	NewRunID    string            `json:"new_run_id"`
	ParentRunID string            `json:"parent_run_id"`
	ForkAnchor  *store.ForkAnchor `json:"fork_anchor,omitempty"`
}

ForkResult is the response shape returned to HTTP / CLI callers.

type ForkSpec added in v0.39.0

type ForkSpec struct {
	// RunID is the parent run to fork from.
	RunID string
	// NodeID identifies the anchor node (i.e. the node the new run
	// will re-execute from). Required.
	NodeID string
	// TurnIndex selects the per-(node, iter) turn checkpoint to
	// rehydrate from. Negative means "latest available turn for this
	// node". For claude_code the index is always 0 (one TurnCheckpoint
	// per delegate call).
	TurnIndex int
	// RewindCode controls the worktree state of the child run:
	//   - false (default): the child's worktree starts at the parent's
	//     current HEAD (inherit current files).
	//   - true: the child's worktree is reset to the snapshot ref
	//     recorded at (NodeID, iter) — `git reset --hard` semantics
	//     against the per-node snapshot.
	// Phase 2 wires per-node refs; per-turn refs are a Phase 5 polish.
	RewindCode bool
	// ForkName, when non-empty, overrides the auto-generated child
	// name. Cosmetic — the child's id is always a fresh UUID.
	ForkName string
	// NewInputs, when non-nil, replaces the child run's Inputs map
	// (merged onto the parent's). Useful for "fork with a different
	// prompt vars" workflows from the studio's ForkDialog JSON editor.
	NewInputs map[string]any
}

ForkSpec describes a fork request — a "create-an-alternative-future" operation that mints a new run id, copies the parent's persisted state up to (NodeID, TurnIndex), and parks the new run in a resumable state ready for the caller to launch via Resume.

type HandleSnapshot

type HandleSnapshot struct {
	RunID  string
	Cancel context.CancelFunc
	Done   <-chan struct{}
	PID    int // 0 for in-process; >0 for detached subprocess
}

HandleSnapshot is one row in the Snapshot view: the run ID plus the in-memory primitives Drain needs (cancel + done) and the optional PID so callers can distinguish in-process from detached runners.

type LaunchPublisher added in v0.4.0

type LaunchPublisher interface {
	// SubmitLaunch persists the run as queued in the cloud store
	// and publishes a RunMessage. Returns the 1-based queue position
	// at submission time.
	SubmitLaunch(ctx context.Context, runID string, spec LaunchSpec, wf *ir.Workflow, hash string) (int, error)
	// CancelRun signals the runner pool to abort the run. Idempotent —
	// flips the Mongo doc to cancelled regardless of whether a runner
	// is currently holding the lease.
	CancelRun(ctx context.Context, runID string) error
	// SubmitResume republishes a RunMessage with ResumeSpec set so
	// the runner picks the run back up.
	SubmitResume(ctx context.Context, spec ResumeSpec, wf *ir.Workflow, hash string) error
}

LaunchPublisher routes Launch / Resume / Cancel to the cloud queue + Mongo store instead of spawning the runtime in-process. When NewService is called with WithLaunchPublisher, every Launch becomes a "submit + return queue_position"; the runner pool drains the queue separately. Plan §F (T-31, T-32, T-33).

type LaunchResult

type LaunchResult struct {
	RunID string
	// Done is closed when the run goroutine exits (success or
	// failure). Callers that want to wait can `<-result.Done`. Cloud-
	// mode launches return a Done channel that is already closed —
	// the runner pod owns the lifecycle, not this server.
	Done <-chan struct{}
	// QueuePosition is the 1-based position on the cloud queue at
	// the moment of submission. Zero when launching in-process.
	QueuePosition int
}

LaunchResult is returned by Launch on success.

type LaunchSpec

type LaunchSpec struct {
	FilePath string // absolute .bot path; sandbox check is the caller's job
	// Source carries the .bot source verbatim. Used by cloud-mode
	// callers when the server pod has no local copy of the workflow
	// (the studio SPA uploads the source inline). When non-empty it
	// takes precedence over FilePath for parsing; FilePath is still
	// retained for display and for the runner to recompile against
	// the same logical workflow.
	Source string
	Vars   map[string]string // --var-style overrides
	// Preset is the name of an in-source preset (presets: block) to
	// apply before Vars. Unknown name → launch error. Empty means no
	// preset.
	Preset  string
	RunID   string        // optional explicit ID; auto-generated when empty
	Timeout time.Duration // 0 disables
	// MergeInto controls the worktree-finalization fast-forward target
	// for `worktree: auto` runs. "" or "current" → FF the user's
	// currently-checked-out branch (default); "none" → skip FF;
	// <branch-name> → FF that branch (only honoured when it matches
	// the currently-checked-out branch).
	MergeInto string
	// BranchName overrides the default storage branch
	// `iterion/run/<friendly>` created on the worktree's HEAD.
	BranchName string
	// MergeStrategy selects how the run's commits are landed on the
	// merge target: "squash" (default — collapse into one commit) or
	// "merge" (fast-forward, preserve history). Persisted on run.json
	// so the deferred-merge UI can pre-fill the same choice.
	MergeStrategy store.MergeStrategy
	// AutoMerge captures the launch-time intent. When true, the engine
	// applies MergeStrategy synchronously at end of run; when false the
	// engine creates the storage branch only and leaves merge_status
	// pending for the UI to drive via POST /api/runs/{id}/merge.
	AutoMerge bool
	// AttachmentPromote, when set, is invoked after CreateRun and
	// before the engine starts. It is expected to materialise every
	// attachment declared in `attachments:` into the run-scoped store
	// (typically by promoting uploads from a staging area). Errors
	// abort the launch.
	AttachmentPromote runtime.AttachmentPromoteFunc
	// Backend, when non-empty, overrides the workflow's `default_backend:`
	// for this run. Node-level explicit `backend:` declarations still
	// win — this is a soft default override, not a hard force. Empty
	// preserves the resolver chain. NOTE: the detached runner path
	// (ITERION_RUNS_DETACHED=1) does not yet honor this field; the
	// service layer logs a warning and ignores it there.
	Backend string
	// Compress is the run-level command-output-compression override
	// ("", "on", "ultra", "off") from the studio Launch toggle. ""
	// inherits the workflow/node `compress:` DSL then ITERION_COMPRESS. Highest
	// priority input to rewrite.Resolve. See docs/plugins.md.
	Compress string
	// Permission is the run-level tool-permission-gate mode override
	// ("", "off", "ask", "deny") from the studio Launch toggle. ""
	// inherits the workflow/node `permission:` DSL then
	// ITERION_PERMISSION. See docs/permissions.md.
	Permission string
	// ReviewMode is the run-level mono/dual review-topology override
	// ("", "auto", "mono", "dual") from the studio Launch toggle /
	// dispatcher. Only affects bots that declare a review_mode var. ""
	// / "auto" resolves from detected provider credentials at launch;
	// "mono"/"dual" force it. See pkg/reviewtopology.
	ReviewMode string
	// ModelOverrides are launch-time per-node/-group backend+model overrides
	// (studio Launch dropdowns). Each entry targets nodes by selector (node id,
	// id glob, or kind keyword agent|judge) and wins over the node's DSL
	// backend:/model:. Empty applies nothing. Composes with ReviewMode. See
	// model_override.go.
	ModelOverrides []ModelOverrideEntry
	// Budget carries launch-time budget-cap overrides for the workflow's
	// `budget:` block — the HTTP equivalent of the CLI --max-cost-usd /
	// --max-tokens / --max-duration / --max-iterations /
	// --max-parallel-branches flags. Applied to the compiled workflow after
	// recipe/preset resolution and before the executor snapshots Budget
	// (non-zero field wins, zero inherits — see ir.ApplyBudgetOverrides).
	// The detached path forwards it as the CLI flags; the queued cloud path
	// does not support it yet and rejects a non-zero Budget explicitly.
	Budget *ir.BudgetOverrides
	// ParentRunID, ShardIndex, ShardCount, ShardLabel are set when a
	// parent run dispatches this as a shard child (see Cap. 3 in
	// docs/security-bots-distributed.md). The cloudpublisher copies
	// them onto the persisted Run document AND onto the published
	// RunMessage so the runner pod that picks up the work knows it's
	// part of a sharded set.
	ParentRunID string
	ShardIndex  int
	ShardCount  int
	ShardLabel  string
	// CallbackURL, when set, is an http/https endpoint the engine POSTs
	// a run-completion webhook to when the run terminates (see
	// pkg/notify). Lets a programmatic caller (chat adapter, CI bridge)
	// be told the run finished without polling. Empty for CLI / studio.
	CallbackURL string
	// CallbackToken is an opaque value echoed back verbatim in the
	// completion payload so the receiver can correlate the callback to
	// the originating request (e.g. a chat thread id) without state.
	CallbackToken string
	// CallbackAnswerNode optionally names the node whose latest artifact
	// holds the run's user-facing answer (the "final_answer" field).
	// Empty → the notifier scans all artifact nodes for "final_answer".
	CallbackAnswerNode string
	// RepoURL / RepoRef, when set, propagate onto the published
	// RunMessage so a cloud runner clones the repo before sandboxing.
	// Used by webhook-launched runs (inbound MR review) where the
	// operator has no local checkout.
	RepoURL string
	RepoRef string
	// ProjectPath is the stable forge slug ("group/project") the run
	// targets, persisted on the run so the studio can filter/group runs
	// by repository. Set by inbound-webhook launches; empty otherwise.
	ProjectPath string
	// BotID is the bot bundle name (e.g. "review-pr") this run launches.
	// The cloud publisher uses it to resolve bot-secret bindings during
	// credential sealing. Empty for plain .bot launches.
	BotID string
	// KeyOverrides pins a specific BYOK key per LLM provider for this run
	// (provider name → api_key id), overriding the org/user default in
	// secrets.Resolve. Set by webhook launches that carry per-webhook key
	// bindings; empty for normal launches. See docs/byok.md.
	KeyOverrides map[string]string
	// SecretOverrides pins a stored secret per workflow-secret name (name ->
	// secret id) for this run, overriding the org bot-secret binding. Set by
	// webhook launches carrying per-webhook secret bindings. See docs/byok.md.
	SecretOverrides map[string]string

	// WorkDir overrides the service-level working directory for this launch
	// (runtime.WithWorkDir). The dispatcher sets it to the per-issue isolated
	// worktree so `${PROJECT_DIR}` in bot var defaults expands to that
	// worktree, not the daemon's cwd. Empty inherits WithWorkDir.
	WorkDir string
	// ExtraObservers are per-launch event observers fired on EVERY run
	// event — both the engine-level events runtime.WithEventObserver sees
	// AND the high-frequency tool_started/tool_called events the backend
	// hook layer emits (which bypass the engine callback) — matching the
	// dispatcher's stall-heartbeat semantics. The dispatcher wires one that
	// advances its last-event watermark for stall detection. Empty adds
	// none. Delivered through TWO disjoint seams — runtime.WithEventObserver
	// for engine events + ExecutorSpec.EventObservers for backend-hook
	// events — so no store wrapper is interposed (a wrapper would shadow the
	// store's optional capabilities against the executor/sandbox type-probes;
	// ADR-046).
	ExtraObservers []func(store.Event)
	// DailyCap, when non-nil, overrides the service-level per-(store, UTC-day)
	// spend-cap guard for this launch (runtime.WithDailyCap). The dispatcher
	// builds it from its SINGLETON SpendStore so every concurrent dispatched
	// run writes the one ledger, serialising on a single mutex. Nil inherits
	// the service's dailyCap.
	DailyCap *runtime.DailyCapGuard
	// SourceRef stamps who originated this run onto the run record
	// (runtime.WithSource); the studio RunHeader links back to it. The
	// dispatcher sets it to the kanban issue that triggered the dispatch. Nil
	// leaves Source unset (CLI / studio / fork launches).
	SourceRef *store.RunSource
	// OnOutcome, when set, is invoked once with the run's terminal Go error
	// (nil on success, runtime.ErrRunPaused/ErrRunPausedOperator on a pause,
	// the failure error otherwise) just before the run goroutine closes
	// LaunchResult.Done. It is the return-path completion of the four data
	// fields above: a blocking caller (the dispatcher's EngineRunner routing
	// through this launch authority) reads the SAME typed error the direct
	// engine.Run would have returned, so its retry / park / sandbox-backoff
	// logic stays byte-identical. Nil for fire-and-forget CLI / studio /
	// webhook launches. Not honoured on the cloud-queue or detached paths.
	OnOutcome func(error)
}

LaunchSpec describes a workflow invocation. Mirrors the inputs of `iterion run` but framed as data so HTTP handlers (and any future programmatic caller) construct it without going through cobra flags.

type ListFilter

type ListFilter struct {
	Status   store.RunStatus // exact match
	Workflow string          // exact match on WorkflowName
	Repo     string          // exact match on ProjectPath (cloud repo slug)
	// Bundle filters runs to those whose resolved bundle name (persisted
	// BundleName, falling back to basename(BundlePath) minus ".botz" —
	// see resolveBundleName) matches case-insensitively. Wire name: ?bot=.
	Bundle string
	Since  time.Time // UpdatedAt >= Since
	Limit  int       // 0 = no limit
	// Node filters runs to those whose persisted events include at
	// least one node_started for this IR node ID. Used by the studio
	// to surface "this node was touched by N runs" without scanning
	// every run on the client. Scanning happens at request time —
	// fine for hundreds of runs; wire an inverted index later if the
	// store grows past low thousands.
	Node string
}

ListFilter scopes a List request. Empty fields mean no filter.

type LogSlice

type LogSlice struct {
	BestEffort bool      `json:"best_effort"`
	StartTime  time.Time `json:"start_time"`
	EndTime    time.Time `json:"end_time"`
	StartByte  int64     `json:"start_byte,omitempty"`
	EndByte    int64     `json:"end_byte,omitempty"`
	Truncated  bool      `json:"truncated,omitempty"`
	Notes      []string  `json:"notes,omitempty"`
	Body       string    `json:"body,omitempty"`
}

LogSlice is a best-effort timestamp-windowed slice of run.log for a single ExecutionState. The free-form log format the iterion logger writes (HH:MM:SS in the host's local TZ + emoji + free text) carries no node ID, so the slice is derived from the execution's [StartedAt, FinishedAt] range. Multi-branch concurrent runs may interleave lines from sibling executions in this window — consumers should treat the slice as a hint, not ground truth.

func BuildLogSlice

func BuildLogSlice(storeDir, runID string, exec *ExecutionState, tail int) *LogSlice

BuildLogSlice extracts the timestamp-windowed slice of run.log matching exec. The window is [exec.StartedAt, exec.FinishedAt]; if FinishedAt is nil, the upper bound is open (read to EOF).

The log timestamp prefix is interpreted in time.Local (matching what the iterion logger writes via time.Now().Format("15:04:05")), so the reference date for each line is the local-TZ calendar date of the execution's start. We track day rollovers within the file by detecting backwards jumps in HH:MM:SS (e.g. 23:59:59 → 00:00:00) and incrementing the working date.

When tail > 0, the matched region is kept in a rolling tail buffer (last tail lines) instead of accumulating the full window in memory and trimming at the end. Truncation is reported in the result.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns the lifecycle of in-process workflow goroutines. A run is "active" between Register and Deregister; Cancel signals it to stop; Stop drains every active run on server shutdown.

func NewManager

func NewManager() *Manager

NewManager creates an empty manager.

func (*Manager) Active

func (m *Manager) Active(runID string) bool

Active reports whether a handle exists for runID.

func (*Manager) ActiveRuns

func (m *Manager) ActiveRuns() []string

ActiveRuns returns the IDs of every run currently held by the manager. Order is undefined.

func (*Manager) Cancel

func (m *Manager) Cancel(runID string) error

Cancel signals the engine goroutine for runID to stop. The goroutine observes ctx.Done() and translates it into a checkpoint + RunCancelled event. Returns ErrRunNotActive if no handle exists.

func (*Manager) Deregister

func (m *Manager) Deregister(runID string)

Deregister removes the handle and closes its done channel. Called by the goroutine on its way out, regardless of success/failure. Idempotent.

func (*Manager) PauseSignal added in v0.39.0

func (m *Manager) PauseSignal(runID string) (<-chan struct{}, error)

PauseSignal returns the engine-side receive-only pause channel for runID, suitable to pass into runtime.WithPauseSignal. The caller is expected to wire it into the Engine before the goroutine starts. Returns nil + ErrRunNotActive when no handle exists. The channel is fresh (not closed) at the time Register returns; RequestPause is the only path that closes it.

func (*Manager) Register

func (m *Manager) Register(parent context.Context, runID string) (context.Context, error)

Register installs a new run handle and returns the cancellable ctx the engine goroutine should use. Register MUST be called before spawning the goroutine — otherwise an immediate Cancel could miss the registration and the run would be uncancellable.

The returned ctx inherits from parent so any parent cancellation (e.g. server shutdown) propagates as well.

Returns an error if the manager has been Stop'd or a handle is already registered for runID (defensive — Service.Launch generates IDs that should be unique).

func (*Manager) RegisterDetached

func (m *Manager) RegisterDetached(runID string, pid int, cancel context.CancelFunc, done chan struct{}) error

RegisterDetached installs a handle for a runner running as a detached subprocess (PID > 0). Cancel is the closure that the caller wants invoked when Manager.Cancel(runID) is called — typically `func() { syscall.Kill(-pid, syscall.SIGTERM) }`. The caller is responsible for closing done when the runner exits.

Unlike Register, this method does NOT create a context — detached runners own their own context inside the spawned process, so the server-side handle has no ctx to propagate.

func (*Manager) RequestPause added in v0.39.0

func (m *Manager) RequestPause(runID string) error

RequestPause asks the engine goroutine for runID to interrupt at the next safe boundary. Closing the pause channel is the signal the engine watches via WithPauseSignal — the run then transitions to paused_operator with a preserved checkpoint and can be resumed like a cancelled run. Idempotent (double-call is a no-op); ErrRunNotActive when no handle exists or the handle is a detached runner (which doesn't carry a pause channel in Phase 1).

func (*Manager) Snapshot

func (m *Manager) Snapshot() []HandleSnapshot

Snapshot returns a point-in-time copy of every active handle. Drain uses this to issue cancel + wait without holding the manager's lock across the wait (which would deadlock with concurrent Deregister).

func (*Manager) Stop

func (m *Manager) Stop(ctx context.Context)

Stop cancels every active run and waits for them to drain. Used during server shutdown — we want every goroutine to reach its failRunWithCheckpoint path so the on-disk checkpoint is preserved for resume. After ctx expires, any still-running goroutine is forcibly forgotten (the goroutine itself keeps running but the manager drops its handle); callers should accept that this drops a small amount of in-flight progress in favour of bounded shutdown latency.

func (*Manager) Wait

func (m *Manager) Wait(ctx context.Context, runID string) error

Wait blocks until the goroutine for runID completes, or until ctx is done. Returns ErrRunNotActive immediately if no handle exists.

type MergeConflictsResponse added in v0.39.0

type MergeConflictsResponse struct {
	Files []runtime.ConflictFile `json:"files"`
	// PendingMessage is the squash commit message that was passed to
	// the original merge attempt — preserved so the finalize path can
	// reuse it without recomputing (the user can still override).
	PendingMessage string `json:"pending_message,omitempty"`
	// PendingMergeInto is the target branch the original merge
	// targeted; finalize must run with the same target.
	PendingMergeInto string `json:"pending_merge_into,omitempty"`
}

MergeConflictsResponse is the payload returned by GetMergeConflicts. Files lists each conflicted path with its current worktree content + parsed hunks; Merging signals whether `MERGE_HEAD` / `SQUASH_MSG` indicates an in-progress merge (vs. a stale conflicted file the operator partially resolved before crashing).

type MergeRequest added in v0.4.0

type MergeRequest struct {
	// Strategy is "squash" (default when empty) or "merge".
	Strategy store.MergeStrategy
	// MergeInto is the target branch override:
	//   ""        → currently-checked-out branch (default)
	//   "current" → same as default
	//   <branch>  → that branch (must equal currently-checked-out)
	MergeInto string
	// CommitMessage overrides the squash commit message. Ignored for
	// "merge" strategy. Empty falls back to a generated message that
	// lists each squashed commit.
	CommitMessage string
}

MergeRequest carries the parameters of a UI-driven merge action. The HTTP handler builds it from the request body; the Service translates it into a runtime.PerformDeferredMerge call and persists the outcome.

type MergeResponse added in v0.4.0

type MergeResponse struct {
	MergedCommit  string              `json:"merged_commit"`
	MergedInto    string              `json:"merged_into"`
	MergeStrategy store.MergeStrategy `json:"merge_strategy"`
	MergeStatus   store.MergeStatus   `json:"merge_status"`
	// SourceIssueID is set when the run was dispatcher-spawned (i.e.
	// Run.Source is non-nil). The HTTP handler reads it to fire the
	// post-merge auto-transition without a second LoadRun round-trip.
	// Internal-only — omitted from the JSON wire.
	SourceIssueID string `json:"-"`
}

MergeResponse mirrors the persisted Run fields after a successful merge so the HTTP handler can return them without re-loading.

type ModelOverrideEntry added in v0.39.0

type ModelOverrideEntry struct {
	Selector string `json:"selector"`
	Backend  string `json:"backend,omitempty"`
	Model    string `json:"model,omitempty"`
	Provider string `json:"provider,omitempty"`
}

ModelOverrideEntry is one launch-time per-node/-group model+backend override directive (studio Launch dropdowns). Selector matches a node by exact id, id glob ("reviewer_*"), or kind keyword ("agent"|"judge"). Empty Backend/Model/Provider fields leave that dimension unchanged.

type PipelineConcurrencyStatus added in v0.50.0

type PipelineConcurrencyStatus struct {
	// Enabled is true when a finite cap is configured. When false the
	// other fields are zero and the board's TODO lane only holds
	// not-yet-launched native tasks.
	Enabled bool `json:"enabled"`
	Max     int  `json:"max"`
	Active  int  `json:"active"`
	Waiting int  `json:"waiting"`
}

PipelineConcurrencyStatus is the read-only view of the local pipeline concurrency gate, surfaced in server_info so the studio can render the limit + how many pipelines are waiting.

type QueueMessageOption added in v0.39.0

type QueueMessageOption func(*queueMessageConfig)

QueueMessageOption is the functional-option form of QueueMessage's extras. Today only WithMessageSkills exists; adding more knobs (e.g. attachments, source attribution) won't break existing callers.

func WithMessageInteraction added in v1.1.0

func WithMessageInteraction(interactionID string) QueueMessageOption

WithMessageInteraction marks the queued message as the delivery of an async question's answer (ADR-081), referencing the answered interaction. The await-resume path cancels superseded deliveries by this typed field.

func WithMessageNode added in v0.39.0

func WithMessageNode(nodeID string) QueueMessageOption

WithMessageNode scopes the queued message to a single workflow node: the engine's drain only releases it while that node is the active executing node (see store.QueuedUserMessage.NodeID). A supervisor watching one node tags its steering messages this way so a late message can't leak into the next node. Empty nodeID = run-scoped (the default for operator-typed chatbox messages).

func WithMessageSkills added in v0.39.0

func WithMessageSkills(skills []string) QueueMessageOption

WithMessageSkills attaches bundle skill names to the queued message. Before the engine injects the message into the agent's conversation, each skill's SKILL.md is mirrored into the workspace's .claude/skills/ directory. Sticky — the skill stays loaded for the rest of the run. Empty/nil slice is a no-op.

type RaiseBudgetRequest added in v0.50.0

type RaiseBudgetRequest struct {
	Budget   ir.BudgetOverrides `json:"budget"`
	IssuedBy string             `json:"-"`
}

RaiseBudgetRequest raises the run's budget caps to absolute values.

type RaiseBudgetResponse added in v0.50.0

type RaiseBudgetResponse struct {
	RunID      string         `json:"run_id"`
	Applied    map[string]any `json:"applied,omitempty"`
	Effective  map[string]any `json:"effective,omitempty"`
	Noop       bool           `json:"noop,omitempty"`
	NoopReason string         `json:"noop_reason,omitempty"`
	Warning    string         `json:"warning,omitempty"`
}

RaiseBudgetResponse reports what was applied and the effective caps.

type ResumeSpec

type ResumeSpec struct {
	RunID    string
	FilePath string // .bot file (loaded fresh; must match the run's WorkflowHash unless Force)
	// Source mirrors LaunchSpec.Source: cloud-mode callers can supply
	// the .bot contents inline so the server pod does not need to
	// resolve FilePath against a local filesystem.
	Source  string
	Answers map[string]any // answers for human nodes; ignored for failed_resumable
	Force   bool           // skip workflow hash check
	Timeout time.Duration  // 0 disables
}

ResumeSpec describes a resume request.

type RunArtifactSummary added in v0.39.0

type RunArtifactSummary struct {
	NodeID    string    `json:"node_id"`
	Version   int       `json:"version"`
	Labels    []string  `json:"labels,omitempty"`
	Title     string    `json:"title,omitempty"`
	WrittenAt time.Time `json:"written_at"`
}

RunArtifactSummary describes the latest published artifact for one node, for the studio's centralized, label-grouped artifact view. Title is a short human label derived from the artifact data (a `title`/`name` field) when present, else empty (the studio falls back to the node id).

type RunHeader

type RunHeader struct {
	ID string `json:"id"`
	// Name is the deterministic, human-friendly label for the run.
	// Empty for legacy runs persisted before this field existed.
	Name         string `json:"name,omitempty"`
	WorkflowName string `json:"workflow_name"`
	WorkflowHash string `json:"workflow_hash,omitempty"`
	FilePath     string `json:"file_path,omitempty"`
	// BundleName mirrors the .botz manifest's `name` field captured
	// at launch (e.g. "feature-dev"). The studio RunHeader pairs it
	// with WorkflowName so the operator can distinguish bundles whose
	// internal workflow name was customised. Empty for plain .bot
	// runs.
	BundleName string `json:"bundle_name,omitempty"`
	// BundleDisplayName mirrors the manifest's `display_name` (e.g.
	// "Nexie"). When set, the studio RunHeader leads the bot chip
	// with this persona name + a ✨ icon so dispatcher-spawned runs
	// belonging to a named bot read at a glance.
	BundleDisplayName string          `json:"bundle_display_name,omitempty"`
	Status            store.RunStatus `json:"status"`
	Inputs            map[string]any  `json:"inputs,omitempty"`
	// PermissionMode is the workflow-declared tool-permission gate mode
	// ("off"|"ask"|"deny"); empty when the gate is off/unset. The studio
	// badges ask/deny. See docs/permissions.md.
	PermissionMode string `json:"permission_mode,omitempty"`
	// ModelOverrides are the launch-time per-node/-group model/backend
	// pins captured on the run, surfaced so the studio Overview can show
	// what the run was launched with. Empty when none were set.
	ModelOverrides []store.RunModelOverride `json:"model_overrides,omitempty"`
	// Budget is the effective budget cap set captured at launch (after
	// overrides + cloud ceiling clamp), surfaced so the studio Overview
	// draws budget meters with a denominator. Nil when the workflow
	// declared no budget: block. See store.RunBudget.
	Budget     *store.RunBudget  `json:"budget,omitempty"`
	CreatedAt  time.Time         `json:"created_at"`
	UpdatedAt  time.Time         `json:"updated_at"`
	FinishedAt *time.Time        `json:"finished_at,omitempty"`
	Error      string            `json:"error,omitempty"`
	Checkpoint *store.Checkpoint `json:"checkpoint,omitempty"`
	// WorkDir is the absolute filesystem path the run executed in
	// (per-run worktree when Worktree is true, otherwise inherited cwd).
	// Empty for runs created before this field was persisted; the studio
	// hides the modified-files panel in that case.
	WorkDir string `json:"work_dir,omitempty"`
	// ProjectPath is the stable forge slug ("group/project") the run
	// targets — the cloud run's repo identity (WorkDir is a runner-pod
	// path there). Empty for local and repo-less runs.
	ProjectPath string `json:"project_path,omitempty"`
	// Worktree is true when WorkDir was created by `worktree: auto`.
	Worktree bool `json:"worktree,omitempty"`
	// WorktreeAvailable is true when WorkDir still exists on THIS server's
	// filesystem — i.e. the inline file-editor + uncommitted/live diff
	// surfaces can be served without a 409. It is false for a cloud run
	// (whose worktree lives on the runner pod) and for a finalized/gc'd
	// local run (whose worktree was torn down). The studio gates the
	// Monaco file-editor affordances on it so the operator never clicks an
	// Edit button that then 409s. Mirrors the /files/content endpoint's own
	// gate (resolveRunWorktreePath).
	WorktreeAvailable bool `json:"worktree_available"`
	// Worktree finalization summary (only populated for `worktree:
	// auto` runs that reached a clean exit). The studio uses these to
	// surface the persistent branch and FF status in the run header.
	FinalCommit      string              `json:"final_commit,omitempty"`
	FinalBranch      string              `json:"final_branch,omitempty"`
	FinalBranchError string              `json:"final_branch_error,omitempty"`
	MergedInto       string              `json:"merged_into,omitempty"`
	MergedCommit     string              `json:"merged_commit,omitempty"`
	MergeStrategy    store.MergeStrategy `json:"merge_strategy,omitempty"`
	MergeStatus      store.MergeStatus   `json:"merge_status,omitempty"`
	AutoMerge        bool                `json:"auto_merge,omitempty"`
	// LocAdded / LocDeleted aggregate the three-dot numstat of the
	// run's commits against its fork point (merge-base of the merge
	// target and FinalCommit), computed server-side with a cache.
	// POINTERS on purpose: nil renders "—" (refs unresolvable — branch
	// deleted, commit GC'd), a zero literal renders "0" (diff resolved
	// and empty). Absent for runs without a FinalCommit.
	LocAdded   *int `json:"loc_added,omitempty"`
	LocDeleted *int `json:"loc_deleted,omitempty"`
	// ActiveDurationMs is the wall-clock the run actually consumed —
	// the sum of run_started/resumed → paused/failed/cancelled/
	// interrupted/finished windows derived from events. Excludes time
	// the run sat paused waiting for human input or sat failed_resumable
	// between a crash and a resume.
	ActiveDurationMs int64 `json:"active_duration_ms"`
	// CurrentRunStart anchors the currently-accruing window. Non-nil
	// while the run is actively executing; nil once it pauses, fails,
	// is cancelled, is interrupted, or finishes. The frontend adds
	// (now - CurrentRunStart) to ActiveDurationMs to drive the live
	// timer and freezes the value once this clears.
	CurrentRunStart *time.Time `json:"current_run_start,omitempty"`
	// QueuePosition is the 1-based position of a queued cloud run on
	// the NATS queue. Populated only while Status == "queued"; absent
	// otherwise. Computed server-side (Mongo aggregation in T-17/T-31).
	// The studio's QueuedBanner uses it to render the "3rd in queue"
	// copy. See cloud-ready plan §F (T-03, T-15, T-31).
	QueuePosition int `json:"queue_position,omitempty"`
	// Source records who originated the run (dispatcher → issue back-
	// reference today; CLI / studio launches leave it nil). The studio
	// RunHeader reads it to render a link back to the kanban ticket
	// that triggered the dispatch.
	Source *store.RunSource `json:"source,omitempty"`
	// Run-tree shard tuple (T4b, refs #125): the child←parent edge plus
	// the shard coordinates mirrored from the queue message. ParentRunID
	// points UP at the run that spawned this shard/child; ShardIndex /
	// ShardCount / ShardLabel describe this run's slot in its parent's
	// fan-out. All empty for a top-level (non-sharded) run. The studio
	// projects these to render a run's shard/child subtree.
	ParentRunID string `json:"parent_run_id,omitempty"`
	// ParentNodeID is the IR node id of the subbot node in the parent
	// workflow that spawned this child run; empty for root runs and
	// non-subbot children. See store.Run.ParentNodeID.
	ParentNodeID string `json:"parent_node_id,omitempty"`
	ShardIndex   int    `json:"shard_index,omitempty"`
	ShardCount   int    `json:"shard_count,omitempty"`
	ShardLabel   string `json:"shard_label,omitempty"`
	// WatchedIssueIDs is the server-authoritative set of native-kanban
	// issue IDs this run subscribed to (MVP3b). The studio's whats-next
	// WatchPanel reads it as the primary watch-list source, falling back
	// to its event-derived list for legacy runs that predate the field.
	WatchedIssueIDs []string `json:"watched_issue_ids,omitempty"`
	// BackendsUsed summarizes the distinct (backend, model) pairs the
	// run's LLM/delegate nodes actually executed against, derived by
	// folding node_finished events (each stamps _backend / _model on its
	// output). Auto-detected backends report their RESOLVED value, not
	// "auto". Nil for runs with no LLM nodes (tool/compute-only) so the
	// studio RunHeader renders no chip. See the studio BackendsUsed row.
	BackendsUsed []BackendUsage `json:"backends_used,omitempty"`
	// Loops is the run-level "real loops" indicator: one entry per
	// declared named loop (e.g. "review_loop"), reporting the SEMANTIC
	// iteration counter (matching the runtime's `node#N` log label and
	// review_loop.iteration), NOT the count of node executions — a
	// resume re-runs a mid-loop iteration, so the physical execution
	// count drifts above the true loop counter. Current is the max
	// iteration observed across the loop's node_started events
	// (iteration_path); Max is the declared bound (0 = unbounded /
	// expression cap / unknown). Absent for runs with no named loops.
	Loops map[string]RunLoopProgress `json:"loops,omitempty"`
	// Deployment is the run's delivery outcome — the live URL, the image
	// actually running, and the traceability verdict — folded from any
	// node whose structured output carries the deployment-report keys
	// (see recordDeployment). Nil for the overwhelming majority of runs,
	// which deploy nothing; the studio then renders no deployment row.
	Deployment *DeploymentReport `json:"deployment,omitempty"`
}

RunHeader is the run-level metadata embedded in a snapshot.

type RunLogBuffer

type RunLogBuffer struct {
	// contains filtered or unexported fields
}

RunLogBuffer captures per-run logger output: io.Writer for io.MultiWriter composition, a bounded ring for catch-up replay, fan-out to live subscribers, and an optional file tee for post-mortem inspection. Thread-safe.

func NewRunLogBuffer

func NewRunLogBuffer(filePath string) (*RunLogBuffer, error)

NewRunLogBuffer creates an empty buffer. Pass filePath to also persist writes to disk; pass "" to keep the buffer purely in-memory. The returned buffer is always usable; fileErr is non-nil when the optional disk persistence couldn't be set up — callers should log a warning and proceed (the in-memory ring is the source of truth for WS subscribers; the file is best-effort post-mortem).

O_APPEND so a resumed run extends the existing file rather than truncating it.

func (*RunLogBuffer) Close

func (b *RunLogBuffer) Close()

Close terminates all live subscriptions and closes the persisted file. Late writes from goroutines racing run completion are silently dropped to avoid surfacing benign races as errors.

func (*RunLogBuffer) Snapshot

func (b *RunLogBuffer) Snapshot(from int64) (offset int64, data []byte, total int64)

Snapshot returns the bytes available with offset >= from. The first returned int is the actual offset of the returned bytes (== from when from is in-range, == b.start when from is older than the retained tail). The second return is total bytes ever written, so callers can detect whether more will arrive.

Pass from=0 for "everything we still have".

func (*RunLogBuffer) Subscribe

func (b *RunLogBuffer) Subscribe() *RunLogSubscription

Subscribe registers a live consumer. Callers should Snapshot(from) historical bytes BEFORE Subscribe to avoid racing the next Write, then drain live chunks dropping any whose offset overlaps the snapshot tail.

func (*RunLogBuffer) Total

func (b *RunLogBuffer) Total() int64

Total returns the total bytes ever written.

func (*RunLogBuffer) Write

func (b *RunLogBuffer) Write(p []byte) (int, error)

Write implements io.Writer.

io.Writer's contract permits the caller to reuse p after Write returns, so we copy bytes before retaining them — the ring (via append) and the subscriber channel each get their own copy.

type RunLogSubscription

type RunLogSubscription struct {
	C <-chan runLogChunk
	// contains filtered or unexported fields
}

RunLogSubscription mirrors EventSubscription: receive on C, call Cancel to unregister. Drops counts chunks lost to a full buffer; clients that see drops > 0 should re-anchor via the REST endpoint.

func (*RunLogSubscription) Cancel

func (s *RunLogSubscription) Cancel()

Cancel unregisters the subscription. Idempotent.

func (*RunLogSubscription) Drops

func (s *RunLogSubscription) Drops() int

type RunLoopProgress added in v0.39.0

type RunLoopProgress struct {
	Current int `json:"current"`
	Max     int `json:"max,omitempty"`
}

RunLoopProgress reports a named loop's semantic progress at the run level: the current iteration counter and its declared bound.

type RunSnapshot

type RunSnapshot struct {
	Run        RunHeader        `json:"run"`
	Executions []ExecutionState `json:"executions"`
	LastSeq    int64            `json:"last_seq"`
}

RunSnapshot is the structured view returned by GET /api/runs/{id} and pushed to WS subscribers on connect. It bundles a RunHeader (slowly- changing run-level metadata) with the dynamic ExecutionState rows derived by folding the run's events.

func BuildSnapshot

func BuildSnapshot(ctx context.Context, s store.RunStore, runID string) (*RunSnapshot, error)

BuildSnapshot is the cold-read convenience: load run.json + events from the store, then fold them into a RunSnapshot. Events are streamed via ScanEvents to keep memory bounded for long runs.

type RunSummary

type RunSummary struct {
	ID string `json:"id"`
	// Name is the deterministic, human-friendly label for the run.
	// Empty for legacy runs persisted before this field existed.
	Name         string `json:"name,omitempty"`
	WorkflowName string `json:"workflow_name"`
	// BundleName is the bot/bundle label (e.g. "docs-refresh"). Sourced
	// from the persisted Run.BundleName; falls back server-side to
	// basename(BundlePath) (stripped of `.botz`) for legacy runs.
	// Empty for plain .bot runs with no bundle.
	BundleName string `json:"bundle_name,omitempty"`
	// BundleDisplayName is the bot's persona label (e.g. "Nexie") from
	// the bundle manifest. Empty for plain runs; the studio falls back
	// to BundleName then WorkflowName. Carried so the run list can
	// render readable bot-filter chips without a per-run fetch.
	BundleDisplayName string `json:"bundle_display_name,omitempty"`
	// SourceKind classifies how the run was triggered, for list filtering /
	// grouping: "manual" | "webhook" | "dispatcher" | "fork" | "shard".
	// Derived server-side from the run's source/owner; never persisted.
	SourceKind string          `json:"source_kind,omitempty"`
	Status     store.RunStatus `json:"status"`
	FilePath   string          `json:"file_path,omitempty"`
	CreatedAt  time.Time       `json:"created_at"`
	UpdatedAt  time.Time       `json:"updated_at"`
	FinishedAt *time.Time      `json:"finished_at,omitempty"`
	Error      string          `json:"error,omitempty"`
	// Active reports whether the run is currently held by this
	// process's manager. A run with status "running" but Active=false
	// belongs to another process or to a previous boot — Cancel won't
	// reach it from here.
	Active bool `json:"active"`
	// Worktree finalization summary (only populated for `worktree:
	// auto` runs that reached a clean exit). See store.Run for the
	// full semantics.
	FinalCommit      string              `json:"final_commit,omitempty"`
	FinalBranch      string              `json:"final_branch,omitempty"`
	FinalBranchError string              `json:"final_branch_error,omitempty"`
	MergedInto       string              `json:"merged_into,omitempty"`
	MergedCommit     string              `json:"merged_commit,omitempty"`
	MergeStrategy    store.MergeStrategy `json:"merge_strategy,omitempty"`
	MergeStatus      store.MergeStatus   `json:"merge_status,omitempty"`
	AutoMerge        bool                `json:"auto_merge,omitempty"`
	// WorkDir / RepoRoot locate where a local run executed: WorkDir is
	// the absolute exec dir (worktree path or inherited cwd), RepoRoot
	// the main git repo it was forked from. The studio uses these to
	// offer a "by folder" run filter in desktop/local mode. Empty for
	// cloud runs (which carry ProjectPath instead) and legacy runs.
	WorkDir  string `json:"work_dir,omitempty"`
	RepoRoot string `json:"repo_root,omitempty"`
	// ProjectPath is the stable forge slug ("group/project") for
	// cloud webhook-launched runs, powering the "by repo" filter.
	// Empty in local mode.
	ProjectPath string `json:"project_path,omitempty"`
	// QueuePosition is set only for cloud-mode runs whose Status is
	// "queued"; nil otherwise. 1 means "next to be picked up". Computed
	// by the server (Mongo aggregation), not persisted on the run doc.
	// See cloud-ready plan §F (T-03, T-31).
	QueuePosition *int `json:"queue_position,omitempty"`
	// Run-tree shard tuple (T4b, refs #125): the child←parent edge plus
	// the shard coordinates mirrored from the queue message. Empty for a
	// top-level (non-sharded) run. Carried so the run list / children
	// endpoint can project a run's shard/child subtree without a per-run
	// fetch. See store.Run.
	ParentRunID string `json:"parent_run_id,omitempty"`
	// ParentNodeID is the IR node id of the subbot node in the parent
	// workflow that spawned this child run; empty for root runs and
	// non-subbot children. See store.Run.ParentNodeID.
	ParentNodeID string `json:"parent_node_id,omitempty"`
	ShardIndex   int    `json:"shard_index,omitempty"`
	ShardCount   int    `json:"shard_count,omitempty"`
	ShardLabel   string `json:"shard_label,omitempty"`
}

RunSummary is the lightweight per-row shape returned by List. Heavier fields (events, artifacts, checkpoint detail) live in RunSnapshot — call Snapshot for the full view.

func BuildChildrenFromStore added in v0.50.0

func BuildChildrenFromStore(ctx context.Context, s store.RunStore, parentRunID string) ([]RunSummary, error)

BuildChildrenFromStore returns the shard/child subtree of a run read directly off an arbitrary store — the cross-store (?store=) counterpart to Service.ListChildren. Runs owned by another daemon are never active in this process, so their summaries carry Active=false. A child that fails to load is skipped rather than failing the whole listing.

type RunTerminalError added in v0.50.0

type RunTerminalError struct{ Status store.RunStatus }

RunTerminalError reports steering on a run that already ended.

func (*RunTerminalError) Error added in v0.50.0

func (e *RunTerminalError) Error() string

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the canonical façade over runtime + store + broker + manager. The HTTP server, the studio, and (optionally) the CLI all route through here — keeping a single source of truth for run lifecycle, validation, and event fan-out.

func NewService

func NewService(storeDir string, opts ...ServiceOption) (*Service, error)

NewService constructs a Service rooted at storeDir. When the caller wires WithStore, storeDir may be "" — the service uses the injected store directly without resolving a filesystem path.

func (*Service) AbortMergeConflict added in v0.39.0

func (s *Service) AbortMergeConflict(ctx context.Context, runID string) error

AbortMergeConflict discards the in-progress squash merge: runs `git reset --merge` on the repo root and flips merge_status back to "failed" so the operator can decide what to do next.

func (*Service) Active added in v0.39.0

func (s *Service) Active(runID string) bool

Active reports whether runID is being produced in-process by this service's lifecycle manager (a studio / CLI Launch). Dispatcher- spawned runs are not registered with the manager and return false — which is exactly the signal ensureEventSource keys off.

func (*Service) AddRunNoteCtx added in v0.48.0

func (s *Service) AddRunNoteCtx(ctx context.Context, runID, author, body string) (store.RunNote, error)

AddRunNoteCtx appends a freeform operator note (author + body) to the run and returns the persisted note with its seq + timestamp populated. author may be empty (the handler defaults it from the caller identity). Returns an error when the store doesn't back the note seam so the caller can surface a clear "not supported" rather than silently dropping the note.

func (*Service) AlertManager added in v0.39.0

func (s *Service) AlertManager() *alert.Manager

AlertManager returns the service's alert Manager, or nil when alerts are disabled. Exposed for tests + shutdown.

func (*Service) AnswerHumanCtx added in v0.50.0

func (s *Service) AnswerHumanCtx(ctx context.Context, runID string, answers map[string]any) (*LaunchResult, error)

AnswerHumanCtx pre-fills answers on a paused_waiting_human run and resumes it. NOT live steering: the engine goroutine exited at the pause, so the correct primitive is the existing Resume with answers — this is its convenience wrapper (the run's own FilePath, no force).

func (*Service) AnswerInteractionCtx added in v1.1.0

func (s *Service) AnswerInteractionCtx(ctx context.Context, runID, interactionID, answer string) (*AnswerInteractionResult, error)

AnswerInteractionCtx records the operator's answer on a pending async interaction, delivers it to the asking node's message queue, wakes any parked await_answers node, and — when the run is paused on an await_answers escalation whose pending set is now fully answered — auto-resumes the run.

answer is the operator's reply text (option id or free text).

func (*Service) Broker

func (s *Service) Broker() *EventBroker

Broker exposes the event broker for transports that need to subscribe directly (the WS handler).

func (*Service) BumpLoopCtx added in v0.50.0

func (s *Service) BumpLoopCtx(ctx context.Context, runID string, req BumpLoopRequest) (*BumpLoopResponse, error)

BumpLoopCtx grants req.Delta extra iterations to loop req.LoopName on a live run. Local-first: the in-process engine wins; a cloud publisher that implements runSteerer is the cross-process fallback.

func (*Service) Cancel

func (s *Service) Cancel(runID string) error

Cancel signals an active run to stop. Returns ErrRunNotActive if the run is not held by this process — cross-process cancel is not supported in the current design.

func (*Service) CancelInactive added in v0.39.0

func (s *Service) CancelInactive(runID string) (bool, error)

CancelInactive flips a persisted-but-not-active run to cancelled status when the operator clicked Cancel on a paused_waiting_human or failed_resumable run. Returns (cancelled, error): cancelled=true means the status was actually flipped; false+nil means the run was already terminal (no-op). Cross-process cancel of a held run is still not supported — this only handles the case where no goroutine owns it.

After flipping, RecoverFinalize fires so the studio's merge UI can act on whatever commits the run produced before it stalled (counterpart to the post-cancel finalize in spawnRun).

func (*Service) CancelInactiveCtx added in v0.39.0

func (s *Service) CancelInactiveCtx(ctx context.Context, runID string) (bool, error)

CancelInactiveCtx is the tenant-aware variant of CancelInactive.

func (*Service) CancelQueuedMessage added in v0.39.0

func (s *Service) CancelQueuedMessage(ctx context.Context, runID, msgID string) error

CancelQueuedMessage marks a queued (not-yet-delivered) message as cancelled. Returns store.ErrQueuedMessageNotFound or store.ErrQueuedMessageStatusConflict (already-delivered) so the HTTP handler can map them to 404 / 409 respectively.

func (*Service) CommitAndFinalizeCtx added in v0.39.0

func (s *Service) CommitAndFinalizeCtx(ctx context.Context, runID, message string) (*CommitAndFinalizeResponse, error)

CommitAndFinalizeCtx commits a run's uncommitted workdir changes with the operator-supplied message, then promotes the new HEAD onto a persistent branch via the standard finalize path. The resulting state is identical to a clean bot-side commit + run completion, so the existing /merge endpoint takes over from there.

Preconditions:

  • run must be a worktree run (worktree=true on run.json).
  • run.FinalBranch must be empty (already-finalized runs go through /merge directly).
  • workdir must be dirty (no-op rejected; the operator should use /merge if the run finalized cleanly).

Errors propagate as-is — handlers translate them to 409 for the expected guard-rejection cases.

func (*Service) DailyCap added in v0.39.0

func (s *Service) DailyCap() *runtime.DailyCapGuard

DailyCap returns the service's shared spend-cap guard, or nil when the daily cap is disabled. The HTTP layer uses it to read status and apply per-day overrides.

func (*Service) DeleteRunCtx added in v0.39.0

func (s *Service) DeleteRunCtx(ctx context.Context, runID string) error

DeleteRunCtx permanently removes a run and all of its data. It LoadRuns first so a run outside the caller's tenant scope surfaces as not-found (a tenant can only delete its own runs); the actual delete is then tenant-scoped by the store as well. Idempotent at the store layer.

func (*Service) Drain

func (s *Service) Drain(ctx context.Context)

Drain performs a graceful shutdown of every active run:

  1. Sets the draining flag so subsequent Launch / Resume return runtime.ErrServerDraining.
  2. Snapshots active handles and cancels each one.
  3. Waits on each handle's done channel up to ctx's deadline.
  4. For every run that was active at the moment of Drain — whether its goroutine exited cleanly within the deadline or not — emits EventRunInterrupted and flips the persisted status to failed_resumable with reason "server drained".

The status flip happens regardless of clean exit so the on-disk state is unambiguous; the runtime's own failure event (typically EventRunFailed with cause "context canceled") may also land in the same events.jsonl, which is acceptable telemetry noise — both events accurately describe what happened.

Drain is intended to be called once during process shutdown. After it returns, the service should not be used to launch new work.

func (*Service) FinalizeMergeAfterConflict added in v0.39.0

func (s *Service) FinalizeMergeAfterConflict(ctx context.Context, runID, messageOverride string) (*MergeResponse, error)

FinalizeMergeAfterConflict commits the squash merge once every conflicted file has been staged. Reuses the pending message stored on the run unless the caller supplies an override. On success the run.json is updated the same way the conflict-free path would.

func (*Service) Fork added in v0.39.0

func (s *Service) Fork(ctx context.Context, spec ForkSpec) (*ForkResult, error)

Fork mints a new run id, copies the parent's persisted state up to (NodeID, TurnIndex), and writes a synthetic Checkpoint anchored there. The child run lands in the `cancelled` status so the existing Resume dispatch can pick it up via resumeFromFailure — the caller (CLI / HTTP) calls Resume separately when ready to actually execute the fork.

Returns ForkResult on success. Errors short-circuit before any persistence so a failed Fork leaves the parent untouched.

func (*Service) GetLogBuffer

func (s *Service) GetLogBuffer(runID string) *RunLogBuffer

GetLogBuffer returns the live log buffer for runID, or nil if the run is not held by this process. Valid only while the run is active; the buffer is Close'd and removed when the run goroutine exits.

func (*Service) GetMergeConflicts added in v0.39.0

func (s *Service) GetMergeConflicts(ctx context.Context, runID string) (*MergeConflictsResponse, error)

GetMergeConflicts inspects the worktree associated with runID and returns the current conflict state. Returns (nil, nil) when the run's merge_status is not "conflicted" — callers should treat that as "no conflicts pending".

func (*Service) GetRunTagsCtx added in v0.49.0

func (s *Service) GetRunTagsCtx(ctx context.Context, runID string) ([]string, error)

GetRunTagsCtx returns the run's operator-assigned tags (filter/group chips shown in the studio run header). Returns an empty slice — never nil — when the store doesn't satisfy RunTagStore or the run has none, so the HTTP surface serves a clean empty list without leaking the backend choice, mirroring ListPlanSnapshotsCtx.

func (*Service) Inject added in v0.39.0

func (s *Service) Inject(ctx context.Context, runID, nodeID, text string) error

Inject enqueues a steering message into runID, scoped to nodeID when non-empty (delivered only while that node is the active executing node). It wraps QueueMessage + WithMessageNode so a supervisor coordinator (pkg/supervise) can drive *Service through the supervise.Injector seam without pkg/supervise importing runview.

func (*Service) Launch

func (s *Service) Launch(parent context.Context, spec LaunchSpec) (*LaunchResult, error)

Launch starts a workflow asynchronously and returns once the run handle has been registered with the manager (i.e. Cancel will work from the moment Launch returns nil error).

The caller is expected to have already validated spec.FilePath against any sandbox / origin policy. The service does not double- check origins — its job is lifecycle, not authentication.

func (*Service) List

func (s *Service) List(f ListFilter) ([]RunSummary, error)

List returns every run in the store filtered by f. The result is sorted by CreatedAt descending (newest first); Limit truncates after sort.

Uses context.Background — does NOT carry caller identity. Cloud HTTP handlers must call ListCtx so the mongo tenant_id filter applies; CLI / system paths (single-tenant) can keep using this.

func (*Service) ListAllArtifacts added in v0.39.0

func (s *Service) ListAllArtifacts(runID string) ([]RunArtifactSummary, error)

ListAllArtifacts enumerates the latest published artifact per node for a run — the data behind the centralized Artifacts view. It walks runs/<id>/artifacts/*/ (filesystem store only; cloud mode returns an empty list, mirroring ListArtifacts) and loads each node's latest version to surface its labels + title. Sorted by node id for stable rendering. Few artifacts per run, so the per-node body read is cheap.

func (*Service) ListArtifactFiles added in v0.39.0

func (s *Service) ListArtifactFiles(runID string) ([]store.RunFileInfo, error)

ListArtifactFiles enumerates the tool-produced files dropped under runs/<id>/artifact_files by in-sandbox tools (write_audit_md, emit_sbom, …). Returns nil when the store doesn't satisfy RunFilesStore (cloud mode) so the HTTP handler can surface an empty list cleanly without leaking the backend choice. Validates the run ID before delegating, mirroring ListArtifacts.

func (*Service) ListArtifactFilesCtx added in v0.39.0

func (s *Service) ListArtifactFilesCtx(ctx context.Context, runID string) ([]store.RunFileInfo, error)

ListArtifactFilesCtx is the tenant-aware variant of ListArtifactFiles.

func (*Service) ListArtifacts

func (s *Service) ListArtifacts(runID, nodeID string) ([]ArtifactSummary, error)

ListArtifacts enumerates the persisted artifacts for one node by reading the artifact directory directly — avoids the O(versions) JSON-decode of the full bodies that LoadArtifact would do just to extract the version number. Returns the versions in ascending order.

func (*Service) ListAttachments added in v0.7.0

func (s *Service) ListAttachments(ctx context.Context, runID string) ([]store.AttachmentRecord, error)

ListAttachments forwards to the underlying RunStore.

func (*Service) ListChildren added in v0.50.0

func (s *Service) ListChildren(ctx context.Context, parentRunID string) ([]RunSummary, error)

ListChildren returns the summaries of every run whose ParentRunID is parentRunID — a run's shard/child subtree (T4b, refs #125), ordered by created_at ascending (the store guarantees the ordering). Propagates the caller's ctx so the mongo tenant filter applies. A run that fails to load is skipped rather than failing the whole listing.

func (*Service) ListCtx added in v0.39.0

func (s *Service) ListCtx(ctx context.Context, f ListFilter) ([]RunSummary, error)

ListCtx is the tenant-aware variant of List: propagates the caller's ctx so mongo's tenant_id filter (stamped by requireAuth via store.WithIdentity) applies to both the ListRuns and per-id LoadRun calls. A cross-tenant caller sees an empty list instead of leaking other tenants' run summaries.

func (*Service) ListPlanSnapshots added in v0.39.0

func (s *Service) ListPlanSnapshots(runID string) ([]store.PlanSnapshot, error)

ListPlanSnapshots returns the chronological plan snapshots captured for a run (agents' TodoWrite/todo_write living TODO lists — filesystem runs/<id>/plans/ or the Mongo run_plans collection). Returns nil when the store doesn't satisfy PlanStore so the HTTP handler surfaces a clean empty list without leaking the backend choice — mirroring ListArtifactFiles. Ascending Seq order (chronological): the sequence shows how the plan evolved.

func (*Service) ListPlanSnapshotsCtx added in v0.39.0

func (s *Service) ListPlanSnapshotsCtx(ctx context.Context, runID string) ([]store.PlanSnapshot, error)

ListPlanSnapshotsCtx is the tenant-aware variant of ListPlanSnapshots.

func (*Service) ListQueuedMessages added in v0.39.0

func (s *Service) ListQueuedMessages(ctx context.Context, runID string) ([]store.QueuedUserMessage, error)

ListQueuedMessages returns every message recorded for the run in FIFO order, regardless of current status. Used by the studio for initial hydration alongside the run snapshot.

func (*Service) ListRunBundleSkills added in v0.39.0

func (s *Service) ListRunBundleSkills(ctx context.Context, runID string) ([]BundleSkill, error)

ListRunBundleSkills enumerates the skill catalog of the bundle backing the given run. Returns an empty slice (no error) when the run has no backing bundle or its bundle has no skills directory.

The bundle for a run is located via the parent run's BundlePath (set at launch). Cloud runs that don't carry a local BundlePath surface an empty catalog — the studio falls back to a "no skills available" badge in that case.

func (*Service) ListRunNotesCtx added in v0.48.0

func (s *Service) ListRunNotesCtx(ctx context.Context, runID string) ([]store.RunNote, error)

ListRunNotesCtx returns the run's freeform operator notes in chronological order (filesystem runs/<id>/notes/ or the Mongo run_notes collection). Returns nil when the store doesn't satisfy RunNoteStore so the HTTP handler surfaces a clean empty list without leaking the backend choice — mirroring ListPlanSnapshotsCtx.

func (*Service) ListRunRecordsCtx added in v0.50.0

func (s *Service) ListRunRecordsCtx(ctx context.Context, f ListFilter) ([]*store.Run, error)

ListRunRecordsCtx returns the persisted runs matching f. It is the tenant-aware, record-level counterpart to ListCtx: the caller's context is propagated to every store operation, including the event scan used by the Node filter. Results are sorted by CreatedAt descending and truncated by Limit after sorting.

A run that cannot be loaded is skipped so one corrupt run document does not break the whole listing. Returning the records directly lets server-side projections consume the same filtered snapshot without listing summaries and loading every run a second time.

func (*Service) LoadArtifact

func (s *Service) LoadArtifact(runID, nodeID string, version int) (*store.Artifact, error)

LoadArtifact returns one persisted artifact body.

Uses context.Background — does NOT carry caller identity. Use LoadArtifactCtx from cloud HTTP handlers so the mongo tenant_id filter applies (cross-tenant LoadArtifact today leaks bodies).

func (*Service) LoadArtifactCtx added in v0.39.0

func (s *Service) LoadArtifactCtx(ctx context.Context, runID, nodeID string, version int) (*store.Artifact, error)

LoadArtifactCtx is the tenant-aware variant of LoadArtifact.

func (*Service) LoadEvents

func (s *Service) LoadEvents(runID string, from, to int64) ([]*store.Event, error)

LoadEvents returns events in [from, to] (inclusive on from, exclusive on to), capped at MaxEventsPerPage. Pass to=0 for "no upper bound". Used by the scrubber to lazy-load segments of a long run.

Streams via store.LoadEventsRange so we never materialise more than the page-cap worth of events at once; callers paginate.

Uses context.Background — does NOT carry caller identity. Use LoadEventsCtx from cloud HTTP/WS handlers.

func (*Service) LoadEventsCtx added in v0.39.0

func (s *Service) LoadEventsCtx(ctx context.Context, runID string, from, to int64) ([]*store.Event, error)

LoadEventsCtx is the tenant-aware variant of LoadEvents.

func (*Service) LoadRun

func (s *Service) LoadRun(runID string) (*store.Run, error)

LoadRun returns the persisted Run metadata for runID.

Uses context.Background — does NOT carry caller identity. Cloud callers that need tenant-scoped lookup (e.g. authorize a WS subscription before upgrading) must use LoadRunCtx.

func (*Service) LoadRunCtx added in v0.39.0

func (s *Service) LoadRunCtx(ctx context.Context, runID string) (*store.Run, error)

LoadRunCtx is the tenant-aware variant of LoadRun: it propagates the caller's ctx so the mongo store applies the tenant_id filter stamped by requireAuth (store.WithIdentity). A cross-tenant ID resolves to not-found instead of leaking the run document.

func (*Service) LoadWireWorkflow

func (s *Service) LoadWireWorkflow(runID string) (*WireWorkflow, error)

LoadWireWorkflow recompiles the .bot source for runID and projects the resulting IR into WireWorkflow shape. Results are memoised by (filePath, content hash) so repeated calls for the same revision don't re-parse. The stale_hash flag is derived by comparing the freshly-computed hash against the one persisted in run.json at launch.

Uses context.Background — does NOT carry caller identity. Cloud HTTP handlers must use LoadWireWorkflowCtx.

func (*Service) LoadWireWorkflowCtx added in v0.39.0

func (s *Service) LoadWireWorkflowCtx(ctx context.Context, runID string) (*WireWorkflow, error)

LoadWireWorkflowCtx is the tenant-aware variant of LoadWireWorkflow.

func (*Service) ObserveRun added in v0.39.0

func (s *Service) ObserveRun(ctx context.Context, runID string) (<-chan *store.Event, func(), error)

ObserveRun streams a run's events for an external observer (the supervisor coordinator): a catch-up replay of everything persisted so far — so a late-attaching observer can reconstruct the currently active node — followed by live events, deduplicated by seq. The returned channel is closed when the run terminates (broker CloseRun) or ctx is cancelled.

The caller MUST invoke release exactly once when done; it cancels the live subscription and releases the on-demand file tailer (started for runs this process did not launch in-process, e.g. a dispatcher- or CLI-spawned run observed from a studio process).

Local broker mode only — cloud event-source mode is out of scope for the supervisor's local attach path and returns a typed error.

func (*Service) OpenArtifactFile added in v0.39.0

func (s *Service) OpenArtifactFile(runID, relPath string) (io.ReadCloser, store.RunFileInfo, error)

OpenArtifactFile streams one tool-produced file from the run's artifact_files area. Path-traversal protection lives in store.OpenRunFile (caller-side defence); the runview wrapper only validates the run-id component and delegates. Returns a nil reader when the store doesn't satisfy RunFilesStore.

func (*Service) OpenArtifactFileCtx added in v0.39.0

func (s *Service) OpenArtifactFileCtx(ctx context.Context, runID, relPath string) (io.ReadCloser, store.RunFileInfo, error)

OpenArtifactFileCtx is the tenant-aware variant of OpenArtifactFile.

func (*Service) OpenAttachment added in v0.7.0

func (s *Service) OpenAttachment(ctx context.Context, runID, name string) (io.ReadCloser, store.AttachmentRecord, error)

OpenAttachment forwards to the underlying RunStore.

func (*Service) Pause added in v0.39.0

func (s *Service) Pause(runID string) error

Pause requests an in-process operator pause for runID — the engine observes the closed pauseCh at the next safe boundary (top of execLoop, between LLM turns inside an agent's loop), saves a checkpoint, flips status to paused_operator, and returns ErrRunPausedOperator. Idempotent.

Returns ErrRunNotActive when no goroutine owns runID. Cloud-mode cross-process pause is out of scope for Phase 1 — the publisher path falls back to ErrRunNotActive (a NATS pause subject is the follow-up).

func (*Service) PendingAsyncInteractions added in v1.1.0

func (s *Service) PendingAsyncInteractions(ctx context.Context, runID string) ([]*store.Interaction, error)

PendingAsyncInteractions lists the run's pending async questions (studio card + CLI inspect surface).

func (*Service) PerformMerge added in v0.4.0

func (s *Service) PerformMerge(runID string, req MergeRequest) (*MergeResponse, error)

PerformMerge runs the deferred merge for runID. Preconditions:

  • run.FinalCommit and run.FinalBranch must be set (the engine must have created the storage branch — runs without commits cannot be merged).
  • run.MergeStatus must not already be "merged" (idempotence; clients that want to redo a merge should explicitly reset state first).

On success, the run.json is updated with the merge outcome and the new state is returned.

func (*Service) PerformMergeCtx added in v0.39.0

func (s *Service) PerformMergeCtx(ctx context.Context, runID string, req MergeRequest) (*MergeResponse, error)

PerformMergeCtx is the tenant-aware variant of PerformMerge.

func (*Service) PipelineConcurrency added in v0.50.0

func (s *Service) PipelineConcurrency() PipelineConcurrencyStatus

PipelineConcurrency returns the current state of the local pipeline concurrency gate (limit, active, waiting) for server_info. A disabled cap reports Enabled=false.

func (*Service) PresignAttachment added in v0.7.0

func (s *Service) PresignAttachment(ctx context.Context, runID, name string, ttl time.Duration) (string, error)

PresignAttachment forwards to the underlying RunStore.

func (*Service) Publish added in v0.39.0

func (s *Service) Publish(_ context.Context, runID string, spec sessionboard.Spec) error

Publish persists an updated Session-board spec for runID. It satisfies the sessionboard.Emitter seam so a curation Coordinator can drive *Service without importing it. The studio picks up the change by refetching the spec as the run's event stream advances (board updates are infrequent by design — the coordinator's cooldown floor).

func (*Service) QueueMessage added in v0.39.0

func (s *Service) QueueMessage(ctx context.Context, runID, text string, opts ...QueueMessageOption) (*store.QueuedUserMessage, error)

QueueMessage appends a new operator chat message to the run's inbox in "queued" status, emits user_message_queued so WS subscribers can update their UI, and returns the persisted record. The engine drains pending messages cooperatively at safe boundaries (between agent-loop iterations for claw, at the next human pause for claude_code / codex) — there is no preemption of the running agent.

func (*Service) RaiseBudgetCtx added in v0.50.0

func (s *Service) RaiseBudgetCtx(ctx context.Context, runID string, req RaiseBudgetRequest) (*RaiseBudgetResponse, error)

RaiseBudgetCtx raises the live run's budget caps (absolute, raise-only). Same local-first/publisher-fallback shape as BumpLoopCtx.

func (*Service) ReadToolBlob added in v0.39.0

func (s *Service) ReadToolBlob(runID, toolUseID, kind string, offset, limit int64) ([]byte, int64, bool, error)

ReadToolBlob streams a slice of a tool's stored I/O body (sidecar blob written by the hooks layer when the call exceeded the inline threshold). offset is the byte offset to start at; limit caps the bytes returned (0 = "all from offset"). Returns the bytes read, the full blob size, eof when offset+len(data) == total, and an error wrapping os.ErrNotExist when the blob doesn't exist.

Returns a clear "unavailable" error when the store doesn't satisfy ToolBlobStore. Both the filesystem and Mongo (cloud) stores satisfy it; for any store that does not, the hooks layer falls back to inline-only persistence, so the studio doesn't issue the fetch.

func (*Service) ReadToolBlobCtx added in v0.39.0

func (s *Service) ReadToolBlobCtx(ctx context.Context, runID, toolUseID, kind string, offset, limit int64) ([]byte, int64, bool, error)

ReadToolBlobCtx is the tenant-aware variant of ReadToolBlob.

func (*Service) RemoveAttachment added in v0.39.0

func (s *Service) RemoveAttachment(ctx context.Context, runID, name string) error

RemoveAttachment forwards to the underlying RunStore. Used by the HTTP layer's transactional rollback in promoteStaged.

func (*Service) RenameRunCtx added in v0.39.0

func (s *Service) RenameRunCtx(ctx context.Context, runID, name string) (*store.Run, error)

RenameRunCtx replaces a run's friendly Name. The run id stays stable; only the human-readable label changes. The store is the source of truth — clients keep their per-runId state and the next snapshot push surfaces the new name.

func (*Service) ResolveAllConflictsWithAgent added in v0.39.0

func (s *Service) ResolveAllConflictsWithAgent(ctx context.Context, runID, model string) (*MergeConflictsResponse, error)

ResolveAllConflictsWithAgent invokes the merge-conflict resolver to produce resolved content for every conflicted file at once via a direct claw LLM call. The model parameter, when non-empty, overrides the detector's pick; format follows claw's "<provider>/<model>" spec.

The actual LLM call lives in conflict_agent.go (separate file so service_control.go doesn't drag in pkg/backend/model). This stub dispatches through resolveAllConflictsWithAgentImpl, which the agent file installs on package init.

func (*Service) ResolveMergeConflictFile added in v0.39.0

func (s *Service) ResolveMergeConflictFile(ctx context.Context, runID, path, content string) error

ResolveMergeConflictFile writes resolved content for one conflicted file and stages it via `git add`. Validates that path is currently in the unmerged set (no arbitrary writes), but tolerates the file being already-staged (idempotent re-resolve).

func (*Service) Resume

func (s *Service) Resume(parent context.Context, spec ResumeSpec) (*LaunchResult, error)

Resume re-enters a paused, failed_resumable, or cancelled run with optional answers. The .bot source must be supplied (and must hash- match the original unless spec.Force).

func (*Service) RunStore added in v0.39.0

func (s *Service) RunStore() store.RunStore

RunStore exposes the underlying store handle for callers that need to drive read-only iteration patterns (ScanEvents over many runs, for instance the /runs/stats aggregator). Mutators are intentionally gated behind Service methods so the broker / manager bookkeeping stays coherent — call those instead of reaching into the store for writes.

func (*Service) SessionBoard added in v0.39.0

func (s *Service) SessionBoard(runID string) (sessionboard.Spec, error)

SessionBoard returns the persisted Session-board spec for runID, or a zero-value spec when none exists / the store is unavailable. Consumed by the REST handler and to seed a resuming coordinator.

func (*Service) SetEventPublisher added in v0.39.0

func (s *Service) SetEventPublisher(p trigger.Publisher)

SetEventPublisher wires the run-completion event source: every terminal run emits a trigger.Event onto p. Called once by the server after the trigger coordinator is up so runs publish onto the same bus the evaluator consumes. Passing nil disables emission. Safe to call before Serve; not safe to call concurrently with active runs (set it once at wiring time).

func (*Service) SetRunTagsCtx added in v0.49.0

func (s *Service) SetRunTagsCtx(ctx context.Context, runID string, tags []string) error

SetRunTagsCtx replaces the run's full tag set. tags must already be normalized (see store.NormalizeTags). Returns a clear "unavailable" error when the store doesn't persist tags — both the filesystem and Mongo stores satisfy RunTagStore, so this only fires for a degenerate store, which the PUT handler maps to a 500.

func (*Service) Snapshot

func (s *Service) Snapshot(runID string) (*RunSnapshot, error)

Snapshot returns the structured RunSnapshot for runID by folding the persisted events through the canonical reducer.

Uses context.Background — does NOT carry caller identity. Use SnapshotCtx from cloud HTTP/WS handlers so the mongo tenant filter applies.

func (*Service) SnapshotCtx added in v0.39.0

func (s *Service) SnapshotCtx(ctx context.Context, runID string) (*RunSnapshot, error)

SnapshotCtx is the tenant-aware variant of Snapshot.

func (*Service) Stop

func (s *Service) Stop(ctx context.Context)

Stop cancels every active run and waits for their goroutines to finish, but does not flip persisted statuses or emit any observability event. Use Stop in tests or for a quiet teardown where the caller takes responsibility for the on-disk state.

Production shutdown should call Drain instead, which additionally publishes EventRunInterrupted and flips each in-flight run to failed_resumable so the next server boot can offer one-click resume.

func (*Service) StoreDir

func (s *Service) StoreDir() string

StoreDir returns the on-disk store directory. Exposed so HTTP handlers can fall back to persisted run.log when the in-memory buffer is gone.

func (*Service) StoreRoot added in v0.7.0

func (s *Service) StoreRoot() string

StoreRoot returns the filesystem root the underlying RunStore operates on, or empty when the store has no filesystem (cloud stores). Used by the upload handlers to materialise a staging directory.

func (*Service) StreamSource added in v0.39.0

func (s *Service) StreamSource() runstream.Source

StreamSource returns the store-agnostic streaming source for the primary store: the injected cloud source when one was wired (WithStreamSource — a construction-time fact), the Service's own broker/buffer-backed filesystem source otherwise.

func (*Service) VerifyAttachmentSignature added in v0.7.0

func (s *Service) VerifyAttachmentSignature(runID, name, exp, sig string) bool

VerifyAttachmentSignature checks an HMAC-signed presign URL when the underlying store implements signature verification (filesystem only — cloud stores rely on AWS SigV4). Returns false on cloud / non-FS stores.

func (*Service) WriteAttachment added in v0.7.0

func (s *Service) WriteAttachment(ctx context.Context, runID string, rec store.AttachmentRecord, body io.Reader) error

WriteAttachment forwards to the underlying RunStore.

type ServiceOption

type ServiceOption func(*Service)

ServiceOption configures a Service at construction time.

func WithAlerts added in v0.39.0

func WithAlerts(set AlertSettings) ServiceOption

WithAlerts enables run-health alerting. The service constructs an alert.Manager, attaches a browser-delivery sink (publishing an in-process `alert` event to the broker), optional webhook + desktop sinks, wires it into the file-event tail, and starts its poll loop.

func WithBoardMCP added in v0.39.0

func WithBoardMCP(handler http.Handler, register func(caps []string, sourceIssueID string) string) ServiceOption

WithBoardMCP wires the board MCP HTTP transport for sandboxed board-capability nodes (C082). handler must serve ONLY the board MCP routes (it is exposed gateway-reachable, token-gated) — never the full server mux. register mints a per-node run token against the server's BoardMCPTokenRegistry. Both are threaded into the engine + executor so sandboxed claude_code can write the operator's board.

func WithLaunchPublisher added in v0.4.0

func WithLaunchPublisher(p LaunchPublisher) ServiceOption

WithLaunchPublisher wires the cloud-mode publisher; when nil the service stays in local-mode (in-process engine).

func WithLocalSecrets added in v0.39.0

func WithLocalSecrets(store secrets.GenericSecretStore, sealer secrets.Sealer) ServiceOption

WithLocalSecrets wires the local (non-cloud) sealed secret store + its AES-GCM sealer so in-process launches resolve the workflow's declared secrets into ctx (BuildExecutor). No-op in cloud mode. Set by the local studio/desktop server wiring from server.Config.GenericSecrets + Sealer.

func WithLogger

func WithLogger(l *iterlog.Logger) ServiceOption

WithLogger sets the logger used for service-level diagnostics.

func WithMaxConcurrentPipelines added in v0.50.0

func WithMaxConcurrentPipelines(n int) ServiceOption

WithMaxConcurrentPipelines caps how many ROOT pipelines run at once on this machine. Over-limit launches wait in a FIFO (surfaced on the pipeline board's TODO lane) and start when a slot frees. 0 (or negative) disables the cap — every launch starts eagerly, as before. The CLI wires it from --max-concurrent-pipelines; a non-positive value leaves the ITERION_MAX_CONCURRENT_PIPELINES env default to apply.

func WithSandboxDefault added in v1.4.0

func WithSandboxDefault(mode string) ServiceOption

WithSandboxDefault sets the global sandbox default injected into every in-process engine this service launches (the studio/dispatch counterpart of `iterion run`'s ITERION_SANDBOX_DEFAULT resolution — product daemons pass runtime.ResolveGlobalSandboxDefault()). Empty keeps the service neutral: workflows without a sandbox: block run unsandboxed, the pre-sandbox-by-default behaviour.

func WithStore added in v0.4.0

func WithStore(s store.RunStore) ServiceOption

WithStore replaces the default filesystem store with a caller- supplied implementation. When set, NewService skips the store auto-discovery and uses the supplied store directly. Used by cloud-mode entry points to inject the Mongo+S3 store. Plan §F (T-19, T-30).

func WithStreamSource added in v0.39.0

func WithStreamSource(src runstream.Source) ServiceOption

WithStreamSource installs an alternative streaming source (typically runstream.MongoSource in cloud mode) so the WS handler streams from change streams instead of the in-process EventBroker / RunLogBuffer machinery, which only sees this process's writes. See ADR-053.

func WithWorkDir added in v0.39.0

func WithWorkDir(dir string) ServiceOption

WithWorkDir sets the working directory the engine should use for `${PROJECT_DIR}` expansion and as the seed for the worktree git-repo lookup. Without this, the engine falls back to os.Getwd() at Run() time, which in the desktop server case is whatever cwd the desktop process was launched from (typically the user's home dir, not the project root). Set this to the same directory the host server's WorkDir was configured with.

type SnapshotBuilder

type SnapshotBuilder struct {
	// contains filtered or unexported fields
}

func NewSnapshotBuilder

func NewSnapshotBuilder(run *store.Run) *SnapshotBuilder

NewSnapshotBuilder seeds a builder from the persisted Run metadata. Pass run=nil for an empty initial snapshot (e.g. when the WS catch-up races run.json creation).

func (*SnapshotBuilder) Apply

func (b *SnapshotBuilder) Apply(evt *store.Event)

Apply folds a single event into the running snapshot. Events MUST be applied in non-decreasing seq order; out-of-order events are ignored (the reducer is monotonic — re-applying a stale event would not produce a deterministic state).

func (*SnapshotBuilder) SetRun

func (b *SnapshotBuilder) SetRun(run *store.Run)

SetRun refreshes the run-level header. Call this when a fresh run.json was just persisted (e.g. on terminal events). The event-derived timer fields (ActiveDurationMs, CurrentRunStart) are preserved across the refresh — run.json carries CreatedAt/FinishedAt but not the per-window accumulation, so we keep what events have already taught us.

func (*SnapshotBuilder) Snapshot

func (b *SnapshotBuilder) Snapshot() *RunSnapshot

Snapshot returns the current snapshot. Callers receive a fresh value (the slice is copied); the underlying ExecutionState pointers are shared but treated as immutable from the caller's side.

type SteerCommand added in v0.50.0

type SteerCommand struct {
	// CommandID is minted by the sender (ULID/UUID); the runner dedups
	// on it so a retried publish cannot double-apply.
	CommandID string              `json:"command_id"`
	Kind      SteerCommandKind    `json:"kind"`
	LoopName  string              `json:"loop_name,omitempty"`
	Delta     int                 `json:"delta,omitempty"`
	Budget    *ir.BudgetOverrides `json:"budget,omitempty"`
	IssuedAt  time.Time           `json:"issued_at"`
	// IssuedBy names the operator for the persisted run_steered event.
	IssuedBy string `json:"issued_by,omitempty"`
}

SteerCommand is one steering request in flight to a runner pod.

type SteerCommandKind added in v0.50.0

type SteerCommandKind string

SteerCommandKind discriminates the steering commands on the wire.

const (
	SteerBumpLoop    SteerCommandKind = "bump_loop"
	SteerRaiseBudget SteerCommandKind = "raise_budget"
)

type SteerError added in v0.50.0

type SteerError struct {
	// Code: "unknown_loop" | "invalid" | "no_budget" | "terminal" |
	// "not_active" | "engine_stalled" | "internal".
	Code    string         `json:"code"`
	Message string         `json:"message"`
	Details map[string]any `json:"details,omitempty"`
}

SteerError codes mirror the local typed errors so the HTTP layer maps both paths identically.

func (*SteerError) Error added in v0.50.0

func (e *SteerError) Error() string

type SteerReply added in v0.50.0

type SteerReply struct {
	CommandID  string         `json:"command_id"`
	RunID      string         `json:"run_id"`
	Applied    map[string]any `json:"applied,omitempty"`
	Effective  map[string]any `json:"effective,omitempty"`
	Noop       bool           `json:"noop,omitempty"`
	NoopReason string         `json:"noop_reason,omitempty"`
	Warning    string         `json:"warning,omitempty"`
	Err        *SteerError    `json:"error,omitempty"`
	RunnerID   string         `json:"runner_id,omitempty"`
}

SteerReply is the runner's typed answer — the cross-process carrier of the truthful contract.

type WireEdge

type WireEdge struct {
	From       string `json:"from"`
	To         string `json:"to"`
	Condition  string `json:"condition,omitempty"`
	Negated    bool   `json:"negated,omitempty"`
	Expression string `json:"expression,omitempty"`
	Loop       string `json:"loop,omitempty"`
}

WireEdge mirrors the runtime-relevant fields of ir.Edge. Expression is sent as the original source string (the AST itself isn't useful to the UI, and serializing it would leak compiler internals).

type WireNode

type WireNode struct {
	ID              string            `json:"id"`
	Kind            string            `json:"kind"`
	Description     string            `json:"description,omitempty"`
	Model           string            `json:"model,omitempty"`
	Backend         string            `json:"backend,omitempty"`
	ReasoningEffort string            `json:"reasoning_effort,omitempty"`
	OutputFields    []WireSchemaField `json:"output_schema,omitempty"`
	Source          string            `json:"source,omitempty"`
	Isolated        bool              `json:"isolated,omitempty"`
}

WireNode is the minimal node projection used by the run-console canvas. Model/backend/reasoning_effort are populated for LLM-driving nodes (Agent, Judge, Router-LLM); OutputFields is populated for HumanNode so the run console can render a typed answer form on pause; Source/Isolated are populated for subbot nodes — the child .bot path relative to the parent file, and whether the child is workspace-isolated (parallel-safe).

type WireSchemaField added in v0.39.0

type WireSchemaField struct {
	Name       string   `json:"name"`
	Type       string   `json:"type"`
	EnumValues []string `json:"enum_values,omitempty"`
}

WireSchemaField projects an ir.SchemaField as JSON. Type uses the canonical string form ("string", "bool", "int", "float", "json", "string[]") so the frontend doesn't track ir's iota values.

type WireWorkflow

type WireWorkflow struct {
	Name string `json:"name"`
	// Entry is the first node ID the runtime picks. Useful so the
	// frontend can mark it specially and start its layout from there.
	Entry string     `json:"entry"`
	Nodes []WireNode `json:"nodes"`
	Edges []WireEdge `json:"edges"`
	// StaleHash signals that the .bot source on disk no longer matches
	// the hash captured at run launch. The frontend can warn the user
	// that the IR they are viewing may diverge from what was executed.
	StaleHash bool `json:"stale_hash,omitempty"`
}

WireWorkflow is the JSON projection of an IR workflow used by the studio's "IR overlay" view. Heavier fields (schemas, prompts, vars, MCP config, full expression ASTs) are intentionally omitted — the overlay only needs the topology so it can layer execution counts.

func BuildWireWorkflowFromStore added in v0.39.0

func BuildWireWorkflowFromStore(ctx context.Context, s store.RunStore, runID string) (*WireWorkflow, error)

BuildWireWorkflowFromStore is the store-agnostic projection used by LoadWireWorkflow and by cross-store HTTP handlers that need to surface a run living in a different iterion store than the daemon's primary. The cache argument may be nil for one-shot reads where memoisation isn't worth the lock contention.

Directories

Path Synopsis
Package runstream is the store-agnostic run-streaming seam (ADR-053): one Source per store delivers BOTH the structured event timeline and the raw log bytes of any run — persisted replay first, then live tail, gap-free — so the run-console WS layer never branches on how a run was produced (in-process launch, detached subprocess, external daemon, cross-store, cloud runner pod).
Package runstream is the store-agnostic run-streaming seam (ADR-053): one Source per store delivers BOTH the structured event timeline and the raw log bytes of any run — persisted replay first, then live tail, gap-free — so the run-console WS layer never branches on how a run was produced (in-process launch, detached subprocess, external daemon, cross-store, cloud runner pod).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL