primcli 1.36.0 → 1.37.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.
@@ -1,4 +1,4 @@
1
- import { _ as acquireListenLock, a as mailLockBusy, b as listenProcessIdentity, d as readMailJson, f as removeMailFile, g as ListenStateError, h as writeMailJson, i as mailId, l as privateMailDirectory, o as mailObject, s as mailString, v as compareListenProcessIdentity } from "./shared-mail-files-Cbwnf79a.js";
1
+ import { _ as acquireListenLock, a as mailLockBusy, b as listenProcessIdentity, d as readMailJson, f as removeMailFile, g as ListenStateError, h as writeMailJson, i as mailId, l as privateMailDirectory, o as mailObject, s as mailString, v as compareListenProcessIdentity } from "./shared-mail-files-CZqI1kj6.js";
2
2
  import { lstatSync, realpathSync } from "node:fs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { join } from "node:path";
@@ -14,6 +14,7 @@ const STALE_AFTER_MS = 15e3;
14
14
  const RESTART_WINDOW_MS = 10 * 6e4;
15
15
  const HEALTHY_RESET_MS = 10 * 6e4;
16
16
  const MAX_FAILURES = 5;
17
+ const RESTART_COOLDOWN_MS = 5 * 6e4;
17
18
  const phases = [
18
19
  "starting",
19
20
  "receiving",
@@ -26,11 +27,49 @@ const failureCodes = [
26
27
  "notification-outcome-unknown",
27
28
  "connection-changed",
28
29
  "receiving-failed",
29
- "restart-budget-exhausted"
30
+ "restart-budget-exhausted",
31
+ "disk-full",
32
+ "storage-unavailable",
33
+ "crashed",
34
+ "unrecognized"
35
+ ];
36
+ const HOLDING_FAILURE_CODES = [
37
+ "notification-outcome-unknown",
38
+ "connection-changed",
39
+ "unrecognized"
30
40
  ];
31
41
  function knownFailureCode(value) {
32
42
  return failureCodes.includes(value) ? value : null;
33
43
  }
44
+ /** A newer CLI may record a code this version does not know; keep the record readable. */
45
+ function savedFailureCode(value) {
46
+ if (value === null || value === void 0) return null;
47
+ return knownFailureCode(value) ?? "unrecognized";
48
+ }
49
+ const DISK_FULL_ERRORS = new Set(["ENOSPC", "EDQUOT"]);
50
+ const STORAGE_ERRORS = new Set([
51
+ "EIO",
52
+ "EROFS",
53
+ "EMFILE",
54
+ "ENFILE",
55
+ "EAGAIN",
56
+ "EBUSY",
57
+ "ENOMEM"
58
+ ]);
59
+ /** Classify local storage failures that a later retry can outlive. */
60
+ function storageFailureCode(error) {
61
+ const errno = (value) => value && typeof value === "object" ? value.code : void 0;
62
+ const code = errno(error) ?? errno(error && typeof error === "object" ? error.cause : null);
63
+ if (typeof code !== "string") return null;
64
+ if (DISK_FULL_ERRORS.has(code)) return "disk-full";
65
+ if (STORAGE_ERRORS.has(code)) return "storage-unavailable";
66
+ return null;
67
+ }
68
+ /** Whether a stopped receiver may be restarted without an explicit owner action. */
69
+ function backgroundListenRestartable(status) {
70
+ if (status.reason === "exited") return true;
71
+ return status.reason === "failed" && status.failureCode !== null && !HOLDING_FAILURE_CODES.includes(status.failureCode);
72
+ }
34
73
  function failureGuidance(code) {
35
74
  switch (code) {
36
75
  case "native-session-unavailable": return "Open the same native session with local-session support and check its socket permissions before restarting the listener.";
@@ -38,6 +77,26 @@ function failureGuidance(code) {
38
77
  case "connection-changed": return "The selected connection changed. Check the intended profile and API origin before starting the listener again.";
39
78
  case "receiving-failed": return "Inspect listener status and run the same command without --background for details.";
40
79
  case "restart-budget-exhausted": return "The listener repeatedly exited. Inspect listener status before starting it again.";
80
+ case "disk-full": return "The disk was full, so the receiver could not save its state. Free disk space; the receiver retries on its own.";
81
+ case "storage-unavailable": return "The receiver could not read or write its local state. Check the config directory's disk and permissions; the receiver retries on its own.";
82
+ case "crashed": return "The receiver exited without reporting a reason. It is restarted automatically; inspect status if this repeats.";
83
+ case "unrecognized": return "A newer Primitive CLI recorded this receiver's state. Inspect it with that version before restarting.";
84
+ }
85
+ }
86
+ /** One sentence an owner or agent can act on; never raw error text. */
87
+ function statusDetail(status) {
88
+ if (status.healthy) return null;
89
+ const guidance = status.failureCode ? failureGuidance(status.failureCode) : "";
90
+ const withGuidance = (text) => guidance ? `${text} ${guidance}` : text;
91
+ switch (status.reason) {
92
+ case "exited": return "The receiver stopped unexpectedly and new mail is not reaching this session. Any primitive command run from this session restarts it, or start it again with the same listen command.";
93
+ case "restarting": return withGuidance("The receiver is restarting; new mail waits until it is back.");
94
+ case "failed": return withGuidance("The receiver stopped and new mail is not reaching this session.");
95
+ case "stale": return withGuidance("The receiver has not recorded a heartbeat recently. It may be unable to write local state.");
96
+ case "unverifiable": return "The receiver's process cannot be verified.";
97
+ case "stopped": return "The receiver was stopped.";
98
+ case "absent": return "No background receiver is recorded for this session.";
99
+ default: return guidance || null;
41
100
  }
42
101
  }
43
102
  function backgroundListenToken(env = process.env) {
@@ -117,7 +176,7 @@ function readSupervisor(path) {
117
176
  "running",
118
177
  "stopped",
119
178
  "failed"
120
- ].includes(String(row.phase)) || !Number.isSafeInteger(row.updatedAt) || Number(row.updatedAt) < 0 || row.failureCode !== null && knownFailureCode(row.failureCode) === null) throw new ListenStateError("Background supervisor state is invalid. Preserve it before retrying.");
179
+ ].includes(String(row.phase)) || !Number.isSafeInteger(row.updatedAt) || Number(row.updatedAt) < 0 || row.failureCode !== null && typeof row.failureCode !== "string") throw new ListenStateError("Background supervisor state is invalid. Preserve it before retrying.");
121
180
  return {
122
181
  version: 1,
123
182
  token: mailId(row.token),
@@ -125,7 +184,7 @@ function readSupervisor(path) {
125
184
  identity: mailString(row.identity),
126
185
  phase: row.phase,
127
186
  updatedAt: Number(row.updatedAt),
128
- failureCode: knownFailureCode(row.failureCode)
187
+ failureCode: savedFailureCode(row.failureCode)
129
188
  };
130
189
  }
