--- id: mem-engine-scheduler-outbox name: Mem engine — scheduler (`schedule`/`unschedule`/`run_schedules`) and outbox helper status: pending depends_on: [mem-engine-queues, mem-engine-locks] scope: moderate risk: medium impact: component level: implementation tags: [wave-6, mem-engine, scheduler, outbox] --- ## Description The collapsed scheduler and the outbox helper ([ADR-009](../architecture/decisions/009-scheduler-collapse.md), [ADR-014](../architecture/decisions/014-outbox-tx-enqueue.md)): - **Scheduler surface**: `schedule(name, spec, queue, payload, opts)` (upsert by name; *both* name-bearing arguments validated — schedule name and queue argument identically, `InvalidName`/`ReservedName`), `unschedule(name) -> bool`, and the opt-in `run_schedules(stop: StopToken)` runner over a mem-owned tick task. - **Tick machinery — engine-uniform** ([ADR-009](../architecture/decisions/009-scheduler-collapse.md) §4's rule; single-instance leadership is trivially owned but the machinery still runs): each boundary fire enqueues ordinary work stamped with the schedule's `ScheduleOpts` resolved over the target queue's derived engine defaults; fire+advance is atomic under the guarded state (the no-double-fire floor); **bounded catch-up with the fixed 64-boundary cap and skip-forward past the cap** (mem-owned tick implementation — third owner per [ADR-012](../architecture/decisions/012-forked-substrate-design.md) §2); leadership rides the reserved `__alkstore_scheduler` lock through the lock mechanism (so two runners on one instance contend through the same machinery the other engines use; loss → `Err(LeadershipLost)`, clean stop → `Ok(())`). - **Spec grammar v1**: `@every ` (`s|m|h|d`) only — mem owns its parser (third copy, the grammar is small and pinned); cron strings rejected (`InvalidSpec`). Boundary waits ride tokio time. - **Outbox helper**: `Outbox { enqueue, run_once }` per [ADR-019](../architecture/decisions/019-mechanism-handle-surfaces.md) §5 — `run_once(worker_id, delivery) -> bool` is pull: claim one, ack on `Ok`, `retry(err, None)` on `Err`; **no engine-issued heartbeat inside delivery** (inherited posture); `outbox_enqueue_tx` staged in the overlay by `mem-engine-seam-tx` (the 60/5/5 stamp set); the derived backing queue `__alkstore_outbox:{outbox}` unreachable by `enqueue_tx` (validation already pins the reserved prefix), name format pinned identical to the other engines (the suite's derived-queue-naming row composes it). Unit tests pin: boundary fires (deterministic, tokio-time driven), bounded catch-up + skip-forward, leadership lock contention/loss through two `run_schedules` calls, outbox run_once ack/retry shapes, derived-name format. ## Acceptance Criteria - [ ] Scheduler surface + tick: boundary fire, catch-up (≤64), skip-forward — unit-pinned with tokio-time determinism - [ ] Leadership contention through the `__alkstore_scheduler` lock; `LeadershipLost` on loss, `Ok(())` on clean stop — pinned - [ ] Schedule name + queue argument validation; `@every`-only parser (cron → `InvalidSpec`) — pinned - [ ] Outbox: `enqueue`/`run_once` semantics (no engine heartbeat); derived backing-queue name format identical to both engines — pinned - [ ] `cargo check --target wasm32-unknown-unknown -p alkstore-mem` clean; workspace gates green ## References - docs/architecture/queues.md; core-contract.md §scheduler, §outbox - docs/architecture/decisions/009-scheduler-collapse.md §1/§3/§4/§6; 014-outbox-tx-enqueue.md; 019 §4/§5; 020 §3 - docs/architecture/engine-mem.md §Mapping the contract (scheduler/outbox row) - tasks/pg-engine-scheduler-outbox.md (the structural twin's task of record — tick/catch-up/leadership shapes) ## Notes > To be filled by implementation agent ## Summary > To be filled on completion