@apnex/network-adapter 0.1.1 → 0.1.5

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.
Files changed (86) hide show
  1. package/dist/build-info.json +6 -0
  2. package/dist/file-logger.d.ts +42 -0
  3. package/dist/file-logger.js +122 -0
  4. package/dist/file-logger.js.map +1 -0
  5. package/dist/index.d.ts +37 -13
  6. package/dist/index.js +42 -15
  7. package/dist/index.js.map +1 -1
  8. package/dist/kernel/adapter-config.d.ts +47 -0
  9. package/dist/kernel/adapter-config.js +90 -0
  10. package/dist/kernel/adapter-config.js.map +1 -0
  11. package/dist/kernel/agent-client.d.ts +25 -20
  12. package/dist/kernel/build-identity.d.ts +24 -0
  13. package/dist/kernel/build-identity.js +45 -0
  14. package/dist/kernel/build-identity.js.map +1 -0
  15. package/dist/kernel/event-router.d.ts +22 -1
  16. package/dist/kernel/event-router.js +45 -7
  17. package/dist/kernel/event-router.js.map +1 -1
  18. package/dist/kernel/handshake.d.ts +40 -4
  19. package/dist/kernel/handshake.js +46 -7
  20. package/dist/kernel/handshake.js.map +1 -1
  21. package/dist/kernel/harness-manifest.d.ts +51 -0
  22. package/dist/kernel/harness-manifest.js +96 -0
  23. package/dist/kernel/harness-manifest.js.map +1 -0
  24. package/dist/kernel/liveness-signal.d.ts +35 -0
  25. package/dist/kernel/liveness-signal.js +61 -0
  26. package/dist/kernel/liveness-signal.js.map +1 -0
  27. package/dist/kernel/liveness-watchdog.d.ts +78 -0
  28. package/dist/kernel/liveness-watchdog.js +115 -0
  29. package/dist/kernel/liveness-watchdog.js.map +1 -0
  30. package/dist/kernel/mcp-agent-client.d.ts +2 -2
  31. package/dist/kernel/mcp-agent-client.js +49 -3
  32. package/dist/kernel/mcp-agent-client.js.map +1 -1
  33. package/dist/kernel/poll-backstop.d.ts +93 -4
  34. package/dist/kernel/poll-backstop.js +193 -25
  35. package/dist/kernel/poll-backstop.js.map +1 -1
  36. package/dist/kernel/state-sync.d.ts +2 -13
  37. package/dist/kernel/state-sync.js.map +1 -1
  38. package/dist/notification-log.d.ts +8 -0
  39. package/dist/notification-log.js +10 -0
  40. package/dist/notification-log.js.map +1 -1
  41. package/dist/observability.d.ts +24 -0
  42. package/dist/observability.js +52 -0
  43. package/dist/observability.js.map +1 -0
  44. package/dist/prompt-format.js +82 -9
  45. package/dist/prompt-format.js.map +1 -1
  46. package/dist/tool-manager/catalog/health-revision.d.ts +43 -0
  47. package/dist/tool-manager/catalog/health-revision.js +61 -0
  48. package/dist/tool-manager/catalog/health-revision.js.map +1 -0
  49. package/dist/tool-manager/{tool-catalog-cache.d.ts → catalog/tool-catalog-cache.d.ts} +39 -19
  50. package/dist/tool-manager/{tool-catalog-cache.js → catalog/tool-catalog-cache.js} +53 -26
  51. package/dist/tool-manager/catalog/tool-catalog-cache.js.map +1 -0
  52. package/dist/tool-manager/catalog/tool-surface-reconciler.d.ts +93 -0
  53. package/dist/tool-manager/catalog/tool-surface-reconciler.js +101 -0
  54. package/dist/tool-manager/catalog/tool-surface-reconciler.js.map +1 -0
  55. package/dist/tool-manager/contracts.d.ts +91 -0
  56. package/dist/tool-manager/contracts.js +20 -0
  57. package/dist/tool-manager/contracts.js.map +1 -0
  58. package/dist/tool-manager/dispatch/dispatch.d.ts +77 -0
  59. package/dist/tool-manager/dispatch/dispatch.js +170 -0
  60. package/dist/tool-manager/dispatch/dispatch.js.map +1 -0
  61. package/dist/tool-manager/dispatch/tool-call-policy.d.ts +32 -0
  62. package/dist/tool-manager/dispatch/tool-call-policy.js +56 -0
  63. package/dist/tool-manager/dispatch/tool-call-policy.js.map +1 -0
  64. package/dist/tool-manager/{dispatcher.d.ts → orchestrator/dispatcher.d.ts} +110 -31
  65. package/dist/tool-manager/orchestrator/dispatcher.js +559 -0
  66. package/dist/tool-manager/orchestrator/dispatcher.js.map +1 -0
  67. package/dist/tool-manager/work-protocol/claimable-digest-tracker.d.ts +60 -0
  68. package/dist/tool-manager/work-protocol/claimable-digest-tracker.js +66 -0
  69. package/dist/tool-manager/work-protocol/claimable-digest-tracker.js.map +1 -0
  70. package/dist/tool-manager/work-protocol/work-lease-tracker.d.ts +59 -0
  71. package/dist/tool-manager/work-protocol/work-lease-tracker.js +134 -0
  72. package/dist/tool-manager/work-protocol/work-lease-tracker.js.map +1 -0
  73. package/dist/wire/mcp-transport.d.ts +1 -1
  74. package/dist/wire/mcp-transport.js +31 -7
  75. package/dist/wire/mcp-transport.js.map +1 -1
  76. package/dist/wire/slot-gate.d.ts +40 -0
  77. package/dist/wire/slot-gate.js +61 -0
  78. package/dist/wire/slot-gate.js.map +1 -0
  79. package/dist/wire/transport.d.ts +18 -0
  80. package/package.json +6 -3
  81. package/dist/kernel/instance.d.ts +0 -40
  82. package/dist/kernel/instance.js +0 -79
  83. package/dist/kernel/instance.js.map +0 -1
  84. package/dist/tool-manager/dispatcher.js +0 -379
  85. package/dist/tool-manager/dispatcher.js.map +0 -1
  86. package/dist/tool-manager/tool-catalog-cache.js.map +0 -1
