Closing a post-merge follow-up PR must not report the issue as abandoned #250
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck#250
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
When an agent-authored pull request is merged and the merge commit's CI goes red, omp-deck keeps the implementer session alive and has it open one follow-up pull request. If a human then closes that follow-up without merging, the deck runs the "scrapped implementation" path on the original issue: a final turn telling the agent to call
forgejo_abandon_implementation, thenhandleAbandoncloses remaining PRs, swaps the progress label back to the planning label and posts an "implementation abandoned" notice.That report is wrong. The planned implementation was merged and stands; only the post-merge CI fix was dismissed.
The same false report is reachable a second way:
forgejo_abandon_implementationstays permitted for the whole life of the session, and the implementer system prompt tells the agent to call it when it hits "CI you cannot fix" — exactly the situation of a post-merge follow-up turn.A third, adjacent defect surfaced while tracing this: the deck adds
implementation.progress_labelwhen an implementation starts (dispatcher.ts:2764) and the only code that ever removes it is the abandon reset (dispatcher.ts:2414). An implementation that ends merged keeps the label forever, and after this change the follow-up close stops clearing it too.Decisions taken for this work:
forgejo_post_mergerecord for another pull request, i.e. it already merged one.forgejo_abandon_implementationis denied once the session has a merged pull request; the prompt instead tells a stuck agent to file a new issue withforgejo_create_issue.Plan
1. Database predicate: has this session already merged a pull request?
In
apps/server/src/db/forgejo-post-merge.ts, add next togetPostMerge:Implementation:
SELECT 1 FROM forgejo_post_merge WHERE repo_key = ? AND session_id = ? AND (? IS NULL OR number != ?) LIMIT 1, bound withrepo.toLowerCase(); returnrow !== undefined/nullin the style the neighbouring functions use (getPostMergeuses.get(...)).No new table, index or migration:
forgejo_post_mergealready storessession_idand is keyed(repo_key, number).This predicate is complete for live sessions, which is the only case that matters: a merged agent PR either produced a
forgejo_post_mergerecord (and then the session stays alive,dispatcher.ts:2457-2486) or the session was archived at merge time — and the scrap path already skips archived sessions (dispatcher.ts:2336).Add a unit test in
apps/server/src/db/forgejo-post-merge.test.tsalongside the existing one: afterinsertPostMerge({repo:"o/r", number:7, sessionId:"s1", …}),hasMergedPull("O/R","s1",null)is true,hasMergedPull("o/r","s1",7)is false,hasMergedPull("o/r","s1",12)is true,hasMergedPull("o/r","s2",null)is false; still true aftersettlePostMerge("o/r",7,"failed").2. Dispatcher: a closed follow-up archives silently instead of scrapping
In
apps/server/src/forgejo/dispatcher.ts,handleLifecycle, inside the unmerged-implementer branch (:2336-2349), after the existing "closed by the agent while abandoning" drop and beforethis.queueScrap(...):Import
hasMergedPullfrom../db/forgejo-post-merge.ts(that import block already exists at:80-87).Nothing is posted and no row is queued, so
queueScrap,buildScrappedTurnandhandleAbandonare untouched for the genuine case (the session's only PR closed unmerged without any merge behind it).3. Dispatcher: one ending for merged implementations that clears the progress label
Add a private method near
archiveAfterIdle(dispatcher.ts:2489):Behaviour, in order:
getSessionTrigger(sessionId)insidetry/catch(same defensive shape ashandleLifecycle:2293-2297). Skip the label work — go straight to archiving — when the lookup throws, the trigger is null,trigger.kind !== "forgejo_implementer", ortrigger.scope.pull === trigger.scope.number(a PR-started session with no linked issue: its "number" is a pull request and carries no implementation label).const api = this.deps.api();— null means Forgejo is not configured; skip to archiving.api.readRepoFile(owner, repo, REPO_CONFIG_PATH)thenparseRepoConfig, exactly ashandleAbandon:2409-2410does (do not reusereadImplementationConfig: it throwsImplementerStartError, which is wrong outside the manual-start path). On a failed read or parse,log.warnwith the reason and skip to archiving.implementation.progressLabel !== null, call the existingthis.swapLabels(api, { owner, repo, number }, { add: null, remove: progressLabel })(:2792;owner/repofromrepo.split("/"),numberfrom the trigger scope). A non-empty return or a thrown error becomes a singlelog.warn; never a comment, never a ticket, never a thrown error.await this.archiveAfterIdle(sessionId, "forgejo_implementer");swapLabelsalready no-ops (no API write) when the label is absent, so repeated or late calls are harmless.4. Dispatcher: route every merged ending through it
Replace
archiveAfterIdlewithfinishMergedImplementationat the four places where a session ends because its implementation merged. Each takes(record.sessionId | mapped, record.repo | row.repo):resolvePostMerge, green verdict::1705.sweepPostMerge, green/no-CI verdict::1792.sweepPostMerge, pending-past-timeout::1795(the merge landed; only CI never reported). Keep the existingthis.ticket(...)call after it unchanged.handleLifecycle, merged pull request that is not monitored: the fall-througharchiveAfterIdleat:2351is shared with the issue-closed and non-implementer paths, so gate it. In theisPullbranch compute, right afterconst merged = …(:2327):declared with the other
let/consts ofhandleLifecycleso it is visible at:2351, where:(
awaitPostMergeCireturning"waiting"already returned earlier, so reaching:2351withmergedImplementationtrue means monitoring was off or no merge sha was found.)Step 2's new branch calls
finishMergedImplementationtoo, so the label is cleared there as well.5. Policy: refuse to abandon an implementation that already merged
In
apps/server/src/bridge/session-policy.ts,forgejoImplementerPolicy.decide, replace the existing check at:545-547with:decide'ssessionIdis a non-nullstring(SessionPolicy,:119), so no null guard is needed. ImporthasMergedPullfrom../db/forgejo-post-merge.ts; the file already importslistForgejoSessionNumbersfrom../db/forgejo-sessions.ts, so synchronous DB access inside the policy is the established pattern.6. Policy: tell a stuck agent what to do instead
In the same file's
systemContext(:589), the abandon bullet is built unconditionally at:600. Make it state-dependent — compute once, next to the existingconst [pr] = …line (:604), which already branches onsessionId === null:When
mergedis true, the bullet inrulesbecomes:Otherwise it stays exactly as today. The denial text of step 5 repeats the instruction, so an agent acting on a stale system prompt is still told where to go.
Critical files & anchors
apps/server/src/forgejo/dispatcher.ts—handleLifecycle(:2286, unmerged-implementer branch:2336, fall-through archive:2351),resolvePostMerge(:1695),sweepPostMerge(:1767),swapLabels(:2792),archiveAfterIdle(:2489),handleAbandon(:2382, the shape to copy for the config read).apps/server/src/db/forgejo-post-merge.ts—getPostMerge(:97) is the query to copy; the module docstring explains who owns the table.apps/server/src/bridge/session-policy.ts—forgejoImplementerPolicy.decide(:538) andsystemContext(:589).apps/server/src/forgejo/dispatcher.test.ts—describe("ForgejoDispatcher post-merge CI")setup/enqueueMerged/enqueueStatus(:3234-3300) anddescribe("ForgejoDispatcher abandoned implementations")setup/enqueueClose(:3497-3539) are the harnesses the new tests extend.apps/server/src/db/migrations/019-forgejo-post-merge.sql— states whyfailedrows are kept, the invariant step 1 relies on.Verification
Run from the repository root.
bun test apps/server/src/db/forgejo-post-merge.test.ts— thehasMergedPullcases from step 1.bun test apps/server/src/forgejo/dispatcher.test.tswith three new tests:await settle(d, enqueueMerged()), then the red-status trigger used by the existing "red post-merge CI resumes the session" test (:3368) soforgejo_post_mergefor #7 isfailed. Then map a follow-up:setForgejoSession(REPO, 12, "impl")and enqueue apull_request/closedrow for number 12 withpull_request.user.login = AGENT,merged: false,sender.login = "alice". Expect:rows("implementation_scrapped")is[],h.api.postedis[],h.archivedcontains"impl", and the closing row's reason isarchived: follow-up closed without merge.:3570(close of #7 byalicewith no merge behind it) must still queue the scrapped turn and produce the abandon notice. No change expected; it is the regression guard for step 2's placement.h.api.repoFile = ok(CONFIG)(theimplementation.progress_label: Agent/In Progressconfig string from the abandoned-implementations describe,:3494) and seedh.api.issues.set(4, { title: "Do it", state: "closed", isPull: false, author: "alice", labels: [{ id: 2, name: "Agent/In Progress" }] }). Drive merge + green status so the session archives; expecth.api.labelPutsto contain{ number: 4, ids: [] }andh.archivedto contain"impl". The existing post-merge tests do not setrepoFileto an implementation config, so they must keep passing with the label step logging and skipping — check the harness's defaultrepoFileand fix the assertions only if the new config read makes an existing expectation (e.g.labelPuts) non-empty.bun test apps/server/src/bridge/session-policy.test.tswith a new test next to "only the session's own issue can be abandoned" (:232): with no post-merge record,forgejo_abandon_implementationon#7is still"allow"; afterinsertPostMerge({ repo: "o/r", number: 12, sessionId: "s1", mergeSha: <40-hex>, baseRef: "main" })the same call denies with a message containingalready mergedandforgejo_create_issue, andpolicySystemContext(IMPL, "s1")no longer containsforgejo_abandon_implementationbut does containforgejo_create_issue.bun test apps/server/srcandbun run typecheckonce at the end.Assumptions & contingencies
h.api.repoFileat its default. If that default is an error result, the new label step logs a warning and archives — existing assertions stay green; if it is a config that does define a progress label, adjust those tests' expectations to include the resultinglabelPutsentry rather than suppressing the label step.dispatcher.ts:1930describes prompt rebuilding on recycle) — unverified for this specific path. If it does not, the behaviour is still correct: the tool denial in step 5 carries the same instruction, and no extra work is warranted.clearProgressLabelcalls the removedthis.swapLabels#256