131
190
  function readState(path) {
@@ -142,7 +201,7 @@ function readState(path) {
142
201
  ...typeof value === "object" && Object.hasOwn(value, "failureCode") ? ["failureCode"] : [],
143
202
  "updatedAt"
144
203
  ]);
145
- if (row.version !== 1 || !Number.isSafeInteger(row.pid) || Number(row.pid) < 1 || !phases.includes(row.phase) || typeof row.detached !== "boolean" || row.configuration !== null && typeof row.configuration !== "string" || row.failureCode !== void 0 && row.failureCode !== null && knownFailureCode(row.failureCode) === null || !Number.isSafeInteger(row.updatedAt) || Number(row.updatedAt) < 0) throw new ListenStateError("Background listener state is invalid. Preserve it before retrying.");
204
+ if (row.version !== 1 || !Number.isSafeInteger(row.pid) || Number(row.pid) < 1 || !phases.includes(row.phase) || typeof row.detached !== "boolean" || row.configuration !== null && typeof row.configuration !== "string" || row.failureCode !== void 0 && row.failureCode !== null && typeof row.failureCode !== "string" || !Number.isSafeInteger(row.updatedAt) || Number(row.updatedAt) < 0) throw new ListenStateError("Background listener state is invalid. Preserve it before retrying.");
146
205
  return {
147
206
  version: 1,
148
207
  token: mailId(row.token),
@@ -151,7 +210,7 @@ function readState(path) {
151
210
  phase: row.phase,
152
211
  detached: row.detached,
153
212
  configuration: configurationFingerprint(row.configuration === null ? void 0 : String(row.configuration)),
154
- failureCode: knownFailureCode(row.failureCode),
213
+ failureCode: savedFailureCode(row.failureCode),
155
214
  updatedAt: Number(row.updatedAt)
156
215
  };
157
216
  }
@@ -174,7 +233,12 @@ function status(state) {
174
233
  healthy: false,
175
234
  failureCode: null,
176
235
  reason: "absent",
177
- updatedAt: null
236
+ updatedAt: null,
237
+ detail: statusDetail({
238
+ healthy: false,
239
+ reason: "absent",
240
+ failureCode: null
241
+ })
178
242
  };
