@hivefloor/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,629 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ releaseSpool,
4
+ takeSpool
5
+ } from "./chunk-SGDM7MAT.js";
6
+ import {
7
+ buildProfiles,
8
+ cliVersionOs,
9
+ scanProcesses
10
+ } from "./chunk-6UZBZ7LR.js";
11
+ import {
12
+ claudeDirsFromRc,
13
+ profileKeyOf,
14
+ profileKeys,
15
+ uniqueProfileKey
16
+ } from "./chunk-M3PLXINO.js";
17
+ import {
18
+ readMachine,
19
+ writeMachine,
20
+ writeServiceState
21
+ } from "./chunk-3IMV5EHP.js";
22
+ import {
23
+ VERSION
24
+ } from "./chunk-IEYWMDXA.js";
25
+ import {
26
+ apiRequest
27
+ } from "./chunk-PY74LT2W.js";
28
+ import {
29
+ configDir,
30
+ home,
31
+ isRecord,
32
+ machineSpoolFile,
33
+ serviceLogFile
34
+ } from "./chunk-WNDUGQDX.js";
35
+ import {
36
+ HOOK_BATCH_MAX,
37
+ MACHINE_HEARTBEAT_SECONDS,
38
+ MACHINE_MIN_INTERVAL_SECONDS,
39
+ machineHeartbeatResponseSchema,
40
+ parseUsageKey,
41
+ runCmd
42
+ } from "./chunk-RE2EQZWL.js";
43
+ import "./chunk-K3NQKI34.js";
44
+
45
+ // src/service/run.ts
46
+ import fs2 from "fs";
47
+
48
+ // src/service/collectors.ts
49
+ import fs from "fs";
50
+ import os from "os";
51
+ import path from "path";
52
+ var USAGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
53
+ var USAGE_MAX_FILES = 200;
54
+ var USAGE_MAX_KEYS = 64;
55
+ var CHUNK_BYTES = 64 * 1024;
56
+ var MAX_LINE_BYTES = 2 * 1024 * 1024;
57
+ var MAX_SEEN_IDS = 1e4;
58
+ var SESSION_ID_RE = /^[A-Za-z0-9._:-]{1,60}$/;
59
+ function asNonNegInt(v) {
60
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) return void 0;
61
+ return Math.floor(v);
62
+ }
63
+ function asPositiveCost(v) {
64
+ if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) return void 0;
65
+ return v;
66
+ }
67
+ function asShortId(v) {
68
+ return typeof v === "string" && v.length > 0 && v.length <= 120 ? v : void 0;
69
+ }
70
+ function isDir(p) {
71
+ try {
72
+ return fs.statSync(p).isDirectory();
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ function claudeProfileDirs(env, homeDir) {
78
+ const std = path.resolve(path.join(homeDir, ".claude"));
79
+ const raw = [std];
80
+ if (env.CLAUDE_CONFIG_DIR) raw.push(path.resolve(env.CLAUDE_CONFIG_DIR));
81
+ raw.push(...claudeDirsFromRc(homeDir));
82
+ try {
83
+ for (const name of fs.readdirSync(homeDir).sort()) {
84
+ if (!name.startsWith(".claude") || name === ".claude.json" || name.endsWith(".bak")) continue;
85
+ raw.push(path.resolve(path.join(homeDir, name)));
86
+ }
87
+ } catch {
88
+ }
89
+ const xdg = path.resolve(path.join(env.XDG_CONFIG_HOME || path.join(homeDir, ".config"), "claude"));
90
+ raw.push(xdg);
91
+ const unique = [];
92
+ const seen = /* @__PURE__ */ new Set();
93
+ for (const d of raw) {
94
+ if (seen.has(d)) continue;
95
+ if (d !== std && !isDir(path.join(d, "projects"))) continue;
96
+ seen.add(d);
97
+ unique.push(d);
98
+ }
99
+ return profileKeys("claude", unique, homeDir);
100
+ }
101
+ function codexProfileDirs(env, homeDir) {
102
+ const std = path.resolve(path.join(homeDir, ".codex"));
103
+ const raw = [std];
104
+ if (env.CODEX_HOME) raw.push(path.resolve(env.CODEX_HOME));
105
+ try {
106
+ for (const name of fs.readdirSync(homeDir).sort()) {
107
+ if (!name.startsWith(".codex") || name.endsWith(".bak")) continue;
108
+ raw.push(path.resolve(path.join(homeDir, name)));
109
+ }
110
+ } catch {
111
+ }
112
+ const unique = [];
113
+ const seen = /* @__PURE__ */ new Set();
114
+ for (const d of raw) {
115
+ if (seen.has(d)) continue;
116
+ if (d !== std && !isDir(path.join(d, "sessions"))) continue;
117
+ seen.add(d);
118
+ unique.push(d);
119
+ }
120
+ const map = profileKeys("codex", unique, homeDir);
121
+ const multi = path.join(homeDir, "Library", "Application Support", "Multi Codex", "profiles");
122
+ if (isDir(multi)) {
123
+ const used = new Set(map.values());
124
+ try {
125
+ for (const name of fs.readdirSync(multi).sort()) {
126
+ if (name.startsWith(".")) continue;
127
+ const dir = path.resolve(path.join(multi, name, "codex"));
128
+ if (map.has(dir) || !isDir(path.join(dir, "sessions"))) continue;
129
+ const key = uniqueProfileKey(profileKeyOf("codex", name, homeDir), used);
130
+ map.set(dir, key);
131
+ }
132
+ } catch {
133
+ }
134
+ }
135
+ return map;
136
+ }
137
+ function codexSessionFromFile(filePath) {
138
+ const base = path.basename(filePath, ".jsonl");
139
+ const stripped = base.replace(/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(?:\.\d+)?-/i, "");
140
+ return { session: stripped, fromRollout: stripped !== base || base.startsWith("rollout") };
141
+ }
142
+ function collectClaudeFiles(dir, profile, cutoffMs, out) {
143
+ const projectsDir = path.join(dir, "projects");
144
+ let projects;
145
+ try {
146
+ projects = fs.readdirSync(projectsDir, { withFileTypes: true });
147
+ } catch {
148
+ return;
149
+ }
150
+ for (const proj of projects) {
151
+ if (!proj.isDirectory()) continue;
152
+ const projPath = path.join(projectsDir, proj.name);
153
+ let entries;
154
+ try {
155
+ entries = fs.readdirSync(projPath, { withFileTypes: true });
156
+ } catch {
157
+ continue;
158
+ }
159
+ for (const ent of entries) {
160
+ if (!ent.isFile() || !ent.name.endsWith(".jsonl")) continue;
161
+ const session = ent.name.slice(0, -6);
162
+ if (!SESSION_ID_RE.test(session)) continue;
163
+ const filePath = path.join(projPath, ent.name);
164
+ try {
165
+ const st = fs.statSync(filePath);
166
+ if (st.mtimeMs < cutoffMs || st.size <= 0) continue;
167
+ out.push({ tool: "claude", profile, session, fromRollout: false, filePath, mtimeMs: st.mtimeMs, size: st.size, ino: st.ino });
168
+ } catch {
169
+ }
170
+ }
171
+ }
172
+ }
173
+ function collectCodexFiles(dir, profile, cutoffMs, out) {
174
+ const walk = (cur, depth) => {
175
+ if (depth > 6) return;
176
+ let entries;
177
+ try {
178
+ entries = fs.readdirSync(cur, { withFileTypes: true });
179
+ } catch {
180
+ return;
181
+ }
182
+ for (const ent of entries) {
183
+ const full = path.join(cur, ent.name);
184
+ if (ent.isDirectory()) {
185
+ walk(full, depth + 1);
186
+ } else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
187
+ const { session, fromRollout } = codexSessionFromFile(ent.name);
188
+ if (!SESSION_ID_RE.test(session)) continue;
189
+ try {
190
+ const st = fs.statSync(full);
191
+ if (st.mtimeMs < cutoffMs || st.size <= 0) continue;
192
+ out.push({ tool: "codex", profile, session, fromRollout, filePath: full, mtimeMs: st.mtimeMs, size: st.size, ino: st.ino });
193
+ } catch {
194
+ }
195
+ }
196
+ }
197
+ };
198
+ walk(path.join(dir, "sessions"), 0);
199
+ }
200
+ function readLinesFromOffset(filePath, startOffset, onLine) {
201
+ const fd = fs.openSync(filePath, "r");
202
+ try {
203
+ const buf = Buffer.allocUnsafe(CHUNK_BYTES);
204
+ let pos = startOffset;
205
+ let carry = Buffer.alloc(0);
206
+ let tooLong = false;
207
+ for (; ; ) {
208
+ const bytesRead = fs.readSync(fd, buf, 0, CHUNK_BYTES, pos);
209
+ if (bytesRead <= 0) break;
210
+ pos += bytesRead;
211
+ const chunk = buf.subarray(0, bytesRead);
212
+ let searchFrom = 0;
213
+ for (; ; ) {
214
+ const nl = chunk.indexOf(10, searchFrom);
215
+ if (nl === -1) {
216
+ const tail = chunk.subarray(searchFrom);
217
+ if (!tooLong) {
218
+ if (carry.length + tail.length > MAX_LINE_BYTES) {
219
+ carry = Buffer.alloc(0);
220
+ tooLong = true;
221
+ } else if (tail.length > 0) {
222
+ carry = carry.length === 0 ? Buffer.from(tail) : Buffer.concat([carry, tail]);
223
+ }
224
+ }
225
+ break;
226
+ }
227
+ const part = chunk.subarray(searchFrom, nl);
228
+ searchFrom = nl + 1;
229
+ if (tooLong) {
230
+ tooLong = false;
231
+ carry = Buffer.alloc(0);
232
+ continue;
233
+ }
234
+ if (carry.length + part.length <= MAX_LINE_BYTES) {
235
+ const line = carry.length === 0 ? part.toString("utf8") : Buffer.concat([carry, part]).toString("utf8");
236
+ carry = Buffer.alloc(0);
237
+ onLine(line);
238
+ } else {
239
+ carry = Buffer.alloc(0);
240
+ }
241
+ }
242
+ }
243
+ if (!tooLong && carry.length > 0) {
244
+ const text = carry.toString("utf8").trim();
245
+ if (text !== "") {
246
+ if (!text.startsWith("{") || !text.endsWith("}") || !onLine(text)) {
247
+ return pos - carry.length;
248
+ }
249
+ }
250
+ }
251
+ return pos;
252
+ } finally {
253
+ try {
254
+ fs.closeSync(fd);
255
+ } catch {
256
+ }
257
+ }
258
+ }
259
+ function parseClaudeLine(state, line) {
260
+ const trimmed = line.trim();
261
+ if (trimmed === "") return true;
262
+ if (trimmed[0] !== "{") return false;
263
+ if (!trimmed.includes('"usage"')) return true;
264
+ let raw;
265
+ try {
266
+ raw = JSON.parse(trimmed);
267
+ } catch {
268
+ return false;
269
+ }
270
+ if (!isRecord(raw)) return true;
271
+ if (raw.type !== void 0 && raw.type !== "assistant") return true;
272
+ const msg = isRecord(raw.message) ? raw.message : void 0;
273
+ if (!msg) return true;
274
+ if (msg.role !== void 0 && msg.role !== "assistant") return true;
275
+ const usage = isRecord(msg.usage) ? msg.usage : void 0;
276
+ if (!usage) return true;
277
+ const msgId = asShortId(msg.id ?? msg.message_id ?? raw.message_id ?? raw.messageId);
278
+ if (msgId) {
279
+ if (state.seenIds.has(msgId)) return true;
280
+ if (state.seenIds.size < MAX_SEEN_IDS) state.seenIds.add(msgId);
281
+ }
282
+ const input = asNonNegInt(usage.input_tokens);
283
+ const cacheCreate = asNonNegInt(usage.cache_creation_input_tokens);
284
+ const cacheRead = asNonNegInt(usage.cache_read_input_tokens);
285
+ const output = asNonNegInt(usage.output_tokens);
286
+ if (input === void 0 && cacheCreate === void 0 && cacheRead === void 0 && output === void 0) return true;
287
+ state.tokensIn += (input ?? 0) + (cacheCreate ?? 0) + (cacheRead ?? 0);
288
+ state.tokensOut += output ?? 0;
289
+ state.hasUsage = true;
290
+ const cost = asPositiveCost(raw.costUSD ?? raw.cost_usd ?? msg.costUSD ?? msg.cost_usd ?? usage.cost_usd ?? usage.costUSD);
291
+ if (cost !== void 0) {
292
+ state.costUsd += cost;
293
+ state.hasCost = true;
294
+ }
295
+ return true;
296
+ }
297
+ function parseCodexLine(state, line) {
298
+ const trimmed = line.trim();
299
+ if (trimmed === "") return true;
300
+ if (trimmed[0] !== "{") return false;
301
+ if (!trimmed.includes("token_count") && !trimmed.includes("session_meta")) return true;
302
+ let raw;
303
+ try {
304
+ raw = JSON.parse(trimmed);
305
+ } catch {
306
+ return false;
307
+ }
308
+ if (!isRecord(raw)) return true;
309
+ if (raw.type === "session_meta" && state.fromRollout && isRecord(raw.payload)) {
310
+ const id = asShortId(raw.payload.id ?? raw.payload.session_id);
311
+ if (id && SESSION_ID_RE.test(id)) state.session = id;
312
+ return true;
313
+ }
314
+ const payload = isRecord(raw.payload) ? raw.payload : isRecord(raw.msg) ? raw.msg : void 0;
315
+ const ev = raw.type === "token_count" || raw.event === "token_count" ? raw : payload && (payload.type === "token_count" || payload.event === "token_count") ? payload : void 0;
316
+ if (!ev) return true;
317
+ const info = isRecord(ev.info) ? ev.info : void 0;
318
+ const total = (info && (isRecord(info.total_token_usage) ? info.total_token_usage : isRecord(info.total_usage) ? info.total_usage : isRecord(info.usage) ? info.usage : void 0)) ?? (isRecord(ev.total_token_usage) ? ev.total_token_usage : isRecord(ev.total_usage) ? ev.total_usage : isRecord(ev.usage) ? ev.usage : info ?? ev);
319
+ const tin = asNonNegInt(total.input_tokens ?? total.tokens_in ?? total.prompt_tokens);
320
+ const tout = asNonNegInt(total.output_tokens ?? total.tokens_out ?? total.completion_tokens);
321
+ if (tin === void 0 && tout === void 0) return true;
322
+ state.tokensIn = tin ?? 0;
323
+ state.tokensOut = tout ?? 0;
324
+ state.hasUsage = true;
325
+ const cost = asPositiveCost(total.cost_usd ?? total.costUSD ?? ev.cost_usd ?? ev.costUSD ?? raw.cost_usd ?? raw.costUSD);
326
+ if (cost !== void 0) {
327
+ state.costUsd = cost;
328
+ state.hasCost = true;
329
+ }
330
+ return true;
331
+ }
332
+ function createUsageCollector(opts = {}) {
333
+ const files = /* @__PURE__ */ new Map();
334
+ return () => {
335
+ try {
336
+ const homeDir = opts.homeDir ?? home();
337
+ const env = opts.env ?? process.env;
338
+ const now = opts.now ? opts.now() : Date.now();
339
+ const windowMs = opts.windowMs ?? USAGE_WINDOW_MS;
340
+ const maxFiles = opts.maxFiles ?? USAGE_MAX_FILES;
341
+ const maxKeys = opts.maxKeys;
342
+ const cutoffMs = now - windowMs;
343
+ const candidates = [];
344
+ for (const [dir, profile] of claudeProfileDirs(env, homeDir)) {
345
+ collectClaudeFiles(dir, profile, cutoffMs, candidates);
346
+ }
347
+ for (const [dir, profile] of codexProfileDirs(env, homeDir)) {
348
+ collectCodexFiles(dir, profile, cutoffMs, candidates);
349
+ }
350
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
351
+ const active = candidates.slice(0, maxFiles);
352
+ const activePaths = new Set(active.map((c) => c.filePath));
353
+ for (const k of files.keys()) {
354
+ if (!activePaths.has(k)) files.delete(k);
355
+ }
356
+ const usage = {};
357
+ const seenSessions = /* @__PURE__ */ new Set();
358
+ let keyCount = 0;
359
+ for (const c of active) {
360
+ let state = files.get(c.filePath);
361
+ if (!state || c.size < state.offset || c.ino !== 0 && state.ino !== 0 && c.ino !== state.ino) {
362
+ state = {
363
+ tool: c.tool,
364
+ profile: c.profile,
365
+ session: c.session,
366
+ fromRollout: c.fromRollout,
367
+ offset: 0,
368
+ size: 0,
369
+ mtimeMs: 0,
370
+ ino: c.ino,
371
+ tokensIn: 0,
372
+ tokensOut: 0,
373
+ costUsd: 0,
374
+ hasCost: false,
375
+ hasUsage: false,
376
+ seenIds: /* @__PURE__ */ new Set()
377
+ };
378
+ files.set(c.filePath, state);
379
+ } else {
380
+ state.profile = c.profile;
381
+ }
382
+ if (c.size > state.size || c.mtimeMs !== state.mtimeMs) {
383
+ try {
384
+ const parser = state.tool === "claude" ? parseClaudeLine : parseCodexLine;
385
+ const st = state;
386
+ state.offset = readLinesFromOffset(c.filePath, state.offset, (line) => parser(st, line));
387
+ state.size = c.size;
388
+ state.mtimeMs = c.mtimeMs;
389
+ } catch {
390
+ continue;
391
+ }
392
+ }
393
+ if (!state.hasUsage) continue;
394
+ const sessionKey = `${state.tool}:${state.profile}:${state.session}`;
395
+ if (seenSessions.has(sessionKey)) continue;
396
+ const base = `${state.tool}:${state.profile}:s:${state.session}`;
397
+ const kIn = `${base}:tokens_in`;
398
+ const kOut = `${base}:tokens_out`;
399
+ const kCost = `${base}:cost_usd`;
400
+ if (kIn.length > 80 || kOut.length > 80 || !parseUsageKey(kIn) || !parseUsageKey(kOut)) continue;
401
+ const includeCost = state.hasCost && kCost.length <= 80 && parseUsageKey(kCost) !== null;
402
+ const needed = includeCost ? 3 : 2;
403
+ if (maxKeys !== void 0 && keyCount + needed > maxKeys) continue;
404
+ seenSessions.add(sessionKey);
405
+ usage[kIn] = state.tokensIn;
406
+ usage[kOut] = state.tokensOut;
407
+ if (includeCost) usage[kCost] = Math.round(state.costUsd * 1e6) / 1e6;
408
+ keyCount += needed;
409
+ }
410
+ return keyCount > 0 ? { usage } : void 0;
411
+ } catch {
412
+ return void 0;
413
+ }
414
+ };
415
+ }
416
+ var basics = () => {
417
+ const total = os.totalmem();
418
+ const free = os.freemem();
419
+ return {
420
+ mem_total_mb: Math.round(total / 1048576),
421
+ mem_pct: total > 0 ? Math.round((total - free) / total * 1e3) / 10 : void 0,
422
+ load1: Math.round(os.loadavg()[0] * 100) / 100
423
+ };
424
+ };
425
+ var usageCounters = createUsageCollector({ maxKeys: USAGE_MAX_KEYS });
426
+ var collectors = [basics, usageCounters];
427
+ async function collectStats(list = collectors) {
428
+ const stats = {};
429
+ for (const c of list) {
430
+ try {
431
+ const part = await Promise.race([Promise.resolve(c()), new Promise((r) => setTimeout(() => r(void 0), 2e3).unref())]);
432
+ if (part) {
433
+ for (const [k, v] of Object.entries(part)) {
434
+ if (v === void 0) continue;
435
+ if (k === "usage" && isRecord(v)) {
436
+ const merged = isRecord(stats.usage) ? { ...stats.usage } : {};
437
+ for (const [uk, uv] of Object.entries(v)) {
438
+ if (typeof uv !== "number" || !Number.isFinite(uv)) continue;
439
+ if (!(uk in merged) && Object.keys(merged).length >= USAGE_MAX_KEYS) break;
440
+ merged[uk] = uv;
441
+ }
442
+ if (Object.keys(merged).length > 0) stats.usage = merged;
443
+ } else {
444
+ stats[k] = v;
445
+ }
446
+ }
447
+ }
448
+ } catch {
449
+ }
450
+ }
451
+ return Object.keys(stats).length > 0 ? stats : void 0;
452
+ }
453
+
454
+ // src/service/run.ts
455
+ var FLUSH_EVERY_MS = 2e3;
456
+ var FLUSH_BUDGET_MS = 5e3;
457
+ var DISCOVERY_EVERY_MS = 6e4;
458
+ var BEAT_EVERY_MS = 1e4;
459
+ var BACKOFF_MAX_MS = 3e5;
460
+ var LOG_MAX_BYTES = 1024 * 1024;
461
+ var newSched = (now) => ({ heartbeat: now, flush: now, discovery: now + 5e3, beat: 0, failures: 0, quietUntil: 0, interval: MACHINE_HEARTBEAT_SECONDS * 1e3, logged: "", started: now, stop: false });
462
+ function backoffMs(failures, random) {
463
+ const base = Math.min(BACKOFF_MAX_MS, MACHINE_HEARTBEAT_SECONDS * 1e3 * 2 ** Math.max(0, failures - 1));
464
+ return Math.max(MACHINE_MIN_INTERVAL_SECONDS * 1e3, Math.round(base * (0.8 + 0.4 * random())));
465
+ }
466
+ function log(st, now, state) {
467
+ if (st.logged === state) return;
468
+ st.logged = state;
469
+ try {
470
+ fs2.mkdirSync(configDir(), { recursive: true, mode: 448 });
471
+ const f = serviceLogFile();
472
+ let size = 0;
473
+ try {
474
+ size = fs2.statSync(f).size;
475
+ } catch {
476
+ }
477
+ if (size > LOG_MAX_BYTES) fs2.writeFileSync(f, "", { mode: 384 });
478
+ fs2.appendFileSync(f, `${new Date(now).toISOString()} ${state}
479
+ `, { mode: 384 });
480
+ } catch {
481
+ }
482
+ }
483
+ async function call(deps, m, path2, body) {
484
+ try {
485
+ const r = await apiRequest(deps.ctx, m.host, `/api/v1/machine${path2}`, { method: "POST", token: m.token, body, timeoutMs: 1e4 });
486
+ if (r.status >= 200 && r.status < 300) return { outcome: "ok", json: r.json };
487
+ if (r.status === 401) return { outcome: "revoked" };
488
+ if (r.status === 429 || r.status >= 500) return { outcome: "retry" };
489
+ return { outcome: "drop" };
490
+ } catch {
491
+ return { outcome: "retry" };
492
+ }
493
+ }
494
+ function fail(st, deps, now, what) {
495
+ st.failures += 1;
496
+ st.lastError = what;
497
+ st.quietUntil = now + backoffMs(st.failures, deps.random ?? Math.random);
498
+ log(st, now, `error ${what}; retrying in ${Math.round((st.quietUntil - now) / 1e3)}s`);
499
+ }
500
+ function succeed(st, now) {
501
+ if (st.failures > 0) log(st, now, "online");
502
+ st.failures = 0;
503
+ st.quietUntil = 0;
504
+ st.lastOk = now;
505
+ st.lastError = void 0;
506
+ }
507
+ function beat(st, now) {
508
+ st.beat = now;
509
+ writeServiceState({ pid: process.pid, beat: now, started: st.started, ...st.lastOk ? { last_ok: st.lastOk } : {}, ...st.lastError ? { last_error: st.lastError } : {} });
510
+ }
511
+ async function serviceStep(deps, st) {
512
+ const now = deps.ctx.now();
513
+ const m = readMachine();
514
+ if (!m || m.revoked) {
515
+ log(st, now, "stopped: not connected");
516
+ return false;
517
+ }
518
+ const revoke = () => {
519
+ writeMachine({ ...m, revoked: true });
520
+ log(st, now, "stopped: the server no longer knows this machine (removed?)");
521
+ return false;
522
+ };
523
+ if (now - st.beat >= BEAT_EVERY_MS) {
524
+ beat(st, now);
525
+ }
526
+ if (now < st.quietUntil) return true;
527
+ if (now >= st.heartbeat) {
528
+ const stats = await collectStats(deps.collectors);
529
+ const r = await call(deps, m, "/heartbeat", { os: cliVersionOs(), cli_version: VERSION, ...stats ? { stats } : {} });
530
+ if (r.outcome === "revoked") return revoke();
531
+ if (r.outcome === "ok") {
532
+ const parsed = machineHeartbeatResponseSchema.safeParse(r.json);
533
+ const secs = parsed.success ? parsed.data.interval : MACHINE_HEARTBEAT_SECONDS;
534
+ st.interval = Math.max(MACHINE_MIN_INTERVAL_SECONDS, secs) * 1e3;
535
+ st.heartbeat = now + st.interval;
536
+ succeed(st, now);
537
+ if (st.logged === "") log(st, now, "online");
538
+ } else {
539
+ fail(st, deps, now, r.outcome === "drop" ? "heartbeat rejected" : "server unreachable");
540
+ st.heartbeat = st.quietUntil;
541
+ return true;
542
+ }
543
+ }
544
+ if (now >= st.flush) {
545
+ st.flush = now + FLUSH_EVERY_MS;
546
+ const claim = takeSpool(machineSpoolFile());
547
+ if (claim) {
548
+ const pending = claim.events;
549
+ const unsent = [];
550
+ const deadline = deps.ctx.now() + (deps.flushBudgetMs ?? FLUSH_BUDGET_MS);
551
+ let down = false;
552
+ let outOfTime = false;
553
+ for (let k = 0; k < pending.length; k += HOOK_BATCH_MAX) {
554
+ const batch = pending.slice(k, k + HOOK_BATCH_MAX);
555
+ if (down || outOfTime) {
556
+ unsent.push(...batch);
557
+ continue;
558
+ }
559
+ if (deps.ctx.now() >= deadline) {
560
+ unsent.push(...batch);
561
+ outOfTime = true;
562
+ continue;
563
+ }
564
+ const r = await call(deps, m, "/hooks", { events: batch });
565
+ if (r.outcome === "revoked") {
566
+ releaseSpool(claim, [...unsent, ...pending.slice(k)], machineSpoolFile());
567
+ return revoke();
568
+ }
569
+ if (r.outcome === "retry") {
570
+ unsent.push(...batch);
571
+ down = true;
572
+ }
573
+ beat(st, deps.ctx.now());
574
+ }
575
+ releaseSpool(claim, unsent, machineSpoolFile());
576
+ if (down) {
577
+ fail(st, deps, now, "server unreachable");
578
+ return true;
579
+ }
580
+ succeed(st, now);
581
+ }
582
+ }
583
+ if (now >= st.discovery) {
584
+ st.discovery = now + DISCOVERY_EVERY_MS;
585
+ let profiles = [];
586
+ try {
587
+ profiles = await (deps.profiles ?? (async () => buildProfiles(await scanProcesses({ platform: deps.ctx.platform ?? process.platform, run: deps.ctx.run ?? runCmd, env: deps.ctx.env }), deps.ctx.env)))();
588
+ } catch {
589
+ }
590
+ const r = await call(deps, m, "/discovery", { profiles });
591
+ if (r.outcome === "revoked") return revoke();
592
+ if (r.outcome === "retry") {
593
+ fail(st, deps, now, "server unreachable");
594
+ st.discovery = st.quietUntil;
595
+ } else if (r.outcome === "ok") succeed(st, now);
596
+ }
597
+ return true;
598
+ }
599
+ async function runService(deps) {
600
+ const st = newSched(deps.ctx.now());
601
+ const stop = () => {
602
+ st.stop = true;
603
+ };
604
+ process.on("SIGTERM", stop);
605
+ process.on("SIGINT", stop);
606
+ try {
607
+ while (!st.stop) {
608
+ const more = await serviceStep(deps, st);
609
+ if (!more) return 0;
610
+ await deps.ctx.sleep(500);
611
+ }
612
+ return 0;
613
+ } finally {
614
+ process.off("SIGTERM", stop);
615
+ process.off("SIGINT", stop);
616
+ }
617
+ }
618
+ export {
619
+ BACKOFF_MAX_MS,
620
+ BEAT_EVERY_MS,
621
+ DISCOVERY_EVERY_MS,
622
+ FLUSH_BUDGET_MS,
623
+ FLUSH_EVERY_MS,
624
+ LOG_MAX_BYTES,
625
+ backoffMs,
626
+ newSched,
627
+ runService,
628
+ serviceStep
629
+ };