@@ -0,0 +1,559 @@
1
+ /**
2
+ * dispatcher.ts — tool-manager handler factory (Layer 1c).
3
+ *
4
+ * Host-independent shared abstraction that owns the MCP server's
5
+ * Initialize / ListTools / CallTool handlers and the supporting
6
+ * pending-action-queueItemId tracking + tool-catalog cache fallback +
7
+ * clientInfo capture + error-envelope normalization.
8
+ *
9
+ * Mounted by per-host shims (Layer 3) which add host-specific transport
10
+ * plumbing (stdio / Bun-HTTP / future) and host-specific render-surface
11
+ * via the `notificationHooks` callback bag (Universal Adapter
12
+ * notification contract).
13
+ *
14
+ * This module is the "tool-manager" per Design v1.2 §4 naming discipline
15
+ * (Director-ratified rename from "MCP-boundary dispatcher" 2026-04-26)
16
+ * — distinct from the "Message-router" which is sovereign-package #6
17
+ * (`@apnex/message-router`) landing in M-Push-Foundation W4. Always
18
+ * qualify ("tool-manager" or "Message-router") in new code; avoid bare
19
+ * "dispatcher".
20
+ */
21
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
+ import { ListToolsRequestSchema, CallToolRequestSchema, InitializeRequestSchema, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js";
23
+ import { MessageRouter, SeenIdCache } from "@apnex/message-router";
24
+ import { PollBackstop, resolveRole } from "../../kernel/poll-backstop.js";
25
+ import { ClaimableDigestTracker } from "../work-protocol/claimable-digest-tracker.js";
26
+ import { WorkLeaseTracker } from "../work-protocol/work-lease-tracker.js";
27
+ import { TOOL_CALL_SIGNAL_SKIP, pendingKey, injectQueueItemId, } from "../dispatch/tool-call-policy.js";
28
+ import { runToolDispatch } from "../dispatch/dispatch.js";
29
+ // M-Tool-Manager Slice B: the pure OIS tool-call policy helpers
30
+ // (TOOL_CALL_SIGNAL_SKIP / pendingKey / injectQueueItemId) moved to
31
+ // `./tool-call-policy.js` so the dispatch authority (`./dispatch.js`) and this
32
+ // god-object shell share them without a circular import. They are imported
33
+ // above (used internally) and RE-EXPORTED here so existing consumers
34
+ // (index.ts, tests) are unaffected.
35
+ export { TOOL_CALL_SIGNAL_SKIP, pendingKey, injectQueueItemId };
36
+ // idea-355 §4.3 (review fix) — tight timeout on the wake/stall reconcile's Hub
37
+ // read. The liveness heartbeat now runs concurrently with the reconcile (see
38
+ // PollBackstop.tickHeartbeat), but this still bounds how long the reconcile can
39
+ // hold the heartbeat cadence (heartbeatInFlight) and resource use. On timeout
40
+ // the read is treated as a failed read (AC3 — tracker skipped, no false replay).
41
+ const WAKE_STALL_READ_TIMEOUT_MS = 10_000;
42
+ function withTimeout(p, ms, label) {
43
+ let timer;
44
+ const timeout = new Promise((_, reject) => {
45
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
46
+ });
47
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
48
+ }
49
+ export function createSharedDispatcher(opts) {
50
+ const log = opts.log ?? (() => { });
51
+ const serverName = opts.serverName ?? "proxy";
52
+ const serverCapabilities = opts.serverCapabilities ?? { tools: {}, logging: {} };
53
+ const pendingActionMap = new Map();
54
+ // idea-353 W1 idle-gate: count of host-driven CallTool requests currently in
55
+ // flight. The wake/stall reconciler reads this on the heartbeat tick to avoid
56
+ // interrupting a mid-task agent (AC4). The tick's own internal `agent.call`s
57
+ // (list_ready_work etc.) do NOT pass through this handler, so an idle poll
58
+ // never self-gates.
59
+ let activeCallCount = 0;
60
+ let capturedClientInfo = {
61
+ name: "unknown",
62
+ version: "0.0.0",
63
+ };
64
+ const getClientInfo = () => capturedClientInfo;
65
+ function isUsableAgent(agent) {
66
+ return !!agent && agent.isConnected !== false;
67
+ }
68
+ // bug-141 retry budget. Bootstrap-path ListTools races the Hub
69
+ // handshake on cold-start; this retry window absorbs the gap before
70
+ // raising a structured error. Worst-case latency:
71
+ // (LIST_TOOLS_RETRY_ATTEMPTS - 1) × LIST_TOOLS_RETRY_DELAY_MS
72
+ // (~2.8s) before the error surfaces. Probe path remains <50ms via
73
+ // cached catalog.
74
+ //
75
+ // Pass-1 (2026-05-26, bug-141 initial fix) set 4 × 200ms = 600ms
76
+ // budget. Pass-2 (2026-05-28) bumped to 15 × 200ms = 2.8s after
77
+ // observing greg-side cold-starts where the McpAgentClient reached
78
+ // `streaming` state (agent.isConnected=true) ~1.3s after
79
+ // identityReady resolved — 4 attempts wasn't enough to bridge the
80
+ // race. The architecturally-correct fix is for hosts to wire
81
+ // listToolsGate to syncReady instead of identityReady (claude-
82
+ // plugin shim does this); the wider retry budget defends opencode-
83
+ // plugin + any host that doesn't wire the gate, and is a belt-and-
84
+ // suspenders defense against future race-window widening.
85
+ const LIST_TOOLS_RETRY_ATTEMPTS = 15;
86
+ const LIST_TOOLS_RETRY_DELAY_MS = 200;
87
+ // ADR-017 Phase 1.1: SSE thread_message events carry queueItemId
88
+ // inline. Capture into pendingActionMap so the next settling
89
+ // create_thread_reply can auto-inject sourceQueueItemId — even if
90
+ // no drain ever populated the map. Eliminates the SSE-vs-drain race
91
+ // that caused false-positive escalations on early thread tests.
92
+ const captureQueueItemFromEvent = (event) => {
93
+ if (event.event !== "thread_message")
94
+ return;
95
+ const data = event.data;
96
+ const qid = data.queueItemId;
97
+ const threadId = data.threadId;
98
+ if (typeof qid === "string" && typeof threadId === "string") {
99
+ pendingActionMap.set(pendingKey("thread_message", threadId), qid);
100
+ }
101
+ };
102
+ // Mission-56 W2.2: Layer-2 routing. Every classified event goes
103
+ // through `@apnex/message-router` so Message-ID dedup (push+poll
104
+ // race) + kind→hook mapping live in one place. The host's
105
+ // `notificationHooks` bag is the router's hook surface — no
106
+ // shape adapter needed (the router's NotificationHooks interface
107
+ // mirrors DispatcherNotificationHooks exactly).
108
+ //
109
+ // The seen-id cache is shared across the construction-time router
110
+ // and any per-call routers minted by `makePendingActionItemHandler`,
111
+ // so a Message ID seen on the SSE inline path will dedup a later
112
+ // drain-path replay (and vice-versa).
113
+ const seenIdCache = new SeenIdCache();
114
+ const router = new MessageRouter({
115
+ hooks: opts.notificationHooks ?? {},
116
+ seenIdCache,
117
+ });
118
+ // Mission-56 W3.3: post-render claim. Extracts the Message ID from
119
+ // `message_arrived` events (W1a SSE shape: event.data.message.id)
120
+ // and fires `claim_message(id)` against the Hub via the agent. Per
121
+ // architect-issued W3 directive: claim happens AFTER the host hook
122
+ // renders (the ordering matches "shim calls after successful render
123
+ // to host" from Design v1.2 commitment #6). Errors are swallowed +
124
+ // logged — claim failure is non-fatal (the SSE path still rendered;
125
+ // the next poll-tick will pick up any unclaimed Message in the
126
+ // status === "new" set if needed).
127
+ //
128
+ // Multi-agent same-role: the Hub-side CAS enforces winner-takes-all
129
+ // (mission-56 W3.2). The wonClaim signal is informational; even if
130
+ // we lost, the host has already rendered (claim is post-render), so
131
+ // the loser still sees the Message — but only the winner's claim
132
+ // flips status to `received`, gating subsequent ack to a single
133
+ // canonical actor.
134
+ function fireClaimMessage(event) {
135
+ if (event.event !== "message_arrived")
136
+ return;
137
+ const data = event.data;
138
+ const message = data?.message;
139
+ const messageId = message?.id;
140
+ if (typeof messageId !== "string")
141
+ return;
142
+ const agent = opts.getAgent();
143
+ if (!agent || agent.state !== "streaming")
144
+ return;
145
+ void agent
146
+ .call("claim_message", { id: messageId })
147
+ .catch((err) => {
148
+ log(`[claim_message] non-fatal failure for ${messageId}: ${err?.message ?? String(err)}`);
149
+ });
150
+ }
151
+ // Phase-1.5 #1 (M-SSE-Filter-List-Adapter-Consumption Design v1.0
152
+ // §1.3): when Hub has set `payload.suppress_peek_line: true` (per
153
+ // §1.5 filter-list at sse-peek-line-render.ts), downgrade the event
154
+ // to informational routing — the host's onInformationalEvent hook
155
+ // logs but does NOT call pushChannelNotification (no LLM wake; no
156
+ // peek-line surface). State-machine consumption preserved via the
157
+ // log path. Operator-visible noise reduction for filter-listed
158
+ // events. Back-compat: undefined / false → current Phase-1 flow
159
+ // unchanged.
160
+ function isSuppressed(event) {
161
+ const data = event.data;
162
+ return data?.suppress_peek_line === true;
163
+ }
164
+ const callbacks = {
165
+ onActionableEvent: (event) => {
166
+ if (isSuppressed(event)) {
167
+ // Filter-listed event: route as informational; skip
168
+ // captureQueueItemFromEvent + fireClaimMessage (actionable-only
169
+ // side effects). State-machine consumption preserved via the
170
+ // informational hook's log path.
171
+ router.route({ kind: "notification.informational", event });
172
+ return;
173
+ }
174
+ captureQueueItemFromEvent(event);
175
+ router.route({ kind: "notification.actionable", event });
176
+ // Mission-56 W3.3: post-render claim (replaces W2.2 stub-claim TODO).
177
+ fireClaimMessage(event);
178
+ },
179
+ onInformationalEvent: (event) => {
180
+ router.route({ kind: "notification.informational", event });
181
+ },
182
+ onStateChange: (state, previous, reason) => {
183
+ log(`Connection: ${previous} → ${state}${reason ? ` (${reason})` : ""}`);
184
+ router.route({ kind: "state.change", state, previous, reason });
185
+ // bug-103: on every transition into `streaming` (first connect AND every
186
+ // reconnect), trigger an immediate poll-backstop catch-up tick — so
187
+ // role-targeted kind:note notifications missed while the adapter was
188
+ // disconnected are recovered promptly, not only on the next ≤cadence
189
+ // interval tick. No-op when the first-timer is disabled or already
190
+ // in-flight (tick() guards both).
191
+ if (state === "streaming")
192
+ void pollBackstop?.tick(opts.getAgent);
193
+ },
194
+ };
195
+ // idea-355 §4.3 — kernel-internal queue wake/stall reconcile. Hoisted from the
196
+ // claude shim so EVERY host gets it via the dispatcher heartbeat tick with ZERO
197
+ // shim wiring (the shim that wires none of these seams still emits via
198
+ // notificationHooks on a tick). The trackers are per-dispatcher-instance and
199
+ // persist ACROSS ticks — the level-triggered, ID-keyed dedup state must survive
200
+ // tick-to-tick or every tick re-emits (digest spam).
201
+ const claimableDigest = new ClaimableDigestTracker();
202
+ // Slice D: use the injected tracker when a native binding shares one (so its
203
+ // runToolDispatch observations land where the reconcile reads); else own it.
204
+ const workLeases = opts.sharedWorkLeases ?? new WorkLeaseTracker();
205
+ let wakeStallInFlight = false;
206
+ // The reconcile body. Drives W1 (inbound claimable digest), W2 (outbound
207
+ // stall-prompt), W3 (emit-only status log). Invoked from the heartbeat tick
208
+ // below (wrapped in its own try/catch alongside the host live-refresh hook).
209
+ async function runWakeStallReconcile() {
210
+ // Invariant: in-flight latch so overlapping ticks don't double-emit. The
211
+ // gates below run BEFORE the latch is taken, so an early return never needs
212
+ // to release it.
213
+ if (wakeStallInFlight)
214
+ return;
215
+ const agent = opts.getAgent();
216
+ // Gate on a live streaming agent (the call path requires it anyway).
217
+ if (!agent || agent.state !== "streaming")
218
+ return;
219
+ // Invariant: gate on identityReady — role + agentId come from the post-
220
+ // handshake identity; a pre-handshake tick would read an undefined identity
221
+ // and list_ready_work the wrong (or empty) set.
222
+ const agentId = agent.getMetrics?.().agentId;
223
+ if (!agentId)
224
+ return;
225
+ // bug-173 — resolve the role at USE-time (it may be a `() => string` thunk
226
+ // for hosts whose dispatcher is constructed before config.role loads), so the
227
+ // idea-353 wake/stall reconcile filters on the SAME configured role the
228
+ // PollBackstop tick uses — not a frozen module-init env default.
229
+ const roleOpt = opts.pollBackstop?.role;
230
+ const role = roleOpt === undefined ? undefined : resolveRole(roleOpt);
231
+ if (!role)
232
+ return;
233
+ wakeStallInFlight = true;
234
+ try {
235
+ // Slice D: a native host (pi) supplies its own idle probe because its tool
236
+ // calls bypass this dispatcher's CallTool handler (so activeCallCount is
237
+ // always 0 for it). MCP hosts leave externalIdle undefined → counter wins.
238
+ const idle = opts.externalIdle ? opts.externalIdle() : activeCallCount === 0;
239
+ const nowMs = Date.now();
240
+ let claimableCount = 0;
241
+ // W1 — inbound claimable digest. Read the CALLER-CLAIMABLE set via the
242
+ // stable list_ready_work contract with scopeToCaller (idea-353 WI-2.1 /
243
+ // audit-4265: the Hub applies claim_work's FULL predicate — deps + role +
244
+ // WIP-cap + quarantine — so the digest never over-reports what this agent
245
+ // can actually claim; AC5 strict parity). On a failed read, skip the
246
+ // tracker entirely so a transient empty/aborted read cannot manufacture a
247
+ // false 0→N replay (AC3).
248
+ try {
249
+ const raw = await withTimeout(agent.call("list_ready_work", { role, scopeToCaller: true }, { internal: true }), WAKE_STALL_READ_TIMEOUT_MS, "list_ready_work wake/stall read");
250
+ const items = raw?.items;
251
+ if (Array.isArray(items)) {
252
+ const claimableIds = items
253
+ .map((i) => i?.id)
254
+ .filter((id) => typeof id === "string");
255
+ claimableCount = claimableIds.length;
256
+ const decision = claimableDigest.reconcile({ claimableIds, isIdle: idle });
257
+ if (decision.emit) {
258
+ router.route({
259
+ kind: "notification.actionable",
260
+ event: {
261
+ event: "work_claimable_digest",
262
+ data: { role, count: decision.count, newCount: decision.newCount },
263
+ },
264
+ });
265
+ log(`[idea-353] inbound digest emitted — ${decision.count} claimable (${decision.newCount} new) for ${role}`);
266
+ }
267
+ }
268
+ }
269
+ catch (err) {
270
+ log(`[idea-353] list_ready_work tick failed (non-fatal): ${err?.message ?? err}`);
271
+ }
272
+ // W2 — outbound stall-prompt. Idle-gated: never pester a visibly-
273
+ // progressing holder (an in-flight CallTool = active progress). A held
274
+ // lease past ~60% of its window without a renew gets ONE nudge.
275
+ if (idle) {
276
+ for (const due of workLeases.dueForStallPrompt(nowMs)) {
277
+ router.route({
278
+ kind: "notification.actionable",
279
+ event: {
280
+ event: "work_lease_stall",
281
+ data: { workId: due.workId, msUntilExpiry: due.msUntilExpiry },
282
+ },
283
+ });
284
+ workLeases.markPrompted(due.workId);
285
+ log(`[idea-353] outbound stall-prompt emitted — ${due.workId} (~${Math.round(due.msUntilExpiry / 60000)}m left)`);
286
+ }
287
+ }
288
+ // W3 — emit-only Agent.status idle/stall telemetry seam. Thin + non-gating.
289
+ const heldLeases = workLeases.size();
290
+ const statusState = !idle ? "working" : heldLeases > 0 ? "holding" : "idle";
291
+ log(`[idea-353][agent-status] state=${statusState} idle=${idle} claimable=${claimableCount} heldLeases=${heldLeases}`);
292
+ }
293
+ finally {
294
+ // Invariant: release the latch on BOTH success and error — a thrown
295
+ // reconcile must not wedge the latch (that would silently disable the
296
+ // wake forever).
297
+ wakeStallInFlight = false;
298
+ }
299
+ }
300
+ // Mission-56 W3.3: PollBackstop construction (opt-in via opts.pollBackstop).
301
+ // The backstop fires `list_messages({status:"new", since:<lastSeen>})`
302
+ // periodically and routes each delta Message through the same
303
+ // MessageRouter as the SSE inline path so seen-id LRU dedup catches
304
+ // push+poll race overlap. Polled Messages also fire claim_message
305
+ // (the router invocation goes through onActionableEvent which
306
+ // already includes fireClaimMessage).
307
+ // idea-355 §4.3 — the kernel owns the heartbeat tick. The host's optional
308
+ // onHeartbeatTick (e.g. claude's bug-180 tool-surface live-refresh, which is
309
+ // host-coupled via mcpServer.sendToolListChanged) and the kernel wake/stall
310
+ // reconcile SHARE the tick; each runs in its OWN try/catch (invariant #5) so a
311
+ // failing or slow one cannot break or block the other.
312
+ const hostHeartbeatTick = opts.pollBackstop?.onHeartbeatTick;
313
+ const pollBackstop = opts.pollBackstop
314
+ ? new PollBackstop({
315
+ ...opts.pollBackstop,
316
+ log: opts.pollBackstop.log ?? log,
317
+ onPolledMessage: (event) => {
318
+ router.route({ kind: "notification.actionable", event });
319
+ fireClaimMessage(event);
320
+ },
321
+ onHeartbeatTick: async () => {
322
+ if (hostHeartbeatTick) {
323
+ try {
324
+ await hostHeartbeatTick();
325
+ }
326
+ catch (err) {
327
+ log(`[tick] host heartbeat hook threw (non-fatal): ${err?.message ?? err}`);
328
+ }
329
+ }
330
+ try {
331
+ await runWakeStallReconcile();
332
+ }
333
+ catch (err) {
334
+ log(`[tick] wake/stall reconcile threw (non-fatal): ${err?.message ?? err}`);
335
+ }
336
+ },
337
+ })
338
+ : undefined;
339
+ // Mission-56 W3.3: explicit-ack-on-action helper. Host shims wire
340
+ // this to fire when the LLM consumer has acted on (or actively
341
+ // deferred) a Message — per Option (i) ratified at thread-325 round-2,
342
+ // ack is tied to consumer-action, not auto-on-render.
343
+ async function ackMessage(messageId) {
344
+ const agent = opts.getAgent();
345
+ if (!agent || agent.state !== "streaming")
346
+ return;
347
+ try {
348
+ await agent.call("ack_message", { id: messageId });
349
+ }
350
+ catch (err) {
351
+ log(`[ack_message] non-fatal failure for ${messageId}: ${err?.message ?? String(err)}`);
352
+ }
353
+ }
354
+ const makePendingActionItemHandler = (hooks) => {
355
+ // Per-call hooks override the construction-time bag for the
356
+ // drain path (preserves the original makePendingActionItemHandler
357
+ // contract — claude-plugin shim uses this to bind a custom log
358
+ // sink). Share the seen-id cache so drain-path replays dedup
359
+ // against SSE-path inline deliveries.
360
+ const drainRouter = new MessageRouter({
361
+ hooks: hooks ?? {},
362
+ seenIdCache,
363
+ });
364
+ return (item) => {
365
+ pendingActionMap.set(pendingKey(item.dispatchType, item.entityRef), item.id);
366
+ drainRouter.route({ kind: "pending-action.dispatch", item });
367
+ };
368
+ };
369
+ function createMcpServer() {
370
+ const server = new Server({ name: serverName, version: opts.proxyVersion }, { capabilities: serverCapabilities });
371
+ // Initialize handler is intentionally NOT gated — host MCP clients
372
+ // (e.g. Claude Code) have a tight initialize timeout that's faster
373
+ // than a full Hub handshake. The Initialize handler captures
374
+ // clientInfo for downstream handshake passthrough.
375
+ server.setRequestHandler(InitializeRequestSchema, async (request) => {
376
+ try {
377
+ const ci = request.params
378
+ .clientInfo;
379
+ if (ci &&
380
+ typeof ci.name === "string" &&
381
+ typeof ci.version === "string") {
382
+ capturedClientInfo = { name: ci.name, version: ci.version };
383
+ log(`[Handshake] Captured clientInfo: ${ci.name}@${ci.version}`);
384
+ }
385
+ }
386
+ catch (err) {
387
+ log(`[Handshake] clientInfo capture failed (non-fatal): ${err}`);
388
+ }
389
+ return {
390
+ protocolVersion: request.params.protocolVersion,
391
+ capabilities: serverCapabilities,
392
+ serverInfo: { name: serverName, version: opts.proxyVersion },
393
+ };
394
+ });
395
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
396
+ // Probe-safe cache fallback. When identity hasn't yet resolved
397
+ // (e.g. `claude mcp list` spawned the adapter and will exit
398
+ // before the handshake completes), serve the persisted catalog
399
+ // if available + valid against the current Hub version. Probe
400
+ // returns in <50ms with zero Hub round-trips.
401
+ if (opts.getCachedCatalog &&
402
+ opts.getIsIdentityReady &&
403
+ !opts.getIsIdentityReady()) {
404
+ const cached = opts.getCachedCatalog();
405
+ if (cached) {
406
+ const currentRevision = opts.getCurrentToolSurfaceRevision?.() ?? null;
407
+ const valid = (opts.isCacheValid ?? (() => false))(cached, currentRevision);
408
+ if (valid) {
409
+ log(`[ListTools] served from cache (${cached.catalog.length} tools)`);
410
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
411
+ return { tools: cached.catalog };
412
+ }
413
+ log(`[ListTools] cache stale (cached.toolSurfaceRevision=${cached.toolSurfaceRevision}, current=${currentRevision ?? "unknown"}) — bootstrapping`);
414
+ }
415
+ else {
416
+ log("[ListTools] no cache (bootstrapping cache from Hub)");
417
+ }
418
+ }
419
+ if (opts.listToolsGate)
420
+ await opts.listToolsGate;
421
+ // bug-114 fallback-gap fix (2026-05-26): on cold-start the
422
+ // bootstrap path can race the Hub transport's connection
423
+ // completion — `listToolsGate` is currently undefined in every
424
+ // host shim, so the await above is a no-op. Pre-fix the
425
+ // handler returned `{ tools: [] }` silently when
426
+ // `isUsableAgent(agent)` was false, and host MCP clients
427
+ // (Claude Code) cached the empty list as the authoritative
428
+ // tool surface for the session — invisible comms-loss across
429
+ // the agent population, especially after a
430
+ // CATALOG_SCHEMA_VERSION bump invalidated all on-disk caches.
431
+ //
432
+ // Post-fix: short bounded retry (LIST_TOOLS_RETRY_ATTEMPTS ×
433
+ // LIST_TOOLS_RETRY_DELAY_MS) to absorb the narrow handshake
434
+ // race, then raise a structured MCP error (-32603 InternalError)
435
+ // so the host surfaces an actionable failure instead of
436
+ // caching `{ tools: [] }`. Bootstrap-completed telemetry emits
437
+ // on every path exit so silent zero-returns are architecturally
438
+ // impossible.
439
+ let agent = null;
440
+ for (let attempt = 1; attempt <= LIST_TOOLS_RETRY_ATTEMPTS; attempt++) {
441
+ const candidate = opts.getAgent();
442
+ if (isUsableAgent(candidate)) {
443
+ agent = candidate;
444
+ break;
445
+ }
446
+ if (attempt < LIST_TOOLS_RETRY_ATTEMPTS) {
447
+ log(`[ListTools] adapter not ready (attempt ${attempt}/${LIST_TOOLS_RETRY_ATTEMPTS}) — retrying in ${LIST_TOOLS_RETRY_DELAY_MS}ms`);
448
+ await new Promise((resolve) => setTimeout(resolve, LIST_TOOLS_RETRY_DELAY_MS));
449
+ }
450
+ }
451
+ if (!agent) {
452
+ log(`[ListTools] bootstrap failed: adapter not ready after ${LIST_TOOLS_RETRY_ATTEMPTS} attempts — raising structured error`);
453
+ throw new McpError(ErrorCode.InternalError, "Adapter not ready: Hub transport has not connected yet. Retry the request.");
454
+ }
455
+ // Route through agent.listTools() so any configured cognitive
456
+ // pipeline's onListTools middleware (ToolDescriptionEnricher,
457
+ // ResponseSummarizer, etc.) observes and modifies the surface.
458
+ const tools = await agent.listTools();
459
+ // Best-effort cache write-back. Skip on empty results — never
460
+ // poison the cache with a zero-tool catalog (bug-114 hardening).
461
+ if (opts.persistCatalog && tools.length > 0) {
462
+ try {
463
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
464
+ opts.persistCatalog(tools);
465
+ }
466
+ catch (err) {
467
+ log(`[ListTools] persistCatalog hook threw (non-fatal): ${err.message ?? err}`);
468
+ }
469
+ }
470
+ else if (opts.persistCatalog && tools.length === 0) {
471
+ log("[ListTools] persistCatalog skipped (zero tools — refusing to poison cache)");
472
+ }
473
+ log(`[ListTools] bootstrap completed: ${tools.length} tools surfaced`);
474
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
475
+ return { tools: tools };
476
+ });
477
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
478
+ // M-Tool-Manager Slice B: the CallTool handler body is now the shared
479
+ // dispatch authority (`runToolDispatch`). This MCP handler is a thin
480
+ // caller that supplies the closure state as an explicit dispatch context
481
+ // and returns the (Slice-B faithful, still MCP-shaped) result. The
482
+ // per-call behavior — idle-gate, callToolGate+timeout, Hub-not-connected
483
+ // precheck, queueItemId injection, signal-FSM wrap, lease observe,
484
+ // onToolCallResult, error normalization — all lives in `dispatch.ts` and
485
+ // is shared byte-for-byte with any future host binding (pi native, ACP).
486
+ const dispatchCtx = {
487
+ getAgent: opts.getAgent,
488
+ pendingActionMap,
489
+ workLeases,
490
+ onCallStart: () => {
491
+ activeCallCount++;
492
+ },
493
+ onCallEnd: () => {
494
+ activeCallCount--;
495
+ },
496
+ callToolGate: opts.callToolGate,
497
+ callToolGateTimeoutMs: opts.callToolGateTimeoutMs,
498
+ onToolCallResult: opts.onToolCallResult,
499
+ log,
500
+ };
501
+ const incomingArgs = (request.params.arguments ?? {});
502
+ // The dispatch authority returns the Slice-B-faithful MCP-shaped result
503
+ // ({ content, isError }); the SDK's CallTool result type is broader
504
+ // (optional task/_meta fields the original literal also omitted). Cast at
505
+ // this MCP-binding boundary — MCP-shape concerns are the binding's job.
506
+ const dispatchResult = await runToolDispatch(dispatchCtx, request.params.name, incomingArgs);
507
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
508
+ return dispatchResult;
509
+ });
510
+ return server;
511
+ }
512
+ return {
513
+ pendingActionMap,
514
+ createMcpServer,
515
+ callbacks,
516
+ getClientInfo,
517
+ makePendingActionItemHandler,
518
+ pollBackstop,
519
+ ackMessage,
520
+ getActiveCallCount: () => activeCallCount,
521
+ isIdle: () => activeCallCount === 0,
522
+ workLeases,
523
+ };
524
+ }
525
+ /**
526
+ * bug-53 boot-time fail-fast: assert host adapter has opted into
527
+ * pollBackstop wiring (transport_heartbeat periodic timer). Throws if
528
+ * `dispatcher.pollBackstop === undefined` UNLESS `TRANSPORT_HEARTBEAT_ENABLED=false`
529
+ * is set explicitly (opt-out path).
530
+ *
531
+ * Class: substrate-runtime-gap (sister to bug-49/50/51). Mission-75 §3.3
532
+ * substrate (PollBackstop heartbeat-second-timer) shipped at thread-472 with
533
+ * unit tests green, but the host integration was never written — both
534
+ * adapters call `createSharedDispatcher({...})` without supplying
535
+ * `opts.pollBackstop`, so the dispatcher's pollBackstop field stayed
536
+ * `undefined` and neither the poll-timer nor the heartbeat-timer ever
537
+ * scheduled. ZERO `transport_heartbeat` MCP tool calls fired in 96 minutes
538
+ * post Hub-restart (5127-line shim.log + 1789-event ndjson confirmed).
539
+ *
540
+ * This assertion is a §6.4-equivalent gate: each host MUST call
541
+ * `assertHostWiringComplete(dispatcher)` post-startup so the bug-53 class
542
+ * cannot recur silently. Misconfiguration fails fast at boot, not invisibly
543
+ * after 96 minutes of clinical observability degradation.
544
+ */
545
+ export function assertHostWiringComplete(dispatcher, log = (m) => console.error(m)) {
546
+ if (dispatcher.pollBackstop !== undefined) {
547
+ return; // wiring complete
548
+ }
549
+ if (process.env.TRANSPORT_HEARTBEAT_ENABLED === "false") {
550
+ // Explicit opt-out path — log info-level so operators have forensics.
551
+ log("[adapter] pollBackstop intentionally disabled via TRANSPORT_HEARTBEAT_ENABLED=false");
552
+ return;
553
+ }
554
+ throw new Error("[adapter] HOST WIRING ERROR: pollBackstop not configured in createSharedDispatcher opts. " +
555
+ "Transport heartbeat will not fire (lastHeartbeatAt will stay frozen at adapter-startup). " +
556
+ "Per mission-75 §3.3 + bug-53 closure, host adapters MUST opt in to pollBackstop. " +
557
+ "Set TRANSPORT_HEARTBEAT_ENABLED=false to explicitly disable.");
558
+ }
559
+ //# sourceMappingURL=dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../../src/tool-manager/orchestrator/dispatcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * claimable-digest-tracker.ts — idea-353 W1 inbound wake (level-triggered,
3
+ * idle-gated, idempotent claimable-digest).
4
+ *
5
+ * The C1 work-queue is a pull model with no wake-on-ready signal: an idle
6
+ * eligible agent never learns that newly-claimable work appeared and sits
7
+ * unclaimed until someone manually nudges it (proven 3× in dogfood-1). This
8
+ * tracker is the **decision core** of the inbound wake — given the set of
9
+ * truly-claimable work ids for the agent's role (read via the stable
10
+ * `list_ready_work` contract) and whether the agent is idle, it decides whether
11
+ * to surface a digest ("N items claimable for your role").
12
+ *
13
+ * The design's ONE genuinely-new substrate is this **level-trigger / de-dup
14
+ * state** (the FR-22 storm-risk locus). The rules:
15
+ *
16
+ * - **Idle-gated (AC4):** never surface while the agent is mid-task. A busy
17
+ * tick neither emits NOR advances the baseline — so items that appeared
18
+ * while the agent was working are surfaced on the FIRST idle tick, not
19
+ * silently swallowed.
20
+ * - **Level-triggered / idempotent (AC3):** emit ONLY on an upward edge —
21
+ * 0→N, or a genuinely-NEW claimable id appeared since the last surfaced
22
+ * set. A steady N>0, a re-tick, or a Hub restart (the in-memory baseline
23
+ * survives, since the adapter process does not restart) yields no new ids
24
+ * → no emit. ID-keyed, not count-keyed, so a swap (one item claimed away,
25
+ * another appearing at the same count) still edges.
26
+ *
27
+ * Pairs with the at-tick guard in the host: the tracker is only fed on a
28
+ * SUCCESSFUL `list_ready_work` read — a failed read (Hub mid-restart, agent not
29
+ * streaming) skips the tick entirely and leaves the baseline untouched, so a
30
+ * transient empty/aborted read cannot manufacture a false 0→N replay.
31
+ *
32
+ * Pure + synchronous: all I/O (the list_ready_work read, the host emit) lives
33
+ * in the host tick; this class holds only the de-dup state + the decision, so
34
+ * the storm-proof contract is unit-testable without a live Hub (AC3/AC4).
35
+ */
36
+ export interface ClaimableDigestInput {
37
+ /** The truly-claimable work ids for the agent's role this tick (post-bug-181). */
38
+ claimableIds: string[];
39
+ /** The idle-gate: false = the agent is actively mid-task (never interrupt it). */
40
+ isIdle: boolean;
41
+ }
42
+ export interface ClaimableDigestDecision {
43
+ /** True iff an upward edge was detected while idle → surface the digest. */
44
+ emit: boolean;
45
+ /** Current claimable count (whether or not we emit). */
46
+ count: number;
47
+ /** Number of newly-appeared claimable ids since the last surfaced set. */
48
+ newCount: number;
49
+ }
50
+ export declare class ClaimableDigestTracker {
51
+ /** The claimable id set the agent was last woken about (the de-dup baseline). */
52
+ private lastSurfaced;
53
+ /**
54
+ * Decide whether this tick should surface a claimable-digest wake.
55
+ * @param input current claimable ids + idle state for this tick.
56
+ */
57
+ reconcile(input: ClaimableDigestInput): ClaimableDigestDecision;
58
+ /** Diagnostic/test accessor for the current de-dup baseline size. */
59
+ getSurfacedCount(): number;
60
+ }