Skip to content

feat(worker): retain raw agent output in R2 with a per-attempt cap - #142

Merged
kixelated merged 5 commits into
mainfrom
quest/m0/kix/output
Oct 19, 2026
Merged

kixelated merged 5 commits into
mainfrom
quest/m0/kix/output

Conversation

@kixelated

@kixelated kixelated commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Completes quest/m0/kix/output.md (Retain raw agent output in R2).

Problem

#135 stored raw agent output as text in D1 job_output rows, with no total cap and no retention. Launch would ship unbounded transcripts in D1.

Behavior

  • R2 storage. Each chunk is one object at output/<job>/<attempt>/<seq> in the quest-output bucket (binding OUTPUT). D1 keeps one small index row per chunk (hash, complete flag, upload time) and one totals row per attempt (job_output_attempts). Migration 0008_output.sql drops the old table; nothing is deployed, so there is no data copy.
  • Contiguous seq. An attempt accepts only its next seq, starting at 0, or an identical retry of a chunk still indexed. Each chunk is reserved first: its index row and hash are inserted in a D1 batch, only while the attempt is current. A conflicting chunk is refused before anything reaches R2. A chunk is reserved only once the one before it is stored, so an attempt's one pending chunk is its last. Then the object is written and the row is marked complete with the upload time, only while the attempt is still current. A retry that matches a completed row writes nothing, so the object's lifecycle never restarts. A replay of an expired seq is refused.
  • Cap. 64 MiB of UTF-8 and 4096 chunks per attempt. The first chunk over either cap sets truncated once, and it and every later chunk get { "type": "reply", "id": …, "truncated": true } instead of an error. The job and its summaries are unaffected. Expiry never gives capacity back, because the totals row lives as long as the job.
  • Reads. GET …/jobs/<job>/output still pages by (attempt, seq) behind the per-request maintainer check. It lists only stored chunks, so a cursor never passes a chunk that may still land, skips rows whose object is already gone, and adds truncated: [attempts].
  • Retention. A bucket lifecycle rule expires output/ after 30 days (documented, since lifecycle is bucket config). A daily cron (17 4 * * *) deletes index rows older than 30 days and retries pending cleanups. The Worker's default export is now { fetch, scheduled }, and the Hono app is the named export app, which the tests import.
  • Disconnect. The batch that deletes the project also inserts one output_cleanup row per job with output. The route then deletes each output/<job>/ prefix and its row, and the cron retries any failures. An upload that races the disconnect finds its row gone after writing, queues its job's cleanup again, and deletes its own object.
  • Docs: cloudflare/README.md gets a new "Raw output" section and the bucket and lifecycle deployment commands. The runner protocol note in quest/m0/kix/runner.md is updated. The finished quest is deleted and its references removed.

Validation

just check and just test pass in nix develop (225 CLI tests, 212 Worker tests). New Worker tests against local R2 cover:

  • in-order acceptance and retries that don't rewrite (same R2 version)
  • conflicting chunks refused before R2
  • another runner refused
  • completing a reservation after a failed R2 put, with retention timed from the upload
  • a pending chunk that fills a page: the cursor stops before it, later chunks wait for it, and a late retry after reassignment is refused
  • the byte and chunk caps, with the job still running
  • expiry through the real scheduled handler, including refused replays and skipped missing objects
  • a disconnect racing an upload, including a failed self-delete that the cron retries
  • cleanup retry after an R2 failure
  • disconnect deleting objects through the real route

The route test also checks that signed-out visitors, other signed-in GitHub users, and revoked maintainers are refused.

Settled decisions

The maintainer accepted each recommendation:

  1. Chunks per attempt. MAX_CHUNKS stays 4096 (cloudflare/src/jobs/output.ts). Reaching 64 MiB needs chunks of at least 16 KiB on average, and an attempt costs at most about 8k R2 Class A operations.
  2. Shape of the truncated reply. A chunk over the cap gets a successful reply with truncated: true, not an error, so a runner never mistakes it for a failure to retry.
  3. Lifetime of the per-attempt totals. job_output_attempts rows live as long as the job and are deleted with it.

Suggested follow-ups

  • Deploy kix.quest: include r2 bucket create quest-output and the 30-day lifecycle rule in the deploy checklist.
  • Jobs removed any way other than a disconnect (e.g. a future account deletion, which cascades through jobs.actor) leave their R2 objects until the lifecycle rule expires them. Route any future job-deletion path through output_cleanup.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 18, 2026 22:09
Store each output chunk as an R2 object at output/<job>/<attempt>/<seq>,
indexed in D1 by a reservation row with the text's hash. Chunks must arrive
with a contiguous seq; identical retries succeed without rewriting, and a
conflicting chunk is refused before it reaches R2. Each attempt keeps at most
64 MiB and 4096 chunks; past either cap chunks get a `truncated` reply and
the job keeps running.

A bucket lifecycle rule expires output after 30 days and a daily cron deletes
the index rows. Disconnecting a project queues its jobs' output for deletion
in the same D1 batch, deletes the prefixes at once, and the cron retries
failures. An upload racing a disconnect deletes its own object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 19, 2026 05:29
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T06:09:27.346998Z 7a179e8 New commits
🔒 Security Review ✅ Completed 2026-10-19T06:12:35.506001Z 7a179e8 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02456795c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/jobs/output.ts Resolved
kixelated added a commit that referenced this pull request Oct 19, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 19, 2026
* docs(quest): add R2 output bucket setup to the kix.quest deploy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(quest): make the deploy's R2 step depend on #142

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A pending chunk could fill a page and advance the cursor past it, so a
reader following `next` missed it once a retry completed it. Reads now list
only stored chunks, a chunk is reserved only after the one before it is
stored, and a pending chunk completes only while its attempt is current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0975be38b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/jobs/output.ts Resolved
Comment thread cloudflare/src/jobs/output.ts Resolved
… self-cleanup

A chunk whose first upload failed kept its reservation time, so the index
could expire before the object the lifecycle rule had just restarted.
Completing a reservation now records the upload time. An upload that races a
disconnect queues its job's prefix for cleanup again before deleting its own
object, so a failed delete is retried by the cron.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary

Raw agent output now lives in R2 (cloudflare/src/jobs/output.ts), indexed in D1 by 0008_output.sql, with a per-attempt cap, retention timed from the successful upload, and cron cleanup for disconnected projects. The quest quest/m0/kix/output.md is removed.

Review: Codex raised three P2 findings over two rounds, all accepted and fixed (b0975be: pagination never passes a chunk that may still land; ef024f4: retention starts at upload time, and a failed self-delete re-queues cleanup). Codex then completed code and security reviews on 7a179e8 with no new findings. The three addressed threads are resolved. Two of the three automated fix rounds were used.

CI is green on 7a179e8, and the branch is current with main. #144 and #145 also add migration 0008 and have not merged, so this PR keeps 0008_output.sql; whichever of them lands next should renumber its migration.

Enabling auto-merge on 7a179e8.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 9b6e30c into main Oct 19, 2026
5 checks passed
kixelated added a commit that referenced this pull request Oct 19, 2026
Brings in raw output retention (#142) and the harness and answer-flow
plans (#147). The inbox migration becomes 0009_inbox.sql after
0008_output.sql. The shared maintainerOf() fixture adopts main's
stricter GitHub mock, where only the signed-in user holds the maintain
role. harness.md and answer-flow.md drop this deleted quest from
Required, and activity.md drops both deleted quests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated deleted the quest/m0/kix/output branch October 21, 2026 21:05
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.

1 participant