@fml-inc/fml 0.7.0 → 0.8.0-rc.1

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 (45) hide show
  1. package/.claude-plugin/plugin.json +2 -4
  2. package/README.md +312 -79
  3. package/agent-assets/fml/SKILL.md +180 -0
  4. package/bin/fml +4 -18
  5. package/bin/hook-handler +7 -0
  6. package/bin/mcp-server +4 -18
  7. package/bin/observer +6 -0
  8. package/dist/chunk-7DR5X7XI.js +1522 -0
  9. package/dist/chunk-CK5MEL5J.js +140 -0
  10. package/dist/chunk-DEMVOVVY.js +4442 -0
  11. package/dist/chunk-F66IBM3Z.js +1037 -0
  12. package/dist/chunk-HUR2HCUG.js +953 -0
  13. package/dist/chunk-I4GZPTBM.js +109 -0
  14. package/dist/chunk-JXSSPQ5B.js +3331 -0
  15. package/dist/{chunk-PZ5AY32C.js → chunk-MLKGABMK.js} +0 -1
  16. package/dist/{chunk-PTUZMKSN.js → chunk-P3QLARXE.js} +34 -23
  17. package/dist/chunk-T5YVLSUC.js +138 -0
  18. package/dist/cli.js +7523 -1005
  19. package/dist/{esm-2EUDFZWP.js → esm-4YKLKFRX.js} +546 -741
  20. package/dist/hook-handler.js +190 -0
  21. package/dist/mcp/server.js +112 -98
  22. package/dist/observer-server.js +1566 -0
  23. package/dist/sync-cycle.js +31 -0
  24. package/dist/{token-store-3IQSLOTQ.js → token-store-R7WXEPLI.js} +10 -3
  25. package/hooks/hooks.json +290 -0
  26. package/package.json +24 -27
  27. package/dist/chunk-PTUZMKSN.js.map +0 -1
  28. package/dist/chunk-PZ5AY32C.js.map +0 -1
  29. package/dist/chunk-S4TJNLEN.js +0 -61
  30. package/dist/chunk-S4TJNLEN.js.map +0 -1
  31. package/dist/chunk-VV4JHH4H.js +0 -437
  32. package/dist/chunk-VV4JHH4H.js.map +0 -1
  33. package/dist/chunk-YH5ROR7G.js +0 -69
  34. package/dist/chunk-YH5ROR7G.js.map +0 -1
  35. package/dist/cli.js.map +0 -1
  36. package/dist/daemon-MSLDHLMH.js +0 -21
  37. package/dist/daemon-MSLDHLMH.js.map +0 -1
  38. package/dist/daemon-utils-OODBIYB7.js +0 -10
  39. package/dist/daemon-utils-OODBIYB7.js.map +0 -1
  40. package/dist/esm-2EUDFZWP.js.map +0 -1
  41. package/dist/mcp/server.js.map +0 -1
  42. package/dist/postinstall.js +0 -16
  43. package/dist/postinstall.js.map +0 -1
  44. package/dist/token-store-3IQSLOTQ.js.map +0 -1
  45. package/scripts/postinstall.cjs +0 -25
