larkin 0.2.70

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 (144) hide show
  1. package/CONTRIBUTING.md +48 -0
  2. package/LICENSE +201 -0
  3. package/README.md +122 -0
  4. package/SECURITY.md +14 -0
  5. package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
  6. package/dist/agent/agent-cli-capabilities.cjs +43 -0
  7. package/dist/agent/agent-cli-capabilities.mjs +35 -0
  8. package/dist/agent/agent-state-store.cjs +961 -0
  9. package/dist/agent/agent-state-store.mjs +923 -0
  10. package/dist/agent/agent-transport.cjs +201 -0
  11. package/dist/agent/agent-transport.mjs +194 -0
  12. package/dist/agent/config-cli-contract.cjs +19 -0
  13. package/dist/agent/config-cli-contract.mjs +16 -0
  14. package/dist/agent/context-prompt.cjs +159 -0
  15. package/dist/agent/context-prompt.mjs +155 -0
  16. package/dist/agent/freshness-gate.mjs +24 -0
  17. package/dist/agent/history-projection.cjs +72 -0
  18. package/dist/agent/history-projection.mjs +65 -0
  19. package/dist/agent/host-reminder-orchestrator.cjs +289 -0
  20. package/dist/agent/host-reminder-orchestrator.mjs +252 -0
  21. package/dist/agent/inbox-projection.cjs +92 -0
  22. package/dist/agent/inbox-projection.mjs +85 -0
  23. package/dist/agent/interaction-state-machine.cjs +710 -0
  24. package/dist/agent/interaction-state-machine.mjs +705 -0
  25. package/dist/agent/reminder-routes.cjs +192 -0
  26. package/dist/agent/reminder-routes.mjs +156 -0
  27. package/dist/agent/reminder-store.cjs +220 -0
  28. package/dist/agent/reminder-store.mjs +205 -0
  29. package/dist/agent/transport-business-context.cjs +486 -0
  30. package/dist/agent/transport-business-context.mjs +446 -0
  31. package/dist/agent/transport-shell.cjs +45 -0
  32. package/dist/agent/transport-shell.mjs +39 -0
  33. package/dist/app/agent-cli.mjs +596 -0
  34. package/dist/app/agent-config.mjs +633 -0
  35. package/dist/app/agent-lark-cli-workspace.cjs +95 -0
  36. package/dist/app/agent-lark-cli-workspace.mjs +85 -0
  37. package/dist/app/agent-readiness.cjs +42 -0
  38. package/dist/app/agent-readiness.mjs +38 -0
  39. package/dist/app/binary-entry.mjs +86 -0
  40. package/dist/app/cli.mjs +176 -0
  41. package/dist/app/dashboard.mjs +205 -0
  42. package/dist/app/internal-command.cjs +57 -0
  43. package/dist/app/internal-command.mjs +48 -0
  44. package/dist/app/lark-channel-secret.mjs +22 -0
  45. package/dist/app/lark-cli.mjs +887 -0
  46. package/dist/app/lark.mjs +45 -0
  47. package/dist/app/local-control.mjs +728 -0
  48. package/dist/app/official-lark-cli.cjs +138 -0
  49. package/dist/app/official-lark-cli.mjs +128 -0
  50. package/dist/app/pi-auth-cli.mjs +67 -0
  51. package/dist/app/run.mjs +364 -0
  52. package/dist/app/runtime-agent-config.mjs +416 -0
  53. package/dist/app/runtime-model-directory.mjs +78 -0
  54. package/dist/app/runtime-process.mjs +113 -0
  55. package/dist/app/session-cli.mjs +83 -0
  56. package/dist/app/setup-dashboard.mjs +26 -0
  57. package/dist/app/setup.mjs +166 -0
  58. package/dist/app/telemetry.mjs +83 -0
  59. package/dist/dashboard/dashboard-assets.mjs +21 -0
  60. package/dist/dashboard/dashboard-avatar.mjs +128 -0
  61. package/dist/dashboard/dashboard-config-controller.mjs +525 -0
  62. package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
  63. package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
  64. package/dist/dashboard/dashboard-template.mjs +29 -0
  65. package/dist/dashboard/dashboard-view-model.mjs +556 -0
  66. package/dist/dashboard/dashboard-workspace.mjs +104 -0
  67. package/dist/dashboard/web/assets/dashboard.css +3 -0
  68. package/dist/dashboard/web/assets/dashboard.js +49 -0
  69. package/dist/feishu/channel-transport.cjs +151 -0
  70. package/dist/feishu/channel-transport.mjs +148 -0
  71. package/dist/feishu/document-comment.cjs +120 -0
  72. package/dist/feishu/document-comment.mjs +109 -0
  73. package/dist/feishu/host-business-state.cjs +477 -0
  74. package/dist/feishu/host-business-state.mjs +462 -0
  75. package/dist/feishu/host-channel-business.cjs +161 -0
  76. package/dist/feishu/host-channel-business.mjs +157 -0
  77. package/dist/feishu/host-processing-eye.cjs +185 -0
  78. package/dist/feishu/host-processing-eye.mjs +181 -0
  79. package/dist/feishu/host-shell.cjs +1425 -0
  80. package/dist/feishu/host-shell.mjs +1385 -0
  81. package/dist/feishu/im-freshness-adapter.mjs +89 -0
  82. package/dist/feishu/interaction-orchestrator.cjs +279 -0
  83. package/dist/feishu/interaction-orchestrator.mjs +242 -0
  84. package/dist/feishu/lark-passthrough.mjs +70 -0
  85. package/dist/feishu/message-policy.cjs +111 -0
  86. package/dist/feishu/message-policy.mjs +98 -0
  87. package/dist/feishu/outbound-transport.cjs +221 -0
  88. package/dist/feishu/outbound-transport.mjs +215 -0
  89. package/dist/platform/build-info.cjs +25 -0
  90. package/dist/platform/build-info.mjs +18 -0
  91. package/dist/platform/callback-capability.cjs +301 -0
  92. package/dist/platform/callback-capability.mjs +252 -0
  93. package/dist/platform/check-bun-version.cjs +12 -0
  94. package/dist/platform/config.cjs +751 -0
  95. package/dist/platform/config.mjs +694 -0
  96. package/dist/platform/process-inspect.cjs +89 -0
  97. package/dist/platform/process-state.cjs +237 -0
  98. package/dist/platform/process-state.mjs +223 -0
  99. package/dist/platform/release-artifacts.mjs +61 -0
  100. package/dist/platform/root-layout.cjs +111 -0
  101. package/dist/platform/root-layout.mjs +72 -0
  102. package/dist/platform/telemetry-config.cjs +64 -0
  103. package/dist/platform/telemetry-config.mjs +56 -0
  104. package/dist/platform/telemetry-spool.cjs +661 -0
  105. package/dist/platform/telemetry-spool.mjs +653 -0
  106. package/dist/platform/telemetry-tracing.cjs +602 -0
  107. package/dist/platform/telemetry-tracing.mjs +596 -0
  108. package/dist/platform/telemetry-uploader.cjs +150 -0
  109. package/dist/platform/telemetry-uploader.mjs +146 -0
  110. package/dist/platform/workspace-service.cjs +469 -0
  111. package/dist/platform/workspace-service.mjs +430 -0
  112. package/dist/runtime/builtin-pi-assets.mjs +33 -0
  113. package/dist/runtime/claude-model-catalog.cjs +102 -0
  114. package/dist/runtime/claude-model-catalog.mjs +99 -0
  115. package/dist/runtime/codex-model-catalog.cjs +108 -0
  116. package/dist/runtime/codex-model-catalog.mjs +105 -0
  117. package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
  118. package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
  119. package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
  120. package/dist/runtime/pi-model-catalog.cjs +116 -0
  121. package/dist/runtime/pi-model-catalog.mjs +75 -0
  122. package/dist/runtime/pi-official-auth.mjs +480 -0
  123. package/dist/runtime/pi-provider-config.cjs +252 -0
  124. package/dist/runtime/pi-provider-config.mjs +239 -0
  125. package/dist/runtime/pi-rpc-client.cjs +255 -0
  126. package/dist/runtime/pi-rpc-client.mjs +251 -0
  127. package/dist/runtime/pi-subagent-injection.mjs +129 -0
  128. package/dist/runtime/pi-subagents.bundle.js +8990 -0
  129. package/dist/runtime/runtime-adapters.mjs +1019 -0
  130. package/dist/runtime/runtime-contracts.cjs +2 -0
  131. package/dist/runtime/runtime-contracts.mjs +1 -0
  132. package/dist/runtime/runtime-host.cjs +930 -0
  133. package/dist/runtime/runtime-host.mjs +922 -0
  134. package/dist/runtime/runtime-model-catalog.cjs +43 -0
  135. package/dist/runtime/runtime-model-catalog.mjs +40 -0
  136. package/dist/runtime/runtime-readiness.cjs +177 -0
  137. package/dist/runtime/runtime-readiness.mjs +133 -0
  138. package/dist/setup/bot-register.mjs +655 -0
  139. package/dist/setup/grant-scopes.mjs +133 -0
  140. package/dist/setup/run-credential-preflight.mjs +89 -0
  141. package/dist/setup/setup-agent-choice.mjs +172 -0
  142. package/dist/setup/setup-bind.mjs +337 -0
  143. package/dist/setup/setup-binding.mjs +52 -0
  144. package/package.json +112 -0
