Gate Antigravity nudges on the invocation counter - #669
Conversation
PreInvocation fires once per turn and the nudges carry no lease, so every turn appended another persistent injectSteps userMessage. The vendor's invocationNum is the only payload field that separates the callbacks.
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
|
|
There was a problem hiding this comment.
Pull request overview
This PR prevents Antigravity’s repeating PreInvocation hook from re-injecting persistent “nudge” userMessage steps on every turn by gating nudge emission on the vendor-provided invocationNum counter (emit only on the first invocation; fail open when the counter is absent/non-numeric).
Changes:
- Gate Antigravity work-items + harness nudges behind
IsFirstInvocation(payload)so only the first callback in a conversation emits nudges. - Add
IsFirstInvocation(JsonObject payload)helper that treats missing/non-numeric (and<= 1) as first invocation. - Add unit tests pinning the first vs later invocation behavior and the fail-open semantics.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| test/Capacitor.Cli.Tests.Unit/Harness/Antigravity/AntigravitySessionStartMemoryTests.cs | Adds tests for the new “first invocation only” nudge gating behavior and fail-open handling. |
| src/Capacitor.Cli/Commands/Harness/AntigravityHookCommand.cs | Implements nudge gating via invocationNum and introduces IsFirstInvocation helper. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /// Whether this PreInvocation is the conversation's first, read from the vendor's own | ||
| /// <c>invocationNum</c> counter — the one payload field that varies between callbacks. An absent | ||
| /// or non-numeric value reads as first: a payload without the counter cannot distinguish | ||
| /// callbacks, and emitting once too often beats never emitting. |
| // Fail-open: a payload that cannot distinguish callbacks must still emit. | ||
| [Test] | ||
| public async Task A_missing_or_non_numeric_counter_reads_as_the_first_invocation() { | ||
| await Assert.That(AntigravityHookCommand.IsFirstInvocation(Payload("{}"))).IsTrue(); | ||
| await Assert.That(AntigravityHookCommand.IsFirstInvocation(Payload("""{"invocationNum":"2"}"""))).IsTrue(); | ||
| await Assert.That(AntigravityHookCommand.IsFirstInvocation(Payload("""{"invocationNum":0}"""))).IsTrue(); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2598b6c782
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var workItemsNudge = IsFirstInvocation(payload) | ||
| ? HarnessNudgeEmitter.Combine( | ||
| WorkItemsNudgeEmitter.Resolve(SessionStartHarness.Antigravity, sessionId, activeProfile?.DisableWorkItemsNudge is true), | ||
| HarnessNudgeEmitter.ResolveFragmentForHook(activeProfile?.DisableHarnessNudge is true, config)) |
There was a problem hiding this comment.
Keep the throttled harness nudge outside the turn gate
In a long-running Antigravity conversation, this gate prevents HarnessNudgeEmitter.ResolveFragmentForHook from ever checking again after turn 1, so an agent installed later—or one missed because the first check fell within the shared six-hour throttle—will not be offered until the user starts another conversation or invokes another surface. Unlike the work-items nudge, the harness nudge is already stateful: TryClaimCheck throttles evaluation for six hours and StampOffered enforces the per-vendor reoffer floor, so it cannot be injected on every turn and should remain outside this first-invocation gate.
Useful? React with 👍 / 👎.
|
Hi @Coldaine , thanks for the PR! May you please sign the CLA and we're happy to accept the pull: https://cla-assistant.io/kurrent-io/kcap-cli?pullRequest=669 |
|
@Coldaine thanks! Do you ming accepting the CLA? |
Closes #668 — Linear id omitted: the issue was filed minutes ago and has not been imported yet.
What & why
Antigravity's
PreInvocationis a per-turn callback (CallbackMayRepeat: true). The memory index is fenced against that by the once-per-conversation lease; the work-items and harness nudges are not — they are resolved at the output layer, after the lease is decided, as pure functions of the session id. Every turn therefore wrote anotherinjectStepsuserMessage, which Antigravity persists for the conversation. This gates both oninvocationNum, the one payload field that separates the callbacks. An absent or non-numeric counter reads as first, so a payload change degrades to emitting rather than to silence.Where to look
The gate lives at the hook's output site, not inside the emitters, so the lease lanes keep their current acquire/complete/retry behaviour untouched.
Kiro, OpenCode and Pi also declare
CallbackMayRepeat: trueand resolve the nudge unconditionally. They are left alone here — each would need its own counter or marker — and #668 records that.Verification
Same conversation id, three invocations, before and after (stdout bytes):
AntigravitySessionStartMemoryTests: 22/22 pass. Release publish is clean of IL2026/IL3050.