Task Lifecycle Transitions, Replays & Progress Tracking #12

Closed
opened 2026-09-22 17:58:55 +00:00 by yuris · 0 comments
Owner

Summary

Implement explicit task lifecycle transition routes (POST /v1/tasks/{id}/complete, POST /v1/tasks/{id}/reopen, POST /v1/tasks/{id}/cancel and CLI commands life task complete, life task reopen, life task cancel), enforcing strict domain state machine validation, RFC 5545 completion timestamps and percentage properties, mandatory If-Match ETag preflight, idempotent replays, and no-op suppression.


Background & Technical Rationale

  1. Explicit Domain Lifecycle State Machine:
    Tasks in life-service do not use arbitrary free-form status strings. VTODO STATUS is restricted to four valid RFC 5545 states: NEEDS-ACTION, IN-PROCESS, COMPLETED, and CANCELLED. Transitions between these states must be explicitly governed by domain logic:

    • Complete (complete):
      • Allowed source states: NEEDS-ACTION or IN-PROCESS.
      • Target state: COMPLETED.
      • Property updates: Sets COMPLETED timestamp to an explicit user-reported outcome instant (occurred_at) or the confirmed action instant. Sets PERCENT-COMPLETE: 100.
    • Reopen (reopen):
      • Allowed source states: COMPLETED or CANCELLED.
      • Target state: NEEDS-ACTION.
      • Property updates: Clears and removes the COMPLETED timestamp; sets PERCENT-COMPLETE: 0.
      • Invariant: Retains original DUE deadline and commitment strength category (life-active, life-queued, life-someday) unless specifically requested.
    • Cancel (cancel):
      • Allowed source states: NEEDS-ACTION or IN-PROCESS.
      • Target state: CANCELLED.
      • Property updates: Clears any completion timestamp.
      • Crucial Invariant: Cancellation is NOT completion! It does not count towards the career floor or represent achieved work. The calendar resource is updated to STATUS:CANCELLED on Radicale, not deleted via HTTP DELETE.
  2. Mandatory ETag Preflight & Optimistic Concurrency:
    All lifecycle mutations require the caller to supply the current resource ETag (--etag / If-Match). If the resource has been concurrently modified on Radicale, the dispatch returns etag_conflict (HTTP 412 / CLI exit code 4). The service must never guess or silently overwrite changes.

  3. Idempotent Replays vs No-Op Suppression (Scenario W09):

    • Same-Key Replay: Repeating a lifecycle transition with the same Idempotency-Key / operation_id immediately replays the recorded outcome from the SQLite operations table without re-executing remote CalDAV writes.
    • New Request on Already-Matching State: If a caller invokes complete on a task that is already COMPLETED under a NEW operation ID:
      • The service recognizes that the item already satisfies the target state.
      • It returns HTTP 200 OK with changed: false.
      • It strictly forbids inventing a duplicate task_completed event in life_events!
  4. Crash Recovery & Uncertainty Handling:
    If a remote write times out or the connection drops after dispatch:

    • The operation state in SQLite is transitioned to 'unknown' or 'needs_reconciliation'.
    • The CLI exits with code 6 (operation_unknown).
    • A stale or unknown remote outcome must never be announced as completed to the caller or user. The caller is directed to inspect the operation via life operation get <ID>.

Exact Technical References & Specifications

Normative Quotations from Technical Architecture

From docs/technical/domain-model.md (Task axes, lines 22–34):

"VTODO STATUS is exactly NEEDS-ACTION, IN-PROCESS, COMPLETED or CANCELLED. A missing external STATUS normalizes to unspecified, not inferred completed. New Life tasks default to NEEDS-ACTION. Blockers are explicit structured user facts in linked context, never a fabricated standard STATUS named BLOCKED...

Lifecycle transitions:

  • Complete: NEEDS-ACTION or IN-PROCESS → COMPLETED; set COMPLETED to the explicit outcome instant or confirmed action time, PERCENT-COMPLETE to 100.
  • Reopen: COMPLETED or CANCELLED → NEEDS-ACTION; remove COMPLETED and reset PERCENT-COMPLETE to 0. Preserve deadline and strength unless explicitly changed.
  • Cancel: open task → CANCELLED; remove any completion timestamp. Cancellation is not completion.
  • Start or progress updates use task update with IN-PROCESS and explicit progress when provided. Invalid combinations are rejected.
  • Queue/someday/activate and defer are explicit updates, not inferred lifecycle transitions. Deferral changes planned work timing or strength; it does not move a deadline unless explicitly requested.
  • Repeating a transition under the same operation identity replays its outcome. A new request for an already-matching state reports no change and never invents another completion fact."

