Repository navigation
Conversation
Rehearsal R0 (G5) found the job-offer path stranded after a claim. On master, POST /api/job-offers/:id/events moved an offer to any status from any status: an UNCLAIMED offer could be taken delivered -> cancelled -> delivered. recordEvent now refuses an event that would leave the lifecycle, and records nothing for it: - in_progress and pickup need a claimed offer (a repeat is a no-op); - delivered needs a claimed or in-progress offer (a repeat is a no-op and keeps the first delivery time); - cancelled applies only before delivery; - settled, cancelled and expired offers stay as they are. Events that change no status (note, progress_update, ...) are always recorded. A refused event answers 409 invalid_transition with the current status. The courier shim shares the store and gets the same rule. Two existing tests posted progress on an unclaimed offer; they now claim first. Who may post an event is kits #395's (claimant binding); what ends a delivered offer is an open product decision (steward #3968). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…81, kits #3989)
The operator-market owner's rules (kits #3989), encoded in the lifecycle:
- delivered is the claimant's assertion: never terminal, never success;
- the poster's 'confirmed' ends it in 'completed' (new status; success; no
money moves on this path);
- 'disputed' is allowed only within 72h of delivery (else 409
review_window_closed) and is terminal;
- a delivery nobody confirms or disputes within the window lapses. The sweeper
sets 'lapsed' (new status; terminal; never success; reason
delivery_unconfirmed);
- 'settled' comes only from the server (a linked job's settlement); a posted
'settled' is refused;
- 'release' puts a claimed or in-progress offer back to 'open' and clears the
claim. The releasing kernel cannot re-claim that offer for 1h (409
recently_released); other kernels can at once.
Claim refusals the routes do not know now fail closed (500), never answered as
{ ok: true }. The courier shim maps completed to delivered and lapsed to expired
(it used to map unknown statuses to open). Who may post each event stays #395's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (N81, #455 review round 1) astra's review of 10f4545 (SHIP-WITH-FIXES) found five gaps. Each was reproduced first (36 failing tests at f3afbe7, plus the F4 and F5 evidence below), then fixed: - F1: an event named after an inherited property (toString, constructor, __proto__ and others) answered 500. The event table is now a Map, and such an event is recorded as status-neutral. - F2: DELETE cancelled delivered and terminal offers through both routes, and a repeat overwrote cancelledAt. DELETE now follows the 'cancelled' rule after the owner check: - open, claimed or in_progress are cancelled; - a repeat keeps the first time; - anything else answers 409 invalid_transition, and nothing changes; - an unknown refusal is 500, never a success; - the courier shim names the status in its own vocabulary. - F3: a source re-verification could cancel an offer that was claimed or delivered while it awaited. After the await, the answer applies only if the offer is still the same open, unclaimed record with the same source address; otherwise it is discarded with no write. - F4: the lifecycle suite now uses authenticated participants and kernel ownership the way #395 requires. On this branch they are harmless. A trial merge with #395 (dedb7f4) passed 565/565. Its conflict resolution is a patch for whoever merges second: returns/pcc-composition-work/n81-x395-conflict-resolution.patch. - F5: the matrix is complete. It covers 10 statuses x 15 events at the store, the route and the shim, checked against an independent table, plus the 72h window boundaries. Every refusal compares the offer and the log before and after. The docs say what DELETE now does. Tests: 549/549. Mutations: 59/59 killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…k survives a restart, and an unknown courier status is unavailable (N81, #455 review round 2) astra's round-2 review of 8c4b863 (DO-NOT-SHIP) closed F1-F5. Its new findings were each reproduced first: - The HIGH (actor roles) is a MERGE ORDER, not a change here. This branch must land after kits' #395, or with it. At 8c4b863, another authenticated operator could confirm, dispute or release (9 of 12 reproductions fail). A trial merge with #395 @cc66f511 refuses all 12 and passes 617 tests. The conflict patch is kept with the lane. - MEDIUM: `confirmed` ignored the 72h review window that `disputed` keeps. A first judgment of a delivery, either one, is now allowed only inside the window. - MEDIUM: the 1h re-claim block lived in memory, so a restart lost it. The releasing kernel now travels in the persisted 'release' event, and hydration re-arms the block from each offer's latest release (no schema change). - MEDIUM: an unknown status projected to the courier view as 'open'. It now projects as 'expired' (unavailable). - LOW: docs/JOB_OFFERS.md describes the current lifecycle. Tests: the lifecycle, job-offers and courier-jobs suites, 1014/1014. The matrix covers every (status, event, age), including the window's edges. Mutations: 4 of 5 killed. The fifth (the rebuild ignoring a release's age) is equivalent, because claim() re-checks the guard's expiry itself. Full gateway suite: 3928 passed. The one unrelated timeout passes alone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…ant; legacy courier claims fall back to the kernel owner Two findings from astra 142 (#395 @cc66f511), each reproduced there first (scratchpad repro-142b). HIGH: the print-and-mail handoff authorized evidence by comparing the body's driverAgent with the job's public claimedBy label. A stranger could copy that label and submit handoff evidence as the claimant (reproduced: 201 instead of 403). The handoff now needs an authenticated principal (401 missing_identity otherwise). That principal must be the job's claimant: the principal recorded privately at claim time, or, for a pre-binding claim, the owner of the kernel its label names. Anyone else gets 403 not_claimant. driverAgent is still required, but it is a label: a claimant whose driverAgent does not name the claim gets 400 driver_label_mismatch. MEDIUM: the courier shim resolved a claimant only from the private binding. A courier claim made before claimant binding therefore lost every event after a restart, while the generic route fell back to the claiming kernel's owner (reproduced: generic 200, shim 403 for the same driver). The shim now uses the same resolution. The fallback holds only when the legacy driverAgent label names a real kernel with a provable owner, and fails closed otherwise. One resolution is shared by all three surfaces: services/kernel-owner.ts (kernelOwnerFromStore, moved from job-offers.ts, and offerClaimant). courierJobsRoutes and printAndMailRoutes take the same optional kernelOwnerOf override as jobOffersRoutes. Not in this commit (see the triage): 142's HIGH on ASCII-case aliases closes with the comparator swap onto WP-A in the merge after #326, and its HIGH on the N81 lifecycle closes in the merge with #455. Atomic rating uniqueness needs a unique index, a schema change for the operator. Tests: print-and-mail 21/21 (401, stranger-with-label 403, and label-mismatch 400 added; the existing handoffs now authenticate), and courier-jobs 34/34 (the legacy fallback and its fail-closed case added). Full gateway: 189 files, 3021 passed, 6 skipped, plus the known completion-real-tier full-run timeout, which passes alone (3/3). Gateway tsc exits 0. Mutations: 4 of 4 killed (the identity check, the actor requirement, the shim fallback, and a fallback that trusts the label). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015zYzsSSFUHbPV5DNxssX4A
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
astra 142 (#395 @cc66f511) required closure 2: the route's role checks sat on a store that did not implement N81, so a claimant could deliver after a cancel and a release left the offer claimed. #395 now carries #455 @ced9b5c6 (frozen) by agreement with composition (#4976, #4984). The operator merges #395 alone; #455 never merges by itself (astra 157). Merge resolution: composition's patch n81-x395-conflict-resolution-dcfe7b1e (#4992). Only job-offers.test.ts and courier-jobs.test.ts conflicted. They take #395's versions plus the patch's hunks 1-3. Hunk 4 makes the second release in job-offers-lifecycle.test.ts come from the current claimant, because #395 makes release claimant-only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015zYzsSSFUHbPV5DNxssX4A
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…ty (astra 142) astra 142, required closure 2, asked for cancel-then-deliver and release-then-reclaim negatives on the integrated head. Writing them showed that #455's release cleared the claim fields but left the private claimant in the store. claimantOf() still named the released operator, so on the open offer it could post the events the claimant or the poster may post (a progress_update answered 200, recorded as by "claimant"), and the persisted row would restore it after a restart. A release now removes the private claimant too, so the row persists null and a restart brings nothing back. Tests: - after the poster cancels, the claimant's delivered, in_progress and pickup each answer 409 invalid_transition, and the offer stays cancelled; - after a release the offer is open with no claimant. The released operator gets 403 not_claimed for delivered and 403 not_participant for progress_update. Another kernel claims at once, and the former claimant cannot advance that claim; - the store's claimantOf() is null after a release, and still null after a restart over the same database. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015zYzsSSFUHbPV5DNxssX4A
This branch has not been deployed
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.
N81: an offer event keeps the offer inside its lifecycle
The ADK rehearsal R0 found the job-offer path stranded after a claim (G5). On master,
POST /api/job-offers/:id/eventsset any status from any status. Nothing checked where an offer was, so progress could be reported on an offer nobody had claimed, and a delivered offer could be cancelled or re-opened by a later event.JobOffersStore.recordEventnow keeps every offer inside its lifecycle, and records nothing for a refused event:in_progress,pickupdeliveredcancellednote,progress_update,error, ...)So nothing advances an unclaimed offer, and settled, cancelled and expired offers stay as they are. A refused event answers
409 {error: "invalid_transition", event, currentStatus}. The courier shim (/api/courier-jobs/:id/events) uses the same store and gets the same rule.Scope
recordEventand the two event routes' error mapping, not claim, persistence or hydration, so it composes with both.settled) is an open product decision with kits. The job-offer tools in the agent package follow once that settles (adk).Tests
job-offers-lifecycle.test.ts(33): every refused transition (409, status unchanged, nothing recorded), every allowed one, notes on every status, a repeat delivery keeps the first time, and the courier shim.🤖 Generated with Claude Code