Skip to content

Add durable local GitHub waitpoints for Codex#4267

Draft
Jules-Astier wants to merge 3 commits into
pingdotgg:mainfrom
Jules-Astier:agent/durable-github-waitpoints
Draft

Add durable local GitHub waitpoints for Codex#4267
Jules-Astier wants to merge 3 commits into
pingdotgg:mainfrom
Jules-Astier:agent/durable-github-waitpoints

Conversation

@Jules-Astier

@Jules-Astier Jules-Astier commented Jul 22, 2026

Copy link
Copy Markdown

Summary

  • expose t3.await_github to Codex for checks-settled, new-review-activity, and PR-closed waits
  • persist registration snapshots and delivery state in SQLite migration 034
  • run a restart-safe local worker through the authenticated gh CLI, without T3 Connect or a public endpoint
  • resume through normal orchestration with a delivery lease and deterministic command/message IDs
  • expire stale waitpoints if the originating canonical T3 turn is no longer current

Closes #4266.

Delivery semantics

The tool registers the waitpoint and returns immediately, telling the agent to finish the current turn. The worker polls due rows every 30 seconds while T3 is running. Pending rows and expired delivery leases are recovered after restart. If T3 crashes after dispatch but before marking delivery, the existing orchestration command-receipt store deduplicates the retry.

The UI needs no new transition for this milestone: delivery is a normal thread.turn.start, so connected clients show the thread running again through existing projection events.

Verification

  • 39 focused tests across migration, repository, GitHub normalization/conditions, registration, worker, orchestration gateway, and Codex dynamic-tool/runtime integration
  • server package typecheck (only the three pre-existing Effect suggestions in orchestration/decider.ts)
  • targeted lint and formatting checks across all 22 changed files
  • server boot smoke with migration 034 and the combined orchestration/watcher service graph
  • live authenticated gh pr view adapter smoke against Add a host-managed wait tool for Codex sessions #4262

Intentional limitations

  • Codex only until other providers expose an equivalent host-tool hook
  • the local T3 server must eventually run to observe and deliver a trigger
  • local polling, not webhook delivery; no GitHub App or T3 Connect dependency
  • no cross-waitpoint request coalescing or conditional HTTP yet
  • no bespoke sidebar waiting state yet

Stack note

This is a stacked draft based on #4262 (t3.wait). Until #4262 lands, GitHub shows its two foundation commits in this PR as well. The durable GitHub implementation begins at 35dba04f.

Note

Add durable GitHub waitpoints and T3 dynamic tools for Codex sessions

  • Adds two dynamic tools (t3.wait and t3.await_github) to Codex session runtimes, allowing threads to pause for a timed delay or until a GitHub PR condition is met.
  • Introduces a GitHubWaitpointRepository backed by a new SQLite table (github_waitpoints, migration 034) with idempotent registration, exclusive claim leases, rescheduling, and expiration.
  • Adds a GitHubWaitpointWorker that polls due waitpoints every 5 seconds, probes current PR state via the GitHub CLI, evaluates conditions (checks_settled, new_review_activity, pull_request_closed), and resumes threads via GitHubWaitpointThreadGateway when conditions are satisfied.
  • Wires the full runtime into the server layer and exposes registerGitHubWaitpoint through CodexAdapter and CodexDriver.
  • Risk: the background worker loop and GitHub CLI probing add persistent I/O load per server instance; threads can be left in delivering state until lease expiry if the worker crashes mid-claim.
📊 Macroscope summarized 35dba04. 15 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c529f66b-429a-4f97-b03d-a93fa926a857

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 22, 2026
});
return;
}
if (threadStatus.value.latestTurnId !== waitpoint.originatingTurnId) {

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.

🟠 High github/GitHubWaitpointWorker.ts:204

The pre-claim latestTurnId check on line 204 expires a waitpoint that was already successfully delivered. If a crash occurs after threads.resume dispatches the continuation but before repository.markDelivered records the delivery, the thread has already advanced past originatingTurnId. On recovery, this check treats that expected advancement as unrelated activity and expires the row instead of allowing the deterministic command ID to be deduplicated by the orchestration layer. The crash-recovery path is thereby defeated — a delivered continuation is recorded as expired. Consider deferring or relaxing this check until after the delivery lease is claimed, or removing it from the pre-claim path so that crash-recovery relies on command-receipt deduplication to complete bookkeeping.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/github/GitHubWaitpointWorker.ts around line 204:

The pre-claim `latestTurnId` check on line 204 expires a waitpoint that was already successfully delivered. If a crash occurs after `threads.resume` dispatches the continuation but before `repository.markDelivered` records the delivery, the thread has already advanced past `originatingTurnId`. On recovery, this check treats that expected advancement as unrelated activity and expires the row instead of allowing the deterministic command ID to be deduplicated by the orchestration layer. The crash-recovery path is thereby defeated — a delivered continuation is recorded as expired. Consider deferring or relaxing this check until after the delivery lease is claimed, or removing it from the pre-claim path so that crash-recovery relies on command-receipt deduplication to complete bookkeeping.

}),
);

yield* client.handleServerRequest("item/tool/call", dynamicToolWaitRegistry.handle);

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.

🟠 High Layers/CodexSessionRuntime.ts:1163

When cancelTurn or cancelAll fires before a t3.await_github tool call is handled, the registry still invokes handleT3CodexDynamicToolCall, which ignores the cancellation signal and calls registerGitHubWaitpoint unconditionally. This persists a durable waitpoint for a turn that was already interrupted or for a session that was closed, causing the thread to resume later even though the turn was explicitly canceled.

