@centerforagenticai/pi-multi-account 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/package.json +5 -5
- package/packages/pi-anthropic-oauth/package.json +2 -2
- package/packages/pi-anthropic-oauth/src/stream.ts +26 -8
- package/packages/pi-anthropic-oauth/src/transport-activity.ts +59 -0
- package/packages/pi-antigravity/package.json +2 -2
- package/packages/pi-antigravity/src/models/discovery.ts +2 -1
- package/packages/pi-antigravity/src/models/grouping.ts +12 -10
- package/packages/pi-antigravity/src/models/models.ts +11 -4
- package/src/account-group-failure.ts +149 -0
- package/src/account-group-members.ts +139 -0
- package/src/anthropic-adaptive-stream.ts +18 -9
- package/src/anthropic-alias-stream.ts +13 -68
- package/src/codex-adapter.ts +78 -50
- package/src/commands.ts +3 -3
- package/src/config.ts +197 -2
- package/src/diagnostic-store.ts +17 -15
- package/src/diagnostics.ts +97 -35
- package/src/host-final-stop-message.ts +20 -118
- package/src/index.ts +452 -87
- package/src/logical-dispatch.ts +12 -13
- package/src/logical-provider.ts +1282 -355
- package/src/model-fallback-policy.ts +384 -0
- package/src/models-declaration.ts +15 -8
- package/src/public-assistant-projection.ts +163 -0
- package/src/recovery-engine.ts +648 -110
- package/src/recovery-plan.ts +7 -1
- package/src/recovery-send-evidence.ts +29 -0
- package/src/refusal-advice.ts +139 -0
- package/src/routing.ts +4 -8
- package/src/runtime-state.ts +7 -0
- package/src/shared-usage.ts +21 -4
- package/src/upstream-anthropic.ts +12 -4
- package/src/upstream-antigravity.ts +2 -38
- package/src/usage-fetch.ts +42 -59
package/src/recovery-plan.ts
CHANGED
|
@@ -30,6 +30,9 @@ export type RecoveryCandidateTier =
|
|
|
30
30
|
| "owning-vendor-api"
|
|
31
31
|
| "cross-family-subscription";
|
|
32
32
|
|
|
33
|
+
/** The one recovery dimension a candidate may change for this logical call. */
|
|
34
|
+
export type RecoveryActionKind = "account" | "model";
|
|
35
|
+
|
|
33
36
|
/** One finite provider/model pair. Logical and physical model identity stay distinct. */
|
|
34
37
|
export interface RecoveryCandidate {
|
|
35
38
|
readonly providerId: string;
|
|
@@ -39,6 +42,7 @@ export interface RecoveryCandidate {
|
|
|
39
42
|
readonly selectedModelId: string;
|
|
40
43
|
readonly tier: RecoveryCandidateTier;
|
|
41
44
|
readonly substitution: "exact" | "configured";
|
|
45
|
+
readonly recoveryAction: RecoveryActionKind;
|
|
42
46
|
readonly capability: RecoveryModelCapability;
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -91,7 +95,7 @@ function immutableCapability(model: RecoveryModelCapability): RecoveryModelCapab
|
|
|
91
95
|
});
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
/** Build the immutable, deterministic candidate
|
|
98
|
+
/** Build the immutable, deterministic candidate order for one logical call. */
|
|
95
99
|
export function buildRecoveryCandidatePlan(
|
|
96
100
|
request: RecoveryCandidatePlanRequest,
|
|
97
101
|
): readonly RecoveryCandidate[] {
|
|
@@ -146,6 +150,8 @@ export function buildRecoveryCandidatePlan(
|
|
|
146
150
|
selectedModelId: request.selectedModelId,
|
|
147
151
|
tier,
|
|
148
152
|
substitution,
|
|
153
|
+
recoveryAction:
|
|
154
|
+
model.modelId === request.selectedModelId ? "account" : "model",
|
|
149
155
|
capability: immutableCapability(model),
|
|
150
156
|
}),
|
|
151
157
|
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export type CodexRecoverySendEvidence = "pre-execution-rejected" | "uncertain";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Classify whether a pinned Codex WebSocket terminal proves that one request was
|
|
7
|
+
* rejected before execution without an internal reconnect or SSE fallback.
|
|
8
|
+
*
|
|
9
|
+
* Pinned pi-ai 0.84.4 sends `response.create` before it observes stream events
|
|
10
|
+
* (`dist/api/openai-codex-responses.js:1182`). Its WebSocket loop can reconnect
|
|
11
|
+
* for two special errors or fall back to SSE (`:218-245`). The
|
|
12
|
+
* `provider_transport_failure` diagnostic is appended only on that transport
|
|
13
|
+
* branch (`:229-237`). Structured `CodexApiError` fields are instead normalized
|
|
14
|
+
* to terminal `errorMessage` (`:483-539`, `:344-346`), which retains neither the
|
|
15
|
+
* code/payload nor proof that no prior reconnect occurred.
|
|
16
|
+
*
|
|
17
|
+
* The per-session `getOpenAICodexWebSocketDebugStats` counters (`:632-654`)
|
|
18
|
+
* are process-global, shared by concurrent requests, and absent from the
|
|
19
|
+
* terminal, so they cannot attribute sends to one invocation either.
|
|
20
|
+
*
|
|
21
|
+
* Consequently no terminal `AssistantMessage` exposed by the pinned package is
|
|
22
|
+
* trustworthy proof of the complete condition. Do not infer safety from prose,
|
|
23
|
+
* status-like text, or the absence of a transport diagnostic.
|
|
24
|
+
*/
|
|
25
|
+
export function classifyCodexRecoverySendEvidence(
|
|
26
|
+
_terminal: AssistantMessage,
|
|
27
|
+
): CodexRecoverySendEvidence {
|
|
28
|
+
return "uncertain";
|
|
29
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator-only advice for a structured provider refusal.
|
|
3
|
+
*
|
|
4
|
+
* A refusal is an outcome, not an account failure: routing keeps the account
|
|
5
|
+
* and nothing is resent (see `src/routing.ts`, status `retained`). This module
|
|
6
|
+
* only tells the operator what happened and which choices remain. It never
|
|
7
|
+
* resends, rephrases, starts a turn, changes the model, rotates an account, or
|
|
8
|
+
* writes state.
|
|
9
|
+
*
|
|
10
|
+
* Only the structured `code` field selects advice. The adapter sets it from the
|
|
11
|
+
* provider's own stop reason (`src/anthropic-adaptive-stream.ts`). Assistant
|
|
12
|
+
* prose, the error message, the raw stop reason, and content are never read,
|
|
13
|
+
* so a refusal is never guessed and no provider text can reach the advice.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Identical advice inside this window is shown once. */
|
|
17
|
+
export const REFUSAL_ADVICE_REPEAT_WINDOW_MS = 60_000;
|
|
18
|
+
|
|
19
|
+
/** Provider and model IDs are named only when they are short, plain tokens. */
|
|
20
|
+
const SAFE_ROUTE_ID = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
|
|
21
|
+
|
|
22
|
+
const CHOICES =
|
|
23
|
+
"Multi-account kept the account and did not resend, reroute, or retry it. " +
|
|
24
|
+
"You can revise the request, or pick another model explicitly with /model.";
|
|
25
|
+
|
|
26
|
+
export type RefusalAdviceRoute = Readonly<{
|
|
27
|
+
providerId?: unknown;
|
|
28
|
+
modelId?: unknown;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
/** Reads one own data property; accessors and proxies count as absent. */
|
|
32
|
+
function ownDataValue(value: object, key: string): unknown {
|
|
33
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
34
|
+
return descriptor !== undefined && "value" in descriptor
|
|
35
|
+
? descriptor.value
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The allowlisted projection: whether `message` is an assistant error terminal
|
|
41
|
+
* carrying the exact structured `refusal` code. Nothing else is read.
|
|
42
|
+
*/
|
|
43
|
+
export function isStructuredRefusal(message: unknown): boolean {
|
|
44
|
+
try {
|
|
45
|
+
if (typeof message !== "object" || message === null) return false;
|
|
46
|
+
return (
|
|
47
|
+
ownDataValue(message, "role") === "assistant" &&
|
|
48
|
+
ownDataValue(message, "stopReason") === "error" &&
|
|
49
|
+
ownDataValue(message, "code") === "refusal"
|
|
50
|
+
);
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeRouteId(value: unknown): string | undefined {
|
|
57
|
+
return typeof value === "string" && SAFE_ROUTE_ID.test(value)
|
|
58
|
+
? value
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function safeRouteLabel(route: RefusalAdviceRoute): string | undefined {
|
|
63
|
+
try {
|
|
64
|
+
const providerId = safeRouteId(route.providerId);
|
|
65
|
+
const modelId = safeRouteId(route.modelId);
|
|
66
|
+
return providerId === undefined || modelId === undefined
|
|
67
|
+
? undefined
|
|
68
|
+
: `${providerId}/${modelId}`;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Bounded advice text for a structured refusal, or `undefined` when the message
|
|
76
|
+
* carries no structured refusal code. The text names only the route identity
|
|
77
|
+
* the caller resolved, and drops it when it is not a short plain token.
|
|
78
|
+
*/
|
|
79
|
+
export function refusalAdvice(
|
|
80
|
+
message: unknown,
|
|
81
|
+
route: RefusalAdviceRoute,
|
|
82
|
+
): string | undefined {
|
|
83
|
+
if (!isStructuredRefusal(message)) return undefined;
|
|
84
|
+
const label = safeRouteLabel(route);
|
|
85
|
+
const subject =
|
|
86
|
+
label === undefined ? "The model refused this request." : `${label} refused this request.`;
|
|
87
|
+
return `${subject} ${CHOICES}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type NotifyContext = {
|
|
91
|
+
readonly hasUI?: unknown;
|
|
92
|
+
readonly ui?: { notify?: unknown };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type RefusalAdvisorOptions = Readonly<{
|
|
96
|
+
/** False for delegate-owned sessions that share the foreground UI. */
|
|
97
|
+
foreground: boolean;
|
|
98
|
+
now?: () => number;
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Shows refusal advice in the foreground UI at most once per terminal and once
|
|
103
|
+
* per route inside {@link REFUSAL_ADVICE_REPEAT_WINDOW_MS}. Fail-soft: a UI
|
|
104
|
+
* failure never reaches the caller. Returns whether advice was shown.
|
|
105
|
+
*/
|
|
106
|
+
export function createRefusalAdvisor(options: RefusalAdvisorOptions) {
|
|
107
|
+
const now = options.now ?? Date.now;
|
|
108
|
+
const advised = new WeakSet<object>();
|
|
109
|
+
let lastText: string | undefined;
|
|
110
|
+
let lastAtMs = Number.NEGATIVE_INFINITY;
|
|
111
|
+
return {
|
|
112
|
+
advise(
|
|
113
|
+
message: unknown,
|
|
114
|
+
context: NotifyContext | undefined,
|
|
115
|
+
route: RefusalAdviceRoute,
|
|
116
|
+
): boolean {
|
|
117
|
+
try {
|
|
118
|
+
if (!options.foreground) return false;
|
|
119
|
+
const text = refusalAdvice(message, route);
|
|
120
|
+
if (text === undefined) return false;
|
|
121
|
+
if (advised.has(message as object)) return false;
|
|
122
|
+
advised.add(message as object);
|
|
123
|
+
if (context?.hasUI !== true) return false;
|
|
124
|
+
const notify = context.ui?.notify;
|
|
125
|
+
if (typeof notify !== "function") return false;
|
|
126
|
+
const atMs = now();
|
|
127
|
+
if (text === lastText && atMs - lastAtMs < REFUSAL_ADVICE_REPEAT_WINDOW_MS) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
lastText = text;
|
|
131
|
+
lastAtMs = atMs;
|
|
132
|
+
notify.call(context.ui, text, "warning");
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
package/src/routing.ts
CHANGED
|
@@ -20,8 +20,7 @@ import { resolveTierModel } from "./tier-model-resolver.js";
|
|
|
20
20
|
import { providerTypeFor, sameVendor } from "./vendor.js";
|
|
21
21
|
import {
|
|
22
22
|
type RuntimeState,
|
|
23
|
-
isCanonicalManagedProviderId,
|
|
24
|
-
type CredentialRevision,
|
|
23
|
+
isCanonicalManagedProviderId, normalizeCredentialRevision, type CredentialRevision,
|
|
25
24
|
} from "./runtime-state.js";
|
|
26
25
|
|
|
27
26
|
export interface SharedUsageHint {
|
|
@@ -424,12 +423,9 @@ function normalizedAccounts(
|
|
|
424
423
|
): readonly ManagedAccount[] {
|
|
425
424
|
const projected = projectCanonicalAccounts(accounts);
|
|
426
425
|
for (const account of projected) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
account.family,
|
|
431
|
-
account.credentialRevision,
|
|
432
|
-
);
|
|
426
|
+
const revision = normalizeCredentialRevision(account.credentialRevision ?? NaN);
|
|
427
|
+
if (revision !== undefined) {
|
|
428
|
+
state.observeCredentialRevision(account.providerId, account.family, revision);
|
|
433
429
|
}
|
|
434
430
|
// Observed unconditionally: an absent fingerprint is meaningful (identity
|
|
435
431
|
// is not derivable) and must not be mistaken for an account change.
|
package/src/runtime-state.ts
CHANGED
|
@@ -7,6 +7,13 @@ import {
|
|
|
7
7
|
/** Numeric revision exposed by maintained, non-secret public credential metadata. */
|
|
8
8
|
export type CredentialRevision = number;
|
|
9
9
|
|
|
10
|
+
/** Convert public expiry metadata to the integer revision accepted by runtime state. */
|
|
11
|
+
export function normalizeCredentialRevision(value: number): CredentialRevision | undefined {
|
|
12
|
+
if (!Number.isFinite(value) || value < 0) return undefined;
|
|
13
|
+
const revision = Math.trunc(value);
|
|
14
|
+
return Number.isSafeInteger(revision) ? revision : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export type CooldownReason =
|
|
11
18
|
| "quota"
|
|
12
19
|
| "rate-limit"
|
package/src/shared-usage.ts
CHANGED
|
@@ -905,10 +905,14 @@ export class SharedUsageStore {
|
|
|
905
905
|
|
|
906
906
|
append(record: SharedUsageLogRecord): boolean {
|
|
907
907
|
try {
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
|
|
908
|
+
// The store is the ONLY producer of a persisted observer id
|
|
909
|
+
// (internal issue #133). The grammar check alone cannot keep a
|
|
910
|
+
// credential out: `sk-ant-api03-...` is a valid hostname-shaped id, and
|
|
911
|
+
// no character rule separates a hostname from a key. So a caller may
|
|
912
|
+
// only restate this store's own id, which the constructor validated;
|
|
913
|
+
// any other value, a peer's included, is refused rather than written.
|
|
914
|
+
// Judged on the caller's own value, before projection slices it.
|
|
915
|
+
if ((record as { observerId?: unknown }).observerId !== this.#observerId)
|
|
912
916
|
return false;
|
|
913
917
|
const projected = validExhaustionHoldRecord(record)
|
|
914
918
|
? projectExhaustionHold(record)
|
|
@@ -1063,15 +1067,28 @@ export class SharedUsageStore {
|
|
|
1063
1067
|
return latest;
|
|
1064
1068
|
}
|
|
1065
1069
|
|
|
1070
|
+
/**
|
|
1071
|
+
* The newest attempt for an account that was observed at or before `nowMs`.
|
|
1072
|
+
*
|
|
1073
|
+
* An attempt observed in the future is ignored here, at the single source,
|
|
1074
|
+
* rather than by each reader (internal issue #133). Such a record always
|
|
1075
|
+
* sorts as the newest, so a reader that merely declined to be suppressed by
|
|
1076
|
+
* it still took its `failureCount` as the prior rung: every real failure
|
|
1077
|
+
* rewrote the same count, the next read returned the future record again,
|
|
1078
|
+
* and the backoff ladder froze. `nowMs` is required so each reader judges
|
|
1079
|
+
* against its own (possibly injected) clock.
|
|
1080
|
+
*/
|
|
1066
1081
|
latestAttempt(
|
|
1067
1082
|
providerId: string,
|
|
1068
1083
|
family: AllowedFamily,
|
|
1084
|
+
nowMs: number,
|
|
1069
1085
|
): SharedUsageAttemptRecord | undefined {
|
|
1070
1086
|
let latest: SharedUsageAttemptRecord | undefined;
|
|
1071
1087
|
for (const record of this.readAttempts()) {
|
|
1072
1088
|
if (
|
|
1073
1089
|
record.providerId === providerId &&
|
|
1074
1090
|
record.family === family &&
|
|
1091
|
+
record.observedAtMs <= nowMs &&
|
|
1075
1092
|
(latest === undefined || record.observedAtMs >= latest.observedAtMs)
|
|
1076
1093
|
) {
|
|
1077
1094
|
latest = record;
|
|
@@ -3,10 +3,12 @@ import type {
|
|
|
3
3
|
ProviderConfig,
|
|
4
4
|
ProviderModelConfig,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { normalizeContext, type Context } from "@earendil-works/pi-ai";
|
|
6
7
|
import { registerApiProvider } from "@earendil-works/pi-ai/compat";
|
|
7
8
|
import {
|
|
8
9
|
ANTHROPIC_ALIAS_API,
|
|
9
10
|
createAnthropicAliasStream,
|
|
11
|
+
type AnthropicLegacyStream,
|
|
10
12
|
type AnthropicUpstreamStream,
|
|
11
13
|
} from "./anthropic-alias-stream.js";
|
|
12
14
|
import { streamAnthropicAdaptive } from "./anthropic-adaptive-stream.js";
|
|
@@ -167,7 +169,7 @@ export function captureUpstreamAnthropicProvider(
|
|
|
167
169
|
* through the other implementation.
|
|
168
170
|
*/
|
|
169
171
|
export function createAnthropicStreamSelector(
|
|
170
|
-
adaptiveStream:
|
|
172
|
+
adaptiveStream: AnthropicLegacyStream,
|
|
171
173
|
pinnedStream: AnthropicUpstreamStream,
|
|
172
174
|
): AnthropicUpstreamStream {
|
|
173
175
|
return (model, context, options) => {
|
|
@@ -175,7 +177,7 @@ export function createAnthropicStreamSelector(
|
|
|
175
177
|
(model.compat as { forceAdaptiveThinking?: boolean } | undefined)
|
|
176
178
|
?.forceAdaptiveThinking === true && options?.reasoning !== undefined;
|
|
177
179
|
const selected = useAdaptiveStream ? adaptiveStream : pinnedStream;
|
|
178
|
-
return selected(model, toPinnedAnthropicContext(context), options);
|
|
180
|
+
return selected(model, toPinnedAnthropicContext(context as Context), options);
|
|
179
181
|
};
|
|
180
182
|
}
|
|
181
183
|
|
|
@@ -229,14 +231,20 @@ const registerAliasApiWithCompat: AliasApiRegistrar = (aliasStream) => {
|
|
|
229
231
|
export async function registerUpstreamAnthropicProvider(
|
|
230
232
|
pi: ExtensionAPI,
|
|
231
233
|
upstream?: UpstreamAnthropicExtension,
|
|
232
|
-
adaptiveStream:
|
|
234
|
+
adaptiveStream: AnthropicLegacyStream = streamAnthropicAdaptive,
|
|
233
235
|
registerAliasApi: AliasApiRegistrar = registerAliasApiWithCompat,
|
|
234
236
|
): Promise<CapturedAnthropicProvider> {
|
|
235
237
|
const extension = upstream ?? (await loadUpstreamAnthropicExtension());
|
|
236
238
|
const captured = captureUpstreamAnthropicProvider(pi, extension);
|
|
239
|
+
const pinnedStream: AnthropicUpstreamStream = (model, context, options) =>
|
|
240
|
+
captured.config.streamSimple(
|
|
241
|
+
model,
|
|
242
|
+
normalizeContext(toPinnedAnthropicContext(context as Context)),
|
|
243
|
+
options,
|
|
244
|
+
);
|
|
237
245
|
const streamSimple = createAnthropicStreamSelector(
|
|
238
246
|
adaptiveStream,
|
|
239
|
-
|
|
247
|
+
pinnedStream,
|
|
240
248
|
);
|
|
241
249
|
const adapted: CompleteAnthropicProviderConfig = {
|
|
242
250
|
...captured.config,
|
|
@@ -5,14 +5,13 @@ import type {
|
|
|
5
5
|
import {
|
|
6
6
|
createAssistantMessageEventStream,
|
|
7
7
|
type Api,
|
|
8
|
-
type AssistantMessage,
|
|
9
|
-
type AssistantMessageEvent,
|
|
10
8
|
type AssistantMessageEventStream,
|
|
11
9
|
type Context,
|
|
12
10
|
type Model,
|
|
13
11
|
type SimpleStreamOptions,
|
|
14
12
|
} from "@earendil-works/pi-ai";
|
|
15
13
|
import { registerApiProvider } from "@earendil-works/pi-ai/compat";
|
|
14
|
+
import { projectAliasAssistantEvent } from "./public-assistant-projection.js";
|
|
16
15
|
|
|
17
16
|
const AUTH_ENTRYPOINT = "../packages/pi-antigravity/src/auth/index.js";
|
|
18
17
|
const CLIENT_ENTRYPOINT = "../packages/pi-antigravity/src/client/index.js";
|
|
@@ -185,41 +184,6 @@ export function assertAntigravityDebugDumpDisabled(
|
|
|
185
184
|
}
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
function withAliasAttribution(
|
|
189
|
-
message: AssistantMessage,
|
|
190
|
-
aliasModel: Model<Api>,
|
|
191
|
-
): AssistantMessage {
|
|
192
|
-
return {
|
|
193
|
-
...message,
|
|
194
|
-
api: aliasModel.api,
|
|
195
|
-
provider: aliasModel.provider,
|
|
196
|
-
model: aliasModel.id,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function withAliasEvent(
|
|
201
|
-
event: AssistantMessageEvent,
|
|
202
|
-
aliasModel: Model<Api>,
|
|
203
|
-
): AssistantMessageEvent {
|
|
204
|
-
switch (event.type) {
|
|
205
|
-
case "done":
|
|
206
|
-
return {
|
|
207
|
-
...event,
|
|
208
|
-
message: withAliasAttribution(event.message, aliasModel),
|
|
209
|
-
};
|
|
210
|
-
case "error":
|
|
211
|
-
return {
|
|
212
|
-
...event,
|
|
213
|
-
error: withAliasAttribution(event.error, aliasModel),
|
|
214
|
-
};
|
|
215
|
-
default:
|
|
216
|
-
return {
|
|
217
|
-
...event,
|
|
218
|
-
partial: withAliasAttribution(event.partial, aliasModel),
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
187
|
function reattributeAntigravityStream(
|
|
224
188
|
upstream: AssistantMessageEventStream,
|
|
225
189
|
aliasModel: Model<Api>,
|
|
@@ -227,7 +191,7 @@ function reattributeAntigravityStream(
|
|
|
227
191
|
const attributed = createAssistantMessageEventStream();
|
|
228
192
|
void (async () => {
|
|
229
193
|
for await (const event of upstream) {
|
|
230
|
-
attributed.push(
|
|
194
|
+
attributed.push(projectAliasAssistantEvent(event, aliasModel));
|
|
231
195
|
}
|
|
232
196
|
})();
|
|
233
197
|
return attributed;
|
package/src/usage-fetch.ts
CHANGED
|
@@ -641,36 +641,27 @@ async function queryEndpoint(
|
|
|
641
641
|
: normalizeAnthropicUsagePayload(payload);
|
|
642
642
|
}
|
|
643
643
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
)
|
|
658
|
-
if (attempt === undefined) return undefined;
|
|
659
|
-
if (attempt.nextAttemptAtMs > nowMs + MAX_USAGE_ATTEMPT_DELAY_MS) {
|
|
660
|
-
return undefined;
|
|
661
|
-
}
|
|
662
|
-
return attempt.nextAttemptAtMs;
|
|
663
|
-
}
|
|
644
|
+
// Every attempt a reader here sees comes from `SharedUsageStore.latestAttempt`
|
|
645
|
+
// called with that reader's own clock, and that is what keeps a far-future
|
|
646
|
+
// deadline (internal issue #108) from suppressing polling:
|
|
647
|
+
//
|
|
648
|
+
// - the store refuses, on append and read, any attempt whose
|
|
649
|
+
// `nextAttemptAtMs - observedAtMs` exceeds MAX_USAGE_ATTEMPT_DELAY_MS; and
|
|
650
|
+
// - `latestAttempt` skips any attempt observed after `nowMs` (#133).
|
|
651
|
+
//
|
|
652
|
+
// Together these give `nextAttemptAtMs <= nowMs + MAX_USAGE_ATTEMPT_DELAY_MS`
|
|
653
|
+
// for every record a reader receives. A per-reader "plausible deadline" check
|
|
654
|
+
// used to restate that bound; once the origin filter moved into the store it
|
|
655
|
+
// could no longer fail on any reachable input and was removed (#133,
|
|
656
|
+
// CR-REFRESH-READER-BOUND-UNREACHABLE). Read attempts only through
|
|
657
|
+
// `latestAttempt(providerId, family, nowMs)` with the clock the decision uses.
|
|
664
658
|
|
|
665
659
|
function statusFromAttempt(
|
|
666
660
|
attempt: SharedUsageAttemptRecord | undefined,
|
|
667
661
|
enabled: boolean,
|
|
668
|
-
nowMs: number,
|
|
669
662
|
): UsageFetchStatus {
|
|
670
663
|
const nextAttemptAtMs =
|
|
671
|
-
attempt?.failureCount === 0
|
|
672
|
-
? undefined
|
|
673
|
-
: plausibleAttemptDeadline(attempt, nowMs);
|
|
664
|
+
attempt?.failureCount === 0 ? undefined : attempt?.nextAttemptAtMs;
|
|
674
665
|
const disabledReason = attempt?.failureReason;
|
|
675
666
|
return {
|
|
676
667
|
enabled,
|
|
@@ -853,9 +844,8 @@ export class UsageFetcher {
|
|
|
853
844
|
): UsageFetchStatus {
|
|
854
845
|
const enabled = config.usageFetchEnabled?.[family] ?? true;
|
|
855
846
|
return statusFromAttempt(
|
|
856
|
-
this.#sharedStore.latestAttempt(providerId, family),
|
|
847
|
+
this.#sharedStore.latestAttempt(providerId, family, this.#now()),
|
|
857
848
|
enabled,
|
|
858
|
-
this.#now(),
|
|
859
849
|
);
|
|
860
850
|
}
|
|
861
851
|
|
|
@@ -1017,25 +1007,20 @@ export class UsageFetcher {
|
|
|
1017
1007
|
// the debounce clause, so `already-answered` and `backoff` returned
|
|
1018
1008
|
// before it ran: an attempt with `observedAtMs` centuries ahead is
|
|
1019
1009
|
// trivially `>= failedAtMs`, and suppressed every failure refresh
|
|
1020
|
-
// forever.
|
|
1021
|
-
//
|
|
1010
|
+
// forever. `latestAttempt(..., nowMs)` now refuses such a record at the
|
|
1011
|
+
// source (#133), which also bounds `nextAttemptAtMs` to one capped rung
|
|
1012
|
+
// past `nowMs` (see the note above `statusFromAttempt`).
|
|
1022
1013
|
//
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
//
|
|
1014
|
+
// The failure trigger is not tied to `observedAtMs` by the store, so a
|
|
1015
|
+
// record observed in the past can still name a failure in the future;
|
|
1016
|
+
// such a marker cannot be honest and must not arm the debounce.
|
|
1026
1017
|
if (
|
|
1027
|
-
attempt.
|
|
1028
|
-
|
|
1029
|
-
attempt.failureTriggeredAtMs > nowMs)
|
|
1018
|
+
attempt.failureTriggeredAtMs !== undefined &&
|
|
1019
|
+
attempt.failureTriggeredAtMs > nowMs
|
|
1030
1020
|
) {
|
|
1031
1021
|
return undefined;
|
|
1032
1022
|
}
|
|
1033
|
-
|
|
1034
|
-
if (
|
|
1035
|
-
attempt.failureCount > 0 &&
|
|
1036
|
-
nextAttemptAtMs !== undefined &&
|
|
1037
|
-
nextAttemptAtMs > nowMs
|
|
1038
|
-
) {
|
|
1023
|
+
if (attempt.failureCount > 0 && attempt.nextAttemptAtMs > nowMs) {
|
|
1039
1024
|
return "backoff";
|
|
1040
1025
|
}
|
|
1041
1026
|
if (attempt.observedAtMs >= failedAtMs) return "already-answered";
|
|
@@ -1070,15 +1055,17 @@ export class UsageFetcher {
|
|
|
1070
1055
|
}
|
|
1071
1056
|
|
|
1072
1057
|
// Step 4/5: durable checks before the lease.
|
|
1058
|
+
const beforeLeaseNowMs = this.#now();
|
|
1073
1059
|
const beforeLease = this.#sharedStore.latestAttempt(
|
|
1074
1060
|
account.providerId,
|
|
1075
1061
|
account.family,
|
|
1062
|
+
beforeLeaseNowMs,
|
|
1076
1063
|
);
|
|
1077
1064
|
if (
|
|
1078
1065
|
this.#failureRefreshSuppressedBy(
|
|
1079
1066
|
beforeLease,
|
|
1080
1067
|
failedAtMs,
|
|
1081
|
-
|
|
1068
|
+
beforeLeaseNowMs,
|
|
1082
1069
|
) !== undefined
|
|
1083
1070
|
) {
|
|
1084
1071
|
return { providerId: account.providerId, status: "not-due" };
|
|
@@ -1098,15 +1085,17 @@ export class UsageFetcher {
|
|
|
1098
1085
|
const leaseGuard: AntigravityLeaseGuard = { lease, handedOff: false };
|
|
1099
1086
|
try {
|
|
1100
1087
|
// Step 7: the same checks again, now that nobody else can write.
|
|
1088
|
+
const underLeaseNowMs = this.#now();
|
|
1101
1089
|
const underLease = this.#sharedStore.latestAttempt(
|
|
1102
1090
|
account.providerId,
|
|
1103
1091
|
account.family,
|
|
1092
|
+
underLeaseNowMs,
|
|
1104
1093
|
);
|
|
1105
1094
|
if (
|
|
1106
1095
|
this.#failureRefreshSuppressedBy(
|
|
1107
1096
|
underLease,
|
|
1108
1097
|
failedAtMs,
|
|
1109
|
-
|
|
1098
|
+
underLeaseNowMs,
|
|
1110
1099
|
) !== undefined
|
|
1111
1100
|
) {
|
|
1112
1101
|
return { providerId: account.providerId, status: "not-due" };
|
|
@@ -1195,6 +1184,7 @@ export class UsageFetcher {
|
|
|
1195
1184
|
const prior = this.#sharedStore.latestAttempt(
|
|
1196
1185
|
account.providerId,
|
|
1197
1186
|
account.family,
|
|
1187
|
+
nowMs,
|
|
1198
1188
|
);
|
|
1199
1189
|
// Backoff is checked BEFORE `disabled`, deliberately.
|
|
1200
1190
|
//
|
|
@@ -1217,12 +1207,7 @@ export class UsageFetcher {
|
|
|
1217
1207
|
// not it was disabled. A retry that fails again simply re-arms the ladder at
|
|
1218
1208
|
// its capped rung, so a genuinely dead endpoint is polled at most once per
|
|
1219
1209
|
// MAX_USAGE_ATTEMPT_DELAY_MS rather than hammered.
|
|
1220
|
-
|
|
1221
|
-
if (
|
|
1222
|
-
prior !== undefined &&
|
|
1223
|
-
priorDeadline !== undefined &&
|
|
1224
|
-
priorDeadline > nowMs
|
|
1225
|
-
) {
|
|
1210
|
+
if (prior !== undefined && prior.nextAttemptAtMs > nowMs) {
|
|
1226
1211
|
await this.#persistWindowGap(
|
|
1227
1212
|
account,
|
|
1228
1213
|
nowMs,
|
|
@@ -1246,21 +1231,17 @@ export class UsageFetcher {
|
|
|
1246
1231
|
}
|
|
1247
1232
|
const leaseGuard: AntigravityLeaseGuard = { lease, handedOff: false };
|
|
1248
1233
|
try {
|
|
1249
|
-
const current = this.#sharedStore.latestAttempt(
|
|
1250
|
-
account.providerId,
|
|
1251
|
-
account.family,
|
|
1252
|
-
);
|
|
1253
1234
|
// Same ordering as the pre-lease check above: an elapsed backoff wins
|
|
1254
1235
|
// over a stale `disabled` flag, so a recovered account can be observed
|
|
1255
1236
|
// again. Re-read under the lease because a peer may have attempted in
|
|
1256
1237
|
// between.
|
|
1257
1238
|
const currentNowMs = this.#now();
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
) {
|
|
1239
|
+
const current = this.#sharedStore.latestAttempt(
|
|
1240
|
+
account.providerId,
|
|
1241
|
+
account.family,
|
|
1242
|
+
currentNowMs,
|
|
1243
|
+
);
|
|
1244
|
+
if (current !== undefined && current.nextAttemptAtMs > currentNowMs) {
|
|
1264
1245
|
await this.#persistWindowGap(
|
|
1265
1246
|
account,
|
|
1266
1247
|
this.#now(),
|
|
@@ -1654,13 +1635,15 @@ export class UsageFetcher {
|
|
|
1654
1635
|
}
|
|
1655
1636
|
// REQ-WINDOW-CADENCE: enforce cross-process 15-minute floor.
|
|
1656
1637
|
// Check durable attempt record to prevent peer-process fetch within the window.
|
|
1638
|
+
const cadenceNowMs = this.#now();
|
|
1657
1639
|
const prior = this.#sharedStore.latestAttempt(
|
|
1658
1640
|
account.providerId,
|
|
1659
1641
|
account.family,
|
|
1642
|
+
cadenceNowMs,
|
|
1660
1643
|
);
|
|
1661
1644
|
if (
|
|
1662
1645
|
prior !== undefined &&
|
|
1663
|
-
|
|
1646
|
+
cadenceNowMs - prior.observedAtMs < WINDOW_SAMPLE_INTERVAL_MS
|
|
1664
1647
|
) {
|
|
1665
1648
|
continue;
|
|
1666
1649
|
}
|