@@ -0,0 +1,653 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import zlib from "node:zlib";
5
+ import { inspectProcess } from "./process-state.mjs";
6
+ const READY = /^(?:span-[0-9a-f-]+|import-[0-9a-f]{64})\.json$/;
7
+ const REMNANT = /^\.(?:corrupt|write|ack|delete|stale-lock|purge)-[0-9A-Za-z-]+(?:\.(?:json|tmp|dir))?$/;
8
+ const HEX_TRACE = /^[0-9a-f]{32}$/;
9
+ const HEX_SPAN = /^[0-9a-f]{16}$/;
10
+ const SPAN_NAMES = new Set(["larkin.message.process", "feishu.receive", "runtime.deliver", "agent.turn", "model.activity", "tool.execute", "inbox.consume", "feishu.send",
11
+ "document.comment.receive", "document.comment.gate", "document.comment.pending", "document.comment.replay", "document.comment.resolve", "document.comment.inbox", "document.comment.reply",
12
+ "pi.prompt.wait", "pi.compaction", "pi.rpc.submit", "pi.rpc.lifecycle", "pi.output.wait", "pi.generation", "pi.tool.wait", "pi.rpc.settle"]);
13
+ const ATTRIBUTE_KEYS = new Set(["service.name", "service.version", "service.instance.id", "larkin.agent.id_hash", "messaging.message.id_hash", "larkin.message.relation", "larkin.message.source",
14
+ "larkin.observation.boundary", "larkin.activity.type", "larkin.filter.reason", "larkin.runtime.id", "larkin.runtime.distribution", "larkin.operation.outcome",
15
+ "larkin.pi.preflight.outcome", "larkin.pi.preflight.progress"]);
16
+ const SENSITIVE = /(?:bearer\s|(?:api[_-]?key|authorization|password|token|secret|cookie)\s*[=:]|(?:sk|ghp|github_pat)-?[a-z0-9_-]{8})/i;
17
+ const ABSOLUTE_PATH = /(?:^|[\s"'=])(?:\/(?!\/)[^\s"']+|[A-Za-z]:[\\/][^\s"']+)/;
18
+ const emptyDiagnostics = () => ({ droppedFiles: 0, cleanedRemnantFiles: 0, droppedSpans: 0, lastUploadAt: null, lastErrorCategory: null });
19
+ const invalidPayload = () => { throw new Error("invalid telemetry payload"); };
20
+ function validateValue(value, depth = 0) {
21
+ if (depth > 6 || !value || typeof value !== "object" || Array.isArray(value))
22
+ invalidPayload();
23
+ const object = value;
24
+ const keys = Object.keys(object);
25
+ if (keys.length !== 1 || !["stringValue", "boolValue", "intValue", "doubleValue", "arrayValue"].includes(keys[0]))
26
+ invalidPayload();
27
+ if ("stringValue" in object) {
28
+ const text = object.stringValue;
29
+ if (typeof text !== "string" || text.length > 128 || ABSOLUTE_PATH.test(text) || SENSITIVE.test(text))
30
+ invalidPayload();
31
+ }
32
+ else if ("boolValue" in object && typeof object.boolValue !== "boolean")
33
+ invalidPayload();
34
+ else if ("intValue" in object && !/^-?[0-9]{1,20}$/.test(String(object.intValue)))
35
+ invalidPayload();
36
+ else if ("doubleValue" in object && (typeof object.doubleValue !== "number" || !Number.isFinite(object.doubleValue)))
37
+ invalidPayload();
38
+ else if ("arrayValue" in object) {
39
+ const arrayValue = object.arrayValue;
40
+ if (!arrayValue || Object.keys(arrayValue).some((key) => key !== "values") || !Array.isArray(arrayValue.values) || arrayValue.values.length > 32)
41
+ invalidPayload();
42
+ for (const item of arrayValue.values)
43
+ validateValue(item, depth + 1);
44
+ }
45
+ }
46
+ function validateAttributes(value) {
47
+ if (!Array.isArray(value) || value.length > 32)
48
+ invalidPayload();
49
+ for (const item of value) {
50
+ if (!item || typeof item !== "object" || Array.isArray(item))
51
+ invalidPayload();
52
+ const attribute = item;
53
+ if (Object.keys(attribute).some((key) => key !== "key" && key !== "value") || !ATTRIBUTE_KEYS.has(String(attribute.key)))
54
+ invalidPayload();
55
+ validateValue(attribute.value);
56
+ const stringValue = attribute.value.stringValue;
57
+ if (typeof stringValue !== "string")
58
+ invalidPayload();
59
+ {
60
+ const key = String(attribute.key);
61
+ const text = stringValue;
62
+ if (key === "service.name" && text !== "larkin")
63
+ invalidPayload();
64
+ if (key === "service.version" && !/^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/.test(text))
65
+ invalidPayload();
66
+ if (key === "service.instance.id" && !/^[0-9a-f-]{16,64}$/i.test(text))
67
+ invalidPayload();
68
+ if (["larkin.agent.id_hash", "messaging.message.id_hash"].includes(key) && !/^[0-9a-f]{24}$/.test(text))
69
+ invalidPayload();
70
+ if (key === "larkin.message.relation" && text !== "fan_in")
71
+ invalidPayload();
72
+ if (key === "larkin.observation.boundary" && !["runtime_host", "runtime_event_interval", "agent_cli", "agent_transport", "pi_rpc", "comment_cli"].includes(text))
73
+ invalidPayload();
74
+ if (key === "larkin.activity.type" && !["thinking", "text", "tool", "internal"].includes(text))
75
+ invalidPayload();
76
+ if (key === "larkin.message.source" && !["im", "document_comment"].includes(text))
77
+ invalidPayload();
78
+ if (key === "larkin.filter.reason" && !["subscription_unverified", "self_or_missing_operator", "unsupported_file_type", "duplicate"].includes(text))
79
+ invalidPayload();
80
+ if (key === "larkin.runtime.id" && text !== "pi")
81
+ invalidPayload();
82
+ if (key === "larkin.runtime.distribution" && !["builtin", "external"].includes(text))
83
+ invalidPayload();
84
+ if (key === "larkin.operation.outcome" && !["success", "error"].includes(text))
85
+ invalidPayload();
86
+ if (key === "larkin.pi.preflight.outcome" && !["accepted", "timeout", "error"].includes(text))
87
+ invalidPayload();
88
+ if (key === "larkin.pi.preflight.progress" && !["compaction", "retry"].includes(text))
89
+ invalidPayload();
90
+ }
91
+ }
92
+ }
93
+ export function validateOtlpPayload(payload) {
94
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).some((key) => key !== "resourceSpans"))
95
+ invalidPayload();
96
+ const resourceSpans = payload.resourceSpans;
97
+ if (!Array.isArray(resourceSpans) || resourceSpans.length < 1 || resourceSpans.length > 1024)
98
+ invalidPayload();
99
+ for (const resourceSpanValue of resourceSpans) {
100
+ const resourceSpan = resourceSpanValue;
101
+ if (!resourceSpan || typeof resourceSpan !== "object" || Object.keys(resourceSpan).some((key) => key !== "resource" && key !== "scopeSpans"))
102
+ invalidPayload();
103
+ const resource = resourceSpan.resource;
104
+ if (!resource || Object.keys(resource).some((key) => key !== "attributes"))
105
+ invalidPayload();
106
+ validateAttributes(resource.attributes);
107
+ if (!Array.isArray(resourceSpan.scopeSpans) || resourceSpan.scopeSpans.length > 16)
108
+ invalidPayload();
109
+ for (const scopeSpanValue of resourceSpan.scopeSpans) {
110
+ const scopeSpan = scopeSpanValue;
111
+ if (!scopeSpan || Object.keys(scopeSpan).some((key) => key !== "scope" && key !== "spans"))
112
+ invalidPayload();
113
+ const scope = scopeSpan.scope;
114
+ if (!scope || Object.keys(scope).some((key) => key !== "name" && key !== "version") || typeof scope.name !== "string" || typeof scope.version !== "string")
115
+ invalidPayload();
116
+ if (!Array.isArray(scopeSpan.spans) || scopeSpan.spans.length > 4096)
117
+ invalidPayload();
118
+ for (const spanValue of scopeSpan.spans) {
119
+ const span = spanValue;
120
+ const allowed = new Set(["traceId", "spanId", "parentSpanId", "name", "kind", "startTimeUnixNano", "endTimeUnixNano", "attributes", "links", "status", "flags"]);
121
+ if (!span || Object.keys(span).some((key) => !allowed.has(key)) || !HEX_TRACE.test(String(span.traceId)) || !HEX_SPAN.test(String(span.spanId))
122
+ || (span.parentSpanId !== undefined && !HEX_SPAN.test(String(span.parentSpanId))) || !SPAN_NAMES.has(String(span.name))
123
+ || !Number.isInteger(span.kind) || Number(span.kind) < 1 || Number(span.kind) > 5
124
+ || !/^[0-9]{1,24}$/.test(String(span.startTimeUnixNano)) || !/^[0-9]{1,24}$/.test(String(span.endTimeUnixNano))
125
+ || BigInt(String(span.endTimeUnixNano)) < BigInt(String(span.startTimeUnixNano)))
126
+ invalidPayload();
127
+ validateAttributes(span.attributes);
128
+ if (!Array.isArray(span.links) || span.links.length > 8)
129
+ invalidPayload();
130
+ for (const linkValue of span.links) {
131
+ const link = linkValue;
132
+ if (!link || Object.keys(link).some((key) => !["traceId", "spanId", "traceState", "flags", "attributes"].includes(key))
133
+ || !HEX_TRACE.test(String(link.traceId)) || !HEX_SPAN.test(String(link.spanId)))
134
+ invalidPayload();
135
+ validateAttributes(link.attributes);
136
+ }
137
+ const status = span.status;
138
+ if (!status || Object.keys(status).some((key) => key !== "code") || !Number.isInteger(status.code) || Number(status.code) < 0 || Number(status.code) > 2)
139
+ invalidPayload();
140
+ }
141
+ }
142
+ }
143
+ }
144
+ export class TelemetrySpool {
145
+ config;
146
+ constructor(config) {
147
+ this.config = config;
148
+ }
149
+ ensure() {
150
+ const parent = path.dirname(this.config.spoolDir);
151
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
152
+ try {
153
+ if (fs.lstatSync(this.config.spoolDir).isSymbolicLink())
154
+ throw new Error("invalid telemetry spool");
155
+ }
156
+ catch (error) {
157
+ if (error.code !== "ENOENT")
158
+ throw error;
159
+ }
160
+ fs.mkdirSync(this.config.spoolDir, { recursive: true, mode: 0o700 });
161
+ const stat = fs.lstatSync(this.config.spoolDir);
162
+ if (!stat.isDirectory() || stat.isSymbolicLink())
163
+ throw new Error("invalid telemetry spool");
164
+ fs.chmodSync(this.config.spoolDir, 0o700);
165
+ }
166
+ acquireLease() {
167
+ this.ensure();
168
+ const lease = path.join(this.config.spoolDir, ".queue.lock");
169
+ const token = crypto.randomUUID();
170
+ const currentStartToken = inspectProcess(process.pid).startToken;
171
+ const create = () => {
172
+ fs.mkdirSync(lease, { mode: 0o700 });
173
+ fs.writeFileSync(path.join(lease, "owner.json"), JSON.stringify({ version: 1, pid: process.pid, processStartToken: currentStartToken,
174
+ token, createdAt: Date.now() }), { mode: 0o600, flag: "wx" });
175
+ };
176
+ try {
177
+ create();
178
+ }
179
+ catch (error) {
180
+ if (error.code !== "EEXIST")
181
+ throw error;
182
+ try {
183
+ const stat = fs.lstatSync(lease);
184
+ if (!stat.isDirectory() || stat.isSymbolicLink())
185
+ return null;
186
+ let owner = null;
187
+ try {
188
+ owner = JSON.parse(fs.readFileSync(path.join(lease, "owner.json"), "utf8"));
189
+ }
190
+ catch { /* reclaim only after timeout */ }
191
+ const inspected = owner && Number.isSafeInteger(Number(owner.pid)) && Number(owner.pid) > 0 ? inspectProcess(Number(owner.pid)) : { ok: false };
192
+ const hasStartToken = typeof owner?.processStartToken === "string" && owner.processStartToken.length > 0;
193
+ const sameProcess = hasStartToken && inspected.ok && inspected.startToken === owner?.processStartToken;
194
+ const identityMismatch = hasStartToken && inspected.ok && inspected.startToken !== owner?.processStartToken;
195
+ const age = Date.now() - (owner && Number.isFinite(Number(owner.createdAt)) ? Number(owner.createdAt) : stat.mtimeMs);
196
+ if (sameProcess || (!identityMismatch && !inspected.dead && age < 2 * 60_000))
197
+ return null;
198
+ const stale = path.join(this.config.spoolDir, `.stale-lock-${crypto.randomUUID()}`);
199
+ fs.renameSync(lease, stale);
200
+ const moved = fs.lstatSync(stale);
201
+ if (!moved.isDirectory() || moved.isSymbolicLink() || moved.dev !== stat.dev || moved.ino !== stat.ino)
202
+ return null;
203
+ fs.rmSync(stale, { recursive: true });
204
+ create();
205
+ }
206
+ catch {
207
+ return null;
208
+ }
209
+ }
210
+ let released = false;
211
+ return () => {
212
+ if (released)
213
+ return;
214
+ released = true;
215
+ try {
216
+ const ownerFile = path.join(lease, "owner.json");
217
+ const owner = JSON.parse(fs.readFileSync(ownerFile, "utf8"));
218
+ if (owner.token !== token)
219
+ return;
220
+ fs.unlinkSync(ownerFile);
221
+ fs.rmdirSync(lease);
222
+ }
223
+ catch { /* isolated */ }
224
+ };
225
+ }
226
+ readyFiles() {
227
+ this.ensure();
228
+ return fs.readdirSync(this.config.spoolDir).filter((name) => READY.test(name)).flatMap((name) => {
229
+ const file = path.join(this.config.spoolDir, name);
230
+ try {
231
+ const stat = fs.lstatSync(file);
232
+ return stat.isFile() && !stat.isSymbolicLink() ? [{ file, size: stat.size, mtimeMs: stat.mtimeMs, device: stat.dev, inode: stat.ino }] : [];
233
+ }
234
+ catch {
235
+ return [];
236
+ }
237
+ }).sort((a, b) => a.mtimeMs - b.mtimeMs || a.file.localeCompare(b.file));
238
+ }
239
+ remnantFiles() {
240
+ this.ensure();
241
+ return fs.readdirSync(this.config.spoolDir).filter((name) => REMNANT.test(name)).flatMap((name) => {
242
+ const file = path.join(this.config.spoolDir, name);
243
+ try {
244
+ const stat = fs.lstatSync(file);
245
+ const directory = stat.isDirectory() && !stat.isSymbolicLink();
246
+ return (stat.isFile() || directory) && !stat.isSymbolicLink()
247
+ ? [{ file, size: directory ? this.directoryBytes(file, stat) : stat.size, mtimeMs: stat.mtimeMs,
248
+ device: stat.dev, inode: stat.ino, directory, remnant: true }] : [];
249
+ }
250
+ catch {
251
+ return [];
252
+ }
253
+ }).sort((a, b) => a.mtimeMs - b.mtimeMs || a.file.localeCompare(b.file));
254
+ }
255
+ directoryBytes(root, rootStat) {
256
+ const saturation = this.config.maxBytes + 1;
257
+ const seen = new Set();
258
+ let entries = 0;
259
+ const spoolRoot = fs.realpathSync(this.config.spoolDir);
260
+ const prefix = `${spoolRoot}${path.sep}`;
261
+ const visit = (directory, stat, depth) => {
262
+ const identity = `${stat.dev}:${stat.ino}`;
263
+ if (seen.has(identity))
264
+ return 0;
265
+ seen.add(identity);
266
+ if (depth > 8 || entries > 1024)
267
+ return saturation;
268
+ try {
269
+ const real = fs.realpathSync(directory);
270
+ if (real !== spoolRoot && !real.startsWith(prefix))
271
+ return saturation;
272
+ }
273
+ catch {
274
+ return saturation;
275
+ }
276
+ let total = stat.size;
277
+ let names;
278
+ try {
279
+ names = fs.readdirSync(directory);
280
+ }
281
+ catch {
282
+ return saturation;
283
+ }
284
+ for (const name of names) {
285
+ entries += 1;
286
+ if (entries > 1024)
287
+ return saturation;
288
+ const child = path.join(directory, name);
289
+ let childStat;
290
+ try {
291
+ childStat = fs.lstatSync(child);
292
+ }
293
+ catch {
294
+ continue;
295
+ }
296
+ if (childStat.isSymbolicLink()) {
297
+ total += childStat.size;
298
+ continue;
299
+ }
300
+ if (childStat.isFile())
301
+ total += childStat.size;
302
+ else if (childStat.isDirectory())
303
+ total += visit(child, childStat, depth + 1);
304
+ }
305
+ try {
306
+ const after = fs.lstatSync(directory);
307
+ if (after.isSymbolicLink() || after.dev !== stat.dev || after.ino !== stat.ino)
308
+ return saturation;
309
+ }
310
+ catch {
311
+ return saturation;
312
+ }
313
+ return total;
314
+ };
315
+ return visit(root, rootStat, 0);
316
+ }
317
+ atomicWrite(file, bytes) {
318
+ this.ensure();
319
+ const temporary = path.join(this.config.spoolDir, `.write-${process.pid}-${crypto.randomUUID()}.tmp`);
320
+ let descriptor;
321
+ try {
322
+ descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
323
+ fs.writeFileSync(descriptor, bytes);
324
+ fs.fsyncSync(descriptor);
325
+ fs.closeSync(descriptor);
326
+ descriptor = undefined;
327
+ fs.renameSync(temporary, file);
328
+ fs.chmodSync(file, 0o600);
329
+ const directory = fs.openSync(this.config.spoolDir, fs.constants.O_RDONLY);
330
+ try {
331
+ fs.fsyncSync(directory);
332
+ }
333
+ finally {
334
+ fs.closeSync(directory);
335
+ }
336
+ }
337
+ finally {
338
+ if (descriptor !== undefined)
339
+ try {
340
+ fs.closeSync(descriptor);
341
+ }
342
+ catch { /* isolated */ }
343
+ try {
344
+ fs.unlinkSync(temporary);
345
+ }
346
+ catch { /* isolated */ }
347
+ }
348
+ }
349
+ removeVerified(entry) {
350
+ const quarantine = path.join(this.config.spoolDir, `.delete-${crypto.randomUUID()}.tmp`);
351
+ try {
352
+ fs.renameSync(entry.file, quarantine);
353
+ const stat = fs.lstatSync(quarantine);
354
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.dev !== entry.device || stat.ino !== entry.inode) {
355
+ try {
356
+ fs.renameSync(quarantine, entry.file);
357
+ }
358
+ catch { /* preserve replacement */ }
359
+ return false;
360
+ }
361
+ fs.unlinkSync(quarantine);
362
+ return true;
363
+ }
364
+ catch {
365
+ try {
366
+ fs.renameSync(quarantine, entry.file);
367
+ }
368
+ catch { /* isolated */ }
369
+ return false;
370
+ }
371
+ }
372
+ removeManaged(entry) {
373
+ if (!entry.directory)
374
+ return this.removeVerified(entry);
375
+ const quarantine = path.join(this.config.spoolDir, `.purge-${crypto.randomUUID()}.dir`);
376
+ try {
377
+ fs.renameSync(entry.file, quarantine);
378
+ const stat = fs.lstatSync(quarantine);
379
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== entry.device || stat.ino !== entry.inode) {
380
+ try {
381
+ fs.renameSync(quarantine, entry.file);
382
+ }
383
+ catch { /* preserve */ }
384
+ return false;
385
+ }
386
+ fs.rmSync(quarantine, { recursive: true });
387
+ return true;
388
+ }
389
+ catch {
390
+ try {
391
+ fs.renameSync(quarantine, entry.file);
392
+ }
393
+ catch { /* isolated */ }
394
+ return false;
395
+ }
396
+ }
397
+ enqueue(payload) {
398
+ validateOtlpPayload(payload);
399
+ const file = path.join(this.config.spoolDir, `span-${crypto.randomUUID()}.json`);
400
+ this.atomicWrite(file, Buffer.from(JSON.stringify(payload)));
401
+ try {
402
+ this.prune();
403
+ }
404
+ catch { /* telemetry cannot alter business behavior */ }
405
+ return file;
406
+ }
407
+ list(limit = Number.MAX_SAFE_INTEGER, quarantineCorrupt = false) {
408
+ const output = [];
409
+ let dropped = 0;
410
+ for (const entry of this.readyFiles().slice(0, limit)) {
411
+ let descriptor;
412
+ let invalid = false;
413
+ try {
414
+ descriptor = fs.openSync(entry.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
415
+ const stat = fs.fstatSync(descriptor);
416
+ if (!stat.isFile() || stat.dev !== entry.device || stat.ino !== entry.inode)
417
+ throw new Error("telemetry spool changed");
418
+ const payload = JSON.parse(fs.readFileSync(descriptor, "utf8"));
419
+ validateOtlpPayload(payload);
420
+ output.push({ file: entry.file, payload, device: stat.dev, inode: stat.ino });
421
+ }
422
+ catch {
423
+ invalid = true;
424
+ }
425
+ finally {
426
+ if (descriptor !== undefined)
427
+ try {
428
+ fs.closeSync(descriptor);
429
+ }
430
+ catch { /* isolated */ }
431
+ }
432
+ if (invalid) {
433
+ if (!quarantineCorrupt)
434
+ throw new Error("invalid telemetry spool record");
435
+ const quarantine = path.join(this.config.spoolDir, `.corrupt-${crypto.randomUUID()}.json`);
436
+ try {
437
+ fs.renameSync(entry.file, quarantine);
438
+ const stat = fs.lstatSync(quarantine);
439
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.dev !== entry.device || stat.ino !== entry.inode)
440
+ throw new Error("record changed");
441
+ dropped += 1;
442
+ }
443
+ catch {
444
+ try {
445
+ fs.renameSync(quarantine, entry.file);
446
+ }
447
+ catch { /* preserve */ }
448
+ }
449
+ }
450
+ }
451
+ if (dropped)
452
+ try {
453
+ this.updateDiagnostics({ droppedFiles: this.diagnostics().droppedFiles + dropped, lastErrorCategory: "corrupt_spool" });
454
+ }
455
+ catch { /* isolated */ }
456
+ return output;
457
+ }
458
+ acknowledge(records) {
459
+ const root = `${path.resolve(this.config.spoolDir)}${path.sep}`;
460
+ const moved = [];
461
+ try {
462
+ for (const record of records) {
463
+ const resolved = path.resolve(record.file);
464
+ const quarantine = path.join(this.config.spoolDir, `.ack-${crypto.randomUUID()}.tmp`);
465
+ if (!resolved.startsWith(root) || !READY.test(path.basename(resolved)))
466
+ throw new Error("invalid record");
467
+ fs.renameSync(resolved, quarantine);
468
+ moved.push({ original: resolved, quarantine });
469
+ const stat = fs.lstatSync(quarantine);
470
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.dev !== record.device || stat.ino !== record.inode)
471
+ throw new Error("record changed");
472
+ }
473
+ for (const entry of moved)
474
+ fs.unlinkSync(entry.quarantine);
475
+ const directory = fs.openSync(this.config.spoolDir, fs.constants.O_RDONLY);
476
+ try {
477
+ fs.fsyncSync(directory);
478
+ }
479
+ finally {
480
+ fs.closeSync(directory);
481
+ }
482
+ }
483
+ catch {
484
+ for (const entry of moved.reverse())
485
+ try {
486
+ fs.renameSync(entry.quarantine, entry.original);
487
+ }
488
+ catch { /* preserve without deleting */ }
489
+ throw new Error("telemetry acknowledgement failed");
490
+ }
491
+ }
492
+ diagnostics() {
493
+ try {
494
+ const parsed = JSON.parse(fs.readFileSync(path.join(this.config.spoolDir, "diagnostics.json"), "utf8"));
495
+ return { droppedFiles: Number.isSafeInteger(parsed.droppedFiles) && Number(parsed.droppedFiles) >= 0 ? Number(parsed.droppedFiles) : 0,
496
+ cleanedRemnantFiles: Number.isSafeInteger(parsed.cleanedRemnantFiles) && Number(parsed.cleanedRemnantFiles) >= 0 ? Number(parsed.cleanedRemnantFiles) : 0,
497
+ droppedSpans: Number.isSafeInteger(parsed.droppedSpans) && Number(parsed.droppedSpans) >= 0 ? Number(parsed.droppedSpans) : 0,
498
+ lastUploadAt: typeof parsed.lastUploadAt === "string" ? parsed.lastUploadAt : null,
499
+ lastErrorCategory: typeof parsed.lastErrorCategory === "string" ? parsed.lastErrorCategory : null };
500
+ }
501
+ catch {
502
+ return emptyDiagnostics();
503
+ }
504
+ }
505
+ updateDiagnostics(patch) {
506
+ this.atomicWrite(path.join(this.config.spoolDir, "diagnostics.json"), Buffer.from(JSON.stringify({ ...this.diagnostics(), ...patch })));
507
+ }
508
+ prune(now = Date.now()) {
509
+ const release = this.acquireLease();
510
+ if (!release)
511
+ return;
512
+ try {
513
+ let files = [...this.readyFiles().map((entry) => ({ ...entry, directory: false, remnant: false })), ...this.remnantFiles()]
514
+ .sort((a, b) => a.mtimeMs - b.mtimeMs || a.file.localeCompare(b.file));
515
+ let dropped = 0;
516
+ let cleaned = 0;
517
+ const remove = (entry) => { if (this.removeManaged(entry)) {
518
+ if (entry.remnant)
519
+ cleaned += 1;
520
+ else
521
+ dropped += 1;
522
+ } };
523
+ for (const entry of files.filter((item) => now - item.mtimeMs > this.config.maxAgeMs))
524
+ remove(entry);
525
+ files = [...this.readyFiles().map((entry) => ({ ...entry, directory: false, remnant: false })), ...this.remnantFiles()]
526
+ .sort((a, b) => a.mtimeMs - b.mtimeMs || a.file.localeCompare(b.file));
527
+ let bytes = files.reduce((sum, entry) => sum + entry.size, 0);
528
+ while (files.length > this.config.maxFiles || bytes > this.config.maxBytes) {
529
+ const oldest = files.shift();
530
+ if (!oldest)
531
+ break;
532
+ const before = dropped + cleaned;
533
+ remove(oldest);
534
+ if (dropped + cleaned > before)
535
+ bytes -= oldest.size;
536
+ }
537
+ if (dropped || cleaned)
538
+ try {
539
+ const diagnostic = this.diagnostics();
540
+ this.updateDiagnostics({ droppedFiles: diagnostic.droppedFiles + dropped,
541
+ cleanedRemnantFiles: diagnostic.cleanedRemnantFiles + cleaned });
542
+ }
543
+ catch { /* isolated */ }
544
+ }
545
+ finally {
546
+ release();
547
+ }
548
+ }
549
+ status(now = Date.now()) {
550
+ try {
551
+ const files = this.readyFiles();
552
+ const remnants = this.remnantFiles();
553
+ const diagnostic = this.diagnostics();
554
+ return { queuedFiles: files.length, queuedBytes: files.reduce((sum, item) => sum + item.size, 0),
555
+ oldestAgeMs: files[0] ? Math.max(0, now - files[0].mtimeMs) : null,
556
+ remnantFiles: remnants.length, remnantBytes: remnants.reduce((sum, item) => sum + item.size, 0),
557
+ oldestRemnantAgeMs: remnants[0] ? Math.max(0, now - remnants[0].mtimeMs) : null, ...diagnostic };
558
+ }
559
+ catch {
560
+ return { queuedFiles: 0, queuedBytes: 0, oldestAgeMs: null, remnantFiles: 0, remnantBytes: 0,
561
+ oldestRemnantAgeMs: null, ...emptyDiagnostics(), lastErrorCategory: "spool" };
562
+ }
563
+ }
564
+ exportBundle(destination) {
565
+ const release = this.acquireLease();
566
+ if (!release)
567
+ throw new Error("telemetry queue busy");
568
+ try {
569
+ const records = this.list().map(({ payload }) => { const bytes = Buffer.from(JSON.stringify(payload)); return { sha256: crypto.createHash("sha256").update(bytes).digest("hex"), payload }; });
570
+ const bundle = { format: "larkin-otlp-bundle", version: 1, createdAt: new Date().toISOString(), records };
571
+ const compressed = zlib.gzipSync(Buffer.from(JSON.stringify(bundle)), { level: 9 });
572
+ const descriptor = fs.openSync(destination, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
573
+ try {
574
+ fs.writeFileSync(descriptor, compressed);
575
+ fs.fsyncSync(descriptor);
576
+ }
577
+ finally {
578
+ fs.closeSync(descriptor);
579
+ }
580
+ return { records: records.length, sha256: crypto.createHash("sha256").update(compressed).digest("hex") };
581
+ }
582
+ finally {
583
+ release();
584
+ }
585
+ }
586
+ importBundle(source, maxBundleBytes = 128 * 1024 * 1024) {
587
+ let descriptor;
588
+ let bytes;
589
+ try {
590
+ descriptor = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
591
+ const stat = fs.fstatSync(descriptor);
592
+ if (!stat.isFile() || stat.size > maxBundleBytes)
593
+ throw new Error("invalid telemetry bundle");
594
+ bytes = fs.readFileSync(descriptor);
595
+ }
596
+ catch {
597
+ throw new Error("invalid telemetry bundle");
598
+ }
599
+ finally {
600
+ if (descriptor !== undefined)
601
+ fs.closeSync(descriptor);
602
+ }
603
+ let parsed;
604
+ try {
605
+ parsed = JSON.parse(zlib.gunzipSync(bytes, { maxOutputLength: maxBundleBytes }).toString("utf8"));
606
+ }
607
+ catch {
608
+ throw new Error("invalid telemetry bundle");
609
+ }
610
+ if (parsed.format !== "larkin-otlp-bundle" || parsed.version !== 1 || !Array.isArray(parsed.records) || parsed.records.length > 100_000)
611
+ throw new Error("invalid telemetry bundle");
612
+ const validated = parsed.records.map((record) => {
613
+ validateOtlpPayload(record?.payload);
614
+ const payloadBytes = Buffer.from(JSON.stringify(record.payload));
615
+ const sha256 = crypto.createHash("sha256").update(payloadBytes).digest("hex");
616
+ if (sha256 !== record.sha256)
617
+ throw new Error("invalid telemetry bundle");
618
+ return { sha256, payloadBytes };
619
+ });
620
+ const release = this.acquireLease();
621
+ if (!release)
622
+ throw new Error("telemetry queue busy");
623
+ const added = [];
624
+ let duplicates = 0;
625
+ try {
626
+ for (const record of validated) {
627
+ const destination = path.join(this.config.spoolDir, `import-${record.sha256}.json`);
628
+ if (fs.existsSync(destination)) {
629
+ duplicates += 1;
630
+ continue;
631
+ }
632
+ this.atomicWrite(destination, record.payloadBytes);
633
+ added.push(destination);
634
+ }
635
+ }
636
+ catch (error) {
637
+ for (const file of added)
638
+ try {
639
+ fs.unlinkSync(file);
640
+ }
641
+ catch { /* rollback best effort */ }
642
+ throw error;
643
+ }
644
+ finally {
645
+ release();
646
+ }
647
+ try {
648
+ this.prune();
649
+ }
650
+ catch { /* isolated */ }
651
+ return { imported: added.length, duplicates };
652
+ }
653
+ }