Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion docs/howto/run-ooda-daemon.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
---
title: How to run the OODA daemon
description: Procedure for installing Simard through the canonical installer rail, running the OODA and Signal services as user-level systemd units, and verifying the autonomous loop.
last_updated: 2026-07-09
last_updated: 2026-07-30
review_schedule: as-needed
owner: simard
doc_type: howto
related:
- ../reference/actor-session-startup-purge.md
- ../reference/simard-installer.md
- ../reference/simard-cli.md
- ../architecture/overview.md
Expand Down Expand Up @@ -91,6 +92,22 @@ journalctl --user -u simard-signal.service -n 100 --no-pager

The OODA service should emit cycle summaries. The Signal service is expected to stay running when the `[signal]` table is configured; if Signal is not configured, it exits or reports the missing configuration according to the Signal channel contract.

!!! note "Actor-session startup cleanup (#5005)"
Every OODA daemon start clears prior-process actor sessions before
goal-cycle work.

The cleanup makes restarts, state-root migrations, and changes to
`SIMARD_OBSERVE_ONLY` self-healing even when copied leases have future expiry
times. It affects only the transient `actor_sessions` table in
`$SIMARD_HOME/typed-ooda/outcomes.sqlite3`; durable outcomes, requests, effects,
and claims must remain intact.

The startup path fails if the ledger cannot be opened or purged instead of
running cycles against stale lease state. Do not work around startup failures
with a manual SQLite `DELETE`; correct the reported path, permission, lock, or
storage error and restart the service. See the
[actor-session startup purge reference](../reference/actor-session-startup-purge.md).

If your provider depends on environment variables, import them into the user
systemd manager before restarting services:

Expand Down Expand Up @@ -210,6 +227,7 @@ See [How to set up the Signal channel](./set-up-the-signal-channel.md).

## See also

