gh-glance 0.17.0 → 0.17.1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.17.1] - 2026-10-26
11
+
12
+ ### Fixed
13
+
14
+ - A full debt-group journal no longer pins every pane of an account behind
15
+ "Local coordination accounting overflow". Quiescent groups now merge once the
16
+ journal is half full, after any active probe finishes or expires, so the
17
+ journal can free space for retiring a control receipt. Ledgers already wedged
18
+ in this state recover automatically after upgrading.
19
+ - Pasted or multi-character input such as `12` is ignored instead of being read
20
+ as a tab number. Single-digit tab keys are unchanged.
21
+ - Webhook events now match configured targets whose repository name differs in
22
+ case from the name GitHub sends, including access-removal events that fence
23
+ cached data. Ambiguity across hosts is still rejected.
24
+ - A collector view with an old snapshot shows `Stale` or `Cached` with its
25
+ source age. It shows `Disconnected`, still with that age, only when the
26
+ collector connection or its app authorization is lost.
27
+
28
+ ### Changed
29
+
30
+ - Updated React to 19.3.0 and refreshed existing tooling/action versions.
31
+ Patched the development-only brace-expansion and Zod vulnerabilities.
32
+ - README: Homebrew installation (`brew install juan294/tap/gh-glance`) is
33
+ documented, with a note to use one installation method.
34
+ - Development CI monitoring uses Sutura v0.4.0.
35
+ - A journal that is genuinely full of unsettled requests reports "Local
36
+ coordination journal is full" with an automatic retry instead of the hard
37
+ accounting-overflow notice.
38
+ - `gh-glance --doctor` reports debt-journal occupancy as `debt groups`, and
39
+ for a hard coordination failure, including a ledger that is missing or
40
+ unreadable, names the state file and the `repair`: quit every pane, move the
41
+ ledger, its recovery journal and `registry.json` aside, relaunch.
42
+
10
43
  ## [0.17.0] - 2026-10-25
11
44
 
12
45
  ### Added
@@ -1282,7 +1315,8 @@ engineering, security, QA and UX. What follows is what changed as a result.
1282
1315
  - The `main` field from `package.json`. It advertised the file as importable,
1283
1316
  but importing it took over the terminal or exited the host process.
1284
1317
 
1285
- [Unreleased]: https://github.com/juan294/gh-glance/compare/v0.17.0...HEAD
1318
+ [Unreleased]: https://github.com/juan294/gh-glance/compare/v0.17.1...HEAD
1319
+ [0.17.1]: https://github.com/juan294/gh-glance/compare/v0.17.0...v0.17.1
1286
1320
  [0.17.0]: https://github.com/juan294/gh-glance/compare/v0.16.2...v0.17.0
1287
1321
  [0.16.2]: https://github.com/juan294/gh-glance/compare/v0.16.1...v0.16.2
1288
1322
  [0.16.1]: https://github.com/juan294/gh-glance/compare/v0.16.0...v0.16.1
package/README.md CHANGED
@@ -121,10 +121,28 @@ current.
121
121
 
122
122
  ## Install
123
123
 
124
+ With Homebrew on macOS or Linux:
125
+
126
+ ```bash
127
+ brew install juan294/tap/gh-glance
128
+ ```
129
+
130
+ Future updates use your normal `brew upgrade`. The
131
+ [Homebrew tap](https://github.com/juan294/homebrew-tap) follows published npm
132
+ releases automatically and supplies its own Node runtime. If moving from npm,
133
+ remove existing npm copies first; the tap README includes migration commands.
134
+ Restart running dashboard panes after upgrading.
135
+
136
+ Alternatively, install with npm (Node.js 22 or newer):
137
+
124
138
  ```bash
125
139
  npm install -g gh-glance
126
140
  ```
127
141
 
142
+ Use one installation method. NVM keeps global npm packages separate for each
143
+ Node installation, so an npm update may not affect a copy launched from another
144
+ prefix.
145
+
128
146
  That is the whole install for the default standalone dashboard. `gh-glance`
129
147
  ships as a single `index.mjs` with no build step, so there is nothing to
130
148
  compile. The optional collector, SSH, webhook, and GitHub App modes use the
@@ -765,11 +783,14 @@ and the lock path separately. A readable metadata file does not make an aged
765
783
  orphan lock or invalid snapshot healthy; plain doctor never repairs either.
766
784
  When one cached identity names the selected host, plain doctor also reads its
767
785
  quota ledger without changing it or contacting GitHub. It reports receipt and
768
- debt totals, observer age, the current recovery cause, and an actual retry time
769
- when one is known. If several cached identities match, it says the scope is
770
- unavailable instead of choosing an account. For a corrupt or unreadable quota
786
+ debt totals, debt-group occupancy (`debt groups`), observer age, the current
787
+ recovery cause, and an actual retry time when one is known. If several cached
788
+ identities match, it says the scope is unavailable instead of choosing an
789
+ account. For a corrupt or unreadable quota
771
790
  file, it names the coordination directory and a `quota-[redacted].json` file
772
- pattern with the cause. The private quota fingerprint stays out of reports.
791
+ pattern with the cause. For a hard coordination failure it also prints a
792
+ `repair` line with the manual steps. The private quota fingerprint stays out of
793
+ reports.
773
794
 
774
795
  Use `gh-glance --doctor --probe` when live capability evidence is needed. Probe
775
796
  mode uses an ephemeral governor lease and admits every quota-consuming endpoint
@@ -1123,7 +1144,8 @@ GH_GLANCE_ICONS=ascii gh-glance
1123
1144
  | `Upgrade waiting for legacy quota reset` | The older panes are gone, but left spend that cannot be proven settled. It waits for the affected rate-limit window to reset rather than assume the quota is free. This one has a deadline and clears itself. |
1124
1145
  | `Upgrade blocked: older spend cannot be settled or waited out` | Older spend has no reliable reset or child evidence. Verify old children ended and use `gh-glance --doctor` to inspect reserved units. An unidentifiable old child may require a verified system restart before a later observer can retire its charge. Keep the quota files as accounting evidence. |
1125
1146
  | `Coordination state unavailable; evidence preserved` | Coordination state could not be read and was deliberately not overwritten, so nothing is lost. Run `gh-glance --doctor` and check the config directory's permissions. |
1126
- | `Local coordination` corruption, overflow, full storage, or unsafe permissions | Run `gh-glance --doctor`; its read-only report gives the exact cause and redacted local file location when known. Repair storage or permissions, then let the app retry. Preserve corrupt and overflow evidence for diagnosis. |
1147
+ | `Local coordination journal is full` | Every slot for unsettled or recently settled requests is in use. The app retries by itself, and older requests settle after about an hour. Nothing needs repair. `gh-glance --doctor` shows the occupancy as `debt groups`. |
1148
+ | `Local coordination` corruption, overflow, full storage, or unsafe permissions | Run `gh-glance --doctor`; its read-only report gives the exact cause and redacted local file location when known. For a hard failure it also prints a `repair` line: quit every pane, move the state file, its `.recovery.json` and `registry.json` aside, then relaunch. Otherwise repair storage or permissions and let the app retry. Preserve corrupt and overflow evidence for diagnosis. |
1127
1149
  | `Saving request result` or `Recovering interrupted request` | The original request may have consumed quota. The app keeps its charge, retries settlement without repeating HTTP, and waits for safe observer evidence before retiring uncertain debt. |
1128
1150
  | `GitHub request limit` | A shared secondary hold preserves GitHub's retry deadline. Wait for the shown time; if the repeated-limit circuit explicitly asks, press `r` once to retry. Repeated refresh cannot bypass a hold. |
1129
1151
  | `Collector connection unavailable` or `GitHub App authorization unavailable` | Restore the selected collector connection or App authorization. The mode does not switch to independent requests. Cached rows remain labeled with their source age. |
package/index.mjs CHANGED
@@ -292,7 +292,7 @@ const ANIMATE = !process.env.GH_GLANCE_NO_ANIMATION;
292
292
  // legitimate mixed-direction Arabic and Hebrew titles render correctly, and
293
293
  // preserving real RTL text is a stated property of this function.
294
294
  // eslint-disable-next-line no-control-regex -- matching control characters is the entire purpose
295
- const CONTROL_CHARS = /[--Ÿ]+/g;
295
+ const CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]+/g;
296
296
  const BIDI_OVERRIDES = /[‪-‮⁦-⁩]/g;
