@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,108 @@
1
+ /**
2
+ * poll-backstop.ts — adapter-side hybrid poll backstop (mission-56 W3.3).
3
+ *
4
+ * Periodic `list_messages({target.role, status: "new", since: <last-seen>})`
5
+ * poll at LONG cadence — the SAFETY NET behind the W1a SSE push pipeline.
6
+ * Per Design v1.2 commitment #5: push is the primary path; polling fills
7
+ * the gaps caused by adapter restart, transient SSE drop between
8
+ * reconnect-replay (W1b) windows, and any push-edge dropped events the
9
+ * adapter didn't see.
10
+ *
11
+ * Anti-pattern guard (per architect-issued W3 directive): poll cadence
12
+ * MUST be measurably longer than push latency. Default 5min
13
+ * (`OIS_ADAPTER_POLL_BACKSTOP_S=300`); 60s minimum (1min) for tests.
14
+ *
15
+ * Cursor persistence: last-seen Message ID is persisted across adapter
16
+ * restarts so the poll fetches only the delta on each tick. Default
17
+ * cursor file: `~/.ois/poll-cursor-<role>-<agentId>.json`. The
18
+ * `since` cursor is REQUIRED on every poll — initial-state (no cursor
19
+ * file) sends `since` undefined and treats the first poll's results as
20
+ * the cold-start baseline (which the seen-id LRU dedup in the
21
+ * MessageRouter de-collides against any concurrent push delivery of
22
+ * the same Message IDs).
23
+ *
24
+ * Each surfaced Message is routed through the host-supplied callback
25
+ * (the same `onActionableEvent` shape used by the SSE inline path).
26
+ * The W2.1 `@apnex/message-router` seen-id LRU catches the push+poll
27
+ * race overlap so a Message that arrived via SSE in the last 5min
28
+ * window is not double-rendered when the next poll-tick sees it too.
29
+ */
30
+ import type { IAgentClient, AgentEvent } from "./agent-client.js";
31
+ export interface PollBackstopOptions {
32
+ /**
33
+ * Role this adapter polls for (e.g. "engineer", "architect"). Becomes
34
+ * the `target.role` filter on each `list_messages` call.
35
+ */
36
+ role: string;
37
+ /**
38
+ * Poll cadence in seconds. Defaults to `OIS_ADAPTER_POLL_BACKSTOP_S`
39
+ * env var (parsed as integer), falling back to 300 (5 minutes).
40
+ * Floored at `MIN_CADENCE_SECONDS` (60) to enforce the
41
+ * "measurably longer than push latency" anti-pattern guard.
42
+ */
43
+ cadenceSeconds?: number;
44
+ /**
45
+ * Override cursor-file location. Defaults to
46
+ * `~/.ois/poll-cursor-<role>-<agentId>.json`. Tests inject a
47
+ * temp path here; production callers can override to land cursors
48
+ * in workspace-local state (e.g. `.ois/poll-cursor.json`).
49
+ */
50
+ cursorFile?: string;
51
+ /**
52
+ * Diagnostic logger. No-op default. Mirrors the dispatcher's `log`
53
+ * convention.
54
+ */
55
+ log?: (msg: string) => void;
56
+ /**
57
+ * Hook fired for each Message surfaced by the poll. Same shape as
58
+ * `DispatcherNotificationHooks.onActionableEvent` so the dispatcher
59
+ * can wire poll output through the same MessageRouter as the SSE
60
+ * inline path (preserving seen-id LRU dedup across both paths).
61
+ */
62
+ onPolledMessage: (event: AgentEvent) => void;
63
+ }
64
+ /**
65
+ * Resolve the cursor file path. Defaults to
66
+ * `~/.ois/poll-cursor-<role>-<agentId>.json`. The agentId is
67
+ * stable across restarts (mission-19 Agent identity) so a single
68
+ * adapter instance always writes/reads the same cursor file.
69
+ */
70
+ export declare function defaultCursorFile(role: string, agentId: string): string;
71
+ /**
72
+ * Read the persisted cursor (or undefined if no cursor file exists).
73
+ * Corruption-tolerant: returns undefined on parse failure (cold-start
74
+ * recovery — the seen-id LRU absorbs any double-delivery from the
75
+ * resulting full replay).
76
+ */
77
+ export declare function readCursor(path: string): string | undefined;
78
+ /**
79
+ * Write the cursor atomically (writeFileSync; same pattern as the
80
+ * tool-catalog cache). Best-effort on failure (poll continues on the
81
+ * in-process cursor; restart will re-read whatever last persisted).
82
+ */
83
+ export declare function writeCursor(path: string, lastSeenId: string | undefined, log?: (msg: string) => void): void;
84
+ /**
85
+ * The PollBackstop runs the periodic `list_messages` tick loop and
86
+ * surfaces each delta Message via the `onPolledMessage` hook.
87
+ *
88
+ * Lifecycle: `start(getAgent)` begins the timer; `stop()` cancels it
89
+ * cleanly. Idempotent — start while already-started is a no-op; stop
90
+ * while not-started is a no-op.
91
+ */
92
+ export declare class PollBackstop {
93
+ private readonly opts;
94
+ private timer;
95
+ private resolvedCursorFile;
96
+ private inFlight;
97
+ constructor(opts: PollBackstopOptions);
98
+ /** Start the periodic poll. Idempotent. */
99
+ start(getAgent: () => IAgentClient | null): void;
100
+ /** Stop the periodic poll. Idempotent. */
101
+ stop(): void;
102
+ /**
103
+ * Single poll iteration. Exposed for tests + diagnostic operators
104
+ * (e.g. force-tick on demand). Reentrant-safe via in-flight guard
105
+ * — concurrent ticks coalesce on the in-flight one.
106
+ */
107
+ tick(getAgent: () => IAgentClient | null): Promise<void>;
108
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * poll-backstop.ts — adapter-side hybrid poll backstop (mission-56 W3.3).
3
+ *
4
+ * Periodic `list_messages({target.role, status: "new", since: <last-seen>})`
5
+ * poll at LONG cadence — the SAFETY NET behind the W1a SSE push pipeline.
6
+ * Per Design v1.2 commitment #5: push is the primary path; polling fills
7
+ * the gaps caused by adapter restart, transient SSE drop between
8
+ * reconnect-replay (W1b) windows, and any push-edge dropped events the
9
+ * adapter didn't see.
10
+ *
11
+ * Anti-pattern guard (per architect-issued W3 directive): poll cadence
12
+ * MUST be measurably longer than push latency. Default 5min
13
+ * (`OIS_ADAPTER_POLL_BACKSTOP_S=300`); 60s minimum (1min) for tests.
14
+ *
15
+ * Cursor persistence: last-seen Message ID is persisted across adapter
16
+ * restarts so the poll fetches only the delta on each tick. Default
17
+ * cursor file: `~/.ois/poll-cursor-<role>-<agentId>.json`. The
18
+ * `since` cursor is REQUIRED on every poll — initial-state (no cursor
19
+ * file) sends `since` undefined and treats the first poll's results as
20
+ * the cold-start baseline (which the seen-id LRU dedup in the
21
+ * MessageRouter de-collides against any concurrent push delivery of
22
+ * the same Message IDs).
23
+ *
24
+ * Each surfaced Message is routed through the host-supplied callback
25
+ * (the same `onActionableEvent` shape used by the SSE inline path).
26
+ * The W2.1 `@apnex/message-router` seen-id LRU catches the push+poll
27
+ * race overlap so a Message that arrived via SSE in the last 5min
28
+ * window is not double-rendered when the next poll-tick sees it too.
29
+ */
30
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
31
+ import { dirname, join } from "node:path";
32
+ import { homedir } from "node:os";
33
+ const DEFAULT_CADENCE_SECONDS = 300; // 5 minutes
34
+ const MIN_CADENCE_SECONDS = 60; // 1 minute floor — anti-pattern guard
35
+ /**
36
+ * Resolve the cursor file path. Defaults to
37
+ * `~/.ois/poll-cursor-<role>-<agentId>.json`. The agentId is
38
+ * stable across restarts (mission-19 Agent identity) so a single
39
+ * adapter instance always writes/reads the same cursor file.
40
+ */
41
+ export function defaultCursorFile(role, agentId) {
42
+ return join(homedir(), ".ois", `poll-cursor-${role}-${agentId}.json`);
43
+ }
44
+ /**
45
+ * Read the persisted cursor (or undefined if no cursor file exists).
46
+ * Corruption-tolerant: returns undefined on parse failure (cold-start
47
+ * recovery — the seen-id LRU absorbs any double-delivery from the
48
+ * resulting full replay).
49
+ */
50
+ export function readCursor(path) {
51
+ if (!existsSync(path))
52
+ return undefined;
53
+ try {
54
+ const raw = readFileSync(path, "utf-8");
55
+ const parsed = JSON.parse(raw);
56
+ if (parsed &&
57
+ typeof parsed === "object" &&
58
+ (parsed.lastSeenId === undefined || typeof parsed.lastSeenId === "string")) {
59
+ return parsed.lastSeenId;
60
+ }
61
+ return undefined;
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ }
67
+ /**
68
+ * Write the cursor atomically (writeFileSync; same pattern as the
69
+ * tool-catalog cache). Best-effort on failure (poll continues on the
70
+ * in-process cursor; restart will re-read whatever last persisted).
71
+ */
72
+ export function writeCursor(path, lastSeenId, log = () => { }) {
73
+ const dir = dirname(path);
74
+ if (!existsSync(dir)) {
75
+ try {
76
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
77
+ }
78
+ catch (err) {
79
+ log(`[poll-backstop] mkdirSync(${dir}) failed: ${err}`);
80
+ return;
81
+ }
82
+ }
83
+ const body = {
84
+ lastSeenId,
85
+ updatedAt: new Date().toISOString(),
86
+ version: 1,
87
+ };
88
+ try {
89
+ writeFileSync(path, JSON.stringify(body, null, 2), { mode: 0o600 });
90
+ }
91
+ catch (err) {
92
+ log(`[poll-backstop] writeFileSync(${path}) failed: ${err}`);
93
+ }
94
+ }
95
+ function parseListMessagesResult(raw) {
96
+ const env = raw;
97
+ if (!env || !env.content || !env.content[0] || env.isError)
98
+ return null;
99
+ try {
100
+ return JSON.parse(env.content[0].text);
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ /**
107
+ * The PollBackstop runs the periodic `list_messages` tick loop and
108
+ * surfaces each delta Message via the `onPolledMessage` hook.
109
+ *
110
+ * Lifecycle: `start(getAgent)` begins the timer; `stop()` cancels it
111
+ * cleanly. Idempotent — start while already-started is a no-op; stop
112
+ * while not-started is a no-op.
113
+ */
114
+ export class PollBackstop {
115
+ opts;
116
+ timer = null;
117
+ resolvedCursorFile = null;
118
+ inFlight = false;
119
+ constructor(opts) {
120
+ const fromEnv = parseInt(process.env.OIS_ADAPTER_POLL_BACKSTOP_S ?? "", 10);
121
+ const cadence = Math.max(MIN_CADENCE_SECONDS, opts.cadenceSeconds ??
122
+ (Number.isFinite(fromEnv) ? fromEnv : DEFAULT_CADENCE_SECONDS));
123
+ this.opts = {
124
+ role: opts.role,
125
+ cadenceSeconds: cadence,
126
+ cursorFile: opts.cursorFile,
127
+ log: opts.log ?? (() => { }),
128
+ onPolledMessage: opts.onPolledMessage,
129
+ };
130
+ }
131
+ /** Start the periodic poll. Idempotent. */
132
+ start(getAgent) {
133
+ if (this.timer)
134
+ return;
135
+ const cadenceMs = this.opts.cadenceSeconds * 1000;
136
+ this.opts.log(`[poll-backstop] starting (role=${this.opts.role}, cadenceS=${this.opts.cadenceSeconds})`);
137
+ this.timer = setInterval(() => {
138
+ // Fire-and-forget; tick() handles its own errors.
139
+ void this.tick(getAgent);
140
+ }, cadenceMs);
141
+ if (this.timer.unref)
142
+ this.timer.unref();
143
+ }
144
+ /** Stop the periodic poll. Idempotent. */
145
+ stop() {
146
+ if (!this.timer)
147
+ return;
148
+ clearInterval(this.timer);
149
+ this.timer = null;
150
+ this.opts.log("[poll-backstop] stopped");
151
+ }
152
+ /**
153
+ * Single poll iteration. Exposed for tests + diagnostic operators
154
+ * (e.g. force-tick on demand). Reentrant-safe via in-flight guard
155
+ * — concurrent ticks coalesce on the in-flight one.
156
+ */
157
+ async tick(getAgent) {
158
+ if (this.inFlight)
159
+ return;
160
+ this.inFlight = true;
161
+ try {
162
+ const agent = getAgent();
163
+ if (!agent || agent.state !== "streaming")
164
+ return;
165
+ // Cursor file resolution: lazy on first tick (agentId only
166
+ // becomes known post-handshake).
167
+ if (!this.resolvedCursorFile) {
168
+ if (this.opts.cursorFile) {
169
+ this.resolvedCursorFile = this.opts.cursorFile;
170
+ }
171
+ else {
172
+ const agentId = agent.getSessionId() ?? "unknown";
173
+ // Prefer `getMetrics().agentId` when available — it's the
174
+ // post-handshake stable Agent identity, distinct from the
175
+ // session id which cycles on reconnect.
176
+ const metrics = agent.getMetrics?.();
177
+ const id = metrics?.agentId ?? agentId;
178
+ this.resolvedCursorFile = defaultCursorFile(this.opts.role, id);
179
+ }
180
+ }
181
+ const cursorFile = this.resolvedCursorFile;
182
+ const since = readCursor(cursorFile);
183
+ const args = {
184
+ targetRole: this.opts.role,
185
+ status: "new",
186
+ };
187
+ if (since !== undefined)
188
+ args.since = since;
189
+ let raw;
190
+ try {
191
+ raw = await agent.call("list_messages", args);
192
+ }
193
+ catch (err) {
194
+ this.opts.log(`[poll-backstop] list_messages failed (non-fatal): ${err?.message ?? String(err)}`);
195
+ return;
196
+ }
197
+ const body = parseListMessagesResult(raw);
198
+ if (!body || !Array.isArray(body.messages)) {
199
+ this.opts.log(`[poll-backstop] unexpected list_messages result shape; skipping tick`);
200
+ return;
201
+ }
202
+ if (body.messages.length === 0) {
203
+ // No delta — keep the existing cursor (no need to rewrite the
204
+ // file with an unchanged value).
205
+ return;
206
+ }
207
+ // Surface each delta Message through the host hook. Mirrors the
208
+ // SSE `message_arrived` envelope shape so the MessageRouter +
209
+ // host hooks don't need a separate code path.
210
+ let maxId = since ?? "";
211
+ for (const message of body.messages) {
212
+ if (!message || typeof message.id !== "string")
213
+ continue;
214
+ if (message.id > maxId)
215
+ maxId = message.id;
216
+ const event = {
217
+ event: "message_arrived",
218
+ data: { message },
219
+ // The SSE envelope id is the W1b Last-Event-ID surface; for
220
+ // poll-sourced events we use the Message ID itself so the
221
+ // MessageRouter's seen-id LRU dedup catches push+poll race.
222
+ id: message.id,
223
+ };
224
+ try {
225
+ this.opts.onPolledMessage(event);
226
+ }
227
+ catch (err) {
228
+ this.opts.log(`[poll-backstop] onPolledMessage handler threw (non-fatal): ${err?.message ?? String(err)}`);
229
+ }
230
+ }
231
+ // Persist the cursor advance only if we observed strictly newer IDs
232
+ // (defensive: if some weirdness made every returned id <= since,
233
+ // don't regress the cursor).
234
+ if (since === undefined || maxId > since) {
235
+ writeCursor(cursorFile, maxId, this.opts.log);
236
+ }
237
+ }
238
+ finally {
239
+ this.inFlight = false;
240
+ }
241
+ }
242
+ }
243
+ //# sourceMappingURL=poll-backstop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poll-backstop.js","sourceRoot":"","sources":["../../src/kernel/poll-backstop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * session-claim.ts — explicit session-claim warmup helpers.
3
+ *
4
+ * Pure helpers for the eager-claim path: when a real adapter session
5
+ * (vs probe spawn) starts, it sets `OIS_EAGER_SESSION_CLAIM=1` to
6
+ * declare intent to claim a Hub session synchronously rather than
7
+ * waiting for the lazy auto-claim path.
8
+ *
9
+ * - isEagerWarmupEnabled(env): tests the env hint.
10
+ * - parseClaimSessionResponse(wrapper): defensively unwraps the MCP
11
+ * tool-call response for `claim_session`.
12
+ * - formatSessionClaimedLogLine(parsed): structured-parseable
13
+ * [Handshake] log line for diagnostic tooling.
14
+ *
15
+ * Renamed from `eager-claim.ts` in mission-55 cleanup — the module
16
+ * owns claim-session helpers regardless of eager-mode usage; the
17
+ * old name implied an eager-only scope that no longer fits.
18
+ */
19
+ /**
20
+ * Surface fields the adapter consumes from `claim_session` response.
21
+ * mission-63 W3: flattened from the canonical wire envelope per Design
22
+ * v1.0 §3.2 + ADR-028 (`{ok, agent: {id, ...}, session: {epoch, claimed,
23
+ * trigger, displacedPriorSession?}, message?}`). Adapter doesn't carry
24
+ * the full canonical nested shape — flatten at parse-time.
25
+ */
26
+ export interface ClaimSessionParsed {
27
+ agentId?: string;
28
+ sessionEpoch?: number;
29
+ sessionClaimed?: boolean;
30
+ displacedPriorSession?: {
31
+ sessionId: string;
32
+ epoch: number;
33
+ };
34
+ }
35
+ /**
36
+ * True iff `OIS_EAGER_SESSION_CLAIM` is set to the literal string
37
+ * `"1"`. Any other value (unset, "0", "true", whitespace, etc.) is
38
+ * lazy-mode. Strict on purpose — a typo doesn't accidentally land on
39
+ * eager mode.
40
+ */
41
+ export declare function isEagerWarmupEnabled(env: NodeJS.ProcessEnv | Record<string, string | undefined>): boolean;
42
+ /**
43
+ * Defensively parse the `claim_session` MCP tool-call response.
44
+ * Handles the three wrapper shapes seen in the wild:
45
+ * - string (JSON-encoded payload)
46
+ * - { content: [{ text: JSON_STRING }] } (canonical MCP result)
47
+ * - already-parsed object
48
+ *
49
+ * mission-63 W3: reads canonical envelope per Design §3.2 — `body.agent.id`
50
+ * + `body.session.{epoch, claimed, trigger, displacedPriorSession?}`.
51
+ * Flattens the nested shape into ClaimSessionParsed for adapter consumers
52
+ * that don't need the full canonical nesting. Returns an empty object on
53
+ * any parse failure (callers fall back to "unknown" / "none" when emitting
54
+ * the [Handshake] log line).
55
+ */
56
+ export declare function parseClaimSessionResponse(wrapper: unknown): ClaimSessionParsed;
57
+ /**
58
+ * Format the `[Handshake] Session claimed` log line in
59
+ * structured-parseable form for dashboard / diagnostic tooling.
60
+ *
61
+ * `[Handshake] Session claimed: epoch=<N> (displaced prior: <session-id|none>)`
62
+ *
63
+ * Used in eager mode after `claim_session` returns. Lazy mode does
64
+ * not log this line — the Hub-side auto-claim happens server-side
65
+ * and the adapter has no synchronous response to format.
66
+ */
67
+ export declare function formatSessionClaimedLogLine(parsed: ClaimSessionParsed): string;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * session-claim.ts — explicit session-claim warmup helpers.
3
+ *
4
+ * Pure helpers for the eager-claim path: when a real adapter session
5
+ * (vs probe spawn) starts, it sets `OIS_EAGER_SESSION_CLAIM=1` to
6
+ * declare intent to claim a Hub session synchronously rather than
7
+ * waiting for the lazy auto-claim path.
8
+ *
9
+ * - isEagerWarmupEnabled(env): tests the env hint.
10
+ * - parseClaimSessionResponse(wrapper): defensively unwraps the MCP
11
+ * tool-call response for `claim_session`.
12
+ * - formatSessionClaimedLogLine(parsed): structured-parseable
13
+ * [Handshake] log line for diagnostic tooling.
14
+ *
15
+ * Renamed from `eager-claim.ts` in mission-55 cleanup — the module
16
+ * owns claim-session helpers regardless of eager-mode usage; the
17
+ * old name implied an eager-only scope that no longer fits.
18
+ */
19
+ /**
20
+ * True iff `OIS_EAGER_SESSION_CLAIM` is set to the literal string
21
+ * `"1"`. Any other value (unset, "0", "true", whitespace, etc.) is
22
+ * lazy-mode. Strict on purpose — a typo doesn't accidentally land on
23
+ * eager mode.
24
+ */
25
+ export function isEagerWarmupEnabled(env) {
26
+ return env.OIS_EAGER_SESSION_CLAIM === "1";
27
+ }
28
+ /**
29
+ * Defensively parse the `claim_session` MCP tool-call response.
30
+ * Handles the three wrapper shapes seen in the wild:
31
+ * - string (JSON-encoded payload)
32
+ * - { content: [{ text: JSON_STRING }] } (canonical MCP result)
33
+ * - already-parsed object
34
+ *
35
+ * mission-63 W3: reads canonical envelope per Design §3.2 — `body.agent.id`
36
+ * + `body.session.{epoch, claimed, trigger, displacedPriorSession?}`.
37
+ * Flattens the nested shape into ClaimSessionParsed for adapter consumers
38
+ * that don't need the full canonical nesting. Returns an empty object on
39
+ * any parse failure (callers fall back to "unknown" / "none" when emitting
40
+ * the [Handshake] log line).
41
+ */
42
+ export function parseClaimSessionResponse(wrapper) {
43
+ if (wrapper === null || wrapper === undefined)
44
+ return {};
45
+ try {
46
+ let body;
47
+ if (typeof wrapper === "string") {
48
+ body = JSON.parse(wrapper);
49
+ }
50
+ else if (typeof wrapper === "object") {
51
+ const w = wrapper;
52
+ if (Array.isArray(w.content) &&
53
+ w.content[0]?.text &&
54
+ typeof w.content[0].text === "string") {
55
+ body = JSON.parse(w.content[0].text);
56
+ }
57
+ else {
58
+ body = wrapper;
59
+ }
60
+ }
61
+ else {
62
+ return {};
63
+ }
64
+ if (typeof body !== "object" || body === null)
65
+ return {};
66
+ const b = body;
67
+ const agent = b.agent;
68
+ const session = b.session;
69
+ const out = {};
70
+ if (agent && typeof agent.id === "string")
71
+ out.agentId = agent.id;
72
+ if (session) {
73
+ if (typeof session.epoch === "number")
74
+ out.sessionEpoch = session.epoch;
75
+ if (typeof session.claimed === "boolean")
76
+ out.sessionClaimed = session.claimed;
77
+ const dps = session.displacedPriorSession;
78
+ if (dps &&
79
+ typeof dps.sessionId === "string" &&
80
+ typeof dps.epoch === "number") {
81
+ out.displacedPriorSession = { sessionId: dps.sessionId, epoch: dps.epoch };
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+ catch {
87
+ /* fall through to empty */
88
+ }
89
+ return {};
90
+ }
91
+ /**
92
+ * Format the `[Handshake] Session claimed` log line in
93
+ * structured-parseable form for dashboard / diagnostic tooling.
94
+ *
95
+ * `[Handshake] Session claimed: epoch=<N> (displaced prior: <session-id|none>)`
96
+ *
97
+ * Used in eager mode after `claim_session` returns. Lazy mode does
98
+ * not log this line — the Hub-side auto-claim happens server-side
99
+ * and the adapter has no synchronous response to format.
100
+ */
101
+ export function formatSessionClaimedLogLine(parsed) {
102
+ const epoch = parsed.sessionEpoch ?? "unknown";
103
+ const displacedPrior = parsed.displacedPriorSession?.sessionId ?? "none";
104
+ return `[Handshake] Session claimed: epoch=${epoch} (displaced prior: ${displacedPrior})`;
105
+ }
106
+ //# sourceMappingURL=session-claim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-claim.js","sourceRoot":"","sources":["../../src/kernel/session-claim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgBH;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAA2D;IAE3D,OAAO,GAAG,CAAC,uBAAuB,KAAK,GAAG,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAgB;IACxD,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACzD,IAAI,CAAC;QACH,IAAI,IAAa,CAAC;QAClB,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;aAAM,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,OAAiD,CAAC;YAC5D,IACE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;gBACxB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI;gBAClB,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,EACrC,CAAC;gBACD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACvC,CAAC;iBAAM,CAAC;gBACN,IAAI,GAAG,OAAO,CAAC;YACjB,CAAC;QACH,CAAC;aAAM,CAAC;YACN,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,EAAE,CAAC;QACzD,MAAM,CAAC,GAAG,IAA+B,CAAC;QAC1C,MAAM,KAAK,GAAG,CAAC,CAAC,KAA4C,CAAC;QAC7D,MAAM,OAAO,GAAG,CAAC,CAAC,OAA8C,CAAC;QACjE,MAAM,GAAG,GAAuB,EAAE,CAAC;QACnC,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ;YAAE,GAAG,CAAC,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC;QAClE,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ;gBAAE,GAAG,CAAC,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC;YACxE,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,SAAS;gBAAE,GAAG,CAAC,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;YAC/E,MAAM,GAAG,GAAG,OAAO,CAAC,qBAEP,CAAC;YACd,IACE,GAAG;gBACH,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ;gBACjC,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,EAC7B,CAAC;gBACD,GAAG,CAAC,qBAAqB,GAAG,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC;YAC7E,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAAC,MAAM,CAAC;QACP,2BAA2B;IAC7B,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,2BAA2B,CACzC,MAA0B;IAE1B,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,IAAI,SAAS,CAAC;IAC/C,MAAM,cAAc,GAAG,MAAM,CAAC,qBAAqB,EAAE,SAAS,IAAI,MAAM,CAAC;IACzE,OAAO,sCAAsC,KAAK,sBAAsB,cAAc,GAAG,CAAC;AAC5F,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * State sync — called on entry to the `synchronizing` phase.
3
+ *
4
+ * Runs `get_task` + `get_pending_actions` in parallel, then calls
5
+ * `completeSync()` to transition to `streaming` and flush buffered events.
6
+ *
7
+ * The enriched handshake is NOT called here — `McpAgentClient.runHandshake`
8
+ * invokes it before this function runs, so `state-sync.ts` can assume the
9
+ * engineer has its canonical agentId by the time it queries pending state.
10
+ */
11
+ import type { ILogger, LegacyStringLogger } from "../logger.js";
12
+ /**
13
+ * A PendingActionItem as returned by `drain_pending_actions` (ADR-017).
14
+ * Adapter-facing shape — subset of the Hub's canonical type that's
15
+ * relevant for consumption. `id` is the queue item's surrogate ID which
16
+ * MUST be passed back as `sourceQueueItemId` on the settling tool call
17
+ * (e.g., create_thread_reply) for completion-ACK.
18
+ */
19
+ export interface DrainedPendingAction {
20
+ id: string;
21
+ dispatchType: string;
22
+ entityRef: string;
23
+ payload: Record<string, unknown>;
24
+ }
25
+ export interface StateSyncContext {
26
+ executeTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
27
+ completeSync: () => void;
28
+ /** Structured logger. A legacy `(msg: string) => void` is auto-bridged. */
29
+ log: ILogger | LegacyStringLogger;
30
+ /** Optional hook for per-engineer logging of pending directives. */
31
+ onPendingTask?: (task: Record<string, unknown>) => void;
32
+ /**
33
+ * ADR-017 drain-on-wake. Called once per item returned from
34
+ * `drain_pending_actions`. The adapter is responsible for:
35
+ * 1. Processing the item (LLM reasoning, user surface, etc.)
36
+ * 2. Threading `item.id` as `sourceQueueItemId` when it issues the
37
+ * settling tool call (create_thread_reply, create_review, …)
38
+ * Missing this hook means the queue items drain without consumer —
39
+ * the Hub's watchdog will eventually escalate to Director.
40
+ */
41
+ onPendingActionItem?: (item: DrainedPendingAction) => void;
42
+ }
43
+ export declare function performStateSync(ctx: StateSyncContext): Promise<void>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * State sync — called on entry to the `synchronizing` phase.
3
+ *
4
+ * Runs `get_task` + `get_pending_actions` in parallel, then calls
5
+ * `completeSync()` to transition to `streaming` and flush buffered events.
6
+ *
7
+ * The enriched handshake is NOT called here — `McpAgentClient.runHandshake`
8
+ * invokes it before this function runs, so `state-sync.ts` can assume the
9
+ * engineer has its canonical agentId by the time it queries pending state.
10
+ */
11
+ import { normalizeToILogger } from "../logger.js";
12
+ export async function performStateSync(ctx) {
13
+ const log = normalizeToILogger(ctx.log, "StateSync");
14
+ log.log("agent.sync.start", undefined, "[StateSync] Starting state sync...");
15
+ try {
16
+ // ADR-017 additive: drain_pending_actions runs alongside the legacy
17
+ // surface. Hubs that don't expose the tool yet (pre-ADR-017) return
18
+ // an error here which we swallow — the other two calls still land.
19
+ const [directive, pendingActions, drainedRaw] = await Promise.all([
20
+ ctx.executeTool("get_task", {}).catch((err) => {
21
+ log.log("agent.sync.get_task.failed", { error: String(err) }, `[StateSync] get_task: ${err}`);
22
+ return null;
23
+ }),
24
+ ctx.executeTool("get_pending_actions", {}).catch((err) => {
25
+ log.log("agent.sync.get_pending_actions.failed", { error: String(err) }, `[StateSync] get_pending_actions: ${err}`);
26
+ return null;
27
+ }),
28
+ ctx.executeTool("drain_pending_actions", {}).catch((err) => {
29
+ log.log("agent.sync.drain_pending_actions.failed", { error: String(err) }, `[StateSync] drain_pending_actions: ${err}`);
30
+ return null;
31
+ }),
32
+ ]);
33
+ if (directive && typeof directive === "object") {
34
+ const d = directive;
35
+ if (d.task && typeof d.task === "object") {
36
+ const task = d.task;
37
+ log.log("agent.sync.pending_task", { taskId: String(task.taskId ?? "unknown") }, `[StateSync] Pending task: ${task.taskId ?? "unknown"}`);
38
+ if (ctx.onPendingTask)
39
+ ctx.onPendingTask(task);
40
+ }
41
+ }
42
+ if (pendingActions && typeof pendingActions === "object") {
43
+ const pa = pendingActions;
44
+ log.log("agent.sync.pending_actions", { totalPending: Number(pa.totalPending ?? 0) }, `[StateSync] Pending actions: ${pa.totalPending ?? 0}`);
45
+ }
46
+ // ADR-017: dispatch drained queue items to the adapter's handler.
47
+ // Shape: { items: PendingActionItem[] }. Tool returns isError=true
48
+ // when no agent is bound to the session — the adapter catch above
49
+ // already swallowed; here we just ensure the items array is safe.
50
+ if (drainedRaw && typeof drainedRaw === "object") {
51
+ const d = drainedRaw;
52
+ const items = Array.isArray(d.items) ? d.items : [];
53
+ if (items.length > 0) {
54
+ log.log("agent.sync.drained_items", { count: items.length }, `[StateSync] Drained ${items.length} pending action item(s)`);
55
+ }
56
+ if (ctx.onPendingActionItem) {
57
+ for (const raw of items) {
58
+ if (!raw || typeof raw !== "object")
59
+ continue;
60
+ const item = raw;
61
+ if (typeof item.id !== "string")
62
+ continue;
63
+ ctx.onPendingActionItem({
64
+ id: item.id,
65
+ dispatchType: String(item.dispatchType ?? ""),
66
+ entityRef: String(item.entityRef ?? ""),
67
+ payload: item.payload ?? {},
68
+ });
69
+ }
70
+ }
71
+ }
72
+ ctx.completeSync();
73
+ log.log("agent.sync.complete", undefined, "[StateSync] Sync complete — now streaming");
74
+ }
75
+ catch (err) {
76
+ log.log("agent.sync.failed", { error: String(err) }, `[StateSync] Failed: ${err}`);
77
+ try {
78
+ ctx.completeSync();
79
+ }
80
+ catch {
81
+ log.log("agent.sync.complete_failed", undefined, "[StateSync] completeSync() also failed");
82
+ }
83
+ }
84
+ }
85
+ //# sourceMappingURL=state-sync.js.map