Skip to content

feat(pi): add Antigravity multi-account routing parity - #15

Open
ComputerByte wants to merge 7 commits into
cortexkit:mainfrom
ComputerByte:feat/pi-multi-account-parity
Open

feat(pi): add Antigravity multi-account routing parity#15
ComputerByte wants to merge 7 commits into
cortexkit:mainfrom
ComputerByte:feat/pi-multi-account-parity

Conversation

@ComputerByte

@ComputerByte ComputerByte commented Sep 10, 2026

Copy link
Copy Markdown

Summary

Adds Pi multi-account parity for Antigravity, including durable account pooling, quota-aware routing, failover, operator controls, Pi 0.85.1 compatibility, stable account identity reconciliation, and live route diagnostics.

Verification

  • Build/typecheck/lint/format: all pass
  • 2,279 tests across unit, Pi, and e2e suites
  • 0 failures
  • 6,545 assertions
  • Worktree clean
  • Audited HEAD: 48460e8

Previously identified issues verified fixed

  • ✅ In-flight token rotation / cooldown attribution
  • ✅ Round-robin cursor remapping
  • ✅ Health/token tracker ownership
  • ✅ Stale-manager double-remap
  • ✅ Pi 0.85.1 API compatibility
  • ✅ Canonical /login identity loss / duplicate-account reconciliation
  • ✅ Live routing diagnostic safety

Live validation

Real Pi 0.85.1 multi-account routing was exercised successfully.

With two eligible non-Gemini accounts and round-robin enabled:

[agy-route] agy1 user@gmail.com strategy=round-robin group=non-gemini
[agy-route] agy2 user1@gmail.com strategy=round-robin group=non-gemini
[agy-route] agy1 user@gmail.com strategy=round-robin group=non-gemini
Quota gating was also observed live: requests fail cleanly when every account in the requested quota group is disabled, cooling down, or below the configured cached-quota threshold.
Audit findings
Independent release audit verdict: SAFE TO MERGE
- 0 HIGH findings
- 3 MEDIUM findings — test coverage/design gaps, not reproduced runtime defects
- 3 LOW findings — non-exploitable implementation/testing caveats
Remaining medium items:
- quota-group crossover test does not directly prove isolation
- Claude quota key is model-agnostic by design
- no direct regression test for conflicting accountId identity collision
These were not considered merge blockers.
Post-merge smoke
After merging locally onto upstream main:
- production build passed
- Pi suite: 121/121 passed
- 0 failures

<!-- codesmith:footer -->
---
<a href="https://app.blacksmith.sh/cortexkit/codesmith/antigravity-auth/pr/15"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1791618929&installation_model_id=22398&pr_number=15&repository=cortexkit%2Fantigravity-auth&return_to=https%3A%2F%2Fgithub.com%2Fcortexkit%2Fantigravity-auth%2Fpull%2F15&signature=9688557536cca79ec76f127e4d1ec08e5d967d428a2c020460223eafd8911290"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Brings Pi Antigravity multi-account routing to parity with OpenCode by sharing the core account pool. Pi previously held a single local credential with no rotation, pooling, or quota routing; it now has durable multi-account storage, quota-aware selection, failover, and operator controls. OpenCode's rate-limit and pool-persistence helpers are extracted into `@cortexkit/antigravity-auth-core` with no OpenCode behavior change.

**New Features**
- Adds `/agy-accounts`, `/agy-add`, `/agy-quota`, `/agy-strategy`, and `/agy-enable` Pi commands; repeat `/login google-antigravity` to add accounts.
- Login upserts by email, then refresh token, and preserves operator disabled flags.
- Tracks stable Google `accountId` identity and normalizes email before deduplication, so rotated credentials stay with the same account.
- Health and token-bucket trackers remap state with surviving accounts; identity reconciliation is idempotent.

**Migration**
- Pi peer dependencies now require `^0.85.1` (previously `*`); hosts on older Pi versions must upgrade.

<sup>Written for commit 48460e85d1e7ea529bd89586430ebe3e77b3cca5. Summary will update on new commits.</sup>

