fix(forgejo): end the CI wait when the session has undelivered input #267

Merged
lucas merged 1 commit from agent/issue-264-ci-wait-interrupt into main 2026-11-12 20:06:55 +01:00
Collaborator

Closes: #264

What changed

forgejo_wait_commit_status no longer holds a session hostage for the whole CI run. The wait stays blocking (this is not a park-and-resume redesign), but it gains an interrupt predicate:

  • Trigger-row source — countPendingForgejoTriggersFor(repo, numbers, exceptAuthor) counts pending rows on the artifacts of this session (listForgejoSessionNumbers) that the agent did not author. The author filter is load-bearing: an implementer's own push leaves a synchronized row behind, and without it every call after a push would return instantly.
  • Queued-message source — CustomToolContext.hasQueuedMessages(), forwarded through ForgejoCallContext. This is the only live source for an interactive cockpit session, which has no forgejo_sessions mapping.

A new in-process hint bus (forgejo/trigger-events.ts, modelled on ci-status-events.ts) is fired from ForgejoDispatcher.pump() — the single funnel every insert path reaches — and wakes the sleeping wait, so latency is milliseconds instead of the 60 s fallback poll. The per-iteration predicate catches anything the bus missed.

On interrupt the tool returns an error result (so a re-call is discouraged) whose text leads with the reason and still carries the full check report, plus details.interrupted for the tool card. done wins over interrupted for an all-terminal commit; a commit with no checks is interrupted immediately instead of sitting out the 30 s empty grace.

Session identity reaches the tool through an explicit sessionIdRef threaded from createSession / resumeSession into attach() (not ctx.sessionManager.getSessionId(), which is a different id), following the existing filterRef / uiBridgeRef pattern.

The implementer prompt bullet, the tool description and docs/configuration.md were reworded accordingly.

Verification

  • bun test apps/server/src/forgejo/tools/ci.test.ts — three new loop cases: interrupt mid-wait carries its reason at waitedMs: 60_000; all-terminal checks still report done under an always-firing predicate; a commit with no checks is interrupted at waitedMs: 0 without any wake.
  • bun test apps/server/src/db/forgejo-triggers.test.ts — the count query excludes other repos, unlisted numbers, the agent's own rows (mixed casing on both repo and comment_author) and non-pending rows; an empty number list returns 0.
  • End-to-end (throwaway, deleted): with a pending Acme/Widgets#7 row by alice and a session mapped to acme/widgets#7, the built tool returned in 3 ms (not 600 s) with isError: true, text error: ended early: 1 unhandled Forgejo event(s) … plus the check report, and details.interrupted === true. Flipping the author to the agent's own login made the same call block until its 1 s timeout — own events do not self-interrupt.
  • bun test — 1661 pass, 0 fail. bun run typecheck clean.
Closes: #264 ## What changed `forgejo_wait_commit_status` no longer holds a session hostage for the whole CI run. The wait stays blocking (this is not a park-and-resume redesign), but it gains an interrupt predicate: - **Trigger-row source** — `countPendingForgejoTriggersFor(repo, numbers, exceptAuthor)` counts `pending` rows on the artifacts of this session (`listForgejoSessionNumbers`) that the agent did not author. The author filter is load-bearing: an implementer's own push leaves a `synchronized` row behind, and without it every call after a push would return instantly. - **Queued-message source** — `CustomToolContext.hasQueuedMessages()`, forwarded through `ForgejoCallContext`. This is the only live source for an interactive cockpit session, which has no `forgejo_sessions` mapping. A new in-process hint bus (`forgejo/trigger-events.ts`, modelled on `ci-status-events.ts`) is fired from `ForgejoDispatcher.pump()` — the single funnel every insert path reaches — and wakes the sleeping wait, so latency is milliseconds instead of the 60 s fallback poll. The per-iteration predicate catches anything the bus missed. On interrupt the tool returns an **error** result (so a re-call is discouraged) whose text leads with the reason and still carries the full check report, plus `details.interrupted` for the tool card. `done` wins over `interrupted` for an all-terminal commit; a commit with no checks is interrupted immediately instead of sitting out the 30 s empty grace. Session identity reaches the tool through an explicit `sessionIdRef` threaded from `createSession` / `resumeSession` into `attach()` (not `ctx.sessionManager.getSessionId()`, which is a different id), following the existing `filterRef` / `uiBridgeRef` pattern. The implementer prompt bullet, the tool description and `docs/configuration.md` were reworded accordingly. ## Verification - `bun test apps/server/src/forgejo/tools/ci.test.ts` — three new loop cases: interrupt mid-wait carries its reason at `waitedMs: 60_000`; all-terminal checks still report `done` under an always-firing predicate; a commit with no checks is interrupted at `waitedMs: 0` without any wake. - `bun test apps/server/src/db/forgejo-triggers.test.ts` — the count query excludes other repos, unlisted numbers, the agent's own rows (mixed casing on both `repo` and `comment_author`) and non-`pending` rows; an empty number list returns 0. - End-to-end (throwaway, deleted): with a `pending` `Acme/Widgets#7` row by `alice` and a session mapped to `acme/widgets#7`, the built tool returned in **3 ms** (not 600 s) with `isError: true`, text `error: ended early: 1 unhandled Forgejo event(s) …` plus the check report, and `details.interrupted === true`. Flipping the author to the agent's own login made the same call block until its 1 s timeout — own events do not self-interrupt. - `bun test` — 1661 pass, 0 fail. `bun run typecheck` clean.
A comment, review or queued message arriving while
`forgejo_wait_commit_status` blocks could not be delivered: the
dispatcher only advances an artifact's queue once the turn ends, so the
event waited out the whole CI run (up to an hour).

The wait now carries an interrupt predicate, checked each iteration and
woken within milliseconds by a new in-process bus the dispatcher fires
whenever pending rows exist. It ends as soon as the session's own issue
or pull request has a pending trigger from someone other than the agent
— the author filter keeps the agent's own push from self-interrupting —
or the SDK reports a queued message. The result is an error carrying the
check report, telling the agent to end its turn and re-issue the wait,
so the pending event becomes the next turn. An all-terminal commit still
reports `done`; the empty-checks grace no longer holds back input.

Co-Authored-By: Claude Opus 5 via omp-deck
lucas approved these changes 2026-11-12 20:06:51 +01:00
lucas merged commit 26c2fc02eb into main 2026-11-12 20:06:55 +01:00
lucas deleted branch agent/issue-264-ci-wait-interrupt 2026-11-12 20:06:56 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/omp-deck!267
No description provided.