Skip to content

feat: migrate polling automations to KV store for cloud-safe state persistence#342

Merged
tofarr merged 6 commits into
mainfrom
feat/kv-store-state-persistence
Jun 18, 2026
Merged

feat: migrate polling automations to KV store for cloud-safe state persistence#342
tofarr merged 6 commits into
mainfrom
feat/kv-store-state-persistence

Conversation

@tofarr

@tofarr tofarr commented Jun 18, 2026

Copy link
Copy Markdown
Contributor
  • A human has tested these changes.

Why

The three polling skills (github-pr-reviewer, github-repo-monitor, slack-channel-monitor) each persisted run-to-run state — last-poll timestamps, active conversation IDs, processed-event dedup sets — to JSON files under WORKSPACE_BASE. On cloud deployments where each cron run lands on a fresh pod, WORKSPACE_BASE points to a new ephemeral directory, so all state was silently lost between runs. This meant polling automations could re-process already-handled events or lose track of active conversations every run.

OpenHands/automation#69 landed a built-in per-automation KV store that survives across runs. AUTOMATION_KV_TOKEN and AUTOMATION_API_URL are now injected into every run by the dispatcher.

Summary

  • Three main.py scripts (github-pr-reviewer, github-repo-monitor, slack-channel-monitor): replace load_state(path) / save_state(path, state) with KV-first equivalents that fall back to the existing file path when AUTOMATION_KV_TOKEN is absent (local/dev). The full state document is stored under a single KV key "state". Also fixes the slack-channel-monitor debug log path, which previously navigated two directory levels above WORKSPACE_BASE — broken on fresh pods.
  • openhands-automation skill docs (SKILL.md + references/custom-automation.md): add a new "State Persistence (KV Store)" section with the KV API endpoint table, ready-to-copy kv_get / kv_set helpers, and a full load_state / save_state pattern with fallback; add AUTOMATION_KV_TOKEN and AUTOMATION_API_URL to the env vars table.
  • Three state-schema.md reference docs: updated to document KV store as primary storage and file as local fallback.

Screenshots

Slack Integration:
image
image
image

Github Code Review Agent:
image
image
image

Github Repository Monitor
image
image
image

Issue Number

Related: OpenHands/automation#69

How to Test

  1. Deploy or run a local instance with AUTOMATION_KV_SECRET set (so AUTOMATION_KV_TOKEN is injected into runs).
  2. Create a github-repo-monitor or slack-channel-monitor automation and trigger it twice. Confirm that last_poll and conversations from run 1 are present at the start of run 2 (visible in run logs: "State loaded from KV store").
  3. Without AUTOMATION_KV_SECRET (local dev), confirm the automation still works by falling back to the file path (visible in logs: "State saved to /path/to/state.json").

Unit tests: uv sync --group test && uv run pytest -q — 339 tests pass.

Notes

  • No API or schema changes. The state document shape is identical; only the storage backend changes.
  • The single-key ("state") pattern matches the KV store's single-document model, keeping all reads/writes atomic and minimising round-trips.
  • Backward compatible: existing local-dev automations continue working with the file fallback.

This PR was created by an AI agent (OpenHands) on behalf of the user.

…rsistence

