@apnex/network-adapter 0.1.0

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 (52) hide show
  1. package/dist/hub-error.d.ts +42 -0
  2. package/dist/hub-error.js +74 -0
  3. package/dist/hub-error.js.map +1 -0
  4. package/dist/index.d.ts +29 -0
  5. package/dist/index.js +31 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/kernel/agent-client.d.ts +192 -0
  8. package/dist/kernel/agent-client.js +44 -0
  9. package/dist/kernel/agent-client.js.map +1 -0
  10. package/dist/kernel/event-router.d.ts +49 -0
  11. package/dist/kernel/event-router.js +150 -0
  12. package/dist/kernel/event-router.js.map +1 -0
  13. package/dist/kernel/handshake.d.ts +161 -0
  14. package/dist/kernel/handshake.js +257 -0
  15. package/dist/kernel/handshake.js.map +1 -0
  16. package/dist/kernel/instance.d.ts +40 -0
  17. package/dist/kernel/instance.js +79 -0
  18. package/dist/kernel/instance.js.map +1 -0
  19. package/dist/kernel/mcp-agent-client.d.ts +139 -0
  20. package/dist/kernel/mcp-agent-client.js +505 -0
  21. package/dist/kernel/mcp-agent-client.js.map +1 -0
  22. package/dist/kernel/poll-backstop.d.ts +108 -0
  23. package/dist/kernel/poll-backstop.js +243 -0
  24. package/dist/kernel/poll-backstop.js.map +1 -0
  25. package/dist/kernel/session-claim.d.ts +67 -0
  26. package/dist/kernel/session-claim.js +106 -0
  27. package/dist/kernel/session-claim.js.map +1 -0
  28. package/dist/kernel/state-sync.d.ts +43 -0
  29. package/dist/kernel/state-sync.js +85 -0
  30. package/dist/kernel/state-sync.js.map +1 -0
  31. package/dist/logger.d.ts +82 -0
  32. package/dist/logger.js +114 -0
  33. package/dist/logger.js.map +1 -0
  34. package/dist/notification-log.d.ts +29 -0
  35. package/dist/notification-log.js +66 -0
  36. package/dist/notification-log.js.map +1 -0
  37. package/dist/prompt-format.d.ts +38 -0
  38. package/dist/prompt-format.js +220 -0
  39. package/dist/prompt-format.js.map +1 -0
  40. package/dist/tool-manager/dispatcher.d.ts +180 -0
  41. package/dist/tool-manager/dispatcher.js +379 -0
  42. package/dist/tool-manager/dispatcher.js.map +1 -0
  43. package/dist/tool-manager/tool-catalog-cache.d.ts +85 -0
  44. package/dist/tool-manager/tool-catalog-cache.js +137 -0
  45. package/dist/tool-manager/tool-catalog-cache.js.map +1 -0
  46. package/dist/wire/mcp-transport.d.ts +120 -0
  47. package/dist/wire/mcp-transport.js +447 -0
  48. package/dist/wire/mcp-transport.js.map +1 -0
  49. package/dist/wire/transport.d.ts +174 -0
  50. package/dist/wire/transport.js +43 -0
  51. package/dist/wire/transport.js.map +1 -0
  52. package/package.json +32 -0
