<!-- task=m-node-provider-execution-liveness-recovery/02+01_stall_watchdog plan=8 tag=REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API milestone-task=activity-contract,stall-watchdog -->

# PLAN — Complete Scheduled Watchdog Expiry Fixtures

## For the Implementing Agent

> **MANDATORY:** Implement only this follow-up checklist and preserve unrelated worktree changes. Run every verification command, fill all implementation-owned sections of `CODE_REVIEW-cloud-G02.md` with literal results, keep the active pair in place, and report ready for review. If blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence. Do not ask the user, call user-input tools, create stop files, classify the next state, archive logs, or write `complete.log`; finalization belongs to the official code-review agent.

## Background

The prior follow-up was never implemented: both deadline-aware fixtures still synthesize a timestamp before the current timer arm's scheduled deadline, and the active review contains only pending evidence. Fresh official review reproduced both failures in all 20 focused runs and in the Node package suite. This follow-up applies the already-bounded test-only repair and restores trustworthy S01/S02 verification without changing production behavior.

## Archive Evidence Snapshot

- The closed pair is `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/plan_cloud_G04_7.log` and `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/code_review_cloud_G04_7.log`.
- Prior verdict: FAIL. Required=1, Suggested=0, Nit=0.
- Required fix: make `TestAttemptObserverProgressResetsAndFenceIsMonotonic` and `TestTunnelSinkStallClaimSerializesAcceptedFrame` fire and consume the scheduled current manual timer arm instead of passing a pre-deadline `clock.Now()` value.
- Fresh reviewer evidence: the exact focused command failed both target tests in every one of 20 runs, and `go test -count=1 ./apps/node/internal/node` failed the same two tests. The active implementation checklist and every verification result remained pending.
- Roadmap carryover: preserve `milestone-task=activity-contract,stall-watchdog`; restore approved SDD S02 verification while retaining S01 coverage, and do not update roadmap state.

## Dependencies and Execution Order

- Runtime predecessor `01_activity_contract` remains satisfied by `agent-task/archive/2026/08/m-node-provider-execution-liveness-recovery/01_activity_contract/complete.log`.
- Complete FIX-1 before VERIFY-1 so the final matrix exercises the corrected fixtures.

## Analysis

### Files Read