From docs/technical/cli-api.md (Command-to-route table, lines 34–36):

Command Method and route Scope
life task complete ID --etag ETAG --operation-id KEY --input - POST /v1/tasks/{id}/complete write
life task reopen ID --etag ETAG --input - POST /v1/tasks/{id}/reopen write
life task cancel ID --etag ETAG --input - POST /v1/tasks/{id}/cancel write

From docs/technical/cli-api.md (Command payloads and results, line 84):

Family Required / optional input Response data
Task complete/reopen/cancel Optional reason, occurred_at, scope; ETag required Same mutation result; explicit domain transition

From docs/technical/verification.md (Workflow scenarios, Scenario W09, line 23):

ID Requirement and specification Input / action Observable acceptance
W09 Complete/reopen/cancel Explicit user confirmation; task completion with current If-Match and Idempotency-Key, then fresh conditional reopen/cancel Correct lifecycle/progress/completion properties; journaled outcomes; stale/unknown result never announced completed

From docs/technical/verification.md (Recovery scenarios, lines 103–109):

"Illustrative pending response for life task complete itm_demo --etag '"v1"' --operation-id op_demo_unknown --input - --json after explicit approval, with stdin {} and a lost remote response:

{"schema_version":1,"request_id":"req_demo_unknown","status":"partial","data":{"operation_id":"op_demo_unknown","operation_state":"unknown"},"warnings":[],"error":{"code":"operation_unknown","message":"Remote outcome uncertain; inspect operation before any retry","retryable":false,"operation_id":"op_demo_unknown"}}

Expected CLI exit 6. life operation get op_demo_unknown --json investigates recovery; it is not evidence of completion until the returned state/evidence establishes it."

From docs/technical/cli-api.md (Complete fictional examples, lines 151–161):

"After explicit user confirmation, life task complete itm_demo --etag '"v1"' --operation-id op_demo_complete --input - --json, stdin:

{"occurred_at":"2030-04-08T06:00:00Z","reason":"User reports outline finished"}

HTTP POST /v1/tasks/itm_demo/complete, If-Match \"v1\", Idempotency-Key op_demo_complete returns:

