304 lines
14 KiB
Markdown
304 lines
14 KiB
Markdown
# Memind Workflow Orchestrator Boundary
|
|
|
|
## Decision
|
|
|
|
The Orchestrator starts in the Memind repository but is designed as an
|
|
independently deployable bounded context. Source colocation does not authorize
|
|
in-process execution inside Portal.
|
|
|
|
The stable boundary is:
|
|
|
|
```text
|
|
Memind Control Plane
|
|
-> versioned Orchestrator API / events
|
|
Workflow Orchestrator
|
|
-> versioned Executor Gateway
|
|
Executor adapters
|
|
-> Goosed / Aider / OpenHands
|
|
```
|
|
|
|
LangGraph remains an internal workflow engine implementation. Memind must not
|
|
depend on LangGraph graph, checkpoint, command, or interrupt types.
|
|
|
|
## Ownership
|
|
|
|
| Data | Owner |
|
|
|---|---|
|
|
| User, authorization, billing, product run | Memind |
|
|
| Workflow node state, checkpoint, interrupt | Orchestrator |
|
|
| Goosed session | Goosed plus Session Broker mapping |
|
|
| Executor job | Executor Gateway / executor adapter |
|
|
| Workspace and deliverable | Workspace / Artifact / MindSpace services |
|
|
| User-visible progress and audit | Memind run-event projection |
|
|
|
|
The Orchestrator must not read or write Memind user, billing, capability, or
|
|
provider-key tables. A shared PostgreSQL instance is acceptable during the
|
|
single-host phase, but the Orchestrator must use its own database and database
|
|
credentials.
|
|
|
|
## Runtime modes
|
|
|
|
memindadm owns the versioned routing configuration:
|
|
|
|
| Mode | Behavior |
|
|
|---|---|
|
|
| `off` | Native Agent Run only |
|
|
| `shadow` | Native executes; LangGraph observes and compares decisions |
|
|
| `canary` | Explicit users and deterministic percentage may use LangGraph |
|
|
| `active` | Workflow-allowlisted tasks use the primary engine |
|
|
|
|
All modes support an explicit Native fallback. The environment kill switch
|
|
`MEMIND_ORCHESTRATOR_KILL_SWITCH=1` overrides admin configuration and forces
|
|
Native selection.
|
|
|
|
The initial workflow allowlist contains only `code-run-v1`. Ordinary chat and
|
|
existing Goosed session traffic remain outside the Orchestrator path.
|
|
|
|
Portal also has an independent wiring gate:
|
|
`MEMIND_ORCHESTRATOR_SHADOW_OBSERVATION_ENABLED=1`. It is off by default.
|
|
When it is off, Portal does not construct the Shadow observer, query routing
|
|
configuration from the Agent Run path, schedule background observations, or
|
|
send data to the Orchestrator. Changing memindadm mode alone therefore cannot
|
|
put existing Native traffic on the Shadow path.
|
|
|
|
Page Data validation enforcement has a second independent Portal gate:
|
|
`MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED=1`. It is off by
|
|
default and becomes effective only while Shadow wiring is effective. When
|
|
enabled, a Page Data run remains `running` after the Native delivery guards
|
|
finish until the Orchestrator checkpoints a `passed` validation observation.
|
|
`failed`, `inconclusive`, not-selected, timeout, and unavailable outcomes fail
|
|
closed and prevent the run from being marked successful. The Orchestrator still
|
|
does not publish artifacts or receive user content in this mode.
|
|
|
|
memindadm projects the requested and effective wiring states separately. Canary
|
|
readiness requires `shadow_wiring_enabled=true`, so a saved Shadow mode with a
|
|
closed Portal gate is visible and cannot be mistaken for a collecting Shadow
|
|
deployment.
|
|
|
|
Phase 2 implemented only the `off` and `shadow` execution semantics. Canary and
|
|
Active were initially planning-only so an administrative configuration mistake
|
|
could not create two task executors.
|
|
|
|
Phase 3 makes that restriction explicit in the contract. Routing returns both
|
|
`candidateEngine` and `engine`: Canary or Active may nominate LangGraph, while
|
|
`engine` remains Native. The decision is projected as
|
|
`workflow_execution_planned` with `dryRun=true` and `handoffAllowed=false`.
|
|
The execution adapter normalizes authorization, idempotency, timeout,
|
|
cancellation, and fallback controls. Phase 5 can transfer ownership only when
|
|
the memindadm switch, Portal environment gate, Orchestrator environment gate,
|
|
adapter enablement, worker health, authorization, and admission policy all
|
|
agree. Any missing gate keeps Native effective.
|
|
|
|
The Dry-run projection records every code-run decision while the configured
|
|
mode is Shadow, Canary, or Active, including `canary_not_selected`. In Shadow,
|
|
the same allowlist and deterministic percentage are evaluated in parallel with
|
|
the remote observation, so collecting routing evidence does not interrupt
|
|
Shadow readiness samples. This prevents a selected-only dataset from reporting
|
|
a meaningless 100% candidate rate. The Memind control plane joins each decision
|
|
to the Native run terminal state and aggregates candidate rate, selection
|
|
reasons, task types, session coverage, and Native terminal coverage without
|
|
querying LangGraph.
|
|
|
|
## Protocol
|
|
|
|
The framework-neutral contracts are:
|
|
|
|
- `orchestrator-run-v1`
|
|
- `orchestrator-event-v1`
|
|
- `workflow-execution-request-v1`
|
|
- `workflow-execution-decision-v1`
|
|
- `executor-job-request-v1`
|
|
- `executor-job-state-v1`
|
|
- `executor-dispatch-decision-v1`
|
|
- `page-data-validation-observation-v1`
|
|
|
|
The Phase 3.2 Executor Gateway owns the adapter registry and executor job state
|
|
transition boundary. Requests contain workspace/artifact references,
|
|
authorization and side-effect policy, timeout, cancellation, fallback, and an
|
|
idempotency key. The store interface requires atomic create-if-absent semantics
|
|
so retries return the original job and conflicting payloads fail closed.
|
|
|
|
Goosed, Aider, and OpenHands began as disabled `contract-only` adapters. The
|
|
reference in-memory store remains non-durable and is not a deployment backend.
|
|
In the current Phase 5 implementation the dispatch capability exists, but
|
|
execution and every adapter remain disabled by default. Shadow jobs still record
|
|
terminal `blocked` state without launching a process or touching a workspace.
|
|
|
|
Phase 3.3 persists that state in the Orchestrator-owned PostgreSQL database.
|
|
LangGraph checkpoint tables and `executor_jobs` share the isolated
|
|
`memind_orchestrator` schema and credentials, but remain behind separate
|
|
storage interfaces and readiness probes. This is a deployment simplification,
|
|
not a domain ownership leak: neither store can access Memind business tables,
|
|
and the Executor Job Store can move to a separate database without changing
|
|
the graph or the Memind-facing protocol.
|
|
|
|
The `build_plan` node creates the deterministic preview job
|
|
`<runId>:executor-preview` with idempotency key
|
|
`<runId>:build_plan:v1`. The stored result is terminal `blocked` state with
|
|
`executor_dispatch_not_implemented`; it is a durable audit and recovery
|
|
boundary, not an execution claim. If the run identifier is unsafe or would
|
|
overflow the 128-character Job id boundary, the graph substitutes a stable
|
|
SHA-256-derived opaque prefix.
|
|
|
|
Phase 3.4 adds the append-only `executor-job-event-v1` contract. Job snapshots
|
|
remain in `executor_jobs`, while cursor-addressable events live in
|
|
`executor_job_events`. A transaction commits the initial snapshot and event
|
|
together; later state transitions lock the job row and commit the updated
|
|
snapshot and next event together. This prevents a visible state transition
|
|
without its corresponding audit event.
|
|
|
|
The read boundary is deliberately narrower than an execution control API:
|
|
|
|
```text
|
|
GET /v1/executor-jobs/:jobId
|
|
GET /v1/executor-jobs/:jobId/events?after=<cursor>&limit=<limit>
|
|
```
|
|
|
|
Phase 3.4 had no HTTP submit, retry, claim, or executor cancel endpoint.
|
|
Phase 3.5 adds a separately authenticated worker protocol. The control-plane
|
|
read projection still contains no task instruction, secret, absolute path, lease
|
|
token, or executor SDK object; only a successful worker claim receives the
|
|
normalized request and lease token.
|
|
|
|
The implemented internal API is:
|
|
|
|
```text
|
|
POST /v1/runs
|
|
GET /v1/runs/:id
|
|
POST /v1/runs/:id/validation-observations
|
|
POST /v1/runs/:id/resume
|
|
POST /v1/runs/:id/cancel
|
|
DELETE /v1/runs/:id
|
|
POST /v1/maintenance/purge-terminal-runs
|
|
GET /v1/runs/:id/events?after=<cursor>
|
|
GET /v1/executor-jobs/:jobId
|
|
GET /v1/executor-jobs/:jobId/events?after=<cursor>&limit=<limit>
|
|
```
|
|
|
|
External state changes must use an idempotency key derived from run, graph
|
|
version, node, and attempt. Graph state stores resource references and decisions,
|
|
not API keys, large logs, binary artifacts, or absolute production paths.
|
|
|
|
Shadow uses the stricter `control-plane-only-v1` data policy. Portal evaluates
|
|
the rollout locally but sends no user message, user identifier, session
|
|
identifier, provider key, or workspace path to LangGraph. The remote RunSpec
|
|
contains only product run/request identifiers, workflow/task type, configuration
|
|
version, and a fixed non-user instruction marker. Terminal runs can be removed
|
|
through `DELETE /v1/runs/:id`; deletion removes the LangGraph thread and its
|
|
linked terminal Executor Job, whose events cascade in PostgreSQL. Active or
|
|
waiting runs fail deletion with `409`.
|
|
|
|
Page Data code runs may append a post-run
|
|
`page-data-validation-observation-v1` after the Native delivery guards finish.
|
|
The observation contains only bounded check identifiers, pass/fail/skipped
|
|
states, error codes, and aggregate page/publication counts. User prompts, code,
|
|
workspace paths, dataset contents, credentials, and database identifiers must
|
|
not cross this boundary. The Orchestrator derives and checkpoints a
|
|
`passed`/`failed`/`inconclusive` verdict and emits
|
|
`workflow_validation_observed`. In the initial observation phase this verdict
|
|
does not mutate the Native run result or publication state. With the independent
|
|
Page Data validation gate enabled, Portal waits for `passed` before committing
|
|
the Native success transition; the default remains observation-only.
|
|
|
|
The graph keeps three deterministic control-plane nodes:
|
|
|
|
```text
|
|
validate_run -> build_plan -> finalize_run
|
|
```
|
|
|
|
Observe-only runs require `sideEffectsAllowed=false`, project the Native
|
|
boundary, and persist a blocked Executor Job. Explicit active runs require
|
|
`sideEffectsAllowed=true` plus every execution gate; they queue a job and enter
|
|
`waiting` until the worker-owned job reaches a terminal state. LangGraph never
|
|
mounts a workspace or imports an executor SDK.
|
|
|
|
## Phase 5 worker and loop boundary
|
|
|
|
The task loop is split across durable owners:
|
|
|
|
```text
|
|
LangGraph run: validate -> queue -> wait -> project terminal
|
|
Executor Job: queued -> leased -> running -> terminal/retryable
|
|
Worker: claim -> heartbeat -> adapter -> artifact references
|
|
```
|
|
|
|
PostgreSQL row locks and lease tokens fence duplicate claims and late worker
|
|
updates. Expired leases recover to `retryable` until `maxAttempts` is exhausted,
|
|
then become `timed_out`. Worker heartbeats are persisted independently of job
|
|
heartbeats so readiness and autoscaling signals also work while the queue is
|
|
empty.
|
|
|
|
Goosed is a remote HTTP/SSE adapter. Aider and OpenHands are process adapters
|
|
intended to run in separate worker containers with workspace aliases, an
|
|
allowlisted writable root, no shell, bounded environment/output, resource
|
|
limits, and no Docker socket. All adapter outputs cross the boundary as bounded
|
|
events and artifact references.
|
|
|
|
Admission is defense in depth: tenant/user/workspace allowlists, global and
|
|
per-subject concurrency, and per-minute limits are evaluated before queueing.
|
|
The public metrics endpoint exposes queue states, claimable jobs, expired
|
|
leases, worker health, and execution-gate state without task contents.
|
|
|
|
## Failure isolation
|
|
|
|
Portal invokes Shadow only after the product run and its `queued` event have
|
|
been committed. The observation is scheduled without awaiting it. A timeout or
|
|
failure:
|
|
|
|
1. cannot reject or delay `createRun`;
|
|
2. cannot mutate the Native run status;
|
|
3. is projected as `workflow_shadow_failed`;
|
|
4. remains removable by deleting the observer wiring and changing only the
|
|
service URL boundary.
|
|
|
|
The observer uses a bounded in-process dispatcher. Concurrency defaults to `2`
|
|
and the waiting queue to `100`; overflow records `workflow_shadow_skipped` and
|
|
never blocks, rejects, or cancels the Native run. Skipped observations are
|
|
included in the denominator, shown separately in memindadm, and any non-zero
|
|
skip rate blocks Canary readiness.
|
|
|
|
Successful observations are projected as `workflow_shadow_completed`; graph
|
|
checkpoints and blocked Executor Jobs stay in the Orchestrator-owned PostgreSQL
|
|
database.
|
|
|
|
Non-loopback Orchestrator binding requires a service token. Enabling Executor
|
|
Jobs requires both distinct service and worker tokens even on loopback.
|
|
Non-loopback service URLs are rejected by Portal unless their origin is
|
|
declared through `MEMIND_ORCHESTRATOR_URL` or
|
|
`MEMIND_ORCHESTRATOR_ALLOWED_ORIGINS`; loopback origins remain allowed for
|
|
local validation.
|
|
|
|
Terminal retention is independently disabled by default. Setting
|
|
`MEMIND_ORCHESTRATOR_RETENTION_DAYS` to a positive value starts a bounded
|
|
periodic sweep after service startup; it uses the same terminal-only deletion
|
|
path and never deletes waiting/running workflows. The authenticated maintenance
|
|
endpoint defaults to dry-run and requires `apply=true` for mutation.
|
|
|
|
Both terminal projection events contain bounded observation latency. The
|
|
Memind-owned observability service aggregates those events and may join them to
|
|
the product run status. Only per-run detail calls cross the service boundary to
|
|
read LangGraph checkpoint state and node events.
|
|
|
|
Canary readiness is also a Memind control-plane projection. It excludes
|
|
synthetic smoke runs and evaluates operational health, durable checkpoint state,
|
|
durable Executor Job storage, sample volume, session coverage, success rate,
|
|
latency coverage, P95 latency, Native terminal coverage, and sample freshness.
|
|
It also requires the independent Portal Shadow wiring gate to be effective.
|
|
The result is advisory:
|
|
`manual_canary_review` never mutates routing configuration or transfers
|
|
execution ownership.
|
|
|
|
## Deployment evolution
|
|
|
|
1. Local native Orchestrator process with an explicit MemorySaver for debugging.
|
|
2. Colima Compose with an isolated PostgreSQL checkpoint database.
|
|
3. Approved server-side shadow service; no production task claim.
|
|
4. User-allowlisted code-run canary with Native rollback.
|
|
5. Isolated Aider/OpenHands executor workers.
|
|
6. Move the same service contract to Linux or Kubernetes when horizontal scaling
|
|
or independent ownership becomes necessary.
|
|
|
|
The Orchestrator must not share the Goosed Compose lifecycle or mount the Docker
|
|
socket. It calls MindSpace through its service API and calls Goosed through the
|
|
existing proxy boundary.
|