Skip to content

Poll for new Fixloop items every five minutes - #6

Draft
eric-ns12 wants to merge 1 commit into
mainfrom
agent/fixloop-five-minute-polling
Draft

Poll for new Fixloop items every five minutes#6
eric-ns12 wants to merge 1 commit into
mainfrom
agent/fixloop-five-minute-polling

Conversation

@eric-ns12

Copy link
Copy Markdown
Contributor

What changed

  • Run /api/process every five minutes instead of every ten.
  • Document the existing processing-event and agent-dispatch behavior.
  • Add a contract test that locks the Vercel cron path and cadence.

Why

New Fixloop reports waited up to ten minutes before routing. The requested cadence is five minutes.

Validation

  • npm run check (23 passed)
  • git diff --check

Closes #1

Reduce the processor schedule from ten minutes to five and lock the cadence with a config contract test.

Co-authored-by: Eric Stark <Eric@nutrasolutions.com>
Signed-off-by: Eric Stark <Eric@nutrasolutions.com>
@eric-ns12

Copy link
Copy Markdown
Contributor Author

Dev rollout: fixloop-dev

  • Vercel deployment dpl_5Yc6UH5rFyvTkmWeUfzjFtxAb3Da: Ready
  • Registered cron: /api/process at */5 * * * *
  • Root smoke test: HTTP 200
  • Manual cron invocation reached /api/process: HTTP 401

The 401 is the intended fail-closed response because the new dev project has no CRON_SECRET, database, GitHub, or agent-dispatch variables. End-to-end report routing remains blocked until dev-only configuration is added.

@eric-ns12

Copy link
Copy Markdown
Contributor Author

Adversarial review at exact ae1d5b71. Ran the suite at that head: 23/23 pass. Verdict: the schedule change is safe, one pre-existing defect is amplified by it, and the invariant that makes it safe is untested.

Why 5 minutes is safe here

Checked the thing that actually breaks when you halve a poll interval, concurrent claims of the same row. It holds:

  • claimNext() uses for update skip locked inside a transaction, so two overlapping invocations claim different rows, never the same one.
  • The claim sets lease_until = now() + interval '10 minutes', and claimNext() skips any row whose lease is live. The lease is twice the new cron period, so a run still in flight cannot have its row re-claimed by the next tick.
  • A timeout between createIssue and finalize does not duplicate: findIssueByMarker(route.repository, marker) finds the existing <!-- fixloop:… --> issue on retry. Idempotent.
  • timingSafeHeader returns false when expected is falsy, so the 401 on unconfigured dev is fail-closed, not a fluke. Correct.

The README's new sentence is accurate against the code: addEvent(client, report.id, "processing", …) fires at api/process.js:38 before routing, then createIssue, then dispatchAgent, which returns false rather than throwing when the webhook is unconfigured.

The invariant is load-bearing and untested

test/schedule.test.js asserts the cron string. Nothing asserts the property the safety argument rests on: cron period < lease interval. Shorten the lease to 3 minutes, or lengthen the cron, and the suite stays green while overlapping claims become possible.

Worth pinning: parse the period out of vercel.json and the interval 'N minutes' out of api/process.js, assert the lease is strictly greater.

Amplified by this change: the reminder path bumps updated_at without reminding

sendReminder() is reached whenever the received queue is empty, which is the normal state.

const dispatched = await dispatchAgent(report, {...}, "reminder");   // false when no webhook configured
await client.query(
  `update fixloop.reports set lease_until = null, updated_at = now(), last_error = null where id = $1`, );
if (dispatched) await addEvent(client, report.id, report.status, "Fix agent reminded");

updated_at = now() is unconditional. dispatchAgent returns false without doing anything when FIXLOOP_AGENT_WEBHOOK_URL or FIXLOOP_AGENT_WEBHOOK_SECRET is unset, which is exactly the fixloop-dev state today.

Failure scenario. No agent webhook configured. A report sits in assigned and goes stale past 12 hours. A cron tick finds nothing to process, claimReminder() picks it, nothing is dispatched, and updated_at is set to now. claimReminder() requires updated_at < now() - interval '12 hours', so that report is invisible for another 12 hours. No event is written, because addEvent is behind if (dispatched). The report's updated_at, which is also the ordering key and a timestamp a human reads as "last activity", advances with no work behind it, and nothing is ever actually reminded.

At */10 the rotation through stale reports ran 6 times an hour; at */5 it runs 12. The defect is pre-existing, the change doubles its rate.

Fix: only clear the lease when nothing was dispatched, and leave updated_at alone.

await client.query(
  dispatched
    ? `update fixloop.reports set lease_until = null, updated_at = now(), last_error = null where id = $1`
    : `update fixloop.reports set lease_until = null where id = $1`,
  [report.id]);

Non-blocking

  • No maxDuration declared for /api/process. The handler chains a DB transaction, an LLM classification, a GitHub search plus create, and an outbound webhook. On the Vercel default a timeout is plausible, and a timeout burns one of MAX_ATTEMPTS = 5 even though the work may have succeeded. Five timeouts mark a healthy report failed. Retry cadence is set by the 10-minute lease, not the cron, so this change does not worsen it, but the budget is worth declaring explicitly.
  • One report per invocation. limit 1 means a backlog of N drains in N × 5 minutes. Halving the interval halves the drain time, which is the point of the issue, but the ceiling is 12 reports an hour.
  • Reminders are starved by processing. claimReminder() only runs when claimNext() returns null, so a continuously non-empty received queue means reminders never fire. Pre-existing.

Blocking on the reminder updated_at bump. Everything else is a note.

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.

[Buzz] Schedule five-minute polling for new items

1 participant