<a href="https://cubic.dev/pr/cortexkit/antigravity-auth/pull/15?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​earendil-works/​pi-coding-agent@​0.79.10 ⏵ 0.85.165 -2100100 +198 +1100
Updated@​earendil-works/​pi-tui@​0.79.10 ⏵ 0.85.197 +21100100 +198 +1100
Updated@​earendil-works/​pi-ai@​0.79.10 ⏵ 0.85.1100 +2010010098 +1100

View full report

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

21 issues found across 31 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/pi/src/settings.ts">

<violation number="1" location="packages/pi/src/settings.ts:30">
P2: When the settings file is `[]` or either field is explicitly `null`, `readSettings` silently accepts it and enables the defaults. Reject arrays and apply defaults only when properties are absent so invalid settings fail closed as documented.</violation>
</file>

<file name="packages/core/src/rate-limit-response.ts">

<violation number="1" location="packages/core/src/rate-limit-response.ts:2">
P3: This rate-limit/cooldown parsing logic moved into the shared core package is now on the critical path for both the OpenCode and Pi harnesses, but none of the three functions (retryAfterMsFromResponse, extractRateLimitBodyInfo, parseDurationToMs) has a unit test. The `parseDurationToMs` compound-duration regex and the multi-section error-details scanning in `extractRateLimitBodyInfo` are the kind of parsing logic most prone to regressions. Add a small test file for rate-limit-response.ts covering header precedence, RetryInfo vs metadata quotaResetDelay vs message-based fallbacks, and compound-duration parsing before this shared logic is relied on further.</violation>

<violation number="2" location="packages/core/src/rate-limit-response.ts:15">
P2: When a server sends the valid HTTP-date form of `Retry-After`, `retryAfterMsFromResponse` treats it as invalid and uses the default cooldown. Parse HTTP-date values after delay-seconds so routing honors the server's reset time.</violation>

<violation number="3" location="packages/core/src/rate-limit-response.ts:36">
P2: When an upstream error has a non-string `error.message`, `extractRateLimitBodyInfo` calls `.match()` on that value and aborts routing instead of failing over. Validate `message` with `typeof message === 'string'` before using it.</violation>
</file>

<file name="packages/core/src/account-storage.ts">

<violation number="1" location="packages/core/src/account-storage.ts:214">
P2: When normalization removes an account before the persisted active index, loading the pool can switch the active account because the loader never remaps `activeIndex` or `activeIndexByFamily`. Remap those indices through deduplication, or retain the survivor at the removed account’s slot.</violation>

<violation number="2" location="packages/core/src/account-storage.ts:214">
P2: When case/whitespace-variant email records carry different stable `accountId` values, this normalization silently drops one account instead of preserving or rejecting the identity conflict. Detect conflicting account IDs before deduplication and fail closed, matching the existing reconciliation behavior.</violation>

<violation number="3" location="packages/core/src/account-storage.ts:214">
P2: When a persisted v4 record contains a truthy non-string `email`, `.trim()` throws instead of returning the typed unreadable-storage error. Validate optional `email` as a string before normalization, so malformed files follow the existing backup and fail-closed path.</violation>
</file>

<file name="packages/pi/src/runtime.ts">

<violation number="1" location="packages/pi/src/runtime.ts:108">
P2: When an OAuth refresh hangs during quota refresh, the quota manager cannot cancel it because this call drops its timeout signal. Pass `signal` to `credentialFor` so quota refreshes honor the manager's timeout and dispose cancellation.</violation>

<violation number="2" location="packages/pi/src/runtime.ts:553">
P2: `dispatch` awaits `refreshQuota()` on every request before routing. In `fetchAccountQuota` each stale account does `credentialFor` (which runs `refreshToken` inside a fenced `mutateAccountStorage` lock) plus a network quota fetch plus a `patch`, so once an account's cache passes the 30-minute `QUOTA_REFRESH_MS` window the next interactive request serially refreshes every stale account's quota before the actual generation request is sent. This adds wall-clock latency (and lock serialization with peer processes) to the request critical path. Consider refreshing quota off the request path or in parallel.</violation>

