@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.
@@ -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: 45;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/mesh-shared",
3
- "version": "1.0.82-rc.32",
3
+ "version": "1.0.82-rc.34",
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",
@@ -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
@@ -48,3 +48,4 @@ export * from './launch-defaults'
48
48
 
49
49
  export * from './role-launch';
50
50
  export * from './task-source-phase';
51
+ export * from './relay-reprobe'
@@ -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
- export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string; attemptId?: number; attemptEpoch?: string }
242
- export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string; attemptId?: number; attemptEpoch?: string }
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
+ }
@@ -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]