# 04: Timing

## Outcome

Run separate `now`, `in`, and `every` phase blocks, observe their independent
task identities, and cancel or replace scheduled work without leaving an old
recurring activation.

## Prerequisites

- Complete Tutorial 01 and understand managed build binding and exact task
  status.
- Permission to create lesson objects, run recipes, and cancel your jobs.
- About 25 minutes. No secret or cloud account is required.

## Files

- `Dockerfile` creates one small recipe image and installs `run.sh`.
- `run.sh` reads only the server-owned `LEMC_PHASE` and `LEMC_STEP_ID` values
  and emits them as one safe output marker.
- `cookbook.yaml` declares one action with three phase blocks and no form or
  secret input.

One accepted action creates one transition that owns separate work for each
phase:

| Phase | Step | Eligibility | Expected marker |
| --- | ---: | --- | --- |
| `now` | 10 | At once | `timing tutorial phase=now step=10` |
| `in` | 20 | Two minutes after acceptance | `timing tutorial phase=in step=20` |
| `every` | 30 | Every five minutes while that activation remains eligible | `timing tutorial phase=every step=30` |

The transition ID groups the action. Each returned task ID identifies one
piece of executable or scheduled work. Do not use the transition ID where a
cancel command requires a task ID.

## Web

1. Review `cookbook.yaml`. Confirm that the durations are `2.minutes` and
   `5.minutes`, that step IDs are unique within their phases, and that the
   Cookbook declares no secret or cloud input.
2. Build `tutorial-timing` from `Dockerfile` and `run.sh`. Wait for the Builder
   execution to reach terminal success and record the verified build.
3. Create **Tutorial 04 Timing** from `cookbook.yaml`. Bind the same verified
   build separately to `now` step 10, `in` step 20, and `every` step 30.
   Confirm all three server coordinates use the managed digest.
4. Publish the Cookbook, create a private test App, and run **Observe Three
   Phases** once. Record the transition ID and every returned task or schedule
   identity before you leave the action view.
5. Confirm that NOW starts without waiting and reaches terminal success with
   `phase=now step=10`. IN and EVERY must remain separate scheduled work; they
   must not appear as extra NOW steps.
6. Wait for the two-minute IN deadline. Refresh the same transition and
   confirm that its delayed task runs once, emits `phase=in step=20`, and
   becomes terminal.
7. Wait for one five-minute EVERY occurrence. Confirm that it emits
   `phase=every step=30` under the same activation. Record the current recurring
   schedule identity and occurrence task identity shown by the server.
8. Start **Observe Three Phases** again with a new action request. Record the
   new transition. Confirm that replacement fences the first activation and
   that its prior EVERY schedule is no longer eligible.
9. Cancel the second activation's pending IN task before its due time. Cancel
   its EVERY schedule after you have recorded the replacement proof. Use the
   exact task or schedule IDs returned for the second transition.
10. Wait for terminal cancellation evidence. Refresh both transitions and
    confirm that no task from either activation remains eligible.

## LEMCSSH

Bind page 1, recipe `observe-three-phases`, steps 10, 20, and 30 with phases
`now`, `in`, and `every`. Inspect the action surface first. Save the transition
ID and the child identities from each JSON response. Then use the reviewed job
commands:

```sh
ssh LEMC_ALIAS jobs surface app --uuid APP_UUID \
  --scope individual --format=json

ssh LEMC_ALIAS jobs run app --uuid APP_UUID --page 1 \
  --scope individual --recipe observe-three-phases \
  --idempotency-key REQUEST_UUID --format=json

ssh LEMC_ALIAS jobs status app --uuid APP_UUID --page 1 \
  --scope individual --transition-id TRANSITION_ID --format=json

# After the first IN and EVERY observations, submit the replacement action
# with a new request UUID. Read its status before cancellation.
ssh LEMC_ALIAS jobs run app --uuid APP_UUID --page 1 \
  --scope individual --recipe observe-three-phases \
  --idempotency-key NEW_REQUEST_UUID --format=json

ssh LEMC_ALIAS jobs cancel --task-id IN_TASK_ID \
  --reason tutorial-cleanup --format=json

ssh LEMC_ALIAS jobs cancel --task-id EVERY_TASK_ID \
  --reason tutorial-cleanup --format=json
```

Use a new request UUID for the intended replacement run. Do not reuse the
first action's key to request different work. If a cancel request has an
unknown transport result, read the exact task status before you send it again.

## Expected result

Keep this evidence for both transitions:

| Check | Expected evidence |
| --- | --- |
| Immediate work | NOW emits `phase=now step=10` and reaches terminal success. |
| Delayed work | IN does not run before its due time, then emits `phase=in step=20` once. |
| Recurring work | One observed occurrence emits `phase=every step=30`. |
| Replacement | The first activation and its old recurring schedule become fenced or otherwise ineligible. |
| Explicit cleanup | The second IN and EVERY identities reach terminal cancellation or were already terminal. |
| Final inventory | No task or recurring schedule from either transition remains eligible. |

A successful NOW task alone does not prove the timing contract. The delayed,
recurring, replacement, and final-inventory checks are separate evidence.

## Cleanup and retry

List both transition inventories. Cancel every active or scheduled IN and
EVERY identity that is still eligible. Wait for terminal evidence and refresh
the inventory once more. Only then remove the App and archive its Cookbook and
Development Image.

If cancel returns an unknown transport result, read status before retrying.
Retry the same cancel intent against the same task. Canceling an already
terminal task must not create another task. Never delete scheduler state or
Runner data as a substitute for the supported cancel path.

## Security and cost

Timing does not widen authorization. Scheduled work keeps the accepted App,
page, recipe, image, and activation boundary. A later permission or lifecycle
change can make due work ineligible; it must not silently grant new authority.
The short intervals are for this disposable lesson only. The lesson has no
external service cost, but an uncanceled EVERY schedule can continue to use
runner capacity and storage.

## Troubleshooting

- No `LEMC_PHASE` on NOW is expected; the script treats an absent value as
  `now`.
- An IN or EVERY image preflight failure means that exact phase coordinate was
  not bound to the verified build.
- A stale scheduled row after replacement must show `fenced`; if it remains
  eligible, stop and preserve the transition status for diagnosis.
- A cancel denial means the current user does not own or cannot manage that
  task. Use an authorized account action; do not access the runner host.
- IN running immediately means the phase or duration was not accepted as
  declared. Inspect the server-owned Cookbook copy and the task coordinate.
- More than one eligible EVERY schedule after replacement means the prior
  activation was not fenced. Stop new actions and preserve both transition
  inventories.
- A completed cancellation request with a later occurrence is inconsistent
  with terminal cleanup. Record the task, schedule, transition, and event IDs
  before any further action.

## Internal validation

Maintainers can run `./scripts/validate.sh`. The default gate validates phase
shape and durations but does not enqueue a scheduler task.
