feat: opt in to chat reuse by idempotency-key#24
Conversation
|
/coder-agents-review |
1 similar comment
|
/coder-agents-review |
There was a problem hiding this comment.
First-pass review (Netero). 2 P2, 2 P3, 1 Nit. Findings below are mechanical checks from the first-pass reviewer. The full review panel has not yet reviewed this PR; it will run after these are addressed.
The idempotency feature is well-structured: clean separation between lookup, label construction, and follow-up; solid test coverage for the integration paths; and a clear README section documenting the contract and known limitations.
"If two workflow runs trigger at the same time with the same idempotency-label-key, both can pass the lookup before either creates its chat, and both will create." Netero liked that the race is documented and the warning message names the duplicates.
🤖 This review was automatically generated with Coder Agents.
There was a problem hiding this comment.
Round 2 is blocked. All 5 findings from the first-pass review (Round 1) are silent: no code changes, no author responses.
Unaddressed findings:
- DEREM-1 (P2)
src/action.ts:397:sanitizeLabelKeyhas no direct unit tests for its four documented edge cases. - DEREM-2 (P2)
src/action.ts:379:buildIdempotencyLabelsallows the sanitized key to collide with reserved label keys (gh-target,coder-agent-chat-action). - DEREM-3 (P3)
src/action.ts:362: Warning message includes the reused chat in the "ignoring" list. - DEREM-4 (P3)
src/action.ts:268: Idempotency-match follow-up path duplicates existing-chat-id path. - DEREM-5 (Nit)
src/test-helpers.ts:114: Mock uses inline type instead of exportedListChatsOptions.
Further review is blocked until the author pushes fixes or responds to the findings. The full review panel will run after these are addressed.
🤖 This review was automatically generated with Coder Agents.
afb1f70 to
a3f39d1
Compare
c48ad7c to
d220aaa
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Panel review (18 reviewers). All 5 R1 findings verified fixed. 3 P2, 10 P3, 1 Note, 3 Nit new.
The DEREM-4 fix (sendFollowUpMessage extraction) is clean, and the sanitizeLabelKey unit tests are thorough. The collision guard, the warning message regression test, and the ListChatsOptions mock fix are all solid.
Three P2 findings stand out. The highest-convergence finding (11/18 reviewers) is the stale outputs on the idempotency-match path: sendFollowUpMessage unified the message logic but left the getChat re-fetch only in the existing-chat-id caller. The cross-issue contamination finding (3 reviewers) traces a scenario where a static idempotency-label-key routes Issue #2's follow-up into Issue #1's chat. The collision check bypass (3 reviewers) notes that DEREM-2's fix only covers the create path; the match path never reaches buildIdempotencyLabels.
"Texture Surprise: sendFollowUpMessage looks like the complete follow-up abstraction, but it only handles half the contract." (Hisoka)
src/comment.ts:53
P3 [DEREM-18] deriveCommentKey returns inputs.idempotencyLabelKey verbatim (line 53). buildCommentMarker wraps it in <!-- coder-agent-chat-action:KEY -->. If the key contains -->, the HTML comment boundary breaks and everything after renders as visible Markdown. Attack chain: a workflow sets idempotency-label-key: ${{ github.event.label.name }}, an attacker creates a label with --> in it, the action fails, and the failure comment renders attacker-controlled Markdown (phishing links). GitHub sanitizes <script> but renders Markdown links as clickable.
The code is pre-existing, but this PR makes idempotencyLabelKey a documented first-class input. Fix: use sanitizeLabelKey(inputs.idempotencyLabelKey) instead of the raw value, since the sanitized output contains only [a-z0-9._/-]. (Kurapika)
🤖
src/action.ts:365
P3 [DEREM-19] listChats errors in findIdempotentMatch surface as generic "Coder API error: Bad Request" with no operation context. The error propagates through classifyError which discards the response body. The workflow log gives no indication it was the idempotency label lookup, what label was used, or what the API said. The listChats call with a label filter is the most likely new source of 400s in this PR (new query parameter, experimental endpoint).
Wrap the call so the error names the operation and the label:
try {
chats = await this.coder.listChats({ label });
} catch (err) {
throw new Error(
`Failed to look up chats by idempotency label "${label}": ${err instanceof Error ? err.message : err}`,
{ cause: err },
);
}(Leorio)
🤖
🤖 This review was automatically generated with Coder Agents.
|
In reply to DEREM-18 (P3,
This touches Added a regression test in In reply to DEREM-19 (P3,
|
3b4551b to
78e4e15
Compare
46449e2 to
2cb4b6e
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Verification round (6 reviewers). All 16 R3 findings verified fixed. 4 P3 new, no P2.
The R3 P2 fixes are solid: the getChat re-fetch on the idempotency path mirrors the existing-chat-id pattern, the multi-label AND scoping eliminates cross-issue contamination, and the collision check hoist covers both paths. The HTML injection fix in deriveCommentKey is correct and tested.
The fix commit (819b8bb) includes structural refactoring beyond the 20 findings (outputs.ts extraction, ClassifiedActionError, FailureDetail discriminated union, classifyError restructuring). Each change is individually clean, but reviewers scoping to the findings would miss it. Worth noting for human review.
"Texture Surprise: sendFollowUpMessage looks like the complete follow-up abstraction, but it only handles half the contract." (Hisoka, R3) The stale-outputs half is now fixed; the verb half has one remaining caller (DEREM-27).
🤖 This review was automatically generated with Coder Agents.
9dee1e9 to
db775d7
Compare
dea9ebf to
69383ef
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
All 31 prior findings resolved (26 author-fixed, 5 dropped). 1 minor P3 test gap remaining (DEREM-32). 144/144 tests pass. CI green.
5 rounds, 3 panel reviews (18 + 6 + 3 reviewers). The three R3 P2 findings (stale outputs, cross-issue contamination, unreachable collision check) are correctly fixed and verified. The R4 P3 findings (verb propagation, predicate tightening, fallback test, verb test) are addressed.
Notable quality in the fix cycle: multi-label AND scoping for cross-issue isolation, defense-in-depth on the collision guard, getChat refresh fallback that degrades to stale data rather than failing the action, and the tightened comment predicate regex.
One item for human review: the fix commit (819b8bb) includes structural refactoring beyond the findings (outputs.ts extraction, ClassifiedActionError, FailureDetail discriminated union, classifyError restructuring). Each change is clean; the scope was undisclosed.
src/action.test.ts:477
P3 [DEREM-32] The "sends message to existing chat" test mocks createComment (line 486) but never asserts the body. This PR changed the existing-chat-id path to pass "reused" to sendFollowUpMessage (DEREM-27 fix). If someone removes that argument, the test passes silently and the comment reverts to "Agent chat created:". The idempotency-match test at line 1311 asserts /^Agent chat reused: / for its path, but the existing-chat-id path has no body assertion.
Fix: add expect(commentCall?.body).toMatch(/^Agent chat reused: /) after the existing assertions. (Bisky)
🤖
🤖 This review was automatically generated with Coder Agents.
|
In reply to DEREM-32 (P3, Added
|
db775d7 to
02bdec9
Compare
2e401ea to
943d7fb
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
All 32 findings resolved. Zero open items. CI green.
DEREM-32 (the last open P3, existing-chat-id verb assertion) fixed in commit 943d7fb. The test now asserts /^Agent chat reused: / on the comment body for the existing-chat-id path, matching the idempotency path's assertion.
6 rounds, 32 findings tracked, 27 author-fixed, 5 dropped. Clean.
🤖 This review was automatically generated with Coder Agents.
|
/coder-agents-review |
24839c3 to
80bad02
Compare
There was a problem hiding this comment.
All 32 findings resolved. Zero open items. 145/145 tests pass. CI green.
Delta since R6: one comment-only cleanup commit (9613bb30, "trim verbose code comments"). No behavior change. Netero confirmed all 8 sections clean.
7 rounds, 32 findings tracked, 27 author-fixed, 5 dropped. Ship it.
🤖 This review was automatically generated with Coder Agents.
c578040 to
273a767
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Verification of per-user scoping delta (4 reviewers). 3 P3 new, no P2. Prior 32 findings remain resolved.
The new commits add per-user idempotency scoping (coder-agent-chat-action-user:<userId> label), rename idempotency-label-key to idempotency-key, and add getCoderUserByUsername. The core logic is sound: the user ID is resolved early, threaded into both the lookup and the label builder, and tested via a cross-user hijack scenario. 154/154 tests pass.
Three test coverage gaps and one operational signal issue noted below.
src/action.test.ts:169
P3 [DEREM-34] No test pins the predicate match for an existing "Agent chat reused:" comment. The "updates existing Agent chat comment" test (line 169) only matches "Agent chat: old-url". If the optional group ( reused)? were removed from the regex during a refactor, a second reuse run would miss the first reuse comment and create a duplicate. No test catches it.
Sketch: set up an existing comment with body "Agent chat reused: old-url", call commentOnIssue with verb="reused", assert updateComment is called on that comment. (Bisky)
🤖
🤖 This review was automatically generated with Coder Agents.
|
In reply to DEREM-34 (P3, Added a
|
3025c30 to
de8e57f
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
All 35 findings resolved (30 author-fixed, 5 dropped). Zero open items. CI green.
DEREM-33 through DEREM-35 (R8 P3s) verified fixed: per-user scoping tested via both identity resolution paths, "Agent chat reused:" predicate match pinned, and getCoderUserByUsername 404 now maps to user_not_found.
9 rounds, 35 findings tracked. Ship it.
🤖 This review was automatically generated with Coder Agents.
de8e57f to
1023e6e
Compare
|
/coder-agents-review |
b043119 to
878dc61
Compare
1023e6e to
d8d3105
Compare
…r user When the `idempotency-key` input is set, the action lists chats filtered by labels matching the sanitized key, `gh-target`, and the resolved Coder user UUID. If a non-archived match exists, the action sends a follow-up message via the chat API instead of creating a duplicate. The chat is created with the same four labels so subsequent runs converge. The per-user scope (`coder-agent-chat-action-user: <uuid>`) prevents a malicious or accidental shared `idempotency-key` on the same `gh-target` from letting one Coder user hijack another's chat. The `coder-username` input path now eagerly fetches the user via `getCoderUserByUsername` so `user.id` is available; the helper re-throws 404 with `kind: "user_not_found"` so a typo routes through the helpful failure-comment path. The input is sanitized into a chat-label key (regex `^[a-zA-Z0-9][a-zA-Z0-9._/-]*$`, max 64 bytes) via a new `sanitize-label-key` module. Sanitized keys that collide with the reserved set (`coder-agent-chat-action`, `gh-target`, `coder-agent-chat-action-user`) are rejected upstream of any API call. `ListChatsOptions` adds `label` (string or string[]) and `archived` options to the client. Multiple `label` params are ANDed by the chats API. The lookup passes `?q=archived:false` explicitly to pin the contract; client-side double-filters archived for belt-and-braces. Reuse path uses S8's marker-based `commentOnIssue` so the comment body renders with the new `**Coder Agent Chat: message sent**` heading and updates the same marker as create-path comments. Failed lookups propagate with operation context; concurrent triggers can race, the picked-most-recent chat is logged via `core.warning`, and the README documents the known v0 race.
d8d3105 to
c981b5a
Compare
Adds opt-in idempotency by chat label. When
idempotency-label-keyis set, the action lists chats filtered by the sanitized label scoped to the GitHub target and reuses the most recent non-archived match (sending a follow-up viacreateChatMessage) instead of creating a duplicate. Default behavior (key unset) is unchanged: always create, nolistChatscall.What changed
src/coder-client.ts:listChatstakes optional{ label, archived }and emits one repeated?label=k:vquery parameter per entry plus?q=archived:falsewhen requested.CreateChatRequestgains an optionallabelsmap.src/action.ts: whenidempotency-label-keyis set andexisting-chat-idis not, look up chats by<sanitized-key>:trueANDed withgh-target:<owner>/<repo>#<n>, sort byupdated_atdesc, send a follow-up to the most recent match (re-fetching afterward so outputs reflect post-message state), andcore.warninglisting duplicates on multi-match. On lookup miss (or all-archived), create a new chat carryingcoder-agent-chat-action: "true",gh-target: <owner>/<repo>#<n>, and<sanitized-key>: "true".src/sanitize-label-key.ts(new):sanitizeLabelKeyenforces the chats API's chat-label regex ([a-z0-9._/-], 64-byte cap) on the user input.RESERVED_LABEL_KEYSare checked upstream of the lookup so anidempotency-label-key: "gh-target"is rejected before any API call.src/comment.ts:commentOnIssueaccepts averbparameter ("created"default,"reused"for the auto-reuse path);deriveCommentKeyruns the user-provided key throughsanitizeLabelKeyso it cannot break the<!-- coder-agent-chat-action:KEY -->HTML-comment boundary.src/test-helpers.ts:MockCoderClient.listChatsaccepts the typedListChatsOptionsargument.src/action.test.ts,src/coder-client.test.ts,src/comment.test.ts,src/sanitize-label-key.test.ts: cover unset, no-match, single match, race, archived-only, existing-chat-id wins, lookup-throws, reserved-key collision, the multi-label?label=URL contract, the marker-injection regression, and the four documentedsanitizeLabelKeyedge cases.README.md: new "Idempotency by label" section covering the label set, sanitization rule (with an example), lookup query, race limitation, and the futureIdempotency-Keyheader path.dist/index.jsrebuilt.Closes CODAGT-289
🤖 Authored by Coder Agents.