@@ -0,0 +1,180 @@
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 type { AgentClientCallbacks, AgentEvent, SessionState, SessionReconnectReason } from "../kernel/agent-client.js";
23
+ import type { McpAgentClient } from "../kernel/mcp-agent-client.js";
24
+ import { PollBackstop, type PollBackstopOptions } from "../kernel/poll-backstop.js";
25
+ import type { DrainedPendingAction } from "../kernel/state-sync.js";
26
+ import type { CachedCatalog } from "./tool-catalog-cache.js";
27
+ export interface DispatcherClientInfo {
28
+ name: string;
29
+ version: string;
30
+ }
31
+ /**
32
+ * Universal Adapter notification contract — generic shim-injection
33
+ * callback bag. Layer 3 (per-host shim) implements these to bind
34
+ * dispatcher events into host-specific render-surfaces (claude
35
+ * `<channel>` / opencode `promptAsync` / future hosts).
36
+ *
37
+ * Spec: docs/specs/universal-adapter-notification-contract.md
38
+ */
39
+ export interface DispatcherNotificationHooks {
40
+ onActionableEvent?: (event: AgentEvent) => void;
41
+ onInformationalEvent?: (event: AgentEvent) => void;
42
+ onStateChange?: (state: SessionState, previous: SessionState, reason?: SessionReconnectReason) => void;
43
+ onPendingActionItem?: (item: DrainedPendingAction) => void;
44
+ }
45
+ export interface SharedDispatcherOptions {
46
+ /**
47
+ * Late-binding agent accessor. Some hosts (opencode) construct the
48
+ * dispatcher before the McpAgentClient connection is established.
49
+ * Returning `null` means "not connected yet" — the dispatcher
50
+ * surfaces a "Hub not connected" error envelope on CallTool and an
51
+ * empty tool-list on ListTools.
52
+ */
53
+ getAgent: () => McpAgentClient | null;
54
+ /** Adapter version reported in MCP serverInfo. */
55
+ proxyVersion: string;
56
+ /** MCP server name. Default: "proxy". */
57
+ serverName?: string;
58
+ /**
59
+ * MCP server capabilities advertised at Initialize. Default:
60
+ * `{ tools: {}, logging: {} }`. Hosts with extra capabilities
61
+ * (e.g. claude `experimental.claude/channel`) override.
62
+ */
63
+ serverCapabilities?: Record<string, unknown>;
64
+ /** Diagnostic logger. No-op default. */
65
+ log?: (msg: string) => void;
66
+ /**
67
+ * Resolves when the underlying McpAgentClient handshake completes
68
+ * (transport connected + identity asserted). Gates the ListTools
69
+ * handler so the host's catalog fetch doesn't block on the slow
70
+ * full-sync phase. Omit when no gating is needed (tests).
71
+ *
72
+ * Was: `handshakeComplete` (per-plugin dispatchers). Renamed in
73
+ * mission-55 cleanup per Design v1.2 Q5: name what is gated, not
74
+ * what is complete.
75
+ */
76
+ listToolsGate?: Promise<void>;
77
+ /**
78
+ * Resolves when the McpAgentClient session is claim-eligible
79
+ * (eager mode: claim_session returned; lazy mode: identity ready).
80
+ * Gates the CallTool handler so tool dispatch waits until the
81
+ * Hub will accept it. Omit when no gating is needed.
82
+ *
83
+ * Was: `agentReady` (per-plugin dispatchers). Renamed in mission-55
84
+ * cleanup per Design v1.2 Q5.
85
+ */
86
+ callToolGate?: Promise<void>;
87
+ getCachedCatalog?: () => CachedCatalog | null;
88
+ getIsIdentityReady?: () => boolean;
89
+ getCurrentHubVersion?: () => string | null;
90
+ persistCatalog?: (catalog: unknown[]) => void;
91
+ /**
92
+ * Optional cache-validity check. When omitted, cache-fallback
93
+ * conservatively treats every cached entry as invalid (live-fetch
94
+ * dominates). Wire `isCacheValid` from `./tool-catalog-cache.js`
95
+ * to enable Hub-version-keyed validity.
96
+ */
97
+ isCacheValid?: (cached: CachedCatalog, currentHubVersion: string | null | undefined) => boolean;
98
+ /**
99
+ * Universal Adapter notification contract. Host shim attaches its
100
+ * render-surface bindings here.
101
+ */
102
+ notificationHooks?: DispatcherNotificationHooks;
103
+ /**
104
+ * Mission-56 W3.3: opt-in adapter-side hybrid poll backstop. When
105
+ * supplied, the dispatcher constructs a PollBackstop (Design v1.2
106
+ * commitment #5) that periodically calls `list_messages` with a
107
+ * `since` cursor + `status: "new"` filter and surfaces each delta
108
+ * Message via the same MessageRouter as the SSE inline path
109
+ * (preserving seen-id LRU dedup across both paths).
110
+ *
111
+ * Pass `{ role: "engineer" | "architect" }` minimum; cadence
112
+ * defaults to 5min (`OIS_ADAPTER_POLL_BACKSTOP_S` env override).
113
+ * Omit to disable polling (push-only mode).
114
+ */
115
+ pollBackstop?: Omit<PollBackstopOptions, "onPolledMessage">;
116
+ }
117
+ export interface SharedDispatcher {
118
+ /** ADR-017 queueItemId tracking map. Keyed by `${dispatchType}:${entityRef}`. */
119
+ pendingActionMap: Map<string, string>;
120
+ /**
121
+ * Lazy MCP server factory. Hosts call this to obtain a fresh Server
122
+ * instance for each host transport (stdio: once at startup;
123
+ * Bun-HTTP: once per HTTP session).
124
+ */
125
+ createMcpServer: () => Server;
126
+ /**
127
+ * AgentClientCallbacks suitable for `agent.setCallbacks(...)`. Wires
128
+ * pendingActionMap-population + propagates to notificationHooks for
129
+ * host-specific render-surface.
130
+ */
131
+ callbacks: AgentClientCallbacks;
132
+ /** Returns last-captured Initialize-time clientInfo. */
133
+ getClientInfo: () => DispatcherClientInfo;
134
+ /**
135
+ * Builds an `onPendingActionItem` handshake callback that populates
136
+ * pendingActionMap (drain-path parity with the SSE inline-queueItemId
137
+ * path) and forwards to the supplied hooks.
138
+ */
139
+ makePendingActionItemHandler: (hooks?: DispatcherNotificationHooks) => (item: DrainedPendingAction) => void;
140
+ /**
141
+ * Mission-56 W3.3: PollBackstop instance, present iff `opts.pollBackstop`
142
+ * was supplied. Hosts MAY call `pollBackstop.start(getAgent)` at the
143
+ * appropriate lifecycle moment (typically post-handshake, when the
144
+ * agent reaches `streaming`). Hosts MUST call `pollBackstop.stop()`
145
+ * on shutdown to clear the timer. Omitted (`undefined`) when polling
146
+ * is disabled (push-only mode).
147
+ */
148
+ pollBackstop?: PollBackstop;
149
+ /**
150
+ * Mission-56 W3.3: explicit-ack-on-action surface. Host shims call
151
+ * this when the consumer (LLM) has acted on or actively-deferred a
152
+ * Message that was previously claimed (per Option (i) ratified at
153
+ * thread-325 round-2). Idempotent: re-acks on already-acked Messages
154
+ * are no-ops. Errors swallowed (logged, non-fatal) — a missed ack
155
+ * leaves the Message at status `received`, which the next poll-tick
156
+ * naturally excludes from `status: "new"` so the consumer doesn't
157
+ * re-render it.
158
+ */
159
+ ackMessage: (messageId: string) => Promise<void>;
160
+ }
161
+ /** Compose the pendingActionMap key. Pure helper; exported for tests. */
162
+ /**
163
+ * Mission-62 W3: tools that should NOT trigger signal_working_*
164
+ * wrapping. The signal_* tools themselves would recurse infinitely;
165
+ * register_role + claim_session + drain_pending_actions are lifecycle
166
+ * tools, not semantic tool-call-work.
167
+ */
168
+ export declare const TOOL_CALL_SIGNAL_SKIP: ReadonlySet<string>;
169
+ export declare function pendingKey(dispatchType: string, entityRef: string): string;
170
+ /**
171
+ * Inject `sourceQueueItemId` into a settling tool call's arguments
172
+ * when a pendingActionMap entry is registered for the call's target.
173
+ * Currently only `create_thread_reply` settles a thread_message
174
+ * dispatch; extend this set as new auto-injection rules are ratified.
175
+ *
176
+ * Side effect: deletes the consumed map entry. Explicit
177
+ * sourceQueueItemId in the args wins over the map (no rewrite).
178
+ */
179
+ export declare function injectQueueItemId(name: string, args: Record<string, unknown>, pendingActionMap: Map<string, string>): Record<string, unknown>;
180
+ export declare function createSharedDispatcher(opts: SharedDispatcherOptions): SharedDispatcher;
@@ -0,0 +1,379 @@
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, } from "@modelcontextprotocol/sdk/types.js";
23
+ import { MessageRouter, SeenIdCache } from "@apnex/message-router";
24
+ import { PollBackstop } from "../kernel/poll-backstop.js";
25
+ /** Compose the pendingActionMap key. Pure helper; exported for tests. */
26
+ /**
27
+ * Mission-62 W3: tools that should NOT trigger signal_working_*
28
+ * wrapping. The signal_* tools themselves would recurse infinitely;
29
+ * register_role + claim_session + drain_pending_actions are lifecycle
30
+ * tools, not semantic tool-call-work.
31
+ */
32
+ export const TOOL_CALL_SIGNAL_SKIP = new Set([
33
+ "signal_working_started",
34
+ "signal_working_completed",
35
+ "signal_quota_blocked",
36
+ "signal_quota_recovered",
37
+ "register_role",
38
+ "claim_session",
39
+ "drain_pending_actions",
40
+ ]);
41
+ export function pendingKey(dispatchType, entityRef) {
42
+ return `${dispatchType}:${entityRef}`;
43
+ }
44
+ /**
45
+ * Inject `sourceQueueItemId` into a settling tool call's arguments
46
+ * when a pendingActionMap entry is registered for the call's target.
47
+ * Currently only `create_thread_reply` settles a thread_message
48
+ * dispatch; extend this set as new auto-injection rules are ratified.
49
+ *
50
+ * Side effect: deletes the consumed map entry. Explicit
51
+ * sourceQueueItemId in the args wins over the map (no rewrite).
52
+ */
53
+ export function injectQueueItemId(name, args, pendingActionMap) {
54
+ if (name !== "create_thread_reply")
55
+ return args;
56
+ const threadId = args.threadId;
57
+ if (typeof threadId !== "string")
58
+ return args;
59
+ if ("sourceQueueItemId" in args)
60
+ return args;
61
+ const queueItemId = pendingActionMap.get(pendingKey("thread_message", threadId));
62
+ if (!queueItemId)
63
+ return args;
64
+ pendingActionMap.delete(pendingKey("thread_message", threadId));
65
+ return { ...args, sourceQueueItemId: queueItemId };
66
+ }
67
+ export function createSharedDispatcher(opts) {
68
+ const log = opts.log ?? (() => { });
69
+ const serverName = opts.serverName ?? "proxy";
70
+ const serverCapabilities = opts.serverCapabilities ?? { tools: {}, logging: {} };
71
+ const pendingActionMap = new Map();
72
+ let capturedClientInfo = {
73
+ name: "unknown",
74
+ version: "0.0.0",
75
+ };
76
+ const getClientInfo = () => capturedClientInfo;
77
+ function isUsableAgent(agent) {
78
+ return !!agent && agent.isConnected !== false;
79
+ }
80
+ // ADR-017 Phase 1.1: SSE thread_message events carry queueItemId
81
+ // inline. Capture into pendingActionMap so the next settling
82
+ // create_thread_reply can auto-inject sourceQueueItemId — even if
83
+ // no drain ever populated the map. Eliminates the SSE-vs-drain race
84
+ // that caused false-positive escalations on early thread tests.
85
+ const captureQueueItemFromEvent = (event) => {
86
+ if (event.event !== "thread_message")
87
+ return;
88
+ const data = event.data;
89
+ const qid = data.queueItemId;
90
+ const threadId = data.threadId;
91
+ if (typeof qid === "string" && typeof threadId === "string") {
92
+ pendingActionMap.set(pendingKey("thread_message", threadId), qid);
93
+ }
94
+ };
95
+ // Mission-56 W2.2: Layer-2 routing. Every classified event goes
96
+ // through `@apnex/message-router` so Message-ID dedup (push+poll
97
+ // race) + kind→hook mapping live in one place. The host's
98
+ // `notificationHooks` bag is the router's hook surface — no
99
+ // shape adapter needed (the router's NotificationHooks interface
100
+ // mirrors DispatcherNotificationHooks exactly).
101
+ //
102
+ // The seen-id cache is shared across the construction-time router
103
+ // and any per-call routers minted by `makePendingActionItemHandler`,
104
+ // so a Message ID seen on the SSE inline path will dedup a later
105
+ // drain-path replay (and vice-versa).
106
+ const seenIdCache = new SeenIdCache();
107
+ const router = new MessageRouter({
108
+ hooks: opts.notificationHooks ?? {},
109
+ seenIdCache,
110
+ });
111
+ // Mission-56 W3.3: post-render claim. Extracts the Message ID from
112
+ // `message_arrived` events (W1a SSE shape: event.data.message.id)
113
+ // and fires `claim_message(id)` against the Hub via the agent. Per
114
+ // architect-issued W3 directive: claim happens AFTER the host hook
115
+ // renders (the ordering matches "shim calls after successful render
116
+ // to host" from Design v1.2 commitment #6). Errors are swallowed +
117
+ // logged — claim failure is non-fatal (the SSE path still rendered;
118
+ // the next poll-tick will pick up any unclaimed Message in the
119
+ // status === "new" set if needed).
120
+ //
121
+ // Multi-agent same-role: the Hub-side CAS enforces winner-takes-all
122
+ // (mission-56 W3.2). The wonClaim signal is informational; even if
123
+ // we lost, the host has already rendered (claim is post-render), so
124
+ // the loser still sees the Message — but only the winner's claim
125
+ // flips status to `received`, gating subsequent ack to a single
126
+ // canonical actor.
127
+ function fireClaimMessage(event) {
128
+ if (event.event !== "message_arrived")
129
+ return;
130
+ const data = event.data;
131
+ const message = data?.message;
132
+ const messageId = message?.id;
133
+ if (typeof messageId !== "string")
134
+ return;
135
+ const agent = opts.getAgent();
136
+ if (!agent || agent.state !== "streaming")
137
+ return;
138
+ void agent
139
+ .call("claim_message", { id: messageId })
140
+ .catch((err) => {
141
+ log(`[claim_message] non-fatal failure for ${messageId}: ${err?.message ?? String(err)}`);
142
+ });
143
+ }
144
+ const callbacks = {
145
+ onActionableEvent: (event) => {
146
+ captureQueueItemFromEvent(event);
147
+ router.route({ kind: "notification.actionable", event });
148
+ // Mission-56 W3.3: post-render claim (replaces W2.2 stub-claim TODO).
149
+ fireClaimMessage(event);
150
+ },
151
+ onInformationalEvent: (event) => {
152
+ router.route({ kind: "notification.informational", event });
153
+ },
154
+ onStateChange: (state, previous, reason) => {
155
+ log(`Connection: ${previous} → ${state}${reason ? ` (${reason})` : ""}`);
156
+ router.route({ kind: "state.change", state, previous, reason });
157
+ },
158
+ };
159
+ // Mission-56 W3.3: PollBackstop construction (opt-in via opts.pollBackstop).
160
+ // The backstop fires `list_messages({status:"new", since:<lastSeen>})`
161
+ // periodically and routes each delta Message through the same
162
+ // MessageRouter as the SSE inline path so seen-id LRU dedup catches
163
+ // push+poll race overlap. Polled Messages also fire claim_message
164
+ // (the router invocation goes through onActionableEvent which
165
+ // already includes fireClaimMessage).
166
+ const pollBackstop = opts.pollBackstop
167
+ ? new PollBackstop({
168
+ ...opts.pollBackstop,
169
+ log: opts.pollBackstop.log ?? log,
170
+ onPolledMessage: (event) => {
171
+ router.route({ kind: "notification.actionable", event });
172
+ fireClaimMessage(event);
173
+ },
174
+ })
175
+ : undefined;
176
+ // Mission-56 W3.3: explicit-ack-on-action helper. Host shims wire
177
+ // this to fire when the LLM consumer has acted on (or actively
178
+ // deferred) a Message — per Option (i) ratified at thread-325 round-2,
179
+ // ack is tied to consumer-action, not auto-on-render.
180
+ async function ackMessage(messageId) {
181
+ const agent = opts.getAgent();
182
+ if (!agent || agent.state !== "streaming")
183
+ return;
184
+ try {
185
+ await agent.call("ack_message", { id: messageId });
186
+ }
187
+ catch (err) {
188
+ log(`[ack_message] non-fatal failure for ${messageId}: ${err?.message ?? String(err)}`);
189
+ }
190
+ }
191
+ const makePendingActionItemHandler = (hooks) => {
192
+ // Per-call hooks override the construction-time bag for the
193
+ // drain path (preserves the original makePendingActionItemHandler
194
+ // contract — claude-plugin shim uses this to bind a custom log
195
+ // sink). Share the seen-id cache so drain-path replays dedup
196
+ // against SSE-path inline deliveries.
197
+ const drainRouter = new MessageRouter({
198
+ hooks: hooks ?? {},
199
+ seenIdCache,
200
+ });
201
+ return (item) => {
202
+ pendingActionMap.set(pendingKey(item.dispatchType, item.entityRef), item.id);
203
+ drainRouter.route({ kind: "pending-action.dispatch", item });
204
+ };
205
+ };
206
+ function createMcpServer() {
207
+ const server = new Server({ name: serverName, version: opts.proxyVersion }, { capabilities: serverCapabilities });
208
+ // Initialize handler is intentionally NOT gated — host MCP clients
209
+ // (e.g. Claude Code) have a tight initialize timeout that's faster
210
+ // than a full Hub handshake. The Initialize handler captures
211
+ // clientInfo for downstream handshake passthrough.
212
+ server.setRequestHandler(InitializeRequestSchema, async (request) => {
213
+ try {
214
+ const ci = request.params
215
+ .clientInfo;
216
+ if (ci &&
217
+ typeof ci.name === "string" &&
218
+ typeof ci.version === "string") {
219
+ capturedClientInfo = { name: ci.name, version: ci.version };
220
+ log(`[Handshake] Captured clientInfo: ${ci.name}@${ci.version}`);
221
+ }
222
+ }
223
+ catch (err) {
224
+ log(`[Handshake] clientInfo capture failed (non-fatal): ${err}`);
225
+ }
226
+ return {
227
+ protocolVersion: request.params.protocolVersion,
228
+ capabilities: serverCapabilities,
229
+ serverInfo: { name: serverName, version: opts.proxyVersion },
230
+ };
231
+ });
232
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
233
+ // Probe-safe cache fallback. When identity hasn't yet resolved
234
+ // (e.g. `claude mcp list` spawned the adapter and will exit
235
+ // before the handshake completes), serve the persisted catalog
236
+ // if available + valid against the current Hub version. Probe
237
+ // returns in <50ms with zero Hub round-trips.
238
+ if (opts.getCachedCatalog &&
239
+ opts.getIsIdentityReady &&
240
+ !opts.getIsIdentityReady()) {
241
+ const cached = opts.getCachedCatalog();
242
+ if (cached) {
243
+ const currentVersion = opts.getCurrentHubVersion?.() ?? null;
244
+ const valid = (opts.isCacheValid ?? (() => false))(cached, currentVersion);
245
+ if (valid) {
246
+ log("[ListTools] served from cache");
247
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
248
+ return { tools: cached.catalog };
249
+ }
250
+ log(`[ListTools] cache stale (cached.hubVersion=${cached.hubVersion}, current=${currentVersion ?? "unknown"}) — bootstrapping`);
251
+ }
252
+ else {
253
+ log("[ListTools] no cache (bootstrapping cache from Hub)");
254
+ }
255
+ }
256
+ if (opts.listToolsGate)
257
+ await opts.listToolsGate;
258
+ const agent = opts.getAgent();
259
+ if (!isUsableAgent(agent))
260
+ return { tools: [] };
261
+ // Route through agent.listTools() so any configured cognitive
262
+ // pipeline's onListTools middleware (ToolDescriptionEnricher,
263
+ // ResponseSummarizer, etc.) observes and modifies the surface.
264
+ const tools = await agent.listTools();
265
+ // Best-effort cache write-back. Failures log + continue; the
266
+ // primary response is already on its way to the host.
267
+ if (opts.persistCatalog) {
268
+ try {
269
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
270
+ opts.persistCatalog(tools);
271
+ }
272
+ catch (err) {
273
+ log(`[ListTools] persistCatalog hook threw (non-fatal): ${err.message ?? err}`);
274
+ }
275
+ }
276
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
277
+ return { tools: tools };
278
+ });
279
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
280
+ const callStartedAt = Date.now();
281
+ const requestedTool = request.params.name;
282
+ log(`[CallTool] ${requestedTool} entered`);
283
+ try {
284
+ if (opts.callToolGate) {
285
+ log(`[CallTool] ${requestedTool} awaiting callToolGate`);
286
+ await opts.callToolGate;
287
+ log(`[CallTool] ${requestedTool} gate passed (+${Date.now() - callStartedAt}ms)`);
288
+ }
289
+ const agent = opts.getAgent();
290
+ if (!isUsableAgent(agent)) {
291
+ log(`[CallTool] ${requestedTool} aborted — Hub not connected`);
292
+ return {
293
+ content: [
294
+ {
295
+ type: "text",
296
+ text: JSON.stringify({
297
+ error: "Hub not connected",
298
+ message: "The Hub adapter is not currently connected.",
299
+ }),
300
+ },
301
+ ],
302
+ isError: true,
303
+ };
304
+ }
305
+ const { name } = request.params;
306
+ const incomingArgs = (request.params.arguments ?? {});
307
+ const outgoingArgs = injectQueueItemId(name, incomingArgs, pendingActionMap);
308
+ // ── Mission-62 W3 — activity FSM signal wrapping ─────────────
309
+ //
310
+ // Wrap each LLM-driven tool call with signal_working_started +
311
+ // signal_working_completed RPCs (fire-and-forget; eventual-
312
+ // consistency on Hub-side activity FSM). Per Design v1.0 §5.2:
313
+ // implicit-only inference infeasible (LLM-to-MCP-tool-call
314
+ // path doesn't enqueue items per ADR-017 §M1-M2); explicit
315
+ // signaling required for routing peers to see this agent's
316
+ // working state.
317
+ //
318
+ // Skip-list prevents infinite recursion (signal_* calls would
319
+ // wrap themselves) + handshake/lifecycle tools that are not
320
+ // semantically tool-call-work (register_role, claim_session,
321
+ // drain_pending_actions).
322
+ const wrapWithSignal = !TOOL_CALL_SIGNAL_SKIP.has(name);
323
+ if (wrapWithSignal) {
324
+ // Fire-and-forget: don't await; Hub-side eventual-consistency
325
+ // is acceptable for v1.0 routing-cache use case.
326
+ agent.call("signal_working_started", { toolName: name }).catch((err) => {
327
+ log(`[mission-62] signal_working_started fire-and-forget failed (non-fatal): ${err?.message ?? err}`);
328
+ });
329
+ }
330
+ let result;
331
+ const agentCallStart = Date.now();
332
+ log(`[CallTool] ${name} dispatching to agent.call (wrapWithSignal=${wrapWithSignal})`);
333
+ try {
334
+ result = await agent.call(name, outgoingArgs);
335
+ log(`[CallTool] ${name} agent.call returned in ${Date.now() - agentCallStart}ms`);
336
+ }
337
+ finally {
338
+ if (wrapWithSignal) {
339
+ agent.call("signal_working_completed", {}).catch((err) => {
340
+ log(`[mission-62] signal_working_completed fire-and-forget failed (non-fatal): ${err?.message ?? err}`);
341
+ });
342
+ }
343
+ }
344
+ log(`[CallTool] ${name} completed in ${Date.now() - callStartedAt}ms total`);
345
+ return {
346
+ content: [
347
+ {
348
+ type: "text",
349
+ text: typeof result === "string"
350
+ ? result
351
+ : JSON.stringify(result, null, 2),
352
+ },
353
+ ],
354
+ };
355
+ }
356
+ catch (err) {
357
+ const message = err instanceof Error ? err.message : String(err);
358
+ log(`[CallTool] ${requestedTool} threw after ${Date.now() - callStartedAt}ms: ${message}`);
359
+ return {
360
+ content: [
361
+ { type: "text", text: JSON.stringify({ error: message }) },
362
+ ],
363
+ isError: true,
364
+ };
365
+ }
366
+ });
367
+ return server;
368
+ }
369
+ return {
370
+ pendingActionMap,
371
+ createMcpServer,
372
+ callbacks,
373
+ getClientInfo,
374
+ makePendingActionItemHandler,
375
+ pollBackstop,
376
+ ackMessage,
377
+ };
378
+ }
379
+ //# sourceMappingURL=dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../src/tool-manager/dispatcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * tool-catalog-cache.ts — per-WORK_DIR Hub tool catalog cache.
3
+ *
4
+ * Probe-safe ListTools support: when the host calls tools/list before
5
+ * the adapter's identityReady has resolved (e.g. `claude mcp list`
6
+ * spawning the adapter just to enumerate available tools), the
7
+ * dispatcher serves the catalog from a persisted cache without
8
+ * touching the Hub. Together with the lazy session-claim path this
9
+ * makes probes fully Hub-free against a warm cache.
10
+ *
11
+ * Storage: $WORK_DIR/.ois/tool-catalog.json
12
+ * {
13
+ * schemaVersion: 1,
14
+ * hubVersion: "1.0.0",
15
+ * fetchedAt: "2026-04-22T...Z",
16
+ * catalog: [...]
17
+ * }
18
+ *
19
+ * Invalidation: Hub-version mismatch only. No TTL — the catalog is
20
+ * static between Hub deploys; TTL would add noise without correctness
21
+ * value. Schema-version mismatch on read returns null (cache treated
22
+ * as invalid; future schema evolution bumps CATALOG_SCHEMA_VERSION).
23
+ *
24
+ * Atomicity: writeCache uses tmp-file + rename so partial writes on
25
+ * crash don't corrupt the cache. Parse errors on read also return
26
+ * null — the cache self-heals on next bootstrap.
27
+ *
28
+ * Failure modes (best-effort; readCache + writeCache never throw on
29
+ * the primary flow):
30
+ * - missing file: readCache returns null
31
+ * - parse error: readCache returns null + logs
32
+ * - schema-version mismatch: readCache returns null
33
+ * - $WORK_DIR readonly: writeCache logs + no-ops
34
+ * - disk full: writeCache logs + no-ops
35
+ */
36
+ /**
37
+ * Bumping CATALOG_SCHEMA_VERSION forces all existing cache files to
38
+ * be treated as invalid + re-bootstrapped. Use when changing the
39
+ * cache file shape.
40
+ */
41
+ export declare const CATALOG_SCHEMA_VERSION = 1;
42
+ /**
43
+ * MCP tool catalog entry shape. Kept loose (`unknown[]`) since the
44
+ * cache is opaque storage; the dispatcher hands the catalog back to
45
+ * the host without re-shaping.
46
+ */
47
+ export type ToolCatalog = unknown[];
48
+ export interface CachedCatalog {
49
+ schemaVersion: number;
50
+ hubVersion: string;
51
+ fetchedAt: string;
52
+ catalog: ToolCatalog;
53
+ }
54
+ /** Compute the canonical cache path for a given WORK_DIR. */
55
+ export declare function cachePathFor(workDir: string): string;
56
+ /**
57
+ * Read the cache file. Returns null on missing file, parse error,
58
+ * schema-version mismatch, or shape mismatch. Never throws — the
59
+ * primary ListTools flow always falls through to a live Hub fetch
60
+ * when readCache returns null.
61
+ */
62
+ export declare function readCache(workDir: string, log?: (msg: string) => void): CachedCatalog | null;
63
+ /**
64
+ * Persist the catalog atomically. Writes a sibling tmp file then
65
+ * renames — so a crash mid-write leaves either the previous cache
66
+ * intact OR the new cache fully landed. Best-effort: failures log
67
+ * and return; the caller's primary flow continues.
68
+ */
69
+ export declare function writeCache(workDir: string, catalog: ToolCatalog, hubVersion: string, log?: (msg: string) => void): void;
70
+ /**
71
+ * Check cache validity against the current Hub version.
72
+ *
73
+ * Two semantics:
74
+ * - currentHubVersion is a non-empty string: strict equality vs
75
+ * cached.hubVersion. Mismatch → invalid → caller re-bootstraps.
76
+ * - currentHubVersion is null/undefined/empty: caller doesn't know
77
+ * the current Hub version yet (e.g. /health fetch in flight at
78
+ * startup). Trust the cache (probe-friendly default) — worst
79
+ * case is serving a stale catalog ONCE until the next /health
80
+ * fetch completes and a real session refreshes the cache.
81
+ *
82
+ * Schema-version check is enforced inside readCache, so isCacheValid
83
+ * never sees a wrong-schema cached object.
84
+ */
85
+ export declare function isCacheValid(cached: CachedCatalog, currentHubVersion: string | null | undefined): boolean;