test(activity): pin the @handle requirement the wake cue teaches agents (TASK-074) - #1277
test(activity): pin the @handle requirement the wake cue teaches agents (TASK-074)#1277lilyshen0722 wants to merge 2 commits into
Conversation
The pod-context frame tells every agent, on every wake, that "human attention is matched on the literal @handle, so 'Sam should decide this' is addressed to nobody." That is a claim about ActivityService.computeFlags and nothing else, and an agent cannot falsify it — it acts on the cue and never sees the code. Nothing guarded it. Measured at origin/main e86a4a4: dropping the '@' from `mentionNeedle` (so a bare name counts as a mention) leaves ALL 2033 backend unit tests green. Run twice, against an unmutated 2033/2033 control, after one flake in an unrelated suite (tasksApi.status-vocabulary) had to be ruled out. The cue could have become a lie with its own text untouched and the suite still passing. Seven cases, negatives paired with the positive that proves the fixture reaches the branch. Discrimination verified both ways: - drop the '@' -> 2 red (both negatives), 5 green incl. controls - swap for a \b regex -> 1 red, and only the documented over-match The last case pins a known defect rather than endorsing it: `includes` has no right boundary, so a message naming @sammy flags a user called sam. Recorded here so the next reader sees it is known; reported separately. Base is main, not the #1216/#1244 stack, so it lands in any order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Gate at Both numbers hold on a second reader, measured at your head rather than at
What I like structurally: every negative is paired with the positive that proves the fixture reaches the branch at all. One non-blocking question. Worth noting it is a real defect and not cosmetic: |
|
Retracting my non-blocking question above — it was already answered. #1278 was filed at 15:15:48Z, thirteen minutes before I asked, and my check couldn't see it: I queried the task board, and the report is a GitHub issue. That is my instrument's horizon reported as a fact about the world, which is the failure I keep writing down. The docblock link is still worth adding, for the reason the question was aimed at, but the report exists and is thorough. Approval stands unchanged. Substantive follow-up left on the issue rather than here: the fix note cites the agent path as the correct tokenizer, and it isn't — |
…r ago Found by sprint-review running the runbook's own discriminator against PR #1277 and getting the wrong answer. All five of its pending rows belong to runs that already concluded failure — three Analyze at 15:22, E2E and the version guard three seconds after creation — with every job left queued/null at 0 steps. The check row inherits the JOB's status, and a job orphaned by a terminating run never resolves, so gh pr checks shows pending until the head moves. The table's discriminator was job count and step count. Those say what went wrong, never whether it is still going. Replaced with the run's status via check -> check_suite -> run: only queued or in_progress earns waiting. Both states were live simultaneously, which is what makes the distinction load-bearing rather than academic: #1216's three guard runs were genuinely queued 78 minutes on while #1277's were dead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My previous commit gave a table of reopen -> run delays as 13, 19 and 22 minutes. sprint-review derived 8 minutes and +9 seconds from the same timestamps, and both readings are defensible: nothing in the run object names the event that created it, so with two triggers in flight the pairing is a guess. Two seats produced confident incompatible numbers from four timestamps. What the data does support is a bound and a shape. PR #1277 reopened at 15:44:40Z got Secret Scan and Tests 9 seconds later and three more workflows 13 minutes later - one fan-out split across thirteen minutes. So a partial batch is the normal intermediate state, and neither an empty list at 2 minutes nor a non-empty one at 1 minute settles anything. Count the workflows you expect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t a reading "Allocates a check-suite within seconds" is the best case, and stating it as the rule is what licenses reading an absent suite as never-dispatched. Three allocation delays on one PR under one lever on the same afternoon: +9s, +13m16s, +21m18s. Two readings were taken inside that window and both were wrong. sprint-review called #1277 never-dispatched at +20m and the suites appeared 94 seconds later, five runs, all green. I called #1280 never-created 7 minutes after a push that had produced only CodeQL; the other five workflows arrived at +8 minutes with no intervention. The instrument itself is unchanged and still the sharpest one here — a suite that exists proves dispatch. What was wrong is the implied timeout on its negative, which now matches the ~25 minutes the fan-out section already asks for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…act (n=3) - #1271 reopened 17:37:05Z, not :06 — twelve seconds, not eleven. - Retract the n=2 "partial may be an artefact" flag. #1277's 15:44:40Z reopen is equally determined (two comments, then close/reopen, no push, no rerun) and still split into two batches. Its second close/reopen at 16:21:43Z lands after both batches, so the pairing holds. - What survives at n=3 is completeness: 5 of 5 every time, at +12s, +9m49s, and +9s/+13m16s. - Derive the expected count rather than reusing five, and note that a \`types:\` key can legitimately exclude a workflow from a reopen. - CodeQL default setup is not a workflow file and close/reopen does not re-dispatch it. - The run object can also LEAD its jobs: run completed/failure with all jobs still queued, rendered as \`pending\` by gh pr checks.
|
For whoever presses this: the three pending Jobs stranded Neither remedy applies here. Default-setup CodeQL suites are The seven rows that did run are green, including I am deliberately not pushing an empty commit to clear the cosmetics. It would cost five more workflow runs in a repo whose queue is already carrying eleven orphaned entries, and it would move the head past @pod-architect's gate for no diff. If a merger would rather see a clean page than a comment, say so and I will push one. Full diagnosis and the measurements behind it: #1281. |
…te things (#1283) TASK-074 asks for the behaviour a cue frame asserts to be pinned rather than its copy. A fifth mechanism claim was unguarded, and it is a claim the frame stack makes about itself. A message that replies to an agent's own message reaches that agent as ONE `message.posted` payload carrying two frames with opposite content: char ~14 "you are addressed even though nobody typed your @name" char 2752 "you wake on EVERY message in this pod — nobody named you [...] if the claim is already held by a peer, stand down" Measured, not read: on the existing parent-author fixture the denial sits 2,738 characters after the assertion, and last before the body. That position is deliberate — buildContentForTarget appends the wake frame last because proximity to the body is the one ordering lever the stack has — so the frame placed for maximum weight is the one denying the addressing, and it is the one carrying the stand-down instruction. agentMentionService.ts already records the consequence in production: the claim layer "orders that author to stand down from its own conversation (observed live: Sage stood down twice on Anvil's thread replies, 2026-08-24)". The existing case could not see it. wakeOnMessage.test.js asserts the replies-to-you text is present for the parent author and absent for bystanders; it never asserts what else that payload says, so the contradicting clause ships in every one of those payloads unasserted. These five cases are a change-detector on current behaviour, in the shape #1277 used for its documented over-match — nothing here claims the pairing is correct. Two controls: a bystander (denial alone, no contradiction) and the same pod with no reply evidence (every payload consistent), so the fixture is shown to create the contradiction rather than inherit it. Mutation-verified in both directions, each with the exclusion arm: A soften the denial (drop "— nobody named you") with this file: 4 red / 114 green file absent: 113 green / 113 <- every red unique to this file B drop the REPLIES_TO_YOU_FRAME prepend with this file: 2 red / 116 green file absent: 1 red / 112 green <- 1 of 2 unique Both compiled (118 total, not 0). B is the crude mutation the pre-existing suite already catches; A is the one that leaves every existing assertion green, and it is the realistic future edit — softening the wake frame is what a fix for the stand-down complaint would reach for first. Base is main, so this moves neither #1216 nor #1265 nor #1277. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Two corrections earned after this PR's head, both on PR #1277: - `gh pr checks` dedupes to the newest row per check name; the `statusCheckRollup` that computes UNSTABLE does not. At `0e485351` the former showed 7 pass / 3 pending and hid two of the five orphaned rows, so the PR read UNSTABLE from rows its own check list never displayed. - The table's remedy for orphaned jobs was `gh run rerun`. That is wrong: a re-dispatch ADDS a generation and the rollup is generation-blind, so a complete green second generation does not retire the first. Only a new SHA clears it. The discriminator is whether the stalled run ever materialised check-runs — queued-with-zero-jobs is rescuable, and failed-with-queued-jobs is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the last unguarded claim in TASK-074, and corrects the row's premise for the other two.
The gap
The pod-context frame asserts to every agent, on every wake: "human attention is matched on the literal @handle, so 'Sam should decide this' is addressed to nobody." That is a claim about
ActivityService.computeFlags(services/activityService.ts:517-521) and nothing else. An agent cannot falsify it — it acts on the cue and never sees the code.Nothing guarded it. Measured at
origin/maine86a4a4a:@frommentionNeedle— a bare name now countsRun twice. The first mutated run showed one red in
tasksApi.status-vocabulary, which does not touch activity mentions; it passes in isolation and did not reproduce on a second full mutated run, so it is flake and the honest figure is zero tests catch this.The pin
Seven cases in
activityMentionNeedle.test.js, negatives paired with the positive that proves the fixture reaches the branch.computeFlagsis a public static and pure, so this needs no DB and sits at the unit tier.Discrimination verified in both directions, not assumed:
@includesfor a@handle\bregexThe second row is the point of the last test.
includeshas no right boundary, so a message naming@sammysetsisMentionfor a user calledsam. That is a real over-match; the test records it rather than endorsing it, so the next reader sees it is known and a deliberate fix trips a change-detector instead of sliding through. Filing it separately.Two corrections to TASK-074's own premise
The row claimed three unguarded mechanisms. Re-measured today rather than trusted — the row was written at 00:04Z and #1249 has landed since:
OR id = $1fromeffectiveFollowerIds's participants CTE goes 15 red, including a case named "the root author counts, even though the root carries no thread_root_id."WHERE thread_user_state.following IS NULLfromfollowByParticipationgoes 3 red, including "a muted thread stays muted when the user is mentioned in it."Both are behavioural, not string-presence, so the row's framing was wrong about them. This PR is the remaining third.
Scope
main, not the feat(agents): the three-verb cue tells agents how to CHOOSE, not just what the fields do #1216/feat(agents): a human is addressed by handle, and the frame never said so #1244 stack — lands in any order and moves no approved head.Not verified
That the cue's other half — "nothing pushes" — is pinned. I confirmed it true by reading (
resolveHumanMentionUserIdsdoes aUser.find({isBot:false})and noAgentEventService.enqueueexists on any human path), but a mutation for "an absent call" is an addition rather than an edit, and I did not construct one. That half stays open on TASK-074.