dsh-keys-manager 0.2.3 → 0.2.4

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/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AccountAttemptView, KeyTestResult, StateView } from './domain.js';
1
+ import type { AccountAttemptView, KeyTestResult, StateView, TransportModelsView } from './domain.js';
2
2
  import type { CredentialsLike, GenerateOptionsLike, HostContext, KeySelection, RequestErrorAction, AgentRequestPayload, RequestErrorPayload, SettingsLike, StreamChunkLike } from './host-types.js';
3
3
  import { KeysManagerStore } from './store.js';
4
4
  export declare const name = "keys-manager";
@@ -718,6 +718,35 @@ export declare class KeysManagerService {
718
718
  removeTransportAccount(route: string): Promise<StateView>;
719
719
  /** The route's removal epoch; absent means never removed. */
720
720
  private transportGenerationOf;
721
+ /**
722
+ * Remote: read one transport route's catalog and configured allow-list.
723
+ *
724
+ * @remarks
725
+ * The catalog comes from the transports package, because the route's
726
+ * `listModels` answers only the allow-list. An empty allow-list means "all
727
+ * models", so an unreadable settings store must not answer `[]` — a save
728
+ * would then clear a narrowed selection — and it rejects instead.
729
+ */
730
+ transportModels(route: string): Promise<TransportModelsView>;
731
+ /**
732
+ * Remote: replace one transport route's model allow-list.
733
+ *
734
+ * Writes only the route's own `providers.<route>.models` path through the
735
+ * live settings revision; an empty list restores the full catalog.
736
+ */
737
+ saveTransportModels(route: string, models: string[]): Promise<TransportModelsView>;
738
+ /** Refuse a route the transports package does not register. */
739
+ private assertTransportRoute;
740
+ /**
741
+ * One transport route's configured model ids, read from the live settings.
742
+ *
743
+ * @remarks
744
+ * Rejects when the descriptor is unreadable or this plugin's namespace is
745
+ * not mounted: a registered route always owns that namespace, so answering
746
+ * "no models configured" would report the all-models state for a store the
747
+ * host simply could not read.
748
+ */
749
+ private transportModelIds;
721
750
  /**
722
751
  * Drop a grant a withdrawn sign-in wrote after its account was removed.
723
752
  *
@@ -1013,9 +1042,30 @@ export declare class KeysManagerService {
1013
1042
  * flush; see {@link compensateActivationFlush} for that compensation.
1014
1043
  */
1015
1044
  activateAccount(provider: string, accountId: string): Promise<StateView>;
1045
+ /**
1046
+ * The row's own grant payload, recovered from the live projection when the
1047
+ * plugin-scope record is gone. The projection IS the route's credential, so a
1048
+ * matching payload is written back (compare-at-absent) and used immediately.
1049
+ *
1050
+ * `recoveredFromProjection` marks a payload the ownership-verified projection
1051
+ * supplied when the repair write did not persist it: the ref stays absent or
1052
+ * unusable, so a caller that re-reads it must treat the payload as the live
1053
+ * credential instead of a concurrent writer's record. That case also returns
1054
+ * the projection record it validated: a caller that compares before writing
1055
+ * must use this snapshot as its baseline, or the compare would accept a
1056
+ * rotation that landed after the payload was read. It never marks a
1057
+ * projection whose row has been detached or repointed since the snapshot —
1058
+ * that recovery is refused, because the projection then belongs to a
1059
+ * generation this row no longer names. For the same reason a repair the
1060
+ * updater declined (row gone, retiring, or repointed) resolves `undefined`:
1061
+ * the callers' own "missing or unusable" failure is the truthful answer, not
1062
+ * the payload of a generation this row has left behind.
1063
+ */
1064
+ private recoverGrantPayload;
1016
1065
  /**
1017
1066
  * The slow half of an activation, outside the queue: read the row's grant
1018
- * generation, validate it, and capture the projection the route serves now.
1067
+ * generation (recovered from the live projection when its record is gone),
1068
+ * validate it, and capture the projection the route serves now.
1019
1069
  */
1020
1070
  private prepareActivation;
1021
1071
  /** Whether the projection still holds the value this activation read. */
@@ -1196,6 +1246,17 @@ export declare class KeysManagerService {
1196
1246
  private testRouteProfile;
1197
1247
  /** One request pinned to the row's API-key literal. */
1198
1248
  private runApiKeyRowTest;
1249
+ /**
1250
+ * Whether this test may still borrow the route, or the result it must report
1251
+ * instead: the window is live, shutdown has not begun, and the row still
1252
+ * exists, is not retiring, and still names the `grantRef` the test captured.
1253
+ *
1254
+ * Read before the borrow and again after every await that precedes it. The
1255
+ * grant recovery is one: it outlives the isolation deadline on a slow seam,
1256
+ * and a window the watchdog already released no longer compensates its late
1257
+ * borrow (that branch is guarded by `abandonedWindows.get(provider) === window`).
1258
+ */
1259
+ private borrowBarrier;
1199
1260
  /** One request with the row's grant temporarily projected as the route's credential. */
1200
1261
  private runOAuthRowTest;
1201
1262
  /** The refusal an abandoned test reports instead of a stale success. */
package/lib/index.js CHANGED
@@ -15,7 +15,8 @@
15
15
  */
16
16
  import { AsyncLocalStorage } from 'node:async_hooks';
17
17
  import { createHash, randomBytes } from 'node:crypto';
18
- import { KEY_LIMIT, accountRef, applyFailureCooldown, bindingKey, classifyOAuthFailure, isSelectable, markKeySuccess, markKeyUsed, normalizeSettings, patchSettings, pickHealthyExcluding, pickRoundRobin, shouldFailover, statusOfKey, statusOfProvider, validateKeyInput, validateKeyPatch, } from './core.js';
18
+ import { CURSOR_ROUTE, cursorCatalogModels } from 'dsh-llm-omp-transports';
19
+ import { KEY_LIMIT, TEST_SUCCESS_FINISH_KINDS, accountRef, applyFailureCooldown, bindingKey, classifyOAuthFailure, isSelectable, markKeySuccess, markKeyUsed, normalizeSettings, patchSettings, pickHealthyExcluding, pickRoundRobin, shouldFailover, statusOfKey, statusOfProvider, validateKeyInput, validateKeyPatch, } from './core.js';
19
20
  import { EXTRA_PROVIDER_MAP, EXTRA_PROVIDERS } from './providers.js';
20
21
  import { KeysManagerStore, ensureStateFile, statePath } from './store.js';
21
22
  export const name = 'keys-manager';
@@ -255,6 +256,11 @@ const INVOCATIONS = [
255
256
  endpoint('beginTransportLogin', ['provider']),
256
257
  endpoint('activateTransportAccount', ['provider']),
257
258
  endpoint('removeTransportAccount', ['provider']),
259
+ // The transport card's model picker: the catalog lives in the transports
260
+ // package (listModels answers only the allow-list), and the selection is
261
+ // written to the route's own settings path.
262
+ endpoint('transportModels', ['provider']),
263
+ endpoint('saveTransportModels', ['provider', 'models']),
258
264
  ];
259
265
  /** Read a nested value by path. */
260
266
  function getPath(value, path) {
@@ -517,6 +523,15 @@ function transportAccountOf(flow, record, now) {
517
523
  function transportMethodOf(flow) {
518
524
  return flow.methods[0]?.id;
519
525
  }
526
+ /**
527
+ * The catalog a transport route advertises to its model picker.
528
+ *
529
+ * Only the Cursor route ships a catalog projection today; a route without one
530
+ * reports no models rather than borrowing another route's catalog.
531
+ */
532
+ function transportCatalogOf(route) {
533
+ return route === CURSOR_ROUTE ? cursorCatalogModels() : [];
534
+ }
520
535
  /** Env names that are a single API-key credential, not an OAuth/federation token. */
521
536
  function isKeyishEnv(name) {
522
537
  if (!/(?:API_KEY|_TOKEN)$/.test(name))
@@ -1068,9 +1083,10 @@ export class KeysManagerService {
1068
1083
  // serves the route ahead of any broker reference exactly like an
1069
1084
  // unowned grant, so it is classified as foreign below (PR review
1070
1085
  // rounds 4 and 13).
1071
- const owner = record.kind === 'grant' && isOAuthGrantPayload(record.payload)
1072
- ? matchOwningAccount(provider, accounts, record.payload)
1086
+ const projection = record.kind === 'grant' && isOAuthGrantPayload(record.payload)
1087
+ ? record.payload
1073
1088
  : undefined;
1089
+ const owner = projection === undefined ? undefined : matchOwningAccount(provider, accounts, projection);
1074
1090
  // A newer activation/login claimed the owner while this read was in
1075
1091
  // flight: the stale read is discarded, never written over it. A
1076
1092
  // remount replaced the whole store: its classification is gone too.
@@ -2105,6 +2121,85 @@ export class KeysManagerService {
2105
2121
  transportGenerationOf(route) {
2106
2122
  return this.transportRemovals.get(route) ?? 0;
2107
2123
  }
2124
+ /**
2125
+ * Remote: read one transport route's catalog and configured allow-list.
2126
+ *
2127
+ * @remarks
2128
+ * The catalog comes from the transports package, because the route's
2129
+ * `listModels` answers only the allow-list. An empty allow-list means "all
2130
+ * models", so an unreadable settings store must not answer `[]` — a save
2131
+ * would then clear a narrowed selection — and it rejects instead.
2132
+ */
2133
+ async transportModels(route) {
2134
+ this.assertTransportRoute(route);
2135
+ const configured = this.transportModelIds(route);
2136
+ const catalog = transportCatalogOf(route);
2137
+ // A settings profile can outlive a catalog id; the picker renders the
2138
+ // catalog, so an unknown id would count toward {n} of {total} while
2139
+ // matching no checkbox and failing every save.
2140
+ const known = new Set(catalog.map(row => row.id));
2141
+ return { catalog, models: configured.filter(id => known.has(id)) };
2142
+ }
2143
+ /**
2144
+ * Remote: replace one transport route's model allow-list.
2145
+ *
2146
+ * Writes only the route's own `providers.<route>.models` path through the
2147
+ * live settings revision; an empty list restores the full catalog.
2148
+ */
2149
+ async saveTransportModels(route, models) {
2150
+ this.assertTransportRoute(route);
2151
+ if (!Array.isArray(models) || models.some(id => typeof id !== 'string')) {
2152
+ throw new TypeError('models must be a list of model ids');
2153
+ }
2154
+ const catalog = transportCatalogOf(route);
2155
+ if (catalog.length === 0)
2156
+ throw new Error(`Provider "${route}" offers no model list`);
2157
+ const known = new Map(catalog.map(row => [row.id, row.name]));
2158
+ const selected = [...new Set(models)];
2159
+ const unknown = selected.find(id => !known.has(id));
2160
+ if (unknown !== undefined)
2161
+ throw new Error(`Provider "${route}" has no model "${unknown}"`);
2162
+ await this.mutateSettings(TRANSPORT_CREDENTIAL_SCOPE, [
2163
+ {
2164
+ op: 'set',
2165
+ path: ['providers', route, 'models'],
2166
+ value: selected.map(id => ({ id, name: known.get(id) })),
2167
+ },
2168
+ ]);
2169
+ return this.transportModels(route);
2170
+ }
2171
+ /** Refuse a route the transports package does not register. */
2172
+ assertTransportRoute(route) {
2173
+ if (typeof route !== 'string' || route.length === 0)
2174
+ throw new TypeError('provider is required');
2175
+ if (this.transportFlowFor(route) === undefined)
2176
+ throw new Error(`Provider "${route}" offers no transport account`);
2177
+ }
2178
+ /**
2179
+ * One transport route's configured model ids, read from the live settings.
2180
+ *
2181
+ * @remarks
2182
+ * Rejects when the descriptor is unreadable or this plugin's namespace is
2183
+ * not mounted: a registered route always owns that namespace, so answering
2184
+ * "no models configured" would report the all-models state for a store the
2185
+ * host simply could not read.
2186
+ */
2187
+ transportModelIds(route) {
2188
+ const descriptor = this.describeSettings();
2189
+ if (!descriptor.readable)
2190
+ throw new Error('Cannot read settings: the settings store is unavailable');
2191
+ const namespace = this.namespace(descriptor, TRANSPORT_CREDENTIAL_SCOPE);
2192
+ if (namespace === undefined) {
2193
+ throw new Error(`Settings namespace "${TRANSPORT_CREDENTIAL_SCOPE}" is not mounted`);
2194
+ }
2195
+ const value = getPath(namespace.value, ['providers', route, 'models']);
2196
+ if (!Array.isArray(value))
2197
+ return [];
2198
+ return value.flatMap(row => {
2199
+ const id = row !== null && typeof row === 'object' ? row.id : undefined;
2200
+ return typeof id === 'string' ? [id] : [];
2201
+ });
2202
+ }
2108
2203
  /**
2109
2204
  * Drop a grant a withdrawn sign-in wrote after its account was removed.
2110
2205
  *
@@ -2636,11 +2731,20 @@ export class KeysManagerService {
2636
2731
  return;
2637
2732
  }
2638
2733
  if (harvest.wroteRef) {
2639
- try {
2640
- await records.deleteRecord(harvest.grantRef);
2734
+ // A live row still names this ref: a same-ref re-authentication writes
2735
+ // over the record its own pre-existing row depends on, and this
2736
+ // compensation must not leave that row dangling (same guard as the
2737
+ // rotation cleanup, R5-MF1).
2738
+ if (this.grantRefServed(attempt.provider, harvest.grantRef)) {
2739
+ this.warn(`harvest compensation kept ${harvest.grantRef} for ${attempt.provider}: an account row still references it`);
2641
2740
  }
2642
- catch (error) {
2643
- this.warn(`harvest compensation could not delete ${harvest.grantRef}: ${error instanceof Error ? error.message : String(error)}`);
2741
+ else {
2742
+ try {
2743
+ await records.deleteRecord(harvest.grantRef);
2744
+ }
2745
+ catch (error) {
2746
+ this.warn(`harvest compensation could not delete ${harvest.grantRef}: ${error instanceof Error ? error.message : String(error)}`);
2747
+ }
2644
2748
  }
2645
2749
  }
2646
2750
  await this.revertHarvestProjection(attempt, harvest);
@@ -3392,9 +3496,16 @@ export class KeysManagerService {
3392
3496
  // changes. A same-ref rotation that lands while the activation waits in
3393
3497
  // the queue is the uncovered case.
3394
3498
  const source = await records.readRecord(prepared.grantRef);
3395
- if (source === undefined
3499
+ const sourceMatches = source !== undefined
3500
+ && source.kind === 'grant'
3501
+ && JSON.stringify(source.payload) === prepared.written;
3502
+ // The preparation can carry a payload its own repair write did not
3503
+ // persist: the ownership-verified projection is then the live credential,
3504
+ // and an absent or unusable ref is that state — not a concurrent writer.
3505
+ const sourceAbsentOrUnusable = source === undefined
3396
3506
  || source.kind !== 'grant'
3397
- || JSON.stringify(source.payload) !== prepared.written) {
3507
+ || !isOAuthGrantPayload(source.payload);
3508
+ if (!sourceMatches && !(prepared.recoveredFromProjection && sourceAbsentOrUnusable)) {
3398
3509
  throw new Error(`The grant payload for "${accountId}" on ${provider} could not be projected; activate the account again`);
3399
3510
  }
3400
3511
  }
@@ -3562,19 +3673,123 @@ export class KeysManagerService {
3562
3673
  throw error;
3563
3674
  }
3564
3675
  }
3676
+ /**
3677
+ * The row's own grant payload, recovered from the live projection when the
3678
+ * plugin-scope record is gone. The projection IS the route's credential, so a
3679
+ * matching payload is written back (compare-at-absent) and used immediately.
3680
+ *
3681
+ * `recoveredFromProjection` marks a payload the ownership-verified projection
3682
+ * supplied when the repair write did not persist it: the ref stays absent or
3683
+ * unusable, so a caller that re-reads it must treat the payload as the live
3684
+ * credential instead of a concurrent writer's record. That case also returns
3685
+ * the projection record it validated: a caller that compares before writing
3686
+ * must use this snapshot as its baseline, or the compare would accept a
3687
+ * rotation that landed after the payload was read. It never marks a
3688
+ * projection whose row has been detached or repointed since the snapshot —
3689
+ * that recovery is refused, because the projection then belongs to a
3690
+ * generation this row no longer names. For the same reason a repair the
3691
+ * updater declined (row gone, retiring, or repointed) resolves `undefined`:
3692
+ * the callers' own "missing or unusable" failure is the truthful answer, not
3693
+ * the payload of a generation this row has left behind.
3694
+ */
3695
+ async recoverGrantPayload(provider, row, records) {
3696
+ const grantRef = row.oauth.grantRef;
3697
+ const stored = await records.readRecord(grantRef);
3698
+ if (stored !== undefined && stored.kind === 'grant' && isOAuthGrantPayload(stored.payload)) {
3699
+ return { payload: stored.payload, recoveredFromProjection: false };
3700
+ }
3701
+ const projectionKey = `${OAUTH_GRANT_RECORD_SCOPE}/${provider}`;
3702
+ // The seam can hand back its own live record and this repair awaits before
3703
+ // it writes: snapshot it once so the ownership check, the write and the
3704
+ // returned baseline all see the payload that was verified, even if the
3705
+ // source is updated in place meanwhile.
3706
+ const projection = structuredClone(await records.readRecord(projectionKey));
3707
+ if (projection === undefined || projection.kind !== 'grant' || !isOAuthGrantPayload(projection.payload))
3708
+ return undefined;
3709
+ // Only a projection this row owns is adopted; recovering a foreign grant
3710
+ // would attach another account's credential to this row.
3711
+ const oauthRows = this.store.provider(provider)?.keys.filter(candidate => candidate.kind === 'oauth' && candidate.oauth !== null) ?? [];
3712
+ if (matchOwningAccount(provider, oauthRows, projection.payload)?.id !== row.id)
3713
+ return undefined;
3714
+ // `row` is a snapshot taken before the reads: a removal or a re-sign-in can
3715
+ // have detached or repointed it since, and the repair must not land on a
3716
+ // ref this row no longer names.
3717
+ if (oauthRows.find(candidate => candidate.id === row.id)?.oauth?.grantRef !== grantRef) {
3718
+ // The row no longer names this ref, so the projection that matched it
3719
+ // belongs to a previous generation: refusing leaves the caller's own
3720
+ // "missing or unusable" failure in place instead of reviving it.
3721
+ return undefined;
3722
+ }
3723
+ // A declined updater is NOT "the ref is occupied by an unusable record":
3724
+ // it means nothing was written for this row, so the projection read above
3725
+ // is a previous generation's payload and must not be served.
3726
+ let declined = false;
3727
+ try {
3728
+ // Compare-at-absent: an occupied ref returns `current`, which the seam
3729
+ // keeps, so a concurrent login's or rotation's record wins the ref.
3730
+ const liveNow = await records.modifyRecord(grantRef, current => {
3731
+ if (current !== undefined)
3732
+ return Promise.resolve(current);
3733
+ // Runs under the seam's per-key lock, so this is the state the write
3734
+ // commits against: a removal that deleted the ref and marked (or
3735
+ // spliced) the row since the checks above must not be undone by
3736
+ // re-creating the credential it deleted.
3737
+ const liveRow = this.store.provider(provider)?.keys.find(candidate => candidate.id === row.id);
3738
+ if (liveRow === undefined
3739
+ || this.retiringAccounts.has(liveRow)
3740
+ || liveRow.kind !== 'oauth'
3741
+ || liveRow.oauth === null
3742
+ || liveRow.oauth.grantRef !== grantRef) {
3743
+ declined = true;
3744
+ return Promise.resolve(undefined);
3745
+ }
3746
+ return Promise.resolve({ kind: 'grant', payload: structuredClone(projection.payload) });
3747
+ });
3748
+ // No post-write compensation (PR #37 I-000233): it would prove what THIS
3749
+ // call wrote, not that the record it deleted was still ours — a same-ref
3750
+ // sign-in can commit a fresh record before attaching its row, and the
3751
+ // seam has no compare-and-delete (`deleteRecord(key)` takes no expected
3752
+ // payload). It is redundant: the removal path owns the ref it retires.
3753
+ // `removeOAuthGrant` marks the row in `retiringAccounts` BEFORE its first
3754
+ // awaited delete, and this updater revalidates synchronously under the
3755
+ // seam's per-key lock, so a removal that started first makes the update
3756
+ // decline, and one that starts after this write deletes
3757
+ // `row.oauth.grantRef` — the very ref this updater just proved the row
3758
+ // names — itself.
3759
+ if (declined)
3760
+ return undefined;
3761
+ // The ref may have been taken while this read was parked: the record's
3762
+ // payload is the credential the caller must borrow now, not the
3763
+ // projection this repair declined to write over it.
3764
+ if (liveNow !== undefined && liveNow.kind === 'grant' && isOAuthGrantPayload(liveNow.payload)) {
3765
+ return { payload: liveNow.payload, recoveredFromProjection: false };
3766
+ }
3767
+ }
3768
+ catch (error) {
3769
+ // Best-effort repair: the projection still served this read.
3770
+ this.warn(`could not restore ${grantRef} for ${provider}: ${error instanceof Error ? error.message : String(error)}`);
3771
+ }
3772
+ return { payload: projection.payload, recoveredFromProjection: true, projection };
3773
+ }
3565
3774
  /**
3566
3775
  * The slow half of an activation, outside the queue: read the row's grant
3567
- * generation, validate it, and capture the projection the route serves now.
3776
+ * generation (recovered from the live projection when its record is gone),
3777
+ * validate it, and capture the projection the route serves now.
3568
3778
  */
3569
3779
  async prepareActivation(provider, row, records) {
3570
3780
  const grantRef = row.oauth.grantRef;
3571
- const stored = await records.readRecord(grantRef);
3572
- if (stored === undefined || stored.kind !== 'grant' || !isOAuthGrantPayload(stored.payload)) {
3781
+ const recovered = await this.recoverGrantPayload(provider, row, records);
3782
+ if (recovered === undefined) {
3573
3783
  throw new Error(`The grant payload at ${grantRef} is missing or unusable; sign in to this account again`);
3574
3784
  }
3575
3785
  const projectionKey = `${OAUTH_GRANT_RECORD_SCOPE}/${provider}`;
3576
- const live = await records.readRecord(projectionKey);
3577
- const payload = structuredClone(stored.payload);
3786
+ // A projection-only recovery already read and ownership-verified the route.
3787
+ // Re-reading it here would open a gap: a same-ref rotation landing in it
3788
+ // leaves the payload from the first read and a baseline from the second, so
3789
+ // the compare would pass and write the older credential. The record the
3790
+ // payload came from is the only truthful baseline for it.
3791
+ const live = recovered.projection ?? await records.readRecord(projectionKey);
3792
+ const payload = structuredClone(recovered.payload);
3578
3793
  // F5: the restored record decides its owner, so the classification is
3579
3794
  // derived from the baseline payload through the shared matcher — the same
3580
3795
  // rule the reconciler applies — rather than from an owner id captured
@@ -3587,6 +3802,7 @@ export class KeysManagerService {
3587
3802
  grantRef,
3588
3803
  payload,
3589
3804
  written: JSON.stringify(payload),
3805
+ recoveredFromProjection: recovered.recoveredFromProjection,
3590
3806
  baseline: live === undefined ? { state: 'absent' } : { state: 'present', record: structuredClone(live) },
3591
3807
  // Captured with the baseline, before the turn claims the route: the
3592
3808
  // compensation has to put back the classification that matches the
@@ -4452,11 +4668,68 @@ export class KeysManagerService {
4452
4668
  test: true,
4453
4669
  });
4454
4670
  }
4671
+ /**
4672
+ * Whether this test may still borrow the route, or the result it must report
4673
+ * instead: the window is live, shutdown has not begun, and the row still
4674
+ * exists, is not retiring, and still names the `grantRef` the test captured.
4675
+ *
4676
+ * Read before the borrow and again after every await that precedes it. The
4677
+ * grant recovery is one: it outlives the isolation deadline on a slow seam,
4678
+ * and a window the watchdog already released no longer compensates its late
4679
+ * borrow (that branch is guarded by `abandonedWindows.get(provider) === window`).
4680
+ */
4681
+ borrowBarrier(window, provider, row, grantRef) {
4682
+ if (window.cleaned) {
4683
+ // QC fix wave 2: the watchdog abandoned this window while an await was in
4684
+ // flight. Starting (or resuming) the borrow now would land it after the
4685
+ // cleanup that already ran, so the test stops here.
4686
+ return this.abandonedTestResult();
4687
+ }
4688
+ if (this.shutdownRequested) {
4689
+ // PR review round 30: shutdown abandons the chain parked here, so the
4690
+ // borrow write must not land after `close()`. Round 34: the marker
4691
+ // is up from shutdown's entry, so the pre-close restoration is covered
4692
+ // too — it does not borrow, and this test must not either.
4693
+ return this.shutdownEntryResult();
4694
+ }
4695
+ // PR review round 12: the reads and the recovery above are awaits, so a
4696
+ // removal can have deleted the provider or this row while they were in
4697
+ // flight (a test that registered and one that did not are covered: the
4698
+ // marker refuses the latter, this re-check the former). Never write the
4699
+ // borrow — and never re-project the removed account's grant — on that state.
4700
+ // PR review round 15: the existence check alone still holds while a removal
4701
+ // is deleting credentials (the row is spliced only after its grant delete),
4702
+ // so the row's retirement mark is part of the assertion. The provider-wide
4703
+ // marker is deliberately not read here: a removal that began while this
4704
+ // test is registered waits for it in {@link settleTestBeforeRemoval} and is
4705
+ // the designed handshake, while the row mark is only set by a removal that
4706
+ // already passed that check.
4707
+ const live = this.store.provider(provider);
4708
+ const removing = this.retiringAccounts.has(row);
4709
+ const liveRow = live?.keys.find(candidate => candidate.id === row.id);
4710
+ if (removing || liveRow === undefined) {
4711
+ return removing
4712
+ ? { ok: false, latencyMs: 0, error: `Provider "${provider}" is being removed; the test did not borrow its credential` }
4713
+ : { ok: false, latencyMs: 0, error: `Key "${row.id}" was removed from ${provider} before its test could borrow it` };
4714
+ }
4715
+ // PR #37 review (I-000234): a re-sign-in repoints the row instead of
4716
+ // removing it. The recovery read then belongs to the generation the row no
4717
+ // longer names, so borrowing it would test the previous credential and
4718
+ // report the outcome against this row.
4719
+ if (liveRow.oauth === null || liveRow.oauth.grantRef !== grantRef) {
4720
+ return { ok: false, latencyMs: 0, error: `Key "${row.id}" was repointed on ${provider} before its test could borrow it` };
4721
+ }
4722
+ return undefined;
4723
+ }
4455
4724
  /** One request with the row's grant temporarily projected as the route's credential. */
4456
4725
  async runOAuthRowTest(provider, row, model, window) {
4457
4726
  const grant = row.oauth;
4458
4727
  if (grant === null)
4459
4728
  return { ok: false, latencyMs: 0, error: `Key "${row.id}" is not an OAuth account` };
4729
+ // Captured here, not read from the row later: a re-sign-in repoints the
4730
+ // row's generation in place, so the live object would answer with the NEW
4731
+ // ref and the borrow would look current.
4732
+ const grantRef = grant.grantRef;
4460
4733
  const records = this.credentialRecords();
4461
4734
  if (records === undefined)
4462
4735
  return { ok: false, latencyMs: 0, error: 'The credential store does not support records' };
@@ -4466,23 +4739,6 @@ export class KeysManagerService {
4466
4739
  // grant chain, so a rotation echo cannot observe or overwrite the
4467
4740
  // temporary projection between them.
4468
4741
  await this.scheduleGrantReconciliation(provider, async () => {
4469
- const stored = await records.readRecord(grant.grantRef);
4470
- if (window.cleaned) {
4471
- // QC fix wave 2: the watchdog abandoned this window while the read was
4472
- // in flight. Starting (or resuming) the borrow now would land it after
4473
- // the cleanup that already ran, so the test stops here.
4474
- result = this.abandonedTestResult();
4475
- return;
4476
- }
4477
- if (stored === undefined || stored.kind !== 'grant' || !isOAuthGrantPayload(stored.payload)) {
4478
- result = {
4479
- ok: false,
4480
- latencyMs: 0,
4481
- error: `The grant payload at ${grant.grantRef} is missing or unusable; sign in to this account again`,
4482
- };
4483
- return;
4484
- }
4485
- const borrowed = structuredClone(stored.payload);
4486
4742
  const captured = await records.readRecord(projectionKey);
4487
4743
  // PR review round 14: the snapshot and its comparison key are frozen
4488
4744
  // here. The seam hands back a live record, so a writer mutating that
@@ -4490,36 +4746,30 @@ export class KeysManagerService {
4490
4746
  // the updater's `current` and make the borrow look unchanged.
4491
4747
  const previous = captured === undefined ? undefined : structuredClone(captured);
4492
4748
  const previousKey = previous === undefined ? undefined : JSON.stringify(previous);
4493
- if (window.cleaned) {
4494
- result = this.abandonedTestResult();
4749
+ const blocked = this.borrowBarrier(window, provider, row, grantRef);
4750
+ if (blocked !== undefined) {
4751
+ result = blocked;
4495
4752
  return;
4496
4753
  }
4497
- if (this.shutdownRequested) {
4498
- // PR review round 30: shutdown abandons the chain parked here, so the
4499
- // borrow write below must not land after `close()`. Round 34: the
4500
- // marker is up from shutdown's entry, so the pre-close restoration is
4501
- // covered too — it does not borrow, and this test must not either.
4502
- result = this.shutdownEntryResult();
4754
+ // The recovery writes a grant record, so it runs only once this test is
4755
+ // known to borrow: an abandoned or retiring test must not restore one.
4756
+ const recovered = await this.recoverGrantPayload(provider, row, records);
4757
+ if (recovered === undefined) {
4758
+ result = {
4759
+ ok: false,
4760
+ latencyMs: 0,
4761
+ error: `The grant payload at ${grantRef} is missing or unusable; sign in to this account again`,
4762
+ };
4503
4763
  return;
4504
4764
  }
4505
- // PR review round 12: both reads above are awaits, so a removal can have
4506
- // deleted the provider or this row while they were in flight (a test that
4507
- // registered and one that did not are covered: the marker refuses the
4508
- // latter, this re-check the former). Never write the borrow — and never
4509
- // re-project the removed account's grant — on that state.
4510
- // PR review round 15: the existence check alone still holds while a
4511
- // removal is deleting credentials (the row is spliced only after its
4512
- // grant delete), so the row's retirement mark is part of the assertion.
4513
- // The provider-wide marker is deliberately not read here: a removal that
4514
- // began while this test is registered waits for it in
4515
- // {@link settleTestBeforeRemoval} and is the designed handshake, while
4516
- // the row mark is only set by a removal that already passed that check.
4517
- const live = this.store.provider(provider);
4518
- const removing = this.retiringAccounts.has(row);
4519
- if (removing || live === undefined || !live.keys.some(candidate => candidate.id === row.id)) {
4520
- result = removing
4521
- ? { ok: false, latencyMs: 0, error: `Provider "${provider}" is being removed; the test did not borrow its credential` }
4522
- : { ok: false, latencyMs: 0, error: `Key "${row.id}" was removed from ${provider} before its test could borrow it` };
4765
+ const borrowed = structuredClone(recovered.payload);
4766
+ // PR #37 review (must-fix): the recovery is an untimed seam round trip
4767
+ // and can outlive the isolation deadline, after which the window's
4768
+ // cleanup has already run and released the window. Registering facts or
4769
+ // borrowing now would land them with no owner: re-run the barrier first.
4770
+ const lateBlocked = this.borrowBarrier(window, provider, row, grantRef);
4771
+ if (lateBlocked !== undefined) {
4772
+ result = lateBlocked;
4523
4773
  return;
4524
4774
  }
4525
4775
  // QC fix wave 2: record the borrow before the write, so an abandoned
@@ -5217,11 +5467,13 @@ export class KeysManagerService {
5217
5467
  return { ok: false, error: failure?.message ?? failure?.code ?? 'The provider reported a failure' };
5218
5468
  }
5219
5469
  // PR review SF3 + round 8: only a real completion is a pass. The ping
5220
- // asks for one token, so `length` is the expected completion; a
5470
+ // asks for one token, so `max-tokens` is the expected completion; a
5221
5471
  // finish chunk with NO reason, `aborted`, or any kind this broker
5222
5472
  // does not know means the provider did not answer, so reporting a
5223
- // pass would claim a credential it never served.
5224
- if (kind === 'stop' || kind === 'length' || kind === 'toolUse') {
5473
+ // pass would claim a credential it never served. The accepted kinds
5474
+ // are the installed `@deepseek-ai/dsh-llm` union (`FinishReasonMap`),
5475
+ // shared with the tests so the two cannot drift again.
5476
+ if (kind !== undefined && TEST_SUCCESS_FINISH_KINDS.includes(kind)) {
5225
5477
  // PR review round 42: a successful ping only proves the route
5226
5478
  // answered. An api-key row test must also prove the pinned literal
5227
5479
  // was the credential that served it — a takeover during the stream