@adhdev/mesh-shared 1.0.82-rc.1 → 1.0.82-rc.11

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.
@@ -94,6 +94,20 @@ export interface MeshNodeFactsProviderQuota {
94
94
  * rather than presenting them as a freshly measured value.
95
95
  */
96
96
  lastGoodWindows?: boolean;
97
+ /**
98
+ * Unix ms the reporting node's NEXT scheduled retry of this transient
99
+ * failure is due (HTTP Retry-After wins when later). Re-stamped to the
100
+ * real backoff time on every failure — see daemon-core
101
+ * `updateFailureRetry`. Absent on persistent failures and on daemons
102
+ * predating the stamp.
103
+ */
104
+ retryAtMs?: number;
105
+ /**
106
+ * True once the node has spent its bounded transient-retry budget and
107
+ * stopped retrying; the next look is the hourly backfill. Readers must
108
+ * stop calling retained numbers "refreshing" (`assessQuotaFreshness`).
109
+ */
110
+ retryExhausted?: boolean;
97
111
  /**
98
112
  * Unix ms when the reporting node last ATTEMPTED a refresh of this
99
113
  * provider — deliberately distinct from `updatedAt`, which dates the
@@ -33,7 +33,7 @@
33
33
  */
34
34
  /** One normalized ownership entry: a repo-relative path, optionally a `**` subtree prefix. */
35
35
  export interface OwnedPathEntry {
36
- /** Repo-relative path with forward slashes, no leading `./`, no trailing slash. */
36
+ /** Repo-relative path with forward slashes, no leading `./`, no trailing slash; empty for the whole-repo subtree. */
37
37
  readonly path: string;
38
38
  /** True when this entry was declared as `<path>/**` (owns the whole subtree). */
39
39
  readonly subtree: boolean;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Quota snapshot freshness — the ONE decision every display surface shares.
3
+ *
4
+ * Why this lives in the leaf package: the dashboard chips (web-core
5
+ * `quotaWindowCue`), the `adhdev quota` CLI (daemon-core `quota/cli.ts`) and the
6
+ * coordinator-facing `mesh_status` fold (mcp-server `summarizeNodeQuota`) each
7
+ * carried a hand-copied version of this rule, and a fix to one left the other
8
+ * two telling a different story about the same snapshot. They all call
9
+ * `assessQuotaFreshness` now; only the WORDING stays per-surface.
10
+ *
11
+ * Pure: plain objects in, plain object out, no clock except the `now` argument.
12
+ */
13
+ import type { MeshNodeFactsProviderQuota } from './node-facts';
14
+ export type QuotaFreshnessCue = 'refreshing' | 'stale';
15
+ export interface QuotaFreshness {
16
+ /** Undefined = nothing to say (a fresh reading, or no retained numbers). */
17
+ cue?: QuotaFreshnessCue;
18
+ /**
19
+ * Only the USER can produce a new reading (aged-out Claude statusline;
20
+ * antigravity expired token — the daemon never redeems it). The surface
21
+ * should say what to run, not promise a self-heal.
22
+ */
23
+ userMustAct: boolean;
24
+ /**
25
+ * Unix ms by which the daemon will have looked again, for a `stale` reading
26
+ * the daemon re-checks on its own. Absent when the user must act, when the
27
+ * reading is `refreshing` (a retry is already scheduled), or when the check
28
+ * is already due.
29
+ */
30
+ nextCheckAt?: number;
31
+ }
32
+ /**
33
+ * How long past `metadata.retryAtMs` a retry still reads as "in flight". The
34
+ * retry timer fires at retryAtMs and the probe itself takes a few seconds; a
35
+ * retryAtMs well in the past with no newer attempt means no retry is coming.
36
+ */
37
+ export declare const QUOTA_RETRY_INFLIGHT_GRACE_MS = 60000;
38
+ /**
39
+ * The daemon's backfill horizon: a snapshot is re-probed once it is this old
40
+ * even on an idle machine. Mirrors daemon-core `QUOTA_ROUTABLE_MAX_AGE_MS`
41
+ * (quota/refresh.ts); daemon-core's quota-freshness test pins the two equal.
42
+ */
43
+ export declare const QUOTA_BACKFILL_HORIZON_MS: number;
44
+ /**
45
+ * Which freshness cue a snapshot's retained numbers should carry.
46
+ *
47
+ * - `stale` + `userMustAct` — nothing will refresh it without the user:
48
+ * · `no-data` on any provider EXCEPT codex (Claude's statusline aged out —
49
+ * a session must run). Codex's `no-data` is the daemon re-reading the
50
+ * rollout files itself (≈60s while a CLI is active, ≤60 min idle), so it
51
+ * is `stale` WITHOUT userMustAct;
52
+ * · antigravity `expired-token` — the daemon deliberately does not redeem
53
+ * the refresh token. Kimi's expired-token is NOT here: its CLI refreshes
54
+ * the token on its own cadence.
55
+ * - `refreshing` — retained numbers after a transient failure with a retry
56
+ * actually pending. ★Honest about the end of the retry budget: once the
57
+ * daemon has stopped retrying (`metadata.retryExhausted`, or `retryAtMs`
58
+ * long past with no newer attempt) the numbers are `stale` with the next
59
+ * scheduled check, not "refreshing" for the up-to-60 minutes until the
60
+ * hourly backfill. A snapshot with NO retry bookkeeping at all (a daemon
61
+ * predating the stamp) keeps the old `refreshing` reading.
62
+ */
63
+ export declare function assessQuotaFreshness(quota: MeshNodeFactsProviderQuota, now?: number): QuotaFreshness;
64
+ /** Whole minutes (min 1) until `nextCheckAt`, for the "next check ~Nm" wording. */
65
+ export declare function minutesUntilQuotaCheck(nextCheckAt: number, now?: number): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/mesh-shared",
3
- "version": "1.0.82-rc.1",
3
+ "version": "1.0.82-rc.11",
4
4
  "description": "ADHDev mesh-shared — pure mesh/git status normalizers shared by daemon-core and web-core",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export * from './git-normalize'
16
16
  export * from './session-normalize'
17
17
  export * from './node-normalize'
18
18
  export * from './node-facts'
19
+ export * from './quota-freshness'
19
20
  export * from './workspace-normalize'
20
21
  export * from './daemon-normalize'
21
22
  export * from './git-summarize'
@@ -326,7 +326,15 @@ export function foldKeyedDoc<T extends Record<string, unknown>>(held: T, delta:
326
326
  const heldList = heldCollections.get(path)
327
327
  if (change?.absent) {
328
328
  const [head, tail] = splitPath(path)
329
- if (!tail) delete next[head]
329
+ if (!tail) {
330
+ delete next[head]
331
+ continue
332
+ }
333
+ const parent = next[head]
334
+ if (!isRecord(parent) || !(tail in parent)) continue
335
+ const copy = { ...parent }
336
+ delete copy[tail]
337
+ next[head] = copy
330
338
  continue
331
339
  }
332
340
  if (!change && heldList === undefined) continue
package/src/node-facts.ts CHANGED
@@ -97,6 +97,20 @@ export interface MeshNodeFactsProviderQuota {
97
97
  * rather than presenting them as a freshly measured value.
98
98
  */
99
99
  lastGoodWindows?: boolean
100
+ /**
101
+ * Unix ms the reporting node's NEXT scheduled retry of this transient
102
+ * failure is due (HTTP Retry-After wins when later). Re-stamped to the
103
+ * real backoff time on every failure — see daemon-core
104
+ * `updateFailureRetry`. Absent on persistent failures and on daemons
105
+ * predating the stamp.
106
+ */
107
+ retryAtMs?: number
108
+ /**
109
+ * True once the node has spent its bounded transient-retry budget and
110
+ * stopped retrying; the next look is the hourly backfill. Readers must
111
+ * stop calling retained numbers "refreshing" (`assessQuotaFreshness`).
112
+ */
113
+ retryExhausted?: boolean
100
114
  /**
101
115
  * Unix ms when the reporting node last ATTEMPTED a refresh of this
102
116
  * provider — deliberately distinct from `updatedAt`, which dates the
@@ -34,7 +34,7 @@
34
34
 
35
35
  /** One normalized ownership entry: a repo-relative path, optionally a `**` subtree prefix. */
36
36
  export interface OwnedPathEntry {
37
- /** Repo-relative path with forward slashes, no leading `./`, no trailing slash. */
37
+ /** Repo-relative path with forward slashes, no leading `./`, no trailing slash; empty for the whole-repo subtree. */
38
38
  readonly path: string
39
39
  /** True when this entry was declared as `<path>/**` (owns the whole subtree). */
40
40
  readonly subtree: boolean
@@ -159,16 +159,14 @@ export function normalizeOwnedPaths(input: unknown): NormalizeOwnedPathsResult {
159
159
  /** True when `path` is exactly `base`, or (when `base` is a subtree entry) nested under it. */
160
160
  function entryContains(base: OwnedPathEntry, path: string): boolean {
161
161
  if (path === base.path) return true
162
- if (base.subtree && (path === base.path || path.startsWith(`${base.path}/`))) return true
162
+ // An empty subtree prefix is the repo root, so it contains every normalized path.
163
+ if (base.subtree && (base.path === '' || path.startsWith(`${base.path}/`))) return true
163
164
  return false
164
165
  }
165
166
 
166
167
  /** True when two normalized entries' claimed sets intersect (exact match, or one contains the other's directory). */
167
168
  function entriesOverlap(a: OwnedPathEntry, b: OwnedPathEntry): boolean {
168
- if (a.path === b.path) return true
169
- if (a.subtree && (b.path === a.path || b.path.startsWith(`${a.path}/`))) return true
170
- if (b.subtree && (a.path === b.path || a.path.startsWith(`${b.path}/`))) return true
171
- return false
169
+ return entryContains(a, b.path) || entryContains(b, a.path)
172
170
  }
173
171
 
174
172
  /** True when two declarations claim any overlapping path. Pure set comparison — no policy (refuse vs warn) here. */
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Quota snapshot freshness — the ONE decision every display surface shares.
3
+ *
4
+ * Why this lives in the leaf package: the dashboard chips (web-core
5
+ * `quotaWindowCue`), the `adhdev quota` CLI (daemon-core `quota/cli.ts`) and the
6
+ * coordinator-facing `mesh_status` fold (mcp-server `summarizeNodeQuota`) each
7
+ * carried a hand-copied version of this rule, and a fix to one left the other
8
+ * two telling a different story about the same snapshot. They all call
9
+ * `assessQuotaFreshness` now; only the WORDING stays per-surface.
10
+ *
11
+ * Pure: plain objects in, plain object out, no clock except the `now` argument.
12
+ */
13
+ import type { MeshNodeFactsProviderQuota } from './node-facts'
14
+
15
+ export type QuotaFreshnessCue = 'refreshing' | 'stale'
16
+
17
+ export interface QuotaFreshness {
18
+ /** Undefined = nothing to say (a fresh reading, or no retained numbers). */
19
+ cue?: QuotaFreshnessCue
20
+ /**
21
+ * Only the USER can produce a new reading (aged-out Claude statusline;
22
+ * antigravity expired token — the daemon never redeems it). The surface
23
+ * should say what to run, not promise a self-heal.
24
+ */
25
+ userMustAct: boolean
26
+ /**
27
+ * Unix ms by which the daemon will have looked again, for a `stale` reading
28
+ * the daemon re-checks on its own. Absent when the user must act, when the
29
+ * reading is `refreshing` (a retry is already scheduled), or when the check
30
+ * is already due.
31
+ */
32
+ nextCheckAt?: number
33
+ }
34
+
35
+ /**
36
+ * How long past `metadata.retryAtMs` a retry still reads as "in flight". The
37
+ * retry timer fires at retryAtMs and the probe itself takes a few seconds; a
38
+ * retryAtMs well in the past with no newer attempt means no retry is coming.
39
+ */
40
+ export const QUOTA_RETRY_INFLIGHT_GRACE_MS = 60_000
41
+
42
+ /**
43
+ * The daemon's backfill horizon: a snapshot is re-probed once it is this old
44
+ * even on an idle machine. Mirrors daemon-core `QUOTA_ROUTABLE_MAX_AGE_MS`
45
+ * (quota/refresh.ts); daemon-core's quota-freshness test pins the two equal.
46
+ */
47
+ export const QUOTA_BACKFILL_HORIZON_MS = 60 * 60 * 1000
48
+
49
+ function hasUsableWindow(window: { usedPercent?: unknown } | null | undefined): boolean {
50
+ return !!window && typeof window.usedPercent === 'number' && Number.isFinite(window.usedPercent)
51
+ }
52
+
53
+ /** Does the snapshot carry any renderable number — windows OR per-pool buckets? */
54
+ function hasReading(quota: MeshNodeFactsProviderQuota): boolean {
55
+ if (hasUsableWindow(quota.session) || hasUsableWindow(quota.weekly)) return true
56
+ return Array.isArray(quota.buckets) && quota.buckets.some((b) => !!b && typeof b.usedPercent === 'number' && Number.isFinite(b.usedPercent))
57
+ }
58
+
59
+ function finiteMs(value: unknown): number | undefined {
60
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined
61
+ }
62
+
63
+ /**
64
+ * When the daemon will next look, from the snapshot alone — the same
65
+ * "both clocks" rule the refresh loop's backfill uses (data age AND attempt
66
+ * age must both pass the horizon). Undefined when that moment has passed.
67
+ */
68
+ function backfillCheckAt(quota: MeshNodeFactsProviderQuota, now: number): number | undefined {
69
+ const dataDue = finiteMs(quota.updatedAt)
70
+ const attempted = finiteMs(quota.metadata?.fetchedAt)
71
+ const dueAt = Math.max(
72
+ dataDue === undefined ? Number.NEGATIVE_INFINITY : dataDue + QUOTA_BACKFILL_HORIZON_MS,
73
+ attempted === undefined ? Number.NEGATIVE_INFINITY : attempted + QUOTA_BACKFILL_HORIZON_MS,
74
+ )
75
+ return Number.isFinite(dueAt) && dueAt > now ? dueAt : undefined
76
+ }
77
+
78
+ /**
79
+ * Which freshness cue a snapshot's retained numbers should carry.
80
+ *
81
+ * - `stale` + `userMustAct` — nothing will refresh it without the user:
82
+ * · `no-data` on any provider EXCEPT codex (Claude's statusline aged out —
83
+ * a session must run). Codex's `no-data` is the daemon re-reading the
84
+ * rollout files itself (≈60s while a CLI is active, ≤60 min idle), so it
85
+ * is `stale` WITHOUT userMustAct;
86
+ * · antigravity `expired-token` — the daemon deliberately does not redeem
87
+ * the refresh token. Kimi's expired-token is NOT here: its CLI refreshes
88
+ * the token on its own cadence.
89
+ * - `refreshing` — retained numbers after a transient failure with a retry
90
+ * actually pending. ★Honest about the end of the retry budget: once the
91
+ * daemon has stopped retrying (`metadata.retryExhausted`, or `retryAtMs`
92
+ * long past with no newer attempt) the numbers are `stale` with the next
93
+ * scheduled check, not "refreshing" for the up-to-60 minutes until the
94
+ * hourly backfill. A snapshot with NO retry bookkeeping at all (a daemon
95
+ * predating the stamp) keeps the old `refreshing` reading.
96
+ */
97
+ export function assessQuotaFreshness(quota: MeshNodeFactsProviderQuota, now: number = Date.now()): QuotaFreshness {
98
+ const meta = quota.metadata
99
+ const kind = meta?.failureKind
100
+ const userMustAct = (kind === 'no-data' && quota.provider !== 'codex-cli')
101
+ || (kind === 'expired-token' && quota.provider === 'antigravity-cli')
102
+ // Order matters: the aged-out Claude shape and the retained antigravity
103
+ // shape both ALSO mark lastGoodWindows (mesh routing trusts retained numbers
104
+ // until their reset), so they must be classified before the generic branch.
105
+ if (userMustAct && hasReading(quota)) return { cue: 'stale', userMustAct: true }
106
+ if (meta?.lastGoodWindows !== true) return { userMustAct }
107
+
108
+ const retryAtMs = finiteMs(meta?.retryAtMs)
109
+ const retryStopped = meta?.retryExhausted === true
110
+ || (retryAtMs !== undefined && retryAtMs + QUOTA_RETRY_INFLIGHT_GRACE_MS < now)
111
+ // A `no-data` retained reading (codex) has no retry at all — it is a
112
+ // snapshot the daemon will simply look at again, never an in-flight refresh.
113
+ if (kind !== 'no-data' && !retryStopped) return { cue: 'refreshing', userMustAct: false }
114
+ const nextCheckAt = backfillCheckAt(quota, now)
115
+ return nextCheckAt === undefined
116
+ ? { cue: 'stale', userMustAct: false }
117
+ : { cue: 'stale', userMustAct: false, nextCheckAt }
118
+ }
119
+
120
+ /** Whole minutes (min 1) until `nextCheckAt`, for the "next check ~Nm" wording. */
121
+ export function minutesUntilQuotaCheck(nextCheckAt: number, now: number = Date.now()): number {
122
+ return Math.max(1, Math.round((nextCheckAt - now) / 60_000))
123
+ }