Repository navigation
feat(core): hand a long Code Mode build to a durable job that reaches its turn as it ends - #409
Merged
Merged
Conversation
… its turn as it ends
…, and free dead jobs' places
nickruigrok
force-pushed
the
feat/code-mode-build-jobs
branch
from
October 11, 2026 15:34
6c44d58 to
382d9d8
Compare
…o its App's builders
…failed engine status
nickruigrok
force-pushed
the
feat/code-mode-build-jobs
branch
from
October 11, 2026 15:44
32712ff to
70bc7b1
Compare
nickruigrok
marked this pull request as ready for review
October 11, 2026 15:44
Contributor
Author
|
@greptileai review |
|
Contributor
Author
|
@greptileai review |
nickruigrok
added a commit
that referenced
this pull request
Oct 11, 2026
…bs of a chat being deleted (#411) ## What and why GRA-443: follow-ups from the post-merge audit of #409 (Code Mode build jobs). 1. **Medium: job notices never left the prompt.** A notice that a build job ended was stored as a system message. `recentHistory` sends every system message and `measureTurns` subtracts them all, so after a few hundred notices every question failed with `agent.question_too_long`, and old notices pointed at pruned jobs. - Notices now stay the turn's alone. The Workspace DO still queues them for the submitting turn, and Pi carries them in that turn's context. - The turn's `keep` passes over them by content, so they are never written to `chat_messages` and leave the prompt with their turn. - Nothing visible is lost, since the person never sees system messages. A later turn reads how a job ended with `env.build.job`. 2. **Low: deletion race.** `deleteChat` now cancels the chat's jobs and terminates their instances right after marking the chat deleting, before it awaits connect. `admitFromBuildJob` also refuses any job of a chat in `#deleting`. If the delete then fails, the jobs stay cancelled. 3. **Low: "Submit it again" after a commit.** A job that broke off now reconciles its receipt first, whether its instance went (`failDeadJobs`) or `finish` ran after a step failed for good. It then ends with `brokenOff(version)`: "ended unexpectedly after committing version N, which is proposed for review. Don't propose it again". Only a job that committed nothing is told to submit again. The receipt lookup is shared in the new `build-job-receipts.ts`. 4. **Revocation scope (decided, recorded on GRA-443 and in `build-jobs.ts`):** any revocation of the agent's permissions still ends a job. A revocation narrows the agent's authority; a job mustn't outlive it on a guess at which grants it rests on, and a job is cheap to submit again. 5. **Nits** - `admitJob`'s doc now says its own check runs first and last. - `buildJobInputSchema`, `admitJob` and `maxRunningJobs` are no longer exported. - `reconcileReceipt` now reads plainly. - The submitting run's ID is now recorded in each job call's audit detail. - `codeRunRetention()` stays: workerd exposes every public method of a Durable Object, it returns only counts, and the tests need the object's own private state. ## How it was tested `apps/core/test/build-jobs.test.ts` now has 21 tests, in workerd against the real engine, DO, model loop and D1. **Changed test** - **Outlives its run:** the notice reaches the submitting turn's last request. It is not stored in the transcript, and a later turn's request doesn't carry it. **New tests** - **Chat deleted mid-commit:** a propose job is held in its tree write while `deleteChat` waits on connect. The job ends `cancelled` and no version is created. - **Broke off after its commit:** the instance is terminated after the commit landed, before its receipt was recorded. `job()` then reports `failed` with `version: 1` and an error that names the commit, not "Submit it again". - **DO restart mid-job (test gap):** the Workspace object is aborted while the job checks. The job completes with version 1 and no notice is stored. - **Revocation landing in finish for a check job (test gap):** the job ends `revoked` with no result. Role loss in finish for a check job was already covered. **Mutation checks.** Each was applied on its own and reverted: | Mutation | Test that fails | | --- | --- | | Store notices | Outlives its run | | Remove both deletion guards | Chat deleted mid-commit | | `brokenOff(null)` after a commit | Broke off after its commit | | A wake cancels running jobs | DO restart mid-job | | No admission in finish | Revocation in finish | Removing either deletion guard alone keeps that test green, because the other guard still refuses: the two are deliberate defence in depth. **Regression:** `build-jobs.test.ts`, `agent.test.ts`, `chats.test.ts` and `agent-builds.test.ts` pass: 162 tests, 3 workers. `vp check` and `vp run check:migrations` pass. ## Checklist - [x] `vp check` and targeted `vp test` pass - [x] Schema changes are expand-only (none) - [x] New external calls go through connect; new model calls through the model gateway (none added) - [x] No secrets, client names or client configuration
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
GRA-429, split from GRA-349. A Code Mode build that takes longer than its run's 30 s budget now returns a durable job ID. The job keeps going after the transient Worker and its lease end, and how it ended reaches the turn that submitted it, never another turn and never a closed one.
Security-relevant (leases and authority): needs a person's review before merge. The threat model is written out in
build-jobs.tsand in the design comment on GRA-429.Mechanism: no new framework. The job is a second internal workflow (
build) on the existing engine and dispatcher, next to uploadextraction. Its record is a newchat_build_jobstable in the Workspace DO, next tochat_code_runs. The migration only adds that table.API
env.build.submit(app, { kind: "check" } | { kind: "propose", message })returns{ job, status, revision }.checkorproposedoes (takeCheck). It settles that check against the turn's counter when it ends, and its result then carries the realfailedInARow. So jobs can't get the agent pastmaxFailedChecks. If the turn has already ended when the job finishes, nothing is settled: the counter belonged to that turn, and the next question resets it anyway.runningrecord whose instance is errored, terminated, or complete without its last step, or that has no instance a minute after it was submitted, is failed with a generic error. Bothsubmitandjobdo this, which frees its place and settles its check. A failed status read counts as unknown, not dead, so one transient engine error doesn't fail the call.env.build.job(id)returns the job's status, its result (whatcheckorproposereturns), its receipt (the version proposed) and any error.appFor(..., "builder")). A later execution, with its own new ID, follows the job; the module is never replayed.Completion
getSteeringMessagesandgetFollowUpMessagesdrain that queue between steps, never between a code step and its result.Authority
writeon Apps, the person's membership and builder role, and restricted mode.proposeDraft's admit hook), reads revocations since the grant epoch. Finding one ends the job asrevoked.finishadmits and authorizes a completed job once more: grant, membership, builder role and restricted mode. So a job revoked at the very end, or whose person lost the App meanwhile, hands on no result.submitcan't start the instance, the job stores a genericinternal.unexpectederror; the engine's own message goes only to the log.Idempotency
parent + 1with that tree, proposed from this chat, and takes it as its receipt instead of committing again.proposedBy, and that version is adopted as the receipt only if it carries this job's ID. So a commit of an identical tree from the same chat is never recorded as this job's.finishends only a running job.Out of scope, recorded on the issue
resolveDependenciesstays in-call: its effect is already admitted right before it commits (feat(core): record every Code Mode run, and close its lease on revocation #389).engine.ts.How it was tested
apps/core/test/build-jobs.test.tshas 17 tests, in workerd against the real engine, DO, model loop and D1. Only the model provider is faked. The tests hold a job inside its check step by holding its screen-build cache read onenv.FILES, so each scenario is deterministic:completedwhile the job runs. The job then completes with version 1. The system message lands right after the answer the turn gave meanwhile, and the model's last request carries it.revokedwith no result and no version, and the refusal is audited.cancelled, and nothing is committed.restart({ from: "commit" })after the commit landed gives one version and oneapp.committedevent.revokedwithversion: 1.failedInARow1 to 5. After that, a sixthsubmit(check or propose) and an in-callcheckare all refused withapp.checks_exhausted.agent.jobs_exhausted. Terminating one job's instance behind its record's back fails that job (internal.unexpected), and a fifth submit then starts.failed,role.forbidden,result: null.permission.restricted.job()after losing the App. Once the person no longer builds the App,job()refuses withrole.forbidden, and the job record is kept.proposeDraft's tree write, then the person loses the builder role (or the chat turns restricted). The job fails withrole.forbidden(orpermission.restricted), and no version is created. The commit's admit callback now rechecks the person's build rights and restricted mode, and the draft's revision, right before the commit.app.conflictand no result. A write while a proposal stores its files fails it withapp.conflictand no version.env.build.job(id)from another chat of the same person and agent readsagent.job_not_found.Mutation checks. Each mutation was applied on its own and reverted; each failed the test named:
finishjob()without the builder checkjob()after losing the AppRegression.
agent.test.ts,agent-builds.test.ts,chats.test.ts,agent-instructions.test.tsanduploads.test.tspass: 181 tests, 3 workers.vp checkandvp run check:migrationspass. The branch is rebased on current main.Checklist
vp checkand targetedvp testpass