- `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/PLAN-cloud-G04.md`
- `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/CODE_REVIEW-cloud-G04.md`
- `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/code_review_cloud_G08_6.log`
- `agent-task/archive/2026/08/m-node-provider-execution-liveness-recovery/01_activity_contract/complete.log`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-provider-execution-liveness-recovery.md`
- `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-contract/inner/execution-runtime.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `apps/node/internal/node/liveness_watchdog.go`
- `apps/node/internal/node/liveness_watchdog_test.go`
- `apps/node/internal/node/run_handler.go`
- `apps/node/internal/node/tunnel_handler.go`
- `apps/node/internal/transport/session.go`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`; status `[승인됨]`, lock released, and no `USER_REVIEW.md`.
- Header ids remain `activity-contract,stall-watchdog`; both ids exist in the active Milestone.
- S01 preserves normalized/tunnel progress reset and terminal behavior. S02 requires timer/event/reset/cancel/close races to converge on one terminal and a monotonic local fence.
- The S02 Evidence Map requires deterministic threshold and timer/event race evidence. FIX-1 repairs the two invalid current-arm fixtures; VERIFY-1 reruns the S01/S02 matrix without claiming roadmap completion.

### Verification Context

- No external handoff was supplied. Repository-native sources were `agent-test/local/rules.md`, `agent-test/local/node-smoke.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/testing-smoke.md`, the Node/platform/testing domain rules, and `agent-ops/skills/project/e2e-smoke/SKILL.md`.
- Preconditions are the current local checkout, Go `go1.26.2 linux/arm64`, and module `/config/workspace/iop-s1/go.mod`. No external provider, credential, remote runner, user authorization, or external-execution preflight is required.
- Fresh evidence: the focused count-20 command exited 1 with both target tests failing every run; the Node package command exited 1 with the same two failures. Confidence is high because both failures map directly to `clock.Now()` before `expiresAt`.
- `scripts/e2e-smoke.sh` is auxiliary mock smoke. The prebuilt reconnect diagnostic is a repository-local binary diagnostic. Full real-CLI cycle is excluded because this follow-up changes only deterministic test fixtures, not a production execution path.
- Final Go commands use explicit counts; cached output is not acceptance evidence.

### Test Coverage Gaps

- `TestAttemptObserverProgressResetsAndFenceIsMonotonic` currently fails before proving duplicate fence rejection because it never consumes the reset arm's scheduled signal.
- `TestTunnelSinkStallClaimSerializesAcceptedFrame` currently fails before proving the stall terminal ordering because it never consumes the current arm after the accepted frame Send completes.
- Existing initial-fire, reset-during-fire, receive-before-capture, capture-before-claim, lifecycle, ownership, metadata, session cancellation, and credential cases remain the regression matrix; no new production test shape is needed.

### Symbol References

- None. No production or test symbol is renamed or removed.

### Split Judgment

- Keep one atomic follow-up. Both failures are the same manual-timer fixture contract in one test file and share one deterministic PASS state.
- Predecessor index `01` is satisfied by the archived `complete.log` listed above.

### Scope Rationale

- In scope: two timer-signal call sites in `apps/node/internal/node/liveness_watchdog_test.go`, the active follow-up review evidence, and fresh verification.
- Excluded: `apps/node/internal/node/liveness_watchdog.go`, run/tunnel handlers, transport, common runtime contracts, specs, scripts, readability baselines/read sets, and roadmap state. Fresh failures require no production change.

### Final Routing

- `evaluation_mode=isolated-reassessment`; `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`; status `routed`.
- Build closures: scope/context/verification/evidence/ownership/decision all true; scores 0/1/0/0/1 => G02; base `local-fit`; `recovery-boundary` selects cloud and `PLAN-cloud-G02.md`.
- Build signals: `large_indivisible_context=false`; positive risks `temporal_state`, `concurrent_consistency`, `variant_product` (3); `review_rework_count=6`; `evidence_integrity_failure=false`; recovery boundary matched and risk boundary did not match; capability gap none.
- Review closures: scope/context/verification/evidence/ownership/decision all true; scores 0/1/0/0/1 => G02; `official-review`, cloud, Codex `gpt-5.6-sol` xhigh, `CODE_REVIEW-cloud-G02.md`; capability gap none.

## Implementation Checklist

- [ ] [FIX-1] Fire and consume the scheduled current manual timer signal in both stale fixtures, preserving monotonic duplicate-fence rejection and accepted-frame-before-terminal serialization.
- [ ] [VERIFY-1] Run the complete fresh S01/S02 verification matrix and record literal stdout/stderr plus exit codes in `CODE_REVIEW-cloud-G02.md`.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [FIX-1] Consume the scheduled current timer signal

**Problem**

At `apps/node/internal/node/liveness_watchdog_test.go:332` and `apps/node/internal/node/liveness_watchdog_test.go:397`, the tests pass `clock.Now()` immediately after a progress reset. That timestamp is earlier than the current arm's `expiresAt`, so the production validity check correctly rejects it before the intended assertions run.

**Solution**

Before (`liveness_watchdog_test.go:332`, with the same invalid pattern at line 397):

```go
expiry, valid := observer.expiryForSignal(clock.Now())
```

After for the observer fixture:

```go
timer.fire()
expiry, valid := observer.expiryForSignal(<-observer.expired())
```

For the tunnel fixture, retain timer 0 from `clock.waitTimer(t, 0)`, call `fire()` only after the accepted body frame's `Send` has completed, then consume `<-sink.observer.expired()` before `claimStall`. Preserve the second `claimFence` rejection, frame order, exactly-one terminal, and late usage rejection.

**Modified Files and Checklist**

- [ ] `apps/node/internal/node/liveness_watchdog_test.go` — repair the two current-arm expiry fixtures only.

**Test Strategy**

Repair the existing regression tests rather than add duplicates. `TestAttemptObserverProgressResetsAndFenceIsMonotonic` must reach duplicate-claim rejection; `TestTunnelSinkStallClaimSerializesAcceptedFrame` must retain accepted body before stall terminal and reject late usage.

**Verification**

- `go test -count=20 ./apps/node/internal/node -run 'Test(AttemptObserverProgressResetsAndFenceIsMonotonic|AttemptObserverCurrentArmSignalSurvivesImmediateFire|RunWatchdogOldArmFireDuringResetYieldsToProgress|TunnelWatchdogOldArmFireDuringResetYieldsToProgress|RunWatchdogStaleExpiryYieldsToProgress|TunnelWatchdogStaleExpiryYieldsToProgress|RunWatchdogStaleExpiryBeforeCaptureYieldsToProgress|TunnelWatchdogStaleExpiryBeforeCaptureYieldsToProgress|TunnelConfirmedFenceClosesOwnershipBeforeTerminal|RunWatchdogLifecycle|TunnelWatchdogLifecycle|TunnelSinkStallClaimSerializesAcceptedFrame|TunnelCredentialFailureReleasesAdmission)$'`
- `go test -count=1 ./apps/node/internal/node`

### [VERIFY-1] Restore complete verification evidence

**Problem**

The closed review contains no implementation or verification evidence, while fresh official review proves the required focused and package suites are red.

**Solution**

Run every final command after FIX-1. Record literal stdout/stderr and exit code; do not reconstruct zero-exit evidence. Keep the Node binary under `/tmp` and use `IOP_NODE_BIN` for the reconnect diagnostic.

**Modified Files and Checklist**

- [ ] `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/CODE_REVIEW-cloud-G02.md` — record implementation notes and literal final evidence.

**Test Strategy**

Use the existing focused temporal matrix, package/race/full Go suites, auxiliary mock smoke, prebuilt reconnect diagnostic, readability ratchet, formatting, and diff checks. No external provider or real-CLI profile is required for a test-only fixture correction.

**Verification**

- `go test -race -count=3 ./packages/go/execution ./apps/node/internal/node ./apps/node/internal/transport`
- `go test -count=1 ./...`
- `IOP_NODE_BIN=/tmp/iop-review-node IOP_DEV_RECONNECT_BIND_TIMEOUT=300 ./scripts/dev/edge-node-reconnect-diagnostic.sh`

## Modified Files Summary

| Path | Action | Checklist |
|------|--------|-----------|
| `apps/node/internal/node/liveness_watchdog_test.go` | modify | FIX-1 |
| `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/CODE_REVIEW-cloud-G02.md` | update evidence | FIX-1, VERIFY-1 |

## Final Verification

1. `go version && go env GOMOD`
2. `go test -count=20 ./apps/node/internal/node -run 'Test(AttemptObserverProgressResetsAndFenceIsMonotonic|AttemptObserverCurrentArmSignalSurvivesImmediateFire|RunWatchdogOldArmFireDuringResetYieldsToProgress|TunnelWatchdogOldArmFireDuringResetYieldsToProgress|RunWatchdogStaleExpiryYieldsToProgress|TunnelWatchdogStaleExpiryYieldsToProgress|RunWatchdogStaleExpiryBeforeCaptureYieldsToProgress|TunnelWatchdogStaleExpiryBeforeCaptureYieldsToProgress|TunnelConfirmedFenceClosesOwnershipBeforeTerminal|RunWatchdogLifecycle|TunnelWatchdogLifecycle|TunnelSinkStallClaimSerializesAcceptedFrame|TunnelCredentialFailureReleasesAdmission)$'`
3. `go test -count=1 ./apps/node/internal/node`
4. `go test -count=10 ./apps/node/internal/transport -run 'TestSessionLifetimeCancels(Run|Tunnel)Handler$'`
5. `go test -race -count=3 ./packages/go/execution ./apps/node/internal/node ./apps/node/internal/transport`
6. `go vet ./packages/go/execution ./apps/node/internal/node ./apps/node/internal/transport`
7. `go test -count=1 ./packages/go/execution ./apps/node/...`
8. `go test -count=1 ./...`
9. `./scripts/e2e-smoke.sh`
10. `go build -o /tmp/iop-review-node ./apps/node/cmd/node`
11. `IOP_NODE_BIN=/tmp/iop-review-node IOP_DEV_RECONNECT_BIND_TIMEOUT=300 ./scripts/dev/edge-node-reconnect-diagnostic.sh`
12. `make readability-audit || test $? -eq 2`
13. `python3 - <<'PY'
import json

