primcli 1.35.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.
- package/README.md +73 -10
- package/bin/run.js +37 -0
- package/dist/auto-signals-UPcJmV3j.js +702 -0
- package/dist/{cli-config-DmhlcS3s.js → cli-config-UOlrfcPz.js} +2 -711
- package/dist/connect-skill-DednypYJ.js +1084 -0
- package/dist/connected-agent-profile-8tpOMdq3.js +730 -0
- package/dist/{listen-background-D6UhIbkO.js → listen-background-Bsofqq_6.js} +243 -50
- package/dist/oclif/auto-signal-worker.js +167 -0
- package/dist/oclif/auto-signals.js +2 -0
- package/dist/oclif/index.js +31501 -48161
- package/dist/oclif/listen-supervisor.js +1 -1
- package/dist/oclif/proxy-auto-detect.js +1 -1
- package/dist/oclif/receiver-heal.js +91 -0
- package/dist/oclif/root-signup-hint.js +1 -1
- package/dist/repeats-CQmyvUGQ-CMnOmWgL.js +10442 -0
- package/dist/sdk.gen-CugwPoCf.js +3281 -0
- package/dist/{shared-mail-files-Cbwnf79a.js → shared-mail-files-CZqI1kj6.js} +5 -2
- package/dist/signal-command-CKaEqXro.js +4514 -0
- package/dist/skills/primitive-connect/SKILL.md +896 -0
- package/dist/skills/primitive-connect/package-lock.json +1033 -0
- package/dist/skills/primitive-connect/package.json +8 -0
- package/dist/skills/primitive-connect/references/communication.md +207 -0
- package/dist/skills/primitive-connect/references/contact-requests.md +161 -0
- package/dist/skills/primitive-connect/references/native-session.md +165 -0
- package/dist/skills/primitive-connect/references/private-api-fallback.md +107 -0
- package/dist/skills/primitive-connect/scripts/check-cli-capabilities.mjs +48 -0
- package/dist/skills/primitive-connect/scripts/connection.mjs +197 -0
- package/dist/skills/primitive-connect/scripts/mail.mjs +189 -0
- package/dist/skills/primitive-connect.json +21 -0
- package/man/primitive.1 +35 -4
- package/package.json +10 -1
- /package/dist/{json-output-KAIuP9rG.js → json-output-BVRgl6jf.js} +0 -0
|
@@ -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-
|
|
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 &&
|
|
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:
|
|
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 &&
|
|
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:
|
|
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
|
|
277
|
+
healthy,
|
|
207
278
|
reason,
|
|
208
|
-
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
362
|
-
|
|
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
|
-
|
|
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 (
|
|
584
|
+
if (stopRequested()) {
|
|
413
585
|
stopped = true;
|
|
414
586
|
child?.kill("SIGTERM");
|
|
415
587
|
}
|
|
416
|
-
|
|
588
|
+
tryPublish();
|
|
417
589
|
} catch {
|
|
418
590
|
cancel();
|
|
419
591
|
}
|
|
420
592
|
}, 1e3);
|
|
421
593
|
const failures = [];
|
|
422
594
|
try {
|
|
423
|
-
|
|
595
|
+
tryPublish();
|
|
424
596
|
while (!stopped) {
|
|
425
|
-
if (
|
|
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"
|
|
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
|
-
|
|
454
|
-
await new Promise((resolve) => {
|
|
455
|
-
child?.once("exit", () => resolve(
|
|
456
|
-
|
|
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 ||
|
|
460
|
-
|
|
461
|
-
|
|
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 =
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
664
|
+
record.failureCode = failureCode;
|
|
665
|
+
tryPublish();
|
|
480
666
|
const end = Date.now() + backoff;
|
|
481
|
-
while (!stopped && Date.now() < end &&
|
|
667
|
+
while (!stopped && Date.now() < end && !stopRequested()) await setTimeout$1(Math.min(200, end - Date.now()));
|
|
482
668
|
}
|
|
483
669
|
record.phase = "stopped";
|
|
484
|
-
|
|
670
|
+
record.failureCode = null;
|
|
671
|
+
tryPublish();
|
|
485
672
|
} catch (error) {
|
|
486
673
|
record.phase = "failed";
|
|
487
|
-
record.failureCode = "
|
|
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
|
-
|
|
498
|
-
|
|
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 (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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 {
|
|
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 };
|