Evidence — the ticket's reproduction, before and after
Work item: issue #172 · Testing plan row T11 (manual exploratory) · captured 2026-08-07, re-captured the same day after the owner-review rebuild to the
pullRequests[]endpoint model.The ticket's five reproduction steps, driven through a real
Dispatcherwith an injectedFakeTmux(the same seam the integration tests use). The harness is reproduced verbatim at the end of this file rather than checked in as a script — a throwaway that exists to produce one capture belongs with that capture.
--prefixrestores the pre-issue-172 behaviour by rebinding two methods —SessionRegistry.record_owningback to a barefind_by_work_item, andDispatcher._record_pr_bindingto a no-op — so both columns run the same code path everywhere else. No redaction was needed: the output holdsgithub:octo/repo#Nrefs and file names.
Before — the defect
# BEFORE the fix (pre-issue-172 resolver)
## step 2 — a session is registered against the issue
registry: ['github-octo-repo-15.json']
## step 3 — a comment on PR #16, whose description says 'Closes #15'
routed refs: ['github:octo/repo#15', 'github:octo/repo#16']
delivered to: ['github:octo/repo#15']
spawned: []
registry: ['github-octo-repo-15.json']
record.pullRequests: []
## steps 4+5 — the link is removed; another comment on PR #16
routed refs: ['github:octo/repo#16']
delivered to: ['github:octo/repo#15']
spawned: []Two lines carry the whole ticket:
- After step 3,
record.pullRequestsis empty. That is the ticket's own observation — "Note thatsessions/contains no file for the PR" — one representation later: the routing decision was made and discarded. - After step 5,
delivered tohas not grown. The second comment was routed togithub:octo/repo#16, which nothing on disk claims, and dropped. The issue's session is still running and still owns the work. (UnderspawnOnUnmatched: alwaysthe same event would instead have started a duplicate session against the PR.)
After — the PR is on the record, with its own session
# AFTER the fix
## step 2 — a session is registered against the issue
registry: ['github-octo-repo-15.json']
## step 3 — a comment on PR #16, whose description says 'Closes #15'
routed refs: ['github:octo/repo#15', 'github:octo/repo#16']
delivered to: []
spawned: ['github:octo/repo#16']
registry: ['github-octo-repo-15.json']
record.pullRequests: ['github:octo/repo#16']
## steps 4+5 — the link is removed; another comment on PR #16
routed refs: ['github:octo/repo#16']
delivered to: ['github:octo/repo#16']
spawned: ['github:octo/repo#16']The same lines, inverted — and one new behaviour:
- Step 3 records PR #16 on the issue's own record (
record.pullRequests) and, becausesessionPerPrdefaults on, spawns the PR's own session with the comment as its boot prompt (spawned: ['github:octo/repo#16']). The registry still holds exactly one file. - Step 5's
routed refsis identical to the before-run (['github:octo/repo#16']): derivation still fails, exactly as the ticket describes. What changed is that the failure is no longer the end of the line — the second comment is delivered into the PR's recorded session (delivered to: ['github:octo/repo#16']), with no new spawn and no record ever minted for the PR.
The record on disk
{
"workItem": {"ref": "github:octo/repo#15", "provider": "github",
"owner": "octo", "repo": "repo", "number": 15},
"harness": "claude",
"harnessSessionId": "sess-1",
"status": "active",
"tmuxTarget": "loop-github-octo-repo-15",
"pullRequests": [
{
"workItem": {"ref": "github:octo/repo#16", "provider": "github",
"owner": "octo", "repo": "repo", "number": 16},
"harness": "claude",
"harnessSessionId": "<uuid assigned at the endpoint spawn>",
"status": "active",
"tmuxTarget": "loop-github-octo-repo-16",
"recentDeliveries": ["d-1", "d-2"]
}
]
}(Trimmed to the load-bearing fields; timestamps and the run's real uuid omitted.)
The harness
"""The ticket's reproduction, driven through a real Dispatcher.
Usage: repro.py [--prefix] (--prefix restores the pre-issue-172 resolver)
"""
import sys, tempfile, time
from pathlib import Path
sys.path.insert(0, "cli")
sys.path.insert(0, "cli/tests")
from conftest import FakeTmux, StubInteractiveAdapter
from the_loop.control import ControlConfig
from the_loop.sessions import Session, SessionRegistry, WorkItemRef
from the_loop.webhook.dispatcher import Dispatcher, RoutingConfig
from the_loop.webhook.router import RoutedEvent, extract_work_items
PREFIX = "--prefix" in sys.argv
if PREFIX:
SessionRegistry.record_owning = lambda self, ref: self.find_by_work_item(ref)
Dispatcher._record_pr_binding = lambda self, routed, target: None
ISSUE, PR = "github:octo/repo#15", "github:octo/repo#16"
def pr_comment(body, pr_body):
return {
"action": "created",
"repository": {"full_name": "octo/repo"},
"issue": {
"number": 16,
"body": pr_body,
"pull_request": {"html_url": "https://github.com/octo/repo/pull/16"},
},
"comment": {"body": body, "user": {"login": "octocat"}},
"sender": {"login": "octocat"},
}
def routed(body, pr_body, delivery):
payload = pr_comment(body, pr_body)
return RoutedEvent(
event="issue_comment",
action="created",
delivery_id=delivery,
work_items=extract_work_items("issue_comment", payload),
payload=payload,
)
tmp = Path(tempfile.mkdtemp())
local = tmp / "local"
registry = SessionRegistry(local)
tmux = FakeTmux()
dispatcher = Dispatcher(
registry=registry,
adapters={"claude": StubInteractiveAdapter()},
config=RoutingConfig(control=ControlConfig(require_start_command=False)),
tmux_runner=tmux,
)
print(f"# {'BEFORE the fix (pre-issue-172 resolver)' if PREFIX else 'AFTER the fix'}")
print()
print("## step 2 — a session is registered against the issue")
item = WorkItemRef.parse(ISSUE)
registry.register(
Session(
work_item=item,
harness="claude",
harness_session_id="sess-1",
cwd=str(tmp),
tmux_target=f"loop-{item.slug}",
)
)
print("registry:", sorted(p.name for p in local.iterdir()))
print()
print("## step 3 — a comment on PR #16, whose description says 'Closes #15'")
event = routed("please rerun CI", "Closes #15", "d-1")
print("routed refs:", [r.ref for r in event.work_items])
dispatcher.handle(event)
time.sleep(0.5)
print("delivered to:", [ref for ref, _ in tmux.delivers])
print("spawned:", [ref for ref, *_ in tmux.spawns])
print("registry:", sorted(p.name for p in local.iterdir()))
rec = registry.find_by_work_item(ISSUE)
if rec is not None:
print("record.pullRequests:", [pr.work_item.ref for pr in rec.pull_requests])
print()
print("## steps 4+5 — the link is removed; another comment on PR #16")
event = routed("and again please", "", "d-2")
print("routed refs:", [r.ref for r in event.work_items])
dispatcher.handle(event)
time.sleep(0.5)
print("delivered to:", [ref for ref, _ in tmux.delivers])
print("spawned:", [ref for ref, *_ in tmux.spawns])
dispatcher.stop()Run as uv run python repro.py [--prefix] from the repository root. Two log lines about a missing gh binary and a workspace that is not a checkout of octo/repo are filtered from the captures above; both are the fixture's environment, not the behaviour under test.