297
297
 
298
298
  // Titles are rendered at whatever length GitHub returns, and ink memoizes
@@ -307,7 +307,7 @@ const MAX_FIELD_LENGTH = 300;
307
307
  function safe(value) {
308
308
  // Coerce first, sanitize always. Returning a non-string early skipped both the
309
309
  // control-character strip and the length clamp, so an array or an object with a
310
- // toString() came through untouched -- `safe(["x"])` returned the escape
310
+ // toString() came through untouched -- `safe(["\x1b[2Jx"])` returned the escape
311
311
  // verbatim. GitHub returns strings for every field this is called on today, so
312
312
  // the guarantee was being provided by the upstream schema rather than by the
313
313
  // function that claims to provide it.
@@ -3275,8 +3275,8 @@ function claimIdentityBootstrap(root, { credentialKey, host, resource = "core",
3275
3275
  }, { now, kill });
3276
3276
  }
3277
3277
 
3278
- function readIdentityQuotaState(registry, scope, now) {
3279
- const loaded = readGovernorState(scope.path, now);
3278
+ function readIdentityQuotaState(registry, scope, now, options = {}) {
3279
+ const loaded = readGovernorState(scope.path, now, options);
3280
3280
  const known = Object.values(registry.identities).some((identity) => identity.quotaKey === scope.quotaKey) ||
3281
3281
  Object.values(registry.attempts).some((attempt) => attempt.imported && attempt.quotaKey === scope.quotaKey);
3282
3282
  return loaded.ok && loaded.missing && known ? { ok: false, reason: "corrupt" } : loaded;
@@ -3302,9 +3302,9 @@ function importIdentityDebts(root, state, credentialKey, identity, now) {
3302
3302
  notBefore: attempt.startedAt, status: "started", epochs: { core: null, graphql: null },
3303
3303
  startedAt: attempt.startedAt, completedAt: null, outcome: null,
3304
3304
  };
3305
- if (!loaded.value.importMarkers[id] &&
3306
- !transferReceiptToDebt(loaded.value, legacyControl, now, id)) {
3307
- return { ok: false, reason: "accounting-overflow" };
3305
+ if (!loaded.value.importMarkers[id]) {
3306
+ const failure = transferReceiptToDebt(loaded.value, legacyControl, now, id);
3307
+ if (failure) return failure;
3308
3308
  }
3309
3309
  if (Object.keys(loaded.value.importMarkers).length >= IDENTITY_MAX_ATTEMPTS &&
3310
3310
  !loaded.value.importMarkers[id]) return { ok: false, reason: "identity-capacity" };
@@ -3519,7 +3519,8 @@ function startIdentityControl(coordinator, now = Date.now()) {
3519
3519
  if (Object.keys(registry.attempts).length >= IDENTITY_MAX_ATTEMPTS) return { ok: false, reason: "identity-capacity" };
3520
3520
  const priorControl = state.controlReceipts.core;
3521
3521
  if (priorControl) {
3522
- if (!retireControlReceiptToDebt(state, "core", now)) return { ok: false, reason: "accounting-overflow" };
3522
+ const failure = retireControlReceiptToDebt(state, "core", now);
3523
+ if (failure) return failure;
3523
3524
  }
3524
3525
  const id = governorId();
3525
3526
  registry.attempts[id] = { credentialKey: identity.credentialKey, host: identity.host, resource: "core", startedAt: now,
@@ -3574,8 +3575,9 @@ function settleIdentityControl(coordinator, control, stdout, now = Date.now(),
3574
3575
  reservation.actualCosts = { core: cost, graphql: 0 };
3575
3576
  reservation.accountedCosts = { core: cost, graphql: 0 };
3576
3577
  loaded.value.controlReceipts.core = null;
3577
- } else if (!retireControlReceiptToDebt(loaded.value, "core", now)) {
3578
- return { ok: false, reason: "accounting-overflow" };
3578
+ } else {
3579
+ const failure = retireControlReceiptToDebt(loaded.value, "core", now);
3580
+ if (failure) return failure;
3579
3581
  }
3580
3582
  return writeGovernorQuotaState(scope.path, loaded.value, now);
3581
3583
  });
@@ -3844,6 +3846,7 @@ function identityCoordinationMessage(reason) {
3844
3846
  if (reason === "throttle-paused") return "Paused after repeated rate limits; refresh to retry";
3845
3847
  if (retryableCoordination(reason)) return "Coordinating with your other panes";
3846
3848
  if (reason === "identity-capacity") return "Coordination state full; retrying after the current window";
3849
+ if (reason === "debt-journal-full") return "Coordination journal full; retrying after older requests settle";
3847
3850
  return "Verified identity unavailable; waiting to retry";
3848
3851
  }
3849
3852
 
@@ -3889,6 +3892,9 @@ const GOVERNOR_MAX_LEASES = 128;
3889
3892
  const GOVERNOR_MAX_INTENTS = 512;
3890
3893
  const GOVERNOR_MAX_RESERVATIONS = 512;
3891
3894
  const GOVERNOR_MAX_DEBT_GROUPS = 128;
3895
+ // Quiescent groups merge once the journal is this full, so retirement never
3896
+ // waits on a probe publication that a full journal would itself block.
3897
+ const DEBT_JOURNAL_COMPACT_AT = GOVERNOR_MAX_DEBT_GROUPS / 2;
3892
3898
  const GOVERNOR_MAX_LEDGER_BYTES = 2 * 1024 * 1024;
3893
3899
  const GOVERNOR_LOCK_WAIT_MS = 250;
3894
3900
  // How old an unreadable lock record may be before it counts as abandoned. A
@@ -4526,10 +4532,8 @@ function migrateLegacyGovernorReceipts(legacy, nowMs) {
4526
4532
  if (RATE_RESOURCES.some((resource) => units[resource] > 0)) {
4527
4533
  const nonce = governorId();
4528
4534
  state.debtGroups[nonce] = {
4529
- nonce, ownerNonce: null, ownerPid: null, ownerBirth: null, bootId: coordinationBootId(),
4530
- unknownChild: true, sealed: true, quiescent: false,
4531
- units, counts, at: nowMs, barrierAt: null,
4532
- barrierRevisions: { core: 0, graphql: 0 },
4535
+ ...newDebtGroup({ nonce, bootId: coordinationBootId(), unknownChild: true, at: nowMs }),
4536
+ units, counts,
4533
4537
  };
4534
4538
  for (const resource of RATE_RESOURCES) {
4535
4539
  state.debt[resource].unresolvedUnits = units[resource];
@@ -4596,25 +4600,57 @@ function debtGroupTarget(state, receipt, groupId = null) {
4596
4600
  return state.debtGroups[UNKNOWN_DEBT_GROUP_ID] ? governorId() : UNKNOWN_DEBT_GROUP_ID;
4597
4601
  }
4598
4602
 
4603
+ const ACCOUNTING_OVERFLOW = Object.freeze({ ok: false, reason: "accounting-overflow" });
4604
+
4605
+ // The one shape normalizeGovernorState accepts for a group; every site that
4606
+ // creates a group starts from it so a new field cannot be missed in one copy.
4607
+ function newDebtGroup({ nonce = governorId(), bootId, unknownChild, at, quiescent = false, barrierAt = null }) {
4608
+ return {
4609
+ nonce, ownerNonce: null, ownerPid: null, ownerBirth: null, bootId,
4610
+ unknownChild, sealed: true, quiescent,
4611
+ units: { core: 0, graphql: 0 }, counts: { core: 0, graphql: 0 },
4612
+ at, barrierAt, barrierRevisions: { core: 0, graphql: 0 },
4613
+ };
4614
+ }
4615
+
4616
+ // When age alone settles an unresolved group. Only a sealed group ages out;
4617
+ // an owner acknowledgement can settle either kind earlier.
4618
+ function debtGroupSettlesAt(group) {
4619
+ return group.sealed ? group.at + IDENTITY_UNCERTAIN_MAX_MS : Number.POSITIVE_INFINITY;
4620
+ }
4621
+
4622
+ function hasLiveProbeClaim(state, at) {
4623
+ return RATE_RESOURCES.some((resource) => state.probeClaims[resource]?.leaseUntil > at);
4624
+ }
4625
+
4626
+ // A journal with no free slot is a capacity condition, not an arithmetic one.
4627
+ // A retry can succeed once an unresolved group ages out or a live claim ends
4628
+ // and lets compaction run; the probe interval bounds the wait so a pane never
4629
+ // shows a retry hours away.
4630
+ function debtJournalFullFailure(state, nowMs) {
4631
+ const unblockAt = Math.min(
4632
+ ...Object.values(state.debtGroups).filter((group) => !group.quiescent).map(debtGroupSettlesAt),
4633
+ ...RATE_RESOURCES.map((resource) => state.probeClaims[resource]?.leaseUntil ?? Number.POSITIVE_INFINITY),
4634
+ nowMs + BUDGET_PROBE_MS,
4635
+ );
4636
+ return { ok: false, reason: "debt-journal-full", retryAt: Math.max(unblockAt, nowMs + 1000) };
4637
+ }
4638
+
4639
+ // Returns null when the charge is recorded, otherwise the failure to report.
4599
4640
  function transferReceiptToDebt(state, receipt, nowMs, groupId = null) {
4600
4641
  const units = Object.fromEntries(RATE_RESOURCES.map((resource) => [resource,
4601
4642
  reservationCost(receipt, resource, state.leases, nowMs)]));
4602
- if (RATE_RESOURCES.every((resource) => units[resource] === 0)) return true;
4643
+ if (RATE_RESOURCES.every((resource) => units[resource] === 0)) return null;
4603
4644
  const id = debtGroupTarget(state, receipt, groupId);
4604
- if (id === null) return false;
4645
+ if (id === null) return debtJournalFullFailure(state, nowMs);
4605
4646
  const incomingBootId = receipt.receipt?.bootId ?? coordinationBootId();
4606
4647
  const dedicated = id === groupId;
4607
- const group = state.debtGroups[id] ?? {
4608
- nonce: governorId(), ownerNonce: null, ownerPid: null, ownerBirth: null,
4609
- bootId: incomingBootId,
4610
- unknownChild: !dedicated || !receipt.receipt, sealed: true, quiescent: false,
4611
- units: { core: 0, graphql: 0 }, counts: { core: 0, graphql: 0 },
4612
- at: nowMs, barrierAt: null,
4613
- barrierRevisions: { core: 0, graphql: 0 },
4614
- };
4615
- if (group.quiescent) return false;
4648
+ const group = state.debtGroups[id] ?? newDebtGroup({
4649
+ bootId: incomingBootId, unknownChild: !dedicated || !receipt.receipt, at: nowMs,
4650
+ });
4651
+ if (group.quiescent) return ACCOUNTING_OVERFLOW;
4616
4652
  if (receipt.receipt && dedicated) {
4617
- if (group.ownerNonce !== null && group.ownerNonce !== receipt.receipt.ownerNonce) return false;
4653
+ if (group.ownerNonce !== null && group.ownerNonce !== receipt.receipt.ownerNonce) return ACCOUNTING_OVERFLOW;
4618
4654
  group.ownerNonce = receipt.receipt.ownerNonce;
4619
4655
  group.ownerPid = receipt.receipt.ownerPid;
4620
4656
  group.ownerBirth = receipt.receipt.ownerBirth;
@@ -4629,7 +4665,7 @@ function transferReceiptToDebt(state, receipt, nowMs, groupId = null) {
4629
4665
  !Number.isSafeInteger(group.units[resource] + amount) ||
4630
4666
  !Number.isSafeInteger(state.debt[resource].unresolvedCount + (amount > 0 ? 1 : 0)) ||
4631
4667
  !Number.isSafeInteger(state.debt[resource].revision + 1) ||
4632
- !Number.isSafeInteger(group.counts[resource] + (amount > 0 ? 1 : 0))) return false;
4668
+ !Number.isSafeInteger(group.counts[resource] + (amount > 0 ? 1 : 0))) return ACCOUNTING_OVERFLOW;
4633
4669
  }
4634
4670
  for (const resource of RATE_RESOURCES) {
4635
4671
  const amount = units[resource];
@@ -4643,7 +4679,7 @@ function transferReceiptToDebt(state, receipt, nowMs, groupId = null) {
4643
4679
  // which reconcileBootDebt may treat the group as settled.
4644
4680
  group.at = Math.max(group.at, nowMs);
4645
4681
  state.debtGroups[id] = group;
4646
- return true;
4682
+ return null;
4647
4683
  }
4648
4684
 
4649
4685
  function makeDebtGroupQuiescent(state, group, at) {
@@ -4670,23 +4706,25 @@ function makeDebtGroupQuiescent(state, group, at) {
4670
4706
  return true;
4671
4707
  }
4672
4708
 
4709
+ // Returns null when the receipt is retired, otherwise the failure to report.
4673
4710
  function retireControlReceiptToDebt(state, resource, at) {
4674
4711
  const control = state.controlReceipts[resource];
4675
- if (!control) return true;
4712
+ if (!control) return null;
4676
4713
  // A control receipt without an issued dispatch cannot have started HTTP.
4677
4714
  // Removing it also fences a late owner before it can issue a child.
4678
4715
  if (control.receipt.dispatches.length === 0) {
4679
4716
  state.controlReceipts[resource] = null;
4680
- return true;
4717
+ return null;
4681
4718
  }
4682
4719
  const generation = control.receipt.generation;
4683
- if (!transferReceiptToDebt(state, control, at, generation)) return false;
4720
+ const failure = transferReceiptToDebt(state, control, at, generation);
4721
+ if (failure) return failure;
4684
4722
  const group = state.debtGroups[generation];
4685
4723
  if (group && !group.unknownChild && group.ownerPid !== null &&
4686
4724
  group.ownerBirth !== null && group.bootId !== null &&
4687
- !makeDebtGroupQuiescent(state, group, at)) return false;
4725
+ !makeDebtGroupQuiescent(state, group, at)) return ACCOUNTING_OVERFLOW;
4688
4726
  state.controlReceipts[resource] = null;
4689
- return true;
4727
+ return null;
4690
4728
  }
4691
4729
 
4692
4730
  // A reboot proves an unknown owner's children are gone. So does age: a group's
@@ -4698,16 +4736,49 @@ function reconcileBootDebt(state, at) {
4698
4736
  for (const group of Object.values(state.debtGroups)) {
4699
4737
  if (group.quiescent) continue;
4700
4738
  const priorBoot = bootId !== null && group.bootId !== null && group.bootId !== bootId;
4701
- const outlivedWindow = group.sealed && at - group.at >= IDENTITY_UNCERTAIN_MAX_MS;
4739
+ const outlivedWindow = at >= debtGroupSettlesAt(group);
4702
4740
  if (!priorBoot && !outlivedWindow) continue;
4703
4741
  if (!makeDebtGroupQuiescent(state, group, at)) {
4704
- return { ok: false, reason: "accounting-overflow" };
4742
+ return ACCOUNTING_OVERFLOW;
4705
4743
  }
4706
4744
  changed = true;
4707
4745
  }
4708
4746
  return { ok: true, changed };
4709
4747
  }
4710
4748
 
4749
+ // Quiescent groups are frozen until a probe publication retires them, and a
4750
+ // publication needs a claim that first charges its control receipt to a group.
4751
+ // With the journal full of quiescent groups that charge has nowhere to go, so
4752
+ // the only operation that drains the journal is the one it blocks. Merging the
4753
+ // frozen groups keeps the totals and frees the slots. A claim compares the
4754
+ // snapshot it took against the groups it finds at publication, so the shape may
4755
+ // only change while no claim is live.
4756
+ function compactQuiescentDebtGroups(state, at) {
4757
+ if (Object.keys(state.debtGroups).length < DEBT_JOURNAL_COMPACT_AT || hasLiveProbeClaim(state, at)) {
4758
+ return { ok: true, changed: false };
4759
+ }
4760
+ const quiescent = Object.entries(state.debtGroups).filter(([, group]) => group.quiescent);
4761
+ let changed = false;
4762
+ for (const [bootId, entries] of Map.groupBy(quiescent, ([, group]) => group.bootId)) {
4763
+ if (entries.length < 2) continue;
4764
+ const groups = entries.map(([, group]) => group);
4765
+ const merged = newDebtGroup({ bootId, unknownChild: true, quiescent: true,
4766
+ at: Math.max(...groups.map((group) => group.at)),
4767
+ barrierAt: Math.max(0, ...groups.map((group) => group.barrierAt ?? 0)) });
4768
+ for (const resource of RATE_RESOURCES) {
4769
+ merged.units[resource] = groups.reduce((sum, group) => sum + group.units[resource], 0);
4770
+ merged.counts[resource] = groups.reduce((sum, group) => sum + group.counts[resource], 0);
4771
+ if (!Number.isSafeInteger(merged.units[resource]) ||
4772
+ !Number.isSafeInteger(merged.counts[resource])) return ACCOUNTING_OVERFLOW;
4773
+ merged.barrierRevisions[resource] = Math.max(...groups.map((group) => group.barrierRevisions[resource]));
4774
+ }
4775
+ for (const [id] of entries) delete state.debtGroups[id];
4776
+ state.debtGroups[merged.nonce] = merged;
4777
+ changed = true;
4778
+ }
4779
+ return { ok: true, changed };
4780
+ }
4781
+
4711
4782
  function compactGovernorReceipts(state, nowMs) {
4712
4783
  const sealed = new Set();
4713
4784
  const full = Object.keys(state.reservations).length >= GOVERNOR_MAX_RESERVATIONS;
@@ -4731,18 +4802,16 @@ function compactGovernorReceipts(state, nowMs) {
4731
4802
  const trial = structuredClone(state);
4732
4803
  for (const reservation of candidates) {
4733
4804
  const groupId = reservation.receipt?.generation ?? null;
4734
- if (debtGroupTarget(trial, reservation, groupId) === null) return { ok: true, changed: false };
4735
- if (!transferReceiptToDebt(trial, reservation, nowMs, groupId)) {
4736
- return { ok: false, reason: "accounting-overflow" };
4737
- }
4805
+ const failure = transferReceiptToDebt(trial, reservation, nowMs, groupId);
4806
+ if (failure?.reason === "debt-journal-full") return { ok: true, changed: false };
4807
+ if (failure) return failure;
4738
4808
  }
4739
4809
  }
4740
4810
  if (full) {
4741
4811
  for (const [id, reservation] of Object.entries(state.reservations)) {
4742
4812
  if (reservation.status !== "completed" || reservation.receipt) continue;
4743
- if (!transferReceiptToDebt(state, reservation, nowMs)) {
4744
- return { ok: false, reason: "accounting-overflow" };
4745
- }
4813
+ const failure = transferReceiptToDebt(state, reservation, nowMs);
4814
+ if (failure) return failure;
4746
4815
  delete state.reservations[id];
4747
4816
  changed = true;
4748
4817
  }
@@ -4763,9 +4832,9 @@ function compactGovernorReceipts(state, nowMs) {
4763
4832
  for (const generation of sealed) {
4764
4833
  for (const [id, reservation] of Object.entries(state.reservations)) {
4765
4834
  if (reservation.receipt?.generation !== generation) continue;
4766
- if (!(reservation.status === "started" && reservation.receipt.dispatches.length === 0) &&
4767
- !transferReceiptToDebt(state, reservation, nowMs, generation)) {
4768
- return { ok: false, reason: "accounting-overflow" };
4835
+ if (!(reservation.status === "started" && reservation.receipt.dispatches.length === 0)) {
4836
+ const failure = transferReceiptToDebt(state, reservation, nowMs, generation);
4837
+ if (failure) return failure;
4769
4838
  }
4770
4839
  delete state.reservations[id];
4771
4840
  state.ownerGenerations[reservation.leaseId] = governorId();
@@ -4774,7 +4843,7 @@ function compactGovernorReceipts(state, nowMs) {
4774
4843
  if (group && !group.unknownChild && group.ownerPid !== null &&
4775
4844
  group.ownerBirth !== null && group.bootId !== null &&
4776
4845
  !makeDebtGroupQuiescent(state, group, nowMs)) {
4777
- return { ok: false, reason: "accounting-overflow" };
4846
+ return ACCOUNTING_OVERFLOW;
4778
4847
  }
4779
4848
  }
4780
4849
  retireOwnerGenerations(state);
@@ -4860,7 +4929,7 @@ function writeGovernorState(path, state) {
4860
4929
  try {
4861
4930
  if (state?.version === GOVERNOR_STATE_VERSION) {
4862
4931
  if (!Number.isSafeInteger(state.revision) || state.revision === Number.MAX_SAFE_INTEGER) {
4863
- return { ok: false, reason: "accounting-overflow" };
4932
+ return ACCOUNTING_OVERFLOW;
4864
4933
  }
4865
4934
  state.revision += 1;
4866
4935
  }
@@ -4890,7 +4959,7 @@ function writeGovernorQuotaState(path, state, nowMs) {
4890
4959
  const normalized = normalizeGovernorState(state, nowMs, { prune: false });
4891
4960
  if (!normalized) return { ok: false, reason: "corrupt" };
4892
4961
  if (!governorAccountingSafe(normalized, nowMs)) {
4893
- return { ok: false, reason: "accounting-overflow" };
4962
+ return ACCOUNTING_OVERFLOW;
4894
4963
  }
4895
4964
  return writeGovernorState(path, normalized);
4896
4965
  }
@@ -5204,17 +5273,19 @@ function mutateGovernor(scope, nowMs, mutate) {
5204
5273
  const loaded = readGovernorState(scope.path, at, { persistMigration: false });
5205
5274
  if (!loaded.ok) return loaded;
5206
5275
  const state = loaded.value;
5207
- if (!governorAccountingSafe(state, at)) return { ok: false, reason: "accounting-overflow" };
5276
+ if (!governorAccountingSafe(state, at)) return ACCOUNTING_OVERFLOW;
5208
5277
  const oldBootReconciled = reconcileBootDebt(state, at);
5209
5278
  if (!oldBootReconciled.ok) return oldBootReconciled;
5210
5279
  const compacted = compactGovernorReceipts(state, at);
5211
5280
  if (!compacted.ok) return compacted;
5212
5281
  const bootReconciled = reconcileBootDebt(state, at);
5213
5282
  if (!bootReconciled.ok) return bootReconciled;
5283
+ const journal = compactQuiescentDebtGroups(state, at);
5284
+ if (!journal.ok) return journal;
5214
5285
  // A mutator can reject without writing. Keep an untouched migration so
5215
5286
  // that even that path upgrades the file once before releasing the lock.
5216
5287
  const migration = loaded.migrated || compacted.changed ||
5217
- oldBootReconciled.changed || bootReconciled.changed
5288
+ oldBootReconciled.changed || bootReconciled.changed || journal.changed
5218
5289
  ? structuredClone(state) : null;
5219
5290
  const returnAfterMigration = (result) => {
5220
5291
  if (migration === null) return result;
@@ -5223,7 +5294,7 @@ function mutateGovernor(scope, nowMs, mutate) {
5223
5294
  };
5224
5295
  const value = mutate(state, at);
5225
5296
  if (value?.ok === false && value.write !== true) return returnAfterMigration(value);
5226
- if (!governorAccountingSafe(state, at)) return returnAfterMigration({ ok: false, reason: "accounting-overflow" });
5297
+ if (!governorAccountingSafe(state, at)) return returnAfterMigration(ACCOUNTING_OVERFLOW);
5227
5298
  const normalized = normalizeGovernorState(state, at);
5228
5299
  if (!normalized) return returnAfterMigration({ ok: false, reason: "corrupt" });
5229
5300
  const written = writeGovernorState(scope.path, normalized);
@@ -5455,9 +5526,8 @@ function claimProbe(scope, leaseId, nowMs, resource) {
5455
5526
  if (resource === "graphql" && state.controlReceipts.graphql) {
5456
5527
  // The old claim's lease has expired. Keep an issued child charged as
5457
5528
  // debt, and fence the old capability so a new observer can make progress.
5458
- if (!retireControlReceiptToDebt(state, "graphql", at)) {
5459
- return { ok: false, reason: "accounting-overflow" };
5460
- }
5529
+ const failure = retireControlReceiptToDebt(state, "graphql", at);
5530
+ if (failure) return failure;
5461
5531
  }
5462
5532
  const nonce = randomUUID();
5463
5533
  const startedReservationIds = Object.entries(state.reservations)
@@ -5476,7 +5546,7 @@ function claimProbe(scope, leaseId, nowMs, resource) {
5476
5546
  const debtUnits = quiescentGroups.reduce((sum, group) => sum + group.units[resource], 0);
5477
5547
  const debtCount = quiescentGroups.reduce((sum, group) => sum + group.counts[resource], 0);
5478
5548
  if (!Number.isSafeInteger(debtUnits) || !Number.isSafeInteger(debtCount)) {
5479
- return { ok: false, reason: "accounting-overflow" };
5549
+ return ACCOUNTING_OVERFLOW;
5480
5550
  }
5481
5551
  const debtSnapshot = {
5482
5552
  units: debtUnits,
@@ -5683,7 +5753,7 @@ function publishProbe(scope, leaseId, nonce, budgets, nowMs, resource) {
5683
5753
  }
5684
5754
  if (changedResources.length === 0) return { ok: false, reason: "corrupt" };
5685
5755
  if (!retireClaimedQuiescentDebt(state, resource, claim)) {
5686
- return { ok: false, reason: "accounting-overflow" };
5756
+ return ACCOUNTING_OVERFLOW;
5687
5757
  }
5688
5758
  const completedBeforeClaim = Object.entries(state.reservations)
5689
5759
  .filter(([, reservation]) =>
@@ -5803,9 +5873,8 @@ function failProbeClaim(scope, leaseId, nonce, nowMs, resource) {
5803
5873
  const control = state.controlReceipts.graphql;
5804
5874
  if (control.receipt.dispatches.length === 0) state.controlReceipts.graphql = null;
5805
5875
  else if (control.receipt.dispatches.every((dispatch) => dispatch.terminalAt !== null)) {
5806
- if (!retireControlReceiptToDebt(state, "graphql", at)) {
5807
- return { ok: false, reason: "accounting-overflow" };
5808
- }
5876
+ const failure = retireControlReceiptToDebt(state, "graphql", at);
5877
+ if (failure) return failure;
5809
5878
  }
5810
5879
  }
5811
5880
  state.observers[resource] = {
@@ -6109,7 +6178,7 @@ function acknowledgeSealedGeneration(scope, capability, sealedNonce, nowMs) {
6109
6178
  reservation.receipt?.generation === capability.generation)) {
6110
6179
  return { ok: false, reason: "quiescence-unverified" };
6111
6180
  }
6112
- if (!makeDebtGroupQuiescent(state, group, at)) return { ok: false, reason: "accounting-overflow" };
6181
+ if (!makeDebtGroupQuiescent(state, group, at)) return ACCOUNTING_OVERFLOW;
6113
6182
  return { value: { status: "acknowledged", nonce: group.nonce } };
6114
6183
  });
6115
6184
  }
@@ -6368,13 +6437,16 @@ function governorHealth(result, nowMs = Date.now()) {
6368
6437
  if (!result?.ok) return { status: `unavailable (${result?.reason ?? "unknown"})`, leases: 0, resources: {} };
6369
6438
  const state = result.value;
6370
6439
  let status = "healthy";
6371
- if (RATE_RESOURCES.some((resource) => state.probeClaims[resource]?.leaseUntil > nowMs)) status = "waiting for probe";
6440
+ if (hasLiveProbeClaim(state, nowMs)) status = "waiting for probe";
6372
6441
  else if (RATE_RESOURCES.some((resource) => !state.budgets[resource] ||
6373
6442
  nowMs - state.budgets[resource].observedAt > budgetSnapshotTtl(resource))) status = "stale";
6374
6443
  else if (RATE_RESOURCES.some((resource) => state.budgets[resource].blockUntil > nowMs)) status = "blocked";
6444
+ const groups = Object.values(state.debtGroups);
6375
6445
  return {
6376
6446
  status,
6377
6447
  leases: Object.keys(state.leases).length,
6448
+ debtGroups: groups.length,
6449
+ quiescentGroups: groups.filter((group) => group.quiescent).length,
6378
6450
  resources: Object.fromEntries(RATE_RESOURCES.flatMap((resource) => {
6379
6451
  const budget = state.budgets[resource];
6380
6452
  return budget ? [[resource, {
@@ -6413,12 +6485,15 @@ function doctorCachedQuotaScope(host, { root = identityRegistryRoot(), nowMs = D
6413
6485
  try {
6414
6486
  if (!lstatSync(path).isFile()) return { ok: false, reason: "unsafe quota path", path };
6415
6487
  } catch (error) {
6416
- return { ok: false, reason: error?.code === "ENOENT" ? "cached quota missing" : "cached quota unreadable", path };
6488
+ if (error?.code !== "ENOENT") return { ok: false, reason: "cached quota unreadable", path };
6417
6489
  }
6418
- const result = readGovernorState(path, nowMs, { persistMigration: false });
6490
+ // The runtime's own guard classifies the file: a missing ledger for a quota
6491
+ // key the registry still lists is corrupt, never a fresh start, so doctor
6492
+ // reports the same reason the panes act on.
6493
+ const result = readIdentityQuotaState(registry, { path, quotaKey }, nowMs, { persistMigration: false });
6419
6494
  return result.ok && !result.missing
6420
6495
  ? { ok: true, result, path, quotaKey }
6421
- : { ok: false, reason: result.missing ? "cached quota missing" : result.reason, path };
6496
+ : { ok: false, reason: result.ok ? "cached quota missing" : result.reason, path };
6422
6497
  }
6423
6498
 
6424
6499
  async function retryGovernorMutation(run, { now, wait, deadline, signal }) {
@@ -8075,7 +8150,7 @@ function mapWebhookInvalidations({ event, payload, config, enterpriseHost = null
8075
8150
  ? config.targets.map((target) => ({ ...target, resources: TAB_KEYS }))
8076
8151
  : config.webhook.targets;
8077
8152
  let targets = mappedTargets.filter((target) =>
8078
- (!selectedHost || target.host === selectedHost) && (event === "installation" || names.has(target.repo)));
8153
+ (!selectedHost || target.host === selectedHost) && (event === "installation" || names.has(target.repo.toLowerCase())));
8079
8154
  if (["installation", "installation_repositories"].includes(event)) {
8080
8155
  targets = targets.filter((target) => {
8081
8156
  const provider = config.providers[target.provider];
@@ -8085,7 +8160,10 @@ function mapWebhookInvalidations({ event, payload, config, enterpriseHost = null
8085
8160
  }
8086
8161
  if (!selectedHost && event !== "installation") {
8087
8162
  const hostsByRepo = new Map();
8088
- for (const target of targets) hostsByRepo.set(target.repo, (hostsByRepo.get(target.repo) ?? new Set()).add(target.host));
8163
+ for (const target of targets) {
8164
+ const repo = target.repo.toLowerCase();
8165
+ hostsByRepo.set(repo, (hostsByRepo.get(repo) ?? new Set()).add(target.host));
8166
+ }
8089
8167
  if ([...hostsByRepo.values()].some((hosts) => hosts.size !== 1)) return { ok: false, reason: "target" };
8090
8168
  }
8091
8169
  if (targets.length === 0) return ["installation", "installation_repositories"].includes(event)
@@ -11268,6 +11346,11 @@ async function runDoctor({ probeEndpoints = false } = {}) {
11268
11346
  .filter(Boolean).sort((left, right) => right.at - left.at)[0] ?? null;
11269
11347
  const lastFailedRecovery = [...(recoveryJournal?.entries ?? [])].reverse()
11270
11348
  .find((entry) => entry.code !== "clear") ?? null;
11349
+ const ledgerPath = cachedQuota?.path ?? diagnosticScope?.path ?? null;
11350
+ // The journal cannot speak for a ledger that cannot be read, so the cached
11351
+ // read's own reason counts too; both use the governor's reason vocabulary.
11352
+ const hardRecovery = Boolean(ledgerPath) &&
11353
+ [currentRecovery?.code, cachedQuota?.reason].some((reason) => HARD_STORAGE_REASONS.includes(reason));
11271
11354
  const retryDates = RATE_RESOURCES.flatMap((resource) => {
11272
11355
  const active = recoveryJournal?.current[resource];
11273
11356
  const observer = (probeEndpoints ? governorResult : cachedQuota?.result)?.value?.observers[resource];
@@ -11324,11 +11407,16 @@ async function runDoctor({ probeEndpoints = false } = {}) {
11324
11407
  "",
11325
11408
  ...section("API governor"),
11326
11409
  field("cached scope", probeEndpoints ? "verified by probe" : cachedQuota?.ok ? "read-only cached identity" : `unavailable (${cachedQuota?.reason ?? "unknown"})`),
11327
- ...(cachedQuota?.path && !cachedQuota.ok
11328
- ? [field("state file", `${join(dirname(cachedQuota.path), "quota-[redacted].json")} (quota fingerprint withheld)`)]
11410
+ // The user has to find the file to move it.
11411
+ ...(ledgerPath && cachedQuota?.ok === false || hardRecovery
11412
+ ? [field("state file", `${join(dirname(ledgerPath), "quota-[redacted].json")} (quota fingerprint withheld)`)]
11329
11413
  : []),
11414
+ ...(hardRecovery ? [field("repair", LEDGER_REPAIR_ADVICE)] : []),
11330
11415
  field("status", governor.status),
11331
11416
  field("live leases", governor.leases),
11417
+ field("debt groups", governor.debtGroups == null
11418
+ ? "unavailable"
11419
+ : `${governor.debtGroups}/${GOVERNOR_MAX_DEBT_GROUPS} (${governor.quiescentGroups} quiescent)`),
11332
11420
  ...RATE_RESOURCES.map((resource) => {
11333
11421
  const detail = governor.resources[resource];
11334
11422
  return field(
@@ -15118,7 +15206,7 @@ function createAcquisitionEngine({
15118
15206
  ledger.debt[resource].unresolvedUnits + ledger.debt[resource].quiescentUnits +
15119
15207
  reservationCost(ledger.controlReceipts[resource], resource, ledger.leases, now()))]));
15120
15208
  if (Object.values(units).some((value) => !Number.isSafeInteger(value) || value < 0)) {
15121
- return { ok: false, reason: "accounting-overflow" };
15209
+ return ACCOUNTING_OVERFLOW;
15122
15210
  }
15123
15211
  return transact((state) => {
15124
15212
  const activeKeys = new Set(Object.values(state.subscriptions)
@@ -15699,6 +15787,14 @@ function coordinationNotice(reason) {
15699
15787
  const RECOVERY_JOURNAL_MAX_ENTRIES = 128;
15700
15788
  const RECOVERY_JOURNAL_MAX_BYTES = 128 * 1024;
15701
15789
  const RECOVERY_ENTRY_MAX_BYTES = 1024;
15790
+ // Reasons that no retry can clear: the ledger itself must be repaired. The
15791
+ // runtime keeps them sticky and doctor names the manual remedy for them.
15792
+ const HARD_STORAGE_REASONS = Object.freeze(["corrupt", "unwritable", "capacity", "accounting-overflow"]);
15793
+ // A source failure on a tab may be superseded by these coordination causes.
15794
+ const STICKY_OVER_SOURCE_REASONS = Object.freeze([...HARD_STORAGE_REASONS, "receipt-retry", "interrupted-request"]);
15795
+ const LEDGER_REPAIR_ADVICE = "quit every gh-glance pane, then move the state file, its .recovery.json and " +
15796
+ "registry.json out of that directory and relaunch; a fresh ledger starts, and at most the receipt and " +
15797
+ "debt units below (when shown) may be spent twice this hour";
15702
15798
  const RECOVERY_COPY = Object.freeze({
15703
15799
  busy: ["Coordinating with your other panes", "Retry automatically", "Other pane; retry"],
15704
15800
  stale: ["Coordinating with your other panes", "Retry automatically", "Other pane; retry"],
@@ -15708,6 +15804,7 @@ const RECOVERY_COPY = Object.freeze({
15708
15804
  corrupt: ["Local coordination is corrupt", "Run gh-glance --doctor; preserve the reported state file", "State corrupt; doctor"],
15709
15805
  capacity: ["Local coordination is full", "Run gh-glance --doctor; preserve the reported state file", "State full; doctor"],
15710
15806
  "accounting-overflow": ["Local coordination accounting overflow", "Run gh-glance --doctor; preserve the reported state file", "Overflow; doctor"],
15807
+ "debt-journal-full": ["Local coordination journal is full", "Retry automatically; older requests settle within an hour", "Journal full; retry"],
15711
15808
  "reservations-invalid": ["Local coordination reservation invalid", "Run gh-glance --doctor; preserve the reported state file", "Reservation; doctor"],
15712
15809
  "external-factor-invalid": ["Local coordination budget invalid", "Run gh-glance --doctor; preserve the reported state file", "Budget invalid; doctor"],
15713
15810
  "pacing-invalid": ["Local coordination pacing invalid", "Run gh-glance --doctor; preserve the reported state file", "Pacing invalid; doctor"],
@@ -15791,8 +15888,8 @@ function shouldClearRecoveryCause(cause, { sourceSuccess = false, tab = null, sc
15791
15888
 
15792
15889
  function recoveryRetryAt(decision, budget, observer, nowMs) {
15793
15890
  if (budget?.blockUntil > nowMs) return budget.blockUntil;
15794
- if (["reservations-invalid", "external-factor-invalid", "pacing-invalid", "budget-resource",
15795
- "accounting-overflow", "corrupt", "unwritable", "capacity"].includes(decision?.reason)) return null;
15891
+ if ([...HARD_STORAGE_REASONS, "reservations-invalid", "external-factor-invalid", "pacing-invalid",
15892
+ "budget-resource"].includes(decision?.reason)) return null;
15796
15893
  const resetHold = ["budget-reset", "reset", "rate-limit"].includes(decision?.reason);
15797
15894
  if (decision?.mode === "probe" && !resetHold) {
15798
15895
  return observer?.nextAt > nowMs ? observer.nextAt : null;
@@ -17937,7 +18034,7 @@ function App({ onCreateRemote = () => {} } = {}) {
17937
18034
  // which is exactly when the key is most likely to be pressed.
17938
18035
  const request = manualRefreshRequest(refreshIntent(input));
17939
18036
  if (request) fetchTabRef.current?.(TABS[activeIndexRef.current].key, request);
17940
- } else if (input >= "1" && input <= String(TABS.length)) {
18037
+ } else if (input.length === 1 && input >= "1" && input <= String(TABS.length)) {
17941
18038
  setActiveIndex(Number(input) - 1);
17942
18039
  } else if (key.tab && key.shift) {
17943
18040
  setActiveIndex((i) => (i - 1 + TABS.length) % TABS.length);
@@ -18042,18 +18139,16 @@ function App({ onCreateRemote = () => {} } = {}) {
18042
18139
  reason, resource, at, retryAt, sourceAt: lastOkRef.current[key] ?? null,
18043
18140
  origin, tab: origin === "source" ? key : null,
18044
18141
  });
18045
- const storageFailures = ["corrupt", "unwritable", "capacity", "accounting-overflow"];
18046
- const hardFailure = storageFailures.includes(candidate.code);
18142
+ const hardFailure = HARD_STORAGE_REASONS.includes(candidate.code);
18047
18143
  // Contention does not prove that previously unreadable storage recovered.
18048
18144
  // The existing healthy-control/source-success paths clear that remedy.
18049
- if (!hardFailure && storageFailures.includes(previous?.code)) return;
18145
+ if (!hardFailure && HARD_STORAGE_REASONS.includes(previous?.code)) return;
18050
18146
  if (!hardFailure && previous?.code === "receipt-retry" &&
18051
18147
  terminalizationBacklogForResource(terminalizationBacklog, scope?.hash, resource) > 0) return;
18052
18148
  if (!hardFailure && previous?.code === "request-queue" &&
18053
18149
  cleanupQueueForResource(cleanupQueue, resource) > 0) return;
18054
18150
  if (previous?.origin === "source" && previous.tab === key && origin === "coordination" &&
18055
- !["corrupt", "unwritable", "capacity", "accounting-overflow",
18056
- "receipt-retry", "interrupted-request"].includes(candidate.code)) return;
18151
+ !STICKY_OVER_SOURCE_REASONS.includes(candidate.code)) return;
18057
18152
  if (previous?.code === candidate.code && previous?.retryAt === candidate.retryAt &&
18058
18153
  previous?.origin === candidate.origin && previous?.tab === candidate.tab &&
18059
18154
  at - previous.at < 60_000) return;
@@ -18166,8 +18261,11 @@ function App({ onCreateRemote = () => {} } = {}) {
18166
18261
  if (!refreshed?.ok && !tabReady) {
18167
18262
  const resource = RATE_RESOURCES.find((candidate) => tabRequestCost(key)[candidate] > 0 &&
18168
18263
  snapshot.value.observers[candidate]?.outcome !== "healthy");
18264
+ // A rejected mutation writes nothing, so the observer's stored nextAt
18265
+ // predates the failure; a failure that names its own retry time wins.
18169
18266
  pauseCoordination(key, refreshed?.reason, "observer", false,
18170
- resource ? snapshot.value.observers[resource]?.nextAt : null);
18267
+ Number.isFinite(refreshed?.retryAt) ? refreshed.retryAt
18268
+ : resource ? snapshot.value.observers[resource]?.nextAt : null);
18171
18269
  return;
18172
18270
  }
18173
18271
  publishGovernorEpochs(snapshot);
@@ -20169,7 +20267,7 @@ function App({ onCreateRemote = () => {} } = {}) {
20169
20267
  activeGovernorDecision?.cause)) {
20170
20268
  semanticStatus = refreshStatus({
20171
20269
  stale: true,
20172
- disconnected: data[tab.key] !== null && !runtimeIdentityCoordinator?.current(),
20270
+ disconnected: statusDisconnected,
20173
20271
  });
20174
20272
  }
20175
20273
 
@@ -20803,6 +20901,9 @@ export {
20803
20901
  GOVERNOR_MAX_LEASES,
20804
20902
  GOVERNOR_MAX_INTENTS,
20805
20903
  GOVERNOR_MAX_RESERVATIONS,
20904
+ GOVERNOR_MAX_DEBT_GROUPS,
20905
+ DEBT_JOURNAL_COMPACT_AT,
20906
+ UNKNOWN_DEBT_GROUP_ID,
20806
20907
  GOVERNOR_LOCK_ORPHAN_MS,
20807
20908
  GOVERNOR_LOCK_WAIT_MS,
20808
20909
  GOVERNOR_PROBE_DRAIN_MS,
@@ -20818,6 +20919,7 @@ export {
20818
20919
  normalizeGovernorState,
20819
20920
  serializeGovernorState,
20820
20921
  readGovernorState,
20922
+ readIdentityQuotaState,
20821
20923
  writeGovernorState,
20822
20924
  writeGovernorQuotaState,
20823
20925
  pidIsDead,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-glance",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "A live-refreshing GitHub dashboard for your terminal: Actions, Issues, Pull Requests, and Security alerts in one narrow pane",
5
5
  "type": "module",
6
6
  "bin": {