From 1329d778aff3475e1b80c3b205ffa8a88ff4e923 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Wed, 19 Aug 2026 21:53:57 -0300 Subject: [PATCH] docs: sync and archive production release specs --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/production-release/spec.md | 0 .../specs/redis-operations/spec.md | 0 .../specs/scheduler-delivery/spec.md | 0 .../specs/storage-protocol/spec.md | 0 .../specs/task-completion-policies/spec.md | 0 .../specs/task-delivery-safety/spec.md | 0 .../specs/task-events-stream/spec.md | 0 .../specs/task-pinning/spec.md | 0 .../specs/task-relationships/spec.md | 0 .../specs/task-retry-policy/spec.md | 0 .../tasks.md | 0 openspec/specs/production-release/spec.md | 82 +++++++++++++ openspec/specs/redis-operations/spec.md | 69 +++++++++++ openspec/specs/scheduler-delivery/spec.md | 44 +++++++ openspec/specs/storage-protocol/spec.md | 59 ++++++++++ .../specs/task-completion-policies/spec.md | 66 ++++++++--- openspec/specs/task-delivery-safety/spec.md | 82 +++++++++++++ openspec/specs/task-events-stream/spec.md | 110 ++++++++++-------- openspec/specs/task-relationships/spec.md | 95 ++++++++++----- openspec/specs/task-retry-policy/spec.md | 53 +++++++-- 23 files changed, 552 insertions(+), 108 deletions(-) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/.openspec.yaml (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/design.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/proposal.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/production-release/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/redis-operations/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/scheduler-delivery/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/storage-protocol/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/task-completion-policies/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/task-delivery-safety/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/task-events-stream/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/task-pinning/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/task-relationships/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/specs/task-retry-policy/spec.md (100%) rename openspec/changes/{harden-core-for-production-release => archive/2026-08-19-harden-core-for-production-release}/tasks.md (100%) create mode 100644 openspec/specs/production-release/spec.md create mode 100644 openspec/specs/redis-operations/spec.md create mode 100644 openspec/specs/scheduler-delivery/spec.md create mode 100644 openspec/specs/storage-protocol/spec.md create mode 100644 openspec/specs/task-delivery-safety/spec.md diff --git a/openspec/changes/harden-core-for-production-release/.openspec.yaml b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/.openspec.yaml similarity index 100% rename from openspec/changes/harden-core-for-production-release/.openspec.yaml rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/.openspec.yaml diff --git a/openspec/changes/harden-core-for-production-release/design.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/design.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/design.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/design.md diff --git a/openspec/changes/harden-core-for-production-release/proposal.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/proposal.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/proposal.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/proposal.md diff --git a/openspec/changes/harden-core-for-production-release/specs/production-release/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/production-release/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/production-release/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/production-release/spec.md diff --git a/openspec/changes/harden-core-for-production-release/specs/redis-operations/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/redis-operations/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/redis-operations/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/redis-operations/spec.md diff --git a/openspec/changes/harden-core-for-production-release/specs/scheduler-delivery/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/scheduler-delivery/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/scheduler-delivery/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/scheduler-delivery/spec.md diff --git a/openspec/changes/harden-core-for-production-release/specs/storage-protocol/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/storage-protocol/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/storage-protocol/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/storage-protocol/spec.md diff --git a/openspec/changes/harden-core-for-production-release/specs/task-completion-policies/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-completion-policies/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/task-completion-policies/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-completion-policies/spec.md diff --git a/openspec/changes/harden-core-for-production-release/specs/task-delivery-safety/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-delivery-safety/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/task-delivery-safety/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-delivery-safety/spec.md diff --git a/openspec/changes/harden-core-for-production-release/specs/task-events-stream/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-events-stream/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/task-events-stream/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-events-stream/spec.md diff --git a/openspec/changes/harden-core-for-production-release/specs/task-pinning/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-pinning/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/task-pinning/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-pinning/spec.md diff --git a/openspec/changes/harden-core-for-production-release/specs/task-relationships/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-relationships/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/task-relationships/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-relationships/spec.md diff --git a/openspec/changes/harden-core-for-production-release/specs/task-retry-policy/spec.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-retry-policy/spec.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/specs/task-retry-policy/spec.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/specs/task-retry-policy/spec.md diff --git a/openspec/changes/harden-core-for-production-release/tasks.md b/openspec/changes/archive/2026-08-19-harden-core-for-production-release/tasks.md similarity index 100% rename from openspec/changes/harden-core-for-production-release/tasks.md rename to openspec/changes/archive/2026-08-19-harden-core-for-production-release/tasks.md diff --git a/openspec/specs/production-release/spec.md b/openspec/specs/production-release/spec.md new file mode 100644 index 0000000..23a2593 --- /dev/null +++ b/openspec/specs/production-release/spec.md @@ -0,0 +1,82 @@ +# production-release + +## Purpose + +Defines the compatibility, verification, packaging, documentation, security, +and publication evidence required before effectmq is presented as +production-ready. + +## Requirements + +### Requirement: Compatibility matrix is tested + +The release SHALL publish and continuously test its supported Node.js, Redis, +Effect, RESP, and Redis-client versions and topologies. + +#### Scenario: Supported matrix entry +- **WHEN** a version combination is listed as supported +- **THEN** its build, consumer typecheck, integration suite, and fault smoke tests pass in CI + +### Requirement: Candidate uses the current Effect beta baseline + +Immediately before cutting the candidate, the release SHALL resolve npm's +`beta` dist-tag for `effect` and every direct `@effect/*` dependency, record the +exact resolved versions, update the peer minimum and development dependencies, +and rerun all release gates. A newer `rc` dist-tag SHALL NOT replace the beta +baseline unless the release plan is explicitly changed. + +#### Scenario: Effect beta has advanced +- **WHEN** npm's `beta` dist-tag differs from the versions in the candidate manifest +- **THEN** the manifest, lockfile, documentation, and packed-consumer fixture are updated to the resolved beta versions +- **AND** the full correctness, compatibility, package, benchmark, and soak gates pass again + +### Requirement: Correctness and fault gates pass + +Release CI SHALL pass reference-state-model/property tests, crash and +lease-loss tests, Redis restart and Sentinel failover tests, script-cache-loss +tests, clock-skew tests, and mixed-version upgrade/rollback tests. + +#### Scenario: Release commit is published +- **WHEN** a package version is eligible for publication +- **THEN** every required correctness and fault job has succeeded for that exact commit + +### Requirement: Performance bounds are published + +The release SHALL publish reproducible throughput and p50/p95/p99 latency +measurements across documented backlog, payload, concurrency, and expiry-sweep +sizes, including the maximum configured atomic batch. + +#### Scenario: Adversarial backlog benchmark +- **WHEN** the benchmark runs with a large due-task and expired-lease backlog +- **THEN** no single atomic queue operation exceeds the documented work bound + +### Requirement: Packed package is consumer-tested + +CI SHALL build and pack the package, assert the intended file list and +generated-artifact drift, and install the tarball into representative ESM +consumer projects that exercise every public export. + +#### Scenario: Experimental source is present in the tarball +- **WHEN** the packed file list includes scratchpad, test, or unintended generated declarations +- **THEN** the release gate fails + +### Requirement: Release documentation is complete + +The release SHALL include accurate delivery guarantees, task-relationship +language, supported topology and compatibility tables, upgrade/rollback and +operations guides, security policy, contribution guide, and pre-1.0 +compatibility policy. + +#### Scenario: Exactly-once wording remains +- **WHEN** release documentation validation finds an exactly-once handler-execution claim +- **THEN** the release gate fails + +### Requirement: Publication is provenance-bearing and gated + +Publishing SHALL depend on the successful release gate for the exact commit and +SHALL use npm trusted publishing with generated provenance rather than a +long-lived publication token. + +#### Scenario: Release gate has not passed +- **WHEN** the publication workflow runs for a commit without a successful release gate +- **THEN** no npm package is published diff --git a/openspec/specs/redis-operations/spec.md b/openspec/specs/redis-operations/spec.md new file mode 100644 index 0000000..228dcfc --- /dev/null +++ b/openspec/specs/redis-operations/spec.md @@ -0,0 +1,69 @@ +# redis-operations + +## Purpose + +Defines the supported Redis deployment contract and operational bounds needed +to run the queue safely through restarts, failover, backlog spikes, and +retention cleanup. + +## Requirements + +### Requirement: Supported Redis topologies are explicit + +The release SHALL support standalone Redis and Sentinel-managed non-sharded +primary/replica deployments. Redis Cluster SHALL be documented as unsupported. + +#### Scenario: Cluster configuration is supplied +- **WHEN** a user attempts to configure Redis Cluster +- **THEN** startup fails with an unsupported-topology error before processing tasks + +### Requirement: Application scripts recover after cache loss + +Queue scripts SHALL be content-addressed, loaded into the application script +cache, invoked by digest, and transparently reloaded once when Redis reports a +missing script. + +#### Scenario: Redis script cache is flushed +- **WHEN** the script cache is cleared between queue operations +- **THEN** the next operation reloads the expected script and retries without duplicating the transition + +### Requirement: Mixed application versions do not replace each other's code + +Starting one library version SHALL NOT globally replace the Redis-side +implementation used by another running version. + +#### Scenario: Rolling application upgrade +- **WHEN** old and new workers start in either order against the same supported Redis deployment +- **THEN** each invokes the script version matching its own storage protocol + +### Requirement: Maintenance work is bounded + +Lease recovery, delayed promotion, retention trimming, relationship release, +and list inspection SHALL process configurable bounded batches and SHALL expose +a continuation until work is complete. + +#### Scenario: Thousands of leases expire together +- **WHEN** more leases expire than the configured sweep batch size +- **THEN** one atomic operation processes at most the configured batch +- **AND** remaining work stays discoverable for subsequent sweeps + +### Requirement: Retention is configurable and observable + +Task records, terminal results, dead-letter entries, and lifecycle events SHALL +have configurable age/count retention. Health and metrics SHALL expose queue +depth, oldest age, sweep lag, Redis errors, and retention failures. + +#### Scenario: Event stream reaches its configured limit +- **WHEN** appending an event would exceed the configured retention window +- **THEN** old events are trimmed within the documented approximation +- **AND** consumers can determine the earliest resumable cursor + +### Requirement: Deployment safety requirements are documented + +The production guide SHALL define persistence, `noeviction`, backup/restore, +replication data-loss windows, ACLs, TLS, timeouts, reconnection, graceful +shutdown, and indeterminate-write behavior. + +#### Scenario: Producer loses connection after sending an offer +- **WHEN** the producer cannot determine whether Redis committed the offer +- **THEN** the API returns an indeterminate-write error that instructs retry with the same idempotency identity diff --git a/openspec/specs/scheduler-delivery/spec.md b/openspec/specs/scheduler-delivery/spec.md new file mode 100644 index 0000000..37c8719 --- /dev/null +++ b/openspec/specs/scheduler-delivery/spec.md @@ -0,0 +1,44 @@ +# scheduler-delivery + +## Purpose + +Defines recurring scheduling as durable creation of idempotently identified +queue tasks so process failure cannot silently lose an already-consumed tick. + +## Requirements + +### Requirement: Every due tick maps to one task identity + +A scheduled definition SHALL derive a deterministic task identity from the +schedule name and nominal tick time and SHALL offer that task idempotently. + +#### Scenario: Two scheduler processes observe the same tick +- **WHEN** two processes attempt to materialize the same due tick +- **THEN** exactly one task generation exists for that schedule and tick identity + +### Requirement: Tick execution uses queue delivery semantics + +Scheduled handlers SHALL run through the managed queue worker and SHALL inherit +its at-least-once delivery, lease, retry, retention, and observability behavior. + +#### Scenario: Scheduler process dies after offering +- **WHEN** the scheduler process dies after the tick task is stored but before a worker executes it +- **THEN** the task remains eligible for normal queue processing + +### Requirement: Missed-tick policy is explicit + +A schedule SHALL declare whether startup skips, coalesces, or backfills ticks +missed while no scheduler was running, including a maximum backfill count. + +#### Scenario: Coalescing missed ticks +- **WHEN** a coalescing schedule restarts after multiple nominal ticks were missed +- **THEN** it creates one task representing the documented coalesced interval + +### Requirement: Scheduler API does not claim exactly-once execution + +Scheduler documentation SHALL distinguish idempotent tick creation from +at-least-once handler execution. + +#### Scenario: Tick task is retried +- **WHEN** the worker loses its lease after beginning a scheduled handler +- **THEN** the same tick task may execute again under normal retry semantics diff --git a/openspec/specs/storage-protocol/spec.md b/openspec/specs/storage-protocol/spec.md new file mode 100644 index 0000000..168c4fe --- /dev/null +++ b/openspec/specs/storage-protocol/spec.md @@ -0,0 +1,59 @@ +# storage-protocol + +## Purpose + +Defines a durable, lossless, versioned storage contract for typed task values +and metadata across compatible releases and rolling deployments. + +## Requirements + +### Requirement: User values round-trip losslessly + +Payloads, successes, and typed failures SHALL remain opaque to Redis-side logic +and SHALL decode to the same supported value, including nested nulls, empty +arrays/maps, Unicode, binary values, and safe numeric values. + +#### Scenario: Typed failure contains nested nulls +- **WHEN** a typed failure containing nested null values is stored and read +- **THEN** the decoded failure is deeply equal to the encoded failure + +### Requirement: Stored records carry a protocol version + +Every task generation and event record SHALL identify its storage protocol +version and task-schema identity. Readers SHALL reject unsupported versions +with a typed compatibility error rather than mis-decoding them. + +#### Scenario: New reader encounters an unsupported record +- **WHEN** a reader encounters a record with an unsupported protocol version +- **THEN** it fails with an error that identifies the encountered and supported versions + +### Requirement: Corruption is never normalized into valid empty data + +Malformed bytes, invalid field types, and structurally invalid collections +SHALL fail decoding in the typed error channel. Only explicitly documented +canonical representations may normalize to an equivalent value. + +#### Scenario: Error history decodes to a map +- **WHEN** the stored error-history field contains a non-list value +- **THEN** decoding fails instead of returning an empty history + +### Requirement: Rolling compatibility is declared + +Each release SHALL declare which protocol versions it can read and write. A +rolling upgrade SHALL either preserve a mutually readable write version or +require an explicit migration before mixed-version workers start. + +#### Scenario: Mixed-version deployment +- **WHEN** old and new supported workers run concurrently +- **THEN** every record they exchange uses a protocol version readable by both + +### Requirement: Encoded values are size bounded + +The producer and worker APIs SHALL reject payloads, results, failures, error +histories, and relationship sets that exceed configured encoded-size or count +limits before an unbounded Redis operation occurs. + +#### Scenario: Oversized result +- **WHEN** a handler returns a result larger than the configured maximum +- **THEN** acknowledgement fails with a typed size-limit error +- **AND** the task follows the configured terminal handling policy diff --git a/openspec/specs/task-completion-policies/spec.md b/openspec/specs/task-completion-policies/spec.md index c4d2aba..6c55cc8 100644 --- a/openspec/specs/task-completion-policies/spec.md +++ b/openspec/specs/task-completion-policies/spec.md @@ -18,32 +18,60 @@ policy without consuming remaining handler retries. ### Requirement: onFailurePolicy placement -After handler retries are exhausted or skipped, the engine SHALL settle the -generation as failed. `delete` removes its record when no retention hold -prevents disposal; `mark-as-failure` indexes and retains it; `keep` retains it -outside terminal indexes. Active holds SHALL affect disposal only and SHALL NOT -hide terminal state. +After retries are exhausted or skipped, the engine SHALL settle the task as +failed and apply `onFailurePolicy`: `delete` removes the task from execution +indexes and deletes its record once no explicit result-retention holds remain; +`mark-as-failure` places it in the failed index and retains its record according +to configured retention; `keep` retains the record outside terminal indexes +according to configured retention. Active holds SHALL affect record disposal +only and SHALL NOT hide the terminal execution state. -#### Scenario: Marked failure with a hold -- **WHEN** a held task settles with `mark-as-failure` -- **THEN** it appears in the failed index immediately and remains readable +#### Scenario: Delete policy without holds +- **WHEN** a terminally failed task has delete policy and no result-retention holds +- **THEN** it appears in no execution index and its record is deleted + +#### Scenario: Delete policy with a hold +- **WHEN** a terminally failed task has delete policy and an active result-retention hold +- **THEN** it is visibly terminal, appears in no runnable state, and its failure remains readable until the final hold releases + +#### Scenario: Mark-as-failure policy with a hold +- **WHEN** a terminally failed task has mark-as-failure policy and an active hold +- **THEN** it appears in the failed index immediately and its record remains readable + +#### Scenario: Keep failure policy +- **WHEN** a terminally failed task has keep policy +- **THEN** it appears in no execution index and its record is retained until configured expiry or administrative removal ### Requirement: onSuccessPolicy placement -On a valid fenced acknowledgement, `delete` removes the record when allowed, -`mark-as-success` indexes and retains it, and `keep` retains it outside terminal -indexes. Active holds SHALL affect disposal only. +On valid success acknowledgement, the engine SHALL settle the task as succeeded +and apply `onSuccessPolicy`: `delete` removes the task from execution indexes +and deletes its record once no explicit result-retention holds remain; +`mark-as-success` places it in the success index and retains its record according +to configured retention; `keep` retains the record outside terminal indexes +according to configured retention. Active holds SHALL affect record disposal +only and SHALL NOT hide the terminal execution state. #### Scenario: Delete success with a hold -- **WHEN** a held task succeeds under delete policy -- **THEN** it is terminal and non-runnable while its result remains readable +- **WHEN** a succeeded task has delete policy and an active result-retention hold +- **THEN** its success remains readable until the final hold releases +- **AND** it is not runnable or hidden as an unfinished task + +#### Scenario: Mark-as-success policy +- **WHEN** a succeeded task has mark-as-success policy +- **THEN** it appears in the success index immediately and remains according to configured retention + +#### Scenario: Keep success policy +- **WHEN** a succeeded task has keep policy +- **THEN** it appears in no execution index and its record remains readable until configured expiry or administrative removal -### Requirement: Terminal indexes contain terminal tasks +### Requirement: Success and failed lists contain only dead tasks -Success and failed indexes SHALL contain only settled generations. A terminal -generation MAY have active retention holds; membership describes outcome and -does not imply immediate disposability. +The success and failed indexes SHALL contain only terminal tasks. A terminal +task MAY have active result-retention holds; index membership records outcome +and SHALL NOT imply that the task record is immediately disposable. -#### Scenario: Held marked success -- **WHEN** a held task succeeds with `mark-as-success` +#### Scenario: Held terminal task is indexed +- **WHEN** a task with mark-as-success policy succeeds while a result-retention hold is active - **THEN** it appears in the success index immediately +- **AND** its result remains protected from disposal by the hold diff --git a/openspec/specs/task-delivery-safety/spec.md b/openspec/specs/task-delivery-safety/spec.md new file mode 100644 index 0000000..05646b4 --- /dev/null +++ b/openspec/specs/task-delivery-safety/spec.md @@ -0,0 +1,82 @@ +# task-delivery-safety + +## Purpose + +Defines the execution-state and ownership guarantees that make task delivery +safe under retries, crashes, lease expiry, duplicate offers, and concurrent +workers. + +## Requirements + +### Requirement: Delivery guarantee is at-least-once + +The library SHALL describe task execution as at-least-once and SHALL NOT claim +that a handler or its external side effects execute exactly once. + +#### Scenario: Worker dies after an external side effect +- **WHEN** a handler performs an external side effect and the worker dies before acknowledging success +- **THEN** the task becomes eligible for another attempt after ownership expires +- **AND** the documentation identifies idempotent handlers or an inbox/outbox as the consumer's duplicate-safety mechanism + +### Requirement: Every runnable task has exactly one execution state + +A task SHALL be in exactly one execution state among delayed, waiting, leased, +retry-scheduled, succeeded, or failed. Repeating a transition to the current +state SHALL preserve membership and SHALL NOT strand or duplicate the task. + +#### Scenario: Lease renewal preserves active membership +- **WHEN** the current owner renews a leased task +- **THEN** the task remains leased exactly once and remains discoverable for expiry recovery + +#### Scenario: Repeating a waiting transition +- **WHEN** a waiting task is transitioned to waiting again +- **THEN** it remains present exactly once in the waiting state + +### Requirement: Each attempt has a fenced lease + +Every task attempt SHALL receive a unique lease token. Renewal, success, +failure, and release SHALL require the current token, and a token from an +earlier attempt SHALL never mutate the task. + +#### Scenario: Late acknowledgement from an old attempt +- **WHEN** attempt A loses its lease, attempt B acquires a new lease, and attempt A later reports success +- **THEN** the acknowledgement fails with `LeaseLost` +- **AND** attempt B remains the sole owner + +### Requirement: Lease loss stops managed work + +The managed processing API SHALL supervise lease renewal together with the +handler. Missing ownership or a terminal renewal failure SHALL interrupt the +handler and fail processing with a typed ownership error. + +#### Scenario: Heartbeat loses ownership +- **WHEN** lease renewal reports that the token is no longer current +- **THEN** the handler fiber is interrupted +- **AND** the stale attempt does not report a task outcome + +### Requirement: Duplicate offers are state-aware + +Offering an existing id SHALL return the existing task unchanged by default. +Replacement SHALL require an explicit mode and SHALL be rejected for leased or +terminal tasks unless a new generation is requested. + +#### Scenario: Duplicate offer while leased +- **WHEN** a producer offers an id that is currently leased +- **THEN** the existing payload, lease, history, and state remain unchanged +- **AND** the producer receives the existing-task outcome + +#### Scenario: New generation after terminal outcome +- **WHEN** a producer explicitly requests a new generation for a terminal task id +- **THEN** the new task has a distinct generation identity and no fields from the terminal generation are retained implicitly + +### Requirement: Stalled recovery is bounded + +Lease-expired attempts SHALL be counted separately from handler failures and +SHALL be retried only up to a configurable `maxStalledCount`. Exceeding the +limit SHALL produce a terminal stalled failure eligible for dead-letter +retention. + +#### Scenario: Repeated worker crashes +- **WHEN** a task exceeds its configured stalled-attempt limit +- **THEN** it is not returned to waiting +- **AND** it settles with a typed terminal stalled error diff --git a/openspec/specs/task-events-stream/spec.md b/openspec/specs/task-events-stream/spec.md index bb9f1ed..0ce3c5d 100644 --- a/openspec/specs/task-events-stream/spec.md +++ b/openspec/specs/task-events-stream/spec.md @@ -2,82 +2,96 @@ ## Purpose -Defines how the TaskEngine publishes task-lifecycle events to a per-queue Redis Stream and exposes them as a decoded Effect `Stream`, and how TaskQueue builds typed streaming, `wait`, and `execute` on top of it. +Defines how the TaskEngine publishes task-lifecycle events to a per-queue Redis +Stream and exposes them as a decoded Effect `Stream`, and how TaskQueue builds +typed streaming, `wait`, and `execute` on top of it. ## Requirements ### Requirement: Engine publishes task-lifecycle events -The TaskEngine SHALL publish an event to a per-queue Redis Stream (`::events`) whenever a task's lifecycle state changes. The emitted event types SHALL be: `task.created`, `task.updated`, `task.failed`, `task.completed`, and `task.moved`. Each event SHALL carry the task id and a tag-specific payload. +The TaskEngine SHALL publish a versioned event whenever a task's lifecycle +state changes. Events SHALL include a stable event id, task generation +identity, event tag, protocol version, and tag-specific payload. Returning an +unchanged task for a duplicate offer SHALL NOT emit an update event; an explicit +accepted replacement SHALL emit `task.updated`. #### Scenario: Task created emits task.created +- **WHEN** a new task generation is created +- **THEN** a `task.created` event is published carrying its generation identity and new state -- **WHEN** a task is created that did not previously exist -- **THEN** a `task.created` event is published carrying the new task +#### Scenario: Duplicate offer returns unchanged +- **WHEN** a create is issued for an existing task without an accepted replacement +- **THEN** the existing task is returned and no lifecycle update event is emitted -#### Scenario: Existing task re-created emits task.updated +#### Scenario: Task state changes +- **WHEN** a task moves between delayed, waiting, leased, retry-scheduled, succeeded, and failed states +- **THEN** a state-change event is published carrying the previous and new states -- **WHEN** a create is issued for a task id that already exists -- **THEN** a `task.updated` event is published carrying both the existing and the new task +#### Scenario: Task attempt fails +- **WHEN** an attempt reports a typed handler failure, lease loss, or stall +- **THEN** a failure event identifies the failure kind, attempt, terminal status, and next retry time when present -#### Scenario: Task moved between lists emits task.moved - -- **WHEN** a task is moved between the wait/scheduled/active/failed/success lists (or removed) -- **THEN** a `task.moved` event is published carrying `from` and `to` list names - -#### Scenario: Task failure emits task.failed - -- **WHEN** a handler reports a failure -- **THEN** a `task.failed` event is published carrying the error, `maxRetries`, `retryCount`, failure `policy`, and `willRetry` - -#### Scenario: Task success emits task.completed - -- **WHEN** a handler reports success -- **THEN** a `task.completed` event is published carrying the success value and success `policy` +#### Scenario: Task succeeds +- **WHEN** the current fenced attempt reports success +- **THEN** a terminal completion event is published carrying the encoded success envelope and completion policy ### Requirement: Engine exposes an event stream -The TaskEngine SHALL expose a `stream(name, options?)` method that returns an Effect `Stream` of decoded events read from the queue's Redis Stream via `XREAD`. The stream SHALL start from an optional `cursor` (defaulting to the current time) and SHALL poll at a configurable interval, advancing the cursor past events it has yielded. +The TaskEngine SHALL expose a decoded Effect `Stream` beginning after an +explicit authoritative Redis cursor. When no cursor is supplied, the API SHALL +obtain a server-authoritative start position. It SHALL advance from the last +yielded id and expose the earliest retained cursor and a typed cursor-expired +error. #### Scenario: Stream yields published events +- **WHEN** a consumer opens a stream and a later event is published +- **THEN** the consumer receives the decoded event exactly once within that stream run -- **WHEN** a consumer runs the engine stream for a queue and an event is published -- **THEN** the consumer receives the decoded event +#### Scenario: Stream resumes from a retained cursor +- **WHEN** a consumer opens the stream with a retained prior event id +- **THEN** only retained events after that cursor are delivered -#### Scenario: Stream resumes from a cursor - -- **WHEN** a consumer opens the stream with a cursor of a prior event id -- **THEN** only events after that cursor are delivered +#### Scenario: Cursor has expired +- **WHEN** a consumer resumes from an id older than event retention +- **THEN** the stream fails with a typed cursor-expired error containing the earliest available cursor ### Requirement: TaskQueue stream decodes payloads against queue schemas -`TaskQueue.stream(queue)` SHALL wrap the engine stream and decode each event's task-shaped payload against the queue's payload/success/error schemas, so consumers receive typed tasks, success values, and errors rather than raw stored strings. +`TaskQueue.stream(queue)` SHALL validate the storage protocol and decode task +payloads, successes, and failures against the queue's schemas. Unsupported +versions, corruption, and schema mismatches SHALL remain typed stream failures +and SHALL NOT be normalized into valid-looking events. #### Scenario: Typed task in created event +- **WHEN** a supported `task.created` event is streamed for a typed queue +- **THEN** its task value is decoded to the queue's task type -- **WHEN** a `task.created` event is streamed for a typed queue -- **THEN** `payload.newTask` is decoded to the queue's task type - -#### Scenario: Typed error in failed event - -- **WHEN** a `task.failed` event is streamed for a typed queue -- **THEN** `payload.error` is decoded against the queue's error schema +#### Scenario: Corrupt failure event +- **WHEN** a failure event contains invalid encoded bytes +- **THEN** the stream fails with a typed decoding error rather than yielding an empty or partial failure ### Requirement: wait and execute await a task's terminal event -`TaskQueue.wait(queue, taskId)` SHALL await the task's terminal event and resolve with its typed success value or fail with its typed error. `TaskQueue.execute(queue, payload, options?)` SHALL offer the task and then await its terminal event as a single call. - -#### Scenario: wait resolves on completion - -- **WHEN** `wait` is called for a task id that subsequently completes -- **THEN** it resolves with the decoded success value +`TaskQueue.wait` SHALL accept a task/result handle containing generation +identity and an authoritative cursor. It SHALL check durable terminal state, +subscribe from the cursor, then recheck state to close the race. It SHALL +resolve typed success or fail with typed task failure, task-not-found, +result-expired, cursor-expired, or timeout. `TaskQueue.execute` SHALL offer and +await through the same protocol. -#### Scenario: wait fails on failure +#### Scenario: Task already completed +- **WHEN** `wait` begins after the retained task has already completed +- **THEN** it resolves immediately from durable terminal state -- **WHEN** `wait` is called for a task id that subsequently fails terminally -- **THEN** it fails with the decoded error +#### Scenario: Completion races subscription +- **WHEN** completion occurs between the initial state read and stream subscription +- **THEN** the subscription or recheck observes the same terminal generation and `wait` resolves -#### Scenario: execute round-trips a task +#### Scenario: Result expired +- **WHEN** terminal metadata exists but its result retention has expired +- **THEN** `wait` fails with a typed result-expired error -- **WHEN** `execute` is called and a handler completes the task -- **THEN** it resolves with the handler's success value +#### Scenario: Execute round-trips a task +- **WHEN** `execute` offers a task and a managed worker completes it +- **THEN** it resolves with the decoded success for the offered generation diff --git a/openspec/specs/task-relationships/spec.md b/openspec/specs/task-relationships/spec.md index 9818a5a..aa3a460 100644 --- a/openspec/specs/task-relationships/spec.md +++ b/openspec/specs/task-relationships/spec.md @@ -2,55 +2,90 @@ ## Purpose -Separates creator provenance, explicit result retention, and execution -dependency so nested offers do not silently acquire workflow-like semantics. +Separates task creator provenance, explicit result retention, and execution +dependency so nested offers do not silently acquire workflow-like lifecycle +semantics. ## Requirements -### Requirement: Creator provenance is informational +### Requirement: Creator provenance is automatic and informational -A task offered from a managed handler SHALL record the running task as its -creator. Provenance SHALL NOT retain results, order execution, join, cancel, or -propagate failure. +A task offered from a managed task handler SHALL record the running task as its +creator. Creator provenance SHALL NOT retain results, order execution, +propagate cancellation, or propagate failure. -#### Scenario: Nested offer +#### Scenario: Nested offer records its creator - **WHEN** task A's handler offers task B -- **THEN** B records A as creator and both execute independently +- **THEN** B records A as its creator +- **AND** A's completion has no effect on B's execution -### Requirement: Result retention is explicit and set-idempotent +### Requirement: Result retention is explicit -A producer SHALL explicitly request a result-retention hold. A hold is -identified by holder generation and retained generation; replaying it has no -additional effect while a different holder acquires independently. +A producer SHALL explicitly request a result-retention hold when it needs a +terminal task record to remain readable until a named holder settles. Nested +offers SHALL NOT create such a hold by default. -#### Scenario: Two holders -- **WHEN** two live task generations retain the same terminal generation -- **THEN** each relationship independently prevents result disposal +#### Scenario: Nested offer without retention +- **WHEN** task A offers task B without a retention option +- **THEN** B has creator provenance for A but no retention hold owned by A -### Requirement: Retention holders are live +#### Scenario: Explicit retention +- **WHEN** task A offers task B with a result-retention hold owned by A +- **THEN** B's terminal result remains readable until A settles or explicitly releases the hold -A hold SHALL be acquired only for an existing unsettled holder. Failed -validation SHALL leave the task and all relationships unchanged. +### Requirement: Retention holds are set-idempotent -#### Scenario: Settled holder -- **WHEN** a settled holder requests retention +A retention hold SHALL be identified by holder and retained task generation. +Reacquiring the same hold SHALL have no additional effect, while a different +holder SHALL acquire an independent hold. + +#### Scenario: Replay reacquires the same hold +- **WHEN** a replaying holder repeats the same retained offer +- **THEN** exactly one hold exists for that holder and task generation + +#### Scenario: Second holder retains an existing task +- **WHEN** another live holder explicitly retains the same task generation +- **THEN** both holders independently prevent result disposal + +### Requirement: Retention holders must be live + +A retention hold SHALL be acquired only for an existing holder that has not +settled. Failure to validate any requested holder SHALL leave the retained task +and all relationships unchanged. + +#### Scenario: Settled holder requests retention +- **WHEN** a retained offer names a holder that has already settled - **THEN** the offer fails without acquiring a hold ### Requirement: Removal respects active holds -Ordinary removal SHALL reject active incoming holds. Holder settlement or -removal SHALL release owned holds in bounded resumable batches. A separately -named administrative force-removal operation MAY revoke relationships. +Removing a task generation with active retention holds SHALL fail unless an +explicit administrative force mode is used. Settling or removing a holder SHALL +release every hold owned by that holder in bounded, resumable batches. -#### Scenario: Retained terminal result -- **WHEN** ordinary removal targets a terminal generation with an active hold +#### Scenario: Remove retained result +- **WHEN** a terminal task has an active retention hold and ordinary removal is requested - **THEN** removal fails and the result remains readable +#### Scenario: Holder removal releases retention +- **WHEN** a holder is removed before normal settlement +- **THEN** its retention holds are eventually released exactly once + ### Requirement: Retention does not create execution dependency -The engine SHALL NOT delay holder settlement, cancel retained work, join tasks, -or translate failures solely because a retention relationship exists. +The library SHALL NOT delay a holder's completion, cancel a retained task, or +translate a retained task's failure solely because a retention hold exists. + +#### Scenario: Holder settles before retained task +- **WHEN** holder A settles while retained task B is still runnable +- **THEN** A's hold is released and B continues independently + +### Requirement: Child terminology is reserved + +Public documentation SHALL use creator, spawned task, retention hold, and +execution dependency for their distinct meanings. It SHALL use child task only +for a future relationship that actually defines structured lifecycle behavior. -#### Scenario: Holder settles first -- **WHEN** a holder settles while retained work remains runnable -- **THEN** its hold releases and the retained task continues independently +#### Scenario: API documentation for nested offer +- **WHEN** a user reads the nested-offer documentation +- **THEN** it describes provenance and optional retention without promising parent/child cancellation, joining, or failure propagation diff --git a/openspec/specs/task-retry-policy/spec.md b/openspec/specs/task-retry-policy/spec.md index 40db059..83fd992 100644 --- a/openspec/specs/task-retry-policy/spec.md +++ b/openspec/specs/task-retry-policy/spec.md @@ -49,23 +49,54 @@ On failure, when a retry is due, the engine SHALL compute the next run time from ### Requirement: maxRetries caps an unbounded schedule -`maxRetries` SHALL act as a hard cap on retry attempts that bounds an otherwise-unbounded schedule (e.g. `Schedule.forever`). A queue-level default cap of **5** SHALL apply when the offer does not specify one; a per-offer `maxRetries` SHALL override the queue-level default for that task. Once the recorded error count reaches the effective cap, the task SHALL NOT be retried regardless of the schedule. Setting `maxRetries` to `null` or `Infinity` SHALL disable the cap, allowing the schedule to run unbounded. +`maxRetries` SHALL cap retries caused by typed handler failures and SHALL NOT be +overloaded with lease-loss recovery. A task-definition default of 5 SHALL apply +unless explicitly overridden for the task; `null` SHALL disable the +handler-failure cap. The stored attempt history SHALL distinguish handler +failures from stalls and ownership loss. -#### Scenario: Default cap prevents infinite loop +#### Scenario: Default cap prevents infinite handler retries -- **WHEN** a task uses an unbounded schedule and is offered without a per-offer `maxRetries` -- **THEN** retries stop once the error count reaches the queue-level default cap of 5 -- **AND** the failure policy is then applied +- **WHEN** a task uses an unbounded retry schedule and no override +- **THEN** handler-failure retries stop at the default cap of 5 +- **AND** its terminal failure policy is applied subject to result retention -#### Scenario: Per-offer override wins +#### Scenario: Per-task override wins -- **WHEN** a task is offered with an explicit `maxRetries` -- **THEN** that value is used as the cap for that task instead of the queue-level default +- **WHEN** a task is offered with an explicit handler retry cap +- **THEN** that cap is used instead of the task-definition default -#### Scenario: Explicit unbounded retries +#### Scenario: Explicit unbounded handler retries -- **WHEN** a task's `maxRetries` is set to `null` or `Infinity` -- **THEN** no cap is applied and retries continue for as long as the schedule yields run times +- **WHEN** the effective handler retry cap is `null` +- **THEN** the schedule alone determines whether another handler attempt is made + +### Requirement: Stalled attempts have a separate bounded policy + +Lease expiry and ownership loss SHALL increment a stalled-attempt counter and +SHALL NOT be fed into the user's typed error `Schedule`. A configurable +`maxStalledCount` SHALL bound recovery and default to a finite value. + +#### Scenario: Stall below the cap + +- **WHEN** an attempt loses its lease and stalled attempts remain +- **THEN** the task returns to an eligible retry state with a stalled event + +#### Scenario: Stall cap exhausted + +- **WHEN** another lease expires after the stalled cap is exhausted +- **THEN** the task settles with a terminal built-in stalled error + +### Requirement: Ownership loss is not a handler failure + +A stale or lost lease SHALL fail the processing attempt with `LeaseLost` and +SHALL NOT append the handler's typed error or run its retry schedule. + +#### Scenario: Old attempt reports failure + +- **WHEN** a stale attempt tries to report a typed handler failure +- **THEN** acknowledgement fails with `LeaseLost` +- **AND** neither error history nor retry schedule state changes ### Requirement: Canceled errors short-circuit retries