Replace disk-based state files with the new automation KV store API
(landed in OpenHands/automation#69) across all three polling plugins.

Problem: github-pr-reviewer, github-repo-monitor, and slack-channel-monitor
each stored their run-to-run state (last_poll timestamp, active conversation
IDs, processed event dedup sets) in JSON files derived from WORKSPACE_BASE.
On cloud deployments where each cron run lands on a fresh pod, WORKSPACE_BASE
points to a new ephemeral directory, so state was never persisted between runs.

Solution: KV-first load/save with file fallback
- Detect KV availability via AUTOMATION_KV_TOKEN (injected by the dispatcher
  whenever the service has AUTOMATION_KV_SECRET configured)
- Store the full state document under a single key ("state") via
  PUT {AUTOMATION_API_URL}/v1/kv/state
- Fall back to the existing file-based path for local/dev environments

Changes:
- github-pr-reviewer, github-repo-monitor, slack-channel-monitor main.py:
  add _kv_available/_kv_get/_kv_set helpers; load_state/save_state no longer
  take a path argument; update main() callers accordingly
- slack-channel-monitor: fix debug log path to write within WORKSPACE_BASE
  rather than two levels up (safe on fresh cloud pods)
- openhands-automation SKILL.md + references/custom-automation.md: add
  "State Persistence (KV Store)" section with API reference, ready-to-copy
  helpers, and load/save pattern; add AUTOMATION_KV_TOKEN and
  AUTOMATION_API_URL to env vars table
- All three state-schema.md docs: document KV store as primary, file as fallback

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions github-actions Bot added the type: feat A new feature label Jun 18, 2026
The GET /v1/kv/{key} endpoint returns {"key": ..., "value": ...}.
_kv_get was returning the full envelope instead of data["value"],
causing load_state() to receive {"key": "state", "value": {...}}
on the second run and raising KeyError on any state field access.

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr

tofarr commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a fix in commit 9e43cf4 for a bug affecting all three scripts introduced in this PR.

Root cause: _kv_get was returning json.loads(r.read()) — the full KVKeyResponse envelope {"key": "state", "value": {...}} — rather than just data["value"] (the actual state dict). On the first run there is no KV entry yet, so _kv_get returns None and load_state falls back to _default_state(), which succeeds. On the second run the KV store returns 200 with the wrapped response, load_state hands back {"key": "state", "value": {...}} as the state dict, and any access to a real state field (e.g. state["last_poll"]) raises KeyError.

Fix: Change return json.loads(r.read()) to return json.loads(r.read())["value"] in _kv_get in all three scripts. The second json.loads call in each file (inside get_settings) is a different API and is unchanged.

This comment was added by an AI agent (OpenHands) on behalf of the user.

@tofarr
tofarr marked this pull request as ready for review June 18, 2026 17:11
Comment thread skills/github-pr-reviewer/references/state-schema.md
@malhotra5
malhotra5 requested a review from all-hands-bot June 18, 2026 17:18

all-hands-bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Taste Rating: Acceptable - the KV-store direction is the right fix, but the current fallback semantics and copy-paste docs still leave real correctness traps.

[CRITICAL ISSUES]

  • [skills/github-pr-reviewer/scripts/main.py, Line 142] Correctness: When KV is configured, falling back to a local file on KV read/write errors silently reintroduces ephemeral state in cloud runs. That can duplicate label/comment processing or lose active conversation tracking. The file fallback should only be used when KV is not configured; KV operation failures should fail the run so it can retry against the durable store.
  • [skills/openhands-automation/references/custom-automation.md, Line 536] Documentation Correctness: The ready-to-copy KV helpers still return the full {"key": "state", "value": ...} envelope instead of the stored value, while the scripts were fixed to unwrap ["value"]. Users pasting the documented pattern will load the wrong state shape.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    This changes state persistence for three polling automations and touches token-based access to a new service-side store. The blast radius is limited to these skills/scripts, but incorrect state handling can cause duplicate automation work or lost conversation tracking across runs.

VERDICT:
Needs rework: The durable-state path is sound, but cloud runs should fail closed on KV errors and the docs must match the actual KV response shape before this is safe to merge.

KEY INSIGHT:
Durable state only helps if KV remains the single source of truth whenever it is configured; otherwise transient KV failures degrade back to the exact ephemeral-state behavior this PR is trying to eliminate.

Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.
  2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it's merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

Comment thread skills/github-pr-reviewer/scripts/main.py Outdated
Comment thread skills/openhands-automation/references/custom-automation.md Outdated
- load_state / save_state in all three poller scripts (github-pr-reviewer,
  github-repo-monitor, slack-channel-monitor): remove the broad except-and-
  fallback around KV calls.  When AUTOMATION_KV_TOKEN/AUTOMATION_API_URL are
  present the file path is ephemeral on cloud pods, so swallowing a transient
  5xx/network error and re-starting from an empty default state would re-
  process already-handled events.  Now any KV error propagates and lets the
  scheduler retry against the durable store.  The local-file branch is
  reached only when the KV env-vars are absent.
- custom-automation.md: apply the same pattern to the reference load_state /
  save_state snippet so copy-pasted code behaves identically.
- custom-automation.md: fix both kv_get helpers to return json.loads(r.read())["value"]
  (unwrap the response envelope) matching the live scripts.

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr
tofarr requested a review from all-hands-bot June 18, 2026 17:40

all-hands-bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Taste Rating: Acceptable - the KV-store migration is directionally sound, and the previously flagged KV fallback/envelope issues are fixed. The PR still leaves an existing skill test suite broken, so it is not merge-ready yet.

[CRITICAL ISSUES]

  • [skills/github-repo-monitor/scripts/main.py, Line 183] Testing Regression: The changed load_state / save_state signatures break the existing skills/github-repo-monitor/tests/test_main.py state tests. I ran uv run --group test pytest skills/github-repo-monitor/tests/test_main.py -q; 6 tests fail with TypeError because they still call load_state(path) and save_state(path, state). Update the tests to cover the new file-fallback behavior and add KV-path coverage, or preserve a compatible helper seam.

[TESTING GAPS]

  • The runtime change touches durable state persistence for three polling automations, but only the repo-monitor has local state tests and those are now stale. Before merging, the PR should include regression coverage that proves: missing/corrupt local fallback still starts from defaults, valid local state still loads, KV 404 returns defaults, KV GET unwraps value, and configured KV errors propagate instead of falling back to ephemeral files.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    This changes run-to-run state persistence for three polling automations and introduces token-authenticated calls to the automation KV store. The blast radius is limited to these skills, but regressions can duplicate GitHub/Slack event handling or lose active conversation tracking across runs. Existing CI checks pass, but a relevant in-repo skill test suite fails when run directly.

VERDICT:
Needs rework: Core direction is sound, but the stale/broken tests need to be updated before this is safe to merge.

KEY INSIGHT:
Durable state is now the critical contract for these pollers, so the tests need to exercise the new storage boundary rather than only the old path-argument file helpers.

Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.
  2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it is merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

Comment thread skills/github-repo-monitor/scripts/main.py
…KV coverage

The load_state/save_state signatures changed from load_state(path)/
save_state(path, state) to load_state()/save_state(state) in the KV
migration. Update the six affected state tests to patch _kv_available
(False) and _state_file_path so they exercise the file-fallback branch
without touching the real env. Add a new TestKVState class covering:
- KV read returning data
- KV miss returning default
- KV write via _kv_set
- KV errors propagating on load (no silent fallback)
- KV errors propagating on save (no silent fallback)

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr
tofarr requested a review from all-hands-bot June 18, 2026 18:33

Copy link
Copy Markdown
Contributor

🔍 Review in progress…

We are performing the review through OpenHands Cloud Automation. You can log in and view the conversation here.

@tofarr
tofarr dismissed all-hands-bot’s stale review June 18, 2026 18:55

This literally just crashed on a second run.

@tofarr
tofarr requested a review from malhotra5 June 18, 2026 18:55

all-hands-bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Taste Rating: Good taste

The PR solves a real operational problem with a simple KV-first state model while preserving the local-file fallback for development. The core choice to keep the existing state document shape and store it under a single per-automation key avoids migration complexity and keeps the live poller logic easy to reason about.

I reviewed the three poller state changes, the KV helper documentation, generated catalog updates, and existing review threads. The earlier fallback/error-handling and KV envelope concerns appear resolved: configured KV failures now propagate instead of silently using ephemeral files, and GET responses are unwrapped to the stored value.

Validation checked:

  • uv run --group test pytest skills/github-repo-monitor/tests/test_main.py -q - 43 passed, 3 subtests passed
  • uv run python scripts/sync_extensions.py --check - passed with only the existing non-blocking coverage warning
  • uv run python scripts/sync_openhands_sdk_skill.py --check - SDK skill is up to date
  • GitHub PR checks are all successful

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    This touches persisted state handling for multiple polling automations and depends on the automation service KV API, so the blast radius is higher than a documentation-only change. The implementation is intentionally narrow, keeps existing state schemas intact, preserves local fallback behavior, and has both focused tests plus passing CI, so I do not see a blocking merge risk.

VERDICT:
Worth merging: Core logic is sound and the PR addresses the cloud-persistence failure mode directly.

KEY INSIGHT:
The important design win is that state ownership stays local to each automation while storage durability moves behind a service-provided per-automation KV namespace.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

@malhotra5 malhotra5 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tofarr
tofarr merged commit af767dd into main Jun 18, 2026
6 checks passed
@tofarr
tofarr deleted the feat/kv-store-state-persistence branch June 18, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants