# orca
`orca` is the ecosystem's active orchestration artifact. Its name comes
from **ORChestration Active** and its job is not to transport events, nor to know
concrete modules, nor to replace `bus` . Its responsibility is to run declarative
actions when events arrive from the bus, with ordering, dependencies,
results, failure policies, timers, optional transactions and traceability.
The central idea:
```txt
modules / server / connection
-> bus publishes typed events
-> orca selects registered actions
-> orca runs a pipeline
-> actions return Result and tokens
-> orca decides to continue, abort, skip or finish
```
`orca` does not know that `session` , `cache` , `perm` , `connection` , `storage` or
`http` exist. The application registers actions that call those modules. This
avoids inter-module coupling being hidden inside `bus` , `connection` or
`active-app` .
## Document Status
This README is orca's design reference. **v1 is 100% closed** and the global suite
passes (115 files / 1471 tests, 161 on orca between engine and reactive wrapper).
The engine exposes:
- `createEngineOrca({ bus, timers, logger?, maxRuns?, reentry? })` —
imperative engine
- `createActiveOrca({ ... })` — Svelte 5 wrapper with reactive
snapshots (`runningSnapshot`, `recentRunsSnapshot` , `latestRun` ,
`committedSnapshot` , `disposedSnapshot` )
- `onEvent(event, action) → detach`
- `configureEvent(event, { queuePolicy })` — `fifo` (default) /
`replace` / `drop-latest` / `parallel`
- `validate()` — static analysis of the graph (issues with severity
error or warn)
- `commit()` — freezes the graph; a later `onEvent` throws
`OrcaFrozenError`
- `onChange(listener)` — notifies register/detach/run-start/run-end/
commit/dispose for reactive integrations
- Canonical stages (`guard / pre / main / post / cleanup / finally`)
- Per-event run-queue + `OrcaEnvelope` with runtime metadata
(`eventId`, `traceId` , `parentEventId` , `depth` , `stack` )
- `OrcaActionContext` : `eventId` , `traceId` , `depth` , `signal` ,
`emit()` , `tokens` , `tokenPayloads` , `logger`
- `OrcaResult` : `success / skipped / error / interrupted / timeout /
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
fatal`
- `OrcaRunResult` with `tokens` , `tokenPayloads` , `actions[]` and
`compensations[]`
- Reentry guards: `maxDepth` , `maxEventsPerTrace` ,
`repeatedEventLimit` , `dedupeKey` with policies
`skip / abort-trace / error`
- `actionTimeoutMs` per action — race against timer + per-action
abort, isolated from siblings
- `OrcaFatal` with precedence `FATAL > TIMEOUT > ABORTED >
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
INTERRUPTED > PARTIAL > SUCCESS`
- Gates `unless` → `abortOn` → `fanIn` → `after` (evaluation
order); `fanIn: { tokens, min }` for k-of-n quorum
- `parallel: true` per action — concurrent waves with `Promise.all`
- `transaction: 'tx-id'` — atomic groups with immediate LIFO
rollback when a member fails
- Tokens with payload (`emits: [{ token, payload }]`) +
`ctx.tokenPayloads`
- `compensate` per action — LIFO rollback before `FINALLY` ,
best-effort, run-once
- Bus interception — `bus.publish` from within an action is
attributed to the active run/action via `AsyncLocalStorage` (Node/Bun;
root-event fallback in browsers without `AsyncContext` )
- Structured diagnostics (`orca.run.*`, `orca.action.*` ,
`orca.event.emitted` , `orca.reentry.blocked` , `orca.trace.aborted` ,
`orca.compensation.*` , `orca.queue.dropped` )
- `applyStandardOrca(App)` preset aggregator in
`arts/active-app/presets/`
What is accepted for v2 is described in the "Roadmap v2" section at the
end.
The architectural decisions that underpin the design:
- `bus` transports local events.
- `orca` runs actions associated with bus events.
- `timer` coordinates timers, timeouts and clocks.
- `logger` receives diagnostics/logs.
- The application decides which modules each action touches.
- Nothing destructive runs by default without being registered.
- Clean payload, separate runtime envelope: the module never knows
`orca` ; only the `OrcaAction` s receive `ctx` .
## Naming
| Concept | Name |
| --------------- | ----------------------------------- |
| Artifact | `orca` |
| Imperative root | `createEngineOrca()` / `EngineOrca` |
| Reactive root | `createActiveOrca()` / `ActiveOrca` |
| Action | `OrcaAction` |
| Result | `OrcaResult` |
| Event execution | `OrcaRunResult` |
| Semantic token | `OrcaToken` |
The intended public alias would be:
```ts
import { createEngineOrca, orcaSuccess } from '$orca';
```
## What Problem It Solves
Without `orca` , inter-module reactions tend to end up scattered:
```txt
session knows cache
cache knows perm
connection knows session
active-app knows everything
```
That scales badly. A change of identity, permissions, tenant, connectivity or a
remote event from the future `active-server` can involve several actions:
```txt
APP_EVENT_USER_IDENTITY_CHANGED
-> cancel private HTTP
-> clear private cache
-> invalidate permissions
-> reauthenticate connections
-> clear private storage
-> record audit/diagnostics
```
`orca` lets that flow be declared in a single place, testable and traceable,
without the modules knowing about each other.
## What It Is Not
`orca` is not:
- An event bus. That's `bus` .
- A realtime transport. That's `connection` .
- A permissions engine. That's `perm` .
- A cache. That's `cache` .
- A replacement for `timer` , `logger` or `active-app` .
- A durable server-side workflow in the Temporal style.
- A system that decides business logic on its own.
- An internal dependency that other artifacts consume in order to work.
The rule:
```txt
orca does not know modules; orca knows events, actions and results.
```
Ecosystem invariant:
```txt
artifacts publish events on bus
the application decides what to orchestrate with orca
```
`session` , `cache` , `perm` , `connection` , `auth` or `http` must not depend on
`orca` for their internal flows. If an artifact needs to coordinate its own
internal state, it does so inside the artifact. If an application wants to
coordinate several artifacts when something happens, it registers actions in
`orca` .
## Prior Art
`orca` takes ideas from several ecosystems, but copies none of them:
| Reference | Usable idea | Difference from `orca` |
| --------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------- |
| Redux Toolkit listener middleware | listeners, async workflows, cancellation, `take` , `condition` , `fork` | `orca` is not tied to Redux or reducers |
| Redux-Saga | concurrency, `fork` , `join` , `race` , `takeLatest` | `orca` avoids generators and uses explicit Result |
| NgRx Effects | isolate side-effects from components | `orca` does not depend on RxJS or Angular |
| redux-observable | actions in, actions out | `orca` does not force an Rx stream |
| Effector | events, effects, scopes, `allSettled` | `orca` defines stages, tokens and policies |
| Effect-TS | Result, timeout, retry, schedules | `orca` must be much smaller and adapter-driven |
| Temporal / Durable Functions | workflows with steps, retries and timers | `orca` v0 is not durable or server-authoritative |
Concrete ideas worth stealing:
- RTK listener middleware: cancellation and concurrency policies without a
generator DSL.
- XState v5 `setup()` : declare events, tokens and actions before building
the runtime to gain inference and validation.
- Effect Workflow: separate action/activity from workflow/run, and study
explicit compensations.
- Temporal: keep deterministic discipline for a possible future replay/audit.
## Mental Contract
An orchestration is defined like this:
```ts
Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
id: ORCA_ACTION_CLEAR_PRIVATE_CACHE,
stage: ORCA_STAGE_MAIN,
after: [ORCA_TOKEN_HTTP_PRIVATE_CANCELLED],
abortOn: [ORCA_TOKEN_CACHE_ERROR],
actionTimeoutMs: 2_000,
onError: ORCA_ON_ERROR_ABORT_RUN,
action: async (payload) => {
const result = await App.cache.clearActorScope(payload.previousActorId);
if (!result.ok) {
return orcaError(result.error, {
emits: [ORCA_TOKEN_CACHE_ERROR]
});
}
return orcaSuccess({
emits: [ORCA_TOKEN_CACHE_OK]
});
}
});
```
`orca` only sees:
- event received
- registered action
- stage
- required tokens
- emitted tokens
- result
- error policy
- timers
The action is the one that decides to call `App.cache` , `App.perm` ,
`App.connections` or any other service.
## Typed Setup _(roadmap v2)_
> Not implemented. See "Roadmap v2" below.
The current API is direct: `createEngineOrca()` or `createActiveOrca()` with
`onEvent()` and `configureEvent()` . Events, tokens and action ids live as
constants exported by the app or by the presets:
```ts
const Orca = createEngineOrca({ bus, timers, logger });
Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
id: ORCA_ACTION_CLEAR_PRIVATE_CACHE,
stage: ORCA_STAGE_MAIN,
provides: [ORCA_TOKEN_CACHE_CLEARED],
action: (payload, ctx) => {
// ...
}
});
```
For v2 a typed wrapper such as `setupOrca({ events, tokens, actions })` is being
considered that would infer payloads, validate phantom tokens in gates, restrict
`emits` to `provides` and emit a navigable graph. Today that validation is static
and best-effort via `Orca.validate()` (see the "Validate" section).
## Events
`orca` connects to `bus` and listens to events declared via constants.
Loose strings must not be used in application actions.
Example of an app event:
```ts
export const APP_EVENT_USER_IDENTITY_CHANGED = 'app.user.identity.changed' as const;
```
Example registration:
```ts
Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, ACTION_RESET_PRIVATE_STATE);
```
The event payload must be safe:
- no tokens
- no passwords
- no secrets
- no authorization headers
- no raw webhook body
- no unnecessary private data
If an action needs sensitive information, it must resolve it from the
corresponding module at the moment of running the action.
## Actions
An action is a unit of work associated with an event.
```ts
interface OrcaAction< TPayload = unknown , TValue = unknown > {
readonly id: OrcaActionId;
readonly stage: OrcaStage;
readonly after?: readonly OrcaToken[]; // wait for these tokens before running
readonly unless?: readonly OrcaToken[]; // idempotency: skipped if any is present
readonly abortOn?: readonly OrcaToken[]; // BLOCKED if any is present
readonly fanIn?: OrcaFanInSpec; // quorum: runs with `min` of `tokens` present
readonly provides?: readonly OrcaToken[]; // tokens it declares it emits (guides validate())
readonly actionTimeoutMs?: number; // per-action timeout → OrcaTimeout + signal abort
readonly transaction?: string; // atomic-group tag (LIFO compensation on failure)
readonly parallel?: boolean; // concurrent wave with consecutive actions of the same stage
readonly onError?: OrcaErrorPolicy; // reaction to OrcaError (CONTINUE by default / ABORT_RUN)
readonly compensate?: OrcaActionFn< TPayload , void > ; // rollback if the run aborts after its success
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
readonly action: OrcaActionFn< TPayload , TValue > ;
}
```
There is no `priority` , `tokenTimeoutMs` , `onFatal` or `onTimeout` : the
intra-stage order is registration order, the only timeout is `actionTimeoutMs` ,
and an unrecoverable failure is signaled by **returning** `OrcaFatal` from the
action (not with a separate policy).
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
An action must be:
- named with a constant
- idempotent when possible
- testable in isolation
- explicit about its failure policy
- explicit if it needs a transaction
- explicit if it can wait for tokens
- safe with respect to payloads carrying sensitive data
## Stages
Stages give structure to the pipeline. Within a stage the order is
**registration** order (there is no numeric priority); to order by dependencies
use tokens (`after` / `provides` ), and for intra-stage concurrency, `parallel` .
```txt
guard validates preconditions
pre prepares the environment and cancels incompatible work
main runs the main effect
post derived reactions after main
cleanup cleanup of temporary state
finally diagnostics, metrics and final traces
```
Intended constants:
```ts
export const ORCA_STAGE_GUARD = 'guard' as const;
export const ORCA_STAGE_PRE = 'pre' as const;
export const ORCA_STAGE_MAIN = 'main' as const;
export const ORCA_STAGE_POST = 'post' as const;
export const ORCA_STAGE_CLEANUP = 'cleanup' as const;
export const ORCA_STAGE_FINALLY = 'finally' as const;
```
`finally` must be able to run even if the pipeline aborted, unless the
orchestrator has been disposed.
## Concurrency within the stage (`parallel`)
Within a stage the execution order is the **registration order** . An action can
declare `parallel: true` : the **consecutive** actions with `parallel: true` and
the same stage form a **wave** that runs in parallel via `Promise.all` . A
sequential action (the default, `parallel: false` ) breaks the wave and runs to
completion before the next one starts.
The tokens emitted within a wave are merged into the run's set **after** the wave
settles; the gates (`after` / `unless` / `abortOn` / `fanIn` ) are evaluated at
the start of the wave against the tokens accumulated before, so the siblings of
the same wave never see each other's tokens. If a parallel action returns
`OrcaFatal` or fails with `onError: 'abort-run'` , the run aborts only after the
wave settles — the siblings are not cancelled mid-execution.
There are no `sync` / `async` modes or `ORCA_EXEC_*` constants: the only
**intra-stage** concurrency lever is `parallel` . The concurrency **between runs**
of the same event is configured separately with
`configureEvent(event, { queuePolicy })` (next section).
## Run Concurrency
A run is a concrete execution of an event. A run's internal pipeline does not by
itself resolve what happens when the same event enters several times while a
previous execution is still alive. That decision is part of the event's contract
and is configured with `Orca.configureEvent(event, { queuePolicy })` .
Live constants (`consts.ts`):
```ts
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
ORCA_QUEUE_FIFO; // 'fifo'
ORCA_QUEUE_REPLACE_QUEUED; // 'replace-queued'
ORCA_QUEUE_DROP_LATEST; // 'drop-latest'
ORCA_QUEUE_PARALLEL; // 'parallel'
```
Semantics:
| Mode | Use |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fifo` (default) | Each event queues a run; non-parallel runs are serialized globally. Simple guarantee. |
| `replace-queued` | If an event arrives while another is queued, the queued one is discarded and replaced by the new one. **Does not abort in-flight.** Latest pending intent wins. |
| `drop-latest` | If there is a run of the same event in-flight or queued, the incoming one is discarded. Ignores retriggers during work. |
| `parallel` | Launches concurrent runs; parallel events do not take the global lock. Footgun: overlaps side-effects. |
`replace-queued` lets the active run finish (FINALLY included) — the "strong"
variant that aborts in-flight (`replace-current`, takeLatest) is in Roadmap v2.
`parallel` is not the default for a reason: in destructive orchestrations
(identity change, logout, tenant switch, permission invalidation, private cache),
overlapping runs is a direct source of race conditions. Explicit opt-in per event
and visible diagnostics.
## Tokens
Tokens are semantic facts produced by actions.
Examples:
```ts
export const ORCA_TOKEN_HTTP_PRIVATE_CANCELLED = 'http:private_cancelled' as const;
export const ORCA_TOKEN_CACHE_OK = 'cache:ok' as const;
export const ORCA_TOKEN_CACHE_ERROR = 'cache:error' as const;
export const ORCA_TOKEN_PERMISSIONS_INVALIDATED = 'perm:invalidated' as const;
export const ORCA_TOKEN_CONNECTIONS_REAUTHENTICATED = 'connection:reauthenticated' as const;
```
An action can:
- `after` : wait for tokens before running.
- `unless` : skip if a token already exists.
- `abortOn` : abort or block if a token appears.
- `provides` : declare the tokens it can produce.
Tokens avoid depending on the concrete name of another action. The action
`connection.reauth` does not need to know whether the cache was cleared by
`clearCacheV1` or `resetPrivateCache` ; it only waits for `cache:ok` .
### Token Scope
Tokens are **scoped to the run** . A token emitted during a run of
`APP_EVENT_USER_IDENTITY_CHANGED` does not exist for the next run of the same
event nor for a different event.
```txt
run A emits cache:ok
run B does not see cache:ok unless one of B's actions emits it again
```
This avoids accidental leaks between executions. If the application needs a
persistent fact, that fact must live in a real module (`cache`, `session` ,
`perm` , `storage` ) or be published as a new event on `bus` , not as a global
token.
Allowed cases:
- tokens emitted by actions of the same run
- explicit seed tokens when creating a run
- tokens derived from the event's configuration
Cases forbidden in v0:
- global token shared between events
- token persisted in storage
- token automatically reused between runs
- token with TTL
- sticky token
This rule must not be configurable in v0. If persistent state is needed, the
source of truth is an artifact (`session`, `cache` , `perm` , `storage` ) and not
`orca` .
### Flag Tokens and Payload Tokens
Tokens are semantic names (`string`) that a run accumulates as actions emit them.
Basic form — pure flag, no data:
```ts
return orcaSuccess({ emits: [ORCA_TOKEN_CACHE_OK] });
```
A downstream action decides whether to act by checking `ctx.tokens.has(...)`
directly or via declarative gates (`after` / `unless` / `abortOn` / `fanIn` ). If
it needs to carry data (beyond "this happened"), use the payload form — `emits`
accepts `{ token, payload }` entries mixed with loose strings:
```ts
return orcaSuccess({
emits: [
ORCA_TOKEN_CACHE_OK,
{ token: ORCA_TOKEN_USER_LOADED, payload: { id: 'u-7', tenantId: 't-3' } }
]
});
```
The payload is read downstream with `ctx.tokenPayloads.get(token)` (a
`ReadonlyMap<OrcaToken, unknown>` ). Tokens emitted as a loose string do **not**
appear in the map, so `ctx.tokens.has('x')` and `ctx.tokenPayloads.has('x')` can
diverge (presence vs. payload). The gates still compare only names — the payload
is an orthogonal channel for intra-run coordination.
Typed limitation: `payload` is typed as `unknown` ; authors cast. Inference such
as `setupOrca({ tokens: { Token: SchemaPayload } })` is left for v2.
### Graph Validation
`orca` must validate the configuration before running in production. At a
minimum:
- detect static cycles between tokens
- detect actions that wait for tokens that no action can produce
- detect tokens declared in `abortOn` or `unless` with obvious typos
- detect actions duplicated by `id` within the same event
- detect `provides` that are never emitted in any possible result if it can be
inferred
Validation does not replace tests, but it must fail early when the graph is
impossible. A pipeline that can get blocked by configuration must fail when
registering actions or during `Orca.validate()` , not six months later in a real
session.
Intended API:
```ts
Orca.validate();
Orca.commit();
```
`validate()` checks the configuration without closing the registry. `commit()`
validates and freezes the configuration for execution. In development, `orca` can
run `validate()` automatically before the first event if the user did not do it.
## OrcaEnvelope and OrcaActionContext
Every event the engine processes is wrapped in an internal `OrcaEnvelope` before
reaching the run queue:
```ts
interface OrcaEnvelope< TPayload > {
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
readonly event: string;
readonly payload: TPayload;
readonly meta: OrcaEventMeta;
}
interface OrcaEventMeta {
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly parentRunId?: OrcaRunId;
readonly emittedByAction?: OrcaActionId;
readonly depth: number;
readonly stack: readonly string[];
readonly publishedAt: number;
readonly dedupeKey?: string;
}
```
The `payload` stays clean (`{ userId, tenantId, ... }`). The `meta` is owned by
orca's runtime.
Each `OrcaAction` receives an `OrcaActionContext` with the metadata useful for
that execution:
```ts
interface OrcaActionContext {
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
readonly runId: OrcaRunId;
readonly event: string;
readonly stage: OrcaStage;
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly depth: number;
readonly tokens: ReadonlySet< OrcaToken > ;
readonly signal: AbortSignal;
readonly logger: Logger;
emit< TPayload > (event: string, payload: TPayload, options?: OrcaEmitOptions): OrcaEventId | null;
}
```
## Rule: module → bus, action → ctx.emit
The **responsibility boundary** is clear:
```txt
Module → bus.publish() (never knows orca)
Action → ctx.emit() (preserves traceId, parentEventId, depth)
```
A module never receives `ctx` . If an `OrcaAction` wants to emit a derived event
during a run, it uses `ctx.emit()` so orca creates a child envelope (same
`traceId` , `parentEventId = ctx.eventId` , `depth = ctx.depth + 1` ,
`emittedByAction` = the action's id).
If instead the action calls a module that internally does `bus.publish(...)` ,
orca will see it as an event during the run but as a **root** from orca's
perspective: new `traceId` , `depth = 0` . It does not mix with the current run's
trace. That is **option A** of the v0-kernel; option B (intercepting
`bus.publish` during a run to attribute a perfect `emittedByAction` ) is now
implemented in v1 as a diagnostic bonus in environments with `AsyncLocalStorage`
— see "Bus interception" in the Roadmap v1 section.
## Reentrancy and Events During a Run
Any event a module publishes on the bus during a run is queued, not run inline.
The same applies to derived events via `ctx.emit()` . The engine keeps a FIFO
queue per event and never runs actions recursively within the same stack.
```txt
main(action A)
-> ctx.emit(B) -> child envelope B queued
-> does NOT run pipeline B inside action A
```
### Reentry guards
`createEngineOrca` accepts `reentry: OrcaReentryOptions` :
```ts
interface OrcaReentryOptions {
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
readonly maxDepth?: number; // default 16
readonly maxEventsPerTrace?: number; // default 128
readonly repeatedEventLimit?: number; // default 2
readonly repeatedEventPolicy?:
| typeof ORCA_REENTRY_SKIP // default
| typeof ORCA_REENTRY_ABORT_TRACE
| typeof ORCA_REENTRY_ERROR;
}
```
When a **derived** envelope (via `ctx.emit()` ) exceeds one of the limits, the
engine applies the configured policy:
- `skip` — the envelope is not queued; the trace continues for other events.
Diagnostic `orca.reentry.blocked` with the appropriate `reason` .
- `abort-trace` — the trace is marked as aborted; any event already queued for
that trace is discarded when its turn comes; the in-flight runs of that trace
finish, but the following ones record `ORCA_RUN_INTERRUPTED` . Diagnostic
`orca.trace.aborted` .
- `error` — `OrcaReentryError` is thrown synchronously from `ctx.emit()` . The
action can catch it or let it escalate.
Reasons (`OrcaReentryReason`):
- `max-depth` — `depth > maxDepth`
- `max-events-per-trace` — `eventCount + 1 > maxEventsPerTrace`
- `repeated-event` — the same name exceeds `repeatedEventLimit`
- `deduped` — the `dedupeKey` already exists in the trace
- `trace-aborted` — the trace was marked as aborted before
- `run-aborted` — the run was aborted by `ORCA_ON_ERROR_ABORT_RUN`
- `disposed` — the engine is disposed
**Important**: direct publishes on the bus from module code do NOT enter these
counters. Each `bus.publish(event, payload)` generates a root envelope with a
fresh `traceId` and `depth = 0` . The counters are born and die with each trace.
`bus` keeps local delivery. `orca` decides when it consumes and starts runs. If a
new event requires immediate execution, it must be modeled as a token of the
current run, not as a reentrant event.
## Result
An action's result must be explicit.
```ts
type OrcaResult< TValue = unknown > =
| OrcaSuccess< TValue >
| OrcaSkipped
| OrcaInterrupted
| OrcaError
| OrcaTimeout
| OrcaFatal;
```
Preliminary form:
```ts
interface OrcaSuccess< TValue = unknown > {
readonly ok: true;
readonly status: typeof ORCA_RESULT_SUCCESS;
readonly value?: TValue;
readonly emits?: readonly OrcaToken[];
}
interface OrcaSkipped {
readonly ok: true;
readonly status: typeof ORCA_RESULT_SKIPPED;
readonly reason?: string;
readonly emits?: readonly OrcaToken[];
}
interface OrcaInterrupted {
readonly ok: false;
readonly status: typeof ORCA_RESULT_INTERRUPTED;
readonly reason?: string;
readonly emits?: readonly OrcaToken[];
}
interface OrcaError {
readonly ok: false;
readonly status: typeof ORCA_RESULT_ERROR;
readonly error: unknown;
readonly recoverable?: boolean;
readonly emits?: readonly OrcaToken[];
}
interface OrcaTimeout {
readonly ok: false;
readonly status: typeof ORCA_RESULT_TIMEOUT;
readonly timeoutMs: number;
readonly emits?: readonly OrcaToken[];
}
interface OrcaFatal {
readonly ok: false;
readonly status: typeof ORCA_RESULT_FATAL;
readonly error: unknown;
readonly emits?: readonly OrcaToken[];
}
```
Intended helpers:
```ts
orcaSuccess({ emits?: tokens, value?: data });
orcaSkipped(reason, { emits?: tokens });
orcaInterrupted(reason, { emits?: tokens });
orcaError(error, { emits?: tokens, recoverable?: boolean });
orcaTimeout(timeoutMs, { emits?: tokens });
orcaFatal(error, { emits?: tokens });
```
Exceptions must not be the normal flow. If an action throws, `orca` catches it
and converts it into `ORCA_RESULT_ERROR` or `ORCA_RESULT_FATAL` according to the
declared policy.
`ORCA_RESULT_INTERRUPTED` represents controlled cancellation/interruption, not a
business failure. It matters for `replace` , `dispose` and future cancellation
policies: aborting a previous run because a new intent arrived must not look the
same as a cache error or an unexpected exception.
## Failure Policies
An action declares what happens if it fails.
```ts
export const ORCA_ON_ERROR_CONTINUE = 'continue' as const;
export const ORCA_ON_ERROR_ABORT_ACTION = 'abort-action' as const;
export const ORCA_ON_ERROR_ABORT_STAGE = 'abort-stage' as const;
export const ORCA_ON_ERROR_ABORT_RUN = 'abort-run' as const;
```
`ABORT_ACTION` and `ABORT_STAGE` are accepted for compat but today behave like
`CONTINUE` ; only `CONTINUE` (default) and `ABORT_RUN` are distinct.
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
An **unrecoverable** failure has no policy constant: the action **returns**
`OrcaFatal` (`orcaFatal(error)`) and the engine **always** aborts the run — it
precedes any other state (timeout, aborted, partial) and ignores the action's
`onError` . There are no `ORCA_ON_FATAL_*` .
### Rollback and Compensation
`rollback` must not be part of v0 as a generic promise. In frontend, an action
can mutate cache, reactive stores, IndexedDB, cookies or connections; there is no
universal rollback like in a database.
For v0, policies must speak of aborting, continuing, blocking or cleaning up.
State repair is done with:
- the `cleanup` stage
- idempotent actions
- explicit compensation actions
Compensations (implemented):
```ts
interface OrcaAction< TPayload = unknown , TValue = unknown > {
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
readonly action: OrcaActionFn< TPayload , TValue > ;
readonly compensate?: OrcaActionFn< TPayload , void > ;
}
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
type OrcaActionFn< TPayload , TValue > = (
payload: TPayload,
context: OrcaActionContext
) => OrcaResult< TValue > | Promise< OrcaResult < TValue > >;
```
The compensator receives the same `OrcaActionContext` as the action (its
`ctx.emit()` returns `null` — rollback is not the place for fan-out).
Compensation is not magic rollback. It is an inverse or sanitizing action
declared by the application. `orca` can invoke it in reverse order when a run
aborts, but only for actions that declared it.
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
## Timeouts
The only implemented timeout is ** `actionTimeoutMs` ** per action. The engine runs
the action's promise against a timer of the injected `TimerScheduler` ; if the
timer expires first, it produces `OrcaTimeout` (`{ ok: false, status:
ORCA_RESULT_TIMEOUT, timeoutMs }`) and aborts that action's `signal` . Sibling
actions are not affected — each has its own controller. `actionTimeoutMs <= 0`
(or omitted) runs without mediation.
A timeout is a **result** (`OrcaTimeout`), not a policy: there is no `onTimeout`
field or `ORCA_ON_TIMEOUT_*` constants. The higher-level timeouts (`runTimeoutMs`
/ `stageTimeoutMs` / `idleTimeoutMs` ) are **not implemented** — they are roadmap.
`orca` never uses `Date.now()` or `setTimeout()` directly: always the
`TimerScheduler` of `timer` .
## Timers
`timer` coordinates time. `orca` only registers deadlines and cancels its timers
when the run finishes or on `dispose()` .
```ts
const Orca = createEngineOrca({
bus: App.bus,
timers: App.timers,
logger: App.logger
});
```
Internally the engine mints a timer per action with `actionTimeoutMs` (tied to
`runId` / `stage` / `actionId` ) and cancels it when the action settles, when the
run finishes or on `dispose()` . There is no public timer-key helper: management is
internal to the engine.
If `orca` creates timers on an injected scheduler, it does not own the scheduler.
`Orca.dispose()` cancels the timers registered by `orca` , but does not destroy
`App.timers` .
## Transactions
A transaction is an **atomic group** of actions that share the same tag
`transaction: string` on the same event (they can span several stages). If any
member ends in `ERROR` or `FATAL` , the engine **compensates** immediately the
members that had already succeeded — in **LIFO** order of completion — and aborts
the run. A member's `onError` is ignored within a transaction: the transactional
semantics always abort.
```ts
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
Orca.onEvent('checkout.submit', {
id: ORCA_ACTION_RESERVE_STOCK,
stage: ORCA_STAGE_MAIN,
transaction: 'checkout',
action: reserveStock,
compensate: releaseStock // invoked in rollback if another member fails
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
});
```
Tagging a member with `transaction` does not force declaring `compensate` — a
member without a compensator simply has nothing to undo, and `validate()` emits a
**warning** when no member of the transaction declares it (it would have no
rollback effect). Transaction compensations are appended to
`OrcaRunResult.compensations[]` just like the standard ones, and each compensator
runs **at most once** per run.
There is no `OrcaTransactionPort` or `ORCA_TX_*` constants: the transaction is the
grouping tag + `compensate` , not an external port or `required` / `requires-new`
modes.
## Run Result
Each event publication can produce a complete summary:
```ts
interface OrcaRunResult {
readonly id: OrcaRunId;
readonly event: string;
readonly status:
| typeof ORCA_RUN_SUCCESS
| typeof ORCA_RUN_PARTIAL
| typeof ORCA_RUN_ABORTED
| typeof ORCA_RUN_FATAL
| typeof ORCA_RUN_TIMEOUT;
readonly startedAt: number;
readonly endedAt?: number;
readonly durationMs?: number;
readonly tokens: readonly OrcaToken[];
readonly actions: readonly OrcaActionRun[];
}
```
Each executed action must be traced:
```ts
interface OrcaActionRun {
readonly id: OrcaActionId;
readonly stage: OrcaStage;
readonly status:
| typeof ORCA_ACTION_STATUS_SUCCESS
| typeof ORCA_ACTION_STATUS_SKIPPED
| typeof ORCA_ACTION_STATUS_BLOCKED
| typeof ORCA_ACTION_STATUS_ERROR
| typeof ORCA_ACTION_STATUS_TIMEOUT
| typeof ORCA_ACTION_STATUS_FATAL;
readonly startedAt?: number;
readonly endedAt?: number;
readonly durationMs?: number;
readonly waitedFor: readonly OrcaToken[];
readonly emitted: readonly OrcaToken[];
readonly error?: unknown;
}
```
This is key for composite tests:
```ts
expect(run.status).toBe(ORCA_RUN_ABORTED);
expect(run.actions.find((a) => a.id === ORCA_ACTION_REAUTH_CONNECTIONS)?.status).toBe(
ORCA_ACTION_STATUS_BLOCKED
);
expect(run.tokens).toContain(ORCA_TOKEN_CACHE_ERROR);
```
## Active Layer
`createActiveOrca()` would exist only for reactive inspection and test/devtools
pages.
It must have a history limit. A long session cannot accumulate all the runs in
memory.
Possible properties:
```ts
ActiveOrca.running;
ActiveOrca.lastRun;
ActiveOrca.runs;
ActiveOrca.registeredEvents;
ActiveOrca.registeredActions;
ActiveOrca.failedRuns;
ActiveOrca.dispose();
```
Intended option:
```ts
const ActiveOrca = createActiveOrca({
maxRuns: 200
});
```
The execution logic must live in `createEngineOrca()` . The active layer must not
be necessary for core tests or for a future server runtime.
## App Integration
`orca` must be present in `active-app` , but inert until the application registers
actions.
Recommended name in `App` :
```ts
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
App.Orchestration;
```
`orca` remains the name of the artifact, alias/import and constant prefix
(`$orca`, `ORCA_*` ). The application field uses a semantic name because it is read
in product code:
```ts
App.Orchestration.onEvent(...);
```
`active-app` can create `Bus` , `Timers` , `Logger` and `Orchestration` , but must
not hide the orchestration policy.
Expected inertia:
- with no registered actions, there are no runs
- with no registered actions for an event, the event is an O(1) no-op
- `orca` subscribes to a bus event only when registering the first action for
that event
- if the last action of an event is removed, `orca` unsubscribes from that event
- `dispose()` is a no-op if it was never used
- the active history is not allocated expensively until the first run
v0 bundle decision: `App.Orchestration` can be a real engine included in the base
bundle. Dynamic import will not be used for the v0 core; the complexity of an
async proxy is not worth it if the inert engine is small.
`Bus` must also be an always-present, inert resource. If an app has
`App.Orchestration` , it must have `App.bus` .
Explicit use:
```ts
App.Orchestration.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
id: ORCA_ACTION_RESET_PRIVATE_STATE,
stage: ORCA_STAGE_MAIN,
onError: ORCA_ON_ERROR_ABORT_RUN,
action: async (payload) => {
await App.cache.clearActorScope(payload.previousActorId);
App.perm.invalidate();
await App.connections.reauthenticateAll();
return orcaSuccess();
}
});
```
A future preset may exist, but it must be a function that registers visible
actions:
```ts
registerStandardAppOrchestration(App, App.Orchestration);
```
There must be no implicit magic where `createActiveApp()` activates destructive
side-effects without the developer being able to see which actions were
registered.
### Dynamic Registration
`orca` must allow registering actions dynamically, for example when a
lazy-loaded feature is activated.
Rule:
```txt
actions registered during a run do not participate in that run
```
Each run uses a snapshot of actions taken at the start. New actions only
participate in future runs. This prevents the working set from changing mid-
pipeline.
## Fan-In Between Events
v0 keeps `orca` as a **single-event-trigger** engine: one event fires a pipeline
of actions. There is no `onEvents([A, B])` in v0.
If an application needs fan-in such as:
```txt
run when auth.ready and perm.loaded have both occurred
```
it must synthesize a composite event outside `orca` :
```txt
APP_EVENT_AUTH_READY
APP_EVENT_PERMISSIONS_LOADED
-> module/combiner publishes APP_EVENT_SECURITY_CONTEXT_READY
-> orca listens to APP_EVENT_SECURITY_CONTEXT_READY
```
This keeps the core simple and avoids opening big questions in v0:
- what happens if A arrives twice before B
- how long the join lives
- whether the join resets the timeout
- how joins are cancelled when the user changes
- whether payloads are combined or replaced
`onEvents()` can be a future extension, but it must not block the core.
## Complete Example
Identity change:
```ts
Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
id: ORCA_ACTION_CANCEL_PRIVATE_HTTP,
stage: ORCA_STAGE_PRE,
provides: [ORCA_TOKEN_HTTP_PRIVATE_CANCELLED],
onError: ORCA_ON_ERROR_CONTINUE,
action: async () => {
await App.http.cancelPrivateRequests();
return orcaSuccess({ emits: [ORCA_TOKEN_HTTP_PRIVATE_CANCELLED] });
}
});
Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
id: ORCA_ACTION_CLEAR_PRIVATE_CACHE,
stage: ORCA_STAGE_MAIN,
after: [ORCA_TOKEN_HTTP_PRIVATE_CANCELLED],
provides: [ORCA_TOKEN_CACHE_OK, ORCA_TOKEN_CACHE_ERROR],
actionTimeoutMs: 2_000,
onError: ORCA_ON_ERROR_ABORT_RUN,
action: async (payload) => {
const result = await App.cache.clearActorScope(payload.previousActorId);
if (!result.ok) return orcaError(result.error, { emits: [ORCA_TOKEN_CACHE_ERROR] });
return orcaSuccess({ emits: [ORCA_TOKEN_CACHE_OK] });
}
});
Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
id: ORCA_ACTION_INVALIDATE_PERMISSIONS,
stage: ORCA_STAGE_POST,
after: [ORCA_TOKEN_CACHE_OK],
abortOn: [ORCA_TOKEN_CACHE_ERROR],
provides: [ORCA_TOKEN_PERMISSIONS_INVALIDATED],
onError: ORCA_ON_ERROR_ABORT_STAGE,
action: async () => {
await App.perm.invalidate();
return orcaSuccess({ emits: [ORCA_TOKEN_PERMISSIONS_INVALIDATED] });
}
});
Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
id: ORCA_ACTION_REAUTH_CONNECTIONS,
stage: ORCA_STAGE_POST,
after: [ORCA_TOKEN_CACHE_OK, ORCA_TOKEN_PERMISSIONS_INVALIDATED],
abortOn: [ORCA_TOKEN_CACHE_ERROR],
onError: ORCA_ON_ERROR_CONTINUE,
action: async () => {
await App.connections.reauthenticateAll();
return orcaSuccess({ emits: [ORCA_TOKEN_CONNECTIONS_REAUTHENTICATED] });
}
});
```
## Preliminary Algorithm
```txt
on bus event:
resolve event concurrency policy
queue/drop/replace/parallel according to policy
create run
collect actions for event
validate graph if not already validated
sort stages in canonical order
for each stage:
start stage timer
while stage has runnable actions:
find actions whose after tokens are satisfied
skip actions whose unless tokens are present
block/abort actions whose abortOn tokens are present
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
run ready actions in registration order (parallel waves via Promise.all)
collect Result
add emitted tokens
apply error/fatal/timeout policies
cancel timers for completed actions
mark remaining waiting actions as blocked or skipped
stop stage timer
run finally actions if allowed
run compensations if configured and policy requires it
finish run result
emit diagnostics
```
Mandatory protections:
- detect token/configuration cycles
- detect actions blocked by tokens nobody produces
- isolate tokens per run
- apply the per-event concurrency policy
- do not run events published during a run inline
- do not run actions after `dispose()`
- cancel own timers on abort or finish
- catch action exceptions
- do not publish secrets in diagnostics
- do not leave dangling promises without a final status
## Diagnostics
`orca` uses the common `Logger` from `libs/logger` with a cataloged diagnostics
layer. The live keys (`ORCA_DIAGNOSTIC_EVENTS` in `consts.ts` ):
```ts
docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard
Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
completed against vite.config.ts
A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
multi-level timeout model, the transaction-port model + ORCA_TX_*,
OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
(parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
/ compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
documented the ActiveEngine contract surface (loading / lastError / disposed /
snapshot / clearError / onChange / dispose); dangling demo URL ->
/active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
/test/adom -> /active/docs/adom
Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
'orca.run.started';
'orca.run.completed';
'orca.run.aborted';
'orca.action.started';
'orca.action.completed';
'orca.action.failed';
'orca.action.fatal';
'orca.action.timeout';
'orca.action.skipped';
'orca.action.blocked';
'orca.action.interrupted';
'orca.compensation.started';
'orca.compensation.completed';
'orca.compensation.failed';
'orca.event.emitted';
'orca.reentry.blocked';
'orca.trace.aborted';
'orca.queue.dropped';
'orca.configuration.invalid';
```
Each event carries meta with `runId` / `eventId` / `traceId` / `depth` when
applicable, plus specific fields (`durationMs`, `error` , `reason` , etc). The
messages and levels live in `diagnostics.ts` — never hardcoded in the runtime.
## Tests
Unit coverage of the engine in `src/arts/orca/test/engine-orca.test.ts`
(registration, stages, error policies, run trace, dispose, envelope/context,
reentry guards, diagnostics, `actionTimeoutMs` , `OrcaFatal` , gates
`after` /`unless`/`abortOn`/`fanIn`, `validate()` , `compensate` , `commit()` ,
`parallel` waves, `transaction` , tokens with payload, queue policies, bus
interception). Coverage of the reactive wrapper in
`src/arts/orca/test/active-orca.svelte.test.ts` (reactive snapshots via
`engine.onChange` ).
Composite test of the live ecosystem in
`src/arts/active-app/test/ecosystem-orca.test.ts` : a user change A → B that
validates in a single orca trace the cache cleanup + perm invalidation +
connections reauth; on revoke it runs cache-clear-on-revoke +
connections-close-on-revoke; verifies that `onError: continue` does not block
sibling presets when one fails; and checks that detaching `applyStandardOrca`
unregisters everything.
## Invariants
- `orca` does not import concrete artifacts except common contracts.
- `orca` does not know business modules.
- Artifacts do not consume `orca` ; they only publish events on `bus` .
- The application registers actions in `orca` .
- `App.orca` always exists, but runs nothing without actions.
- `App.bus` must exist if `App.orca` exists.
- All public strings live in constants.
- Events are constants, not inline strings.
- Tokens are constants, not inline strings.
- Logs use the common `Logger` .
- Diagnostics are cataloged.
- Timers are run by `timer` .
- An action's result is always represented.
- Event payloads do not contain credentials.
- Destructive actions are explicit.
- Modules never receive `ctx` . Only the `OrcaAction` s receive it, and
`ctx.emit()` is the path that preserves traceability.
## Roadmap v1
Accepted in the public contract and honored by the engine — v1 100% closed.
### Already in the engine (from v1)
- ✅ ** `actionTimeoutMs` ** — the engine runs the action against a timer
of the injected scheduler; on expiry it aborts the `signal` and produces
`ORCA_RESULT_TIMEOUT` . Sibling actions carry on.
- ✅ ** `OrcaFatal` ** — `orcaFatal(error)` produces
`ORCA_ACTION_STATUS_FATAL` and aborts the run immediately without
consulting `onError` . The `FINALLY` stage runs anyway.
Run status `ORCA_RUN_FATAL` with precedence over any other.
- ✅ **Gates `after` / `unless` / `abortOn`** — the engine evaluates the
tokens declared in each action against those emitted up to that
point in the run. `unless` is tested first (idempotency: skip if
any is present), then `abortOn` (halt: blocked if any is
present), then `after` (dependency: skip if any is missing).
The reason is serialized in `OrcaActionRun.reason` as
`unless-triggered:<token>` / `abort-on-triggered:<token>` /
`after-not-met:<tok1>,<tok2>,…` . The `FINALLY` stage bypasses them
always.
- ✅ ** `validate()` ** — static analysis that walks the actions
registered per event, in canonical order `(stage, registeredAt)` ,
accumulating the upstream `provides` . For each action it reports:
`unsatisfiable-after` (error: token produced by nobody in an
earlier stage nor higher up in the same stage),
`orphan-unless` / `orphan-abort-on` (warnings: useless gate),
`dependency-cycle` (error: A.after needs what B.provides and
vice versa). Returns `{ ok, issues }` ; never throws. The application
decides whether to treat the issues as blocking — the engine does not
freeze registrations or runs based on the result.
- ✅ ** `commit()` ** — freezes the action graph. After
`commit()` , any `onEvent()` throws `OrcaFrozenError` . It is
idempotent and does not run `validate()` implicitly: the caller
decides whether to validate first and treat the issues as blocking.
The `detach` functions returned before the freeze keep
working — `commit()` closes new registrations, not the
existing ones. `dispose()` operates normally on an
already-committed engine and takes precedence over the frozen guard.
- ✅ ** `parallel` ** — opt-in flag per action. Consecutive actions
with `parallel: true` and the same stage form a **wave** that runs
with `Promise.all` ; the sequential ones break the wave (each is
a wave of one). The gates (`after`/`unless`/`abortOn`) are
evaluated at the **start** of the wave against the tokens accumulated
by previous waves; those emitted during the wave are merged
when the wave **settles** , so two sibling parallels never see
each other. Errors and `OrcaFatal` are evaluated after settle: the
wave completes, then the run aborts if someone threw FATAL
or ERROR+ABORT_RUN. In-flight actions are not cancelled mid-
execution (each keeps its own `actionTimeoutMs` ). The parallels
that succeeded and declare `compensate` enter the LIFO stack and are
compensated in reverse order of their registration.
`validate()` retains `provides` until the end of the wave: two
sibling parallels with crossed `after` /`provides` are reported
as `unsatisfiable-after` .
- ✅ ** `transaction` ** — atomic groups per event (any
stage). If a tx member ends in `ERROR` or `FATAL` , the
engine compensates **immediately** the members of the same tx that
already succeeded in LIFO order of completion, marks the run
as aborted, and lets the standard pre-FINALLY flow compensate
the rest. The tx semantics **override** the `onError` of the failing
member (the tx always aborts the run; there is no need to declare
`abort-run` ). Compensators are invoked **at most once** :
the tx marks its members as `compensated` and the global pass
skips them. Each `OrcaActionRun` carries `transactionId?` for
trace navigation. `validate()` emits warning
`transaction-without-compensate` when all members of a
tx do not declare `compensate` (no-op rollback).
- ✅ **Tokens with payload** — the `emits` array accepts entries
`{ token, payload }` besides loose strings; both formats
mix in the same array. `ctx.tokenPayloads` (map
`ReadonlyMap<OrcaToken, unknown>` ) exposes the payloads by
name — the tokens emitted as a loose string do **not** appear
in the map, so `ctx.tokens.has('x')` and
`ctx.tokenPayloads.has('x')` can diverge (presence vs.
payload). The gates (`after`/`unless`/`abortOn`/`provides`)
still compare only names. The `tokenPayloads` snapshot
per wave is independent: sibling parallels never see the
payloads emitted by their peers mid-flight. Last-write-wins on
name collisions. `OrcaActionRun.emittedPayloads?` carries the
map per action (absent if the action only emitted strings).
`OrcaRunResult.tokenPayloads` is the run's final union (empty
Map when no token carried a payload).
- ✅ ** `fan-in` ** — quorum gate. The action declares
`fanIn: { tokens, min }` and fires when at least `min` of the
listed tokens are present in the wave's token snapshot.
`min` defaults to `tokens.length` (AND semantics
equivalent to `after` ); with `min: 1` you get
"first-to-finish wins". Gate order: `unless` → `abortOn` →
`fanIn` → `after` . If the quorum is not met, the action is
marked SKIPPED with `reason = fan-in-not-met:<count>/<min>:<tokens-present>` .
`fanIn` and `after` can coexist; both must pass.
`validate()` reports error `unsatisfiable-fan-in` when fewer
than `min` tokens of the set have an upstream provider — the gate
would never fire.
- ✅ **Queue policies** — `orca.configureEvent(event, { queuePolicy })`
selects how the concurrent / pending runs are managed
per event. **Important note** : the engine implements a
**single global lane** for all non-parallel events —
at most one `fifo` / `replace-queued` / `drop-latest` run
is in flight at a time, regardless of which event it belongs to.
This preserves the v0 invariant and makes cross-event
composition predictable (cache-invalidate-then-refresh-perm,
identity-change, etc). Per-event concurrency for
non-parallel policies is in Roadmap v2.
- `'fifo'` (default): each event queues a run; the runs are
serialized in the non-parallel global lane.
- `'replace-queued'` : when a new event of the same
name arrives and one is already queued, the queued one is discarded (it
emits `orca.queue.dropped` with `reason: 'replaced'` ). The in-flight one
is NOT aborted; at most "1 in-flight + 1 queued" per event. The
strong takeLatest variant (abort in-flight + queue new)
is reserved as `'replace-current'` for v2.
- `'drop-latest'` : if there is a run of the same event in-flight or
queued, the incoming one is discarded (`reason: 'drop-latest'`).
- `'parallel'` : the runs are launched concurrently via
`Promise` ; parallel events do not take the global lock, so
they can run at the same time as non-parallel events.
`dispose()` aborts all in-flight `AbortController` s at
once. `configureEvent` is idempotent with the same
policy and throws if you try to change it; throws
`OrcaFrozenError` after `commit()` .
- ✅ ** `compensate` ** — each action can declare a
compensating function. When the run aborts (FATAL / ABORT_RUN onError /
trace-aborted), the engine walks the LIFO stack of actions that already
completed `success` with a compensator and invokes them in reverse
order. Each compensation runs with its own `AbortController`
(the run's is already aborted) and a `ctx` whose `emit()` returns
`null` — compensations do not fan-out events, they are pure rollback.
Best-effort: if a compensation throws, it is recorded and the chain
continues with the next one. Compensations appear in
`OrcaRunResult.compensations[]` separate from `actions[]` . The
`FINALLY` stages are not compensable (finally is the cleanup
itself) and run **after** the compensations.
- ✅ **Bus interception (diagnostic bonus, not contract)** — when
a module calls `bus.publish('e', payload)` from within the
body of an action, **if the environment exposes**
`AsyncLocalStorage` , the engine attributes the event to the active
run/action: the resulting envelope is a **child** (depth+1, same
`traceId` , `parentEventId` pointing to the current run,
`emittedByAction` equal to the action's id). Implementation: sync
detection of `globalThis.AsyncLocalStorage` , fallback to
`node:async_hooks` via dynamic import in Node/Bun. In browsers
without `AsyncContext` the field stays `null` and the semantics revert
to root-event.
> **Hard rule**: actions must use `ctx.emit()` whenever
> they need trace attribution / chaining. The
> interception is a diagnostic bonus that improves the trace in
> environments with ALS, not a cross-env contract. An action that
> emits via `bus.publish` gets different results in
> Node/SSR vs. browser without AsyncContext, and the reentry guards do
> not apply the same way when the event enters as root. For
> deterministic fan-out use `ctx.emit()` and leave `bus.publish` for
> genuinely root events (UI input, server message, etc).
- ✅ ** `createActiveOrca()` ** — reactive Svelte 5 wrapper over
`EngineOrca` . Exposes the same engine methods and adds
`$state` -backed snapshots: `runningSnapshot` ,
`recentRunsSnapshot` , `latestRun` , `committedSnapshot` ,
`disposedSnapshot` . The cells are updated in the listener of
`engine.onChange()` (which notifies on register/detach, run-start,
run-complete, `commit()` , `dispose()` ) — without `$effect` to
avoid `effect_update_depth_exceeded` . The wrapper has no
orchestration logic of its own; only the reactive layer. The non-
reactive APIs (`running`, `committed` , `disposed` , `recentRuns()` )
remain available for sync programmatic readers.
## Roadmap v2
List of pending improvements and features, ordered by category. They are not
accepted in the v1 public contract — they may change freely before landing.
### Explicitly deferred from v1
- **`'replace-current'` policy** — the "strong" variant of
`'replace-queued'` (`takeLatest` style) that aborts the in-flight
run in addition to discarding the queued ones. Today `'replace-queued'` only
affects the queue.
- **Per-event concurrency lane for non-parallel policies** — today
all `fifo` / `replace-queued` / `drop-latest` events
share a single serialized global "lane" (at least one non-parallel
run at a time in the whole engine). For apps with many independent
flows that do not want a parallel-policy on each
one, a lane per event would be an upgrade. It requires extending
`canStartRun` with per-event tracking and reviewing the cross-event
sequencing invariants.
- **Cross-event transactions** — a `transaction` today lives in a
single event; spanning across events needs to reconcile tracing
and compensation order.
- **Nested transactions** — nested txs with hierarchical
compensation (partial rollback vs. propagation to the parent tx).
### Original roadmap (`README` v0/v1, untouched)
- **Retry policies** per action (with backoff: linear, exponential,
jitter; max `retryCount` ).
- **Concurrency limits** — a maximum of N simultaneous runs at engine
level or per event (bounds `parallel` and works with
`actionTimeoutMs` ).
- **Tokens with typed payload** — the `payload` field today is
`unknown` ; authors cast it. v2 could introduce a
typed registry `OrcaTokenSchema<Token, Payload>` for end-to-end
inference.
- **Per-event graph visualization** — a tool that draws the
graph `(stages, actions, after, provides, fanIn, transaction)` and
highlights `validate()` issues.
- **Active inspector** — devtools/UI at runtime (list of runs,
token trace, wave timeline, queue state).
- **Visible App presets** — a catalog of predefined
orchestrations (cache-clear-on-revoke, etc.) discoverable from the
app; today they are loose helpers.
- **Composite tests** with `session` , `perm` , `cache` , `connection` ,
`http` — end-to-end scenarios that validate the composition of
arts via orca.
### Future `active-server`
- The same conceptual model ported to Go/Rust on the server.
- Server actions with DB tx / outbox.
- A bridge of server → client events.
- Replay/resume by `runId` / `eventId` .
- Durable audit.