@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.
- package/dist/hub-error.d.ts +42 -0
- package/dist/hub-error.js +74 -0
- package/dist/hub-error.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/agent-client.d.ts +192 -0
- package/dist/kernel/agent-client.js +44 -0
- package/dist/kernel/agent-client.js.map +1 -0
- package/dist/kernel/event-router.d.ts +49 -0
- package/dist/kernel/event-router.js +150 -0
- package/dist/kernel/event-router.js.map +1 -0
- package/dist/kernel/handshake.d.ts +161 -0
- package/dist/kernel/handshake.js +257 -0
- package/dist/kernel/handshake.js.map +1 -0
- package/dist/kernel/instance.d.ts +40 -0
- package/dist/kernel/instance.js +79 -0
- package/dist/kernel/instance.js.map +1 -0
- package/dist/kernel/mcp-agent-client.d.ts +139 -0
- package/dist/kernel/mcp-agent-client.js +505 -0
- package/dist/kernel/mcp-agent-client.js.map +1 -0
- package/dist/kernel/poll-backstop.d.ts +108 -0
- package/dist/kernel/poll-backstop.js +243 -0
- package/dist/kernel/poll-backstop.js.map +1 -0
- package/dist/kernel/session-claim.d.ts +67 -0
- package/dist/kernel/session-claim.js +106 -0
- package/dist/kernel/session-claim.js.map +1 -0
- package/dist/kernel/state-sync.d.ts +43 -0
- package/dist/kernel/state-sync.js +85 -0
- package/dist/kernel/state-sync.js.map +1 -0
- package/dist/logger.d.ts +82 -0
- package/dist/logger.js +114 -0
- package/dist/logger.js.map +1 -0
- package/dist/notification-log.d.ts +29 -0
- package/dist/notification-log.js +66 -0
- package/dist/notification-log.js.map +1 -0
- package/dist/prompt-format.d.ts +38 -0
- package/dist/prompt-format.js +220 -0
- package/dist/prompt-format.js.map +1 -0
- package/dist/tool-manager/dispatcher.d.ts +180 -0
- package/dist/tool-manager/dispatcher.js +379 -0
- package/dist/tool-manager/dispatcher.js.map +1 -0
- package/dist/tool-manager/tool-catalog-cache.d.ts +85 -0
- package/dist/tool-manager/tool-catalog-cache.js +137 -0
- package/dist/tool-manager/tool-catalog-cache.js.map +1 -0
- package/dist/wire/mcp-transport.d.ts +120 -0
- package/dist/wire/mcp-transport.js +447 -0
- package/dist/wire/mcp-transport.js.map +1 -0
- package/dist/wire/transport.d.ts +174 -0
- package/dist/wire/transport.js +43 -0
- package/dist/wire/transport.js.map +1 -0
- 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
|