<violation number="3" location="packages/pi/src/runtime.ts:617">
P2: `markAccountUsed` is called before the provider request is sent, but core's `AccountManager.markAccountUsed` documents it "Should be called AFTER request completion, not during account selection." Calling it here updates `account.lastUsed` to dispatch time, and the following `patch` persists `current.lastUsed = max(...)` even when the request then fails (transport error) or fails over via 429/503/529/500. A failing account is thus scored as freshly used, biasing the freshness/LRU scoring against it on the very next request.</violation>
</file>

<file name="packages/core/src/persist-account-pool.ts">

<violation number="1" location="packages/core/src/persist-account-pool.ts:175">
P2: When a pool write follows different Claude and Gemini cursors, this resets both family routes to `activeIndex` and changes subsequent account selection. Preserve and independently clamp `current.activeIndexByFamily` in the non-replace branch.</violation>

<violation number="2" location="packages/core/src/persist-account-pool.ts:213">
P2: When identity reconciliation collapses an enabled survivor with an operator-disabled duplicate, this merge loses the disable flag and makes the account routable again. Keep `enabled` false when either proven duplicate is disabled.</violation>

<violation number="3" location="packages/core/src/persist-account-pool.ts:296">
P1: When identity resolution has an email but no `accountId`, matching records with different stored account IDs are merged instead of failing closed. Compare each candidate ID with the current survivor ID as well as the optional resolved ID before removing the candidate.</violation>
</file>

<file name="packages/core/src/account-manager.ts">

<violation number="1" location="packages/core/src/account-manager.ts:572">
P2: When a Google account's email changes while its stable `accountId` remains unchanged, `reconcileStorage()` treats it as a different account, especially after refresh-token rotation. Match by the stable `accountId` when available, with normalized email as a fallback, so health, token balance, session pins, and routing state stay with the account.</violation>
</file>

<file name="packages/pi/src/stream.ts">

<violation number="1" location="packages/pi/src/stream.ts:711">
P2: When `dispatch` returns a non-retryable HTTP error such as 400, this catch marks the account unhealthy even though the failure can be request-wide. Only record stream failures after an OK response; let `dispatch` own HTTP status handling.</violation>
</file>

<file name="packages/core/src/quota-manager.ts">

<violation number="1" location="packages/core/src/quota-manager.ts:120">
P2: defaultKeyOf places the new stable accountId fallback after the email check, so accountId is only consulted when no non-empty email exists. Since deduplicateAccountsByEmail also keys by email only, two records for the same physical account that differ in email can't be collapsed and will receive separate quota caches/backoffs. To deliver the stated 'stable account identity reconciliation' goal, prefer accountId over email (or reconcile by accountId when it is present) so the collision case the PR calls out is actually handled.</violation>
</file>

<file name="packages/pi/src/commands.ts">

<violation number="1" location="packages/pi/src/commands.ts:62">
P3: When a user types an invalid argument (e.g. `/agy-quota foobar`), the thrown `Error('Use /agy-quota [refresh]')` is swallowed by the `register` wrapper and replaced with the generic 'Check the account/ settings file, account number, or re-authenticate' notification. The usage hint is never shown, and the generic message points at the wrong problem. Surface the specific error message (e.g. rethrow it or only apply the generic credential-talk), or let `register` include `error.message` for non-Ambiguity errors.</violation>
</file>

<file name="packages/core/src/rotation.ts">

<violation number="1" location="packages/core/src/rotation.ts:601">
P2: When two AccountManager instances manage different account pools, the process-global single-slot `trackerLayouts` makes the second manager read the first manager's identity layout as its `previous`, then remaps its own global health/token state through the foreign layout. The `ownsExpectedLayout` guard does not prevent this — it only switches to the fallback identity-matching path, which still iterates the other manager's identities. Key the stored layout by something pool-scoped, or only trust `trackerLayouts` when the caller's `previousIdentities` match (which the no-op check already computes) rather than the last caller.</violation>
</file>

<file name="packages/pi/src/provider.test.ts">

<violation number="1" location="packages/pi/src/provider.test.ts:397">
P3: This assertion does not prove that the partial stream is emitted only once. A replay regression could append two `partial` text blocks and still pass. Assert the complete text content or count matching blocks exactly once.</violation>