audit = json.load(open('build/readability-audit.json', encoding='utf-8'))
baseline = json.load(open('scripts/readability_baseline.json', encoding='utf-8'))
targets = {
    ('apps/node/internal/node/run_handler.go', 'Node.OnRunRequest'),
    ('apps/node/internal/node/tunnel_handler.go', 'Node.OnProviderTunnelRequest'),
    ('apps/node/internal/transport/session.go', 'newSession'),
}
base_functions = {(x['path'], x.get('function')): x['value'] for x in baseline['function_thresholds']}
current_functions = {(x['path'], x.get('function')): x['value'] for x in audit['violations'] if x.get('metric') == 'function_loc'}
bad = {key: current_functions.get(key, 0) for key in targets if current_functions.get(key, 0) > base_functions[key]}
base_sets = {x['task_id']: x['value'] for x in baseline['task_read_set_totals']}
current_sets = {x['task_id']: x['total_loc'] for x in audit['task_read_sets']}
if current_sets['node-core-readability'] > base_sets['node-core-readability']:
    bad['node-core-readability'] = current_sets['node-core-readability']
if bad:
    raise SystemExit(f'touched readability regression: {bad}')
print('touched readability regression: none')
PY`
14. `test -z "$(gofmt -l apps/node/internal/node/liveness_watchdog.go apps/node/internal/node/liveness_watchdog_test.go)"`
15. `git diff --check`

Expected: commands 1-11 and 13-15 exit 0. Command 12 may exit 0 or the known Make exit 2 only; command 13 must prove no touched-function/read-set regression. Fresh counts are required. Both repaired tests and the existing deadline/order/lifecycle cases must pass without production watchdog changes or readability baseline edits.

**After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.**
