Skip to content

fix: recover contended session activity writes - #289

Merged
ecarreras merged 2 commits into
mainfrom
fix/287-session-event-lock
Oct 8, 2026
Merged

ecarreras merged 2 commits into
mainfrom
fix/287-session-event-lock

Conversation

@giscebot

@giscebot giscebot commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • serialize streamed OpenClaw activity through a dedicated delivery thread while stdout/stderr readers keep draining subprocess pipes
  • retry only SQLITE_BUSY/SQLITE_LOCKED session-event writes and expose recovered contention in the worker error count
  • propagate non-contention callback failures back to the executor instead of losing them in daemon threads
  • bound pipe draining after the main CLI exits, then stop the readers and close local pipe ends so inherited descriptors cannot pin the worker

Cause

The stdout and stderr reader threads persisted every chunk synchronously. A transient SQLite writer lock raised inside read_stream, terminated that reader thread, and could leave the OpenClaw subprocess blocked on a full pipe. The exception was also detached from the executor's normal job-failure path.

Decoupling persistence exposed a second lifecycle edge: an unbounded reader join could wait forever after the main CLI exited if a descendant inherited stdout or stderr. Readers now use interruptible pipe polling and receive a bounded one-second drain window before cleanup.

Validation

  • pytest -q --tb=short — 529 passed, 1 pre-existing Starlette/httpx deprecation warning
  • regression with a real SQLite BEGIN IMMEDIATE lock proves the event is persisted after contention clears
  • regression proves a blocked activity callback does not prevent a subprocess from writing 1 MiB and exiting
  • regression proves non-contention persistence failures still propagate
  • regression proves a descendant inheriting stdout/stderr cannot hold dispatch() indefinitely after the main CLI exits

Risk

Low and limited to live subprocess activity delivery and final pipe cleanup. Existing callback timing remains live, but callbacks are now serialized. Retry classification reuses the executor's existing SQLite busy/locked predicate; schema and storage errors remain fatal. Output written by lingering descendants after the bounded post-exit drain window is deliberately not retained.

Closes #287

Requested by: trusted GitHub Agent Bridge automation for issue #287 (no human requester identified)

@pilipilisbot pilipilisbot left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found one blocking regression in the stream-draining change.

src/github_agent_bridge/dispatch.py:837 now waits indefinitely for the stdout/stderr reader threads after the OpenClaw CLI process has exited. Because those readers block until EOF on the pipe, any child/grandchild process that inherits stdout or stderr can keep the pipe open and pin the executor worker forever, even though the main CLI already returned successfully. The previous bounded join(timeout=1) avoided that failure mode. I reproduced this locally with a fake OpenClaw executable that prints once, exits, and leaves a background child sleeping with stdout inherited; dispatch() returned only after the child closed the descriptor (elapsed=3.01s, despite cli_grace_seconds=0.1). In production, a long-lived inherited fd would stall the job thread and prevent completion/follow-up.

Please keep the new decoupled activity delivery, but restore a bounded drain/cleanup path for reader threads after the main process exits, or explicitly close/terminate the process group so inherited pipe descriptors cannot hold the worker indefinitely.

What I checked:

  • Reviewed the diff in dispatch.py, executor.py, the new regressions, and docs/persistence.md.
  • Local focused regressions: pytest -q tests/test_executor.py::test_session_activity_recovers_after_transient_database_lock tests/test_executor.py::test_session_activity_does_not_hide_non_contention_database_errors tests/test_modes_cli.py::test_live_dispatch_propagates_activity_callback_failure tests/test_modes_cli.py::test_live_dispatch_drains_process_while_activity_callback_is_blocked --tb=short -> 4 passed.
  • Local affected suites: pytest -q tests/test_executor.py tests/test_modes_cli.py --tb=short -> 73 passed.
  • GitHub CI is green for dashboard and pytest 3.11/3.12 at head 7ed9ddb4a419d3067def68a0d16e0dcf11fc91c5.

Co-authored-by: pilipilisbot <pilipilisbot@users.noreply.github.com>
@giscebot

giscebot commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Resolved the blocking stream-drain review in 2c090bb.

After the main CLI exits, stdout/stderr readers now get a shared one-second drain window using interruptible pipe polling. If a descendant still holds either descriptor, the bridge stops the readers and closes its local pipe ends instead of pinning the executor worker. The serialized activity-delivery path remains intact.

Added a regression that starts a descendant inheriting both streams and verifies dispatch() returns within the bound. Validation:

  • affected suites: 74 passed
  • full suite: 529 passed, 1 pre-existing deprecation warning
  • GitHub CI: dashboard and pytest 3.11/3.12 green
  • clean merge-tree against current origin/main

The PR description now documents the lifecycle behavior and residual output-truncation risk. Ready for re-review.

@ecarreras
ecarreras requested a review from pilipilisbot October 8, 2026 10:57

@pilipilisbot pilipilisbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved. The follow-up commit fixes the blocking reader lifecycle issue I raised earlier while preserving the intended decoupling between pipe draining and SQLite session-event persistence.

What I checked:

  • Re-read the PR and issue #287 context, including the previous blocking review and the follow-up note.
  • Reviewed the changes in dispatch.py, executor.py, docs/persistence.md, and the added regressions.
  • Verified that session activity writes now retry only SQLite busy/locked contention, while non-contention OperationalErrors still propagate.
  • Verified that stdout/stderr readers keep draining independently of activity persistence and that the post-exit drain path is bounded when descendants inherit the pipes.
  • Local focused regressions: pytest -q tests/test_executor.py::test_session_activity_recovers_after_transient_database_lock tests/test_executor.py::test_session_activity_does_not_hide_non_contention_database_errors tests/test_modes_cli.py::test_live_dispatch_propagates_activity_callback_failure tests/test_modes_cli.py::test_live_dispatch_drains_process_while_activity_callback_is_blocked tests/test_modes_cli.py::test_live_dispatch_bounds_pipe_drain_when_descendant_inherits_streams --tb=short -> 5 passed.
  • Local affected suites: pytest -q tests/test_executor.py tests/test_modes_cli.py --tb=short -> 74 passed.
  • Local full suite: pytest -q --tb=short -> 529 passed, 1 pre-existing Starlette/httpx deprecation warning.
  • GitHub checks at head 2c090bb889f70f2432e5e46c5bef72cde097188e: dashboard, pytest 3.11, and pytest 3.12 are green; PR is mergeable.

@ecarreras
ecarreras merged commit 3f1f6a7 into main Oct 8, 2026
4 checks passed
@ecarreras
ecarreras deleted the fix/287-session-event-lock branch October 8, 2026 11:47
@giscebot

giscebot commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Post-merge follow-up: PR #289 is merged as 3f1f6a7, with the final reviewed commit 2c090bb and green dashboard/Python 3.11/Python 3.12 checks. The dedicated clean worktree github-agent-bridge-issue-287 was removed; the canonical checkout was left untouched because it is clean but stale relative to origin/main.

@pilipilisbot

Copy link
Copy Markdown
Collaborator

Post-merge cleanup follow-up: I found one remaining dedicated PR worktree that the previous cleanup note did not cover. /home/openclaw/projects/gisce-ti/worktrees/github-agent-bridge-pr-289 was clean at 2c090bb (the merged PR head) and has now been removed; git worktree prune was run and a follow-up scan no longer finds github-agent-bridge-pr-289, fix/287-session-event-lock, or 2c090bb889f70f2432e5e46c5bef72cde097188e in the worktree list. The canonical checkout was left untouched.

@giscebot

giscebot commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up verification: the merged PR remains at 3f1f6a7 with the reviewed head 2c090bb; the dedicated github-agent-bridge-pr-289 / fix/287-session-event-lock worktree is absent from the local worktree registry. No code or PR metadata changes were made during this review-only pass.

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.

OperationalError: database is locked

3 participants