<violation number="2" location="packages/pi/src/provider.test.ts:398">
P3: This test does not verify that failover selects another account. Assert that the two transport calls use `Bearer access-1` and `Bearer access-2` so a same-account retry cannot satisfy the test.</violation>
</file>

<file name="packages/core/src/account-manager.test.ts">

<violation number="1" location="packages/core/src/account-manager.test.ts:155">
P2: In this test `health.recordFailure(0)` is called twice on account 'a', so its score drops to 30 (70-20-20), yet the expected values list index 1 as `[50, 43]`, which only holds if a single failure was applied. After the two reconciliations account 'a' lands at tracker index 1 preserving its (30, 43) state, so the expected value appears to be `[30, 43]`. Verify whether the double failure should apply and correct the assertion, otherwise this remapping case is not asserting the state it intends.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

if (
emailMatch &&
candidate.accountId &&
accountId &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When identity resolution has an email but no accountId, matching records with different stored account IDs are merged instead of failing closed. Compare each candidate ID with the current survivor ID as well as the optional resolved ID before removing the candidate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/persist-account-pool.ts, line 296:

<comment>When identity resolution has an email but no `accountId`, matching records with different stored account IDs are merged instead of failing closed. Compare each candidate ID with the current survivor ID as well as the optional resolved ID before removing the candidate.</comment>

<file context>
@@ -0,0 +1,368 @@
+      if (
+        emailMatch &&
+        candidate.accountId &&
+        accountId &&
+        candidate.accountId !== accountId
+      ) {
</file context>

'Cannot read Pi Antigravity routing settings; repair the settings file',
)
}
if (!value || typeof value !== 'object')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the settings file is [] or either field is explicitly null, readSettings silently accepts it and enables the defaults. Reject arrays and apply defaults only when properties are absent so invalid settings fail closed as documented.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi/src/settings.ts, line 30:

<comment>When the settings file is `[]` or either field is explicitly `null`, `readSettings` silently accepts it and enables the defaults. Reject arrays and apply defaults only when properties are absent so invalid settings fail closed as documented.</comment>

<file context>
@@ -0,0 +1,62 @@
+      'Cannot read Pi Antigravity routing settings; repair the settings file',
+    )
+  }
+  if (!value || typeof value !== 'object')
+    throw new Error('Invalid Pi Antigravity settings')
+  const config = value as Record<string, unknown>
</file context>

