feat: migrate polling automations to KV store for cloud-safe state persistence#342
Conversation
…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>
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>
|
Pushed a fix in commit 9e43cf4 for a bug affecting all three scripts introduced in this PR. Root cause: Fix: Change This comment was added by an AI agent (OpenHands) on behalf of the user. |
|
✅ Review complete. This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here. |
all-hands-bot
left a comment
There was a problem hiding this comment.
🟡 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:
- Add a
.agents/skills/custom-codereview-guide.mdfile to your branch (or edit it if one already exists) with the/codereviewtrigger 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.- Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
- 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
/iterateto 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
- 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>
|
✅ Review complete. This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here. |
all-hands-bot
left a comment
There was a problem hiding this comment.
🟡 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_statesignatures break the existingskills/github-repo-monitor/tests/test_main.pystate tests. I ranuv run --group test pytest skills/github-repo-monitor/tests/test_main.py -q; 6 tests fail withTypeErrorbecause they still callload_state(path)andsave_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:
- Add a
.agents/skills/custom-codereview-guide.mdfile to your branch (or edit it if one already exists) with the/codereviewtrigger 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.- Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
- 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
/iterateto 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
…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>
|
🔍 Review in progress… We are performing the review through OpenHands Cloud Automation. You can log in and view the conversation here. |
This literally just crashed on a second run.
|
✅ Review complete. This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here. |
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 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 passeduv run python scripts/sync_extensions.py --check- passed with only the existing non-blocking coverage warninguv 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
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 underWORKSPACE_BASE. On cloud deployments where each cron run lands on a fresh pod,WORKSPACE_BASEpoints 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_TOKENandAUTOMATION_API_URLare now injected into every run by the dispatcher.Summary
main.pyscripts (github-pr-reviewer,github-repo-monitor,slack-channel-monitor): replaceload_state(path)/save_state(path, state)with KV-first equivalents that fall back to the existing file path whenAUTOMATION_KV_TOKENis absent (local/dev). The full state document is stored under a single KV key"state". Also fixes theslack-channel-monitordebug log path, which previously navigated two directory levels aboveWORKSPACE_BASE— broken on fresh pods.openhands-automationskill docs (SKILL.md+references/custom-automation.md): add a new "State Persistence (KV Store)" section with the KV API endpoint table, ready-to-copykv_get/kv_sethelpers, and a fullload_state/save_statepattern with fallback; addAUTOMATION_KV_TOKENandAUTOMATION_API_URLto the env vars table.state-schema.mdreference docs: updated to document KV store as primary storage and file as local fallback.Screenshots
Slack Integration:



Github Code Review Agent:



Github Repository Monitor



Issue Number
Related: OpenHands/automation#69
How to Test
AUTOMATION_KV_SECRETset (soAUTOMATION_KV_TOKENis injected into runs).github-repo-monitororslack-channel-monitorautomation and trigger it twice. Confirm thatlast_pollandconversationsfrom run 1 are present at the start of run 2 (visible in run logs: "State loaded from KV store").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
"state") pattern matches the KV store's single-document model, keeping all reads/writes atomic and minimising round-trips.This PR was created by an AI agent (OpenHands) on behalf of the user.