The await_github branch in handleT3CodexDynamicToolCall never checks the cancelled effect — it proceeds to register the waitpoint regardless of whether the turn or session was canceled. Consider checking the cancellation state before registering the GitHub waitpoint, so late or concurrent calls after an interrupt or close do not create durable waitpoints.

Also found in 1 other location(s)

apps/server/src/provider/Layers/CodexDynamicTools.ts:155

The await_github branch never observes the cancelled effect. Consequently, when makeT3CodexDynamicToolWaitRegistry.handle rejects registration because the turn was already interrupted or the session is closed, it passes an already-completed cancellation effect but this branch still calls registerGitHubWaitpoint and reports success. A late tool request can therefore create a durable waitpoint for a cancelled/closed turn, unlike the ordinary wait path.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexSessionRuntime.ts around line 1163:

When `cancelTurn` or `cancelAll` fires before a `t3.await_github` tool call is handled, the registry still invokes `handleT3CodexDynamicToolCall`, which ignores the cancellation signal and calls `registerGitHubWaitpoint` unconditionally. This persists a durable waitpoint for a turn that was already interrupted or for a session that was closed, causing the thread to resume later even though the turn was explicitly canceled.

The `await_github` branch in `handleT3CodexDynamicToolCall` never checks the `cancelled` effect — it proceeds to register the waitpoint regardless of whether the turn or session was canceled. Consider checking the cancellation state before registering the GitHub waitpoint, so late or concurrent calls after an interrupt or close do not create durable waitpoints.

Also found in 1 other location(s):
- apps/server/src/provider/Layers/CodexDynamicTools.ts:155 -- The `await_github` branch never observes the `cancelled` effect. Consequently, when `makeT3CodexDynamicToolWaitRegistry.handle` rejects registration because the turn was already interrupted or the session is closed, it passes an already-completed cancellation effect but this branch still calls `registerGitHubWaitpoint` and reports success. A late tool request can therefore create a durable waitpoint for a cancelled/closed turn, unlike the ordinary wait path.

});
}

export const make = Effect.gen(function* () {

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.

🟡 Medium persistence/GitHubWaitpoints.ts:171

markWaitpointDelivered, rescheduleWaitpoint, and expireWaitpoint match rows by id and state alone, without checking the current lease. When worker A's lease expires and worker B reclaims the row, A can still overwrite B's claim — e.g., A reschedules the waitpoint after B has dispatched, then B's markDelivered matches zero rows (since the state is no longer delivering) but the call still returns success, leaving a dispatched waitpoint stuck in pending for further retries. The WHERE clauses in these updates need to include the caller's lease token/value and the calls need to detect zero-row updates so stale workers cannot mutate the current owner's claim.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/persistence/GitHubWaitpoints.ts around line 171:

`markWaitpointDelivered`, `rescheduleWaitpoint`, and `expireWaitpoint` match rows by `id` and state alone, without checking the current lease. When worker A's lease expires and worker B reclaims the row, A can still overwrite B's claim — e.g., A reschedules the waitpoint after B has dispatched, then B's `markDelivered` matches zero rows (since the state is no longer `delivering`) but the call still returns success, leaving a dispatched waitpoint stuck in `pending` for further retries. The `WHERE` clauses in these updates need to include the caller's lease token/value and the calls need to detect zero-row updates so stale workers cannot mutate the current owner's claim.

@macroscopeapp macroscopeapp 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.

Effect service conventions: two new modules import a local service module in a way that erases its module namespace. Both diverge from the repo-wide convention (and the sibling imports in the same files) of importing a service module as a namespace and accessing Module.Tag / Module.layer through it.

Posted via Macroscope — Effect Service Conventions

import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";

import { GitHubCli, type GitHubCliError } from "../sourceControl/GitHubCli.ts";

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.

GitHubCli is consumed here as a service tag (yield* GitHubCli, line 201) and its error type is referenced (GitHubCliError, line 78). At a service boundary the module should be imported as a namespace — every other consumer in the repo uses import * as GitHubCli and GitHubCli.GitHubCli. Suggested change:

  • line 13: import * as GitHubCli from "../sourceControl/GitHubCli.ts";
  • line 78: export type GitHubPullRequestProbeError = GitHubCli.GitHubCliError | GitHubPullRequestProbeDecodeError;
  • line 201: const gitHubCli = yield* GitHubCli.GitHubCli;

Posted via Macroscope — Effect Service Conventions

Comment on lines +10 to +12
import {
GitHubWaitpointWorker,
layer as workerLayer,

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.

layer as workerLayer aliases the export and erases the module namespace. The three sibling imports above use import * as ...; import the worker module the same way and reference its members through the namespace:

-import {
-  GitHubWaitpointWorker,
-  layer as workerLayer,
-  threadGatewayLayer,
-} from "./GitHubWaitpointWorker.ts";
+import * as GitHubWaitpointWorker from "./GitHubWaitpointWorker.ts";

Then update the call sites below to GitHubWaitpointWorker.layer (line 25), GitHubWaitpointWorker.threadGatewayLayer (line 28), and GitHubWaitpointWorker.GitHubWaitpointWorker (line 33).

Posted via Macroscope — Effect Service Conventions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add durable local GitHub waitpoints for agent threads

1 participant