Comment on lines +15 to +20
if (retryAfterHeader) {
const parsed = Number.parseInt(retryAfterHeader, 10)
if (!Number.isNaN(parsed) && parsed > 0) {
return parsed * 1000
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a server sends the valid HTTP-date form of Retry-After, retryAfterMsFromResponse treats it as invalid and uses the default cooldown. Parse HTTP-date values after delay-seconds so routing honors the server's reset time.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/rate-limit-response.ts, line 15:

<comment>When a server sends the valid HTTP-date form of `Retry-After`, `retryAfterMsFromResponse` treats it as invalid and uses the default cooldown. Parse HTTP-date values after delay-seconds so routing honors the server's reset time.</comment>

<file context>
@@ -0,0 +1,157 @@
+  }
+
+  const retryAfterHeader = response.headers.get('retry-after')
+  if (retryAfterHeader) {
+    const parsed = Number.parseInt(retryAfterHeader, 10)
+    if (!Number.isNaN(parsed) && parsed > 0) {
</file context>
Suggested change
if (retryAfterHeader) {
const parsed = Number.parseInt(retryAfterHeader, 10)
if (!Number.isNaN(parsed) && parsed > 0) {
return parsed * 1000
}
}
if (retryAfterHeader) {
if (/^\d+$/.test(retryAfterHeader)) {
const parsed = Number(retryAfterHeader)
if (Number.isFinite(parsed) && parsed >= 0) {
return parsed * 1000
}
}
const retryAt = Date.parse(retryAfterHeader)
if (!Number.isNaN(retryAt)) {
return Math.max(0, retryAt - Date.now())
}
}

Comment on lines +36 to +39
const message =
error && typeof error === 'object'
? (error as { message?: string }).message
: undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When an upstream error has a non-string error.message, extractRateLimitBodyInfo calls .match() on that value and aborts routing instead of failing over. Validate message with typeof message === 'string' before using it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/rate-limit-response.ts, line 36:

<comment>When an upstream error has a non-string `error.message`, `extractRateLimitBodyInfo` calls `.match()` on that value and aborts routing instead of failing over. Validate `message` with `typeof message === 'string'` before using it.</comment>

<file context>
@@ -0,0 +1,157 @@
+  if (!body || typeof body !== 'object') return { retryDelayMs: null }
+
+  const error = (body as { error?: unknown }).error
+  const message =
+    error && typeof error === 'object'
+      ? (error as { message?: string }).message
</file context>
Suggested change
const message =
error && typeof error === 'object'
? (error as { message?: string }).message
: undefined
const message =
error &&
typeof error === 'object' &&
typeof (error as { message?: unknown }).message === 'string'
? (error as { message: string }).message
: undefined

}

const existingIndex = emailToNewestIndex.get(acc.email)
const email = acc.email.trim().toLowerCase()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When normalization removes an account before the persisted active index, loading the pool can switch the active account because the loader never remaps activeIndex or activeIndexByFamily. Remap those indices through deduplication, or retain the survivor at the removed account’s slot.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/account-storage.ts, line 214:

<comment>When normalization removes an account before the persisted active index, loading the pool can switch the active account because the loader never remaps `activeIndex` or `activeIndexByFamily`. Remap those indices through deduplication, or retain the survivor at the removed account’s slot.</comment>

<file context>
@@ -211,15 +211,20 @@ export function deduplicateAccountsByEmail<
     }
 
-    const existingIndex = emailToNewestIndex.get(acc.email)
+    const email = acc.email.trim().toLowerCase()
+    if (!email) {
+      indicesToKeep.add(i)
</file context>

manager.reconcileStorage(next)
expect(values()).toEqual([
[70, 50],
[50, 43],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: In this test health.recordFailure(0) is called twice on account 'a', so its score drops to 30 (70-20-20), yet the expected values list index 1 as [50, 43], which only holds if a single failure was applied. After the two reconciliations account 'a' lands at tracker index 1 preserving its (30, 43) state, so the expected value appears to be [30, 43]. Verify whether the double failure should apply and correct the assertion, otherwise this remapping case is not asserting the state it intends.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/account-manager.test.ts, line 155:

<comment>In this test `health.recordFailure(0)` is called twice on account 'a', so its score drops to 30 (70-20-20), yet the expected values list index 1 as `[50, 43]`, which only holds if a single failure was applied. After the two reconciliations account 'a' lands at tracker index 1 preserving its (30, 43) state, so the expected value appears to be `[30, 43]`. Verify whether the double failure should apply and correct the assertion, otherwise this remapping case is not asserting the state it intends.</comment>

<file context>
@@ -42,6 +43,455 @@ const stored: AccountStorageV4 = {
+      manager.reconcileStorage(next)
+      expect(values()).toEqual([
+        [70, 50],
+        [50, 43],
+        [70, 50],
+      ])
</file context>

@@ -0,0 +1,157 @@
/** Reads `retry-after-ms` / `retry-after` headers, in that order. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This rate-limit/cooldown parsing logic moved into the shared core package is now on the critical path for both the OpenCode and Pi harnesses, but none of the three functions (retryAfterMsFromResponse, extractRateLimitBodyInfo, parseDurationToMs) has a unit test. The parseDurationToMs compound-duration regex and the multi-section error-details scanning in extractRateLimitBodyInfo are the kind of parsing logic most prone to regressions. Add a small test file for rate-limit-response.ts covering header precedence, RetryInfo vs metadata quotaResetDelay vs message-based fallbacks, and compound-duration parsing before this shared logic is relied on further.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/rate-limit-response.ts, line 2:

<comment>This rate-limit/cooldown parsing logic moved into the shared core package is now on the critical path for both the OpenCode and Pi harnesses, but none of the three functions (retryAfterMsFromResponse, extractRateLimitBodyInfo, parseDurationToMs) has a unit test. The `parseDurationToMs` compound-duration regex and the multi-section error-details scanning in `extractRateLimitBodyInfo` are the kind of parsing logic most prone to regressions. Add a small test file for rate-limit-response.ts covering header precedence, RetryInfo vs metadata quotaResetDelay vs message-based fallbacks, and compound-duration parsing before this shared logic is relied on further.</comment>

<file context>
@@ -0,0 +1,157 @@
+/** Reads `retry-after-ms` / `retry-after` headers, in that order. */
+export function retryAfterMsFromResponse(
+  response: Response,
+  defaultRetryMs: number = 60_000,
</file context>

`Quota refresh failed for ${failures} account(s); cached values retained.`,
'warning',
)
} else if (args.trim()) throw new Error('Use /agy-quota [refresh]')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When a user types an invalid argument (e.g. /agy-quota foobar), the thrown Error('Use /agy-quota [refresh]') is swallowed by the register wrapper and replaced with the generic 'Check the account/ settings file, account number, or re-authenticate' notification. The usage hint is never shown, and the generic message points at the wrong problem. Surface the specific error message (e.g. rethrow it or only apply the generic credential-talk), or let register include error.message for non-Ambiguity errors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi/src/commands.ts, line 62:

<comment>When a user types an invalid argument (e.g. `/agy-quota foobar`), the thrown `Error('Use /agy-quota [refresh]')` is swallowed by the `register` wrapper and replaced with the generic 'Check the account/ settings file, account number, or re-authenticate' notification. The usage hint is never shown, and the generic message points at the wrong problem. Surface the specific error message (e.g. rethrow it or only apply the generic credential-talk), or let `register` include `error.message` for non-Ambiguity errors.</comment>

<file context>
@@ -0,0 +1,140 @@
+            `Quota refresh failed for ${failures} account(s); cached values retained.`,
+            'warning',
+          )
+      } else if (args.trim()) throw new Error('Use /agy-quota [refresh]')
+      ctx.ui.notify(await runtime.describe(), 'info')
+    },
</file context>

const result = await stream.result()
expect(result.stopReason).toBe('error')
expect(result.content).toContainEqual({ type: 'text', text: 'partial' })
expect(transport).toHaveBeenCalledTimes(2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This test does not verify that failover selects another account. Assert that the two transport calls use Bearer access-1 and Bearer access-2 so a same-account retry cannot satisfy the test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi/src/provider.test.ts, line 398:

<comment>This test does not verify that failover selects another account. Assert that the two transport calls use `Bearer access-1` and `Bearer access-2` so a same-account retry cannot satisfy the test.</comment>

<file context>
@@ -0,0 +1,404 @@
+    const result = await stream.result()
+    expect(result.stopReason).toBe('error')
+    expect(result.content).toContainEqual({ type: 'text', text: 'partial' })
+    expect(transport).toHaveBeenCalledTimes(2)
+    const bodies = transport.mock.calls.map((call) =>
+      JSON.parse((call[1] as RequestInit).body as string),
</file context>
Suggested change
expect(transport).toHaveBeenCalledTimes(2)
expect(
transport.mock.calls.map(
(call) =>
((call[1] as RequestInit).headers as Record<string, string>)
.Authorization,
),
).toEqual(['Bearer access-1', 'Bearer access-2'])

)
const result = await stream.result()
expect(result.stopReason).toBe('error')
expect(result.content).toContainEqual({ type: 'text', text: 'partial' })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This assertion does not prove that the partial stream is emitted only once. A replay regression could append two partial text blocks and still pass. Assert the complete text content or count matching blocks exactly once.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi/src/provider.test.ts, line 397:

<comment>This assertion does not prove that the partial stream is emitted only once. A replay regression could append two `partial` text blocks and still pass. Assert the complete text content or count matching blocks exactly once.</comment>

<file context>
@@ -0,0 +1,404 @@
+    )
+    const result = await stream.result()
+    expect(result.stopReason).toBe('error')
+    expect(result.content).toContainEqual({ type: 'text', text: 'partial' })
+    expect(transport).toHaveBeenCalledTimes(2)
+    const bodies = transport.mock.calls.map((call) =>
</file context>
Suggested change
expect(result.content).toContainEqual({ type: 'text', text: 'partial' })
expect(result.content).toEqual([{ type: 'text', text: 'partial' }])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant