Skip to content

Execution Log: one source of truth for the poller's pid

Append-only log for issue-205. Ticket: #205.

Phase transitions

PhaseEnteredReviewed/approved byNotes
phase-selection2026-08-11The owner dispatched the ticket straight to a cloud session, so no checklist was posted and no phase was declared away by a human. Rigor scaled to the change per config.autonomy — risk tier 2, no sensitive path, and the deleted field has no consumer — so the chain is one requirements.md carrying its own design and test matrix, as issue-201 did. The human gate is the PR review.
requirements-definition2026-08-11requirements.md locked — two requirements: one source of truth for the pid, and the two-file separation recorded where the next reader meets it.
implementation2026-08-11pid removed from Heartbeat, PollHeartbeat and the written document; the rationale written into the module docstring, docs/cli/state.md, the capability doc and decision-076.
verification2026-08-11Every row of the matrix executed; evidence recorded.
needs-review2026-08-11Self-review; awaiting the human gate on the PR.

Pull requests

PRScope / tasksStatus
MadaraUchiha-314/the-loop — claude/github-issue-205-jn4vq8the whole work itemopen

Progress entries

2026-08-11 — the answer, and the field it removes

  • Phase: requirements-definition → implementation → verification
  • Did: Answered both branches the ticket allowed. Two files are required — the atomic rewrite that keeps the heartbeat crash-safe replaces the inode the flock is held on, so merging them would free the poller's own lock on its first cycle; their lifetimes (removed on release vs kept after exit) and their failure policies (fatal vs swallowed) are opposite too. And the duplicate was removed — not the file, the pid field inside it, written every cycle and read by nothing, since poll status, daemon_status and every client over them take the pid from RunLock.holder(). Deleted from Heartbeat, from PollHeartbeat.__init__ and from the document; from_mapping drops a pid left by an older poller.
  • Checkpoint/tests: the inode claim is a test, not a sentence — test_writing_a_heartbeat_over_the_pidfile_would_free_the_lock performs exactly what PollHeartbeat._write does over a held RunLock and asserts the lock goes free. Added test_a_pid_left_in_an_older_heartbeat_is_never_reported, which plants a liveos.getpid() in the heartbeat and asserts no surface reports it. make test 1800 passed, 1 skipped; gates green.
  • Next: the reviewer briefing on the PR, and the answer posted on the ticket.

Verification results

Recorded in requirements.md § Testing, against the matrix rows that planned them, with the raw output in evidence/verification.md. This work item has no separate testing-plan.md: the matrix is small enough to live with the requirements it proves.

Design critic review

Not selected. design-critic-review is opt-in (issue-188) and this work item did not tick it.

Review cycles

CycleType (self/critic/security)ReviewerOutcomeLink
1selfthe-loop (this session)new findings — the first cut deleted the field and left docs/cli/state.md and the_loop.state.GENERATED_PATHS both claiming the heartbeat holds a pid. The docs↔code parity check is on the paths, not their descriptions, so nothing would have caught itstate.md, state.py
2selfthe-loop (this session)new findings — the rationale existed only as prose. Rewrote it as test_writing_a_heartbeat_over_the_pidfile_would_free_the_lock, so the reason the files stay separate fails the build if it ever stops being truetest_poll_heartbeat.py
3selfthe-loop (this session)zero (converged)
4criticunavailable — reviews.critics is empty in this project's config.the-loop/harness-config.yaml
5securitythe-loop checklistno findings — see the gate belowrequirements.md

Security review (gate)

  • Mechanism: the-loop checklist (security.review.mechanism: auto). The change is a deletion: no new reader, writer, path or parser.
  • Outcome: pass, no findings — and the forgeable surface narrows. The heartbeat has always been untrusted input to poll status, and liveness has always come from the lock (issue-191). Removing pid means a forged or stale heartbeat can no longer even appear to name a live process to an operator reading the file by hand. Abuse case 2 is pinned by test_a_pid_left_in_an_older_heartbeat_is_never_reported, which deliberately uses a live pid.
  • Human sign-off: n/a — risk tier 2, below security.review.humanSignOffMinTier: 4.

Final validation evidence

RequirementProved by
R1 — the pid has exactly one source of truthtest_the_heartbeat_records_no_pid (document keys and the model), test_an_older_heartbeat_carrying_a_pid_still_reads (R1.3), test_a_pid_left_in_an_older_heartbeat_is_never_reported (R1.2), and test_poll_status.py unchanged and green for R1.4
R2 — the separation is recorded where the next reader will meet itdocs/cli/state.md § Why this is a second file, and not part of the pidfile; the the_loop.poller.heartbeat module docstring; decision-076

Capability docs

Capability docWhat changedHistory row
cli.mdThe poll status behaviour now says liveness and the reported pid come from the lock, and a new bullet states that the heartbeat carries no pid, that an older one's is ignored, and the three reasons the two files stay separateissue-205 row added at the top of § History

Documentation

DocumentWhat changed
docs/cli/state.mdThe heartbeat's JSON sample lost its pid; the classification table row says the pid is poll.pid's to name; a new subsection answers why this is a second file, and not part of the pidfile with the three-row comparison, and the poll.pid section links to it
docs/decisions/decision-076.mdNew — the split, the deleted field, and the four alternatives rejected (including both merge directions)
README.md, cli/README.md, docs/cli/commands/poll.mdUnchanged, deliberately: all three describe poll status reporting a pid, which it still does — from the lock, as it always has

Released under the MIT License.