@@ -0,0 +1,4442 @@
1
+ import {
2
+ FML_HOOK_INBOX_DIR
3
+ } from "./chunk-7DR5X7XI.js";
4
+
5
+ // ../../packages/panopticon/src/observer/auth.ts
6
+ import crypto from "crypto";
7
+ import fs from "fs";
8
+ import path from "path";
9
+ function readObserverAuthToken(tokenFile) {
10
+ try {
11
+ const token = fs.readFileSync(tokenFile, "utf8").trim();
12
+ return token.length > 0 ? token : null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function getOrCreateObserverAuthToken(tokenFile) {
18
+ const existing = readObserverAuthToken(tokenFile);
19
+ if (existing) return existing;
20
+ fs.mkdirSync(path.dirname(tokenFile), { recursive: true, mode: 448 });
21
+ const token = crypto.randomBytes(32).toString("hex");
22
+ try {
23
+ fs.writeFileSync(tokenFile, `${token}
24
+ `, { flag: "wx", mode: 384 });
25
+ return token;
26
+ } catch (error) {
27
+ if (error.code === "EEXIST") {
28
+ const raced = readObserverAuthToken(tokenFile);
29
+ if (raced) return raced;
30
+ try {
31
+ if (fs.readFileSync(tokenFile, "utf8").trim().length === 0) {
32
+ fs.rmSync(tokenFile);
33
+ return getOrCreateObserverAuthToken(tokenFile);
34
+ }
35
+ } catch (repairError) {
36
+ if (repairError.code === "ENOENT") {
37
+ return getOrCreateObserverAuthToken(tokenFile);
38
+ }
39
+ }
40
+ }
41
+ throw error;
42
+ }
43
+ }
44
+ function isObserverAuthToken(presented, expected) {
45
+ const leftBytes = Buffer.from(presented, "utf8");
46
+ const rightBytes = Buffer.from(expected, "utf8");
47
+ return leftBytes.length === rightBytes.length && crypto.timingSafeEqual(leftBytes, rightBytes);
48
+ }
49
+ function hasObserverBearerToken(req, expected) {
50
+ const authorization = req.headers.authorization;
51
+ if (typeof authorization !== "string" || !authorization.startsWith("Bearer ")) {
52
+ return false;
53
+ }
54
+ return isObserverAuthToken(
55
+ authorization.slice("Bearer ".length).trim(),
56
+ expected
57
+ );
58
+ }
59
+ function requireObserverBearerToken(req, res, expected) {
60
+ if (hasObserverBearerToken(req, expected)) return true;
61
+ res.writeHead(401, { "Content-Type": "application/json" });
62
+ res.end(JSON.stringify({ error: "unauthorized" }));
63
+ return false;
64
+ }
65
+
66
+ // ../../packages/agent-targets/src/hooks/normalize.ts
67
+ import { createHash } from "crypto";
68
+
69
+ // ../../packages/agent-targets/src/hooks/types.ts
70
+ var HOOK_TARGETS = Object.freeze([
71
+ "claude",
72
+ "codex",
73
+ "gemini",
74
+ "grok"
75
+ ]);
76
+ function isHookTarget(value) {
77
+ return typeof value === "string" && HOOK_TARGETS.some((targetId) => targetId === value);
78
+ }
79
+
80
+ // ../../packages/agent-targets/src/catalog/hook-events.json
81
+ var hook_events_default = {
82
+ claude: [
83
+ "SessionStart",
84
+ "Setup",
85
+ "InstructionsLoaded",
86
+ "UserPromptSubmit",
87
+ "UserPromptExpansion",
88
+ "PreToolUse",
89
+ "PermissionRequest",
90
+ "PostToolUse",
91
+ "PostToolUseFailure",
92
+ "PostToolBatch",
93
+ "PermissionDenied",
94
+ "Notification",
95
+ "SubagentStart",
96
+ "SubagentStop",
97
+ "TaskCreated",
98
+ "TaskCompleted",
99
+ "Stop",
100
+ "StopFailure",
101
+ "TeammateIdle",
102
+ "ConfigChange",
103
+ "CwdChanged",
104
+ "PreCompact",
105
+ "PostCompact",
106
+ "SessionEnd",
107
+ "Elicitation",
108
+ "ElicitationResult"
109
+ ],
110
+ codex: [
111
+ "SessionStart",
112
+ "UserPromptSubmit",
113
+ "PreToolUse",
114
+ "PermissionRequest",
115
+ "PostToolUse",
116
+ "Stop"
117
+ ],
118
+ gemini: [
119
+ "SessionStart",
120
+ "BeforeAgent",
121
+ "BeforeTool",
122
+ "AfterTool",
123
+ "AfterAgent"
124
+ ],
125
+ grok: [
126
+ "SessionStart",
127
+ "UserPromptSubmit",
128
+ "PreToolUse",
129
+ "PostToolUse",
130
+ "PostToolUseFailure",
131
+ "PermissionDenied",
132
+ "Stop",
133
+ "StopFailure",
134
+ "Notification",
135
+ "SubagentStart",
136
+ "SubagentStop",
137
+ "PreCompact",
138
+ "PostCompact",
139
+ "SessionEnd"
140
+ ]
141
+ };
142
+
143
+ // ../../packages/agent-targets/src/catalog/hook-events.ts
144
+ var CLAUDE_HOOK_EVENTS = Object.freeze([
145
+ ...hook_events_default.claude
146
+ ]);
147
+ var CODEX_HOOK_EVENTS = Object.freeze([
148
+ ...hook_events_default.codex
149
+ ]);
150
+ var GEMINI_HOOK_EVENTS = Object.freeze([
151
+ ...hook_events_default.gemini
152
+ ]);
153
+ var GROK_HOOK_EVENTS = Object.freeze([
154
+ ...hook_events_default.grok
155
+ ]);
156
+
157
+ // ../../packages/agent-targets/src/hooks/normalize.ts
158
+ var MAX_EXTRACTED_TEXT_BYTES = 64 * 1024;
159
+ function isRecord(value) {
160
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
161
+ }
162
+ function firstString(record, keys) {
163
+ for (const key of keys) {
164
+ const value = record[key];
165
+ if (typeof value === "string" && value.length > 0) return value;
166
+ }
167
+ return void 0;
168
+ }
169
+ function extractedText(value) {
170
+ if (typeof value === "string") return value;
171
+ if (value === void 0 || value === null) return void 0;
172
+ try {
173
+ return JSON.stringify(value);
174
+ } catch {
175
+ return void 0;
176
+ }
177
+ }
178
+ function firstText(record, keys) {
179
+ for (const key of keys) {
180
+ const value = extractedText(record[key]);
181
+ if (value !== void 0 && value.length > 0) return value;
182
+ }
183
+ return void 0;
184
+ }
185
+ function utf8Prefix(value, maxBytes) {
186
+ let low = 0;
187
+ let high = value.length;
188
+ while (low < high) {
189
+ const middle = Math.ceil((low + high) / 2);
190
+ if (Buffer.byteLength(value.slice(0, middle), "utf8") <= maxBytes) {
191
+ low = middle;
192
+ } else {
193
+ high = middle - 1;
194
+ }
195
+ }
196
+ let end = low;
197
+ if (end > 0 && end < value.length && value.charCodeAt(end - 1) >= 55296 && value.charCodeAt(end - 1) <= 56319 && value.charCodeAt(end) >= 56320 && value.charCodeAt(end) <= 57343) {
198
+ end -= 1;
199
+ }
200
+ return value.slice(0, end);
201
+ }
202
+ function boundedExtractedText(value, field) {
203
+ if (value === void 0) return void 0;
204
+ const originalBytes = Buffer.byteLength(value, "utf8");
205
+ if (originalBytes <= MAX_EXTRACTED_TEXT_BYTES) return value;
206
+ const digest = createHash("sha256").update(value).digest("hex");
207
+ const marker = `
208
+ [fml:${field}:truncated originalBytes=${originalBytes} sha256=${digest}]`;
209
+ return `${utf8Prefix(
210
+ value,
211
+ MAX_EXTRACTED_TEXT_BYTES - Buffer.byteLength(marker, "utf8")
212
+ )}${marker}`;
213
+ }
214
+ function nestedRecord(record, keys) {
215
+ for (const key of keys) {
216
+ const value = record[key];
217
+ if (isRecord(value)) return value;
218
+ }
219
+ return void 0;
220
+ }
221
+ function timestampMs(payload, fallback) {
222
+ for (const key of ["timestamp_ms", "timestampMs", "timestamp"]) {
223
+ const value = payload[key];
224
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
225
+ const milliseconds = Math.round(
226
+ value < 1e10 ? value * 1e3 : value
227
+ );
228
+ if (Number.isSafeInteger(milliseconds) && milliseconds > 0) {
229
+ return milliseconds;
230
+ }
231
+ }
232
+ if (typeof value === "string") {
233
+ const parsed = Date.parse(value);
234
+ if (Number.isSafeInteger(parsed) && parsed >= 0) return parsed;
235
+ }
236
+ }
237
+ return fallback;
238
+ }
239
+ function geminiEventType(eventType) {
240
+ switch (eventType) {
241
+ case "BeforeAgent":
242
+ case "BeforeModel":
243
+ return "UserPromptSubmit";
244
+ case "BeforeTool":
245
+ return "PreToolUse";
246
+ case "AfterTool":
247
+ return "PostToolUse";
248
+ case "AfterAgent":
249
+ return "Stop";
250
+ default:
251
+ return eventType;
252
+ }
253
+ }
254
+ var GROK_EVENT_TYPES = Object.freeze({
255
+ session_start: "SessionStart",
256
+ user_prompt_submit: "UserPromptSubmit",
257
+ pre_tool_use: "PreToolUse",
258
+ post_tool_use: "PostToolUse",
259
+ post_tool_use_failure: "PostToolUseFailure",
260
+ permission_denied: "PermissionDenied",
261
+ stop: "Stop",
262
+ stop_failure: "StopFailure",
263
+ notification: "Notification",
264
+ subagent_start: "SubagentStart",
265
+ subagent_stop: "SubagentStop",
266
+ pre_compact: "PreCompact",
267
+ post_compact: "PostCompact",
268
+ session_end: "SessionEnd"
269
+ });
270
+ function grokEventType(eventType) {
271
+ return Object.hasOwn(GROK_EVENT_TYPES, eventType) ? GROK_EVENT_TYPES[eventType] ?? eventType : eventType;
272
+ }
273
+ function geminiUserPrompt(payload) {
274
+ const request = payload.llm_request;
275
+ if (!isRecord(request) || !Array.isArray(request.messages)) return void 0;
276
+ const userMessage = [...request.messages].reverse().find(
277
+ (message) => isRecord(message) && message.role === "user"
278
+ );
279
+ if (!userMessage) return void 0;
280
+ if (typeof userMessage.content === "string") {
281
+ return userMessage.content || void 0;
282
+ }
283
+ if (!Array.isArray(userMessage.content)) return void 0;
284
+ const text = userMessage.content.filter(
285
+ (part) => isRecord(part) && part.type === "text" && typeof part.text === "string"
286
+ ).map((part) => part.text).join("\n");
287
+ return text || void 0;
288
+ }
289
+ function normalizedEvent(target, payload, context) {
290
+ const sessionId = firstString(payload, [
291
+ "session_id",
292
+ "sessionId",
293
+ "conversation_id",
294
+ "conversationId"
295
+ ]);
296
+ const rawEventType = firstString(payload, [
297
+ "hook_event_name",
298
+ "hookEventName",
299
+ "event_name",
300
+ "eventName",
301
+ "event"
302
+ ]);
303
+ if (!sessionId || !rawEventType) return null;
304
+ const eventType = target === "gemini" ? geminiEventType(rawEventType) : target === "grok" ? grokEventType(rawEventType) : rawEventType;
305
+ const toolInput = nestedRecord(payload, ["tool_input", "toolInput"]);
306
+ const cwd = firstString(payload, [
307
+ "cwd",
308
+ "working_directory",
309
+ "workingDirectory",
310
+ "workspaceRoot"
311
+ ]) ?? context.cwd;
312
+ const filePath = firstString(payload, ["file_path", "filePath", "path"]) ?? (toolInput ? firstString(toolInput, ["file_path", "filePath", "path"]) : void 0);
313
+ const command = boundedExtractedText(
314
+ firstText(payload, ["command"]) ?? (toolInput ? firstText(toolInput, ["command", "cmd"]) : void 0),
315
+ "command"
316
+ );
317
+ const toolName = firstString(payload, ["tool_name", "toolName", "name"]);
318
+ const userPrompt = boundedExtractedText(
319
+ firstText(payload, ["prompt", "user_prompt", "userPrompt"]) ?? (target === "gemini" ? geminiUserPrompt(payload) : void 0),
320
+ "user_prompt"
321
+ );
322
+ const plan = boundedExtractedText(firstText(payload, ["plan"]), "plan");
323
+ const allowedPrompts = boundedExtractedText(
324
+ firstText(payload, ["allowed_prompts", "allowedPrompts"]),
325
+ "allowed_prompts"
326
+ );
327
+ const toolResult = boundedExtractedText(
328
+ firstText(payload, [
329
+ "tool_result",
330
+ "toolResult",
331
+ "tool_response",
332
+ "toolResponse"
333
+ ]),
334
+ "tool_result"
335
+ );
336
+ const nativeEventId = firstString(payload, [
337
+ "hook_id",
338
+ "hookId",
339
+ "event_id",
340
+ "eventId",
341
+ "tool_use_id",
342
+ "toolUseId",
343
+ "tool_call_id",
344
+ "toolCallId",
345
+ "prompt_id",
346
+ "promptId"
347
+ ]) ?? (eventType === "UserPromptSubmit" || eventType === "Stop" ? firstString(payload, ["turn_id", "turnId"]) : void 0);
348
+ return {
349
+ sessionId,
350
+ eventType,
351
+ timestampMs: timestampMs(payload, context.nowMs),
352
+ ...nativeEventId ? { nativeEventId } : {},
353
+ ...cwd ? { cwd } : {},
354
+ ...toolName ? { toolName } : {},
355
+ ...userPrompt ? { userPrompt } : {},
356
+ ...filePath ? { filePath } : {},
357
+ ...command ? { command } : {},
358
+ ...plan ? { plan } : {},
359
+ ...allowedPrompts ? { allowedPrompts } : {},
360
+ ...toolResult ? { toolResult } : {}
361
+ };
362
+ }
363
+ function capability(targetId, nativeEvents) {
364
+ return Object.freeze({
365
+ targetId,
366
+ nativeEvents,
367
+ normalize: (payload, context) => normalizedEvent(targetId, payload, context)
368
+ });
369
+ }
370
+ var HOOK_NORMALIZATION_CAPABILITIES = Object.freeze([
371
+ capability("claude", CLAUDE_HOOK_EVENTS),
372
+ capability("codex", CODEX_HOOK_EVENTS),
373
+ capability("gemini", GEMINI_HOOK_EVENTS),
374
+ capability("grok", GROK_HOOK_EVENTS)
375
+ ]);
376
+ function parseHookCaptureRequest(value) {
377
+ if (!isRecord(value) || !isHookTarget(value.target)) return null;
378
+ if (!isRecord(value.payload)) return null;
379
+ return {
380
+ target: value.target,
381
+ payload: value.payload,
382
+ ...typeof value.cwd === "string" && value.cwd.length > 0 ? { cwd: value.cwd } : {}
383
+ };
384
+ }
385
+ function normalizeTargetHookCapture(request, context) {
386
+ const capability2 = HOOK_NORMALIZATION_CAPABILITIES.find(
387
+ (candidate) => candidate.targetId === request.target
388
+ );
389
+ return capability2?.normalize(request.payload, {
390
+ ...context,
391
+ cwd: context.cwd ?? request.cwd
392
+ }) ?? null;
393
+ }
394
+
395
+ // ../../packages/panopticon/src/observer/capture.ts
396
+ import { createHash as createHash2, randomUUID } from "crypto";
397
+ function hookSyncId(request, sessionId, eventType, nativeEventId) {
398
+ const identity = nativeEventId ?? `delivery:${randomUUID()}`;
399
+ const digest = createHash2("sha256").update(`${request.target}\0${sessionId}\0${eventType}\0${identity}`).digest("hex");
400
+ return `hook|${request.target}|${digest}`;
401
+ }
402
+ function normalizeObserverHookCapture(request, options = {}) {
403
+ const now = (options.nowMs ?? Date.now)();
404
+ const normalized = normalizeTargetHookCapture(request, {
405
+ nowMs: now,
406
+ cwd: request.cwd
407
+ });
408
+ if (!normalized) return null;
409
+ return {
410
+ syncId: options.syncId?.() ?? hookSyncId(
411
+ request,
412
+ normalized.sessionId,
413
+ normalized.eventType,
414
+ normalized.nativeEventId
415
+ ),
416
+ sessionId: normalized.sessionId,
417
+ eventType: normalized.eventType,
418
+ timestampMs: normalized.timestampMs,
419
+ target: request.target,
420
+ payload: request.payload,
421
+ ...normalized.cwd ? { cwd: normalized.cwd } : {},
422
+ ...options.repository ? { repository: options.repository } : {},
423
+ ...normalized.toolName ? { toolName: normalized.toolName } : {},
424
+ ...normalized.userPrompt ? { userPrompt: normalized.userPrompt } : {},
425
+ ...normalized.filePath ? { filePath: normalized.filePath } : {},
426
+ ...normalized.command ? { command: normalized.command } : {},
427
+ ...normalized.plan ? { plan: normalized.plan } : {},
428
+ ...normalized.allowedPrompts ? { allowedPrompts: normalized.allowedPrompts } : {},
429
+ ...normalized.toolResult ? { toolResult: normalized.toolResult } : {},
430
+ capturedAtMs: now
431
+ };
432
+ }
433
+
434
+ // ../../packages/panopticon/src/observer/database.ts
435
+ import crypto2 from "crypto";
436
+ import fs2 from "fs";
437
+ import path2 from "path";
438
+ var ObserverHookSpoolEntryTooLargeError = class extends Error {
439
+ constructor(logicalBytes, maxBytes) {
440
+ super(
441
+ `Hook event requires ${logicalBytes} logical bytes, exceeding the configured ${maxBytes}-byte spool maximum`
442
+ );
443
+ this.logicalBytes = logicalBytes;
444
+ this.maxBytes = maxBytes;
445
+ this.name = "ObserverHookSpoolEntryTooLargeError";
446
+ }
447
+ };
448
+ var SCHEMA_VERSION = 10;
449
+ var MAX_SYNC_RUN_RECORDS = 1e4;
450
+ var DEFAULT_HOOK_OUTBOX_BATCH_SIZE = 100;
451
+ var MAX_HOOK_OUTBOX_BATCH_SIZE = 100;
452
+ var MAX_HOOK_PAYLOAD_BYTES = 16 * 1024 * 1024;
453
+ var MAX_HOOK_EXTRACTED_FIELD_BYTES = 16 * 1024 * 1024;
454
+ var MAX_HOOK_SYNC_ID_BYTES = 1024;
455
+ var MAX_HOOK_SESSION_ID_BYTES = 4096;
456
+ var MAX_HOOK_EVENT_TYPE_BYTES = 1024;
457
+ var MAX_HOOK_TARGET_BYTES = 256;
458
+ var MAX_HOOK_REJECTION_REASON_BYTES = 4 * 1024;
459
+ var MAX_HOOK_DESTINATION_URL_BYTES = 4 * 1024;
460
+ var MAX_HOOK_DESTINATION_IDENTITY_BYTES = 4 * 1024;
461
+ var DEFAULT_OTLP_OUTBOX_BATCH_SIZE = 100;
462
+ var MAX_OTLP_OUTBOX_BATCH_SIZE = 100;
463
+ var MAX_OTLP_ENVELOPE_BYTES = 16 * 1024 * 1024;
464
+ var DEFAULT_OTLP_SPOOL_MAX_BYTES = 1024 * 1024 * 1024;
465
+ var MAX_OTLP_ENVELOPE_ID_BYTES = 1024;
466
+ var MAX_OTLP_OBSERVER_INSTANCE_ID_BYTES = 4 * 1024;
467
+ var MAX_OTLP_OBSERVER_VERSION_BYTES = 4 * 1024;
468
+ var MAX_OTLP_DROP_REASON_BYTES = 4 * 1024;
469
+ var MAX_OTLP_DESTINATION_ORGANIZATION_ID_BYTES = 128;
470
+ var MAX_OTLP_DESTINATION_ORGANIZATION_SLUG_BYTES = 4 * 1024;
471
+ var OWNERLESS_OTLP_DROP_REASON = "ownerless";
472
+ var OTLP_CAPACITY_DROP_REASON = "spool_capacity";
473
+ var OTLP_ENTRY_TOO_LARGE_DROP_REASON = "spool_entry_too_large";
474
+ var OTLP_ROW_FIXED_LOGICAL_BYTES = 256;
475
+ var MAX_TIMESTAMP_MS = 864e13;
476
+ var QUARANTINE_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
477
+ var HOOK_OUTBOX_LOGICAL_BYTES_SQL = `
478
+ length(CAST(sync_id AS BLOB)) +
479
+ length(CAST(session_id AS BLOB)) +
480
+ length(CAST(event_type AS BLOB)) +
481
+ length(CAST(target AS BLOB)) +
482
+ length(CAST(payload_json AS BLOB)) +
483
+ length(CAST(COALESCE(cwd, '') AS BLOB)) +
484
+ length(CAST(COALESCE(repository, '') AS BLOB)) +
485
+ length(CAST(COALESCE(tool_name, '') AS BLOB)) +
486
+ length(CAST(COALESCE(user_prompt, '') AS BLOB)) +
487
+ length(CAST(COALESCE(file_path, '') AS BLOB)) +
488
+ length(CAST(COALESCE(command, '') AS BLOB)) +
489
+ length(CAST(COALESCE(plan, '') AS BLOB)) +
490
+ length(CAST(COALESCE(allowed_prompts, '') AS BLOB)) +
491
+ length(CAST(COALESCE(tool_result, '') AS BLOB)) +
492
+ length(CAST(COALESCE(destination_url, '') AS BLOB)) +
493
+ length(CAST(COALESCE(destination_identity, '') AS BLOB))
494
+ `;
495
+ var OTLP_LOGICAL_BYTES_SQL = `
496
+ ${OTLP_ROW_FIXED_LOGICAL_BYTES} +
497
+ length(CAST(envelope_id AS BLOB)) +
498
+ length(CAST(observer_instance_id AS BLOB)) +
499
+ length(CAST(observer_version AS BLOB)) +
500
+ length(CAST(signal AS BLOB)) +
501
+ length(CAST(content_type AS BLOB)) +
502
+ length(CAST(content_encoding AS BLOB)) +
503
+ length(body) +
504
+ length(CAST(sha256 AS BLOB)) +
505
+ length(CAST(COALESCE(destination_url, '') AS BLOB)) +
506
+ length(CAST(COALESCE(destination_identity, '') AS BLOB)) +
507
+ CASE
508
+ WHEN destination_url IS NOT NULL AND destination_organization_id IS NULL
509
+ THEN ${MAX_OTLP_DESTINATION_ORGANIZATION_ID_BYTES}
510
+ ELSE length(CAST(COALESCE(destination_organization_id, '') AS BLOB))
511
+ END +
512
+ CASE
513
+ WHEN destination_url IS NOT NULL AND destination_organization_slug IS NULL
514
+ THEN ${MAX_OTLP_DESTINATION_ORGANIZATION_SLUG_BYTES}
515
+ ELSE length(CAST(COALESCE(destination_organization_slug, '') AS BLOB))
516
+ END
517
+ `;
518
+ var RECONCILE_HOOK_SPOOL_STATE_SQL = `
519
+ INSERT INTO hook_spool_state (
520
+ singleton,
521
+ logical_bytes,
522
+ evicted_records,
523
+ evicted_bytes,
524
+ evicted_pending_records,
525
+ evicted_quarantined_records,
526
+ last_evicted_at_ms
527
+ )
528
+ SELECT
529
+ 1,
530
+ (SELECT COALESCE(SUM(logical_bytes), 0) FROM hook_event_outbox) +
531
+ (SELECT COALESCE(SUM(logical_bytes), 0) FROM hook_event_quarantine),
532
+ 0,
533
+ 0,
534
+ 0,
535
+ 0,
536
+ NULL
537
+ ON CONFLICT(singleton) DO UPDATE SET
538
+ logical_bytes = excluded.logical_bytes,
539
+ evicted_records = CASE
540
+ WHEN typeof(hook_spool_state.evicted_records) = 'integer'
541
+ AND hook_spool_state.evicted_records >= 0
542
+ AND hook_spool_state.evicted_records <= 9007199254740991
543
+ THEN hook_spool_state.evicted_records ELSE 0 END,
544
+ evicted_bytes = CASE
545
+ WHEN typeof(hook_spool_state.evicted_bytes) = 'integer'
546
+ AND hook_spool_state.evicted_bytes >= 0
547
+ AND hook_spool_state.evicted_bytes <= 9007199254740991
548
+ THEN hook_spool_state.evicted_bytes ELSE 0 END,
549
+ evicted_pending_records = CASE
550
+ WHEN typeof(hook_spool_state.evicted_pending_records) = 'integer'
551
+ AND hook_spool_state.evicted_pending_records >= 0
552
+ AND hook_spool_state.evicted_pending_records <= 9007199254740991
553
+ THEN hook_spool_state.evicted_pending_records ELSE 0 END,
554
+ evicted_quarantined_records = CASE
555
+ WHEN typeof(hook_spool_state.evicted_quarantined_records) = 'integer'
556
+ AND hook_spool_state.evicted_quarantined_records >= 0
557
+ AND hook_spool_state.evicted_quarantined_records <= 9007199254740991
558
+ THEN hook_spool_state.evicted_quarantined_records ELSE 0 END,
559
+ last_evicted_at_ms = CASE
560
+ WHEN hook_spool_state.last_evicted_at_ms IS NULL THEN NULL
561
+ WHEN typeof(hook_spool_state.last_evicted_at_ms) = 'integer'
562
+ AND hook_spool_state.last_evicted_at_ms >= 0
563
+ AND hook_spool_state.last_evicted_at_ms <= 8640000000000000
564
+ THEN hook_spool_state.last_evicted_at_ms ELSE NULL END
565
+ `;
566
+ var ObserverHookSpoolStateInvariantError = class extends Error {
567
+ };
568
+ function jsonStringify(value, description) {
569
+ let serialized;
570
+ try {
571
+ serialized = JSON.stringify(value);
572
+ } catch (error) {
573
+ throw new Error(`${description} must be JSON-serializable`, {
574
+ cause: error
575
+ });
576
+ }
577
+ if (serialized === void 0) {
578
+ throw new Error(`${description} must be JSON-serializable`);
579
+ }
580
+ return serialized;
581
+ }
582
+ function requireBoundedString(value, description, maxBytes) {
583
+ if (value.length === 0) {
584
+ throw new Error(`${description} must not be empty`);
585
+ }
586
+ if (Buffer.byteLength(value, "utf8") > maxBytes) {
587
+ throw new Error(`${description} exceeds ${maxBytes} bytes`);
588
+ }
589
+ }
590
+ function validateOptionalHookField(value, description) {
591
+ if (value !== void 0 && Buffer.byteLength(value, "utf8") > MAX_HOOK_EXTRACTED_FIELD_BYTES) {
592
+ throw new Error(
593
+ `${description} exceeds ${MAX_HOOK_EXTRACTED_FIELD_BYTES} bytes`
594
+ );
595
+ }
596
+ }
597
+ function requireTimestamp(value, description) {
598
+ if (!Number.isSafeInteger(value) || value < 0) {
599
+ throw new Error(`${description} must be a non-negative safe integer`);
600
+ }
601
+ }
602
+ function optionalString(value) {
603
+ return typeof value === "string" ? value : void 0;
604
+ }
605
+ function validateHookDestination(destination) {
606
+ requireBoundedString(
607
+ destination.targetUrl,
608
+ "Hook destination URL",
609
+ MAX_HOOK_DESTINATION_URL_BYTES
610
+ );
611
+ requireBoundedString(
612
+ destination.destinationIdentity,
613
+ "Hook destination identity",
614
+ MAX_HOOK_DESTINATION_IDENTITY_BYTES
615
+ );
616
+ }
617
+ function validateOtlpDestination(destination) {
618
+ validateHookDestination(destination);
619
+ if (destination.organizationId === void 0 !== (destination.organizationSlug === void 0)) {
620
+ throw new Error(
621
+ "OTLP destination organization ID and slug must be provided together"
622
+ );
623
+ }
624
+ if (destination.organizationId !== void 0) {
625
+ requireBoundedString(
626
+ destination.organizationId,
627
+ "OTLP destination organization ID",
628
+ MAX_OTLP_DESTINATION_ORGANIZATION_ID_BYTES
629
+ );
630
+ }
631
+ if (destination.organizationSlug !== void 0) {
632
+ requireBoundedString(
633
+ destination.organizationSlug,
634
+ "OTLP destination organization slug",
635
+ MAX_OTLP_DESTINATION_ORGANIZATION_SLUG_BYTES
636
+ );
637
+ }
638
+ }
639
+ function hookDestinationFromRow(row) {
640
+ const targetUrl = optionalString(row.destination_url);
641
+ const destinationIdentity = optionalString(row.destination_identity);
642
+ if (targetUrl === void 0 && destinationIdentity === void 0) {
643
+ return void 0;
644
+ }
645
+ if (targetUrl === void 0 || destinationIdentity === void 0) {
646
+ throw new Error(
647
+ "FML observer hook outbox has a partial destination binding"
648
+ );
649
+ }
650
+ return {
651
+ targetUrl,
652
+ destinationIdentity
653
+ };
654
+ }
655
+ function otlpDestinationFromRow(row) {
656
+ const destination = hookDestinationFromRow(row);
657
+ if (destination === void 0) return void 0;
658
+ const organizationId = optionalString(row.destination_organization_id);
659
+ const organizationSlug = optionalString(row.destination_organization_slug);
660
+ if (organizationId === void 0 !== (organizationSlug === void 0)) {
661
+ throw new Error(
662
+ "FML observer OTLP outbox has a partial organization binding"
663
+ );
664
+ }
665
+ return {
666
+ ...destination,
667
+ ...organizationId === void 0 ? {} : { organizationId },
668
+ ...organizationSlug === void 0 ? {} : { organizationSlug }
669
+ };
670
+ }
671
+ function hookOutboxRecordFromRow(row) {
672
+ const destination = hookDestinationFromRow(row);
673
+ return {
674
+ id: row.id,
675
+ syncId: row.sync_id,
676
+ sessionId: row.session_id,
677
+ eventType: row.event_type,
678
+ timestampMs: row.timestamp_ms,
679
+ target: row.target,
680
+ payload: JSON.parse(row.payload_json),
681
+ cwd: optionalString(row.cwd),
682
+ repository: optionalString(row.repository),
683
+ toolName: optionalString(row.tool_name),
684
+ userPrompt: optionalString(row.user_prompt),
685
+ filePath: optionalString(row.file_path),
686
+ command: optionalString(row.command),
687
+ plan: optionalString(row.plan),
688
+ allowedPrompts: optionalString(row.allowed_prompts),
689
+ toolResult: optionalString(row.tool_result),
690
+ capturedAtMs: row.captured_at_ms,
691
+ retryCount: row.retry_count,
692
+ nextAttemptAtMs: row.next_attempt_at_ms,
693
+ ...destination === void 0 ? {} : { destination }
694
+ };
695
+ }
696
+ function utf8Bytes(value) {
697
+ return value === void 0 ? 0 : Buffer.byteLength(value, "utf8");
698
+ }
699
+ function hookOutboxLogicalBytes(envelope, payloadJson) {
700
+ return utf8Bytes(envelope.syncId) + utf8Bytes(envelope.sessionId) + utf8Bytes(envelope.eventType) + utf8Bytes(envelope.target) + utf8Bytes(payloadJson) + utf8Bytes(envelope.cwd) + utf8Bytes(envelope.repository) + utf8Bytes(envelope.toolName) + utf8Bytes(envelope.userPrompt) + utf8Bytes(envelope.filePath) + utf8Bytes(envelope.command) + utf8Bytes(envelope.plan) + utf8Bytes(envelope.allowedPrompts) + utf8Bytes(envelope.toolResult) + utf8Bytes(envelope.destination?.targetUrl) + utf8Bytes(envelope.destination?.destinationIdentity);
701
+ }
702
+ function otlpEnvelopeLogicalBytes(envelope, envelopeId) {
703
+ return OTLP_ROW_FIXED_LOGICAL_BYTES + envelope.body.byteLength + utf8Bytes(envelopeId) + utf8Bytes(envelope.observerInstanceId) + utf8Bytes(envelope.observerVersion) + utf8Bytes(envelope.signal) + utf8Bytes(envelope.contentType) + utf8Bytes(envelope.contentEncoding) + // The persisted digest is always lowercase hexadecimal SHA-256.
704
+ 64 + utf8Bytes(envelope.destination?.targetUrl) + utf8Bytes(envelope.destination?.destinationIdentity) + (envelope.destination && envelope.destination.organizationId === void 0 ? MAX_OTLP_DESTINATION_ORGANIZATION_ID_BYTES : utf8Bytes(envelope.destination?.organizationId)) + (envelope.destination && envelope.destination.organizationSlug === void 0 ? MAX_OTLP_DESTINATION_ORGANIZATION_SLUG_BYTES : utf8Bytes(envelope.destination?.organizationSlug));
705
+ }
706
+ function requireOtlpSignal(value) {
707
+ if (value === "logs" || value === "metrics" || value === "traces") {
708
+ return value;
709
+ }
710
+ throw new Error("OTLP envelope signal must be logs, metrics, or traces");
711
+ }
712
+ function requireOtlpContentType(value) {
713
+ if (value === "application/json" || value === "application/x-protobuf") {
714
+ return value;
715
+ }
716
+ throw new Error(
717
+ "OTLP envelope content type must be application/json or application/x-protobuf"
718
+ );
719
+ }
720
+ function requireOtlpContentEncoding(value) {
721
+ if (value === "identity" || value === "gzip") return value;
722
+ throw new Error("OTLP envelope content encoding must be identity or gzip");
723
+ }
724
+ function otlpOutboxRecordFromRow(row) {
725
+ const rawBody = row.body;
726
+ if (!(rawBody instanceof Uint8Array)) {
727
+ throw new Error("FML observer OTLP outbox body is not a BLOB");
728
+ }
729
+ const destination = otlpDestinationFromRow(row);
730
+ return {
731
+ id: row.id,
732
+ envelopeId: row.envelope_id,
733
+ observerInstanceId: row.observer_instance_id,
734
+ observerVersion: row.observer_version,
735
+ signal: requireOtlpSignal(row.signal),
736
+ contentType: requireOtlpContentType(row.content_type),
737
+ contentEncoding: requireOtlpContentEncoding(row.content_encoding),
738
+ body: Buffer.from(rawBody),
739
+ sha256: row.sha256,
740
+ capturedAtMs: row.captured_at_ms,
741
+ retryCount: row.retry_count,
742
+ nextAttemptAtMs: row.next_attempt_at_ms,
743
+ ...destination === void 0 ? {} : { destination }
744
+ };
745
+ }
746
+ function validateHookBatchLimit(limit) {
747
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_HOOK_OUTBOX_BATCH_SIZE) {
748
+ throw new Error(
749
+ `Hook outbox batch limit must be an integer from 1 to ${MAX_HOOK_OUTBOX_BATCH_SIZE}`
750
+ );
751
+ }
752
+ }
753
+ function validateOtlpBatchLimit(limit) {
754
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_OTLP_OUTBOX_BATCH_SIZE) {
755
+ throw new Error(
756
+ `OTLP outbox batch limit must be an integer from 1 to ${MAX_OTLP_OUTBOX_BATCH_SIZE}`
757
+ );
758
+ }
759
+ }
760
+ function enforceOwnerOnlyMode(filePath, mode) {
761
+ if (process.platform === "win32") return;
762
+ try {
763
+ fs2.chmodSync(filePath, mode);
764
+ } catch (error) {
765
+ if (error.code !== "ENOENT") throw error;
766
+ }
767
+ }
768
+ function openObserverDatabase(databaseFile, options = {}) {
769
+ const maxHookSpoolBytes = options.maxHookSpoolBytes;
770
+ const maxOtlpSpoolBytes = options.maxOtlpSpoolBytes ?? DEFAULT_OTLP_SPOOL_MAX_BYTES;
771
+ const maxHookQuarantineAgeMs = options.maxHookQuarantineAgeMs;
772
+ if (maxHookSpoolBytes !== void 0 && (!Number.isSafeInteger(maxHookSpoolBytes) || maxHookSpoolBytes < 1)) {
773
+ throw new Error(
774
+ "Hook spool maximum must be a positive safe integer number of bytes"
775
+ );
776
+ }
777
+ if (!Number.isSafeInteger(maxOtlpSpoolBytes) || maxOtlpSpoolBytes < 1) {
778
+ throw new Error(
779
+ "OTLP spool maximum must be a positive safe integer number of bytes"
780
+ );
781
+ }
782
+ if (maxHookQuarantineAgeMs !== void 0 && (!Number.isSafeInteger(maxHookQuarantineAgeMs) || maxHookQuarantineAgeMs < 1)) {
783
+ throw new Error(
784
+ "Hook quarantine maximum age must be a positive safe integer number of milliseconds"
785
+ );
786
+ }
787
+ const getBuiltinModule = process.getBuiltinModule;
788
+ const sqlite = getBuiltinModule?.("node:sqlite");
789
+ if (!sqlite?.DatabaseSync) {
790
+ throw new Error("FML observer requires the Node.js built-in SQLite module");
791
+ }
792
+ const databaseDirectory = path2.dirname(databaseFile);
793
+ fs2.mkdirSync(databaseDirectory, { recursive: true, mode: 448 });
794
+ if (options.restrictDatabaseDirectory !== false) {
795
+ enforceOwnerOnlyMode(databaseDirectory, 448);
796
+ }
797
+ const database = new sqlite.DatabaseSync(databaseFile);
798
+ enforceOwnerOnlyMode(databaseFile, 384);
799
+ try {
800
+ database.exec(`
801
+ PRAGMA busy_timeout = 5000;
802
+ PRAGMA journal_mode = WAL;
803
+ -- Inbox files and OTLP requests are acknowledged immediately after a
804
+ -- database commit. FULL syncs each WAL commit before it can become the
805
+ -- sole durable copy.
806
+ PRAGMA synchronous = FULL;
807
+ `);
808
+ const readSchemaVersion = () => {
809
+ const version2 = database.prepare("PRAGMA user_version").get()?.user_version;
810
+ if (typeof version2 !== "number" || !Number.isInteger(version2)) {
811
+ throw new Error("FML observer database has an invalid schema version");
812
+ }
813
+ return version2;
814
+ };
815
+ const version = readSchemaVersion();
816
+ if (version > SCHEMA_VERSION) {
817
+ throw new Error(
818
+ `FML observer database schema ${version} is newer than supported schema ${SCHEMA_VERSION}`
819
+ );
820
+ }
821
+ const runMigration = (targetVersion, sql) => {
822
+ database.exec("BEGIN IMMEDIATE");
823
+ try {
824
+ const lockedVersion = readSchemaVersion();
825
+ if (lockedVersion > SCHEMA_VERSION) {
826
+ throw new Error(
827
+ `FML observer database schema ${lockedVersion} is newer than supported schema ${SCHEMA_VERSION}`
828
+ );
829
+ }
830
+ if (lockedVersion < targetVersion) {
831
+ database.exec(typeof sql === "string" ? sql : sql());
832
+ database.exec(`PRAGMA user_version = ${targetVersion}`);
833
+ }
834
+ database.exec("COMMIT");
835
+ } catch (error) {
836
+ try {
837
+ database.exec("ROLLBACK");
838
+ } catch {
839
+ }
840
+ throw error;
841
+ }
842
+ };
843
+ if (version < 1) {
844
+ runMigration(
845
+ 1,
846
+ `
847
+ CREATE TABLE IF NOT EXISTS runtime_state (
848
+ key TEXT PRIMARY KEY,
849
+ value_json TEXT NOT NULL,
850
+ updated_at_ms INTEGER NOT NULL
851
+ );
852
+ CREATE TABLE IF NOT EXISTS sync_runs (
853
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
854
+ started_at_ms INTEGER NOT NULL,
855
+ completed_at_ms INTEGER NOT NULL,
856
+ environment TEXT,
857
+ outcome TEXT NOT NULL,
858
+ uploaded_bytes INTEGER NOT NULL,
859
+ failed INTEGER NOT NULL,
860
+ error TEXT
861
+ );
862
+ CREATE INDEX IF NOT EXISTS sync_runs_completed_at
863
+ ON sync_runs(completed_at_ms DESC);
864
+ `
865
+ );
866
+ }
867
+ if (version < 2) {
868
+ runMigration(
869
+ 2,
870
+ `
871
+ CREATE TABLE hook_event_outbox (
872
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
873
+ sync_id TEXT NOT NULL UNIQUE,
874
+ session_id TEXT NOT NULL,
875
+ event_type TEXT NOT NULL,
876
+ timestamp_ms INTEGER NOT NULL,
877
+ target TEXT NOT NULL,
878
+ payload_json TEXT NOT NULL,
879
+ cwd TEXT,
880
+ repository TEXT,
881
+ tool_name TEXT,
882
+ user_prompt TEXT,
883
+ file_path TEXT,
884
+ command TEXT,
885
+ plan TEXT,
886
+ allowed_prompts TEXT,
887
+ tool_result TEXT,
888
+ captured_at_ms INTEGER NOT NULL
889
+ );
890
+ CREATE INDEX hook_event_outbox_captured_at
891
+ ON hook_event_outbox(captured_at_ms, id);
892
+ `
893
+ );
894
+ }
895
+ if (version < 3) {
896
+ runMigration(
897
+ 3,
898
+ `
899
+ CREATE TABLE hook_event_quarantine (
900
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
901
+ outbox_id INTEGER NOT NULL,
902
+ sync_id TEXT NOT NULL UNIQUE,
903
+ session_id TEXT NOT NULL,
904
+ event_type TEXT NOT NULL,
905
+ timestamp_ms INTEGER NOT NULL,
906
+ target TEXT NOT NULL,
907
+ payload_json TEXT NOT NULL,
908
+ cwd TEXT,
909
+ repository TEXT,
910
+ tool_name TEXT,
911
+ user_prompt TEXT,
912
+ file_path TEXT,
913
+ command TEXT,
914
+ plan TEXT,
915
+ allowed_prompts TEXT,
916
+ tool_result TEXT,
917
+ captured_at_ms INTEGER NOT NULL,
918
+ rejection_reason TEXT NOT NULL,
919
+ quarantined_at_ms INTEGER NOT NULL
920
+ );
921
+ CREATE INDEX hook_event_quarantine_quarantined_at
922
+ ON hook_event_quarantine(quarantined_at_ms DESC, id DESC);
923
+ `
924
+ );
925
+ }
926
+ if (version < 4) {
927
+ runMigration(
928
+ 4,
929
+ `
930
+ ALTER TABLE hook_event_outbox ADD COLUMN destination_url TEXT;
931
+ ALTER TABLE hook_event_outbox ADD COLUMN destination_organization TEXT;
932
+ ALTER TABLE hook_event_outbox ADD COLUMN destination_identity TEXT;
933
+ CREATE INDEX hook_event_outbox_destination
934
+ ON hook_event_outbox(
935
+ destination_identity,
936
+ destination_url,
937
+ destination_organization,
938
+ id
939
+ );
940
+ ALTER TABLE hook_event_quarantine ADD COLUMN destination_url TEXT;
941
+ ALTER TABLE hook_event_quarantine ADD COLUMN destination_organization TEXT;
942
+ ALTER TABLE hook_event_quarantine ADD COLUMN destination_identity TEXT;
943
+ `
944
+ );
945
+ }
946
+ if (version < 5) {
947
+ runMigration(
948
+ 5,
949
+ `
950
+ DROP INDEX hook_event_outbox_destination;
951
+ ALTER TABLE hook_event_outbox DROP COLUMN destination_organization;
952
+ ALTER TABLE hook_event_quarantine DROP COLUMN destination_organization;
953
+ CREATE INDEX hook_event_outbox_destination
954
+ ON hook_event_outbox(destination_identity, destination_url, id);
955
+ `
956
+ );
957
+ }
958
+ if (version < 6) {
959
+ runMigration(
960
+ 6,
961
+ `
962
+ ALTER TABLE hook_event_outbox
963
+ ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0;
964
+ ALTER TABLE hook_event_outbox
965
+ ADD COLUMN next_attempt_at_ms INTEGER NOT NULL DEFAULT 0;
966
+ ALTER TABLE hook_event_outbox
967
+ ADD COLUMN logical_bytes INTEGER NOT NULL DEFAULT 0;
968
+ UPDATE hook_event_outbox
969
+ SET logical_bytes = ${HOOK_OUTBOX_LOGICAL_BYTES_SQL};
970
+ CREATE INDEX IF NOT EXISTS hook_event_outbox_attempt
971
+ ON hook_event_outbox(next_attempt_at_ms, id);
972
+
973
+ ALTER TABLE hook_event_quarantine
974
+ ADD COLUMN logical_bytes INTEGER NOT NULL DEFAULT 0;
975
+ UPDATE hook_event_quarantine
976
+ SET logical_bytes =
977
+ ${HOOK_OUTBOX_LOGICAL_BYTES_SQL} +
978
+ length(CAST(rejection_reason AS BLOB));
979
+
980
+ CREATE TABLE IF NOT EXISTS hook_spool_state (
981
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
982
+ logical_bytes INTEGER NOT NULL,
983
+ evicted_records INTEGER NOT NULL,
984
+ evicted_bytes INTEGER NOT NULL,
985
+ evicted_pending_records INTEGER NOT NULL,
986
+ evicted_quarantined_records INTEGER NOT NULL,
987
+ last_evicted_at_ms INTEGER
988
+ );
989
+ INSERT OR IGNORE INTO hook_spool_state (
990
+ singleton,
991
+ logical_bytes,
992
+ evicted_records,
993
+ evicted_bytes,
994
+ evicted_pending_records,
995
+ evicted_quarantined_records,
996
+ last_evicted_at_ms
997
+ )
998
+ SELECT
999
+ 1,
1000
+ (SELECT COALESCE(SUM(logical_bytes), 0) FROM hook_event_outbox) +
1001
+ (SELECT COALESCE(SUM(logical_bytes), 0) FROM hook_event_quarantine),
1002
+ 0,
1003
+ 0,
1004
+ 0,
1005
+ 0,
1006
+ NULL;
1007
+ `
1008
+ );
1009
+ }
1010
+ if (version < 7) {
1011
+ runMigration(
1012
+ 7,
1013
+ `
1014
+ CREATE INDEX IF NOT EXISTS hook_event_outbox_destination_session
1015
+ ON hook_event_outbox(
1016
+ destination_identity,
1017
+ destination_url,
1018
+ session_id,
1019
+ id
1020
+ );
1021
+ `
1022
+ );
1023
+ }
1024
+ if (version < 9) {
1025
+ runMigration(9, () => {
1026
+ database.exec(`
1027
+ CREATE INDEX IF NOT EXISTS hook_event_outbox_destination_session
1028
+ ON hook_event_outbox(
1029
+ destination_identity,
1030
+ destination_url,
1031
+ session_id,
1032
+ id
1033
+ );
1034
+
1035
+ CREATE TABLE IF NOT EXISTS otlp_envelope_outbox (
1036
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1037
+ envelope_id TEXT NOT NULL UNIQUE,
1038
+ observer_instance_id TEXT NOT NULL,
1039
+ observer_version TEXT NOT NULL,
1040
+ signal TEXT NOT NULL CHECK (signal IN ('logs', 'metrics', 'traces')),
1041
+ content_type TEXT NOT NULL CHECK (
1042
+ content_type IN ('application/json', 'application/x-protobuf')
1043
+ ),
1044
+ content_encoding TEXT NOT NULL CHECK (
1045
+ content_encoding IN ('identity', 'gzip')
1046
+ ),
1047
+ body BLOB NOT NULL,
1048
+ sha256 TEXT NOT NULL,
1049
+ captured_at_ms INTEGER NOT NULL,
1050
+ destination_url TEXT,
1051
+ destination_identity TEXT,
1052
+ destination_organization_id TEXT,
1053
+ destination_organization_slug TEXT,
1054
+ CHECK (
1055
+ (
1056
+ destination_url IS NULL
1057
+ AND destination_identity IS NULL
1058
+ AND destination_organization_id IS NULL
1059
+ AND destination_organization_slug IS NULL
1060
+ )
1061
+ OR
1062
+ (
1063
+ destination_url IS NOT NULL
1064
+ AND destination_identity IS NOT NULL
1065
+ AND (
1066
+ (
1067
+ destination_organization_id IS NULL
1068
+ AND destination_organization_slug IS NULL
1069
+ )
1070
+ OR
1071
+ (
1072
+ destination_organization_id IS NOT NULL
1073
+ AND destination_organization_slug IS NOT NULL
1074
+ )
1075
+ )
1076
+ )
1077
+ )
1078
+ );
1079
+ CREATE INDEX IF NOT EXISTS otlp_envelope_outbox_destination
1080
+ ON otlp_envelope_outbox(
1081
+ destination_identity,
1082
+ destination_url,
1083
+ destination_organization_id,
1084
+ id
1085
+ );
1086
+ CREATE INDEX IF NOT EXISTS otlp_envelope_outbox_captured_at
1087
+ ON otlp_envelope_outbox(captured_at_ms, id);
1088
+ CREATE TABLE IF NOT EXISTS otlp_envelope_quarantine (
1089
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1090
+ outbox_id INTEGER NOT NULL,
1091
+ envelope_id TEXT NOT NULL UNIQUE,
1092
+ observer_instance_id TEXT NOT NULL,
1093
+ observer_version TEXT NOT NULL,
1094
+ signal TEXT NOT NULL CHECK (signal IN ('logs', 'metrics', 'traces')),
1095
+ content_type TEXT NOT NULL CHECK (
1096
+ content_type IN ('application/json', 'application/x-protobuf')
1097
+ ),
1098
+ content_encoding TEXT NOT NULL CHECK (
1099
+ content_encoding IN ('identity', 'gzip')
1100
+ ),
1101
+ body BLOB NOT NULL,
1102
+ sha256 TEXT NOT NULL,
1103
+ captured_at_ms INTEGER NOT NULL,
1104
+ destination_url TEXT,
1105
+ destination_identity TEXT,
1106
+ destination_organization_id TEXT,
1107
+ destination_organization_slug TEXT,
1108
+ rejection_reason TEXT NOT NULL,
1109
+ quarantined_at_ms INTEGER NOT NULL,
1110
+ CHECK (
1111
+ (
1112
+ destination_url IS NULL
1113
+ AND destination_identity IS NULL
1114
+ AND destination_organization_id IS NULL
1115
+ AND destination_organization_slug IS NULL
1116
+ )
1117
+ OR
1118
+ (
1119
+ destination_url IS NOT NULL
1120
+ AND destination_identity IS NOT NULL
1121
+ AND (
1122
+ (
1123
+ destination_organization_id IS NULL
1124
+ AND destination_organization_slug IS NULL
1125
+ )
1126
+ OR
1127
+ (
1128
+ destination_organization_id IS NOT NULL
1129
+ AND destination_organization_slug IS NOT NULL
1130
+ )
1131
+ )
1132
+ )
1133
+ )
1134
+ );
1135
+ CREATE INDEX IF NOT EXISTS otlp_envelope_quarantine_quarantined_at
1136
+ ON otlp_envelope_quarantine(quarantined_at_ms DESC, id DESC);
1137
+ `);
1138
+ const otlpOutboxColumns = new Set(
1139
+ database.prepare("PRAGMA table_info(otlp_envelope_outbox)").all().flatMap((row) => typeof row.name === "string" ? [row.name] : [])
1140
+ );
1141
+ const otlpQuarantineColumns = new Set(
1142
+ database.prepare("PRAGMA table_info(otlp_envelope_quarantine)").all().flatMap((row) => typeof row.name === "string" ? [row.name] : [])
1143
+ );
1144
+ return `
1145
+ ${otlpOutboxColumns.has("retry_count") ? "" : `ALTER TABLE otlp_envelope_outbox
1146
+ ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0;`}
1147
+ ${otlpOutboxColumns.has("next_attempt_at_ms") ? "" : `ALTER TABLE otlp_envelope_outbox
1148
+ ADD COLUMN next_attempt_at_ms INTEGER NOT NULL DEFAULT 0;`}
1149
+ ${otlpOutboxColumns.has("logical_bytes") ? "" : `ALTER TABLE otlp_envelope_outbox
1150
+ ADD COLUMN logical_bytes INTEGER NOT NULL DEFAULT 0;`}
1151
+ UPDATE otlp_envelope_outbox
1152
+ SET logical_bytes = ${OTLP_LOGICAL_BYTES_SQL};
1153
+ CREATE INDEX IF NOT EXISTS otlp_envelope_outbox_attempt
1154
+ ON otlp_envelope_outbox(
1155
+ destination_identity,
1156
+ destination_url,
1157
+ destination_organization_id,
1158
+ next_attempt_at_ms,
1159
+ id
1160
+ );
1161
+ ${otlpQuarantineColumns.has("logical_bytes") ? "" : `ALTER TABLE otlp_envelope_quarantine
1162
+ ADD COLUMN logical_bytes INTEGER NOT NULL DEFAULT 0;`}
1163
+ UPDATE otlp_envelope_quarantine
1164
+ SET logical_bytes = ${OTLP_LOGICAL_BYTES_SQL};
1165
+
1166
+ CREATE TABLE IF NOT EXISTS otlp_spool_state (
1167
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
1168
+ logical_bytes INTEGER NOT NULL CHECK (logical_bytes >= 0),
1169
+ body_bytes INTEGER NOT NULL CHECK (body_bytes >= 0),
1170
+ quarantined_ownerless_records INTEGER NOT NULL,
1171
+ quarantined_ownerless_logical_bytes INTEGER NOT NULL,
1172
+ quarantined_ownerless_body_bytes INTEGER NOT NULL,
1173
+ last_quarantined_ownerless_at_ms INTEGER
1174
+ );
1175
+ INSERT OR IGNORE INTO otlp_spool_state (
1176
+ singleton,
1177
+ logical_bytes,
1178
+ body_bytes,
1179
+ quarantined_ownerless_records,
1180
+ quarantined_ownerless_logical_bytes,
1181
+ quarantined_ownerless_body_bytes,
1182
+ last_quarantined_ownerless_at_ms
1183
+ ) VALUES (
1184
+ 1,
1185
+ (SELECT COALESCE(SUM(logical_bytes), 0) FROM otlp_envelope_outbox) +
1186
+ (SELECT COALESCE(SUM(logical_bytes), 0) FROM otlp_envelope_quarantine),
1187
+ (SELECT COALESCE(SUM(length(body)), 0) FROM otlp_envelope_outbox) +
1188
+ (SELECT COALESCE(SUM(length(body)), 0) FROM otlp_envelope_quarantine),
1189
+ 0,
1190
+ 0,
1191
+ 0,
1192
+ NULL
1193
+ );
1194
+
1195
+ DROP TRIGGER IF EXISTS otlp_outbox_body_bytes_insert;
1196
+ DROP TRIGGER IF EXISTS otlp_outbox_body_bytes_delete;
1197
+ DROP TRIGGER IF EXISTS otlp_quarantine_body_bytes_insert;
1198
+ DROP TRIGGER IF EXISTS otlp_quarantine_body_bytes_delete;
1199
+
1200
+ CREATE TRIGGER otlp_outbox_body_bytes_insert
1201
+ AFTER INSERT ON otlp_envelope_outbox
1202
+ BEGIN
1203
+ UPDATE otlp_spool_state
1204
+ SET
1205
+ logical_bytes = logical_bytes + NEW.logical_bytes,
1206
+ body_bytes = body_bytes + length(NEW.body)
1207
+ WHERE singleton = 1;
1208
+ END;
1209
+ CREATE TRIGGER otlp_outbox_body_bytes_delete
1210
+ AFTER DELETE ON otlp_envelope_outbox
1211
+ BEGIN
1212
+ UPDATE otlp_spool_state
1213
+ SET
1214
+ logical_bytes = logical_bytes - OLD.logical_bytes,
1215
+ body_bytes = body_bytes - length(OLD.body)
1216
+ WHERE singleton = 1;
1217
+ END;
1218
+ CREATE TRIGGER otlp_quarantine_body_bytes_insert
1219
+ AFTER INSERT ON otlp_envelope_quarantine
1220
+ BEGIN
1221
+ UPDATE otlp_spool_state
1222
+ SET
1223
+ logical_bytes = logical_bytes + NEW.logical_bytes,
1224
+ body_bytes = body_bytes + length(NEW.body)
1225
+ WHERE singleton = 1;
1226
+ END;
1227
+ CREATE TRIGGER otlp_quarantine_body_bytes_delete
1228
+ AFTER DELETE ON otlp_envelope_quarantine
1229
+ BEGIN
1230
+ UPDATE otlp_spool_state
1231
+ SET
1232
+ logical_bytes = logical_bytes - OLD.logical_bytes,
1233
+ body_bytes = body_bytes - length(OLD.body)
1234
+ WHERE singleton = 1;
1235
+ END;
1236
+
1237
+ `;
1238
+ });
1239
+ }
1240
+ if (version < 10) {
1241
+ runMigration(
1242
+ 10,
1243
+ `
1244
+ -- OTLP is operational telemetry, not a lossless artifact. Collapse the
1245
+ -- prototype quarantine into durable drop accounting and retain only a
1246
+ -- bounded pending spool.
1247
+ DROP TRIGGER IF EXISTS otlp_outbox_body_bytes_insert;
1248
+ DROP TRIGGER IF EXISTS otlp_outbox_body_bytes_delete;
1249
+ DROP TRIGGER IF EXISTS otlp_quarantine_body_bytes_insert;
1250
+ DROP TRIGGER IF EXISTS otlp_quarantine_body_bytes_delete;
1251
+
1252
+ UPDATE otlp_envelope_outbox
1253
+ SET logical_bytes = ${OTLP_LOGICAL_BYTES_SQL};
1254
+
1255
+ CREATE TABLE otlp_spool_state_v10 (
1256
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
1257
+ logical_bytes INTEGER NOT NULL CHECK (logical_bytes >= 0),
1258
+ body_bytes INTEGER NOT NULL CHECK (body_bytes >= 0),
1259
+ dropped_records INTEGER NOT NULL CHECK (dropped_records >= 0),
1260
+ dropped_logical_bytes INTEGER NOT NULL CHECK (dropped_logical_bytes >= 0),
1261
+ dropped_body_bytes INTEGER NOT NULL CHECK (dropped_body_bytes >= 0),
1262
+ last_dropped_at_ms INTEGER,
1263
+ last_drop_reason TEXT
1264
+ );
1265
+ INSERT INTO otlp_spool_state_v10 (
1266
+ singleton,
1267
+ logical_bytes,
1268
+ body_bytes,
1269
+ dropped_records,
1270
+ dropped_logical_bytes,
1271
+ dropped_body_bytes,
1272
+ last_dropped_at_ms,
1273
+ last_drop_reason
1274
+ )
1275
+ SELECT
1276
+ 1,
1277
+ COALESCE((
1278
+ SELECT SUM(logical_bytes)
1279
+ FROM otlp_envelope_outbox
1280
+ WHERE destination_url IS NOT NULL
1281
+ ), 0),
1282
+ COALESCE((
1283
+ SELECT SUM(length(body))
1284
+ FROM otlp_envelope_outbox
1285
+ WHERE destination_url IS NOT NULL
1286
+ ), 0),
1287
+ quarantined_ownerless_records +
1288
+ (SELECT COUNT(*) FROM otlp_envelope_quarantine
1289
+ WHERE rejection_reason <> 'ownerless_ttl_expired') +
1290
+ (SELECT COUNT(*) FROM otlp_envelope_outbox
1291
+ WHERE destination_url IS NULL),
1292
+ quarantined_ownerless_logical_bytes +
1293
+ COALESCE((SELECT SUM(logical_bytes) FROM otlp_envelope_quarantine
1294
+ WHERE rejection_reason <> 'ownerless_ttl_expired'), 0) +
1295
+ COALESCE((SELECT SUM(logical_bytes) FROM otlp_envelope_outbox
1296
+ WHERE destination_url IS NULL), 0),
1297
+ quarantined_ownerless_body_bytes +
1298
+ COALESCE((SELECT SUM(length(body)) FROM otlp_envelope_quarantine
1299
+ WHERE rejection_reason <> 'ownerless_ttl_expired'), 0) +
1300
+ COALESCE((SELECT SUM(length(body)) FROM otlp_envelope_outbox
1301
+ WHERE destination_url IS NULL), 0),
1302
+ (
1303
+ SELECT MAX(timestamp_ms)
1304
+ FROM (
1305
+ SELECT last_quarantined_ownerless_at_ms AS timestamp_ms
1306
+ UNION ALL
1307
+ SELECT MAX(quarantined_at_ms) FROM otlp_envelope_quarantine
1308
+ UNION ALL
1309
+ SELECT MAX(captured_at_ms) FROM otlp_envelope_outbox
1310
+ WHERE destination_url IS NULL
1311
+ )
1312
+ ),
1313
+ CASE
1314
+ WHEN quarantined_ownerless_records > 0
1315
+ OR EXISTS (SELECT 1 FROM otlp_envelope_quarantine)
1316
+ OR EXISTS (SELECT 1 FROM otlp_envelope_outbox
1317
+ WHERE destination_url IS NULL)
1318
+ THEN 'migration_cleanup'
1319
+ ELSE NULL
1320
+ END
1321
+ FROM otlp_spool_state
1322
+ WHERE singleton = 1;
1323
+
1324
+ DROP TABLE otlp_spool_state;
1325
+ ALTER TABLE otlp_spool_state_v10 RENAME TO otlp_spool_state;
1326
+ DELETE FROM otlp_envelope_outbox WHERE destination_url IS NULL;
1327
+ DROP TABLE otlp_envelope_quarantine;
1328
+
1329
+ CREATE TRIGGER otlp_outbox_body_bytes_insert
1330
+ AFTER INSERT ON otlp_envelope_outbox
1331
+ BEGIN
1332
+ UPDATE otlp_spool_state
1333
+ SET
1334
+ logical_bytes = logical_bytes + NEW.logical_bytes,
1335
+ body_bytes = body_bytes + length(NEW.body)
1336
+ WHERE singleton = 1;
1337
+ END;
1338
+ CREATE TRIGGER otlp_outbox_body_bytes_delete
1339
+ AFTER DELETE ON otlp_envelope_outbox
1340
+ BEGIN
1341
+ UPDATE otlp_spool_state
1342
+ SET
1343
+ logical_bytes = logical_bytes - OLD.logical_bytes,
1344
+ body_bytes = body_bytes - length(OLD.body)
1345
+ WHERE singleton = 1;
1346
+ END;
1347
+ `
1348
+ );
1349
+ }
1350
+ database.exec(`
1351
+ BEGIN IMMEDIATE;
1352
+ ${RECONCILE_HOOK_SPOOL_STATE_SQL};
1353
+ INSERT OR IGNORE INTO otlp_spool_state (
1354
+ singleton,
1355
+ logical_bytes,
1356
+ body_bytes,
1357
+ dropped_records,
1358
+ dropped_logical_bytes,
1359
+ dropped_body_bytes,
1360
+ last_dropped_at_ms,
1361
+ last_drop_reason
1362
+ ) VALUES (1, 0, 0, 0, 0, 0, NULL, NULL);
1363
+ UPDATE otlp_spool_state
1364
+ SET
1365
+ logical_bytes =
1366
+ (SELECT COALESCE(SUM(logical_bytes), 0) FROM otlp_envelope_outbox),
1367
+ body_bytes =
1368
+ (SELECT COALESCE(SUM(length(body)), 0) FROM otlp_envelope_outbox)
1369
+ WHERE singleton = 1;
1370
+ COMMIT;
1371
+ `);
1372
+ const setRuntimeState = database.prepare(`
1373
+ INSERT INTO runtime_state (key, value_json, updated_at_ms)
1374
+ VALUES (?, ?, ?)
1375
+ ON CONFLICT(key) DO UPDATE SET
1376
+ value_json = excluded.value_json,
1377
+ updated_at_ms = excluded.updated_at_ms
1378
+ `);
1379
+ const recordSyncRun = database.prepare(`
1380
+ INSERT INTO sync_runs (
1381
+ started_at_ms,
1382
+ completed_at_ms,
1383
+ environment,
1384
+ outcome,
1385
+ uploaded_bytes,
1386
+ failed,
1387
+ error
1388
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
1389
+ `);
1390
+ const trimSyncRuns = database.prepare(`
1391
+ DELETE FROM sync_runs
1392
+ WHERE id <= (SELECT COALESCE(MAX(id), 0) - ? FROM sync_runs)
1393
+ `);
1394
+ const findPendingHookEvent = database.prepare(`
1395
+ SELECT 1 AS present
1396
+ FROM hook_event_outbox
1397
+ WHERE sync_id = ?
1398
+ `);
1399
+ const findQuarantinedHookEvent = database.prepare(`
1400
+ SELECT 1 AS present
1401
+ FROM hook_event_quarantine
1402
+ WHERE sync_id = ?
1403
+ `);
1404
+ const countPendingHookEvents = database.prepare(`
1405
+ SELECT COUNT(*) AS count
1406
+ FROM hook_event_outbox
1407
+ `);
1408
+ const countQuarantinedHookEvents = database.prepare(`
1409
+ SELECT COUNT(*) AS count
1410
+ FROM hook_event_quarantine
1411
+ `);
1412
+ const captureHookEvent = database.prepare(`
1413
+ INSERT INTO hook_event_outbox (
1414
+ sync_id,
1415
+ session_id,
1416
+ event_type,
1417
+ timestamp_ms,
1418
+ target,
1419
+ payload_json,
1420
+ cwd,
1421
+ repository,
1422
+ tool_name,
1423
+ user_prompt,
1424
+ file_path,
1425
+ command,
1426
+ plan,
1427
+ allowed_prompts,
1428
+ tool_result,
1429
+ captured_at_ms,
1430
+ destination_url,
1431
+ destination_identity,
1432
+ logical_bytes
1433
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1434
+ `);
1435
+ const listPendingHookEvents = database.prepare(`
1436
+ SELECT
1437
+ id,
1438
+ sync_id,
1439
+ session_id,
1440
+ event_type,
1441
+ timestamp_ms,
1442
+ target,
1443
+ payload_json,
1444
+ cwd,
1445
+ repository,
1446
+ tool_name,
1447
+ user_prompt,
1448
+ file_path,
1449
+ command,
1450
+ plan,
1451
+ allowed_prompts,
1452
+ tool_result,
1453
+ captured_at_ms,
1454
+ destination_url,
1455
+ destination_identity,
1456
+ retry_count,
1457
+ next_attempt_at_ms
1458
+ FROM hook_event_outbox
1459
+ ORDER BY id ASC
1460
+ LIMIT ?
1461
+ `);
1462
+ const claimUnboundHookEvents = database.prepare(`
1463
+ UPDATE hook_event_outbox
1464
+ SET
1465
+ destination_url = ?,
1466
+ destination_identity = ?,
1467
+ logical_bytes = logical_bytes + ?
1468
+ WHERE destination_url IS NULL
1469
+ AND destination_identity IS NULL
1470
+ `);
1471
+ const listDestinationPendingHookEvents = database.prepare(`
1472
+ SELECT
1473
+ id,
1474
+ sync_id,
1475
+ session_id,
1476
+ event_type,
1477
+ timestamp_ms,
1478
+ target,
1479
+ payload_json,
1480
+ cwd,
1481
+ repository,
1482
+ tool_name,
1483
+ user_prompt,
1484
+ file_path,
1485
+ command,
1486
+ plan,
1487
+ allowed_prompts,
1488
+ tool_result,
1489
+ captured_at_ms,
1490
+ destination_url,
1491
+ destination_identity,
1492
+ retry_count,
1493
+ next_attempt_at_ms
1494
+ FROM hook_event_outbox AS candidate
1495
+ WHERE candidate.destination_url = ?
1496
+ AND candidate.destination_identity = ?
1497
+ AND candidate.next_attempt_at_ms <= ?
1498
+ AND NOT EXISTS (
1499
+ SELECT 1
1500
+ FROM hook_event_outbox AS predecessor
1501
+ WHERE predecessor.destination_url = candidate.destination_url
1502
+ AND predecessor.destination_identity = candidate.destination_identity
1503
+ AND predecessor.session_id = candidate.session_id
1504
+ AND predecessor.id < candidate.id
1505
+ AND predecessor.next_attempt_at_ms > ?
1506
+ )
1507
+ -- Once a deferred row is eligible again, restore durable FIFO order so
1508
+ -- a steady stream of new captures cannot starve it indefinitely.
1509
+ ORDER BY id ASC
1510
+ LIMIT ?
1511
+ `);
1512
+ const nextDestinationHookAttempt = database.prepare(`
1513
+ SELECT MIN(candidate.next_attempt_at_ms) AS next_attempt_at_ms
1514
+ FROM hook_event_outbox AS candidate
1515
+ WHERE candidate.destination_url = ?
1516
+ AND candidate.destination_identity = ?
1517
+ AND NOT EXISTS (
1518
+ SELECT 1
1519
+ FROM hook_event_outbox AS predecessor
1520
+ WHERE predecessor.destination_url = candidate.destination_url
1521
+ AND predecessor.destination_identity = candidate.destination_identity
1522
+ AND predecessor.session_id = candidate.session_id
1523
+ AND predecessor.id < candidate.id
1524
+ )
1525
+ `);
1526
+ const quarantineHookEvent = database.prepare(`
1527
+ INSERT INTO hook_event_quarantine (
1528
+ outbox_id,
1529
+ sync_id,
1530
+ session_id,
1531
+ event_type,
1532
+ timestamp_ms,
1533
+ target,
1534
+ payload_json,
1535
+ cwd,
1536
+ repository,
1537
+ tool_name,
1538
+ user_prompt,
1539
+ file_path,
1540
+ command,
1541
+ plan,
1542
+ allowed_prompts,
1543
+ tool_result,
1544
+ captured_at_ms,
1545
+ destination_url,
1546
+ destination_identity,
1547
+ rejection_reason,
1548
+ quarantined_at_ms,
1549
+ logical_bytes
1550
+ )
1551
+ SELECT
1552
+ id,
1553
+ sync_id,
1554
+ session_id,
1555
+ event_type,
1556
+ timestamp_ms,
1557
+ target,
1558
+ payload_json,
1559
+ cwd,
1560
+ repository,
1561
+ tool_name,
1562
+ user_prompt,
1563
+ file_path,
1564
+ command,
1565
+ plan,
1566
+ allowed_prompts,
1567
+ tool_result,
1568
+ captured_at_ms,
1569
+ destination_url,
1570
+ destination_identity,
1571
+ ?,
1572
+ ?,
1573
+ logical_bytes + ?
1574
+ FROM hook_event_outbox
1575
+ WHERE destination_url = ?
1576
+ AND destination_identity = ?
1577
+ AND sync_id = ?
1578
+ ON CONFLICT(sync_id) DO NOTHING
1579
+ `);
1580
+ const hookSpoolState = database.prepare(`
1581
+ SELECT
1582
+ logical_bytes,
1583
+ evicted_records,
1584
+ evicted_bytes,
1585
+ evicted_pending_records,
1586
+ evicted_quarantined_records,
1587
+ last_evicted_at_ms
1588
+ FROM hook_spool_state
1589
+ WHERE singleton = 1
1590
+ `);
1591
+ const reconcileHookSpoolState = database.prepare(
1592
+ RECONCILE_HOOK_SPOOL_STATE_SQL
1593
+ );
1594
+ const adjustHookSpoolBytes = database.prepare(`
1595
+ UPDATE hook_spool_state
1596
+ SET logical_bytes = logical_bytes + ?
1597
+ WHERE singleton = 1
1598
+ `);
1599
+ const recordHookSpoolEviction = database.prepare(`
1600
+ UPDATE hook_spool_state
1601
+ SET
1602
+ logical_bytes = logical_bytes - ?,
1603
+ evicted_records = evicted_records + ?,
1604
+ evicted_bytes = evicted_bytes + ?,
1605
+ evicted_pending_records = evicted_pending_records + ?,
1606
+ evicted_quarantined_records = evicted_quarantined_records + ?,
1607
+ last_evicted_at_ms = ?
1608
+ WHERE singleton = 1
1609
+ `);
1610
+ const recordOversizedHookCapture = database.prepare(`
1611
+ UPDATE hook_spool_state
1612
+ SET
1613
+ evicted_records = evicted_records + 1,
1614
+ evicted_bytes = evicted_bytes + ?,
1615
+ evicted_pending_records = evicted_pending_records + 1,
1616
+ last_evicted_at_ms = ?
1617
+ WHERE singleton = 1
1618
+ `);
1619
+ const quarantineEvictionPlan = database.prepare(`
1620
+ WITH ordered AS (
1621
+ SELECT
1622
+ logical_bytes,
1623
+ SUM(logical_bytes) OVER (ORDER BY id ASC) - logical_bytes
1624
+ AS preceding_bytes
1625
+ FROM hook_event_quarantine
1626
+ )
1627
+ SELECT
1628
+ COUNT(*) AS record_count,
1629
+ COALESCE(SUM(logical_bytes), 0) AS logical_bytes
1630
+ FROM ordered
1631
+ WHERE preceding_bytes < ?
1632
+ `);
1633
+ const pendingEvictionPlan = database.prepare(`
1634
+ WITH ordered AS (
1635
+ SELECT
1636
+ logical_bytes,
1637
+ SUM(logical_bytes) OVER (ORDER BY id ASC) - logical_bytes
1638
+ AS preceding_bytes
1639
+ FROM hook_event_outbox
1640
+ )
1641
+ SELECT
1642
+ COUNT(*) AS record_count,
1643
+ COALESCE(SUM(logical_bytes), 0) AS logical_bytes
1644
+ FROM ordered
1645
+ WHERE preceding_bytes < ?
1646
+ `);
1647
+ const evictOldestQuarantinedHookEvents = database.prepare(`
1648
+ DELETE FROM hook_event_quarantine
1649
+ WHERE id IN (
1650
+ SELECT id FROM hook_event_quarantine ORDER BY id ASC LIMIT ?
1651
+ )
1652
+ `);
1653
+ const evictOldestPendingHookEvents = database.prepare(`
1654
+ DELETE FROM hook_event_outbox
1655
+ WHERE id IN (
1656
+ SELECT id FROM hook_event_outbox ORDER BY id ASC LIMIT ?
1657
+ )
1658
+ `);
1659
+ const expiredQuarantinePlan = database.prepare(`
1660
+ SELECT
1661
+ COUNT(*) AS record_count,
1662
+ COALESCE(SUM(logical_bytes), 0) AS logical_bytes
1663
+ FROM hook_event_quarantine
1664
+ WHERE quarantined_at_ms <= ?
1665
+ `);
1666
+ const pruneExpiredQuarantine = database.prepare(`
1667
+ DELETE FROM hook_event_quarantine
1668
+ WHERE quarantined_at_ms <= ?
1669
+ `);
1670
+ const deletePendingHookEvent = database.prepare(`
1671
+ DELETE FROM hook_event_outbox
1672
+ WHERE destination_url = ?
1673
+ AND destination_identity = ?
1674
+ AND sync_id = ?
1675
+ `);
1676
+ const findPendingOtlpEnvelope = database.prepare(`
1677
+ SELECT 1 AS present
1678
+ FROM otlp_envelope_outbox
1679
+ WHERE envelope_id = ?
1680
+ `);
1681
+ const countPendingOtlpEnvelopes = database.prepare(`
1682
+ SELECT COUNT(*) AS count
1683
+ FROM otlp_envelope_outbox
1684
+ `);
1685
+ const captureOtlpEnvelope = database.prepare(`
1686
+ INSERT INTO otlp_envelope_outbox (
1687
+ envelope_id,
1688
+ observer_instance_id,
1689
+ observer_version,
1690
+ signal,
1691
+ content_type,
1692
+ content_encoding,
1693
+ body,
1694
+ sha256,
1695
+ captured_at_ms,
1696
+ destination_url,
1697
+ destination_identity,
1698
+ destination_organization_id,
1699
+ destination_organization_slug,
1700
+ logical_bytes
1701
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1702
+ `);
1703
+ const listPendingOtlpEnvelopes = database.prepare(`
1704
+ SELECT
1705
+ id,
1706
+ envelope_id,
1707
+ observer_instance_id,
1708
+ observer_version,
1709
+ signal,
1710
+ content_type,
1711
+ content_encoding,
1712
+ body,
1713
+ sha256,
1714
+ captured_at_ms,
1715
+ destination_url,
1716
+ destination_identity,
1717
+ destination_organization_id,
1718
+ destination_organization_slug,
1719
+ retry_count,
1720
+ next_attempt_at_ms
1721
+ FROM otlp_envelope_outbox
1722
+ ORDER BY id ASC
1723
+ LIMIT ?
1724
+ `);
1725
+ const claimOrganizationForOtlpEnvelopes = database.prepare(`
1726
+ UPDATE otlp_envelope_outbox
1727
+ SET
1728
+ destination_organization_id = ?,
1729
+ destination_organization_slug = ?
1730
+ WHERE destination_url = ?
1731
+ AND destination_identity = ?
1732
+ AND destination_organization_id IS NULL
1733
+ AND destination_organization_slug IS NULL
1734
+ `);
1735
+ const listDestinationPendingOtlpEnvelopes = database.prepare(`
1736
+ SELECT
1737
+ id,
1738
+ envelope_id,
1739
+ observer_instance_id,
1740
+ observer_version,
1741
+ signal,
1742
+ content_type,
1743
+ content_encoding,
1744
+ body,
1745
+ sha256,
1746
+ captured_at_ms,
1747
+ destination_url,
1748
+ destination_identity,
1749
+ destination_organization_id,
1750
+ destination_organization_slug,
1751
+ retry_count,
1752
+ next_attempt_at_ms
1753
+ FROM otlp_envelope_outbox
1754
+ WHERE destination_url = ?
1755
+ AND destination_identity = ?
1756
+ AND destination_organization_id IS ?
1757
+ AND next_attempt_at_ms <= ?
1758
+ ORDER BY id ASC
1759
+ LIMIT ?
1760
+ `);
1761
+ const deferDestinationPendingOtlpEnvelope = database.prepare(`
1762
+ UPDATE otlp_envelope_outbox
1763
+ SET
1764
+ retry_count = retry_count + 1,
1765
+ next_attempt_at_ms = ?
1766
+ WHERE destination_url = ?
1767
+ AND destination_identity = ?
1768
+ AND destination_organization_id IS ?
1769
+ AND envelope_id = ?
1770
+ `);
1771
+ const nextDestinationPendingOtlpAttempt = database.prepare(`
1772
+ SELECT MIN(next_attempt_at_ms) AS next_attempt_at_ms
1773
+ FROM otlp_envelope_outbox
1774
+ WHERE destination_url = ?
1775
+ AND destination_identity = ?
1776
+ AND destination_organization_id IS ?
1777
+ `);
1778
+ const getOtlpSpoolState = database.prepare(`
1779
+ SELECT
1780
+ logical_bytes,
1781
+ body_bytes,
1782
+ dropped_records,
1783
+ dropped_logical_bytes,
1784
+ dropped_body_bytes,
1785
+ last_dropped_at_ms,
1786
+ last_drop_reason
1787
+ FROM otlp_spool_state
1788
+ WHERE singleton = 1
1789
+ `);
1790
+ const recordOtlpDrops = database.prepare(`
1791
+ UPDATE otlp_spool_state
1792
+ SET
1793
+ dropped_records = dropped_records + ?,
1794
+ dropped_logical_bytes = dropped_logical_bytes + ?,
1795
+ dropped_body_bytes = dropped_body_bytes + ?,
1796
+ last_dropped_at_ms = ?,
1797
+ last_drop_reason = ?
1798
+ WHERE singleton = 1
1799
+ `);
1800
+ const otlpCapacityEvictionPlan = database.prepare(`
1801
+ WITH ordered AS (
1802
+ SELECT
1803
+ logical_bytes,
1804
+ length(body) AS body_bytes,
1805
+ SUM(logical_bytes) OVER (ORDER BY id ASC) - logical_bytes
1806
+ AS preceding_bytes
1807
+ FROM otlp_envelope_outbox
1808
+ )
1809
+ SELECT
1810
+ COUNT(*) AS record_count,
1811
+ COALESCE(SUM(logical_bytes), 0) AS logical_bytes,
1812
+ COALESCE(SUM(body_bytes), 0) AS body_bytes
1813
+ FROM ordered
1814
+ WHERE preceding_bytes < ?
1815
+ `);
1816
+ const evictOldestPendingOtlpEnvelopes = database.prepare(`
1817
+ DELETE FROM otlp_envelope_outbox
1818
+ WHERE id IN (
1819
+ SELECT id FROM otlp_envelope_outbox ORDER BY id ASC LIMIT ?
1820
+ )
1821
+ `);
1822
+ const getDestinationPendingOtlpEnvelopeAccounting = database.prepare(`
1823
+ SELECT logical_bytes, length(body) AS body_bytes
1824
+ FROM otlp_envelope_outbox
1825
+ WHERE destination_url = ?
1826
+ AND destination_identity = ?
1827
+ AND destination_organization_id IS ?
1828
+ AND envelope_id = ?
1829
+ `);
1830
+ const deleteDestinationPendingOtlpEnvelope = database.prepare(`
1831
+ DELETE FROM otlp_envelope_outbox
1832
+ WHERE destination_url = ?
1833
+ AND destination_identity = ?
1834
+ AND destination_organization_id IS ?
1835
+ AND envelope_id = ?
1836
+ `);
1837
+ const runImmediateTransaction = (operation) => {
1838
+ database.exec("BEGIN IMMEDIATE");
1839
+ try {
1840
+ const result = operation();
1841
+ database.exec("COMMIT");
1842
+ return result;
1843
+ } catch (error) {
1844
+ try {
1845
+ database.exec("ROLLBACK");
1846
+ } catch {
1847
+ }
1848
+ throw error;
1849
+ }
1850
+ };
1851
+ const requireSpoolInteger = (row, key) => {
1852
+ const value = row?.[key];
1853
+ if (!Number.isSafeInteger(value) || value < 0) {
1854
+ throw new ObserverHookSpoolStateInvariantError(
1855
+ `FML observer spool ${key} is invalid`
1856
+ );
1857
+ }
1858
+ return value;
1859
+ };
1860
+ const recordOtlpDrop = (records, logicalBytes, bodyBytes, droppedAtMs, reason) => {
1861
+ if (records === 0) return;
1862
+ requireTimestamp(droppedAtMs, "OTLP drop timestamp");
1863
+ requireBoundedString(
1864
+ reason,
1865
+ "OTLP drop reason",
1866
+ MAX_OTLP_DROP_REASON_BYTES
1867
+ );
1868
+ const updated = recordOtlpDrops.run(
1869
+ records,
1870
+ logicalBytes,
1871
+ bodyBytes,
1872
+ droppedAtMs,
1873
+ reason
1874
+ ).changes;
1875
+ if (updated !== 1) {
1876
+ throw new Error("FML observer OTLP drop accounting was incomplete");
1877
+ }
1878
+ };
1879
+ const readHookSpoolState = () => {
1880
+ try {
1881
+ return hookSpoolState.get();
1882
+ } catch (error) {
1883
+ if (error instanceof RangeError) {
1884
+ throw new ObserverHookSpoolStateInvariantError(
1885
+ "FML observer hook spool contains an unsafe integer"
1886
+ );
1887
+ }
1888
+ throw error;
1889
+ }
1890
+ };
1891
+ const readHookSpoolLogicalBytes = () => {
1892
+ try {
1893
+ return requireSpoolInteger(readHookSpoolState(), "logical_bytes");
1894
+ } catch (error) {
1895
+ if (!(error instanceof ObserverHookSpoolStateInvariantError)) {
1896
+ throw error;
1897
+ }
1898
+ reconcileHookSpoolState.run();
1899
+ return requireSpoolInteger(readHookSpoolState(), "logical_bytes");
1900
+ }
1901
+ };
1902
+ const runImmediateWithSpoolRepair = (operation) => {
1903
+ try {
1904
+ return runImmediateTransaction(operation);
1905
+ } catch (error) {
1906
+ if (!(error instanceof ObserverHookSpoolStateInvariantError)) {
1907
+ throw error;
1908
+ }
1909
+ runImmediateTransaction(() => reconcileHookSpoolState.run());
1910
+ return runImmediateTransaction(operation);
1911
+ }
1912
+ };
1913
+ const evictionPlan = (statement, bytesToFree) => {
1914
+ if (bytesToFree <= 0) return { records: 0, bytes: 0 };
1915
+ const row = statement.get(bytesToFree);
1916
+ return {
1917
+ records: requireSpoolInteger(row, "record_count"),
1918
+ bytes: requireSpoolInteger(row, "logical_bytes")
1919
+ };
1920
+ };
1921
+ let nextQuarantinePruneAtMs = 0;
1922
+ const enforceHookQuarantineAge = (nowMs, force = false) => {
1923
+ if (maxHookQuarantineAgeMs === void 0) return;
1924
+ requireTimestamp(nowMs, "Hook quarantine prune timestamp");
1925
+ if (!force && nowMs < nextQuarantinePruneAtMs) return;
1926
+ nextQuarantinePruneAtMs = Math.min(
1927
+ Number.MAX_SAFE_INTEGER,
1928
+ nowMs + Math.min(maxHookQuarantineAgeMs, QUARANTINE_PRUNE_INTERVAL_MS)
1929
+ );
1930
+ const cutoffMs = Math.max(0, nowMs - maxHookQuarantineAgeMs);
1931
+ const expiredRow = expiredQuarantinePlan.get(cutoffMs);
1932
+ const expired = {
1933
+ records: requireSpoolInteger(expiredRow, "record_count"),
1934
+ bytes: requireSpoolInteger(expiredRow, "logical_bytes")
1935
+ };
1936
+ if (expired.records === 0) return;
1937
+ const changes = pruneExpiredQuarantine.run(cutoffMs).changes;
1938
+ if (changes !== expired.records) {
1939
+ throw new Error("FML observer hook quarantine pruning was incomplete");
1940
+ }
1941
+ recordHookSpoolEviction.run(
1942
+ expired.bytes,
1943
+ expired.records,
1944
+ expired.bytes,
1945
+ 0,
1946
+ expired.records,
1947
+ nowMs
1948
+ );
1949
+ };
1950
+ const enforceHookSpoolLimit = (evictedAtMs, additionalBytes = 0) => {
1951
+ if (maxHookSpoolBytes === void 0) return;
1952
+ requireTimestamp(evictedAtMs, "Hook spool eviction timestamp");
1953
+ if (!Number.isSafeInteger(additionalBytes) || additionalBytes < 0) {
1954
+ throw new Error("FML observer hook spool additional bytes are invalid");
1955
+ }
1956
+ const currentBytes = readHookSpoolLogicalBytes();
1957
+ let bytesToFree = currentBytes + additionalBytes - maxHookSpoolBytes;
1958
+ if (bytesToFree <= 0) return;
1959
+ const requiredBytes = bytesToFree;
1960
+ const quarantined = evictionPlan(quarantineEvictionPlan, bytesToFree);
1961
+ if (quarantined.records > 0) {
1962
+ const changes = evictOldestQuarantinedHookEvents.run(
1963
+ quarantined.records
1964
+ ).changes;
1965
+ if (changes !== quarantined.records) {
1966
+ throw new Error(
1967
+ "FML observer hook quarantine eviction was incomplete"
1968
+ );
1969
+ }
1970
+ bytesToFree -= quarantined.bytes;
1971
+ }
1972
+ const pending = evictionPlan(pendingEvictionPlan, bytesToFree);
1973
+ if (pending.records > 0) {
1974
+ const changes = evictOldestPendingHookEvents.run(
1975
+ pending.records
1976
+ ).changes;
1977
+ if (changes !== pending.records) {
1978
+ throw new Error("FML observer hook outbox eviction was incomplete");
1979
+ }
1980
+ }
1981
+ const evictedRecords = quarantined.records + pending.records;
1982
+ const evictedBytes = quarantined.bytes + pending.bytes;
1983
+ if (evictedRecords === 0 || evictedBytes === 0 || evictedBytes < requiredBytes) {
1984
+ throw new ObserverHookSpoolStateInvariantError(
1985
+ "FML observer hook spool accounting could not enforce its limit"
1986
+ );
1987
+ }
1988
+ recordHookSpoolEviction.run(
1989
+ evictedBytes,
1990
+ evictedRecords,
1991
+ evictedBytes,
1992
+ pending.records,
1993
+ quarantined.records,
1994
+ evictedAtMs
1995
+ );
1996
+ };
1997
+ if (maxHookSpoolBytes !== void 0 || maxHookQuarantineAgeMs !== void 0) {
1998
+ runImmediateTransaction(() => {
1999
+ const nowMs = Date.now();
2000
+ enforceHookQuarantineAge(nowMs, true);
2001
+ enforceHookSpoolLimit(nowMs);
2002
+ });
2003
+ }
2004
+ enforceOwnerOnlyMode(databaseFile, 384);
2005
+ enforceOwnerOnlyMode(`${databaseFile}-wal`, 384);
2006
+ enforceOwnerOnlyMode(`${databaseFile}-shm`, 384);
2007
+ return {
2008
+ setRuntimeState: (key, value, updatedAtMs = Date.now()) => {
2009
+ setRuntimeState.run(key, JSON.stringify(value), updatedAtMs);
2010
+ },
2011
+ recordSyncRun: (record) => {
2012
+ recordSyncRun.run(
2013
+ record.startedAtMs,
2014
+ record.completedAtMs,
2015
+ record.environment ?? null,
2016
+ record.outcome,
2017
+ record.uploadedBytes,
2018
+ record.failed,
2019
+ record.error ?? null
2020
+ );
2021
+ trimSyncRuns.run(MAX_SYNC_RUN_RECORDS);
2022
+ },
2023
+ captureHookEvent: (envelope) => {
2024
+ requireBoundedString(
2025
+ envelope.syncId,
2026
+ "Hook event sync ID",
2027
+ MAX_HOOK_SYNC_ID_BYTES
2028
+ );
2029
+ requireBoundedString(
2030
+ envelope.sessionId,
2031
+ "Hook event session ID",
2032
+ MAX_HOOK_SESSION_ID_BYTES
2033
+ );
2034
+ requireBoundedString(
2035
+ envelope.eventType,
2036
+ "Hook event type",
2037
+ MAX_HOOK_EVENT_TYPE_BYTES
2038
+ );
2039
+ requireBoundedString(
2040
+ envelope.target,
2041
+ "Hook event target",
2042
+ MAX_HOOK_TARGET_BYTES
2043
+ );
2044
+ requireTimestamp(envelope.timestampMs, "Hook event timestamp");
2045
+ const capturedAtMs = envelope.capturedAtMs ?? Date.now();
2046
+ requireTimestamp(capturedAtMs, "Hook event capture timestamp");
2047
+ const payloadJson = jsonStringify(
2048
+ envelope.payload,
2049
+ "Hook event payload"
2050
+ );
2051
+ if (Buffer.byteLength(payloadJson, "utf8") > MAX_HOOK_PAYLOAD_BYTES) {
2052
+ throw new Error(
2053
+ `Hook event payload exceeds ${MAX_HOOK_PAYLOAD_BYTES} bytes`
2054
+ );
2055
+ }
2056
+ validateOptionalHookField(envelope.cwd, "Hook event cwd");
2057
+ validateOptionalHookField(envelope.repository, "Hook event repository");
2058
+ validateOptionalHookField(envelope.toolName, "Hook event tool name");
2059
+ validateOptionalHookField(
2060
+ envelope.userPrompt,
2061
+ "Hook event user prompt"
2062
+ );
2063
+ validateOptionalHookField(envelope.filePath, "Hook event file path");
2064
+ validateOptionalHookField(envelope.command, "Hook event command");
2065
+ validateOptionalHookField(envelope.plan, "Hook event plan");
2066
+ validateOptionalHookField(
2067
+ envelope.allowedPrompts,
2068
+ "Hook event allowed prompts"
2069
+ );
2070
+ validateOptionalHookField(
2071
+ envelope.toolResult,
2072
+ "Hook event tool result"
2073
+ );
2074
+ if (envelope.destination) {
2075
+ validateHookDestination(envelope.destination);
2076
+ }
2077
+ const logicalBytes = hookOutboxLogicalBytes(envelope, payloadJson);
2078
+ const captureResult = runImmediateWithSpoolRepair(() => {
2079
+ enforceHookQuarantineAge(Date.now());
2080
+ if (findPendingHookEvent.get(envelope.syncId) || findQuarantinedHookEvent.get(envelope.syncId)) {
2081
+ return "duplicate";
2082
+ }
2083
+ const evictedAtMs = Date.now();
2084
+ if (maxHookSpoolBytes !== void 0 && logicalBytes > maxHookSpoolBytes) {
2085
+ recordOversizedHookCapture.run(logicalBytes, evictedAtMs);
2086
+ return "too_large";
2087
+ }
2088
+ enforceHookSpoolLimit(evictedAtMs, logicalBytes);
2089
+ captureHookEvent.run(
2090
+ envelope.syncId,
2091
+ envelope.sessionId,
2092
+ envelope.eventType,
2093
+ envelope.timestampMs,
2094
+ envelope.target,
2095
+ payloadJson,
2096
+ envelope.cwd ?? null,
2097
+ envelope.repository ?? null,
2098
+ envelope.toolName ?? null,
2099
+ envelope.userPrompt ?? null,
2100
+ envelope.filePath ?? null,
2101
+ envelope.command ?? null,
2102
+ envelope.plan ?? null,
2103
+ envelope.allowedPrompts ?? null,
2104
+ envelope.toolResult ?? null,
2105
+ capturedAtMs,
2106
+ envelope.destination?.targetUrl ?? null,
2107
+ envelope.destination?.destinationIdentity ?? null,
2108
+ logicalBytes
2109
+ );
2110
+ adjustHookSpoolBytes.run(logicalBytes);
2111
+ return "captured";
2112
+ });
2113
+ if (captureResult === "too_large" && maxHookSpoolBytes !== void 0) {
2114
+ throw new ObserverHookSpoolEntryTooLargeError(
2115
+ logicalBytes,
2116
+ maxHookSpoolBytes
2117
+ );
2118
+ }
2119
+ return captureResult === "captured";
2120
+ },
2121
+ listPendingHookEvents: (limit = DEFAULT_HOOK_OUTBOX_BATCH_SIZE) => {
2122
+ validateHookBatchLimit(limit);
2123
+ return listPendingHookEvents.all(limit).map(hookOutboxRecordFromRow);
2124
+ },
2125
+ claimPendingHookEvents: (destination, limit = DEFAULT_HOOK_OUTBOX_BATCH_SIZE, eligibleAtMs = Date.now()) => {
2126
+ validateHookDestination(destination);
2127
+ validateHookBatchLimit(limit);
2128
+ requireTimestamp(eligibleAtMs, "Hook event eligibility timestamp");
2129
+ return runImmediateWithSpoolRepair(() => {
2130
+ const destinationBytes = utf8Bytes(destination.targetUrl) + utf8Bytes(destination.destinationIdentity);
2131
+ const claimed = claimUnboundHookEvents.run(
2132
+ destination.targetUrl,
2133
+ destination.destinationIdentity,
2134
+ destinationBytes
2135
+ ).changes;
2136
+ if (claimed > 0) {
2137
+ adjustHookSpoolBytes.run(destinationBytes * claimed);
2138
+ enforceHookSpoolLimit(eligibleAtMs);
2139
+ }
2140
+ return listDestinationPendingHookEvents.all(
2141
+ destination.targetUrl,
2142
+ destination.destinationIdentity,
2143
+ eligibleAtMs,
2144
+ eligibleAtMs,
2145
+ limit
2146
+ ).map(hookOutboxRecordFromRow);
2147
+ });
2148
+ },
2149
+ deferHookEvents: (destination, syncIds, nextAttemptAtMs) => {
2150
+ validateHookDestination(destination);
2151
+ requireTimestamp(nextAttemptAtMs, "Hook event next-attempt timestamp");
2152
+ const uniqueSyncIds = [...new Set(syncIds)];
2153
+ if (uniqueSyncIds.length === 0) return 0;
2154
+ if (uniqueSyncIds.length > MAX_HOOK_OUTBOX_BATCH_SIZE) {
2155
+ throw new Error(
2156
+ `Cannot defer more than ${MAX_HOOK_OUTBOX_BATCH_SIZE} hook events at once`
2157
+ );
2158
+ }
2159
+ for (const syncId of uniqueSyncIds) {
2160
+ requireBoundedString(
2161
+ syncId,
2162
+ "Hook event sync ID",
2163
+ MAX_HOOK_SYNC_ID_BYTES
2164
+ );
2165
+ }
2166
+ const placeholders = uniqueSyncIds.map(() => "?").join(", ");
2167
+ const defer = database.prepare(`
2168
+ UPDATE hook_event_outbox
2169
+ SET
2170
+ retry_count = retry_count + 1,
2171
+ next_attempt_at_ms = ?
2172
+ WHERE destination_url = ?
2173
+ AND destination_identity = ?
2174
+ AND sync_id IN (${placeholders})
2175
+ `);
2176
+ return runImmediateTransaction(
2177
+ () => defer.run(
2178
+ nextAttemptAtMs,
2179
+ destination.targetUrl,
2180
+ destination.destinationIdentity,
2181
+ ...uniqueSyncIds
2182
+ ).changes
2183
+ );
2184
+ },
2185
+ nextPendingHookAttemptAtMs: (destination) => {
2186
+ validateHookDestination(destination);
2187
+ const value = nextDestinationHookAttempt.get(
2188
+ destination.targetUrl,
2189
+ destination.destinationIdentity
2190
+ )?.next_attempt_at_ms;
2191
+ if (value === null || value === void 0) return void 0;
2192
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
2193
+ throw new Error(
2194
+ "FML observer hook next-attempt timestamp is invalid"
2195
+ );
2196
+ }
2197
+ return value;
2198
+ },
2199
+ countPendingHookEvents: () => {
2200
+ const count = countPendingHookEvents.get()?.count;
2201
+ if (typeof count !== "number" || !Number.isSafeInteger(count)) {
2202
+ throw new Error("FML observer hook outbox count is invalid");
2203
+ }
2204
+ return count;
2205
+ },
2206
+ acknowledgeHookEvents: (destination, syncIds) => {
2207
+ validateHookDestination(destination);
2208
+ const uniqueSyncIds = [...new Set(syncIds)];
2209
+ if (uniqueSyncIds.length === 0) return 0;
2210
+ if (uniqueSyncIds.length > MAX_HOOK_OUTBOX_BATCH_SIZE) {
2211
+ throw new Error(
2212
+ `Cannot acknowledge more than ${MAX_HOOK_OUTBOX_BATCH_SIZE} hook events at once`
2213
+ );
2214
+ }
2215
+ for (const syncId of uniqueSyncIds) {
2216
+ requireBoundedString(
2217
+ syncId,
2218
+ "Hook event sync ID",
2219
+ MAX_HOOK_SYNC_ID_BYTES
2220
+ );
2221
+ }
2222
+ const placeholders = uniqueSyncIds.map(() => "?").join(", ");
2223
+ const acknowledge = database.prepare(`
2224
+ DELETE FROM hook_event_outbox
2225
+ WHERE destination_url = ?
2226
+ AND destination_identity = ?
2227
+ AND sync_id IN (${placeholders})
2228
+ `);
2229
+ const acknowledgedBytes = database.prepare(`
2230
+ SELECT COALESCE(SUM(logical_bytes), 0) AS logical_bytes
2231
+ FROM hook_event_outbox
2232
+ WHERE destination_url = ?
2233
+ AND destination_identity = ?
2234
+ AND sync_id IN (${placeholders})
2235
+ `);
2236
+ return runImmediateWithSpoolRepair(() => {
2237
+ const values = [
2238
+ destination.targetUrl,
2239
+ destination.destinationIdentity,
2240
+ ...uniqueSyncIds
2241
+ ];
2242
+ const logicalBytes = requireSpoolInteger(
2243
+ acknowledgedBytes.get(...values),
2244
+ "logical_bytes"
2245
+ );
2246
+ const changes = acknowledge.run(...values).changes;
2247
+ if (changes > 0) adjustHookSpoolBytes.run(-logicalBytes);
2248
+ return changes;
2249
+ });
2250
+ },
2251
+ quarantineHookEvents: (destination, rejections, quarantinedAtMs = Date.now()) => {
2252
+ validateHookDestination(destination);
2253
+ const uniqueRejections = [
2254
+ ...new Map(
2255
+ rejections.map((rejection) => [rejection.syncId, rejection])
2256
+ ).values()
2257
+ ];
2258
+ if (uniqueRejections.length === 0) return 0;
2259
+ if (uniqueRejections.length > MAX_HOOK_OUTBOX_BATCH_SIZE) {
2260
+ throw new Error(
2261
+ `Cannot quarantine more than ${MAX_HOOK_OUTBOX_BATCH_SIZE} hook events at once`
2262
+ );
2263
+ }
2264
+ requireTimestamp(quarantinedAtMs, "Hook event quarantine timestamp");
2265
+ for (const rejection of uniqueRejections) {
2266
+ requireBoundedString(
2267
+ rejection.syncId,
2268
+ "Hook event sync ID",
2269
+ MAX_HOOK_SYNC_ID_BYTES
2270
+ );
2271
+ requireBoundedString(
2272
+ rejection.reason,
2273
+ "Hook event rejection reason",
2274
+ MAX_HOOK_REJECTION_REASON_BYTES
2275
+ );
2276
+ }
2277
+ return runImmediateWithSpoolRepair(() => {
2278
+ let quarantined = 0;
2279
+ for (const rejection of uniqueRejections) {
2280
+ const reasonBytes = utf8Bytes(rejection.reason);
2281
+ const inserted = quarantineHookEvent.run(
2282
+ rejection.reason,
2283
+ quarantinedAtMs,
2284
+ reasonBytes,
2285
+ destination.targetUrl,
2286
+ destination.destinationIdentity,
2287
+ rejection.syncId
2288
+ ).changes;
2289
+ if (inserted === 0) continue;
2290
+ quarantined += deletePendingHookEvent.run(
2291
+ destination.targetUrl,
2292
+ destination.destinationIdentity,
2293
+ rejection.syncId
2294
+ ).changes;
2295
+ adjustHookSpoolBytes.run(reasonBytes);
2296
+ }
2297
+ enforceHookQuarantineAge(Date.now(), true);
2298
+ enforceHookSpoolLimit(quarantinedAtMs);
2299
+ return quarantined;
2300
+ });
2301
+ },
2302
+ countQuarantinedHookEvents: () => {
2303
+ if (maxHookQuarantineAgeMs !== void 0) {
2304
+ runImmediateTransaction(() => enforceHookQuarantineAge(Date.now()));
2305
+ }
2306
+ const count = countQuarantinedHookEvents.get()?.count;
2307
+ if (typeof count !== "number" || !Number.isSafeInteger(count)) {
2308
+ throw new Error("FML observer hook quarantine count is invalid");
2309
+ }
2310
+ return count;
2311
+ },
2312
+ getHookSpoolStatus: () => {
2313
+ const readStatus = () => {
2314
+ const row = readHookSpoolState();
2315
+ const lastEvictedAtMs = row?.last_evicted_at_ms;
2316
+ if (lastEvictedAtMs !== null && lastEvictedAtMs !== void 0 && (typeof lastEvictedAtMs !== "number" || !Number.isSafeInteger(lastEvictedAtMs) || lastEvictedAtMs < 0 || lastEvictedAtMs > MAX_TIMESTAMP_MS)) {
2317
+ throw new ObserverHookSpoolStateInvariantError(
2318
+ "FML observer hook spool eviction timestamp is invalid"
2319
+ );
2320
+ }
2321
+ return {
2322
+ unit: "logical_bytes",
2323
+ maxBytes: maxHookSpoolBytes ?? null,
2324
+ quarantineMaxAgeMs: maxHookQuarantineAgeMs ?? null,
2325
+ logicalBytes: requireSpoolInteger(row, "logical_bytes"),
2326
+ evictedRecords: requireSpoolInteger(row, "evicted_records"),
2327
+ evictedBytes: requireSpoolInteger(row, "evicted_bytes"),
2328
+ evictedPendingRecords: requireSpoolInteger(
2329
+ row,
2330
+ "evicted_pending_records"
2331
+ ),
2332
+ evictedQuarantinedRecords: requireSpoolInteger(
2333
+ row,
2334
+ "evicted_quarantined_records"
2335
+ ),
2336
+ ...typeof lastEvictedAtMs === "number" ? { lastEvictedAtMs } : {}
2337
+ };
2338
+ };
2339
+ try {
2340
+ return readStatus();
2341
+ } catch (error) {
2342
+ if (!(error instanceof ObserverHookSpoolStateInvariantError)) {
2343
+ throw error;
2344
+ }
2345
+ runImmediateTransaction(() => reconcileHookSpoolState.run());
2346
+ return readStatus();
2347
+ }
2348
+ },
2349
+ captureOtlpEnvelope: (envelope) => {
2350
+ if (envelope.envelopeId !== void 0) {
2351
+ requireBoundedString(
2352
+ envelope.envelopeId,
2353
+ "OTLP envelope ID",
2354
+ MAX_OTLP_ENVELOPE_ID_BYTES
2355
+ );
2356
+ }
2357
+ requireBoundedString(
2358
+ envelope.observerInstanceId,
2359
+ "OTLP observer instance ID",
2360
+ MAX_OTLP_OBSERVER_INSTANCE_ID_BYTES
2361
+ );
2362
+ requireBoundedString(
2363
+ envelope.observerVersion,
2364
+ "OTLP observer version",
2365
+ MAX_OTLP_OBSERVER_VERSION_BYTES
2366
+ );
2367
+ const signal = requireOtlpSignal(envelope.signal);
2368
+ const contentType = requireOtlpContentType(envelope.contentType);
2369
+ const contentEncoding = requireOtlpContentEncoding(
2370
+ envelope.contentEncoding
2371
+ );
2372
+ if (!Buffer.isBuffer(envelope.body)) {
2373
+ throw new Error("OTLP envelope body must be a Buffer");
2374
+ }
2375
+ if (envelope.body.byteLength > MAX_OTLP_ENVELOPE_BYTES) {
2376
+ throw new Error(
2377
+ `OTLP envelope body exceeds ${MAX_OTLP_ENVELOPE_BYTES} bytes`
2378
+ );
2379
+ }
2380
+ if (envelope.sha256 !== void 0 && !/^[0-9a-f]{64}$/.test(envelope.sha256)) {
2381
+ throw new Error(
2382
+ "OTLP envelope SHA-256 must be 64 lowercase hexadecimal characters"
2383
+ );
2384
+ }
2385
+ const capturedAtMs = envelope.capturedAtMs ?? Date.now();
2386
+ requireTimestamp(capturedAtMs, "OTLP envelope capture timestamp");
2387
+ if (envelope.destination) {
2388
+ validateOtlpDestination(envelope.destination);
2389
+ }
2390
+ return runImmediateTransaction(() => {
2391
+ let envelopeId = envelope.envelopeId;
2392
+ if (envelopeId === void 0) {
2393
+ for (let attempt = 0; attempt < 8; attempt += 1) {
2394
+ const candidate = crypto2.randomUUID();
2395
+ if (!findPendingOtlpEnvelope.get(candidate)) {
2396
+ envelopeId = candidate;
2397
+ break;
2398
+ }
2399
+ }
2400
+ if (envelopeId === void 0) {
2401
+ throw new Error("Unable to allocate a unique OTLP envelope ID");
2402
+ }
2403
+ } else if (envelope.destination !== void 0 && findPendingOtlpEnvelope.get(envelopeId)) {
2404
+ return null;
2405
+ }
2406
+ const logicalBytes = otlpEnvelopeLogicalBytes(envelope, envelopeId);
2407
+ const actualSha256 = crypto2.createHash("sha256").update(envelope.body).digest("hex");
2408
+ if (envelope.sha256 !== void 0 && actualSha256 !== envelope.sha256) {
2409
+ throw new Error("OTLP envelope SHA-256 does not match its body");
2410
+ }
2411
+ if (envelope.destination === void 0) {
2412
+ recordOtlpDrop(
2413
+ 1,
2414
+ logicalBytes,
2415
+ envelope.body.byteLength,
2416
+ capturedAtMs,
2417
+ OWNERLESS_OTLP_DROP_REASON
2418
+ );
2419
+ return null;
2420
+ }
2421
+ if (logicalBytes > maxOtlpSpoolBytes) {
2422
+ recordOtlpDrop(
2423
+ 1,
2424
+ logicalBytes,
2425
+ envelope.body.byteLength,
2426
+ capturedAtMs,
2427
+ OTLP_ENTRY_TOO_LARGE_DROP_REASON
2428
+ );
2429
+ return null;
2430
+ }
2431
+ const retainedBytes = requireSpoolInteger(
2432
+ getOtlpSpoolState.get(),
2433
+ "logical_bytes"
2434
+ );
2435
+ const bytesToFree = retainedBytes + logicalBytes - maxOtlpSpoolBytes;
2436
+ if (bytesToFree > 0) {
2437
+ const plan = otlpCapacityEvictionPlan.get(bytesToFree);
2438
+ const evictedRecords = requireSpoolInteger(plan, "record_count");
2439
+ const evictedLogicalBytes = requireSpoolInteger(
2440
+ plan,
2441
+ "logical_bytes"
2442
+ );
2443
+ const evictedBodyBytes = requireSpoolInteger(plan, "body_bytes");
2444
+ if (evictedRecords === 0 || evictedLogicalBytes < bytesToFree || evictOldestPendingOtlpEnvelopes.run(evictedRecords).changes !== evictedRecords) {
2445
+ throw new Error(
2446
+ "FML observer OTLP spool accounting could not enforce its limit"
2447
+ );
2448
+ }
2449
+ recordOtlpDrop(
2450
+ evictedRecords,
2451
+ evictedLogicalBytes,
2452
+ evictedBodyBytes,
2453
+ capturedAtMs,
2454
+ OTLP_CAPACITY_DROP_REASON
2455
+ );
2456
+ }
2457
+ captureOtlpEnvelope.run(
2458
+ envelopeId,
2459
+ envelope.observerInstanceId,
2460
+ envelope.observerVersion,
2461
+ signal,
2462
+ contentType,
2463
+ contentEncoding,
2464
+ envelope.body,
2465
+ actualSha256,
2466
+ capturedAtMs,
2467
+ envelope.destination.targetUrl,
2468
+ envelope.destination.destinationIdentity,
2469
+ envelope.destination.organizationId ?? null,
2470
+ envelope.destination.organizationSlug ?? null,
2471
+ logicalBytes
2472
+ );
2473
+ return envelopeId;
2474
+ });
2475
+ },
2476
+ listPendingOtlpEnvelopes: (limit = DEFAULT_OTLP_OUTBOX_BATCH_SIZE) => {
2477
+ validateOtlpBatchLimit(limit);
2478
+ return listPendingOtlpEnvelopes.all(limit).map(otlpOutboxRecordFromRow);
2479
+ },
2480
+ claimPendingOtlpEnvelopes: (destination, limit = DEFAULT_OTLP_OUTBOX_BATCH_SIZE, eligibleAtMs = Date.now()) => {
2481
+ validateOtlpDestination(destination);
2482
+ validateOtlpBatchLimit(limit);
2483
+ requireTimestamp(eligibleAtMs, "OTLP envelope eligibility timestamp");
2484
+ return runImmediateTransaction(() => {
2485
+ if (destination.organizationId !== void 0) {
2486
+ claimOrganizationForOtlpEnvelopes.run(
2487
+ destination.organizationId,
2488
+ destination.organizationSlug,
2489
+ destination.targetUrl,
2490
+ destination.destinationIdentity
2491
+ );
2492
+ }
2493
+ return listDestinationPendingOtlpEnvelopes.all(
2494
+ destination.targetUrl,
2495
+ destination.destinationIdentity,
2496
+ destination.organizationId ?? null,
2497
+ eligibleAtMs,
2498
+ limit
2499
+ ).map(otlpOutboxRecordFromRow);
2500
+ });
2501
+ },
2502
+ deferOtlpEnvelopes: (destination, envelopeIds, nextAttemptAtMs) => {
2503
+ validateOtlpDestination(destination);
2504
+ requireTimestamp(
2505
+ nextAttemptAtMs,
2506
+ "OTLP envelope next-attempt timestamp"
2507
+ );
2508
+ const uniqueEnvelopeIds = [...new Set(envelopeIds)];
2509
+ if (uniqueEnvelopeIds.length === 0) return 0;
2510
+ if (uniqueEnvelopeIds.length > MAX_OTLP_OUTBOX_BATCH_SIZE) {
2511
+ throw new Error(
2512
+ `Cannot defer more than ${MAX_OTLP_OUTBOX_BATCH_SIZE} OTLP envelopes at once`
2513
+ );
2514
+ }
2515
+ for (const envelopeId of uniqueEnvelopeIds) {
2516
+ requireBoundedString(
2517
+ envelopeId,
2518
+ "OTLP envelope ID",
2519
+ MAX_OTLP_ENVELOPE_ID_BYTES
2520
+ );
2521
+ }
2522
+ return runImmediateTransaction(() => {
2523
+ let deferred = 0;
2524
+ for (const envelopeId of uniqueEnvelopeIds) {
2525
+ deferred += deferDestinationPendingOtlpEnvelope.run(
2526
+ nextAttemptAtMs,
2527
+ destination.targetUrl,
2528
+ destination.destinationIdentity,
2529
+ destination.organizationId ?? null,
2530
+ envelopeId
2531
+ ).changes;
2532
+ }
2533
+ return deferred;
2534
+ });
2535
+ },
2536
+ nextPendingOtlpAttemptAtMs: (destination) => {
2537
+ validateOtlpDestination(destination);
2538
+ const value = nextDestinationPendingOtlpAttempt.get(
2539
+ destination.targetUrl,
2540
+ destination.destinationIdentity,
2541
+ destination.organizationId ?? null
2542
+ )?.next_attempt_at_ms;
2543
+ if (value === null || value === void 0) return void 0;
2544
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
2545
+ throw new Error(
2546
+ "FML observer OTLP outbox next-attempt timestamp is invalid"
2547
+ );
2548
+ }
2549
+ return value;
2550
+ },
2551
+ countPendingOtlpEnvelopes: () => {
2552
+ const count = countPendingOtlpEnvelopes.get()?.count;
2553
+ if (typeof count !== "number" || !Number.isSafeInteger(count)) {
2554
+ throw new Error("FML observer OTLP outbox count is invalid");
2555
+ }
2556
+ return count;
2557
+ },
2558
+ acknowledgeOtlpEnvelopes: (destination, envelopeIds) => {
2559
+ validateOtlpDestination(destination);
2560
+ const uniqueEnvelopeIds = [...new Set(envelopeIds)];
2561
+ if (uniqueEnvelopeIds.length === 0) return 0;
2562
+ if (uniqueEnvelopeIds.length > MAX_OTLP_OUTBOX_BATCH_SIZE) {
2563
+ throw new Error(
2564
+ `Cannot acknowledge more than ${MAX_OTLP_OUTBOX_BATCH_SIZE} OTLP envelopes at once`
2565
+ );
2566
+ }
2567
+ for (const envelopeId of uniqueEnvelopeIds) {
2568
+ requireBoundedString(
2569
+ envelopeId,
2570
+ "OTLP envelope ID",
2571
+ MAX_OTLP_ENVELOPE_ID_BYTES
2572
+ );
2573
+ }
2574
+ const placeholders = uniqueEnvelopeIds.map(() => "?").join(", ");
2575
+ const acknowledge = database.prepare(`
2576
+ DELETE FROM otlp_envelope_outbox
2577
+ WHERE destination_url = ?
2578
+ AND destination_identity = ?
2579
+ AND destination_organization_id IS ?
2580
+ AND envelope_id IN (${placeholders})
2581
+ `);
2582
+ return runImmediateTransaction(
2583
+ () => acknowledge.run(
2584
+ destination.targetUrl,
2585
+ destination.destinationIdentity,
2586
+ destination.organizationId ?? null,
2587
+ ...uniqueEnvelopeIds
2588
+ ).changes
2589
+ );
2590
+ },
2591
+ dropOtlpEnvelopes: (destination, drops, droppedAtMs = Date.now()) => {
2592
+ validateOtlpDestination(destination);
2593
+ const uniqueDrops = [
2594
+ ...new Map(drops.map((drop) => [drop.envelopeId, drop])).values()
2595
+ ];
2596
+ if (uniqueDrops.length === 0) return 0;
2597
+ if (uniqueDrops.length > MAX_OTLP_OUTBOX_BATCH_SIZE) {
2598
+ throw new Error(
2599
+ `Cannot drop more than ${MAX_OTLP_OUTBOX_BATCH_SIZE} OTLP envelopes at once`
2600
+ );
2601
+ }
2602
+ requireTimestamp(droppedAtMs, "OTLP drop timestamp");
2603
+ for (const drop of uniqueDrops) {
2604
+ requireBoundedString(
2605
+ drop.envelopeId,
2606
+ "OTLP envelope ID",
2607
+ MAX_OTLP_ENVELOPE_ID_BYTES
2608
+ );
2609
+ requireBoundedString(
2610
+ drop.reason,
2611
+ "OTLP drop reason",
2612
+ MAX_OTLP_DROP_REASON_BYTES
2613
+ );
2614
+ }
2615
+ return runImmediateTransaction(() => {
2616
+ let droppedRecords = 0;
2617
+ let droppedLogicalBytes = 0;
2618
+ let droppedBodyBytes = 0;
2619
+ let lastDropReason = uniqueDrops[0].reason;
2620
+ for (const drop of uniqueDrops) {
2621
+ const row = getDestinationPendingOtlpEnvelopeAccounting.get(
2622
+ destination.targetUrl,
2623
+ destination.destinationIdentity,
2624
+ destination.organizationId ?? null,
2625
+ drop.envelopeId
2626
+ );
2627
+ if (row === void 0) continue;
2628
+ const logicalBytes = requireSpoolInteger(row, "logical_bytes");
2629
+ const bodyBytes = requireSpoolInteger(row, "body_bytes");
2630
+ const deleted = deleteDestinationPendingOtlpEnvelope.run(
2631
+ destination.targetUrl,
2632
+ destination.destinationIdentity,
2633
+ destination.organizationId ?? null,
2634
+ drop.envelopeId
2635
+ ).changes;
2636
+ if (deleted !== 1) {
2637
+ throw new Error("FML observer OTLP fenced drop was incomplete");
2638
+ }
2639
+ droppedRecords += 1;
2640
+ droppedLogicalBytes += logicalBytes;
2641
+ droppedBodyBytes += bodyBytes;
2642
+ lastDropReason = drop.reason;
2643
+ }
2644
+ recordOtlpDrop(
2645
+ droppedRecords,
2646
+ droppedLogicalBytes,
2647
+ droppedBodyBytes,
2648
+ droppedAtMs,
2649
+ lastDropReason
2650
+ );
2651
+ return droppedRecords;
2652
+ });
2653
+ },
2654
+ getOtlpSpoolStatus: () => {
2655
+ const row = getOtlpSpoolState.get();
2656
+ const lastDroppedAtMs = row?.last_dropped_at_ms;
2657
+ if (lastDroppedAtMs !== null && lastDroppedAtMs !== void 0 && (typeof lastDroppedAtMs !== "number" || !Number.isSafeInteger(lastDroppedAtMs) || lastDroppedAtMs < 0)) {
2658
+ throw new Error("FML observer OTLP drop timestamp is invalid");
2659
+ }
2660
+ const lastDropReason = row?.last_drop_reason;
2661
+ if (lastDropReason !== null && lastDropReason !== void 0 && typeof lastDropReason !== "string") {
2662
+ throw new Error("FML observer OTLP drop reason is invalid");
2663
+ }
2664
+ return {
2665
+ unit: "logical_bytes",
2666
+ maxBytes: maxOtlpSpoolBytes,
2667
+ logicalBytes: requireSpoolInteger(row, "logical_bytes"),
2668
+ bodyBytes: requireSpoolInteger(row, "body_bytes"),
2669
+ droppedRecords: requireSpoolInteger(row, "dropped_records"),
2670
+ droppedLogicalBytes: requireSpoolInteger(
2671
+ row,
2672
+ "dropped_logical_bytes"
2673
+ ),
2674
+ droppedBodyBytes: requireSpoolInteger(row, "dropped_body_bytes"),
2675
+ ...typeof lastDroppedAtMs === "number" ? { lastDroppedAtMs } : {},
2676
+ ...typeof lastDropReason === "string" ? { lastDropReason } : {}
2677
+ };
2678
+ },
2679
+ close: () => database.close()
2680
+ };
2681
+ } catch (error) {
2682
+ try {
2683
+ database.exec("ROLLBACK");
2684
+ } catch {
2685
+ }
2686
+ database.close();
2687
+ throw error;
2688
+ }
2689
+ }
2690
+
2691
+ // ../../packages/panopticon/src/observer/protocol.ts
2692
+ var OBSERVER_PROTOCOL_VERSION = 1;
2693
+ var OBSERVER_SERVICE_NAME = "fml-observer";
2694
+ function isObserverHealth(value) {
2695
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2696
+ const record = value;
2697
+ return record.status === "ok" && record.service === OBSERVER_SERVICE_NAME && record.protocolVersion === OBSERVER_PROTOCOL_VERSION && typeof record.instanceId === "string" && record.instanceId.length > 0 && typeof record.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && typeof record.port === "number" && Number.isInteger(record.port) && record.port > 0 && typeof record.version === "string" && (record.capabilities === void 0 || Array.isArray(record.capabilities) && record.capabilities.every(
2698
+ (capability2) => typeof capability2 === "string"
2699
+ ));
2700
+ }
2701
+
2702
+ // ../../packages/panopticon/src/observer/server.ts
2703
+ import http from "http";
2704
+ var MAX_HOOK_REQUEST_BODY_BYTES = 4 * 1024 * 1024;
2705
+ var LOOPBACK_HOST_HEADER_PATTERN = /^(?:127\.0\.0\.1|localhost|\[::1\])(?::(\d{1,5}))?$/iu;
2706
+ var HookRequestBodyError = class extends Error {
2707
+ constructor(status, code) {
2708
+ super(code);
2709
+ this.status = status;
2710
+ this.code = code;
2711
+ }
2712
+ };
2713
+ var OtlpRequestBodyError = class extends Error {
2714
+ constructor(status, rpcCode, message) {
2715
+ super(message);
2716
+ this.status = status;
2717
+ this.rpcCode = rpcCode;
2718
+ }
2719
+ };
2720
+ function isAllowedObserverHost(host) {
2721
+ if (host === void 0) return false;
2722
+ const match = LOOPBACK_HOST_HEADER_PATTERN.exec(host);
2723
+ if (!match) return false;
2724
+ const port = match[1];
2725
+ if (port === void 0) return true;
2726
+ const numericPort = Number(port);
2727
+ return numericPort > 0 && numericPort <= 65535;
2728
+ }
2729
+ function writeJson(res, status, body) {
2730
+ const payload = JSON.stringify(body);
2731
+ res.writeHead(status, { "Content-Type": "application/json" });
2732
+ res.end(payload);
2733
+ }
2734
+ function readHookRequestBody(req) {
2735
+ const contentLengthHeader = req.headers["content-length"];
2736
+ if (contentLengthHeader !== void 0) {
2737
+ if (!/^\d+$/.test(contentLengthHeader)) {
2738
+ throw new HookRequestBodyError(400, "invalid_json");
2739
+ }
2740
+ if (Number(contentLengthHeader) > MAX_HOOK_REQUEST_BODY_BYTES) {
2741
+ req.resume();
2742
+ throw new HookRequestBodyError(413, "payload_too_large");
2743
+ }
2744
+ }
2745
+ return new Promise((resolve, reject) => {
2746
+ const chunks = [];
2747
+ let byteCount = 0;
2748
+ let settled = false;
2749
+ const rejectOnce = (error) => {
2750
+ if (settled) return;
2751
+ settled = true;
2752
+ chunks.length = 0;
2753
+ reject(error);
2754
+ };
2755
+ req.on("data", (chunk) => {
2756
+ if (settled) return;
2757
+ byteCount += chunk.byteLength;
2758
+ if (byteCount > MAX_HOOK_REQUEST_BODY_BYTES) {
2759
+ rejectOnce(new HookRequestBodyError(413, "payload_too_large"));
2760
+ req.resume();
2761
+ return;
2762
+ }
2763
+ chunks.push(chunk);
2764
+ });
2765
+ req.on("end", () => {
2766
+ if (settled) return;
2767
+ settled = true;
2768
+ try {
2769
+ resolve(JSON.parse(Buffer.concat(chunks, byteCount).toString("utf8")));
2770
+ } catch {
2771
+ reject(new HookRequestBodyError(400, "invalid_json"));
2772
+ }
2773
+ });
2774
+ req.on("error", (error) => {
2775
+ rejectOnce(error);
2776
+ });
2777
+ req.on("aborted", () => rejectOnce(new Error("hook request aborted")));
2778
+ });
2779
+ }
2780
+ function getOtlpRoute(pathname) {
2781
+ const directSignal = pathname.startsWith("/v1/") ? pathname.slice("/v1/".length) : void 0;
2782
+ if (directSignal === "logs" || directSignal === "metrics" || directSignal === "traces") {
2783
+ return { signal: directSignal };
2784
+ }
2785
+ const segments = pathname.split("/");
2786
+ if (segments.length === 5 && segments[0] === "" && segments[1] === "otel" && segments[3] === "v1" && (segments[4] === "logs" || segments[4] === "metrics" || segments[4] === "traces")) {
2787
+ try {
2788
+ return {
2789
+ signal: segments[4],
2790
+ pathToken: decodeURIComponent(segments[2] ?? "")
2791
+ };
2792
+ } catch {
2793
+ return void 0;
2794
+ }
2795
+ }
2796
+ return void 0;
2797
+ }
2798
+ function parseOtlpContentType(header) {
2799
+ const mediaType = header?.split(";", 1)[0]?.trim().toLowerCase();
2800
+ if (mediaType === "application/json" || mediaType === "application/x-protobuf") {
2801
+ return mediaType;
2802
+ }
2803
+ return void 0;
2804
+ }
2805
+ function parseOtlpContentEncoding(header) {
2806
+ if (header === void 0) return "identity";
2807
+ const encoding = header.trim().toLowerCase();
2808
+ if (encoding === "identity" || encoding === "gzip") return encoding;
2809
+ return void 0;
2810
+ }
2811
+ function encodeVarint(value) {
2812
+ const bytes = [];
2813
+ let remaining = value;
2814
+ do {
2815
+ let byte = remaining & 127;
2816
+ remaining >>>= 7;
2817
+ if (remaining > 0) byte |= 128;
2818
+ bytes.push(byte);
2819
+ } while (remaining > 0);
2820
+ return Buffer.from(bytes);
2821
+ }
2822
+ function encodeProtobufStatus(code, message) {
2823
+ const messageBytes = Buffer.from(message, "utf8");
2824
+ return Buffer.concat([
2825
+ Buffer.from([8]),
2826
+ encodeVarint(code),
2827
+ Buffer.from([18]),
2828
+ encodeVarint(messageBytes.byteLength),
2829
+ messageBytes
2830
+ ]);
2831
+ }
2832
+ function writeOtlpError(res, status, contentType, rpcCode, message, extraHeaders = {}) {
2833
+ const payload = contentType === "application/x-protobuf" ? encodeProtobufStatus(rpcCode, message) : Buffer.from(JSON.stringify({ code: rpcCode, message }), "utf8");
2834
+ res.writeHead(status, {
2835
+ "Content-Type": contentType,
2836
+ "Content-Length": String(payload.byteLength),
2837
+ ...extraHeaders
2838
+ });
2839
+ res.end(payload);
2840
+ }
2841
+ function writeOtlpSuccess(res, contentType) {
2842
+ const payload = contentType === "application/x-protobuf" ? Buffer.alloc(0) : Buffer.from("{}", "utf8");
2843
+ res.writeHead(200, {
2844
+ "Content-Type": contentType,
2845
+ "Content-Length": String(payload.byteLength)
2846
+ });
2847
+ res.end(payload);
2848
+ }
2849
+ function readOpaqueOtlpBody(req) {
2850
+ const contentLengthHeader = req.headers["content-length"];
2851
+ if (contentLengthHeader !== void 0) {
2852
+ if (!/^\d+$/.test(contentLengthHeader)) {
2853
+ req.resume();
2854
+ throw new OtlpRequestBodyError(400, 3, "invalid content-length");
2855
+ }
2856
+ if (Number(contentLengthHeader) > MAX_OTLP_ENVELOPE_BYTES) {
2857
+ req.resume();
2858
+ throw new OtlpRequestBodyError(413, 8, "OTLP request exceeds 16 MiB");
2859
+ }
2860
+ }
2861
+ return new Promise((resolve, reject) => {
2862
+ const chunks = [];
2863
+ let byteCount = 0;
2864
+ let settled = false;
2865
+ const rejectOnce = (error) => {
2866
+ if (settled) return;
2867
+ settled = true;
2868
+ chunks.length = 0;
2869
+ reject(error);
2870
+ };
2871
+ req.on("data", (chunk) => {
2872
+ if (settled) return;
2873
+ byteCount += chunk.byteLength;
2874
+ if (byteCount > MAX_OTLP_ENVELOPE_BYTES) {
2875
+ req.resume();
2876
+ rejectOnce(
2877
+ new OtlpRequestBodyError(413, 8, "OTLP request exceeds 16 MiB")
2878
+ );
2879
+ return;
2880
+ }
2881
+ chunks.push(chunk);
2882
+ });
2883
+ req.on("end", () => {
2884
+ if (settled) return;
2885
+ settled = true;
2886
+ resolve(Buffer.concat(chunks, byteCount));
2887
+ });
2888
+ req.on("aborted", () => rejectOnce(new Error("OTLP request aborted")));
2889
+ req.on("error", rejectOnce);
2890
+ });
2891
+ }
2892
+ async function handleOtlpRequest(req, res, opts, route, method) {
2893
+ const responseContentType = parseOtlpContentType(req.headers["content-type"]) ?? "application/json";
2894
+ const authenticated = route.pathToken === void 0 ? hasObserverBearerToken(req, opts.authToken) : isObserverAuthToken(route.pathToken, opts.authToken);
2895
+ if (!authenticated) {
2896
+ req.resume();
2897
+ writeOtlpError(res, 401, responseContentType, 16, "unauthorized");
2898
+ return;
2899
+ }
2900
+ if (method !== "POST") {
2901
+ req.resume();
2902
+ writeOtlpError(res, 405, responseContentType, 12, "method not allowed", {
2903
+ Allow: "POST"
2904
+ });
2905
+ return;
2906
+ }
2907
+ const contentType = parseOtlpContentType(req.headers["content-type"]);
2908
+ if (contentType === void 0) {
2909
+ req.resume();
2910
+ writeOtlpError(
2911
+ res,
2912
+ 415,
2913
+ "application/json",
2914
+ 3,
2915
+ "content-type must be application/json or application/x-protobuf"
2916
+ );
2917
+ return;
2918
+ }
2919
+ const contentEncoding = parseOtlpContentEncoding(
2920
+ req.headers["content-encoding"]
2921
+ );
2922
+ if (contentEncoding === void 0) {
2923
+ req.resume();
2924
+ writeOtlpError(
2925
+ res,
2926
+ 415,
2927
+ contentType,
2928
+ 12,
2929
+ "content-encoding must be identity or gzip"
2930
+ );
2931
+ return;
2932
+ }
2933
+ if (opts.onOtlp === void 0) {
2934
+ req.resume();
2935
+ writeOtlpError(res, 503, contentType, 14, "OTLP capture is unavailable", {
2936
+ "Retry-After": "1"
2937
+ });
2938
+ return;
2939
+ }
2940
+ const capturedAtMs = Date.now();
2941
+ let body;
2942
+ try {
2943
+ body = await readOpaqueOtlpBody(req);
2944
+ } catch (error) {
2945
+ if (error instanceof OtlpRequestBodyError) {
2946
+ writeOtlpError(
2947
+ res,
2948
+ error.status,
2949
+ contentType,
2950
+ error.rpcCode,
2951
+ error.message
2952
+ );
2953
+ return;
2954
+ }
2955
+ writeOtlpError(res, 400, contentType, 3, "failed to read OTLP request");
2956
+ return;
2957
+ }
2958
+ try {
2959
+ const health = opts.getHealth();
2960
+ await opts.onOtlp({
2961
+ observerInstanceId: health.instanceId,
2962
+ observerVersion: health.version,
2963
+ signal: route.signal,
2964
+ contentType,
2965
+ contentEncoding,
2966
+ body,
2967
+ capturedAtMs
2968
+ });
2969
+ } catch (error) {
2970
+ console.error("FML observer OTLP capture failed", error);
2971
+ writeOtlpError(res, 503, contentType, 14, "OTLP capture failed", {
2972
+ "Retry-After": "1"
2973
+ });
2974
+ return;
2975
+ }
2976
+ writeOtlpSuccess(res, contentType);
2977
+ }
2978
+ async function handleObserverRequest(req, res, opts) {
2979
+ try {
2980
+ if (!isAllowedObserverHost(req.headers.host)) {
2981
+ writeJson(res, 403, { error: "invalid_host" });
2982
+ return;
2983
+ }
2984
+ const pathname = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
2985
+ const method = req.method ?? "GET";
2986
+ if (pathname === "/health") {
2987
+ if (method !== "GET") {
2988
+ writeJson(res, 405, { error: "method_not_allowed" });
2989
+ return;
2990
+ }
2991
+ writeJson(res, 200, opts.getHealth());
2992
+ return;
2993
+ }
2994
+ const otlpRoute = getOtlpRoute(pathname);
2995
+ if (otlpRoute !== void 0) {
2996
+ await handleOtlpRequest(req, res, opts, otlpRoute, method);
2997
+ return;
2998
+ }
2999
+ const knownProtectedPath = pathname === "/status" || pathname === "/sync/wake" || pathname === "/hooks" || pathname === "/control/stop";
3000
+ if (!knownProtectedPath) {
3001
+ writeJson(res, 404, { error: "not_found" });
3002
+ return;
3003
+ }
3004
+ if (!requireObserverBearerToken(req, res, opts.authToken)) return;
3005
+ if (pathname === "/status") {
3006
+ if (method !== "GET") {
3007
+ writeJson(res, 405, { error: "method_not_allowed" });
3008
+ return;
3009
+ }
3010
+ writeJson(res, 200, await opts.getStatus());
3011
+ return;
3012
+ }
3013
+ if (pathname === "/sync/wake") {
3014
+ if (method !== "POST") {
3015
+ writeJson(res, 405, { error: "method_not_allowed" });
3016
+ return;
3017
+ }
3018
+ opts.onWake();
3019
+ writeJson(res, 202, { ok: true });
3020
+ return;
3021
+ }
3022
+ if (pathname === "/hooks") {
3023
+ if (method !== "POST") {
3024
+ writeJson(res, 405, { error: "method_not_allowed" });
3025
+ return;
3026
+ }
3027
+ let payload;
3028
+ try {
3029
+ payload = await readHookRequestBody(req);
3030
+ } catch (error) {
3031
+ if (error instanceof HookRequestBodyError) {
3032
+ writeJson(res, error.status, { error: error.code });
3033
+ return;
3034
+ }
3035
+ throw error;
3036
+ }
3037
+ await opts.onHook(payload);
3038
+ writeJson(res, 202, {});
3039
+ return;
3040
+ }
3041
+ if (method !== "POST") {
3042
+ writeJson(res, 405, { error: "method_not_allowed" });
3043
+ return;
3044
+ }
3045
+ writeJson(res, 202, { ok: true });
3046
+ setImmediate(() => {
3047
+ try {
3048
+ opts.onShutdown();
3049
+ } catch (error) {
3050
+ console.error("FML observer shutdown handler failed", error);
3051
+ }
3052
+ });
3053
+ } catch (error) {
3054
+ console.error("FML observer request handler failed", error);
3055
+ if (!res.headersSent) {
3056
+ writeJson(res, 500, { error: "internal_error" });
3057
+ } else {
3058
+ res.destroy();
3059
+ }
3060
+ }
3061
+ }
3062
+ function createObserverHttpServer(opts) {
3063
+ return http.createServer(async (req, res) => {
3064
+ await handleObserverRequest(req, res, opts);
3065
+ });
3066
+ }
3067
+
3068
+ // src/hooks/inbox.ts
3069
+ import { randomUUID as randomUUID2 } from "crypto";
3070
+ import fs3 from "fs";
3071
+ import path3 from "path";
3072
+
3073
+ // src/hooks/inbox-config.ts
3074
+ var BYTE_SUFFIXES = {
3075
+ b: 1,
3076
+ kib: 1024,
3077
+ mib: 1024 * 1024,
3078
+ gib: 1024 * 1024 * 1024
3079
+ };
3080
+ var MAX_BYTES_PATTERN = /^([1-9]\d*)(b|kib|mib|gib)?$/i;
3081
+ var POSITIVE_INTEGER_PATTERN = /^[1-9]\d*$/;
3082
+ var DEFAULT_HOOK_INBOX_MAX_BYTES = 512 * 1024 * 1024;
3083
+ var DEFAULT_HOOK_INBOX_MAX_ENTRIES = 1e4;
3084
+ function parseHookInboxMaxBytes(configured) {
3085
+ if (configured === void 0) return DEFAULT_HOOK_INBOX_MAX_BYTES;
3086
+ const value = configured.trim();
3087
+ if (value.toLowerCase() === "unbounded") return void 0;
3088
+ const match = MAX_BYTES_PATTERN.exec(value);
3089
+ if (!match) {
3090
+ throw new Error(
3091
+ "FML_HOOK_INBOX_MAX_BYTES must be `unbounded` or a positive integer optionally followed by B, KiB, MiB, or GiB"
3092
+ );
3093
+ }
3094
+ const quantity = Number(match[1]);
3095
+ const suffix = match[2]?.toLowerCase() ?? "b";
3096
+ const bytes = quantity * BYTE_SUFFIXES[suffix];
3097
+ if (!Number.isSafeInteger(bytes)) {
3098
+ throw new Error("FML_HOOK_INBOX_MAX_BYTES exceeds the safe integer range");
3099
+ }
3100
+ return bytes;
3101
+ }
3102
+ function parseHookInboxMaxEntries(configured) {
3103
+ if (configured === void 0) return DEFAULT_HOOK_INBOX_MAX_ENTRIES;
3104
+ const value = configured.trim();
3105
+ if (value.toLowerCase() === "unbounded") return void 0;
3106
+ if (!POSITIVE_INTEGER_PATTERN.test(value)) {
3107
+ throw new Error(
3108
+ "FML_HOOK_INBOX_MAX_ENTRIES must be `unbounded` or a positive integer"
3109
+ );
3110
+ }
3111
+ const entries = Number(value);
3112
+ if (!Number.isSafeInteger(entries)) {
3113
+ throw new Error(
3114
+ "FML_HOOK_INBOX_MAX_ENTRIES exceeds the safe integer range"
3115
+ );
3116
+ }
3117
+ return entries;
3118
+ }
3119
+ function hookInboxLimitsFromEnv(env = process.env) {
3120
+ const maxBytes = parseHookInboxMaxBytes(env.FML_HOOK_INBOX_MAX_BYTES);
3121
+ const maxEntries = parseHookInboxMaxEntries(env.FML_HOOK_INBOX_MAX_ENTRIES);
3122
+ return {
3123
+ ...maxBytes === void 0 ? {} : { maxBytes },
3124
+ ...maxEntries === void 0 ? {} : { maxEntries }
3125
+ };
3126
+ }
3127
+
3128
+ // src/hooks/inbox.ts
3129
+ var HOOK_INBOX_SCHEMA_VERSION = 1;
3130
+ var MAX_HOOK_INBOX_ENTRY_BYTES = 16 * 1024 * 1024;
3131
+ var MAX_HOOK_INBOX_DRAIN_ENTRIES = 100;
3132
+ var MAX_HOOK_INBOX_DRAIN_CURSORS = 8;
3133
+ var INBOX_ID_PATTERN = /^\d{1,16}-[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
3134
+ var INBOX_TEMP_PATTERN = /^\.(\d{1,16}-[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.\d+\.[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/i;
3135
+ var SYNC_ID_MAX_BYTES = 1024;
3136
+ var STALE_INBOX_TEMP_AGE_MS = 5 * 60 * 1e3;
3137
+ var DRAIN_CURSOR_SAFETY_AGE_MS = STALE_INBOX_TEMP_AGE_MS;
3138
+ var INBOX_METADATA_KEY = "_fmlHookInbox";
3139
+ var RETIREMENT_STATE_FILE = ".retirement-state.json";
3140
+ var RETIREMENT_STATE_SCHEMA_VERSION = 1;
3141
+ var CAPACITY_STATE_FILE = ".capacity-state.json";
3142
+ var CAPACITY_LOCK_FILE = ".capacity.lock";
3143
+ var CAPACITY_STATE_SCHEMA_VERSION = 1;
3144
+ var CAPACITY_LOCK_WAIT_MS = 2e3;
3145
+ var CAPACITY_LOCK_POLL_MS = 5;
3146
+ var CAPACITY_LOCK_INITIALIZATION_GRACE_MS = 1e3;
3147
+ var CAPACITY_LOCK_STALE_MS = 5 * 60 * 1e3;
3148
+ var INBOX_STATE_TEMP_PATTERN = /^\.(?:capacity-state|retirement-state)\.json\.\d+\.[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/iu;
3149
+ var ABANDONED_CAPACITY_LOCK_PATTERN = /^\.capacity\.lock\.stale\.(\d{1,16})\.[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
3150
+ var UNSUPPORTED_DIRECTORY_FSYNC_CODES = /* @__PURE__ */ new Set([
3151
+ "EBADF",
3152
+ "EINVAL",
3153
+ "EISDIR",
3154
+ "ENOTSUP",
3155
+ "EOPNOTSUPP",
3156
+ "EPERM"
3157
+ ]);
3158
+ var HOOK_INBOX_DRAIN_CURSORS = /* @__PURE__ */ new Map();
3159
+ var HookInboxCapacityError = class extends Error {
3160
+ reason;
3161
+ constructor(reason) {
3162
+ super(
3163
+ reason === "max_bytes" ? "FML hook inbox byte limit reached" : "FML hook inbox entry limit reached"
3164
+ );
3165
+ this.name = "HookInboxCapacityError";
3166
+ this.reason = reason;
3167
+ }
3168
+ };
3169
+ function isRecord2(value) {
3170
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3171
+ }
3172
+ function validTimestamp(value) {
3173
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 864e13;
3174
+ }
3175
+ function validSyncId(value) {
3176
+ return typeof value === "string" && value.length > 0 && !/[\u0000-\u001f\u007f]/u.test(value) && Buffer.byteLength(value, "utf8") <= SYNC_ID_MAX_BYTES;
3177
+ }
3178
+ function parseDestination(value) {
3179
+ if (value === void 0) return void 0;
3180
+ if (!isRecord2(value)) return null;
3181
+ const { targetUrl, destinationIdentity } = value;
3182
+ if (typeof targetUrl !== "string" || targetUrl.length === 0 || Buffer.byteLength(targetUrl, "utf8") > 4096 || typeof destinationIdentity !== "string" || destinationIdentity.length === 0 || Buffer.byteLength(destinationIdentity, "utf8") > 4096) {
3183
+ return null;
3184
+ }
3185
+ return { targetUrl, destinationIdentity };
3186
+ }
3187
+ function parseMetadata(value) {
3188
+ if (!isRecord2(value)) return null;
3189
+ if (typeof value.id !== "string" || !INBOX_ID_PATTERN.test(value.id) || !validSyncId(value.syncId) || !validTimestamp(value.capturedAtMs)) {
3190
+ return null;
3191
+ }
3192
+ const destination = parseDestination(value.destination);
3193
+ if (destination === null) return null;
3194
+ const repository = value.repository;
3195
+ if (repository !== void 0 && (typeof repository !== "string" || repository.length === 0 || Buffer.byteLength(repository, "utf8") > 4096)) {
3196
+ return null;
3197
+ }
3198
+ return {
3199
+ id: value.id,
3200
+ syncId: value.syncId,
3201
+ capturedAtMs: value.capturedAtMs,
3202
+ ...repository === void 0 ? {} : { repository },
3203
+ ...destination === void 0 ? {} : { destination }
3204
+ };
3205
+ }
3206
+ function entryFileName(id) {
3207
+ if (!INBOX_ID_PATTERN.test(id)) {
3208
+ throw new Error("Invalid FML hook inbox entry ID");
3209
+ }
3210
+ return `${id}.json`;
3211
+ }
3212
+ function entryPath(inboxDir, id) {
3213
+ return path3.join(inboxDir, entryFileName(id));
3214
+ }
3215
+ function enforceOwnerOnlyDirectory(inboxDir) {
3216
+ fs3.mkdirSync(inboxDir, { recursive: true, mode: 448 });
3217
+ try {
3218
+ fs3.chmodSync(inboxDir, 448);
3219
+ } catch {
3220
+ }
3221
+ }
3222
+ function fsyncDirectory(inboxDir) {
3223
+ if (process.platform === "win32") return;
3224
+ let descriptor;
3225
+ try {
3226
+ descriptor = fs3.openSync(inboxDir, "r");
3227
+ fs3.fsyncSync(descriptor);
3228
+ } catch (error) {
3229
+ const code = error.code;
3230
+ if (code === void 0 || !UNSUPPORTED_DIRECTORY_FSYNC_CODES.has(code)) {
3231
+ throw error;
3232
+ }
3233
+ } finally {
3234
+ if (descriptor !== void 0) fs3.closeSync(descriptor);
3235
+ }
3236
+ }
3237
+ async function fsyncDirectoryAsync(inboxDir) {
3238
+ if (process.platform === "win32") return;
3239
+ let descriptor;
3240
+ try {
3241
+ descriptor = await fs3.promises.open(inboxDir, "r");
3242
+ await descriptor.sync();
3243
+ } catch (error) {
3244
+ const code = error.code;
3245
+ if (code === void 0 || !UNSUPPORTED_DIRECTORY_FSYNC_CODES.has(code)) {
3246
+ throw error;
3247
+ }
3248
+ } finally {
3249
+ await descriptor?.close();
3250
+ }
3251
+ }
3252
+ function defaultRetirementState() {
3253
+ return {
3254
+ schemaVersion: RETIREMENT_STATE_SCHEMA_VERSION,
3255
+ retiredEntries: 0,
3256
+ retiredBytes: 0
3257
+ };
3258
+ }
3259
+ function nonNegativeInteger(value) {
3260
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3261
+ }
3262
+ function saturatingAdd(left, right) {
3263
+ return Math.min(Number.MAX_SAFE_INTEGER, left + right);
3264
+ }
3265
+ function parseCapacityRejectionReason(value) {
3266
+ return value === "max_bytes" || value === "max_entries" ? value : void 0;
3267
+ }
3268
+ function parseNullablePositiveInteger(value) {
3269
+ if (value === null) return null;
3270
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
3271
+ }
3272
+ function parsePendingCapacityMutation(value) {
3273
+ if (value === void 0) return void 0;
3274
+ if (!isRecord2(value)) return null;
3275
+ if (typeof value.fileName !== "string" || !value.fileName.endsWith(".json") || !INBOX_ID_PATTERN.test(value.fileName.slice(0, -5)) || !nonNegativeInteger(value.bytes) || !validTimestamp(value.startedAtMs)) {
3276
+ return null;
3277
+ }
3278
+ let temporaryFileName;
3279
+ if (value.temporaryFileName !== void 0) {
3280
+ if (typeof value.temporaryFileName !== "string") return null;
3281
+ const temporaryMatch = INBOX_TEMP_PATTERN.exec(value.temporaryFileName);
3282
+ if (!temporaryMatch || temporaryMatch[1]?.toLowerCase() !== value.fileName.slice(0, -5).toLowerCase()) {
3283
+ return null;
3284
+ }
3285
+ temporaryFileName = value.temporaryFileName;
3286
+ }
3287
+ return {
3288
+ fileName: value.fileName,
3289
+ ...temporaryFileName === void 0 ? {} : { temporaryFileName },
3290
+ bytes: value.bytes,
3291
+ startedAtMs: value.startedAtMs
3292
+ };
3293
+ }
3294
+ function parseCapacityState(value) {
3295
+ if (!isRecord2(value) || value.schemaVersion !== CAPACITY_STATE_SCHEMA_VERSION || !nonNegativeInteger(value.pendingEntries) || !nonNegativeInteger(value.pendingBytes) || !nonNegativeInteger(value.rejectedEntries) || !nonNegativeInteger(value.rejectedBytes)) {
3296
+ return null;
3297
+ }
3298
+ const maxEntries = parseNullablePositiveInteger(value.maxEntries);
3299
+ const maxBytes = parseNullablePositiveInteger(value.maxBytes);
3300
+ const pendingPersist = parsePendingCapacityMutation(value.pendingPersist);
3301
+ const pendingRemoval = parsePendingCapacityMutation(value.pendingRemoval);
3302
+ if (maxEntries === void 0 || maxBytes === void 0 || pendingPersist === null || pendingRemoval === null || pendingPersist !== void 0 && pendingRemoval !== void 0) {
3303
+ return null;
3304
+ }
3305
+ const lastRejectionReason = parseCapacityRejectionReason(
3306
+ value.lastRejectionReason
3307
+ );
3308
+ return {
3309
+ schemaVersion: CAPACITY_STATE_SCHEMA_VERSION,
3310
+ pendingEntries: value.pendingEntries,
3311
+ pendingBytes: value.pendingBytes,
3312
+ rejectedEntries: value.rejectedEntries,
3313
+ rejectedBytes: value.rejectedBytes,
3314
+ maxEntries,
3315
+ maxBytes,
3316
+ ...validTimestamp(value.lastRejectedAtMs) ? { lastRejectedAtMs: value.lastRejectedAtMs } : {},
3317
+ ...lastRejectionReason === void 0 ? {} : { lastRejectionReason },
3318
+ ...pendingPersist === void 0 ? {} : { pendingPersist },
3319
+ ...pendingRemoval === void 0 ? {} : { pendingRemoval }
3320
+ };
3321
+ }
3322
+ function capacityStatePath(inboxDir) {
3323
+ return path3.join(inboxDir, CAPACITY_STATE_FILE);
3324
+ }
3325
+ function readCapacityStateFile(inboxDir) {
3326
+ try {
3327
+ return parseCapacityState(
3328
+ JSON.parse(
3329
+ fs3.readFileSync(capacityStatePath(inboxDir), "utf8")
3330
+ )
3331
+ );
3332
+ } catch {
3333
+ return null;
3334
+ }
3335
+ }
3336
+ function defaultCapacityState(limits, pendingEntries, pendingBytes) {
3337
+ return {
3338
+ schemaVersion: CAPACITY_STATE_SCHEMA_VERSION,
3339
+ pendingEntries,
3340
+ pendingBytes,
3341
+ rejectedEntries: 0,
3342
+ rejectedBytes: 0,
3343
+ maxEntries: limits.maxEntries ?? null,
3344
+ maxBytes: limits.maxBytes ?? null
3345
+ };
3346
+ }
3347
+ function writeCapacityState(inboxDir, state, capacityLock) {
3348
+ enforceOwnerOnlyDirectory(inboxDir);
3349
+ capacityLock.assertOwned();
3350
+ const destination = capacityStatePath(inboxDir);
3351
+ const temporary = path3.join(
3352
+ inboxDir,
3353
+ `${CAPACITY_STATE_FILE}.${process.pid}.${randomUUID2()}.tmp`
3354
+ );
3355
+ let descriptor;
3356
+ try {
3357
+ descriptor = fs3.openSync(temporary, "wx", 384);
3358
+ fs3.writeFileSync(descriptor, `${JSON.stringify(state)}
3359
+ `, "utf8");
3360
+ fs3.fsyncSync(descriptor);
3361
+ capacityLock.assertOwned();
3362
+ fs3.closeSync(descriptor);
3363
+ descriptor = void 0;
3364
+ fs3.renameSync(temporary, destination);
3365
+ fsyncDirectory(inboxDir);
3366
+ capacityLock.assertOwned();
3367
+ } catch (error) {
3368
+ if (descriptor !== void 0) fs3.closeSync(descriptor);
3369
+ try {
3370
+ fs3.rmSync(temporary, { force: true });
3371
+ } catch {
3372
+ }
3373
+ throw error;
3374
+ }
3375
+ }
3376
+ function capacityLockIsOwned(lockPath, descriptor, ownerToken) {
3377
+ try {
3378
+ const openStat = fs3.fstatSync(descriptor);
3379
+ const pathStat = fs3.statSync(lockPath);
3380
+ if (openStat.dev !== pathStat.dev || openStat.ino !== pathStat.ino) {
3381
+ return false;
3382
+ }
3383
+ const value = JSON.parse(fs3.readFileSync(lockPath, "utf8"));
3384
+ return isRecord2(value) && value.ownerToken === ownerToken;
3385
+ } catch {
3386
+ return false;
3387
+ }
3388
+ }
3389
+ function parseCapacityLockOwner(value) {
3390
+ if (!isRecord2(value) || typeof value.ownerToken !== "string" || value.ownerToken.length === 0 || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0 || !validTimestamp(value.createdAtMs)) {
3391
+ return null;
3392
+ }
3393
+ return {
3394
+ ownerToken: value.ownerToken,
3395
+ pid: value.pid,
3396
+ createdAtMs: value.createdAtMs
3397
+ };
3398
+ }
3399
+ function readCapacityLockOwner(lockPath) {
3400
+ try {
3401
+ return parseCapacityLockOwner(
3402
+ JSON.parse(fs3.readFileSync(lockPath, "utf8"))
3403
+ );
3404
+ } catch {
3405
+ return null;
3406
+ }
3407
+ }
3408
+ function capacityLockOwnerIsAlive(pid) {
3409
+ try {
3410
+ process.kill(pid, 0);
3411
+ return true;
3412
+ } catch (error) {
3413
+ const code = error.code;
3414
+ if (code === "ESRCH") return false;
3415
+ return true;
3416
+ }
3417
+ }
3418
+ function restoreMovedCapacityLock(abandonedPath, lockPath) {
3419
+ try {
3420
+ fs3.renameSync(abandonedPath, lockPath);
3421
+ } catch (error) {
3422
+ if (!capacityLockRaceError(error)) throw error;
3423
+ }
3424
+ }
3425
+ function capacityLockRaceError(error) {
3426
+ const code = error.code;
3427
+ return code === "ENOENT" || code === "EEXIST" || code === "EACCES" || code === "EPERM" || code === "EBUSY";
3428
+ }
3429
+ function recoverStaleCapacityLock(lockPath, nowMs) {
3430
+ let observed;
3431
+ try {
3432
+ observed = fs3.statSync(lockPath);
3433
+ } catch (error) {
3434
+ if (capacityLockRaceError(error)) return;
3435
+ throw error;
3436
+ }
3437
+ const ageMs = Math.max(0, nowMs - observed.mtimeMs);
3438
+ if (ageMs < CAPACITY_LOCK_INITIALIZATION_GRACE_MS) return;
3439
+ const observedOwner = readCapacityLockOwner(lockPath);
3440
+ const beyondStaleHorizon = ageMs >= CAPACITY_LOCK_STALE_MS;
3441
+ if (!beyondStaleHorizon && (!observedOwner || capacityLockOwnerIsAlive(observedOwner.pid))) {
3442
+ return;
3443
+ }
3444
+ const abandoned = `${lockPath}.stale.${nowMs}.${randomUUID2()}`;
3445
+ try {
3446
+ fs3.renameSync(lockPath, abandoned);
3447
+ const moved = fs3.statSync(abandoned);
3448
+ if (moved.dev !== observed.dev || moved.ino !== observed.ino) {
3449
+ restoreMovedCapacityLock(abandoned, lockPath);
3450
+ return;
3451
+ }
3452
+ const movedOwner = readCapacityLockOwner(abandoned);
3453
+ const ownerChanged = observedOwner ? !movedOwner || movedOwner.ownerToken !== observedOwner.ownerToken || movedOwner.pid !== observedOwner.pid : movedOwner !== null;
3454
+ const ownerRevivedBeforeStaleHorizon = !beyondStaleHorizon && movedOwner !== null && capacityLockOwnerIsAlive(movedOwner.pid);
3455
+ if (ownerChanged || ownerRevivedBeforeStaleHorizon) {
3456
+ restoreMovedCapacityLock(abandoned, lockPath);
3457
+ return;
3458
+ }
3459
+ fs3.rmSync(abandoned, { force: true });
3460
+ fsyncDirectory(path3.dirname(lockPath));
3461
+ } catch (error) {
3462
+ if (!capacityLockRaceError(error)) throw error;
3463
+ }
3464
+ }
3465
+ function sleepSync(milliseconds) {
3466
+ const buffer = new SharedArrayBuffer(4);
3467
+ Atomics.wait(new Int32Array(buffer), 0, 0, milliseconds);
3468
+ }
3469
+ function acquireCapacityLock(inboxDir, waitMs = CAPACITY_LOCK_WAIT_MS) {
3470
+ enforceOwnerOnlyDirectory(inboxDir);
3471
+ const lockPath = path3.join(inboxDir, CAPACITY_LOCK_FILE);
3472
+ const deadlineMs = Date.now() + Math.max(0, waitMs);
3473
+ while (true) {
3474
+ let descriptor;
3475
+ let ownerToken;
3476
+ try {
3477
+ descriptor = fs3.openSync(lockPath, "wx+", 384);
3478
+ ownerToken = randomUUID2();
3479
+ fs3.writeFileSync(
3480
+ descriptor,
3481
+ `${JSON.stringify({ ownerToken, pid: process.pid, createdAtMs: Date.now() })}
3482
+ `,
3483
+ "utf8"
3484
+ );
3485
+ fs3.fsyncSync(descriptor);
3486
+ let released = false;
3487
+ const capacityLock = {
3488
+ assertOwned() {
3489
+ if (descriptor === void 0 || ownerToken === void 0 || !capacityLockIsOwned(lockPath, descriptor, ownerToken)) {
3490
+ throw new Error("FML hook inbox capacity lock ownership was lost");
3491
+ }
3492
+ },
3493
+ release() {
3494
+ if (released) return;
3495
+ released = true;
3496
+ let releaseError;
3497
+ try {
3498
+ if (capacityLockIsOwned(lockPath, descriptor, ownerToken)) {
3499
+ fs3.unlinkSync(lockPath);
3500
+ fsyncDirectory(inboxDir);
3501
+ }
3502
+ } catch (error) {
3503
+ releaseError = error;
3504
+ }
3505
+ try {
3506
+ fs3.closeSync(descriptor);
3507
+ } catch (error) {
3508
+ releaseError ??= error;
3509
+ }
3510
+ if (releaseError !== void 0) throw releaseError;
3511
+ }
3512
+ };
3513
+ capacityLock.assertOwned();
3514
+ return capacityLock;
3515
+ } catch (error) {
3516
+ if (descriptor !== void 0) {
3517
+ const removeOwnedLock = ownerToken !== void 0 && capacityLockIsOwned(lockPath, descriptor, ownerToken);
3518
+ try {
3519
+ fs3.closeSync(descriptor);
3520
+ } catch {
3521
+ }
3522
+ if (removeOwnedLock) {
3523
+ try {
3524
+ fs3.rmSync(lockPath, { force: true });
3525
+ fsyncDirectory(inboxDir);
3526
+ } catch {
3527
+ }
3528
+ }
3529
+ }
3530
+ if (error.code !== "EEXIST") throw error;
3531
+ const nowMs = Date.now();
3532
+ recoverStaleCapacityLock(lockPath, nowMs);
3533
+ if (nowMs >= deadlineMs) return null;
3534
+ sleepSync(CAPACITY_LOCK_POLL_MS);
3535
+ }
3536
+ }
3537
+ }
3538
+ function capacityMutationEvidence(inboxDir, pending, includeTemporary) {
3539
+ const { fileName } = pending;
3540
+ const id = fileName.slice(0, -5);
3541
+ const prefix = `.${id}.`;
3542
+ const candidates = [];
3543
+ if (includeTemporary && pending.temporaryFileName) {
3544
+ candidates.push(path3.join(inboxDir, pending.temporaryFileName));
3545
+ } else if (includeTemporary) {
3546
+ try {
3547
+ for (const name of fs3.readdirSync(inboxDir)) {
3548
+ if (name.startsWith(prefix) && name.endsWith(".tmp")) {
3549
+ candidates.push(path3.join(inboxDir, name));
3550
+ }
3551
+ }
3552
+ } catch (error) {
3553
+ return error.code === "ENOENT" ? { kind: "missing" } : { kind: "unavailable" };
3554
+ }
3555
+ }
3556
+ candidates.push(path3.join(inboxDir, fileName));
3557
+ let presentBytes;
3558
+ for (const candidate of candidates) {
3559
+ try {
3560
+ const stat = fs3.lstatSync(candidate);
3561
+ if (!stat.isFile() || presentBytes !== void 0) {
3562
+ return { kind: "unavailable" };
3563
+ }
3564
+ presentBytes = stat.size;
3565
+ } catch (error) {
3566
+ if (error.code !== "ENOENT") {
3567
+ return { kind: "unavailable" };
3568
+ }
3569
+ }
3570
+ }
3571
+ return presentBytes === void 0 ? { kind: "missing" } : { kind: "present", bytes: presentBytes };
3572
+ }
3573
+ function replaceAccountedBytes(totalBytes, reservedBytes, actualBytes) {
3574
+ const withoutReservation = Math.max(0, totalBytes - reservedBytes);
3575
+ return saturatingAdd(withoutReservation, actualBytes);
3576
+ }
3577
+ function settleCapacityState(inboxDir, state) {
3578
+ if (state.pendingPersist) {
3579
+ const pending = state.pendingPersist;
3580
+ const evidence = capacityMutationEvidence(inboxDir, pending, true);
3581
+ if (evidence.kind === "unavailable") {
3582
+ throw new Error(
3583
+ `FML hook inbox capacity evidence is unavailable for ${pending.fileName}`
3584
+ );
3585
+ }
3586
+ const survives = evidence.kind === "present";
3587
+ return {
3588
+ ...state,
3589
+ pendingEntries: Math.max(0, state.pendingEntries - (survives ? 0 : 1)),
3590
+ pendingBytes: survives ? replaceAccountedBytes(
3591
+ state.pendingBytes,
3592
+ pending.bytes,
3593
+ evidence.bytes
3594
+ ) : Math.max(0, state.pendingBytes - pending.bytes),
3595
+ pendingPersist: void 0
3596
+ };
3597
+ }
3598
+ if (state.pendingRemoval) {
3599
+ const pending = state.pendingRemoval;
3600
+ const evidence = capacityMutationEvidence(inboxDir, pending, false);
3601
+ if (evidence.kind === "unavailable") {
3602
+ throw new Error(
3603
+ `FML hook inbox capacity evidence is unavailable for ${pending.fileName}`
3604
+ );
3605
+ }
3606
+ if (evidence.kind === "present") {
3607
+ return {
3608
+ ...state,
3609
+ pendingEntries: state.pendingEntries + 1,
3610
+ pendingBytes: saturatingAdd(state.pendingBytes, evidence.bytes),
3611
+ pendingRemoval: void 0
3612
+ };
3613
+ }
3614
+ return { ...state, pendingRemoval: void 0 };
3615
+ }
3616
+ return state;
3617
+ }
3618
+ function loadCapacityState(inboxDir, limits) {
3619
+ const existing = readCapacityStateFile(inboxDir);
3620
+ if (!existing) {
3621
+ const current = pendingInboxSize(inboxDir, true);
3622
+ return defaultCapacityState(
3623
+ limits,
3624
+ current.pendingEntries,
3625
+ current.pendingBytes
3626
+ );
3627
+ }
3628
+ const settled = settleCapacityState(inboxDir, existing);
3629
+ return {
3630
+ ...settled,
3631
+ maxEntries: limits.maxEntries ?? null,
3632
+ maxBytes: limits.maxBytes ?? null
3633
+ };
3634
+ }
3635
+ function maintenanceCapacityLimits(inboxDir) {
3636
+ const current = readCapacityStateFile(inboxDir);
3637
+ if (current) {
3638
+ return {
3639
+ ...current.maxEntries === null ? {} : { maxEntries: current.maxEntries },
3640
+ ...current.maxBytes === null ? {} : { maxBytes: current.maxBytes }
3641
+ };
3642
+ }
3643
+ try {
3644
+ return hookInboxLimitsFromEnv();
3645
+ } catch {
3646
+ return {};
3647
+ }
3648
+ }
3649
+ function removeCapacityTrackedEntry(filePath, inboxDir, expectedBytes, missingIsSuccess = false) {
3650
+ const capacityLock = acquireCapacityLock(inboxDir, 0);
3651
+ if (!capacityLock) return false;
3652
+ try {
3653
+ let state = loadCapacityState(
3654
+ inboxDir,
3655
+ maintenanceCapacityLimits(inboxDir)
3656
+ );
3657
+ let bytes = expectedBytes;
3658
+ try {
3659
+ const stat = fs3.lstatSync(filePath);
3660
+ if (!stat.isFile()) return false;
3661
+ bytes = stat.size;
3662
+ } catch (error) {
3663
+ if (error.code !== "ENOENT") return false;
3664
+ return missingIsSuccess;
3665
+ }
3666
+ if (bytes === void 0) return false;
3667
+ const pendingRemoval = {
3668
+ fileName: path3.basename(filePath),
3669
+ bytes,
3670
+ startedAtMs: Date.now()
3671
+ };
3672
+ state = {
3673
+ ...state,
3674
+ pendingEntries: Math.max(0, state.pendingEntries - 1),
3675
+ pendingBytes: Math.max(0, state.pendingBytes - bytes),
3676
+ pendingRemoval
3677
+ };
3678
+ writeCapacityState(inboxDir, state, capacityLock);
3679
+ capacityLock.assertOwned();
3680
+ try {
3681
+ fs3.unlinkSync(filePath);
3682
+ } catch (error) {
3683
+ if (error.code !== "ENOENT") {
3684
+ try {
3685
+ writeCapacityState(
3686
+ inboxDir,
3687
+ {
3688
+ ...state,
3689
+ pendingEntries: state.pendingEntries + 1,
3690
+ pendingBytes: state.pendingBytes + bytes,
3691
+ pendingRemoval: void 0
3692
+ },
3693
+ capacityLock
3694
+ );
3695
+ } catch {
3696
+ }
3697
+ return false;
3698
+ }
3699
+ }
3700
+ try {
3701
+ capacityLock.assertOwned();
3702
+ } catch {
3703
+ return false;
3704
+ }
3705
+ return true;
3706
+ } catch {
3707
+ return false;
3708
+ } finally {
3709
+ capacityLock.release();
3710
+ }
3711
+ }
3712
+ function parseRetirementReason(value) {
3713
+ return value === "malformed" || value === "terminal_rejected" ? value : void 0;
3714
+ }
3715
+ function parseRetirementState(value) {
3716
+ if (!isRecord2(value) || value.schemaVersion !== RETIREMENT_STATE_SCHEMA_VERSION || !nonNegativeInteger(value.retiredEntries) || !nonNegativeInteger(value.retiredBytes)) {
3717
+ return null;
3718
+ }
3719
+ let pending;
3720
+ if (value.pending !== void 0) {
3721
+ if (!isRecord2(value.pending)) return null;
3722
+ const reason = parseRetirementReason(value.pending.reason);
3723
+ if (typeof value.pending.fileName !== "string" || !value.pending.fileName.endsWith(".json") || !INBOX_ID_PATTERN.test(value.pending.fileName.slice(0, -5)) || !nonNegativeInteger(value.pending.bytes) || !validTimestamp(value.pending.retiredAtMs) || reason === void 0) {
3724
+ return null;
3725
+ }
3726
+ pending = {
3727
+ fileName: value.pending.fileName,
3728
+ bytes: value.pending.bytes,
3729
+ reason,
3730
+ retiredAtMs: value.pending.retiredAtMs
3731
+ };
3732
+ }
3733
+ const lastRetirementReason = parseRetirementReason(
3734
+ value.lastRetirementReason
3735
+ );
3736
+ return {
3737
+ schemaVersion: RETIREMENT_STATE_SCHEMA_VERSION,
3738
+ retiredEntries: value.retiredEntries,
3739
+ retiredBytes: value.retiredBytes,
3740
+ ...validTimestamp(value.lastRetiredAtMs) ? { lastRetiredAtMs: value.lastRetiredAtMs } : {},
3741
+ ...lastRetirementReason === void 0 ? {} : { lastRetirementReason },
3742
+ ...pending === void 0 ? {} : { pending }
3743
+ };
3744
+ }
3745
+ function readRetirementState(inboxDir) {
3746
+ try {
3747
+ return parseRetirementState(
3748
+ JSON.parse(
3749
+ fs3.readFileSync(path3.join(inboxDir, RETIREMENT_STATE_FILE), "utf8")
3750
+ )
3751
+ ) ?? defaultRetirementState();
3752
+ } catch {
3753
+ return defaultRetirementState();
3754
+ }
3755
+ }
3756
+ function writeRetirementState(inboxDir, state) {
3757
+ enforceOwnerOnlyDirectory(inboxDir);
3758
+ const destination = path3.join(inboxDir, RETIREMENT_STATE_FILE);
3759
+ const temporary = path3.join(
3760
+ inboxDir,
3761
+ `${RETIREMENT_STATE_FILE}.${process.pid}.${randomUUID2()}.tmp`
3762
+ );
3763
+ let descriptor;
3764
+ try {
3765
+ descriptor = fs3.openSync(temporary, "wx", 384);
3766
+ fs3.writeFileSync(descriptor, `${JSON.stringify(state)}
3767
+ `, "utf8");
3768
+ fs3.fsyncSync(descriptor);
3769
+ fs3.closeSync(descriptor);
3770
+ descriptor = void 0;
3771
+ fs3.renameSync(temporary, destination);
3772
+ fsyncDirectory(inboxDir);
3773
+ } catch (error) {
3774
+ if (descriptor !== void 0) fs3.closeSync(descriptor);
3775
+ try {
3776
+ fs3.rmSync(temporary, { force: true });
3777
+ } catch {
3778
+ }
3779
+ throw error;
3780
+ }
3781
+ }
3782
+ function completePendingRetirement(inboxDir, state) {
3783
+ const pending = state.pending;
3784
+ if (!pending) return state;
3785
+ if (!removeCapacityTrackedEntry(
3786
+ path3.join(inboxDir, pending.fileName),
3787
+ inboxDir,
3788
+ pending.bytes,
3789
+ true
3790
+ )) {
3791
+ return null;
3792
+ }
3793
+ const completed = {
3794
+ schemaVersion: RETIREMENT_STATE_SCHEMA_VERSION,
3795
+ retiredEntries: state.retiredEntries + 1,
3796
+ retiredBytes: state.retiredBytes + pending.bytes,
3797
+ lastRetiredAtMs: pending.retiredAtMs,
3798
+ lastRetirementReason: pending.reason
3799
+ };
3800
+ try {
3801
+ writeRetirementState(inboxDir, completed);
3802
+ return completed;
3803
+ } catch {
3804
+ return null;
3805
+ }
3806
+ }
3807
+ function retireEntry(filePath, reason, inboxDir) {
3808
+ const current = completePendingRetirement(
3809
+ inboxDir,
3810
+ readRetirementState(inboxDir)
3811
+ );
3812
+ if (!current) return false;
3813
+ let bytes;
3814
+ try {
3815
+ bytes = fs3.lstatSync(filePath).size;
3816
+ } catch {
3817
+ return false;
3818
+ }
3819
+ const pending = {
3820
+ ...current,
3821
+ pending: {
3822
+ fileName: path3.basename(filePath),
3823
+ bytes,
3824
+ reason,
3825
+ retiredAtMs: Date.now()
3826
+ }
3827
+ };
3828
+ try {
3829
+ writeRetirementState(inboxDir, pending);
3830
+ } catch {
3831
+ return false;
3832
+ }
3833
+ return completePendingRetirement(inboxDir, pending) !== null;
3834
+ }
3835
+ function createHookInboxEntry(request, event, destination, persistedAtMs = Date.now()) {
3836
+ const capturedAtMs = event.capturedAtMs ?? persistedAtMs;
3837
+ return {
3838
+ schemaVersion: HOOK_INBOX_SCHEMA_VERSION,
3839
+ // File ordering follows the local durable write, not an agent-supplied
3840
+ // event timestamp that may be delayed or backdated behind a drain cursor.
3841
+ id: `${persistedAtMs}-${randomUUID2()}`,
3842
+ syncId: event.syncId,
3843
+ capturedAtMs,
3844
+ ...event.repository ? { repository: event.repository } : {},
3845
+ request,
3846
+ ...destination === void 0 ? {} : {
3847
+ destination: {
3848
+ targetUrl: destination.targetUrl,
3849
+ destinationIdentity: destination.destinationIdentity
3850
+ }
3851
+ }
3852
+ };
3853
+ }
3854
+ function persistHookInboxEntry(entry, inboxDir = FML_HOOK_INBOX_DIR, limits = hookInboxLimitsFromEnv(), capacityLockWaitMs = CAPACITY_LOCK_WAIT_MS) {
3855
+ const parsed = parseHookInboxEntry(entry);
3856
+ if (!parsed || parsed.id !== entry.id) {
3857
+ throw new Error("Invalid FML hook inbox entry");
3858
+ }
3859
+ const serialized = `${JSON.stringify(entry)}
3860
+ `;
3861
+ const serializedBytes = Buffer.byteLength(serialized, "utf8");
3862
+ if (serializedBytes > MAX_HOOK_INBOX_ENTRY_BYTES) {
3863
+ throw new Error(
3864
+ `FML hook inbox entry exceeds ${MAX_HOOK_INBOX_ENTRY_BYTES} bytes`
3865
+ );
3866
+ }
3867
+ enforceOwnerOnlyDirectory(inboxDir);
3868
+ const capacityLock = acquireCapacityLock(inboxDir, capacityLockWaitMs);
3869
+ if (!capacityLock) {
3870
+ throw new Error("FML hook inbox admission is busy");
3871
+ }
3872
+ const destination = entryPath(inboxDir, entry.id);
3873
+ const temporary = path3.join(
3874
+ inboxDir,
3875
+ `.${entry.id}.${process.pid}.${randomUUID2()}.tmp`
3876
+ );
3877
+ let descriptor;
3878
+ let state;
3879
+ try {
3880
+ state = loadCapacityState(inboxDir, limits);
3881
+ } catch (error) {
3882
+ capacityLock.release();
3883
+ throw error;
3884
+ }
3885
+ const rejectionReason = limits.maxEntries !== void 0 && state.pendingEntries + 1 > limits.maxEntries ? "max_entries" : limits.maxBytes !== void 0 && state.pendingBytes + serializedBytes > limits.maxBytes ? "max_bytes" : void 0;
3886
+ if (rejectionReason) {
3887
+ state = {
3888
+ ...state,
3889
+ rejectedEntries: saturatingAdd(state.rejectedEntries, 1),
3890
+ rejectedBytes: saturatingAdd(state.rejectedBytes, serializedBytes),
3891
+ lastRejectedAtMs: Date.now(),
3892
+ lastRejectionReason: rejectionReason
3893
+ };
3894
+ try {
3895
+ writeCapacityState(inboxDir, state, capacityLock);
3896
+ throw new HookInboxCapacityError(rejectionReason);
3897
+ } finally {
3898
+ capacityLock.release();
3899
+ }
3900
+ }
3901
+ try {
3902
+ const pendingPersist = {
3903
+ fileName: path3.basename(destination),
3904
+ temporaryFileName: path3.basename(temporary),
3905
+ bytes: serializedBytes,
3906
+ startedAtMs: Date.now()
3907
+ };
3908
+ state = {
3909
+ ...state,
3910
+ pendingEntries: state.pendingEntries + 1,
3911
+ pendingBytes: state.pendingBytes + serializedBytes,
3912
+ pendingPersist
3913
+ };
3914
+ writeCapacityState(inboxDir, state, capacityLock);
3915
+ capacityLock.assertOwned();
3916
+ descriptor = fs3.openSync(temporary, "wx", 384);
3917
+ fs3.writeFileSync(descriptor, serialized, "utf8");
3918
+ fs3.fsyncSync(descriptor);
3919
+ capacityLock.assertOwned();
3920
+ fs3.closeSync(descriptor);
3921
+ descriptor = void 0;
3922
+ fs3.renameSync(temporary, destination);
3923
+ try {
3924
+ fs3.chmodSync(destination, 384);
3925
+ } catch {
3926
+ }
3927
+ fsyncDirectory(inboxDir);
3928
+ capacityLock.assertOwned();
3929
+ } catch (error) {
3930
+ if (descriptor !== void 0) fs3.closeSync(descriptor);
3931
+ try {
3932
+ fs3.rmSync(temporary, { force: true });
3933
+ } catch {
3934
+ }
3935
+ if (state.pendingPersist) {
3936
+ try {
3937
+ const settled = settleCapacityState(inboxDir, state);
3938
+ writeCapacityState(inboxDir, settled, capacityLock);
3939
+ } catch {
3940
+ }
3941
+ }
3942
+ throw error;
3943
+ } finally {
3944
+ capacityLock.release();
3945
+ }
3946
+ }
3947
+ function hookInboxDeliveryPayload(entry) {
3948
+ return {
3949
+ ...entry.request,
3950
+ [INBOX_METADATA_KEY]: {
3951
+ id: entry.id,
3952
+ syncId: entry.syncId,
3953
+ capturedAtMs: entry.capturedAtMs,
3954
+ ...entry.repository === void 0 ? {} : { repository: entry.repository },
3955
+ ...entry.destination === void 0 ? {} : { destination: entry.destination }
3956
+ }
3957
+ };
3958
+ }
3959
+ function parseHookInboxMetadata(value) {
3960
+ return isRecord2(value) ? parseMetadata(value[INBOX_METADATA_KEY]) : null;
3961
+ }
3962
+ function parseHookInboxEntry(value) {
3963
+ if (!isRecord2(value) || value.schemaVersion !== HOOK_INBOX_SCHEMA_VERSION) {
3964
+ return null;
3965
+ }
3966
+ const metadata = parseMetadata(value);
3967
+ const request = parseHookCaptureRequest(value.request);
3968
+ if (!metadata || !request) return null;
3969
+ return {
3970
+ schemaVersion: HOOK_INBOX_SCHEMA_VERSION,
3971
+ ...metadata,
3972
+ request
3973
+ };
3974
+ }
3975
+ function sourceDeclaresForwardSchema(source) {
3976
+ const match = /^\s*\{\s*"schemaVersion"\s*:\s*(\d+)/u.exec(source);
3977
+ if (!match) return false;
3978
+ const schemaVersion = Number(match[1]);
3979
+ return Number.isSafeInteger(schemaVersion) && schemaVersion > HOOK_INBOX_SCHEMA_VERSION;
3980
+ }
3981
+ function readEntryResult(filePath) {
3982
+ try {
3983
+ const stat = fs3.lstatSync(filePath);
3984
+ if (!stat.isFile()) {
3985
+ return { kind: "malformed" };
3986
+ }
3987
+ if (stat.size > MAX_HOOK_INBOX_ENTRY_BYTES) {
3988
+ return { kind: "unsupported_schema" };
3989
+ }
3990
+ const source = fs3.readFileSync(filePath, "utf8");
3991
+ let value;
3992
+ try {
3993
+ value = JSON.parse(source);
3994
+ } catch {
3995
+ return sourceDeclaresForwardSchema(source) ? { kind: "unsupported_schema" } : { kind: "malformed" };
3996
+ }
3997
+ if (isRecord2(value) && typeof value.schemaVersion === "number" && Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > HOOK_INBOX_SCHEMA_VERSION) {
3998
+ return { kind: "unsupported_schema" };
3999
+ }
4000
+ const entry = parseHookInboxEntry(value);
4001
+ return entry ? { kind: "valid", entry } : { kind: "malformed" };
4002
+ } catch (error) {
4003
+ if (error.code === "ENOENT") {
4004
+ return { kind: "missing" };
4005
+ }
4006
+ return { kind: "failed" };
4007
+ }
4008
+ }
4009
+ function readEntry(filePath) {
4010
+ const result = readEntryResult(filePath);
4011
+ return result.kind === "valid" ? result.entry : null;
4012
+ }
4013
+ function acknowledgeHookInboxEntry(metadata, inboxDir = FML_HOOK_INBOX_DIR) {
4014
+ let filePath;
4015
+ try {
4016
+ filePath = entryPath(inboxDir, metadata.id);
4017
+ } catch {
4018
+ return false;
4019
+ }
4020
+ const entry = readEntry(filePath);
4021
+ if (!entry || entry.id !== metadata.id || entry.syncId !== metadata.syncId || entry.capturedAtMs !== metadata.capturedAtMs) {
4022
+ return false;
4023
+ }
4024
+ return removeCapacityTrackedEntry(filePath, inboxDir);
4025
+ }
4026
+ function isPendingEntryName(name) {
4027
+ return name.endsWith(".json") && INBOX_ID_PATTERN.test(name.slice(0, -5));
4028
+ }
4029
+ function insertOldestEntryName(names, selected, name, maximumEntries) {
4030
+ if (selected.has(name)) return;
4031
+ if (names.length === maximumEntries && name > names[names.length - 1]) {
4032
+ return;
4033
+ }
4034
+ let low = 0;
4035
+ let high = names.length;
4036
+ while (low < high) {
4037
+ const middle = low + high >>> 1;
4038
+ if (names[middle] < name) low = middle + 1;
4039
+ else high = middle;
4040
+ }
4041
+ names.splice(low, 0, name);
4042
+ selected.add(name);
4043
+ if (names.length > maximumEntries) {
4044
+ selected.delete(names.pop());
4045
+ }
4046
+ }
4047
+ async function scanDrainEntryNames(inboxDir, maximumEntries, skipThrough, excludedName, nowMs = Date.now()) {
4048
+ const names = [];
4049
+ const selected = /* @__PURE__ */ new Set();
4050
+ let failed = 0;
4051
+ let resetCursor = false;
4052
+ let blockCursorAdvance = false;
4053
+ let removedOrphan = false;
4054
+ const cutoffMs = nowMs - STALE_INBOX_TEMP_AGE_MS;
4055
+ const directory = await fs3.promises.opendir(inboxDir);
4056
+ try {
4057
+ for (let dirent = await directory.read(); dirent !== null; dirent = await directory.read()) {
4058
+ const name = dirent.name;
4059
+ const abandonedLockMatch = ABANDONED_CAPACITY_LOCK_PATTERN.exec(name);
4060
+ if (INBOX_STATE_TEMP_PATTERN.test(name) || abandonedLockMatch !== null) {
4061
+ try {
4062
+ const orphanPath = path3.join(inboxDir, name);
4063
+ const stat = await fs3.promises.lstat(orphanPath);
4064
+ const abandonedAtMs = abandonedLockMatch ? Number(abandonedLockMatch[1]) : void 0;
4065
+ const isStale = abandonedLockMatch ? validTimestamp(abandonedAtMs) && abandonedAtMs <= cutoffMs : stat.mtimeMs <= cutoffMs;
4066
+ if (stat.isFile() && isStale) {
4067
+ await fs3.promises.unlink(orphanPath);
4068
+ removedOrphan = true;
4069
+ }
4070
+ } catch (error) {
4071
+ if (error.code !== "ENOENT") failed += 1;
4072
+ }
4073
+ continue;
4074
+ }
4075
+ if (isPendingEntryName(name)) {
4076
+ if (name !== excludedName && (!skipThrough || name > skipThrough)) {
4077
+ insertOldestEntryName(names, selected, name, maximumEntries);
4078
+ }
4079
+ continue;
4080
+ }
4081
+ const match = INBOX_TEMP_PATTERN.exec(name);
4082
+ const id = match?.[1];
4083
+ if (!id) continue;
4084
+ const destination = entryPath(inboxDir, id);
4085
+ const destinationName = path3.basename(destination);
4086
+ const temporary = path3.join(inboxDir, name);
4087
+ try {
4088
+ const stat = await fs3.promises.lstat(temporary);
4089
+ if (!stat.isFile()) continue;
4090
+ blockCursorAdvance = true;
4091
+ if (skipThrough && destinationName <= skipThrough) resetCursor = true;
4092
+ if (stat.mtimeMs > cutoffMs) continue;
4093
+ let destinationExists = false;
4094
+ try {
4095
+ await fs3.promises.lstat(destination);
4096
+ destinationExists = true;
4097
+ } catch (error) {
4098
+ if (error.code !== "ENOENT") throw error;
4099
+ }
4100
+ if (destinationExists) {
4101
+ await fs3.promises.unlink(temporary);
4102
+ } else {
4103
+ await fs3.promises.rename(temporary, destination);
4104
+ try {
4105
+ await fs3.promises.chmod(destination, 384);
4106
+ } catch {
4107
+ }
4108
+ }
4109
+ const recoveredBehindCursor = !destinationExists && skipThrough !== void 0 && destinationName <= skipThrough;
4110
+ if (recoveredBehindCursor) resetCursor = true;
4111
+ if (destinationName !== excludedName && (recoveredBehindCursor || !skipThrough || destinationName > skipThrough)) {
4112
+ insertOldestEntryName(
4113
+ names,
4114
+ selected,
4115
+ destinationName,
4116
+ maximumEntries
4117
+ );
4118
+ }
4119
+ await fsyncDirectoryAsync(inboxDir);
4120
+ } catch (error) {
4121
+ blockCursorAdvance = true;
4122
+ if (skipThrough && destinationName <= skipThrough) resetCursor = true;
4123
+ if (error.code !== "ENOENT") failed += 1;
4124
+ }
4125
+ }
4126
+ } finally {
4127
+ await directory.close();
4128
+ }
4129
+ if (removedOrphan) {
4130
+ try {
4131
+ await fsyncDirectoryAsync(inboxDir);
4132
+ } catch {
4133
+ failed += 1;
4134
+ }
4135
+ }
4136
+ return { names, failed, resetCursor, blockCursorAdvance };
4137
+ }
4138
+ function drainCursor(inboxDir) {
4139
+ const key = path3.resolve(inboxDir);
4140
+ const existing = HOOK_INBOX_DRAIN_CURSORS.get(key);
4141
+ if (existing) {
4142
+ HOOK_INBOX_DRAIN_CURSORS.delete(key);
4143
+ HOOK_INBOX_DRAIN_CURSORS.set(key, existing);
4144
+ return existing;
4145
+ }
4146
+ if (HOOK_INBOX_DRAIN_CURSORS.size >= MAX_HOOK_INBOX_DRAIN_CURSORS) {
4147
+ const oldestKey = HOOK_INBOX_DRAIN_CURSORS.keys().next().value;
4148
+ if (oldestKey !== void 0) HOOK_INBOX_DRAIN_CURSORS.delete(oldestKey);
4149
+ }
4150
+ const created = {};
4151
+ HOOK_INBOX_DRAIN_CURSORS.set(key, created);
4152
+ return created;
4153
+ }
4154
+ function inboxEntryTimestamp(fileName) {
4155
+ const separator = fileName.indexOf("-");
4156
+ if (separator <= 0) return null;
4157
+ const timestamp = Number(fileName.slice(0, separator));
4158
+ return validTimestamp(timestamp) ? timestamp : null;
4159
+ }
4160
+ function safeDrainCursor(cursor, nowMs) {
4161
+ if (cursor.skipThrough && cursor.advancedAtMs !== void 0 && nowMs >= cursor.advancedAtMs) {
4162
+ return cursor.skipThrough;
4163
+ }
4164
+ cursor.skipThrough = void 0;
4165
+ cursor.advancedAtMs = void 0;
4166
+ return void 0;
4167
+ }
4168
+ async function pendingInboxSizeAsync(inboxDir) {
4169
+ let pendingEntries = 0;
4170
+ let pendingBytes = 0;
4171
+ const directory = await fs3.promises.opendir(inboxDir);
4172
+ try {
4173
+ for (let dirent = await directory.read(); dirent !== null; dirent = await directory.read()) {
4174
+ if (!isPendingEntryName(dirent.name)) continue;
4175
+ try {
4176
+ const stat = await fs3.promises.lstat(path3.join(inboxDir, dirent.name));
4177
+ if (stat.isFile()) {
4178
+ pendingEntries += 1;
4179
+ pendingBytes += stat.size;
4180
+ }
4181
+ } catch (error) {
4182
+ if (error.code !== "ENOENT") throw error;
4183
+ }
4184
+ }
4185
+ } finally {
4186
+ await directory.close();
4187
+ }
4188
+ return { pendingEntries, pendingBytes };
4189
+ }
4190
+ function pendingInboxSize(inboxDir, includeTemporary = false) {
4191
+ let pendingEntries = 0;
4192
+ let pendingBytes = 0;
4193
+ const directory = fs3.opendirSync(inboxDir);
4194
+ try {
4195
+ for (let dirent = directory.readSync(); dirent !== null; dirent = directory.readSync()) {
4196
+ if (!isPendingEntryName(dirent.name) && !(includeTemporary && INBOX_TEMP_PATTERN.test(dirent.name))) {
4197
+ continue;
4198
+ }
4199
+ try {
4200
+ const stat = fs3.lstatSync(path3.join(inboxDir, dirent.name));
4201
+ if (stat.isFile()) {
4202
+ pendingEntries += 1;
4203
+ pendingBytes += stat.size;
4204
+ }
4205
+ } catch (error) {
4206
+ if (error.code !== "ENOENT") throw error;
4207
+ }
4208
+ }
4209
+ } finally {
4210
+ directory.closeSync();
4211
+ }
4212
+ return { pendingEntries, pendingBytes };
4213
+ }
4214
+ function retireHookInboxEntry(metadata, reason, inboxDir = FML_HOOK_INBOX_DIR) {
4215
+ let filePath;
4216
+ try {
4217
+ filePath = entryPath(inboxDir, metadata.id);
4218
+ } catch {
4219
+ return false;
4220
+ }
4221
+ const entry = readEntry(filePath);
4222
+ if (!entry || entry.syncId !== metadata.syncId || entry.capturedAtMs !== metadata.capturedAtMs) {
4223
+ return false;
4224
+ }
4225
+ return retireEntry(filePath, reason, inboxDir);
4226
+ }
4227
+ function emptyHookInboxStatus(limits, unreadable) {
4228
+ return {
4229
+ pendingEntries: 0,
4230
+ pendingBytes: 0,
4231
+ maxEntries: limits.maxEntries ?? null,
4232
+ maxBytes: limits.maxBytes ?? null,
4233
+ capacityRejectedEntries: 0,
4234
+ capacityRejectedBytes: 0,
4235
+ retiredEntries: 0,
4236
+ retiredBytes: 0,
4237
+ unreadable
4238
+ };
4239
+ }
4240
+ function readHookInboxStatus(inboxDir = FML_HOOK_INBOX_DIR) {
4241
+ let limits;
4242
+ try {
4243
+ limits = hookInboxLimitsFromEnv();
4244
+ } catch {
4245
+ return emptyHookInboxStatus({}, true);
4246
+ }
4247
+ try {
4248
+ const retirement = readRetirementState(inboxDir);
4249
+ const { pendingBytes, pendingEntries } = pendingInboxSize(inboxDir);
4250
+ const capacity = readCapacityStateFile(inboxDir) ?? defaultCapacityState(limits, pendingEntries, pendingBytes);
4251
+ return {
4252
+ pendingEntries,
4253
+ pendingBytes,
4254
+ maxEntries: limits.maxEntries ?? null,
4255
+ maxBytes: limits.maxBytes ?? null,
4256
+ capacityRejectedEntries: capacity.rejectedEntries,
4257
+ capacityRejectedBytes: capacity.rejectedBytes,
4258
+ ...capacity.lastRejectedAtMs === void 0 ? {} : { lastCapacityRejectedAtMs: capacity.lastRejectedAtMs },
4259
+ ...capacity.lastRejectionReason === void 0 ? {} : { lastCapacityRejectionReason: capacity.lastRejectionReason },
4260
+ retiredEntries: retirement.retiredEntries,
4261
+ retiredBytes: retirement.retiredBytes,
4262
+ unreadable: false
4263
+ };
4264
+ } catch (error) {
4265
+ if (error.code === "ENOENT") {
4266
+ return emptyHookInboxStatus(limits, false);
4267
+ }
4268
+ return emptyHookInboxStatus(limits, true);
4269
+ }
4270
+ }
4271
+ async function readHookInboxStatusAsync(inboxDir = FML_HOOK_INBOX_DIR) {
4272
+ let limits;
4273
+ try {
4274
+ limits = hookInboxLimitsFromEnv();
4275
+ } catch {
4276
+ return emptyHookInboxStatus({}, true);
4277
+ }
4278
+ try {
4279
+ const retirement = readRetirementState(inboxDir);
4280
+ const { pendingBytes, pendingEntries } = await pendingInboxSizeAsync(inboxDir);
4281
+ const capacity = readCapacityStateFile(inboxDir) ?? defaultCapacityState(limits, pendingEntries, pendingBytes);
4282
+ return {
4283
+ pendingEntries,
4284
+ pendingBytes,
4285
+ maxEntries: limits.maxEntries ?? null,
4286
+ maxBytes: limits.maxBytes ?? null,
4287
+ capacityRejectedEntries: capacity.rejectedEntries,
4288
+ capacityRejectedBytes: capacity.rejectedBytes,
4289
+ ...capacity.lastRejectedAtMs === void 0 ? {} : { lastCapacityRejectedAtMs: capacity.lastRejectedAtMs },
4290
+ ...capacity.lastRejectionReason === void 0 ? {} : { lastCapacityRejectionReason: capacity.lastRejectionReason },
4291
+ retiredEntries: retirement.retiredEntries,
4292
+ retiredBytes: retirement.retiredBytes,
4293
+ unreadable: false
4294
+ };
4295
+ } catch (error) {
4296
+ if (error.code === "ENOENT") {
4297
+ return emptyHookInboxStatus(limits, false);
4298
+ }
4299
+ return emptyHookInboxStatus(limits, true);
4300
+ }
4301
+ }
4302
+ async function drainHookInbox(capture, inboxDir = FML_HOOK_INBOX_DIR) {
4303
+ const retirement = readRetirementState(inboxDir);
4304
+ const completedRetirement = completePendingRetirement(inboxDir, retirement);
4305
+ let failed = completedRetirement === null ? 1 : 0;
4306
+ const cursor = drainCursor(inboxDir);
4307
+ const drainStartedAtMs = Date.now();
4308
+ const skipThrough = safeDrainCursor(cursor, drainStartedAtMs);
4309
+ const pendingRetirementName = completedRetirement === null ? retirement.pending?.fileName : void 0;
4310
+ let drained = 0;
4311
+ let rejected = 0;
4312
+ let malformed = 0;
4313
+ let unsupported = 0;
4314
+ let names;
4315
+ let effectiveSkipThrough = skipThrough;
4316
+ let blockCursorAdvance = false;
4317
+ try {
4318
+ const scan = await scanDrainEntryNames(
4319
+ inboxDir,
4320
+ MAX_HOOK_INBOX_DRAIN_ENTRIES,
4321
+ skipThrough,
4322
+ pendingRetirementName
4323
+ );
4324
+ failed += scan.failed;
4325
+ names = scan.names;
4326
+ blockCursorAdvance = scan.blockCursorAdvance;
4327
+ if (scan.resetCursor) {
4328
+ cursor.skipThrough = void 0;
4329
+ cursor.advancedAtMs = void 0;
4330
+ effectiveSkipThrough = void 0;
4331
+ }
4332
+ } catch {
4333
+ return {
4334
+ drained,
4335
+ rejected,
4336
+ failed: failed + 1,
4337
+ malformed,
4338
+ unsupported
4339
+ };
4340
+ }
4341
+ let cursorBlocked = blockCursorAdvance;
4342
+ let safeThrough = effectiveSkipThrough;
4343
+ let sawUnsupported = effectiveSkipThrough !== void 0;
4344
+ const markResolved = (name, preservedUnsupported) => {
4345
+ if (preservedUnsupported) sawUnsupported = true;
4346
+ if (!sawUnsupported || cursorBlocked) return;
4347
+ const timestamp = inboxEntryTimestamp(name);
4348
+ if (timestamp === null || timestamp >= drainStartedAtMs - DRAIN_CURSOR_SAFETY_AGE_MS) {
4349
+ cursorBlocked = true;
4350
+ return;
4351
+ }
4352
+ safeThrough = name;
4353
+ };
4354
+ for (const name of names) {
4355
+ const filePath = path3.join(inboxDir, name);
4356
+ const readResult = readEntryResult(filePath);
4357
+ if (readResult.kind === "missing") {
4358
+ markResolved(name, false);
4359
+ continue;
4360
+ }
4361
+ if (readResult.kind === "unsupported_schema") {
4362
+ unsupported += 1;
4363
+ markResolved(name, true);
4364
+ continue;
4365
+ }
4366
+ if (readResult.kind === "failed") {
4367
+ failed += 1;
4368
+ cursorBlocked = true;
4369
+ continue;
4370
+ }
4371
+ if (readResult.kind === "malformed") {
4372
+ if (retireEntry(filePath, "malformed", inboxDir)) {
4373
+ malformed += 1;
4374
+ markResolved(name, false);
4375
+ } else {
4376
+ failed += 1;
4377
+ cursorBlocked = true;
4378
+ }
4379
+ continue;
4380
+ }
4381
+ const { entry } = readResult;
4382
+ try {
4383
+ const outcome = capture(entry);
4384
+ if (outcome === "retry") {
4385
+ failed += 1;
4386
+ cursorBlocked = true;
4387
+ continue;
4388
+ }
4389
+ if (outcome === "terminal_rejected") {
4390
+ if (retireEntry(filePath, "terminal_rejected", inboxDir)) {
4391
+ rejected += 1;
4392
+ markResolved(name, false);
4393
+ } else {
4394
+ failed += 1;
4395
+ cursorBlocked = true;
4396
+ }
4397
+ continue;
4398
+ }
4399
+ if (acknowledgeHookInboxEntry(entry, inboxDir)) {
4400
+ drained += 1;
4401
+ markResolved(name, false);
4402
+ } else {
4403
+ failed += 1;
4404
+ cursorBlocked = true;
4405
+ }
4406
+ } catch {
4407
+ failed += 1;
4408
+ cursorBlocked = true;
4409
+ }
4410
+ }
4411
+ if (safeThrough !== effectiveSkipThrough) {
4412
+ cursor.skipThrough = safeThrough;
4413
+ cursor.advancedAtMs = drainStartedAtMs;
4414
+ }
4415
+ return { drained, rejected, failed, malformed, unsupported };
4416
+ }
4417
+
4418
+ export {
4419
+ readObserverAuthToken,
4420
+ getOrCreateObserverAuthToken,
4421
+ HOOK_TARGETS,
4422
+ CODEX_HOOK_EVENTS,
4423
+ GEMINI_HOOK_EVENTS,
4424
+ GROK_HOOK_EVENTS,
4425
+ parseHookCaptureRequest,
4426
+ normalizeObserverHookCapture,
4427
+ ObserverHookSpoolEntryTooLargeError,
4428
+ openObserverDatabase,
4429
+ OBSERVER_PROTOCOL_VERSION,
4430
+ OBSERVER_SERVICE_NAME,
4431
+ isObserverHealth,
4432
+ createObserverHttpServer,
4433
+ createHookInboxEntry,
4434
+ persistHookInboxEntry,
4435
+ hookInboxDeliveryPayload,
4436
+ parseHookInboxMetadata,
4437
+ acknowledgeHookInboxEntry,
4438
+ retireHookInboxEntry,
4439
+ readHookInboxStatus,
4440
+ readHookInboxStatusAsync,
4441
+ drainHookInbox
4442
+ };