- [Actor-session startup purge](../reference/actor-session-startup-purge.md)
- [Simard installer reference](../reference/simard-installer.md)
- [Simard CLI reference](../reference/simard-cli.md)
- [Daemon mode](../daemon-mode.md)
Expand Down
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Simard documentation
description: Start here for the shipped `simard` operator CLI, typed-capability OODA execution, verified-backup installation, the repo-grounded engineer loop, runtime contracts, and benchmark flow.
last_updated: 2026-07-14
last_updated: 2026-07-30
review_schedule: as-needed
owner: simard
---
Expand Down Expand Up @@ -29,6 +29,7 @@ Terminal sessions and repo-grounded engineer runs now bridge through one explici
- [Reference: `simard ooda record-decision` (typed decision tool)](./reference/ooda-record-decision-cli.md) — the zero-privilege CLI the per-goal-cycle reasoner calls to record exactly one typed, closed-enum decision, replacing the forbidden "recipe emits JSON → Rust scrapes prose → Rust acts" pattern on the core decision path (#4720). Covers the `PerGoalDecisionRecord` file format, the fail-CLOSED `read_verified` matrix (absent/malformed/wrong-schema/out-of-enum/goal-or-cycle-mismatch ⇒ safe no-op, #1711), configuration, and security.
- [Reference: `simard ooda record-orient` / `record-decide` (typed OODA orient+decide tools)](./reference/ooda-record-orient-decide-cli.md) — the two zero-privilege CLIs the orient and decide reasoners call to record one typed judgment each, extending the `record-decision` pattern to the core OODA orient (urgency demotion) and decide (action routing) phases as Group A of epic #4719. Covers the `OrientDecisionRecord` / `DecideDecisionRecord` file formats, the shared writer/reader chokepoints, the fail-CLOSED R1–R8 read matrix (absent/malformed/wrong-schema/out-of-enum/out-of-range/escalation/goal-or-cycle-mismatch ⇒ safe no-op, #1711), configuration, and security.
- [Typed OODA goal-session deterministic rails](./reference/typed-ooda-goal-session-rails.md) — the two thin rail fixes that unblocked live OODA goals (#4076): propagating `AMPLIHACK_AGENT_BINARY` to the goal-session `recipe-runner-rs` subprocess (no silent `claude` fallback) and normalizing bare goal repo names to `rysweet/<name>` at spawn admission, plus the additive Act-loop failure-detail log.
- [Actor-session startup purge](./reference/actor-session-startup-purge.md) — the #5005 daemon-startup contract for clearing prior-process actor leases from the canonical typed-OODA ledger before goal-cycle work while preserving durable records and fail-closed live scope enforcement.
- [Tutorial: Complete a typed OODA cycle](./tutorials/complete-a-typed-ooda-cycle.md) - Deterministic action, no-action, replay, and conflict examples.
- [Tutorial: Run your first local session](./tutorials/run-your-first-local-session.md) - Exercise the local runtime through the primary CLI.
- [Simard installer reference](./reference/simard-installer.md) - Shipped deployment contract for the binary, the owned `~/.local/bin/simard` PATH entrypoint and stale-orphan reconciliation, prompt assets, user systemd units, the post-deploy version-parity gate, rollback artifacts, and dry-run controls.
Expand Down
14 changes: 11 additions & 3 deletions docs/reference/actor-session-scope-key-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ description: >
re-leasing a stable session_id with a new cycle_id is legitimate, the
unchanged actor_sessions upsert, and the regression test list. Fixes the
false AuthorizationScopeViolation crash-loop on PERPETUAL/STANDING goals.
last_updated: 2026-07-19
last_updated: 2026-07-30
review_schedule: as-needed
owner: simard
doc_type: reference
status: implemented
related:
- ./actor-session-startup-purge.md
- ./stable-goal-session-identity-api.md
- ./ooda-capability-api.md
- ../concepts/stable-goal-session-identity.md
Expand Down Expand Up @@ -52,8 +53,11 @@ changes on every tick.
Actor-session leases in the typed-OODA ledger last 30 days
([`route.rs`](https://github.com/rysweet/Simard/blob/main/src/typed_ooda/route.rs),
`ACTOR_SESSION_LEASE = Duration::from_secs(30 * 24 * 60 * 60)`).
`register_actor_session` only `DELETE`s **expired** rows, so a live lease never
clears between the ~7-minute cycle retries of a running goal.
Within a running daemon, `register_actor_session` only `DELETE`s **expired**
rows, so a live lease never clears between the ~7-minute cycle retries of a
running goal. The daemon clears all inherited actor-session leases once at
startup; see the
[actor-session startup purge](./actor-session-startup-purge.md).

The `AuthorizationScopeViolation` guard previously compared the **entire**
`ActorBinding`, including the per-cycle `cycle_id`. On re-lease:
Expand Down Expand Up @@ -263,6 +267,10 @@ so token rotation is preserved.
`validate_identifier` trust boundary are untouched.
- **Request-replay idempotency** (`replay_request(request_id, "actor_session", fingerprint)`)
is untouched; a retried `request_id` still returns the prior lease.
- **The daemon startup lifecycle** clears prior-process actor sessions before
goal-cycle work. It does not change this in-process scope guard or run from
`CapabilityHandler::open`; see the
[actor-session startup purge](./actor-session-startup-purge.md).

## Security invariants

Expand Down
253 changes: 253 additions & 0 deletions docs/reference/actor-session-startup-purge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
---
title: "Reference: Actor-session startup purge"
description: Contract for clearing transient typed-OODA actor-session leases at daemon startup while preserving durable ledger records and live scope enforcement.
last_updated: 2026-07-30
review_schedule: as-needed
owner: simard
doc_type: reference
status: implemented
related:
- ./actor-session-scope-key-api.md
- ./stable-goal-session-identity-api.md
- ./ooda-capability-api.md
- ../howto/run-ooda-daemon.md
- ../daemon-mode.md
---

# Reference: Actor-session startup purge

The OODA daemon deletes persisted `actor_sessions` rows once during startup.
Actor-session leases represent actors that are active in the current daemon
process. No actor session remains in flight after that process exits, so carrying
these leases into a new process is invalid even when their `expires_at` value is
still in the future.

The purge lets a stable per-goal `session_id` bind to the identity and
authorization scope of the new daemon process after a restart, host migration,
state-root migration, or observe-only posture change. It does not relax the
runtime scope guard: changing the immutable scope of a session that is already
registered in the current process still returns
`AuthorizationScopeViolation`.

## Startup contract

`run_ooda_daemon()` must perform actor-session cleanup in this order:

1. Resolve the daemon state root.
2. Create the state-root directory.
3. Resolve the ledger with `typed_ooda::ledger_path(state_root)`.
4. Ensure the canonical ledger parent directory exists.
5. Open the typed-OODA ledger.
6. Call `CapabilityHandler::purge_actor_sessions()`.
7. Continue with the remaining daemon initialization and goal-cycle work.

The canonical ledger path is:

```text
<state-root>/typed-ooda/outcomes.sqlite3
```

The purge must run exactly once per `run_ooda_daemon()` invocation. It must not
run:

- in `CapabilityHandler::open()`;
- during schema initialization or migration;
- when an operator opens the ledger through another command;
- before or after each goal cycle;
- during actor-session registration.

Opening and purging the ledger are part of daemon startup. If either
operation fails, `run_ooda_daemon()` must return the error and must not begin
goal-cycle work. The daemon must never continue with stale actor-session state
after a failed purge.

## Internal ledger operation

The purge is crate-visible for daemon startup use. It is not part of the
public `CapabilityHandler` API for downstream consumers.

```rust
impl CapabilityHandler {
/// Delete every transient actor-session lease.
///
/// This operation is intended for authoritative daemon startup, when no
/// actor session can still be in flight. It is idempotent and returns
/// persistence failures through `CapabilityResult`.
pub(crate) fn purge_actor_sessions(&self) -> CapabilityResult<()>;
}
```

The operation must execute one hard-coded SQL statement:

```sql
DELETE FROM actor_sessions
```

Deleting zero rows must succeed. SQLite open, lock, and execution errors must use
the existing typed-OODA persistence error conversion and be returned to the
caller. The method must not accept SQL, a table name, a state root, or a filter
from the caller.

The daemon owns the lifecycle decision. `CapabilityHandler` exposes the
fallible deletion primitive but does not call it automatically.

## Data lifecycle

`actor_sessions` is transient authentication state. Its rows contain the
current token hash, expiry, cycle metadata, actor identity, repository binding,
grants, engineer permissions, working-directory binding, and observe-only
posture for an in-flight actor.

All other typed-OODA ledger data must remain durable. The startup purge must not
delete or rewrite:

- `terminal_outcomes`;
- `progress_records`;
- `mutation_requests`;
- `mutation_scope_counters`;
- `effect_jobs`;
- `engineer_claims`;
- approvals, process executions, or authorization records;
- schema metadata.

Request replay records must be preserved. A fresh actor registration after
restart must therefore use a fresh `request_id`, as every new registration
already does.

The existing expiry sweep must remain in place:

```sql
DELETE FROM actor_sessions WHERE expires_at < ?1
```

That sweep removes expired rows during schema creation or migration and
ordinary session registration, not on every ledger initialization. The
startup purge has a different purpose: it removes all leases inherited from a
process that no longer exists, including future-dated leases.

## Scope enforcement after startup

The immutable actor-session scope key continues to contain:

| Field | Runtime behavior |
| --- | --- |
| Actor identity | A change on the same live `session_id` is rejected. |
| Repository | A change on the same live `session_id` is rejected. |
| Capability grants | A change on the same live `session_id` is rejected. |
| Engineer permissions | A change on the same live `session_id` is rejected. |
| Working directory | A change on the same live `session_id` is rejected. |
| Observe-only posture | A change on the same live `session_id` is rejected. |

`cycle_id`, `goal_id`, and the rotating token hash must remain outside the
immutable scope key. See the
[actor-session scope-key API](./actor-session-scope-key-api.md) for the complete
registration contract.

The target lifecycle boundary must produce these results:

| Situation | Result |
| --- | --- |
| Same process, same stable session ID, same immutable scope | Registration may refresh the lease. |
| Same process, same stable session ID, changed immutable scope | `AuthorizationScopeViolation`. |
| New daemon process, stale persisted lease, changed immutable scope | Startup deletes the stale row; the first new registration succeeds. |
| Startup cannot open or purge the ledger | Daemon startup fails before goal-cycle work. |

## Configuration

The purge must have no enable/disable switch, retention period, row limit, or
separate database setting.

| Input | Effect |
| --- | --- |
| Daemon state root | Selects the canonical ledger at `typed-ooda/outcomes.sqlite3`. |
| `SIMARD_STATE_ROOT` | Overrides the runtime state root when supported by the invoking command or service. |
| `SIMARD_HOME` | Supplies the installed service state root unless a deliberate state-root override is used. |
| `SIMARD_OBSERVE_ONLY` | Contributes to each new actor's immutable scope; after implementation, changing it across a restart is safe because startup clears prior-process leases. |
| Identity, repository, and working directory | Contribute to the new actor scope but do not change purge behavior. |

`drain.conf` and identity posture can affect whether the new daemon runs
observe-only, but neither may change the cleanup rule. Startup must always
remove prior-process actor sessions.

The lifecycle assumes one authoritative daemon process per state root.
Concurrent daemons sharing one state root are unsupported because one daemon's
startup could invalidate another daemon's live actor sessions.

## Usage examples

### Restart after changing observe-only posture

Set the intended service environment, then restart the daemon:

```bash
systemctl --user restart simard-ooda.service
journalctl --user -u simard-ooda.service -n 100 --no-pager
```

The new process clears actor sessions from its canonical typed-OODA ledger
before it starts a goal cycle. No manual SQLite deletion is required.

For a bounded foreground check against an explicit state root:

```bash
SIMARD_OBSERVE_ONLY=1 \
simard ooda run --cycles=1 "$HOME/.simard"
```

The same startup cleanup must run before the bounded cycle.

### Move a state root to another host

After transferring the durable state root, start the daemon normally on the new
host:

```bash
simard ooda run --cycles=1 "$HOME/.simard"
```

Future-dated actor leases copied with `typed-ooda/outcomes.sqlite3` must be
discarded at startup. Durable outcomes, effect records, claims, and request
history must remain available.

Do not add an operator script that runs
`DELETE FROM actor_sessions`. The daemon startup lifecycle owns this cleanup and
surfaces failures through its normal startup error path.

## Regression contract

The persisted-SQLite regression test must use the real canonical ledger path
and prove all three required transitions:

1. Register a stable session ID with a future-dated lease under one immutable
scope, then close that handler so the row is persisted.
2. Run the daemon startup purge and register the same session ID under a
different scope, including a changed `observe_only` value. Registration
succeeds because the prior-process row is gone.
3. Reuse the same running handler and attempt another scope change with a new
request ID. Registration fails with
`CapabilityErrorCode::AuthorizationScopeViolation`.

Unique request IDs are required at each registration so request replay cannot
mask the lifecycle behavior. The test uses a future expiry so the ordinary
expiration sweep cannot produce a false positive.

## Security properties

- **Startup-only authority:** production code must invoke the purge only from
`run_ooda_daemon()` before any goal-cycle work.
- **Fail-visible startup:** ledger open and purge failures must abort startup.
- **Narrow deletion:** one fixed statement must delete only `actor_sessions`.
- **Live protection preserved:** the runtime scope-key comparison and
`AuthorizationScopeViolation` must remain unchanged.
- **No secret logging:** startup must not log actor identities, token hashes,
scope keys, or row contents.
- **Durable history preserved:** outcomes, requests, effects, claims, and other
authorization history must survive daemon restarts.

## Related

- [Actor-session scope-key API](./actor-session-scope-key-api.md)
- [Stable goal-session identity API](./stable-goal-session-identity-api.md)
- [OODA capability API](./ooda-capability-api.md)
- [How to run the OODA daemon](../howto/run-ooda-daemon.md)
- [Daemon mode](../daemon-mode.md)
12 changes: 11 additions & 1 deletion docs/reference/ooda-capability-api.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
---
title: OODA capability API
description: Implemented typed terminal, authorization, idempotency, actor-session, and effect-outbox contracts for parser-free goal-session execution.
last_updated: 2026-07-14
last_updated: 2026-07-30
review_schedule: as-needed
owner: simard
doc_type: reference
status: implemented
related:
- ./actor-session-startup-purge.md
- ../architecture/typed-ooda-loop.md
- ../howto/spawn-engineers-from-ooda-daemon.md
- ../operations/deploy-and-roll-back-typed-ooda.md
Expand All @@ -33,6 +34,15 @@ The random token is passed through an owner-private context file. `ooda
terminal` must present that token and the exact session, cycle, and goal.
Expired or mismatched sessions return `Unauthenticated`.

Actor sessions are process-transient even though their SQLite rows are
persistent. The authoritative OODA daemon clears every inherited
`actor_sessions` row once at startup, before goal-cycle work. This lets the
stable per-goal session ID bind to the new process's scope after a restart or
posture change. A scope change against a session registered in the current
process still returns
`AuthorizationScopeViolation`. See the
[actor-session startup purge](./actor-session-startup-purge.md).

The terminal actor's tool schemas do not accept caller-supplied session, cycle,
goal, repository authority, or grants. Those values come from the authenticated
session and invocation.
Expand Down
Loading
Loading