179
243
  const age = Date.now() - state.updatedAt;
180
244
  const ownership = owner(state);
@@ -186,7 +250,12 @@ function status(state) {
186
250
  healthy: reason === null,
187
251
  failureCode: state.failureCode,
188
252
  reason,
189
- updatedAt: state.updatedAt
253
+ updatedAt: state.updatedAt,
254
+ detail: statusDetail({
255
+ healthy: reason === null,
256
+ reason,
257
+ failureCode: state.failureCode
258
+ })
190
259
  };
191
260
  }
192
261
  /** Absence means no managed record, not proof that an older foreground listener is absent. */
@@ -200,16 +269,47 @@ function backgroundListenStatus(target) {
200
269
  const fresh = age >= 0 && age < STALE_AFTER_MS;
201
270
  const active = supervisor.phase === "starting" || supervisor.phase === "running";
202
271
  const reason = supervisor.phase === "failed" ? "failed" : supervisor.phase === "stopped" ? "stopped" : ownership === "gone" ? "exited" : ownership === "unknown" ? "unverifiable" : !fresh ? "stale" : worker.healthy ? null : "restarting";
272
+ const healthy = active && reason === null;
273
+ const failureCode = healthy ? null : supervisor.failureCode ?? worker.failureCode;
203
274
  return {
204
275
  ...worker,
205
276
  phase: supervisor.phase === "failed" ? "failed" : reason === "restarting" ? "reconnecting" : worker.phase,
206
- healthy: active && reason === null,
277
+ healthy,
207
278
  reason,
208
- failureCode: supervisor.failureCode ?? worker.failureCode,
279
+ failureCode,
209
280
  supervisorPid: supervisor.pid,
210
- supervisorUpdatedAt: supervisor.updatedAt
281
+ supervisorUpdatedAt: supervisor.updatedAt,
282
+ detail: statusDetail({
283
+ healthy,
284
+ reason,
285
+ failureCode
286
+ })
211
287
  };
212
288
  }
