@adhdev/mesh-shared 1.0.56-rc.8 → 1.0.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +200 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +189 -0
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-tool-names.d.ts +2 -2
- package/dist/node-facts.d.ts +64 -5
- package/dist/rpc-chunking.d.ts +162 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/mesh-tool-names.ts +1 -0
- package/src/node-facts.ts +65 -5
- package/src/rpc-chunking.ts +336 -0
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Order mirrors mcp-server `ALL_MESH_TOOLS` for easy visual diffing, but the consistency
|
|
19
19
|
* checks are set-based (order-insensitive).
|
|
20
20
|
*/
|
|
21
|
-
export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_graph_view", "mesh_graph_gate_claim", "mesh_graph_gate_release", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_plan_onboarding", "mesh_create", "mesh_add_node", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_refine_config", "mesh_change_impact_config", "mesh_init", "mesh_reinit", "mesh_write_mesh_json_config", "mesh_refine_plan", "mesh_cleanup_sessions", "mesh_prune_stale_direct", "mesh_task_history", "mesh_ledger_query", "mesh_record_note", "mesh_forget_note", "mesh_reconcile_ledger", "mesh_requeue_held_events", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_magi_review", "mesh_magi_collect", "mesh_magi_kind_panel_set", "mesh_magi_kind_panel_list", "mesh_node_slots_set", "mesh_node_slots_list", "mesh_node_slots_propose", "mesh_coordinator_prompt_append_get", "mesh_coordinator_prompt_append_set"];
|
|
21
|
+
export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_graph_view", "mesh_graph_gate_claim", "mesh_graph_gate_release", "mesh_graph_gate_abandon", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_plan_onboarding", "mesh_create", "mesh_add_node", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_refine_config", "mesh_change_impact_config", "mesh_init", "mesh_reinit", "mesh_write_mesh_json_config", "mesh_refine_plan", "mesh_cleanup_sessions", "mesh_prune_stale_direct", "mesh_task_history", "mesh_ledger_query", "mesh_record_note", "mesh_forget_note", "mesh_reconcile_ledger", "mesh_requeue_held_events", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_magi_review", "mesh_magi_collect", "mesh_magi_kind_panel_set", "mesh_magi_kind_panel_list", "mesh_node_slots_set", "mesh_node_slots_list", "mesh_node_slots_propose", "mesh_coordinator_prompt_append_get", "mesh_coordinator_prompt_append_set"];
|
|
22
22
|
export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
|
|
23
23
|
/** The count the `NN tools` barrel doc comments and consistency test assert against. */
|
|
24
|
-
export declare const CANONICAL_MESH_TOOL_COUNT:
|
|
24
|
+
export declare const CANONICAL_MESH_TOOL_COUNT: 59;
|
package/dist/node-facts.d.ts
CHANGED
|
@@ -75,10 +75,42 @@ export interface MeshNodeFactsProviderQuota {
|
|
|
75
75
|
* rather than presenting them as a freshly measured value.
|
|
76
76
|
*/
|
|
77
77
|
lastGoodWindows?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Unix ms when the reporting node last ATTEMPTED a refresh of this
|
|
80
|
+
* provider — deliberately distinct from `updatedAt`, which dates the
|
|
81
|
+
* DATA. They differ for file-source providers (claude-cli reports its
|
|
82
|
+
* statusline snapshot's capture time, codex-cli the rollout entry's),
|
|
83
|
+
* whose `updatedAt` does not move while the source file is unchanged
|
|
84
|
+
* however often it is successfully re-read.
|
|
85
|
+
*
|
|
86
|
+
* A reader judging FRESHNESS wants `updatedAt`. This field answers the
|
|
87
|
+
* different question "is that node still looking?", which is what makes
|
|
88
|
+
* "3h old but re-checked a minute ago" distinguishable from "3h old and
|
|
89
|
+
* nobody has looked since". Absent on entries written by daemons
|
|
90
|
+
* predating the field.
|
|
91
|
+
*/
|
|
92
|
+
fetchedAt?: number;
|
|
78
93
|
[extra: string]: unknown;
|
|
79
94
|
};
|
|
80
95
|
[extra: string]: unknown;
|
|
81
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* One provider's enablement state on the reporting machine — see
|
|
99
|
+
* `MeshNodeFacts.providerEnablement`.
|
|
100
|
+
*
|
|
101
|
+
* Both fields are REQUIRED booleans on the wire even though the underlying
|
|
102
|
+
* config defaults are asymmetric (`enabled` defaults false, `quotaEnabled`
|
|
103
|
+
* defaults true). The producer resolves those defaults before stamping, so a
|
|
104
|
+
* reader never re-derives them — a second copy of the default rules is exactly
|
|
105
|
+
* the drift this shape avoids. Absence is expressed by omitting the whole
|
|
106
|
+
* provider entry (or the whole bundle field), never by a missing sub-field.
|
|
107
|
+
*/
|
|
108
|
+
export interface MeshNodeFactsProviderEnablement {
|
|
109
|
+
/** "This machine uses provider X" — gates launching and mesh claims. */
|
|
110
|
+
enabled: boolean;
|
|
111
|
+
/** "...and its quota is probed here" — an independent user opt-out. */
|
|
112
|
+
quotaEnabled: boolean;
|
|
113
|
+
}
|
|
82
114
|
export interface MeshNodeFacts {
|
|
83
115
|
schemaVersion: number;
|
|
84
116
|
reportedAt: number;
|
|
@@ -118,6 +150,31 @@ export interface MeshNodeFacts {
|
|
|
118
150
|
* rule: quotaSnapshotAgeMs in daemon-core mesh-quota-routing.ts).
|
|
119
151
|
*/
|
|
120
152
|
quota?: Record<string, MeshNodeFactsProviderQuota>;
|
|
153
|
+
/**
|
|
154
|
+
* Per-provider ENABLEMENT state on the reporting machine, keyed the same
|
|
155
|
+
* way as `quota`. Exists because `quota` alone cannot answer "why is there
|
|
156
|
+
* no snapshot": a provider that is disabled — on either axis — is pruned
|
|
157
|
+
* from the quota cache entirely (daemon-core quota/refresh.ts drops it and
|
|
158
|
+
* refuses to restore it from disk), so a deliberate opt-out and a
|
|
159
|
+
* never-yet-measured provider both arrive as the SAME absent entry. On the
|
|
160
|
+
* node that owns the config that ambiguity is resolvable by reading the
|
|
161
|
+
* config; for every OTHER node in the mesh it was not resolvable at all,
|
|
162
|
+
* which is what this field fixes.
|
|
163
|
+
*
|
|
164
|
+
* The two axes mirror ProviderLoader exactly and are INDEPENDENT:
|
|
165
|
+
* `enabled` is "this machine uses provider X" (gates launching and mesh
|
|
166
|
+
* claims), `quotaEnabled` gates ONLY the quota probe — a machine can use a
|
|
167
|
+
* provider and still opt out of having its usage read.
|
|
168
|
+
*
|
|
169
|
+
* ★An ABSENT bundle field means "this node did not tell us" — a daemon too
|
|
170
|
+
* old to send it — and must NEVER be read as "disabled". The consumer
|
|
171
|
+
* (daemon-core mesh-quota-routing.ts classifyAbsentQuotaReason) keeps its
|
|
172
|
+
* unclassified fallback for exactly that case; treating absence as
|
|
173
|
+
* disabled would invent a fail-closed verdict out of a missing field.
|
|
174
|
+
*
|
|
175
|
+
* Booleans keyed by provider type only — no free text, no credentials.
|
|
176
|
+
*/
|
|
177
|
+
providerEnablement?: Record<string, MeshNodeFactsProviderEnablement>;
|
|
121
178
|
/** Future fields ride through opaquely — do not enumerate them in relays. */
|
|
122
179
|
[extra: string]: unknown;
|
|
123
180
|
}
|
|
@@ -159,11 +216,13 @@ export declare function normalizeMeshNodeFacts(raw: unknown): MeshNodeFacts | un
|
|
|
159
216
|
* ★antigravity-cli was likewise twice judged impossible, from reading
|
|
160
217
|
* `agy --help` where the usage view does not appear because it is a TUI view.
|
|
161
218
|
* Its quota comes from the SHARED Gemini Code Assist backend
|
|
162
|
-
* (`cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary`
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
219
|
+
* (`daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary` —
|
|
220
|
+
* the host `agy` itself uses; the unprefixed `cloudcode-pa` host 429s Google
|
|
221
|
+
* AI Pro Antigravity accounts), and its credential lives in the OS keyring,
|
|
222
|
+
* not the stale on-disk token file — see the provenance note in daemon-core
|
|
223
|
+
* `quota/fetchers/antigravity.ts`. Supported on macOS and Windows; other
|
|
224
|
+
* platforms report `unsupported` rather than guess at a keyring backend
|
|
225
|
+
* nobody has verified.
|
|
167
226
|
*
|
|
168
227
|
* ★Adding a provider here without adding its fetcher to REFRESHERS re-creates
|
|
169
228
|
* exactly the "switch that does nothing" this constant prevents.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh RPC frame chunking — split/reassemble oversized mesh envelopes.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* The mesh DataChannel path (`daemon-mesh-manager`) writes every RPC envelope as a
|
|
6
|
+
* SINGLE `dc.sendMessage(JSON.stringify(envelope))` frame. That was fine while every
|
|
7
|
+
* mesh arg was small text, but a coordinator dispatching an IMAGE to a worker puts a
|
|
8
|
+
* multi-MB base64 part inside `args` — far past what one DataChannel frame carries, so
|
|
9
|
+
* the send throws or the frame is dropped and the dispatch silently dies.
|
|
10
|
+
*
|
|
11
|
+
* The dashboard P2P path already solved exactly this problem and has been running in
|
|
12
|
+
* production: `packages/daemon-cloud/src/daemon-p2p/data-channel-router.ts` (send +
|
|
13
|
+
* reassemble) and `packages/web-cloud/src/p2p.ts` (browser side). This module is a PORT
|
|
14
|
+
* of that proven scheme onto the mesh envelope shape — deliberately NOT a new protocol.
|
|
15
|
+
* The boundary values are carried over unchanged (see the constants below for why each
|
|
16
|
+
* one is what it is).
|
|
17
|
+
*
|
|
18
|
+
* It lives in mesh-shared because it is pure string/JSON work with no transport, no
|
|
19
|
+
* Node API and no DOM API — the same reason the normalizers live here. `daemon-cloud`
|
|
20
|
+
* (sender/receiver) is the only current consumer, but keeping it in the pure leaf means
|
|
21
|
+
* the standalone side can reassemble with the identical code rather than a hand-synced
|
|
22
|
+
* copy, which is the drift bug class this package was created to kill.
|
|
23
|
+
*
|
|
24
|
+
* FAILURE POLICY (DoD): reassembly never degrades silently. A malformed, out-of-range,
|
|
25
|
+
* over-budget or unparseable chunk stream yields an explicit typed failure that the
|
|
26
|
+
* caller turns into an RPC error — never a partially-applied or truncated envelope.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Inline ceiling: an envelope whose JSON is at or below this goes out as one frame,
|
|
30
|
+
* exactly as before this module existed. 60_000 (not 65_536) matches the dashboard
|
|
31
|
+
* path and leaves headroom under the common 64KiB SCTP message limit so the frame is
|
|
32
|
+
* never the thing that trips a transport-level cap.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MESH_MAX_INLINE_FRAME_BYTES = 60000;
|
|
35
|
+
/**
|
|
36
|
+
* Per-chunk payload slice, in CHARACTERS. Ported unchanged from the dashboard path.
|
|
37
|
+
*
|
|
38
|
+
* Why 16_000 chars is safe under a 60_000-BYTE ceiling, with ~3.75x of headroom:
|
|
39
|
+
* `splitMeshFrame` slices the ALREADY-SERIALIZED outer frame, so the text being cut
|
|
40
|
+
* is JSON output — control characters are pre-expanded to `\u00XX` ASCII before they
|
|
41
|
+
* ever reach the slicer. Re-escaping that slice inside the chunk envelope can at worst
|
|
42
|
+
* double the backslashes. Measured worst cases for a 16_000-char slice:
|
|
43
|
+
* backslashes / astral emoji → 32,073 bytes
|
|
44
|
+
* Korean (3-byte, unescaped) → 48,073 bytes ← densest observed
|
|
45
|
+
* plain ASCII → 16,073 bytes
|
|
46
|
+
* All are under MESH_MAX_INLINE_FRAME_BYTES. `splitMeshFrame` still MEASURES each
|
|
47
|
+
* envelope and shrinks on overflow, so the guarantee is enforced rather than assumed
|
|
48
|
+
* if either constant is ever retuned — but at these values that path is unreachable,
|
|
49
|
+
* which is exactly what `assertMeshChunkConstantsAreSafe` pins down.
|
|
50
|
+
*/
|
|
51
|
+
export declare const MESH_CHUNK_PAYLOAD_CHARS = 16000;
|
|
52
|
+
/**
|
|
53
|
+
* Hard cap on chunk count for one frame. 1024 × ~16KB ≈ 16MB of transferable payload,
|
|
54
|
+
* which comfortably covers a screenshot while bounding what a single peer can make the
|
|
55
|
+
* receiver buffer. Exceeding it is an explicit refusal, never a truncated send.
|
|
56
|
+
*/
|
|
57
|
+
export declare const MESH_MAX_CHUNKS = 1024;
|
|
58
|
+
/**
|
|
59
|
+
* Reassembly budget for one frame, in bytes. Bounds receiver memory independently of
|
|
60
|
+
* chunk count so a peer cannot send 1024 maximally-large chunks to force an oversized
|
|
61
|
+
* allocation. Mirrors the dashboard receiver's MAX_REASSEMBLED_JSON_BYTES.
|
|
62
|
+
*/
|
|
63
|
+
export declare const MESH_MAX_REASSEMBLED_BYTES = 16000000;
|
|
64
|
+
/**
|
|
65
|
+
* How long a partially-received frame is retained. A sender that dies mid-stream must
|
|
66
|
+
* not pin receiver memory forever; the partial is swept and the frame simply never
|
|
67
|
+
* completes (the RPC's own deadline then reports it).
|
|
68
|
+
*/
|
|
69
|
+
export declare const MESH_CHUNK_TTL_MS = 60000;
|
|
70
|
+
/** Envelope `kind` for a chunk of a larger mesh frame. */
|
|
71
|
+
export declare const MESH_CHUNK_KIND = "rpc_chunk";
|
|
72
|
+
export interface MeshChunkEnvelope {
|
|
73
|
+
v: number;
|
|
74
|
+
kind: typeof MESH_CHUNK_KIND;
|
|
75
|
+
/** Groups the chunks of one logical frame. */
|
|
76
|
+
chunkId: string;
|
|
77
|
+
/** 0-based position of this chunk. */
|
|
78
|
+
index: number;
|
|
79
|
+
/** Total chunk count for this frame; constant across the group. */
|
|
80
|
+
total: number;
|
|
81
|
+
/** The slice of the original envelope JSON. */
|
|
82
|
+
data: string;
|
|
83
|
+
}
|
|
84
|
+
export type MeshChunkSplitResult = {
|
|
85
|
+
ok: true;
|
|
86
|
+
chunks: MeshChunkEnvelope[];
|
|
87
|
+
} | {
|
|
88
|
+
ok: false;
|
|
89
|
+
reason: 'too_many_chunks' | 'chunk_too_large';
|
|
90
|
+
detail: string;
|
|
91
|
+
};
|
|
92
|
+
export type MeshChunkAcceptResult =
|
|
93
|
+
/** Chunk stored; the frame is not complete yet. */
|
|
94
|
+
{
|
|
95
|
+
status: 'partial';
|
|
96
|
+
received: number;
|
|
97
|
+
total: number;
|
|
98
|
+
}
|
|
99
|
+
/** Final chunk landed and the frame parsed cleanly. */
|
|
100
|
+
| {
|
|
101
|
+
status: 'complete';
|
|
102
|
+
frame: unknown;
|
|
103
|
+
}
|
|
104
|
+
/** Explicitly rejected — the caller must surface this, never ignore it. */
|
|
105
|
+
| {
|
|
106
|
+
status: 'failed';
|
|
107
|
+
reason: MeshChunkFailureReason;
|
|
108
|
+
detail: string;
|
|
109
|
+
chunkId: string;
|
|
110
|
+
};
|
|
111
|
+
export type MeshChunkFailureReason = 'malformed_chunk' | 'too_many_chunks' | 'inconsistent_total' | 'duplicate_chunk_mismatch' | 'budget_exceeded' | 'reassembled_parse_failed';
|
|
112
|
+
/** UTF-8 byte length without assuming Buffer or TextEncoder is present. */
|
|
113
|
+
export declare function meshUtf8ByteLength(value: string): number;
|
|
114
|
+
/**
|
|
115
|
+
* Worst-case size of one chunk envelope at the current constants.
|
|
116
|
+
*
|
|
117
|
+
* Exported so the safety margin documented on MESH_CHUNK_PAYLOAD_CHARS is a CHECKED
|
|
118
|
+
* invariant rather than a comment that rots. The densest slice the splitter can produce
|
|
119
|
+
* is 3-byte unescaped text (JSON.stringify leaves non-ASCII as-is), so that is what this
|
|
120
|
+
* measures. If someone raises MESH_CHUNK_PAYLOAD_CHARS or lowers the frame ceiling past
|
|
121
|
+
* the safe point, the accompanying test fails loudly instead of the overflow only
|
|
122
|
+
* showing up as dropped frames on a live mesh.
|
|
123
|
+
*/
|
|
124
|
+
export declare function measureWorstCaseChunkEnvelopeBytes(): number;
|
|
125
|
+
/** True when the serialized frame must be chunked rather than sent inline. */
|
|
126
|
+
export declare function meshFrameNeedsChunking(json: string): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Split a serialized mesh envelope into chunk envelopes.
|
|
129
|
+
*
|
|
130
|
+
* Each slice is measured AS ITS FINAL SERIALIZED ENVELOPE and shrunk (×0.8, as in the
|
|
131
|
+
* dashboard implementation) until it fits the inline ceiling — so multi-byte UTF-8 and
|
|
132
|
+
* the envelope overhead are both accounted for rather than assumed away. `total` is
|
|
133
|
+
* stamped only after the full split is known, so every chunk in a group agrees.
|
|
134
|
+
*/
|
|
135
|
+
export declare function splitMeshFrame(json: string, chunkId: string, version: number): MeshChunkSplitResult;
|
|
136
|
+
/**
|
|
137
|
+
* Receiver-side reassembly buffer, one per peer connection.
|
|
138
|
+
*
|
|
139
|
+
* Keyed by chunkId only — callers construct one assembler per peer, so chunk groups
|
|
140
|
+
* from different peers can never collide in the same map.
|
|
141
|
+
*/
|
|
142
|
+
export declare class MeshChunkAssembler {
|
|
143
|
+
private readonly now;
|
|
144
|
+
private readonly buffers;
|
|
145
|
+
constructor(now?: () => number);
|
|
146
|
+
/** True when the frame is a chunk envelope this assembler should handle. */
|
|
147
|
+
static isChunkFrame(frame: unknown): boolean;
|
|
148
|
+
/** Drop partials older than the TTL so a dead sender cannot pin memory. */
|
|
149
|
+
private sweep;
|
|
150
|
+
/** Discard any partial state for a peer (call on disconnect). */
|
|
151
|
+
reset(): void;
|
|
152
|
+
/** Number of frames currently mid-reassembly — for tests and diagnostics. */
|
|
153
|
+
get pendingCount(): number;
|
|
154
|
+
/**
|
|
155
|
+
* Accept one chunk envelope.
|
|
156
|
+
*
|
|
157
|
+
* Never throws and never returns a partially-applied frame: the result is exactly one
|
|
158
|
+
* of partial / complete / failed, and `failed` carries a typed reason the transport
|
|
159
|
+
* turns into an explicit RPC error.
|
|
160
|
+
*/
|
|
161
|
+
accept(frame: unknown): MeshChunkAcceptResult;
|
|
162
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/mesh-tool-names.ts
CHANGED
package/src/node-facts.ts
CHANGED
|
@@ -78,11 +78,44 @@ export interface MeshNodeFactsProviderQuota {
|
|
|
78
78
|
* rather than presenting them as a freshly measured value.
|
|
79
79
|
*/
|
|
80
80
|
lastGoodWindows?: boolean
|
|
81
|
+
/**
|
|
82
|
+
* Unix ms when the reporting node last ATTEMPTED a refresh of this
|
|
83
|
+
* provider — deliberately distinct from `updatedAt`, which dates the
|
|
84
|
+
* DATA. They differ for file-source providers (claude-cli reports its
|
|
85
|
+
* statusline snapshot's capture time, codex-cli the rollout entry's),
|
|
86
|
+
* whose `updatedAt` does not move while the source file is unchanged
|
|
87
|
+
* however often it is successfully re-read.
|
|
88
|
+
*
|
|
89
|
+
* A reader judging FRESHNESS wants `updatedAt`. This field answers the
|
|
90
|
+
* different question "is that node still looking?", which is what makes
|
|
91
|
+
* "3h old but re-checked a minute ago" distinguishable from "3h old and
|
|
92
|
+
* nobody has looked since". Absent on entries written by daemons
|
|
93
|
+
* predating the field.
|
|
94
|
+
*/
|
|
95
|
+
fetchedAt?: number
|
|
81
96
|
[extra: string]: unknown
|
|
82
97
|
}
|
|
83
98
|
[extra: string]: unknown
|
|
84
99
|
}
|
|
85
100
|
|
|
101
|
+
/**
|
|
102
|
+
* One provider's enablement state on the reporting machine — see
|
|
103
|
+
* `MeshNodeFacts.providerEnablement`.
|
|
104
|
+
*
|
|
105
|
+
* Both fields are REQUIRED booleans on the wire even though the underlying
|
|
106
|
+
* config defaults are asymmetric (`enabled` defaults false, `quotaEnabled`
|
|
107
|
+
* defaults true). The producer resolves those defaults before stamping, so a
|
|
108
|
+
* reader never re-derives them — a second copy of the default rules is exactly
|
|
109
|
+
* the drift this shape avoids. Absence is expressed by omitting the whole
|
|
110
|
+
* provider entry (or the whole bundle field), never by a missing sub-field.
|
|
111
|
+
*/
|
|
112
|
+
export interface MeshNodeFactsProviderEnablement {
|
|
113
|
+
/** "This machine uses provider X" — gates launching and mesh claims. */
|
|
114
|
+
enabled: boolean
|
|
115
|
+
/** "...and its quota is probed here" — an independent user opt-out. */
|
|
116
|
+
quotaEnabled: boolean
|
|
117
|
+
}
|
|
118
|
+
|
|
86
119
|
export interface MeshNodeFacts {
|
|
87
120
|
schemaVersion: number
|
|
88
121
|
reportedAt: number
|
|
@@ -122,6 +155,31 @@ export interface MeshNodeFacts {
|
|
|
122
155
|
* rule: quotaSnapshotAgeMs in daemon-core mesh-quota-routing.ts).
|
|
123
156
|
*/
|
|
124
157
|
quota?: Record<string, MeshNodeFactsProviderQuota>
|
|
158
|
+
/**
|
|
159
|
+
* Per-provider ENABLEMENT state on the reporting machine, keyed the same
|
|
160
|
+
* way as `quota`. Exists because `quota` alone cannot answer "why is there
|
|
161
|
+
* no snapshot": a provider that is disabled — on either axis — is pruned
|
|
162
|
+
* from the quota cache entirely (daemon-core quota/refresh.ts drops it and
|
|
163
|
+
* refuses to restore it from disk), so a deliberate opt-out and a
|
|
164
|
+
* never-yet-measured provider both arrive as the SAME absent entry. On the
|
|
165
|
+
* node that owns the config that ambiguity is resolvable by reading the
|
|
166
|
+
* config; for every OTHER node in the mesh it was not resolvable at all,
|
|
167
|
+
* which is what this field fixes.
|
|
168
|
+
*
|
|
169
|
+
* The two axes mirror ProviderLoader exactly and are INDEPENDENT:
|
|
170
|
+
* `enabled` is "this machine uses provider X" (gates launching and mesh
|
|
171
|
+
* claims), `quotaEnabled` gates ONLY the quota probe — a machine can use a
|
|
172
|
+
* provider and still opt out of having its usage read.
|
|
173
|
+
*
|
|
174
|
+
* ★An ABSENT bundle field means "this node did not tell us" — a daemon too
|
|
175
|
+
* old to send it — and must NEVER be read as "disabled". The consumer
|
|
176
|
+
* (daemon-core mesh-quota-routing.ts classifyAbsentQuotaReason) keeps its
|
|
177
|
+
* unclassified fallback for exactly that case; treating absence as
|
|
178
|
+
* disabled would invent a fail-closed verdict out of a missing field.
|
|
179
|
+
*
|
|
180
|
+
* Booleans keyed by provider type only — no free text, no credentials.
|
|
181
|
+
*/
|
|
182
|
+
providerEnablement?: Record<string, MeshNodeFactsProviderEnablement>
|
|
125
183
|
/** Future fields ride through opaquely — do not enumerate them in relays. */
|
|
126
184
|
[extra: string]: unknown
|
|
127
185
|
}
|
|
@@ -173,11 +231,13 @@ export function normalizeMeshNodeFacts(raw: unknown): MeshNodeFacts | undefined
|
|
|
173
231
|
* ★antigravity-cli was likewise twice judged impossible, from reading
|
|
174
232
|
* `agy --help` where the usage view does not appear because it is a TUI view.
|
|
175
233
|
* Its quota comes from the SHARED Gemini Code Assist backend
|
|
176
|
-
* (`cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary`
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
234
|
+
* (`daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary` —
|
|
235
|
+
* the host `agy` itself uses; the unprefixed `cloudcode-pa` host 429s Google
|
|
236
|
+
* AI Pro Antigravity accounts), and its credential lives in the OS keyring,
|
|
237
|
+
* not the stale on-disk token file — see the provenance note in daemon-core
|
|
238
|
+
* `quota/fetchers/antigravity.ts`. Supported on macOS and Windows; other
|
|
239
|
+
* platforms report `unsupported` rather than guess at a keyring backend
|
|
240
|
+
* nobody has verified.
|
|
181
241
|
*
|
|
182
242
|
* ★Adding a provider here without adding its fetcher to REFRESHERS re-creates
|
|
183
243
|
* exactly the "switch that does nothing" this constant prevents.
|