{"schema_version":1,"request_id":"req_demo_complete","status":"ok","data":{"operation_id":"op_demo_complete","operation_state":"applied","changed":true,"item":{"id":"itm_demo","kind":"task","collection_id":"personal","uid":"fictional-demo-uid","recurrence_id":null,"etag":"\"v2\"","title":"Draft fictional portfolio outline","status":"COMPLETED","categories":["area-career","life-active"],"strength":"life-active","deadline":{"kind":"date","value":"2030-04-12"},"planned_start":null,"estimated_minutes":null,"energy":null,"project_id":null},"context_delivery":"not_requested"},"warnings":[],"error":null}
```"

What to Build (Step-by-Step)

1. Lifecycle State Machine Engine (src/domain/lifecycle.rs)

  • Implement TaskLifecycle:
    pub enum LifecycleTransition {
        Complete { occurred_at: Option<DateTime<Utc>>, reason: Option<String> },
        Reopen { reason: Option<String> },
        Cancel { reason: Option<String> },
    }
    
  • Transition validation function:
    validate_transition(current_status: TaskStatus, transition: &LifecycleTransition) -> Result<TargetChanges, LifecycleError>:
    • If target status equals current status: return Ok(TargetChanges::NoOp).
    • For Complete:
      • Require current_status in [NEEDS-ACTION, IN-PROCESS].
      • Compute changes: STATUS: COMPLETED, COMPLETED: <occurred_at or now>, PERCENT-COMPLETE: 100.
    • For Reopen:
      • Require current_status in [COMPLETED, CANCELLED].
      • Compute changes: STATUS: NEEDS-ACTION, remove COMPLETED, PERCENT-COMPLETE: 0.
    • For Cancel:
      • Require current_status in [NEEDS-ACTION, IN-PROCESS].
      • Compute changes: STATUS: CANCELLED, remove COMPLETED.

2. Preservation-Safe iCalendar Lifecycle Patcher

  • Read raw resource from Radicale or projection table using verified baseline ETag.
  • Locate target VTODO component:
    • Apply computed property modifications:
      • Update or set STATUS.
      • Update or delete COMPLETED.
      • Update or set PERCENT-COMPLETE.
    • Leave all other properties (UID, SUMMARY, DUE, DTSTART, CATEGORIES, X-LIFE-*, VALARM) untouched.
  • Serialize resource to RFC 5545 compliant string.

3. Execution Pipeline & Two-Phase Journaling

  • If TargetChanges::NoOp:
    • In a local transaction via DatabaseHandle, commit row to operations with state: 'applied', changed: false, canonical request fingerprint, and the recorded result.
    • Return HTTP 200 OK with changed: false and current item.
    • Do NOT issue remote PUT and do NOT append duplicate lifecycle events to life_events.
  • Otherwise:
    • Commit state: 'prepared' to operations table with canonical fingerprint.
    • Dispatch HTTP PUT to Radicale with If-Match: <baseline_etag>.
    • Handle results:
      • On HTTP 412: commit operations.state = 'failed' and return etag_conflict (HTTP 412, exit code 4).
      • On timeout/network error: commit operations.state = 'unknown' and return operation_unknown (exit code 6).
      • On HTTP 200/204: extract confirmed ETag, update operations.state = 'applied', and append corresponding fact (task_completed, task_reopened, or task_cancelled) to life_events with before_json and after_json.

4. Axum Routes & CLI Handlers

  • Axum routes:
    • POST /v1/tasks/{id}/complete
    • POST /v1/tasks/{id}/reopen
    • POST /v1/tasks/{id}/cancel
  • CLI commands:
    • life task complete <ID> --etag <ETAG> --operation-id <KEY> [--input -] [--json]
    • life task reopen <ID> --etag <ETAG> [--operation-id <KEY>] [--input -] [--json]
    • life task cancel <ID> --etag <ETAG> [--operation-id <KEY>] [--input -] [--json]

Acceptance Criteria

  • State Machine Enforcement: Attempting an invalid state transition (such as completing an already cancelled task without reopening, or reopening a task that is already in NEEDS-ACTION or IN-PROCESS) returns validation error (HTTP 422, exit code 2), while reopening from CANCELLED or COMPLETED succeeds (Scenario W09).
  • Complete Lifecycle Action (Scenario W09): life task complete updates status to COMPLETED, records PERCENT-COMPLETE: 100, sets COMPLETED timestamp, and appends a task_completed event to life_events.
  • Reopen Lifecycle Action (Scenario W09): life task reopen on a completed task restores STATUS: NEEDS-ACTION, resets PERCENT-COMPLETE: 0, removes COMPLETED property, retains original DUE deadline, and appends a task_reopened event.
  • Cancel Lifecycle Action: life task cancel on an open task updates status to CANCELLED and clears completion timestamp without deleting the CalDAV resource.
  • Mandatory ETag Preflight & Concurrency Handling: Calling a transition without --etag (or missing If-Match) is rejected before remote dispatch with HTTP 400 invalid_input (exit code 2); passing a stale ETag where Radicale returns HTTP 412 produces etag_conflict (HTTP 412 / exit code 4).
  • Idempotent Replay: Invoking a transition with an identical operation_id replays the recorded outcome without sending an additional HTTP PUT to Radicale.
  • No-Op Persistence & Replay: Invoking a transition on a task that is already in the target state persists an applied operation with changed: false in operations without remote PUT or duplicate lifecycle events; subsequent same-key requests replay this outcome.
  • Uncertainty Reporting (Scenario W09 / Recovery): If a network timeout or disconnect occurs mid-transition, the service responds with operation_unknown (exit code 6) and logs the operation ID; it never reports the task completed until evidence confirms it.

Notes

  • One review round: reopen was a recorded no-op from NEEDS-ACTION but a 422 from IN-PROCESS; it is now refused 422 from both open states, with the no-op retained for complete@COMPLETED and cancel@CANCELLED.
  • No Cargo.toml change was needed, so the deferred authors chore stays pending.

Blocked by

  • #011: Conditional Item Updates with Quoted ETag Preflight
## Summary Implement explicit task lifecycle transition routes (`POST /v1/tasks/{id}/complete`, `POST /v1/tasks/{id}/reopen`, `POST /v1/tasks/{id}/cancel` and CLI commands `life task complete`, `life task reopen`, `life task cancel`), enforcing strict domain state machine validation, RFC 5545 completion timestamps and percentage properties, mandatory `If-Match` ETag preflight, idempotent replays, and no-op suppression. --- ## Background & Technical Rationale 1. **Explicit Domain Lifecycle State Machine:** Tasks in `life-service` do not use arbitrary free-form status strings. VTODO `STATUS` is restricted to four valid RFC 5545 states: `NEEDS-ACTION`, `IN-PROCESS`, `COMPLETED`, and `CANCELLED`. Transitions between these states must be explicitly governed by domain logic: - **Complete (`complete`):** - Allowed source states: `NEEDS-ACTION` or `IN-PROCESS`. - Target state: `COMPLETED`. - Property updates: Sets `COMPLETED` timestamp to an explicit user-reported outcome instant (`occurred_at`) or the confirmed action instant. Sets `PERCENT-COMPLETE: 100`. - **Reopen (`reopen`):** - Allowed source states: `COMPLETED` or `CANCELLED`. - Target state: `NEEDS-ACTION`. - Property updates: Clears and removes the `COMPLETED` timestamp; sets `PERCENT-COMPLETE: 0`. - Invariant: Retains original `DUE` deadline and commitment strength category (`life-active`, `life-queued`, `life-someday`) unless specifically requested. - **Cancel (`cancel`):** - Allowed source states: `NEEDS-ACTION` or `IN-PROCESS`. - Target state: `CANCELLED`. - Property updates: Clears any completion timestamp. - Crucial Invariant: Cancellation is NOT completion! It does not count towards the career floor or represent achieved work. The calendar resource is updated to `STATUS:CANCELLED` on Radicale, not deleted via HTTP DELETE. 2. **Mandatory ETag Preflight & Optimistic Concurrency:** All lifecycle mutations require the caller to supply the current resource ETag (`--etag` / `If-Match`). If the resource has been concurrently modified on Radicale, the dispatch returns `etag_conflict` (HTTP 412 / CLI exit code 4). The service must never guess or silently overwrite changes. 3. **Idempotent Replays vs No-Op Suppression (Scenario W09):** - **Same-Key Replay:** Repeating a lifecycle transition with the same `Idempotency-Key` / `operation_id` immediately replays the recorded outcome from the SQLite `operations` table without re-executing remote CalDAV writes. - **New Request on Already-Matching State:** If a caller invokes `complete` on a task that is already `COMPLETED` under a NEW operation ID: - The service recognizes that the item already satisfies the target state. - It returns HTTP 200 OK with `changed: false`. - It strictly forbids inventing a duplicate `task_completed` event in `life_events`! 4. **Crash Recovery & Uncertainty Handling:** If a remote write times out or the connection drops after dispatch: - The operation state in SQLite is transitioned to `'unknown'` or `'needs_reconciliation'`. - The CLI exits with code 6 (`operation_unknown`). - A stale or unknown remote outcome must **never** be announced as completed to the caller or user. The caller is directed to inspect the operation via `life operation get <ID>`. --- ## Exact Technical References & Specifications ### Normative Quotations from Technical Architecture From `docs/technical/domain-model.md` (*Task axes*, lines 22–34): > "VTODO `STATUS` is exactly `NEEDS-ACTION`, `IN-PROCESS`, `COMPLETED` or `CANCELLED`. A missing external STATUS normalizes to unspecified, not inferred completed. New Life tasks default to NEEDS-ACTION. Blockers are explicit structured user facts in linked context, never a fabricated standard STATUS named BLOCKED... > > Lifecycle transitions: > - Complete: NEEDS-ACTION or IN-PROCESS → COMPLETED; set COMPLETED to the explicit outcome instant or confirmed action time, PERCENT-COMPLETE to 100. > - Reopen: COMPLETED or CANCELLED → NEEDS-ACTION; remove COMPLETED and reset PERCENT-COMPLETE to 0. Preserve deadline and strength unless explicitly changed. > - Cancel: open task → CANCELLED; remove any completion timestamp. Cancellation is not completion. > - Start or progress updates use task update with IN-PROCESS and explicit progress when provided. Invalid combinations are rejected. > - Queue/someday/activate and defer are explicit updates, not inferred lifecycle transitions. Deferral changes planned work timing or strength; it does not move a deadline unless explicitly requested. > - Repeating a transition under the same operation identity replays its outcome. A new request for an already-matching state reports no change and never invents another completion fact." From `docs/technical/cli-api.md` (*Command-to-route table*, lines 34–36): > | Command | Method and route | Scope | > |---|---|---| > | `life task complete ID --etag ETAG --operation-id KEY --input -` | POST /v1/tasks/{id}/complete | write | > | `life task reopen ID --etag ETAG --input -` | POST /v1/tasks/{id}/reopen | write | > | `life task cancel ID --etag ETAG --input -` | POST /v1/tasks/{id}/cancel | write | From `docs/technical/cli-api.md` (*Command payloads and results*, line 84): > | Family | Required / optional input | Response data | > |---|---|---| > | Task complete/reopen/cancel | Optional reason, occurred_at, scope; ETag required | Same mutation result; explicit domain transition | From `docs/technical/verification.md` (*Workflow scenarios*, Scenario W09, line 23): > | ID | Requirement and specification | Input / action | Observable acceptance | > |---|---|---|---| > | `W09` | [Complete/reopen/cancel](domain-model.md#task-axes) | Explicit user confirmation; task completion with current If-Match and Idempotency-Key, then fresh conditional reopen/cancel | Correct lifecycle/progress/completion properties; journaled outcomes; stale/unknown result never announced completed | From `docs/technical/verification.md` (*Recovery scenarios*, lines 103–109): > "Illustrative pending response for `life task complete itm_demo --etag '"v1"' --operation-id op_demo_unknown --input - --json` after explicit approval, with stdin `{}` and a lost remote response: > > ```json > {"schema_version":1,"request_id":"req_demo_unknown","status":"partial","data":{"operation_id":"op_demo_unknown","operation_state":"unknown"},"warnings":[],"error":{"code":"operation_unknown","message":"Remote outcome uncertain; inspect operation before any retry","retryable":false,"operation_id":"op_demo_unknown"}} > ``` > > Expected CLI exit 6. `life operation get op_demo_unknown --json` investigates recovery; it is not evidence of completion until the returned state/evidence establishes it." From `docs/technical/cli-api.md` (*Complete fictional examples*, lines 151–161): > "After explicit user confirmation, `life task complete itm_demo --etag '"v1"' --operation-id op_demo_complete --input - --json`, stdin: > > ```json > {"occurred_at":"2030-04-08T06:00:00Z","reason":"User reports outline finished"} > ``` > > HTTP POST /v1/tasks/itm_demo/complete, If-Match `\"v1\"`, Idempotency-Key op_demo_complete returns: > > ```json > {"schema_version":1,"request_id":"req_demo_complete","status":"ok","data":{"operation_id":"op_demo_complete","operation_state":"applied","changed":true,"item":{"id":"itm_demo","kind":"task","collection_id":"personal","uid":"fictional-demo-uid","recurrence_id":null,"etag":"\"v2\"","title":"Draft fictional portfolio outline","status":"COMPLETED","categories":["area-career","life-active"],"strength":"life-active","deadline":{"kind":"date","value":"2030-04-12"},"planned_start":null,"estimated_minutes":null,"energy":null,"project_id":null},"context_delivery":"not_requested"},"warnings":[],"error":null} > ```" --- ## What to Build (Step-by-Step) ### 1. Lifecycle State Machine Engine (`src/domain/lifecycle.rs`) - Implement `TaskLifecycle`: ```rust pub enum LifecycleTransition { Complete { occurred_at: Option<DateTime<Utc>>, reason: Option<String> }, Reopen { reason: Option<String> }, Cancel { reason: Option<String> }, } ``` - Transition validation function: `validate_transition(current_status: TaskStatus, transition: &LifecycleTransition) -> Result<TargetChanges, LifecycleError>`: - If target status equals current status: return `Ok(TargetChanges::NoOp)`. - For `Complete`: - Require `current_status` in `[NEEDS-ACTION, IN-PROCESS]`. - Compute changes: `STATUS: COMPLETED`, `COMPLETED: <occurred_at or now>`, `PERCENT-COMPLETE: 100`. - For `Reopen`: - Require `current_status` in `[COMPLETED, CANCELLED]`. - Compute changes: `STATUS: NEEDS-ACTION`, remove `COMPLETED`, `PERCENT-COMPLETE: 0`. - For `Cancel`: - Require `current_status` in `[NEEDS-ACTION, IN-PROCESS]`. - Compute changes: `STATUS: CANCELLED`, remove `COMPLETED`. ### 2. Preservation-Safe iCalendar Lifecycle Patcher - Read raw resource from Radicale or projection table using verified baseline ETag. - Locate target `VTODO` component: - Apply computed property modifications: - Update or set `STATUS`. - Update or delete `COMPLETED`. - Update or set `PERCENT-COMPLETE`. - Leave all other properties (`UID`, `SUMMARY`, `DUE`, `DTSTART`, `CATEGORIES`, `X-LIFE-*`, `VALARM`) untouched. - Serialize resource to RFC 5545 compliant string. ### 3. Execution Pipeline & Two-Phase Journaling - If `TargetChanges::NoOp`: - In a local transaction via DatabaseHandle, commit row to `operations` with `state: 'applied'`, `changed: false`, canonical request fingerprint, and the recorded result. - Return HTTP 200 OK with `changed: false` and current item. - Do NOT issue remote `PUT` and do NOT append duplicate lifecycle events to `life_events`. - Otherwise: - Commit `state: 'prepared'` to `operations` table with canonical fingerprint. - Dispatch HTTP `PUT` to Radicale with `If-Match: <baseline_etag>`. - Handle results: - On HTTP 412: commit `operations.state = 'failed'` and return `etag_conflict` (HTTP 412, exit code 4). - On timeout/network error: commit `operations.state = 'unknown'` and return `operation_unknown` (exit code 6). - On HTTP 200/204: extract confirmed ETag, update `operations.state = 'applied'`, and append corresponding fact (`task_completed`, `task_reopened`, or `task_cancelled`) to `life_events` with `before_json` and `after_json`. ### 4. Axum Routes & CLI Handlers - Axum routes: - `POST /v1/tasks/{id}/complete` - `POST /v1/tasks/{id}/reopen` - `POST /v1/tasks/{id}/cancel` - CLI commands: - `life task complete <ID> --etag <ETAG> --operation-id <KEY> [--input -] [--json]` - `life task reopen <ID> --etag <ETAG> [--operation-id <KEY>] [--input -] [--json]` - `life task cancel <ID> --etag <ETAG> [--operation-id <KEY>] [--input -] [--json]` --- ## Acceptance Criteria - [x] **State Machine Enforcement:** Attempting an invalid state transition (such as completing an already cancelled task without reopening, or reopening a task that is already in NEEDS-ACTION or IN-PROCESS) returns validation error (HTTP 422, exit code 2), while reopening from CANCELLED or COMPLETED succeeds (Scenario W09). - [x] **Complete Lifecycle Action (Scenario W09):** `life task complete` updates status to `COMPLETED`, records `PERCENT-COMPLETE: 100`, sets `COMPLETED` timestamp, and appends a `task_completed` event to `life_events`. - [x] **Reopen Lifecycle Action (Scenario W09):** `life task reopen` on a completed task restores `STATUS: NEEDS-ACTION`, resets `PERCENT-COMPLETE: 0`, removes `COMPLETED` property, retains original `DUE` deadline, and appends a `task_reopened` event. - [x] **Cancel Lifecycle Action:** `life task cancel` on an open task updates status to `CANCELLED` and clears completion timestamp without deleting the CalDAV resource. - [x] **Mandatory ETag Preflight & Concurrency Handling:** Calling a transition without `--etag` (or missing `If-Match`) is rejected before remote dispatch with HTTP 400 `invalid_input` (exit code 2); passing a stale ETag where Radicale returns HTTP 412 produces `etag_conflict` (HTTP 412 / exit code 4). - [x] **Idempotent Replay:** Invoking a transition with an identical `operation_id` replays the recorded outcome without sending an additional HTTP `PUT` to Radicale. - [x] **No-Op Persistence & Replay:** Invoking a transition on a task that is already in the target state persists an `applied` operation with `changed: false` in `operations` without remote `PUT` or duplicate lifecycle events; subsequent same-key requests replay this outcome. - [x] **Uncertainty Reporting (Scenario W09 / Recovery):** If a network timeout or disconnect occurs mid-transition, the service responds with `operation_unknown` (exit code 6) and logs the operation ID; it never reports the task completed until evidence confirms it. --- ## Notes - One review round: `reopen` was a recorded no-op from `NEEDS-ACTION` but a 422 from `IN-PROCESS`; it is now refused 422 from both open states, with the no-op retained for `complete`@`COMPLETED` and `cancel`@`CANCELLED`. - No `Cargo.toml` change was needed, so the deferred authors chore stays pending. ## Blocked by - #011: Conditional Item Updates with Quoted ETag Preflight
yuris changed title from Issue #012: Task Lifecycle Transitions, Replays & Progress Tracking to Task Lifecycle Transitions, Replays & Progress Tracking 2026-09-22 20:02:38 +00:00
yuris closed this issue 2026-09-22 22:17:05 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/life-service#12
No description provided.