289
+ /**
290
+ * Claim the shared restart slot for one receiver. False means another restart
291
+ * was attempted recently, or the claim could not be recorded (for example on a
292
+ * full disk), so the caller must not start one.
293
+ */
294
+ function claimBackgroundListenRestart(target, now = Date.now()) {
295
+ let release;
296
+ try {
297
+ const paths = files(target, true);
298
+ release = acquireListenLock(paths.directory, "background-restart-claim");
299
+ const path = join(paths.directory, "restart.json");
300
+ const prior = readMailJson(path, 256);
301
+ const at = prior && typeof prior === "object" && !Array.isArray(prior) ? prior.at : void 0;
302
+ if (typeof at === "number" && at <= now && now - at < 6e4) return false;
303
+ writeMailJson(path, { at: now });
304
+ return true;
305
+ } catch {
306
+ return false;
307
+ } finally {
308
+ try {
309
+ release?.();
310
+ } catch {}
311
+ }
312
+ }
213
313
  function stopToken(path) {
214
314
  const value = readMailJson(path, 512);
215
315
  if (value === null) return null;
@@ -284,14 +384,36 @@ async function runBackgroundListen(options) {
284
384
  writeMailJson(paths.state, state);
285
385
  started = true;
286
386
  };
387
+ let relaunched = false;
388
+ let nextRelaunchCheck = 0;
389
+ const supervised = process.env[SUPERVISOR_WORKER_ENV] === token;
390
+ const watchSupervisor = () => {
391
+ if (!supervised || relaunched || controller.signal.aborted || Date.now() < nextRelaunchCheck) return;
392
+ const supervisor = readSupervisor(paths.supervisor);
393
+ if (!supervisor || supervisor.token !== token) return;
394
+ if (process.connected && Date.now() - supervisor.updatedAt < STALE_AFTER_MS) return;
395
+ if (!(supervisor.phase === "starting" || supervisor.phase === "running" || supervisor.phase === "failed" && supervisor.failureCode === "crashed") || owner(supervisor) !== "gone") return;
396
+ if (!claimBackgroundListenRestart(options)) {
397
+ nextRelaunchCheck = Date.now() + 5e3;
398
+ return;
399
+ }
400
+ relaunched = true;
401
+ (options.relaunchSupervisor ?? relaunchSupervisor)();
402
+ };
287
403
  const tick = () => {
288
404
  try {
289
405
  if (stopToken(paths.stop) === token) cancel();
290
406
  publish();
291
407
  } catch (error) {
292
- failure ??= { error };
293
- cancel();
408
+ if (storageFailureCode(error) === null) {
409
+ failure ??= { error };
410
+ cancel();
411
+ return;
412
+ }
294
413
  }
414
+ try {
415
+ watchSupervisor();
416
+ } catch {}
295
417
  };
296
418
  options.signal?.addEventListener("abort", cancel, { once: true });
297
419
  process.on("SIGTERM", cancel);
@@ -332,7 +454,8 @@ async function runBackgroundListen(options) {
332
454
  } catch (error) {
333
455
  failure ??= { error };
334
456
  try {
335
- state.failureCode = knownFailureCode(options.failureCode?.(failure.error)) ?? "receiving-failed";
457
+ const reported = knownFailureCode(options.failureCode?.(failure.error)) ?? "receiving-failed";
458
+ state.failureCode = reported === "receiving-failed" ? storageFailureCode(failure.error) ?? reported : reported;
336
459
  } catch {
337
460
  state.failureCode = "receiving-failed";
338
461
  }
@@ -358,8 +481,27 @@ async function runBackgroundListen(options) {
358
481
  }
359
482
  if (failure) throw failure.error;
360
483
  }
361
- /** Supervise one exact listener generation until a stop or the restart budget is exhausted. */
362
- async function runBackgroundListenSupervisor() {
484
+ /** Relaunch this worker's own command detached, with --background restored. */
485
+ function relaunchSupervisor() {
486
+ const env = { ...process.env };
487
+ delete env[BACKGROUND_LISTEN_TOKEN_ENV];
488
+ delete env[BACKGROUND_LISTEN_TARGET_ENV];
489
+ delete env[SUPERVISOR_WORKER_ENV];
490
+ const child = spawn(process.execPath, [
491
+ ...process.execArgv,
492
+ ...process.argv.slice(1),
493
+ "--background"
494
+ ], {
495
+ env,
496
+ detached: true,
497
+ windowsHide: true,
498
+ stdio: "ignore"
499
+ });
500
+ child.on("error", () => {});
501
+ child.unref();
502
+ }
503
+ /** Supervise one exact listener generation until a stop or a holding failure. */
504
+ async function runBackgroundListenSupervisor(options = {}) {
363
505
  const token = mailId(process.env[BACKGROUND_LISTEN_TOKEN_ENV]);
364
506
  const expected = mailObject(JSON.parse(process.env["PRIMITIVE_LISTEN_BACKGROUND_TARGET"] ?? ""), [
365
507
  "scope",
@@ -376,6 +518,8 @@ async function runBackgroundListenSupervisor() {
376
518
  const argvValue = JSON.parse(process.env[SUPERVISOR_ARGV_ENV] ?? "");
377
519
  if (!Array.isArray(argvValue) || argvValue.length < 1 || argvValue.some((part) => typeof part !== "string" || part.length > 4096)) throw new ListenStateError("Background supervisor command is invalid.");
378
520
  const argv = argvValue;
521
+ const restartDelayMs = options.restartDelayMs ?? 1e3;
522
+ const cooldownMs = options.cooldownMs ?? RESTART_COOLDOWN_MS;
379
523
  const paths = files(target, true);
380
524
  const release = acquireListenLock(paths.directory, "background-supervisor");
381
525
  const identity = listenProcessIdentity(process.pid);
@@ -398,31 +542,59 @@ async function runBackgroundListenSupervisor() {
398
542
  record.updatedAt = Date.now();
399
543
  writeMailJson(paths.supervisor, record);
400
544
  };
545
+ const tryPublish = () => {
546
+ try {
547
+ publish();
548
+ } catch (error) {
549
+ if (storageFailureCode(error) === null) throw error;
550
+ }
551
+ };
552
+ const stopRequested = () => {
553
+ try {
554
+ return stopToken(paths.stop) === token;
555
+ } catch (error) {
556
+ if (storageFailureCode(error) !== null) return false;
557
+ throw error;
558
+ }
559
+ };
401
560
  let child;
402
561
  let stopped = false;
403
562
  const cancel = () => {
404
563
  stopped = true;
405
- writeMailJson(paths.stop, { token });
564
+ try {
565
+ writeMailJson(paths.stop, { token });
566
+ } catch {}
406
567
  child?.kill("SIGTERM");
407
568
  };
408
569
  process.on("SIGTERM", cancel);
409
570
  process.on("SIGINT", cancel);
571
+ const crashed = () => {
572
+ record.phase = "failed";
573
+ record.failureCode = "crashed";
574
+ try {
575
+ tryPublish();
576
+ } catch {}
577
+ child?.kill("SIGTERM");
578
+ process.exit(1);
579
+ };
580
+ process.on("uncaughtException", crashed);
581
+ process.on("unhandledRejection", crashed);
410
582
  const heartbeat = setInterval(() => {
411
583
  try {
412
- if (stopToken(paths.stop) === token) {
584
+ if (stopRequested()) {
413
585
  stopped = true;
414
586
  child?.kill("SIGTERM");
415
587
  }
416
- publish();
588
+ tryPublish();
417
589
  } catch {
418
590
  cancel();
419
591
  }
420
592
  }, 1e3);
421
593
  const failures = [];
422
594
  try {
423
- publish();
595
+ tryPublish();
424
596
  while (!stopped) {
425
- if (stopToken(paths.stop) === token) break;
597
+ if (stopRequested()) break;
426
598
  const env = {
427
599
  ...process.env,
428
600
  [SUPERVISOR_WORKER_ENV]: token
@@ -440,51 +612,66 @@ async function runBackgroundListenSupervisor() {
440
612
  ]
441
613
  });
442
614
  let readyAt = null;
615
+ let reported = null;
443
616
  child.on("message", (message) => {
444
617
  if (!message || typeof message !== "object") return;
445
618
  const row = message;
446
619
  if (row.type !== "primitive-listen-state" || row.token !== token) return;
447
- if (row.phase === "receiving" && readyAt === null) readyAt = Date.now();
620
+ if (row.phase === "receiving") {
621
+ if (readyAt === null) readyAt = Date.now();
622
+ if (record.failureCode !== null) {
623
+ record.failureCode = null;
624
+ tryPublish();
625
+ }
626
+ }
627
+ if (row.phase === "failed") reported = knownFailureCode(row.failureCode) ?? "receiving-failed";
448
628
  try {
449
629
  if (process.connected) process.send?.(message, () => {});
450
630
  } catch {}
451
631
  });
452
632
  record.phase = "running";
453
- publish();
454
- await new Promise((resolve) => {
455
- child?.once("exit", () => resolve());
456
- child?.once("error", () => resolve());
633
+ tryPublish();
634
+ const exit = await new Promise((resolve) => {
635
+ child?.once("exit", (code, signal) => resolve({
636
+ code,
637
+ signal
638
+ }));
639
+ child?.once("error", () => resolve({
640
+ code: null,
641
+ signal: null
642
+ }));
457
643
  });
458
644
  child = void 0;
459
- if (stopped || stopToken(paths.stop) === token) break;
460
- const worker = readState(paths.state);
461
- if (worker?.token === token && worker.phase === "failed" && ["notification-outcome-unknown", "connection-changed"].includes(worker.failureCode ?? "")) {
645
+ if (stopped || stopRequested()) break;
646
+ let worker = null;
647
+ try {
648
+ worker = readState(paths.state);
649
+ } catch {}
650
+ const recorded = worker?.token === token && worker.phase === "failed" ? worker.failureCode : null;
651
+ const failureCode = reported ?? recorded ?? (exit.code === 0 && exit.signal === null ? "receiving-failed" : "crashed");
652
+ if (HOLDING_FAILURE_CODES.includes(failureCode)) {
462
653
  record.phase = "failed";
463
- record.failureCode = worker.failureCode;
464
- publish();
654
+ record.failureCode = failureCode;
655
+ tryPublish();
465
656
  return;
466
657
  }
467
658
  const now = Date.now();
468
659
  if (readyAt !== null && now - readyAt >= HEALTHY_RESET_MS) failures.length = 0;
469
660
  failures.push(now);
470
661
  while (failures.length && (failures[0] ?? now) < now - RESTART_WINDOW_MS) failures.shift();
471
- if (failures.length >= MAX_FAILURES) {
472
- record.phase = "failed";
473
- record.failureCode = "restart-budget-exhausted";
474
- publish();
475
- return;
476
- }
662
+ const backoff = failures.length >= MAX_FAILURES ? cooldownMs : Math.min(restartDelayMs * 2 ** (failures.length - 1), 3e4);
477
663
  record.phase = "starting";
478
- publish();
479
- const backoff = Math.min(1e3 * 2 ** (failures.length - 1), 3e4);
664
+ record.failureCode = failureCode;
665
+ tryPublish();
480
666
  const end = Date.now() + backoff;
481
- while (!stopped && Date.now() < end && stopToken(paths.stop) !== token) await setTimeout$1(Math.min(200, end - Date.now()));
667
+ while (!stopped && Date.now() < end && !stopRequested()) await setTimeout$1(Math.min(200, end - Date.now()));
482
668
  }
483
669
  record.phase = "stopped";
484
- publish();
670
+ record.failureCode = null;
671
+ tryPublish();
485
672
  } catch (error) {
486
673
  record.phase = "failed";
487
- record.failureCode = "receiving-failed";
674
+ record.failureCode = "crashed";
488
675
  try {
489
676
  publish();
490
677
  } catch {}
@@ -493,9 +680,13 @@ async function runBackgroundListenSupervisor() {
493
680
  clearInterval(heartbeat);
494
681
  process.off("SIGTERM", cancel);
495
682
  process.off("SIGINT", cancel);
683
+ process.off("uncaughtException", crashed);
684
+ process.off("unhandledRejection", crashed);
496
685
  child?.kill("SIGTERM");
497
- if (stopToken(paths.stop) === token) removeMailFile(paths.stop);
498
- if (record.phase === "stopped" && readState(paths.state) === null) removeMailFile(paths.supervisor);
686
+ try {
687
+ if (stopToken(paths.stop) === token) removeMailFile(paths.stop);
688
+ if (record.phase === "stopped" && readState(paths.state) === null) removeMailFile(paths.supervisor);
689
+ } catch {}
499
690
  release();
500
691
  }
501
692
  }
@@ -542,11 +733,13 @@ async function startBackgroundListen(options) {
542
733
  const current = status(prior);
543
734
  if (!current.healthy) throw new ListenStateError("Existing listener ownership is stale or unverifiable; the worker was retained. Inspect status or request a stop before restarting.");
544
735
  if (!prior.detached) throw new ListenStateError("An existing foreground listener owns this session. Stop it before restarting with --background.");
545
- if (prior.configuration !== configuration) throw new ListenStateError("Stop the existing listener before changing receiving options.");
546
- if (!priorSupervisor) return {
547
- started: false,
548
- status: current
549
- };
736
+ if (!priorSupervisor) {
737
+ if (prior.configuration !== configuration) throw new ListenStateError("Stop the existing listener before changing receiving options.");
738
+ return {
739
+ started: false,
740
+ status: current
741
+ };
742
+ }
550
743
  if (!await stopOrphanedWorker(paths, prior.token)) throw new ListenStateError("The existing background listener lost its supervisor and did not stop. Stop it, then start again.");
551
744
  }
552
745
  const token = randomUUID();
@@ -644,4 +837,4 @@ async function startBackgroundListen(options) {
644
837
  }
645
838
  }
646
839
  //#endregion
647
- export { startBackgroundListen as a, runBackgroundListenSupervisor as i, backgroundListenToken as n, stopBackgroundListen as o, runBackgroundListen as r, verifyBackgroundListenTarget as s, backgroundListenStatus as t };
840
+ export { claimBackgroundListenRestart as a, startBackgroundListen as c, backgroundListenToken as i, stopBackgroundListen as l, backgroundListenRestartable as n, runBackgroundListen as o, backgroundListenStatus as r, runBackgroundListenSupervisor as s, BACKGROUND_LISTEN_TOKEN_ENV as t, verifyBackgroundListenTarget as u };
@@ -0,0 +1,167 @@
1
+ import { j as getSentEmail } from "../sdk.gen-CugwPoCf.js";
2
+ import { _ as acquireListenLock } from "../shared-mail-files-CZqI1kj6.js";
3
+ import { Mt as createAuthenticatedCliApiClient, t as sendSignal, z as readThreadContext } from "../signal-command-CKaEqXro.js";
4
+ import { A as stopWorkingLease, b as readWorkingLease, d as autoSignalsDisabled, j as writeWorkingLease, p as clearWorkingSending, r as AUTO_SIGNAL_KIND_ENV, s as AUTO_WORKING_RENEW_MS, v as markWorkingSending, x as recordRenewer, y as readAutoClaim } from "../auto-signals-UPcJmV3j.js";
5
+ import { join } from "node:path";
6
+ import { homedir } from "node:os";
7
+ //#region src/oclif/auto-signal-worker.ts
8
+ function configDirectory(env) {
9
+ if (env.PRIMITIVE_CONFIG_DIR) return env.PRIMITIVE_CONFIG_DIR;
10
+ return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "primitive");
11
+ }
12
+ async function defaultContext(configDir, profile) {
13
+ const { apiClient, auth } = await createAuthenticatedCliApiClient({ configDir });
14
+ if (!auth.connectedAgent || auth.connectedAgent.profileName !== profile) throw new Error("The automatic signal profile is unavailable.");
15
+ return {
16
+ apiClient,
17
+ apiKey: auth.apiKey,
18
+ configDir,
19
+ identity: auth.connectedAgent
20
+ };
21
+ }
22
+ const INTERACTION_PART = "interaction.json";
23
+ /**
24
+ * Whether the agent has sent a plain answer in the lease's thread since
25
+ * working started. Outbound signals and interactions (including this
26
+ * worker's own renewals) are not answers. Unknown state reads as unanswered,
27
+ * so the cap and local stops still bound renewal.
28
+ */
29
+ async function defaultAnswered(context, lease, ownSignals) {
30
+ if (!lease.thread_id) return false;
31
+ const signal = AbortSignal.timeout(1e4);
32
+ const thread = await readThreadContext(context.apiClient.client, lease.thread_id, void 0, signal);
33
+ if (!thread) return false;
34
+ for (const message of thread.messages) {
35
+ if (message.direction !== "outbound" || ownSignals.has(message.id)) continue;
36
+ const at = Date.parse(message.timestamp ?? "");
37
+ if (!Number.isFinite(at) || at < lease.started_at - 5e3) continue;
38
+ const sent = await getSentEmail({
39
+ client: context.apiClient.client,
40
+ path: { id: message.id },
41
+ signal,
42
+ responseStyle: "fields"
43
+ });
44
+ const detail = sent.data?.data;
45
+ if (sent.error || !detail) continue;
46
+ if ((detail.attachments ?? []).some((part) => part.filename?.toLowerCase() === INTERACTION_PART)) {
47
+ ownSignals.add(message.id);
48
+ continue;
49
+ }
50
+ if (detail.to_address?.trim().toLowerCase() === lease.sender) return true;
51
+ }
52
+ return false;
53
+ }
54
+ /**
55
+ * Detached worker entry. `read` sends one read signal; `working` holds the
56
+ * per-email renewer lock and keeps working fresh until the agent answers, a
57
+ * local reply or decline stops it, or the cap passes. Never throws.
58
+ */
59
+ async function runAutoSignalWorker(env = process.env, deps = {}) {
60
+ const now = deps.now ?? Date.now;
61
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
62
+ const send = deps.send ?? sendSignal;
63
+ const answered = deps.answered ?? defaultAnswered;
64
+ try {
65
+ if (autoSignalsDisabled(env)) return;
66
+ const configDir = configDirectory(env);
67
+ const emailId = env["PRIMITIVE_AUTO_SIGNAL_EMAIL"] ?? "";
68
+ const kind = env[AUTO_SIGNAL_KIND_ENV];
69
+ const claim = readAutoClaim(configDir, emailId);
70
+ if (!claim || env.PRIMITIVE_AGENT_PROFILE !== claim.profile) return;
71
+ const context = await (deps.context ?? defaultContext)(configDir, claim.profile);
72
+ if (kind === "read") {
73
+ await send(context, {
74
+ id: claim.email_id,
75
+ kind: "read"
76
+ });
77
+ return;
78
+ }
79
+ if (kind !== "working") return;
80
+ const ownSignals = /* @__PURE__ */ new Set();
81
+ const renew = async () => {
82
+ for (;;) {
83
+ const lease = readWorkingLease(configDir, claim.email_id);
84
+ if (!lease || lease.stopped_at !== null) return;
85
+ for (const id of lease.signal_sent_ids) ownSignals.add(id);
86
+ recordRenewer(configDir, claim.email_id);
87
+ const elapsed = now() - lease.started_at;
88
+ if (elapsed >= 9e5) {
89
+ stopWorkingLease(configDir, claim.email_id, "cap", now);
90
+ return;
91
+ }
92
+ if (autoSignalsDisabled(env)) return;
93
+ let done = false;
94
+ try {
95
+ done = await answered(context, lease, ownSignals);
96
+ } catch {
97
+ done = false;
98
+ }
99
+ if (done) {
100
+ stopWorkingLease(configDir, claim.email_id, "answered", now);
101
+ return;
102
+ }
103
+ const active = () => {
104
+ const current = readWorkingLease(configDir, claim.email_id);
105
+ return Boolean(current && current.stopped_at === null);
106
+ };
107
+ const slot = Math.floor(elapsed / AUTO_WORKING_RENEW_MS);
108
+ markWorkingSending(configDir, claim.email_id);
109
+ let outcome;
110
+ let sentId = null;
111
+ try {
112
+ if (!active()) return;
113
+ const result = await send(context, {
114
+ id: claim.email_id,
115
+ kind: "working",
116
+ expiresIn: 55,
117
+ slot: `auto-working-${slot}`,
118
+ shouldSend: active
119
+ });
120
+ outcome = result.data.outcome;
121
+ sentId = typeof result.data.sent_id === "string" ? result.data.sent_id : null;
122
+ } catch {
123
+ outcome = "failed";
124
+ } finally {
125
+ clearWorkingSending(configDir, claim.email_id);
126
+ }
127
+ if (sentId) {
128
+ ownSignals.add(sentId);
129
+ const current = readWorkingLease(configDir, claim.email_id);
130
+ if (current && current.stopped_at === null && !current.signal_sent_ids.includes(sentId)) writeWorkingLease(configDir, {
131
+ ...current,
132
+ signal_sent_ids: [...current.signal_sent_ids, sentId].slice(-200)
133
+ });
134
+ }
135
+ if (outcome === "not_sent") {
136
+ stopWorkingLease(configDir, claim.email_id, "not_sent", now);
137
+ return;
138
+ }
139
+ const next = lease.started_at + (slot + 1) * AUTO_WORKING_RENEW_MS;
140
+ while (now() < next) {
141
+ if (!active()) return;
142
+ await sleep(Math.min(1e3, next - now()));
143
+ }
144
+ }
145
+ };
146
+ for (let round = 0; round < 10; round++) {
147
+ let release;
148
+ try {
149
+ release = acquireListenLock(join(configDir, "auto-signals", claim.email_id), "auto-signal-working");
150
+ } catch {
151
+ return;
152
+ }
153
+ try {
154
+ await renew();
155
+ } finally {
156
+ release();
157
+ }
158
+ deps.released?.();
159
+ if (autoSignalsDisabled(env)) return;
160
+ const lease = readWorkingLease(configDir, claim.email_id);
161
+ if (!lease || lease.stopped_at !== null) return;
162
+ if (now() - lease.started_at >= 9e5) return;
163
+ }
164
+ } catch {}
165
+ }
166
+ //#endregion
167
+ export { runAutoSignalWorker };
@@ -0,0 +1,2 @@
1
+ import { A as stopWorkingLease, C as releaseStopHold, D as resumeAutoWorking, E as restoreWorkingLease, O as spawnAutoSignalWorker, S as recordSentSignal, T as restoreAutoWorking, _ as isSentSignal, a as AUTO_WORKING_CAP_MS, b as readWorkingLease, c as activeWorkingLeases, d as autoSignalsDisabled, f as claimAutoRead, g as haltAutoWorking, h as dispatchAutoWorking, i as AUTO_SIGNAL_WORKER_ENV, j as writeWorkingLease, k as startWorkingLease, l as autoSignalEligible, m as dispatchAutoRead, n as AUTO_SIGNAL_EMAIL_ENV, o as AUTO_WORKING_EXPIRES_SECONDS, p as clearWorkingSending, r as AUTO_SIGNAL_KIND_ENV, s as AUTO_WORKING_RENEW_MS, t as AUTO_SIGNALS_OPT_OUT_ENV, u as autoSignalEntrypoint, v as markWorkingSending, w as renewerAlive, x as recordRenewer, y as readAutoClaim } from "../auto-signals-UPcJmV3j.js";
2
+ export { AUTO_SIGNALS_OPT_OUT_ENV, AUTO_SIGNAL_EMAIL_ENV, AUTO_SIGNAL_KIND_ENV, AUTO_SIGNAL_WORKER_ENV, AUTO_WORKING_CAP_MS, AUTO_WORKING_EXPIRES_SECONDS, AUTO_WORKING_RENEW_MS, activeWorkingLeases, autoSignalEligible, autoSignalEntrypoint, autoSignalsDisabled, claimAutoRead, clearWorkingSending, dispatchAutoRead, dispatchAutoWorking, haltAutoWorking, isSentSignal, markWorkingSending, readAutoClaim, readWorkingLease, recordRenewer, recordSentSignal, releaseStopHold, renewerAlive, restoreAutoWorking, restoreWorkingLease, resumeAutoWorking, spawnAutoSignalWorker, startWorkingLease, stopWorkingLease, writeWorkingLease };