@cortexkit/antigravity-auth-core 3.0.11 → 3.0.13
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/dist/account-selector.d.ts +9 -1
- package/dist/account-selector.d.ts.map +1 -1
- package/dist/account-selector.js +23 -8
- package/dist/account-selector.js.map +1 -1
- package/dist/agy-transport.d.ts.map +1 -1
- package/dist/agy-transport.js +4 -0
- package/dist/agy-transport.js.map +1 -1
- package/dist/common-auth-embedded/claustrum/consumer.d.ts +83 -2
- package/dist/common-auth-embedded/claustrum/consumer.js +337 -37
- package/dist/common-auth-embedded/claustrum/custody.d.ts +36 -1
- package/dist/common-auth-embedded/claustrum/custody.js +95 -11
- package/dist/common-auth-embedded/claustrum/enrollment.js +37 -12
- package/dist/common-auth-embedded/claustrum/errors.d.ts +1 -1
- package/dist/common-auth-embedded/claustrum/hub.d.ts +195 -0
- package/dist/common-auth-embedded/claustrum/hub.js +988 -0
- package/dist/common-auth-embedded/claustrum/index.d.ts +13 -3
- package/dist/common-auth-embedded/claustrum/index.js +37 -4
- package/dist/common-auth-embedded/claustrum/quota-writer.d.ts +8 -1
- package/dist/common-auth-embedded/claustrum/quota-writer.js +38 -66
- package/dist/common-auth-embedded/claustrum/roster.d.ts +80 -1
- package/dist/common-auth-embedded/claustrum/roster.js +259 -12
- package/dist/common-auth-embedded/claustrum/shutdown.js +42 -0
- package/dist/common-auth-embedded/commands/builtins.d.ts +37 -0
- package/dist/common-auth-embedded/commands/builtins.js +95 -13
- package/dist/common-auth-embedded/commands/index.d.ts +1 -1
- package/dist/common-auth-embedded/fs/file-read-cache.js +57 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +20 -11
- package/dist/common-auth-embedded/quota/commit-interval.d.ts +9 -0
- package/dist/common-auth-embedded/quota/commit-interval.js +18 -0
- package/dist/common-auth-embedded/quota/pending.js +48 -0
- package/dist/common-auth-embedded/source-output.json +152 -88
- package/dist/common-auth-embedded/store/attribution.d.ts +5 -0
- package/dist/common-auth-embedded/store/attribution.js +39 -25
- package/dist/common-auth-embedded/store/mutate.d.ts +2 -0
- package/dist/common-auth-embedded/store/mutate.js +4 -0
- package/dist/common-auth-embedded/store/pool.d.ts +22 -0
- package/dist/common-auth-embedded/store/pool.js +18 -3
- package/dist/common-auth-embedded/store/pull.js +3 -1
- package/dist/common-auth-embedded/store/quota-writer.js +312 -0
- package/dist/common-auth-embedded/store/runtime.d.ts +2 -0
- package/dist/common-auth-embedded/store/settings.d.ts +1 -1
- package/dist/common-auth-embedded/store/settings.js +16 -6
- package/dist/model-registry.d.ts +2 -2
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +6 -4
- package/dist/model-registry.js.map +1 -1
- package/dist/pre-response-deadline.d.ts.map +1 -1
- package/dist/pre-response-deadline.js +6 -5
- package/dist/pre-response-deadline.js.map +1 -1
- package/dist/quota-manager.d.ts +1 -1
- package/dist/quota-manager.d.ts.map +1 -1
- package/dist/quota-manager.js +12 -4
- package/dist/quota-manager.js.map +1 -1
- package/dist/quota-presentation.d.ts.map +1 -1
- package/dist/quota-presentation.js +8 -0
- package/dist/quota-presentation.js.map +1 -1
- package/dist/quota-types.d.ts +1 -1
- package/dist/quota-types.d.ts.map +1 -1
- package/dist/quota-types.js +5 -0
- package/dist/quota-types.js.map +1 -1
- package/dist/vault-account-source.d.ts +6 -1
- package/dist/vault-account-source.d.ts.map +1 -1
- package/dist/vault-account-source.js +9 -0
- package/dist/vault-account-source.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
import { PoolOperationError } from './errors.js';
|
|
2
|
-
import { toFailure, withTransaction } from './mutate.js';
|
|
2
|
+
import { toFailure, withTransaction, } from './mutate.js';
|
|
3
3
|
import { LockStack } from './refresh-lock.js';
|
|
4
4
|
import { refusal, requireBound, unknownRow, } from './runtime.js';
|
|
5
5
|
import { isCredentialEpoch } from './schema.js';
|
|
6
|
+
export function assertQuotaEpoch(id, attribution) {
|
|
7
|
+
if (!isCredentialEpoch(attribution?.credentialEpoch))
|
|
8
|
+
throw refusal('pull', id, 'invalid-input', 'the credential epoch the reading was issued for must be a positive safe integer');
|
|
9
|
+
}
|
|
10
|
+
// Shared by preflight and the locked commit: accepting into memory must never
|
|
11
|
+
// hide a refusal the immediate writer would have returned to its caller.
|
|
12
|
+
export function assertQuotaRow(id, attribution, row) {
|
|
13
|
+
if (!row)
|
|
14
|
+
throw unknownRow('pull', id);
|
|
15
|
+
if (row.invalid)
|
|
16
|
+
throw refusal('pull', id, 'invalid-row', `row ${id} is invalid`);
|
|
17
|
+
if (!row.credential)
|
|
18
|
+
throw refusal('pull', id, 'no-credential', `row ${id} holds no credential`);
|
|
19
|
+
requireBound('pull', row);
|
|
20
|
+
if (row.torn ||
|
|
21
|
+
!row.hasEntry ||
|
|
22
|
+
row.credentialEpoch !== attribution.credentialEpoch ||
|
|
23
|
+
row.identity !== attribution.identity)
|
|
24
|
+
throw new PoolOperationError({
|
|
25
|
+
operation: 'pull',
|
|
26
|
+
rowId: id,
|
|
27
|
+
phase: 'pull',
|
|
28
|
+
retryable: true,
|
|
29
|
+
kind: 'attribution',
|
|
30
|
+
message: `quota for ${id} was issued for a credential the row no longer holds`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function mergedQuota(ctx, id, stored, observation) {
|
|
34
|
+
const merged = ctx.codec.merge(stored, observation);
|
|
35
|
+
if (!ctx.codec.validate(merged))
|
|
36
|
+
throw refusal('pull', id, 'invalid-quota', 'the quota codec rejected the merged map');
|
|
37
|
+
return merged;
|
|
38
|
+
}
|
|
6
39
|
/**
|
|
7
40
|
* Merges a quota observation into a row's stored map under the store locks,
|
|
8
41
|
* after attribution passes. Needs no row lock, so it is permitted from
|
|
@@ -13,37 +46,18 @@ export async function recordQuota(rt, id, attribution, observation) {
|
|
|
13
46
|
const locks = new LockStack(ctx.lockDefaults, ctx.lockEnv);
|
|
14
47
|
const progress = { writes: 0 };
|
|
15
48
|
try {
|
|
16
|
-
|
|
17
|
-
throw refusal('pull', id, 'invalid-input', 'the credential epoch the reading was issued for must be a positive safe integer');
|
|
49
|
+
assertQuotaEpoch(id, attribution);
|
|
18
50
|
await withTransaction(ctx, locks, progress, { operation: 'pull', rowId: id }, async (tx) => {
|
|
19
51
|
tx.assertNotStaged(id);
|
|
20
52
|
const row = tx.row(id);
|
|
21
|
-
if (!row)
|
|
22
|
-
throw unknownRow('pull', id);
|
|
23
|
-
if (row.invalid)
|
|
24
|
-
throw refusal('pull', id, 'invalid-row', `row ${id} is invalid`);
|
|
25
53
|
// A reading belongs to one (epoch, identity, credential). A row
|
|
26
54
|
// holding no credential, or torn between the writes of a replace,
|
|
27
55
|
// has no such triple on disk, so no reading is recorded for it.
|
|
28
|
-
|
|
29
|
-
throw refusal('pull', id, 'no-credential', `row ${id} holds no credential`);
|
|
30
|
-
requireBound('pull', row);
|
|
56
|
+
assertQuotaRow(id, attribution, row);
|
|
31
57
|
const entry = tx.entry(id);
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
row.identity !== attribution.identity)
|
|
36
|
-
throw new PoolOperationError({
|
|
37
|
-
operation: 'pull',
|
|
38
|
-
rowId: id,
|
|
39
|
-
phase: 'pull',
|
|
40
|
-
retryable: true,
|
|
41
|
-
kind: 'attribution',
|
|
42
|
-
message: `quota for ${id} was issued for a credential the row no longer holds`,
|
|
43
|
-
});
|
|
44
|
-
const merged = ctx.codec.merge(entry.quota, observation);
|
|
45
|
-
if (!ctx.codec.validate(merged))
|
|
46
|
-
throw refusal('pull', id, 'invalid-quota', 'the quota codec rejected the merged map');
|
|
58
|
+
if (!entry)
|
|
59
|
+
throw unknownRow('pull', id);
|
|
60
|
+
const merged = mergedQuota(ctx, id, entry.quota, observation);
|
|
47
61
|
tx.setEntry(id, { ...entry, quota: merged, needsFirstReading: false });
|
|
48
62
|
await tx.commitConfig();
|
|
49
63
|
},
|
|
@@ -52,6 +52,8 @@ export type ReadResult = ({
|
|
|
52
52
|
kind?: 'snapshot-contended';
|
|
53
53
|
retryable?: true;
|
|
54
54
|
};
|
|
55
|
+
/** Settings need only the config classification, not a joined roster snapshot. */
|
|
56
|
+
export declare function readPoolConfig(path: string): Promise<import("./schema.js").ConfigClassification>;
|
|
55
57
|
/**
|
|
56
58
|
* Validate config/state/config bytes without locks. Roster replacement writes
|
|
57
59
|
* config before deleting old state credentials. Re-read a changed config so an
|
|
@@ -28,6 +28,10 @@ function parseFile(text) {
|
|
|
28
28
|
return { exists: true, parseError };
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/** Settings need only the config classification, not a joined roster snapshot. */
|
|
32
|
+
export async function readPoolConfig(path) {
|
|
33
|
+
return classifyConfig(parseFile(await readText(path)));
|
|
34
|
+
}
|
|
31
35
|
/**
|
|
32
36
|
* Validate config/state/config bytes without locks. Roster replacement writes
|
|
33
37
|
* config before deleting old state credentials. Re-read a changed config so an
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { QuotaCommitTimer } from '../quota/commit-interval.js';
|
|
1
2
|
import { type Attribution } from './attribution.js';
|
|
2
3
|
import type { PoolOperationError } from './errors.js';
|
|
3
4
|
import type { PoolLogger } from './hooks.js';
|
|
@@ -16,6 +17,23 @@ export interface OpenPoolStoreOptions {
|
|
|
16
17
|
configPath: string;
|
|
17
18
|
statePath: string;
|
|
18
19
|
quota: QuotaCodec;
|
|
20
|
+
/**
|
|
21
|
+
* Opt in to detached, coalesced quota publication, with any `quota` codec.
|
|
22
|
+
* Each accepted observation is queued whole for its row and binding, and
|
|
23
|
+
* the queue is applied in arrival order through `quota.merge` under the
|
|
24
|
+
* store locks, so the codec's own merge decides which fields a newer or
|
|
25
|
+
* older reading replaces. Accepted readings appear in read/load
|
|
26
|
+
* immediately, merged the same way; the returned promise still rejects
|
|
27
|
+
* current-row refusals before queueing. Await dispose or close at shutdown
|
|
28
|
+
* to attempt a bounded drain; readings still pending at its timeout are
|
|
29
|
+
* discarded. Normal quota commits are at least 30 seconds apart; a full
|
|
30
|
+
* queue commits early so response handling never waits for that interval.
|
|
31
|
+
*/
|
|
32
|
+
coalesceQuota?: boolean;
|
|
33
|
+
/** Bounded quota drain on close/dispose, in milliseconds; defaults to 1,000. */
|
|
34
|
+
quotaDrainTimeoutMs?: number;
|
|
35
|
+
/** Monotonic clock and scheduling seam for detached quota commits. */
|
|
36
|
+
quotaCommitTimer?: QuotaCommitTimer;
|
|
19
37
|
/**
|
|
20
38
|
* The codec of the provider state kept beside each row's credential (see
|
|
21
39
|
* `ProviderStateCodec`). Without it no row shows a provider state, and
|
|
@@ -189,6 +207,10 @@ export interface PoolStore {
|
|
|
189
207
|
}>;
|
|
190
208
|
refresh(id: string, provider: ProviderRefresh, options?: RefreshOptions): Promise<RefreshOutcome>;
|
|
191
209
|
recordQuota(id: string, attribution: Attribution, observation: unknown): Promise<void>;
|
|
210
|
+
/** Drain opted-in quota publication; subsequent coalesced readings refuse. Idempotent. */
|
|
211
|
+
close(): Promise<void>;
|
|
212
|
+
/** Same bounded quota drain as close(). Does not wait for provider pull hooks. */
|
|
213
|
+
dispose(): Promise<void>;
|
|
192
214
|
/** Fires a pull for a row admission refused for want of a reading. */
|
|
193
215
|
requestReading(id: string): void;
|
|
194
216
|
/** Resolves once every pull fired so far has settled. */
|
|
@@ -3,6 +3,7 @@ import { initializePool, readPool, } from './mutate.js';
|
|
|
3
3
|
import { updateProviderStateRow, } from './provider-state.js';
|
|
4
4
|
import { publication, publishRoster, } from './publication.js';
|
|
5
5
|
import { PullScheduler } from './pull.js';
|
|
6
|
+
import { PoolQuotaWriter } from './quota-writer.js';
|
|
6
7
|
import { refreshRow, } from './refresh.js';
|
|
7
8
|
import { POOL_LOCK_DEFAULTS, } from './refresh-lock.js';
|
|
8
9
|
import { addRow, disableRow, enableRow, recordRowIdentity, removeRow, reorderRows, replaceRow, rotateRow, } from './rows.js';
|
|
@@ -80,9 +81,21 @@ export function openPoolStore(options) {
|
|
|
80
81
|
rowLockOptions: options.rowLockOptions ?? {},
|
|
81
82
|
firePull: (id, reason) => pulls.fire(id, reason),
|
|
82
83
|
};
|
|
84
|
+
const quotaWriter = options.coalesceQuota
|
|
85
|
+
? new PoolQuotaWriter(rt, options.quotaDrainTimeoutMs, options.quotaCommitTimer)
|
|
86
|
+
: undefined;
|
|
87
|
+
const record = (id, attribution, observation) => quotaWriter
|
|
88
|
+
? quotaWriter.record(id, attribution, observation)
|
|
89
|
+
: recordQuota(rt, id, attribution, observation);
|
|
90
|
+
rt.publishQuota = record;
|
|
91
|
+
const overlay = (result) => {
|
|
92
|
+
if (result.status === 'ready' && quotaWriter)
|
|
93
|
+
return { ...result, rows: quotaWriter.overlay(result.rows) };
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
83
96
|
return {
|
|
84
97
|
async load() {
|
|
85
|
-
const result = await readPool(ctx);
|
|
98
|
+
const result = overlay(await readPool(ctx));
|
|
86
99
|
if (result.status === 'ready') {
|
|
87
100
|
// A torn row is no candidate, but its pull is fired too: the pull
|
|
88
101
|
// completes the row on disk before reading it, so an interrupted
|
|
@@ -96,7 +109,7 @@ export function openPoolStore(options) {
|
|
|
96
109
|
return toLoad(result);
|
|
97
110
|
},
|
|
98
111
|
async read() {
|
|
99
|
-
return toLoad(await readPool(ctx));
|
|
112
|
+
return toLoad(overlay(await readPool(ctx)));
|
|
100
113
|
},
|
|
101
114
|
async initialize(input = {}) {
|
|
102
115
|
return { status: await initializePool(ctx, input.dropKeys ?? []) };
|
|
@@ -115,7 +128,9 @@ export function openPoolStore(options) {
|
|
|
115
128
|
updateSettings: (mutator, callOptions) => updatePoolSettings(rt, mutator, callOptions),
|
|
116
129
|
recordIdentity: (id, identity, attribution, callOptions) => recordRowIdentity(rt, id, identity, attribution, callOptions),
|
|
117
130
|
refresh: (id, provider, callOptions) => refreshRow(rt, id, provider, callOptions),
|
|
118
|
-
recordQuota:
|
|
131
|
+
recordQuota: record,
|
|
132
|
+
close: () => quotaWriter?.close() ?? Promise.resolve(),
|
|
133
|
+
dispose: () => quotaWriter?.close() ?? Promise.resolve(),
|
|
119
134
|
requestReading: (id) => pulls.fire(id, 'admission'),
|
|
120
135
|
pullsSettled: () => pulls.settled(),
|
|
121
136
|
};
|
|
@@ -105,7 +105,9 @@ export class PullScheduler {
|
|
|
105
105
|
cause,
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
const publish = rt.publishQuota ??
|
|
109
|
+
((rowId, attribution, value) => recordQuota(rt, rowId, attribution, value));
|
|
110
|
+
await publish(id, {
|
|
109
111
|
credentialEpoch: request.credentialEpoch,
|
|
110
112
|
...(request.identity !== undefined
|
|
111
113
|
? { identity: request.identity }
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { createLogger } from '../logger/index.js';
|
|
2
|
+
import { QUOTA_COMMIT_INTERVAL_MS, quotaCommitTimer, } from '../quota/commit-interval.js';
|
|
3
|
+
import { assertQuotaEpoch, assertQuotaRow, mergedQuota, } from './attribution.js';
|
|
4
|
+
import { PoolOperationError } from './errors.js';
|
|
5
|
+
import { toFailure, withTransaction } from './mutate.js';
|
|
6
|
+
import { LockStack } from './refresh-lock.js';
|
|
7
|
+
import { readRow, refusal } from './runtime.js';
|
|
8
|
+
import { entryIn } from './schema.js';
|
|
9
|
+
const COALESCE_MS = 100;
|
|
10
|
+
/**
|
|
11
|
+
* The most readings one row keeps waiting for a flush. Hosts record a reading
|
|
12
|
+
* about every 100 ms at most, so for them a queue this long means another
|
|
13
|
+
* writer has held the store locks for many seconds. Reaching it starts
|
|
14
|
+
* a flush at once instead of waiting for the coalescing timer or a retry
|
|
15
|
+
* backoff; a further reading for the row waits for room (never dropping an
|
|
16
|
+
* older one) for as long as the store's normal lock budget, and is then
|
|
17
|
+
* refused with `lock-contention`, as the immediate writer would refuse it.
|
|
18
|
+
*/
|
|
19
|
+
export const POOL_QUOTA_QUEUE_LIMIT = 128;
|
|
20
|
+
// Readings are queued whole per row and applied at flush in arrival order
|
|
21
|
+
// through the codec's own merge, so the codec alone decides which fields a
|
|
22
|
+
// newer or older reading replaces. Like the roster writer, a reading stays
|
|
23
|
+
// queued until the flush that took it commits: one queued while a flush waits
|
|
24
|
+
// for its locks must survive that flush.
|
|
25
|
+
export class PoolQuotaWriter {
|
|
26
|
+
rt;
|
|
27
|
+
timeoutMs;
|
|
28
|
+
clock;
|
|
29
|
+
pending = new Map();
|
|
30
|
+
warned = new Set();
|
|
31
|
+
wake = new Set();
|
|
32
|
+
logger;
|
|
33
|
+
timer;
|
|
34
|
+
committedAt;
|
|
35
|
+
running;
|
|
36
|
+
closing;
|
|
37
|
+
closed = false;
|
|
38
|
+
stopped = false;
|
|
39
|
+
failures = 0;
|
|
40
|
+
constructor(rt, timeoutMs = 1_000, clock = quotaCommitTimer) {
|
|
41
|
+
this.rt = rt;
|
|
42
|
+
this.timeoutMs = timeoutMs;
|
|
43
|
+
this.clock = clock;
|
|
44
|
+
this.logger = rt.ctx.logger ?? createLogger('store');
|
|
45
|
+
}
|
|
46
|
+
async record(id, attribution, observation) {
|
|
47
|
+
try {
|
|
48
|
+
this.assertOpen(id);
|
|
49
|
+
assertQuotaEpoch(id, attribution);
|
|
50
|
+
// Snapshot before the first await: callers may reuse their headers object.
|
|
51
|
+
const publication = structuredClone({
|
|
52
|
+
id,
|
|
53
|
+
attribution,
|
|
54
|
+
observation,
|
|
55
|
+
});
|
|
56
|
+
const { row, result } = await readRow(this.rt, 'pull', id);
|
|
57
|
+
assertQuotaRow(id, publication.attribution, row);
|
|
58
|
+
// Merge into what read() shows now, so a reading the codec refuses is
|
|
59
|
+
// refused to its caller here, as the immediate writer would refuse it.
|
|
60
|
+
mergedQuota(this.rt.ctx, id, this.overlayRow({ ...row, quota: entryIn(result.config, id)?.quota })
|
|
61
|
+
.quota, publication.observation);
|
|
62
|
+
await this.room(id);
|
|
63
|
+
this.assertOpen(id);
|
|
64
|
+
// No await between the room check and the push, so the cap holds.
|
|
65
|
+
const queue = this.pending.get(id) ?? [];
|
|
66
|
+
queue.push(publication);
|
|
67
|
+
this.pending.set(id, queue);
|
|
68
|
+
if (queue.length >= POOL_QUOTA_QUEUE_LIMIT)
|
|
69
|
+
this.flushNow();
|
|
70
|
+
else
|
|
71
|
+
this.schedule(COALESCE_MS);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw toFailure(error, 'pull', id, { writes: 0 });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
queued(id) {
|
|
78
|
+
return this.pending.get(id)?.length ?? 0;
|
|
79
|
+
}
|
|
80
|
+
// Waits while the row's queue is full, flushing at once on every wake-up,
|
|
81
|
+
// for at most the store's own lock acquisition budget.
|
|
82
|
+
async room(id) {
|
|
83
|
+
if (this.queued(id) < POOL_QUOTA_QUEUE_LIMIT)
|
|
84
|
+
return;
|
|
85
|
+
const { ctx } = this.rt;
|
|
86
|
+
const budget = Math.max(0, ...ctx.storeLocks.map((spec) => spec.timeoutMs ?? ctx.lockDefaults.timeoutMs));
|
|
87
|
+
const deadline = performance.now() + budget;
|
|
88
|
+
while (this.queued(id) >= POOL_QUOTA_QUEUE_LIMIT) {
|
|
89
|
+
this.assertOpen(id);
|
|
90
|
+
this.flushNow();
|
|
91
|
+
const left = deadline - performance.now();
|
|
92
|
+
if (left <= 0)
|
|
93
|
+
throw refusal('pull', id, 'lock-contention', `pool quota queue for ${id} is full (${POOL_QUOTA_QUEUE_LIMIT} readings) and the store locks stayed held`, true);
|
|
94
|
+
await new Promise((resolve) => {
|
|
95
|
+
const done = () => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
this.wake.delete(done);
|
|
98
|
+
resolve();
|
|
99
|
+
};
|
|
100
|
+
const timer = setTimeout(done, left);
|
|
101
|
+
this.wake.add(done);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
assertOpen(id) {
|
|
106
|
+
if (this.closed)
|
|
107
|
+
throw refusal('pull', id, 'invalid-input', 'pool quota writer is closed');
|
|
108
|
+
}
|
|
109
|
+
// What read() and load() show: the stored map, then each queued reading of
|
|
110
|
+
// the row still bound to it, in arrival order through the codec's merge.
|
|
111
|
+
// A reading the codec now refuses is left out, as the flush drops it.
|
|
112
|
+
overlayRow(row) {
|
|
113
|
+
let next = row;
|
|
114
|
+
for (const publication of this.pending.get(row.id) ?? []) {
|
|
115
|
+
try {
|
|
116
|
+
assertQuotaRow(row.id, publication.attribution, row);
|
|
117
|
+
next = {
|
|
118
|
+
...next,
|
|
119
|
+
quota: mergedQuota(this.rt.ctx, row.id, next.quota, publication.observation),
|
|
120
|
+
needsFirstReading: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch { }
|
|
124
|
+
}
|
|
125
|
+
return next;
|
|
126
|
+
}
|
|
127
|
+
overlay(rows) {
|
|
128
|
+
return rows.map((row) => this.overlayRow(row));
|
|
129
|
+
}
|
|
130
|
+
schedule(delay) {
|
|
131
|
+
if (this.running || this.timer || !this.pending.size || this.stopped)
|
|
132
|
+
return;
|
|
133
|
+
const remaining = this.closed
|
|
134
|
+
? 0
|
|
135
|
+
: (this.committedAt ?? -Infinity) +
|
|
136
|
+
QUOTA_COMMIT_INTERVAL_MS -
|
|
137
|
+
this.clock.now();
|
|
138
|
+
this.timer = this.clock.schedule(Math.max(delay, remaining), () => {
|
|
139
|
+
this.timer = undefined;
|
|
140
|
+
return this.flush();
|
|
141
|
+
});
|
|
142
|
+
if (!this.closed)
|
|
143
|
+
this.timer.unref?.();
|
|
144
|
+
}
|
|
145
|
+
cancelTimer() {
|
|
146
|
+
this.timer?.cancel();
|
|
147
|
+
this.timer = undefined;
|
|
148
|
+
}
|
|
149
|
+
// A full queue overrides the interval: response handling must not wait for
|
|
150
|
+
// a quota commit slot. A flush already running is left to finish.
|
|
151
|
+
flushNow() {
|
|
152
|
+
if (this.running)
|
|
153
|
+
return;
|
|
154
|
+
this.cancelTimer();
|
|
155
|
+
this.flush();
|
|
156
|
+
}
|
|
157
|
+
flush() {
|
|
158
|
+
if (this.running || this.stopped || !this.pending.size)
|
|
159
|
+
return this.running ?? Promise.resolve();
|
|
160
|
+
// Every queued reading, each row's in arrival order.
|
|
161
|
+
const batch = [...this.pending.values()].flat();
|
|
162
|
+
// Short lock attempts allow retries to pick up freshly queued readings and
|
|
163
|
+
// bound shutdown even when the normal store acquisition budget is long.
|
|
164
|
+
const ctx = {
|
|
165
|
+
...this.rt.ctx,
|
|
166
|
+
storeLocks: this.rt.ctx.storeLocks.map((spec) => ({
|
|
167
|
+
...spec,
|
|
168
|
+
timeoutMs: Math.min(spec.timeoutMs ?? this.rt.ctx.lockDefaults.timeoutMs, 100),
|
|
169
|
+
})),
|
|
170
|
+
};
|
|
171
|
+
const locks = new LockStack(ctx.lockDefaults, ctx.lockEnv);
|
|
172
|
+
this.running = (async () => {
|
|
173
|
+
try {
|
|
174
|
+
await withTransaction(ctx, locks, { writes: 0 }, { operation: 'pull', rowId: undefined }, async (tx) => {
|
|
175
|
+
if (this.stopped)
|
|
176
|
+
return;
|
|
177
|
+
let changed = false;
|
|
178
|
+
for (const publication of batch) {
|
|
179
|
+
const { id, attribution, observation } = publication;
|
|
180
|
+
const witness = JSON.stringify([
|
|
181
|
+
id,
|
|
182
|
+
attribution.credentialEpoch,
|
|
183
|
+
attribution.identity,
|
|
184
|
+
]);
|
|
185
|
+
try {
|
|
186
|
+
tx.assertNotStaged(id);
|
|
187
|
+
assertQuotaRow(id, attribution, tx.row(id));
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (!(error instanceof PoolOperationError))
|
|
191
|
+
throw error;
|
|
192
|
+
if (!this.warned.has(witness)) {
|
|
193
|
+
this.warned.add(witness);
|
|
194
|
+
this.warn('pool quota publication dropped; account binding changed', { id, kind: error.kind });
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const entry = tx.entry(id);
|
|
199
|
+
if (!entry)
|
|
200
|
+
continue;
|
|
201
|
+
let quota;
|
|
202
|
+
try {
|
|
203
|
+
quota = mergedQuota(ctx, id, entry.quota, observation);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
// The codec accepted this reading when it was queued, but not
|
|
207
|
+
// against what is stored now (another writer changed it).
|
|
208
|
+
// Drop only this reading: failing the transaction would retry
|
|
209
|
+
// the same refusal forever and hold back every other row.
|
|
210
|
+
const key = `codec:${witness}`;
|
|
211
|
+
if (!this.warned.has(key)) {
|
|
212
|
+
this.warned.add(key);
|
|
213
|
+
this.warn('pool quota publication dropped; the quota codec refused it', {
|
|
214
|
+
id,
|
|
215
|
+
error: error instanceof Error ? error.message : String(error),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
tx.setEntry(id, { ...entry, quota, needsFirstReading: false });
|
|
221
|
+
changed = true;
|
|
222
|
+
}
|
|
223
|
+
if (changed && !this.stopped) {
|
|
224
|
+
await tx.commitConfig();
|
|
225
|
+
this.committedAt = this.clock.now();
|
|
226
|
+
}
|
|
227
|
+
}, { completeTorn: false });
|
|
228
|
+
const flushed = new Set(batch);
|
|
229
|
+
for (const [id, queue] of this.pending) {
|
|
230
|
+
const rest = queue.filter((publication) => !flushed.has(publication));
|
|
231
|
+
if (rest.length)
|
|
232
|
+
this.pending.set(id, rest);
|
|
233
|
+
else
|
|
234
|
+
this.pending.delete(id);
|
|
235
|
+
}
|
|
236
|
+
this.failures = 0;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (!this.stopped && this.failures++ === 0)
|
|
240
|
+
this.warn('pool quota publication delayed; retrying', {
|
|
241
|
+
error: error instanceof Error ? error.message : String(error),
|
|
242
|
+
});
|
|
243
|
+
// Retain the mailbox on failure, including fresher readings queued meanwhile.
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
await locks.releaseAll();
|
|
247
|
+
this.running = undefined;
|
|
248
|
+
for (const wake of this.wake)
|
|
249
|
+
wake();
|
|
250
|
+
const delay = this.failures
|
|
251
|
+
? Math.min(5_000, 100 * 2 ** Math.min(this.failures, 6)) *
|
|
252
|
+
(0.5 + Math.random())
|
|
253
|
+
: COALESCE_MS;
|
|
254
|
+
this.schedule(delay);
|
|
255
|
+
}
|
|
256
|
+
})();
|
|
257
|
+
return this.running;
|
|
258
|
+
}
|
|
259
|
+
warn(message, data) {
|
|
260
|
+
try {
|
|
261
|
+
this.logger.warn(message, data);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
/* Logging cannot stop publication. */
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
close() {
|
|
268
|
+
if (this.closing)
|
|
269
|
+
return this.closing;
|
|
270
|
+
this.closed = true;
|
|
271
|
+
this.closing = this.drain();
|
|
272
|
+
return this.closing;
|
|
273
|
+
}
|
|
274
|
+
async drain() {
|
|
275
|
+
this.cancelTimer();
|
|
276
|
+
let deadline;
|
|
277
|
+
let wake;
|
|
278
|
+
const drained = new Promise((done) => {
|
|
279
|
+
wake = () => {
|
|
280
|
+
if (!this.pending.size)
|
|
281
|
+
done();
|
|
282
|
+
};
|
|
283
|
+
this.wake.add(wake);
|
|
284
|
+
wake();
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
this.flush();
|
|
288
|
+
await Promise.race([
|
|
289
|
+
drained,
|
|
290
|
+
new Promise((done) => {
|
|
291
|
+
deadline = setTimeout(done, Math.max(0, this.timeoutMs));
|
|
292
|
+
}),
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
this.stopped = true;
|
|
297
|
+
clearTimeout(deadline);
|
|
298
|
+
this.cancelTimer();
|
|
299
|
+
this.wake.delete(wake);
|
|
300
|
+
if (this.pending.size)
|
|
301
|
+
this.warn('pool quota drain timed out; pending readings left', {
|
|
302
|
+
accounts: this.pending.size,
|
|
303
|
+
readings: [...this.pending.values()].reduce((sum, queue) => sum + queue.length, 0),
|
|
304
|
+
});
|
|
305
|
+
this.pending.clear();
|
|
306
|
+
// A reading waiting for room in a full queue is refused as closed when
|
|
307
|
+
// the drain ends, not when its own lock budget runs out.
|
|
308
|
+
for (const wake of this.wake)
|
|
309
|
+
wake();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Attribution } from './attribution.js';
|
|
1
2
|
import { type PoolOperation, PoolOperationError } from './errors.js';
|
|
2
3
|
import { type ReadResult, type StoreContext } from './mutate.js';
|
|
3
4
|
import type { PoolLockOptions, PoolLockSpec } from './refresh-lock.js';
|
|
@@ -10,6 +11,7 @@ export interface StoreRuntime {
|
|
|
10
11
|
providerLock: PoolLockSpec;
|
|
11
12
|
rowLockOptions: Partial<PoolLockOptions>;
|
|
12
13
|
firePull(id: string, reason: PullReason): void;
|
|
14
|
+
publishQuota?: (id: string, attribution: Attribution, observation: unknown) => Promise<void>;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* The row lock's (name, path): keyed by the row's recorded wire identity when
|
|
@@ -50,7 +50,7 @@ export type UpdateSettingsResult = {
|
|
|
50
50
|
/** `unchanged` when the mutator left the settings as they were; nothing was written. */
|
|
51
51
|
outcome: 'updated' | 'unchanged';
|
|
52
52
|
};
|
|
53
|
-
/** Reads the settings without taking a lock or writing anything. */
|
|
53
|
+
/** Reads only the config's settings, without taking a lock or writing anything. */
|
|
54
54
|
export declare function readPoolSettings(rt: StoreRuntime): Promise<SettingsRead>;
|
|
55
55
|
/**
|
|
56
56
|
* One locked read-modify-write of the plugin's settings in the config file
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
2
|
import { writeJsonAtomic } from '../fs/atomic-write.js';
|
|
3
|
+
import { FileReadCache } from '../fs/file-read-cache.js';
|
|
4
|
+
import { windowsPrivateFiles } from '../fs/private-files.js';
|
|
3
5
|
import { PoolOperationError } from './errors.js';
|
|
4
6
|
import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
5
|
-
import { notReadyError, readPool, runOperation } from './mutate.js';
|
|
7
|
+
import { notReadyError, readPool, readPoolConfig, runOperation, } from './mutate.js';
|
|
6
8
|
import { isRecord, POOL_KEY } from './schema.js';
|
|
7
9
|
/**
|
|
8
10
|
* Top-level config keys the pool owns: the legacy `version`, the legacy
|
|
@@ -58,12 +60,20 @@ function settingsRefusal(message) {
|
|
|
58
60
|
message,
|
|
59
61
|
});
|
|
60
62
|
}
|
|
61
|
-
|
|
63
|
+
const settingsCache = new FileReadCache();
|
|
64
|
+
/** Reads only the config's settings, without taking a lock or writing anything. */
|
|
62
65
|
export async function readPoolSettings(rt) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const read = async ([configPath = rt.ctx.configPath,] = []) => {
|
|
67
|
+
const result = await readPoolConfig(configPath);
|
|
68
|
+
if (result.status === 'error')
|
|
69
|
+
return { status: 'error', file: 'config', reason: result.reason };
|
|
70
|
+
return { status: result.status, settings: settingsOf(result.config) };
|
|
71
|
+
};
|
|
72
|
+
// Native Windows security checks cannot be replaced by a stat fingerprint.
|
|
73
|
+
const result = windowsPrivateFiles
|
|
74
|
+
? await read()
|
|
75
|
+
: await settingsCache.read([rt.ctx.configPath], read);
|
|
76
|
+
return structuredClone(result);
|
|
67
77
|
}
|
|
68
78
|
/**
|
|
69
79
|
* One locked read-modify-write of the plugin's settings in the config file
|
package/dist/model-registry.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface ModelLimit {
|
|
|
14
14
|
output: number;
|
|
15
15
|
}
|
|
16
16
|
export type ModelModality = 'text' | 'image' | 'pdf';
|
|
17
|
-
export type ModelQuotaGroup = 'gemini' | 'non-gemini';
|
|
17
|
+
export type ModelQuotaGroup = 'gemini' | 'non-gemini' | 'gemini-argon';
|
|
18
18
|
export interface ModelModalities {
|
|
19
19
|
input: ModelModality[];
|
|
20
20
|
output: ModelModality[];
|
|
@@ -61,7 +61,7 @@ export declare function getGemini35FlashAntigravityModel(tier?: ThinkingTier): s
|
|
|
61
61
|
export declare function getGemini36FlashAntigravityModel(tier?: ThinkingTier): string;
|
|
62
62
|
export declare function getGemini37FlashAntigravityModel(tier?: ThinkingTier): string;
|
|
63
63
|
export declare function getGemini38FlashAntigravityModel(tier?: ThinkingTier): string;
|
|
64
|
-
export declare function getQuotaGroupForModel(modelId: string): ModelQuotaGroup | undefined;
|
|
64
|
+
export declare function getQuotaGroupForModel(modelId: string, hasArgonPool?: boolean): ModelQuotaGroup | undefined;
|
|
65
65
|
/** Recognize Argon selectors so family-specific wire and quota policy is consistent. */
|
|
66
66
|
export declare function isGeminiArgonModel(model: string): boolean;
|
|
67
67
|
export declare function getGeminiArgonAntigravityModel(tier?: ThinkingTier): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../src/model-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEtE,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,EAAE,kBAAkB,CAAA;IAClC,cAAc,CAAC,EAAE,mBAAmB,CAAA;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAA;AACpD,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../src/model-registry.ts"],"names":[],"mappings":"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"}
|
package/dist/model-registry.js
CHANGED
|
@@ -395,12 +395,14 @@ export function getGemini37FlashAntigravityModel(tier) {
|
|
|
395
395
|
export function getGemini38FlashAntigravityModel(tier) {
|
|
396
396
|
return getTieredAntigravityModel(GEMINI_38_FLASH_ROUTES, tier);
|
|
397
397
|
}
|
|
398
|
-
export function getQuotaGroupForModel(modelId) {
|
|
398
|
+
export function getQuotaGroupForModel(modelId, hasArgonPool = false) {
|
|
399
399
|
const normalized = modelId.toLowerCase();
|
|
400
|
-
// Argon per-model quota is retained separately: the
|
|
401
|
-
//
|
|
400
|
+
// Argon per-model quota is retained separately: the model alone does not
|
|
401
|
+
// identify an Argon weekly pool, so without evidence that this account
|
|
402
|
+
// reports one the model stays ungrouped and callers fall back to the
|
|
403
|
+
// ordinary Gemini pool (see the fixture README).
|
|
402
404
|
if (isGeminiArgonModel(normalized))
|
|
403
|
-
return undefined;
|
|
405
|
+
return hasArgonPool ? 'gemini-argon' : undefined;
|
|
404
406
|
return (QUOTA_GROUP_BY_MODEL_ID[normalized] ??
|
|
405
407
|
// Check Claude / GPT-OSS substrings BEFORE the `gemini` substring so
|
|
406
408
|
// a `gemini-claude-*` alias (which is a Claude route exposed under
|