@adhdev/mesh-shared 1.0.82-rc.32 → 1.0.82-rc.34
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/assistant-protocol.d.ts +8 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +111 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +106 -5
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-tool-names.d.ts +2 -2
- package/dist/protocol/daemon-server.d.ts +6 -0
- package/dist/protocol/dashboard-server.d.ts +8 -0
- package/dist/relay-reprobe.d.ts +52 -0
- package/dist/worker-protocol.d.ts +1 -1
- package/package.json +1 -1
- package/src/assistant-protocol.ts +11 -1
- package/src/index.ts +1 -0
- package/src/mesh-tool-names.ts +3 -0
- package/src/protocol/daemon-server.ts +6 -2
- package/src/protocol/dashboard-server.ts +7 -2
- package/src/relay-reprobe.ts +117 -0
- package/src/worker-protocol.ts +7 -0
|
@@ -18,10 +18,10 @@
|
|
|
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_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_queue_revise", "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_create", "mesh_add_node", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots", "driver_list", "driver_describe", "artifact_list"];
|
|
21
|
+
export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_queue_revise", "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_create", "mesh_add_node", "mesh_reattach_source_parent", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots", "driver_list", "driver_describe", "artifact_list", "artifact_read", "artifact_register"];
|
|
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: 48;
|
|
25
25
|
/**
|
|
26
26
|
* Tool names retired by the 2026-09-26 tool-surface consolidation (60 → 48).
|
|
27
27
|
* Every capability behind them is still reachable: each retired name maps to the
|
|
@@ -218,6 +218,7 @@ export type P2POfferWirePayload = {
|
|
|
218
218
|
peerId: string;
|
|
219
219
|
attemptId?: number;
|
|
220
220
|
attemptEpoch?: string;
|
|
221
|
+
probe?: boolean;
|
|
221
222
|
};
|
|
222
223
|
export type P2PIceWirePayload = {
|
|
223
224
|
candidate: string;
|
|
@@ -225,6 +226,7 @@ export type P2PIceWirePayload = {
|
|
|
225
226
|
peerId: string;
|
|
226
227
|
attemptId?: number;
|
|
227
228
|
attemptEpoch?: string;
|
|
229
|
+
probe?: boolean;
|
|
228
230
|
};
|
|
229
231
|
/**
|
|
230
232
|
* A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
|
|
@@ -238,6 +240,10 @@ export type RelayedP2PSignalWirePayload = {
|
|
|
238
240
|
daemonId?: string;
|
|
239
241
|
attemptId?: number;
|
|
240
242
|
attemptEpoch?: string;
|
|
243
|
+
/** p2p_ready only: the dashboard's stale-connect backoff index (see DashboardP2PReadyWirePayload). */
|
|
244
|
+
backoffStep?: number;
|
|
245
|
+
/** Make-before-break relay->direct re-probe of an existing peer (see DashboardP2PReadyWirePayload). */
|
|
246
|
+
probe?: boolean;
|
|
241
247
|
sharePermission?: string;
|
|
242
248
|
shareTargetSessionId?: string;
|
|
243
249
|
sdp?: string;
|
|
@@ -111,6 +111,7 @@ export type DashboardP2POfferWirePayload = {
|
|
|
111
111
|
peerId?: string;
|
|
112
112
|
attemptId?: number;
|
|
113
113
|
attemptEpoch?: string;
|
|
114
|
+
probe?: boolean;
|
|
114
115
|
sdp: string;
|
|
115
116
|
type: string;
|
|
116
117
|
};
|
|
@@ -119,6 +120,7 @@ export type DashboardP2PIceWirePayload = {
|
|
|
119
120
|
peerId?: string;
|
|
120
121
|
attemptId?: number;
|
|
121
122
|
attemptEpoch?: string;
|
|
123
|
+
probe?: boolean;
|
|
122
124
|
candidate: string;
|
|
123
125
|
mid?: string;
|
|
124
126
|
sdpMid?: string | null;
|
|
@@ -155,11 +157,15 @@ export declare const isDashboardToUserSessionType: (value: unknown) => value is
|
|
|
155
157
|
export declare const DASHBOARD_P2P_SIGNAL_TYPES: readonly ["p2p_ready", "p2p_answer", "p2p_ice"];
|
|
156
158
|
export type DashboardP2PSignalType = typeof DASHBOARD_P2P_SIGNAL_TYPES[number];
|
|
157
159
|
export declare const isDashboardP2PSignalType: (value: unknown) => value is "p2p_ready" | "p2p_answer" | "p2p_ice";
|
|
160
|
+
/** `backoffStep`: the dashboard's stale-connect backoff index (0 = 15s, 1 = 30s, 2 = 60s), so the daemon holds the connecting slot for the same window. */
|
|
161
|
+
/** `probe`: identifier-only flag — this signal belongs to a make-before-break relay->direct re-probe of the SAME logical peer (never a new peer for the connection cap). */
|
|
158
162
|
export type DashboardP2PReadyWirePayload = {
|
|
159
163
|
daemonId: string;
|
|
160
164
|
peerId: string;
|
|
161
165
|
attemptId?: number;
|
|
162
166
|
attemptEpoch?: string;
|
|
167
|
+
backoffStep?: number;
|
|
168
|
+
probe?: boolean;
|
|
163
169
|
};
|
|
164
170
|
export type DashboardP2PAnswerWirePayload = {
|
|
165
171
|
daemonId: string;
|
|
@@ -168,12 +174,14 @@ export type DashboardP2PAnswerWirePayload = {
|
|
|
168
174
|
type: string;
|
|
169
175
|
attemptId?: number;
|
|
170
176
|
attemptEpoch?: string;
|
|
177
|
+
probe?: boolean;
|
|
171
178
|
};
|
|
172
179
|
export type DashboardP2PLocalIceWirePayload = {
|
|
173
180
|
daemonId: string;
|
|
174
181
|
peerId: string;
|
|
175
182
|
attemptId?: number;
|
|
176
183
|
attemptEpoch?: string;
|
|
184
|
+
probe?: boolean;
|
|
177
185
|
candidate: string;
|
|
178
186
|
sdpMid: string | null;
|
|
179
187
|
sdpMLineIndex: number | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay -> direct re-probe scheduler (pure, dependency-free).
|
|
3
|
+
*
|
|
4
|
+
* A P2P connection that settled on a TURN relay never looks for a direct path
|
|
5
|
+
* again. The owning transport (dashboard `P2PConnection`, daemon peer manager)
|
|
6
|
+
* uses this scheduler to periodically attempt a make-before-break probe: a
|
|
7
|
+
* SECOND peer connection (STUN-only) is built next to the working relay one and
|
|
8
|
+
* only swapped in once it is verified direct. The scheduler owns nothing but
|
|
9
|
+
* timing: backoff 60s -> 5min (doubling), jitter, and the stop conditions. The
|
|
10
|
+
* transport supplies `shouldProbe` (open + idle + visible) and `probe`.
|
|
11
|
+
*/
|
|
12
|
+
export type RelayReprobeOutcome = 'direct' | 'relay' | 'failed' | 'skipped';
|
|
13
|
+
export interface RelayReprobeTiming {
|
|
14
|
+
/** First delay and the unit of growth (default 60s). */
|
|
15
|
+
baseDelayMs?: number;
|
|
16
|
+
/** Backoff ceiling before jitter (default 5min). */
|
|
17
|
+
maxDelayMs?: number;
|
|
18
|
+
/** +/- fraction applied to every delay (default 0.2). */
|
|
19
|
+
jitterRatio?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const RELAY_REPROBE_BASE_DELAY_MS = 60000;
|
|
22
|
+
export declare const RELAY_REPROBE_MAX_DELAY_MS: number;
|
|
23
|
+
export declare const RELAY_REPROBE_JITTER_RATIO = 0.2;
|
|
24
|
+
/** Delay before the next attempt after `failures` consecutive non-direct outcomes. */
|
|
25
|
+
export declare function computeRelayReprobeDelay(failures: number, random: () => number, timing?: RelayReprobeTiming): number;
|
|
26
|
+
export interface RelayReprobeSchedulerOptions extends RelayReprobeTiming {
|
|
27
|
+
random: () => number;
|
|
28
|
+
setTimer: (callback: () => void, ms: number) => unknown;
|
|
29
|
+
clearTimer: (handle: unknown) => void;
|
|
30
|
+
/** Connected over relay, idle, tab visible, auth epoch unchanged... Re-checked on every tick. */
|
|
31
|
+
shouldProbe: () => boolean;
|
|
32
|
+
/** One make-before-break attempt. Must never throw into the scheduler; a rejection counts as 'failed'. */
|
|
33
|
+
probe: () => Promise<RelayReprobeOutcome>;
|
|
34
|
+
}
|
|
35
|
+
export declare class RelayReprobeScheduler {
|
|
36
|
+
private readonly options;
|
|
37
|
+
private handle;
|
|
38
|
+
private running;
|
|
39
|
+
private inFlight;
|
|
40
|
+
private failureCount;
|
|
41
|
+
private generation;
|
|
42
|
+
constructor(options: RelayReprobeSchedulerOptions);
|
|
43
|
+
get isRunning(): boolean;
|
|
44
|
+
get failures(): number;
|
|
45
|
+
get probing(): boolean;
|
|
46
|
+
/** Begin probing (idempotent). Call when a connection is selected as relay. */
|
|
47
|
+
start(): void;
|
|
48
|
+
/** Stop for good (direct path found, connection closed, auth epoch changed). */
|
|
49
|
+
stop(): void;
|
|
50
|
+
private schedule;
|
|
51
|
+
private tick;
|
|
52
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* the protocol contract the marker demarcates.
|
|
31
31
|
*/
|
|
32
32
|
import { type MissionBrief } from './mission-brief';
|
|
33
|
-
export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff", "driver_list", "driver_describe", "driver_invoke", "artifact_register"];
|
|
33
|
+
export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff", "driver_list", "driver_describe", "driver_invoke", "artifact_register", "artifact_list", "artifact_read", "contract_read", "contract_observe", "contract_current"];
|
|
34
34
|
export type WorkerTool = typeof WORKER_TOOLS[number];
|
|
35
35
|
export declare function isWorkerTool(value: unknown): value is WorkerTool;
|
|
36
36
|
/** Stable marker line so a body is never footered twice and tests can find it. */
|
package/package.json
CHANGED
|
@@ -34,6 +34,8 @@ export const ASSISTANT_TOOLS = [
|
|
|
34
34
|
'launch_defaults_set',
|
|
35
35
|
'driver_list',
|
|
36
36
|
'driver_describe',
|
|
37
|
+
'artifact_list',
|
|
38
|
+
'artifact_read',
|
|
37
39
|
] as const
|
|
38
40
|
|
|
39
41
|
export type AssistantTool = typeof ASSISTANT_TOOLS[number]
|
|
@@ -67,6 +69,8 @@ export const ASSISTANT_VERB = {
|
|
|
67
69
|
projectNote: 'assistant_project_note',
|
|
68
70
|
launchDefaults: 'assistant_launch_defaults',
|
|
69
71
|
launchDefaultsSet: 'assistant_launch_defaults_set',
|
|
72
|
+
artifactList: 'assistant_artifact_list',
|
|
73
|
+
artifactRead: 'assistant_artifact_read',
|
|
70
74
|
// assistant-side daemon verbs without a tool of the same name
|
|
71
75
|
launch: 'launch_assistant',
|
|
72
76
|
pendingRelays: 'assistant_pending_relays',
|
|
@@ -101,6 +105,8 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
|
|
|
101
105
|
project_note: ASSISTANT_VERB.projectNote,
|
|
102
106
|
launch_defaults: ASSISTANT_VERB.launchDefaults,
|
|
103
107
|
launch_defaults_set: ASSISTANT_VERB.launchDefaultsSet,
|
|
108
|
+
artifact_list: ASSISTANT_VERB.artifactList,
|
|
109
|
+
artifact_read: ASSISTANT_VERB.artifactRead,
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
/**
|
|
@@ -164,7 +170,7 @@ export const ASSISTANT_REMOTE_FOLLOW_COMMAND = 'assistant_remote_follow' as cons
|
|
|
164
170
|
* `assistant_remote_project` when the host's `assistantRemoteAccess` ACL is active.
|
|
165
171
|
* `note`, `terminal`, and `inventory` remain strictly roster-member-only.
|
|
166
172
|
*/
|
|
167
|
-
export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send'] as const
|
|
173
|
+
export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send', 'artifact_list', 'artifact_read'] as const
|
|
168
174
|
export type AssistantFollowerAllowedOp = typeof ASSISTANT_FOLLOWER_ALLOWED_OPS[number]
|
|
169
175
|
|
|
170
176
|
const ASSISTANT_FOLLOWER_ALLOWED_OP_SET: ReadonlySet<string> = new Set(ASSISTANT_FOLLOWER_ALLOWED_OPS)
|
|
@@ -188,6 +194,7 @@ export interface AssistantRemoteAccessBinding {
|
|
|
188
194
|
status: 'active' | 'revoked'
|
|
189
195
|
allowedOps: readonly AssistantFollowerAllowedOp[]
|
|
190
196
|
allowCoordinatorLaunch?: boolean
|
|
197
|
+
allowArtifactRead?: boolean
|
|
191
198
|
grantedAt: number
|
|
192
199
|
revokedAt?: number
|
|
193
200
|
}
|
|
@@ -234,6 +241,7 @@ export interface FollowedRemoteProjectEntry {
|
|
|
234
241
|
bindingRevision: number
|
|
235
242
|
grantRevision: number
|
|
236
243
|
allowCoordinatorLaunch?: boolean
|
|
244
|
+
allowArtifactRead?: boolean
|
|
237
245
|
state: FollowedRemoteProjectState
|
|
238
246
|
followedAt: number | string
|
|
239
247
|
updatedAt: number | string
|
|
@@ -264,6 +272,7 @@ export interface RemoteProjectCatalogItem {
|
|
|
264
272
|
sanitizedRepoIdentity: string | null
|
|
265
273
|
coordinatorState?: 'none' | 'idle' | 'working' | 'waiting' | string
|
|
266
274
|
allowCoordinatorLaunch?: boolean
|
|
275
|
+
allowArtifactRead?: boolean
|
|
267
276
|
capabilities: {
|
|
268
277
|
status: true
|
|
269
278
|
read: true
|
|
@@ -272,6 +281,7 @@ export interface RemoteProjectCatalogItem {
|
|
|
272
281
|
send: true
|
|
273
282
|
coordinatorPresent?: boolean
|
|
274
283
|
allowCoordinatorLaunch?: boolean
|
|
284
|
+
allowArtifactRead?: boolean
|
|
275
285
|
}
|
|
276
286
|
observedAt: number | string
|
|
277
287
|
}
|
package/src/index.ts
CHANGED
package/src/mesh-tool-names.ts
CHANGED
|
@@ -45,6 +45,7 @@ export const CANONICAL_MESH_TOOL_NAMES = [
|
|
|
45
45
|
'mesh_list_pending_approvals',
|
|
46
46
|
'mesh_create',
|
|
47
47
|
'mesh_add_node',
|
|
48
|
+
'mesh_reattach_source_parent',
|
|
48
49
|
'mesh_attach_remote',
|
|
49
50
|
'mesh_clone_node',
|
|
50
51
|
'mesh_remove_node',
|
|
@@ -69,6 +70,8 @@ export const CANONICAL_MESH_TOOL_NAMES = [
|
|
|
69
70
|
'driver_list',
|
|
70
71
|
'driver_describe',
|
|
71
72
|
'artifact_list',
|
|
73
|
+
'artifact_read',
|
|
74
|
+
'artifact_register',
|
|
72
75
|
] as const;
|
|
73
76
|
|
|
74
77
|
export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
|
|
@@ -263,8 +263,8 @@ export const DAEMON_P2P_SIGNAL_TYPES = ['p2p_offer', 'p2p_ice'] as const
|
|
|
263
263
|
export type DaemonP2PSignalType = typeof DAEMON_P2P_SIGNAL_TYPES[number]
|
|
264
264
|
export const isDaemonP2PSignalType = makeTypeGuard(DAEMON_P2P_SIGNAL_TYPES)
|
|
265
265
|
|
|
266
|
-
export type P2POfferWirePayload = { sdp: string; type: string; peerId: string; attemptId?: number; attemptEpoch?: string }
|
|
267
|
-
export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string; attemptId?: number; attemptEpoch?: string }
|
|
266
|
+
export type P2POfferWirePayload = { sdp: string; type: string; peerId: string; attemptId?: number; attemptEpoch?: string; probe?: boolean }
|
|
267
|
+
export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string; attemptId?: number; attemptEpoch?: string; probe?: boolean }
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
270
|
* A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
|
|
@@ -278,6 +278,10 @@ export type RelayedP2PSignalWirePayload = {
|
|
|
278
278
|
daemonId?: string
|
|
279
279
|
attemptId?: number
|
|
280
280
|
attemptEpoch?: string
|
|
281
|
+
/** p2p_ready only: the dashboard's stale-connect backoff index (see DashboardP2PReadyWirePayload). */
|
|
282
|
+
backoffStep?: number
|
|
283
|
+
/** Make-before-break relay->direct re-probe of an existing peer (see DashboardP2PReadyWirePayload). */
|
|
284
|
+
probe?: boolean
|
|
281
285
|
sharePermission?: string
|
|
282
286
|
shareTargetSessionId?: string
|
|
283
287
|
sdp?: string
|
|
@@ -154,6 +154,7 @@ export type DashboardP2POfferWirePayload = {
|
|
|
154
154
|
peerId?: string
|
|
155
155
|
attemptId?: number
|
|
156
156
|
attemptEpoch?: string
|
|
157
|
+
probe?: boolean
|
|
157
158
|
sdp: string
|
|
158
159
|
type: string
|
|
159
160
|
}
|
|
@@ -163,6 +164,7 @@ export type DashboardP2PIceWirePayload = {
|
|
|
163
164
|
peerId?: string
|
|
164
165
|
attemptId?: number
|
|
165
166
|
attemptEpoch?: string
|
|
167
|
+
probe?: boolean
|
|
166
168
|
candidate: string
|
|
167
169
|
mid?: string
|
|
168
170
|
sdpMid?: string | null
|
|
@@ -238,13 +240,16 @@ export const DASHBOARD_P2P_SIGNAL_TYPES = ['p2p_ready', 'p2p_answer', 'p2p_ice']
|
|
|
238
240
|
export type DashboardP2PSignalType = typeof DASHBOARD_P2P_SIGNAL_TYPES[number]
|
|
239
241
|
export const isDashboardP2PSignalType = makeTypeGuard(DASHBOARD_P2P_SIGNAL_TYPES)
|
|
240
242
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
/** `backoffStep`: the dashboard's stale-connect backoff index (0 = 15s, 1 = 30s, 2 = 60s), so the daemon holds the connecting slot for the same window. */
|
|
244
|
+
/** `probe`: identifier-only flag — this signal belongs to a make-before-break relay->direct re-probe of the SAME logical peer (never a new peer for the connection cap). */
|
|
245
|
+
export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string; attemptId?: number; attemptEpoch?: string; backoffStep?: number; probe?: boolean }
|
|
246
|
+
export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string; attemptId?: number; attemptEpoch?: string; probe?: boolean }
|
|
243
247
|
export type DashboardP2PLocalIceWirePayload = {
|
|
244
248
|
daemonId: string
|
|
245
249
|
peerId: string
|
|
246
250
|
attemptId?: number
|
|
247
251
|
attemptEpoch?: string
|
|
252
|
+
probe?: boolean
|
|
248
253
|
candidate: string
|
|
249
254
|
sdpMid: string | null
|
|
250
255
|
sdpMLineIndex: number | null
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay -> direct re-probe scheduler (pure, dependency-free).
|
|
3
|
+
*
|
|
4
|
+
* A P2P connection that settled on a TURN relay never looks for a direct path
|
|
5
|
+
* again. The owning transport (dashboard `P2PConnection`, daemon peer manager)
|
|
6
|
+
* uses this scheduler to periodically attempt a make-before-break probe: a
|
|
7
|
+
* SECOND peer connection (STUN-only) is built next to the working relay one and
|
|
8
|
+
* only swapped in once it is verified direct. The scheduler owns nothing but
|
|
9
|
+
* timing: backoff 60s -> 5min (doubling), jitter, and the stop conditions. The
|
|
10
|
+
* transport supplies `shouldProbe` (open + idle + visible) and `probe`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type RelayReprobeOutcome = 'direct' | 'relay' | 'failed' | 'skipped'
|
|
14
|
+
|
|
15
|
+
export interface RelayReprobeTiming {
|
|
16
|
+
/** First delay and the unit of growth (default 60s). */
|
|
17
|
+
baseDelayMs?: number
|
|
18
|
+
/** Backoff ceiling before jitter (default 5min). */
|
|
19
|
+
maxDelayMs?: number
|
|
20
|
+
/** +/- fraction applied to every delay (default 0.2). */
|
|
21
|
+
jitterRatio?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const RELAY_REPROBE_BASE_DELAY_MS = 60_000
|
|
25
|
+
export const RELAY_REPROBE_MAX_DELAY_MS = 5 * 60_000
|
|
26
|
+
export const RELAY_REPROBE_JITTER_RATIO = 0.2
|
|
27
|
+
|
|
28
|
+
/** Delay before the next attempt after `failures` consecutive non-direct outcomes. */
|
|
29
|
+
export function computeRelayReprobeDelay(
|
|
30
|
+
failures: number,
|
|
31
|
+
random: () => number,
|
|
32
|
+
timing: RelayReprobeTiming = {},
|
|
33
|
+
): number {
|
|
34
|
+
const base = timing.baseDelayMs ?? RELAY_REPROBE_BASE_DELAY_MS
|
|
35
|
+
const max = timing.maxDelayMs ?? RELAY_REPROBE_MAX_DELAY_MS
|
|
36
|
+
const jitter = timing.jitterRatio ?? RELAY_REPROBE_JITTER_RATIO
|
|
37
|
+
const grown = Math.min(max, base * 2 ** Math.max(0, failures))
|
|
38
|
+
const factor = 1 + jitter * (Math.min(1, Math.max(0, random())) * 2 - 1)
|
|
39
|
+
return Math.round(grown * factor)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RelayReprobeSchedulerOptions extends RelayReprobeTiming {
|
|
43
|
+
random: () => number
|
|
44
|
+
setTimer: (callback: () => void, ms: number) => unknown
|
|
45
|
+
clearTimer: (handle: unknown) => void
|
|
46
|
+
/** Connected over relay, idle, tab visible, auth epoch unchanged... Re-checked on every tick. */
|
|
47
|
+
shouldProbe: () => boolean
|
|
48
|
+
/** One make-before-break attempt. Must never throw into the scheduler; a rejection counts as 'failed'. */
|
|
49
|
+
probe: () => Promise<RelayReprobeOutcome>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class RelayReprobeScheduler {
|
|
53
|
+
private handle: unknown = null
|
|
54
|
+
private running = false
|
|
55
|
+
private inFlight = false
|
|
56
|
+
private failureCount = 0
|
|
57
|
+
private generation = 0
|
|
58
|
+
|
|
59
|
+
constructor(private readonly options: RelayReprobeSchedulerOptions) {}
|
|
60
|
+
|
|
61
|
+
get isRunning(): boolean { return this.running }
|
|
62
|
+
get failures(): number { return this.failureCount }
|
|
63
|
+
get probing(): boolean { return this.inFlight }
|
|
64
|
+
|
|
65
|
+
/** Begin probing (idempotent). Call when a connection is selected as relay. */
|
|
66
|
+
start(): void {
|
|
67
|
+
if (this.running) return
|
|
68
|
+
this.running = true
|
|
69
|
+
this.failureCount = 0
|
|
70
|
+
this.generation++
|
|
71
|
+
this.schedule()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Stop for good (direct path found, connection closed, auth epoch changed). */
|
|
75
|
+
stop(): void {
|
|
76
|
+
this.running = false
|
|
77
|
+
this.generation++
|
|
78
|
+
if (this.handle !== null) {
|
|
79
|
+
this.options.clearTimer(this.handle)
|
|
80
|
+
this.handle = null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private schedule(): void {
|
|
85
|
+
if (!this.running) return
|
|
86
|
+
const generation = this.generation
|
|
87
|
+
const delay = computeRelayReprobeDelay(this.failureCount, this.options.random, this.options)
|
|
88
|
+
this.handle = this.options.setTimer(() => {
|
|
89
|
+
this.handle = null
|
|
90
|
+
void this.tick(generation)
|
|
91
|
+
}, delay)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async tick(generation: number): Promise<void> {
|
|
95
|
+
if (!this.running || generation !== this.generation) return
|
|
96
|
+
if (this.inFlight || !this.options.shouldProbe()) {
|
|
97
|
+
this.schedule()
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
this.inFlight = true
|
|
101
|
+
let outcome: RelayReprobeOutcome
|
|
102
|
+
try {
|
|
103
|
+
outcome = await this.options.probe()
|
|
104
|
+
} catch {
|
|
105
|
+
outcome = 'failed'
|
|
106
|
+
}
|
|
107
|
+
this.inFlight = false
|
|
108
|
+
// stop()/restart while the probe was in flight: the verdict is stale.
|
|
109
|
+
if (!this.running || generation !== this.generation) return
|
|
110
|
+
if (outcome === 'direct') {
|
|
111
|
+
this.stop()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (outcome !== 'skipped') this.failureCount++
|
|
115
|
+
this.schedule()
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/worker-protocol.ts
CHANGED
|
@@ -44,6 +44,13 @@ export const WORKER_TOOLS = [
|
|
|
44
44
|
'driver_describe',
|
|
45
45
|
'driver_invoke',
|
|
46
46
|
'artifact_register',
|
|
47
|
+
'artifact_list',
|
|
48
|
+
'artifact_read',
|
|
49
|
+
// Contract versioning (Cut A): exact pinned-unit reads (E2), the explicit
|
|
50
|
+
// read claim (C1) and the authoritative head/notice pull.
|
|
51
|
+
'contract_read',
|
|
52
|
+
'contract_observe',
|
|
53
|
+
'contract_current',
|
|
47
54
|
] as const
|
|
48
55
|
|
|
49
56
|
export type WorkerTool = typeof WORKER_TOOLS[number]
|