Skip to content

Activity log as a decision log: per-poll observations and rule provenance - #36

Merged
hsliuustc0106 merged 2 commits into
mainfrom
feature/activity-decision-log
Oct 2, 2026
Merged

hsliuustc0106 merged 2 commits into
mainfrom
feature/activity-decision-log

Conversation

@hsliuustc0106

Copy link
Copy Markdown
Contributor

Fixes #35 — makes the activity log a decision log: it now records what the runner observed, not just what changed.

What changed

  • Per-poll observations (runner.py): every completed poll appends a check-observed entry before the events it produced — the commit-pinned snapshot digest plus the dedup fingerprint (new statemachine.observation()). No-change polls are no longer invisible, and any stop/notify decision is replayable from the log alone.
  • Rule provenance (statemachine.py): every event's evidence now names the policy rule that fired (stop: … / notify: … / record: …), and pending events carry the concrete pending_reason that step() already distinguished internally.
  • Atomic recording (runner.py): the supersession check now runs once, after summarizing and before anything is recorded or delivered. A pause/cancel/scope change still leaves zero activity entries, and recording is all-or-nothing per run (previously a lifecycle change mid-loop could leave earlier events recorded).
  • Write-failure isolation (runner.py): activity appends go through a _record helper — a log failure never fails the run or blocks delivery, mirroring the memory-observation pattern.
  • CLI (cli.py): nanodot activity hides check-observed by default; --all includes them. watch list / watch show summaries always skip observations. ActivityLog.query() gained exclude_kinds; same-timestamp entries now order by insertion (rowid) so the log replays as observe → decide → act.

Deliberately unchanged

  • No egress change: the observation digest is the same whitelisted shape as event evidence (docs/design/egress.md), redacted at write time.
  • No schema migration: activity.evidence is free-form JSON.
  • No behavior change to notifications, dedup, or stop conditions.

Verification

  • Full suite: 539 passed (includes new tests: per-poll observation entries, no-op-poll reconstructability, append-failure isolation, rule provenance, CLI --all behavior).
  • Offline first-use demo (examples/first_pr_watch.py): 8/8 scenarios pass through the real CLI.
  • Docs: new docs/design/decision-log.md and a README section.

Medium:
- Classify GitHub's startup_failure conclusion as failing so a workflow
  that fails to start alerts instead of pending forever (github_eval)
- Allow workflow_call events as eligible required PR checks so
  reusable-workflow CI can complete a watch (github_client)
- Map http.client.HTTPException to ProviderError so truncated model
  responses degrade instead of crashing watch add --intent (inference_api)
- Treat a concurrent atomic token rotation as safe (stable regular file
  already opened) and keep the daemon alive through transient
  store-iteration failures (secrets_file, daemon)
- Make config list survive invalid stored values and truncated JSON while
  keeping legacy plaintext secrets masked; guard config unset/keys against
  non-object config.json (cli, config)
- Tokenize query and content symmetrically in relevant_to so the recorded
  PR identity matches remembered context (memory)

Low:
- Reject empty memory content; report hidden older items in memory list
- Decline the watch-add confirmation on EOF stdin; report unusable secret
  stores cleanly across config and watch commands
- Serialize config/secret read-modify-write cycles with sidecar flocks and
  write config.json atomically
- Start the cooperative-stop window after startup-lock acquisition so
  queued time is not deducted from the stop budget (runner_control)
- Classify truncated bodies as retryable network errors, local
  secret-store failures as blockers, and permanent 301/308 redirects as
  PR-not-found blockers instead of endless retries (github_client)
- Default next_check_at at creation so an ACTIVE task is never persisted
  unschedulable (tasks)

535 tests pass (20 new regression tests); the first-use e2e demo passes
all 8 scenarios.
Upgrade the activity log from an event log to a decision log (issue #35):

- Every completed poll appends a check-observed entry (commit-pinned
  snapshot digest + dedup fingerprint) before the events it produced, so
  no-change polls are reconstructable and any decision is replayable from
  the log alone.
- Every event's evidence names the policy rule that fired (stop:/notify:/
  record:) and, for pending states, the concrete pending_reason.
- Recording is all-or-nothing per run: the supersession check now runs
  once, after summarizing and before anything is recorded or delivered,
  so a pause/cancel/scope change still leaves no entries.
- Activity-write failures are isolated: the run and delivery never depend
  on the log accepting an entry.
- nanodot activity hides check-observed by default; --all includes it.
  watch list/show summaries always skip observations.

Docs: README section and docs/design/decision-log.md. Tests: 539 passing,
including new observation/replay/isolation/provenance coverage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Activity log → decision log: record per-poll observations and rule provenance for full replay

1 participant