primcli 3.0.0-preview.5 → 3.0.0-preview.6
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 +8 -7
- package/dist/{api-command-DIo8Rg0w.js → api-command-CRjIK1QL.js} +1258 -115
- package/dist/{listen-background-C0F9gNDD.js → listen-background-DyBYca28.js} +323 -36
- package/dist/{machine-session-BIOGBT9g.js → machine-session-3H-rNH8v.js} +5 -5
- package/dist/{notify-session-o9hEtJ_c.js → notify-session-DTyg-sfS.js} +2 -2
- package/dist/oclif/auto-signal-worker.js +3 -3
- package/dist/oclif/auto-signals.js +1 -1
- package/dist/oclif/index.js +1204 -217
- package/dist/oclif/listen-supervisor.js +1 -1
- package/dist/oclif/receiver-heal.js +4 -3
- package/dist/oclif/root-signup-hint.js +15 -15
- package/dist/{repeats-Dj_o6c-X-Ix28GRRc.js → repeats-Dj_o6c-X-CP0h4alF.js} +173 -26
- package/dist/{scoped-chat-DcymzGgB.js → scoped-chat-CvqdlWuX.js} +1 -1
- package/dist/{signal-command-CRGoKO4A.js → signal-command-CPxRPbHK.js} +4 -4
- package/man/primitive.1 +7 -1
- package/package.json +4 -4
|
@@ -10,6 +10,7 @@ const BACKGROUND_LISTEN_TARGET_ENV = "PRIMITIVE_LISTEN_BACKGROUND_TARGET";
|
|
|
10
10
|
const SUPERVISOR_ENV = "PRIMITIVE_LISTEN_SUPERVISOR";
|
|
11
11
|
const SUPERVISOR_ARGV_ENV = "PRIMITIVE_LISTEN_SUPERVISOR_ARGV";
|
|
12
12
|
const SUPERVISOR_WORKER_ENV = "PRIMITIVE_LISTEN_SUPERVISOR_WORKER";
|
|
13
|
+
const BACKGROUND_LISTEN_RELAUNCH_ENV = "PRIMITIVE_LISTEN_RELAUNCH_FOR";
|
|
13
14
|
const STALE_AFTER_MS = 15e3;
|
|
14
15
|
const RESTART_WINDOW_MS = 6e5;
|
|
15
16
|
const HEALTHY_RESET_MS = 6e5;
|
|
@@ -38,6 +39,27 @@ const HOLDING_FAILURE_CODES = [
|
|
|
38
39
|
"connection-changed",
|
|
39
40
|
"unrecognized"
|
|
40
41
|
];
|
|
42
|
+
const startFailureReasons = [
|
|
43
|
+
"timed-out",
|
|
44
|
+
"cancelled",
|
|
45
|
+
"supervisor-exited",
|
|
46
|
+
"spawn-failed",
|
|
47
|
+
"worker-failed",
|
|
48
|
+
"ownership-changed",
|
|
49
|
+
"options-changed",
|
|
50
|
+
"existing-kept",
|
|
51
|
+
"unverifiable"
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* A failure code as a worker reported it. One from a newer CLI keeps its name
|
|
55
|
+
* in supervisor.json, so that CLI reads its own code back; readers that do
|
|
56
|
+
* not know it see "unrecognized". Anything that is not a plain code name is
|
|
57
|
+
* "unrecognized" outright.
|
|
58
|
+
*/
|
|
59
|
+
function reportedFailureCode(value) {
|
|
60
|
+
if (value === null || value === void 0) return null;
|
|
61
|
+
return typeof value === "string" && /^[a-z][a-z0-9-]{0,63}$/.test(value) ? value : "unrecognized";
|
|
62
|
+
}
|
|
41
63
|
function knownFailureCode(value) {
|
|
42
64
|
return failureCodes.includes(value) ? value : null;
|
|
43
65
|
}
|
|
@@ -83,8 +105,38 @@ function failureGuidance(code) {
|
|
|
83
105
|
case "unrecognized": return "A newer Primitive CLI recorded this receiver's state. Inspect it with that version before restarting.";
|
|
84
106
|
}
|
|
85
107
|
}
|
|
108
|
+
function startFailureCause(reason) {
|
|
109
|
+
switch (reason) {
|
|
110
|
+
case "timed-out": return "the receiver was not ready before the startup timeout";
|
|
111
|
+
case "cancelled": return "the start was cancelled, or the receiver was stopped while it started";
|
|
112
|
+
case "supervisor-exited": return "its supervisor exited before the receiver was ready";
|
|
113
|
+
case "spawn-failed": return "its supervisor could not be started";
|
|
114
|
+
case "worker-failed": return "the receiver reported a failure while starting";
|
|
115
|
+
case "ownership-changed": return "another listener held this receiver, or its supervisor no longer did, when startup was checked";
|
|
116
|
+
case "options-changed": return "the receiver ran with options other than those requested";
|
|
117
|
+
case "existing-kept": return "the receiver already recorded for this session could not be verified or stopped, so it was left in place";
|
|
118
|
+
case "unverifiable": return "its records could not be read to confirm it";
|
|
119
|
+
case "unrecognized": return "its reason is one this version of the CLI does not recognize";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A failed start is history once the receiver is healthy and receiving. A
|
|
124
|
+
* healthy receiver that is still starting or reconnecting has not shown that
|
|
125
|
+
* the start which failed was recovered from.
|
|
126
|
+
*/
|
|
127
|
+
function startFailureCurrent(status) {
|
|
128
|
+
return !(status.healthy && status.phase === "receiving");
|
|
129
|
+
}
|
|
86
130
|
/** One sentence an owner or agent can act on; never raw error text. */
|
|
87
131
|
function statusDetail(status) {
|
|
132
|
+
const base = receiverDetail(status);
|
|
133
|
+
const failure = status.lastStartFailure;
|
|
134
|
+
if (!failure || !startFailureCurrent(status)) return base;
|
|
135
|
+
const restart = status.reason === "stopped" || status.reason === "absent" ? " Start it again with the same listen command, or run it without --background for details." : "";
|
|
136
|
+
const note = `The last background start, at ${new Date(failure.at).toISOString()}, failed: ${startFailureCause(failure.reason)}.${restart}`;
|
|
137
|
+
return base ? `${base} ${note}` : note;
|
|
138
|
+
}
|
|
139
|
+
function receiverDetail(status) {
|
|
88
140
|
if (status.healthy) return null;
|
|
89
141
|
const guidance = status.failureCode ? failureGuidance(status.failureCode) : "";
|
|
90
142
|
const withGuidance = (text) => guidance ? `${text} ${guidance}` : text;
|
|
@@ -103,6 +155,23 @@ function backgroundListenToken(env = process.env) {
|
|
|
103
155
|
const value = env[BACKGROUND_LISTEN_TOKEN_ENV];
|
|
104
156
|
return value === void 0 ? null : mailId(value);
|
|
105
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* The fingerprint of the options a receiver runs with. A supervisor restarts
|
|
160
|
+
* its worker with the same command, also after the CLI was upgraded in place,
|
|
161
|
+
* so the value must not depend on the CLI version or anything else that
|
|
162
|
+
* changes between releases. Earlier releases included their version; a worker
|
|
163
|
+
* restarted by a supervisor of one of them hands its receiver over (see
|
|
164
|
+
* verifyBackgroundListenTarget).
|
|
165
|
+
*/
|
|
166
|
+
function backgroundListenConfiguration(options) {
|
|
167
|
+
return createHash("sha256").update(JSON.stringify({
|
|
168
|
+
contacts: options.contacts,
|
|
169
|
+
contactRequests: options.contactRequests,
|
|
170
|
+
senders: [...options.senders].sort(),
|
|
171
|
+
socketPath: options.socketPath,
|
|
172
|
+
events: options.events?.map((event) => event.trim()).sort()
|
|
173
|
+
})).digest("hex");
|
|
174
|
+
}
|
|
106
175
|
function configurationFingerprint(value) {
|
|
107
176
|
if (value === void 0) return null;
|
|
108
177
|
if (!/^[a-f0-9]{64}$/i.test(value)) throw new ListenStateError("Listener configuration fingerprint is invalid.");
|
|
@@ -110,7 +179,10 @@ function configurationFingerprint(value) {
|
|
|
110
179
|
}
|
|
111
180
|
/** Verify the parent's selection before a child reads or mutates listener state. */
|
|
112
181
|
function verifyBackgroundListenTarget(target, env = process.env) {
|
|
113
|
-
|
|
182
|
+
const token = backgroundListenToken(env);
|
|
183
|
+
if (token === null) return;
|
|
184
|
+
let changedConnection = true;
|
|
185
|
+
let changedOptions = false;
|
|
114
186
|
try {
|
|
115
187
|
const value = JSON.parse(env["PRIMITIVE_LISTEN_BACKGROUND_TARGET"] ?? "");
|
|
116
188
|
const expected = mailObject(value, [
|
|
@@ -122,11 +194,34 @@ function verifyBackgroundListenTarget(target, env = process.env) {
|
|
|
122
194
|
if (expected.configuration !== null && typeof expected.configuration !== "string") throw new Error("Invalid configuration fingerprint");
|
|
123
195
|
const configuration = configurationFingerprint(expected.configuration === null ? void 0 : String(expected.configuration));
|
|
124
196
|
const configDir = mailString(expected.configDir, 4096);
|
|
125
|
-
if (mailString(expected.scope) !== mailString(target.scope) || mailId(expected.threadId) !== mailId(target.threadId)
|
|
197
|
+
if (mailString(expected.scope) !== mailString(target.scope) || mailId(expected.threadId) !== mailId(target.threadId)) throw new Error("Changed listener target");
|
|
198
|
+
let resolved;
|
|
199
|
+
try {
|
|
200
|
+
resolved = [realpathSync(configDir), realpathSync(target.configDir)];
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (storageFailureCode(error) !== null) changedConnection = false;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
if (resolved.some((path) => path !== configDir)) throw new Error("Changed listener target");
|
|
206
|
+
changedConnection = false;
|
|
207
|
+
changedOptions = configuration !== configurationFingerprint(target.configuration);
|
|
208
|
+
if (changedOptions) throw new Error("Changed listener options");
|
|
126
209
|
} catch {
|
|
210
|
+
if (changedConnection && env === process.env) announce(token, "failed", "connection-changed");
|
|
211
|
+
if (changedOptions && env === process.env && env[SUPERVISOR_WORKER_ENV] === token) handOverReceiver(target, token);
|
|
127
212
|
throw new ListenStateError("Background listener target changed or its parent expectation is invalid. No listener was started.");
|
|
128
213
|
}
|
|
129
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Relaunch this worker's command with --background to replace the supervisor
|
|
217
|
+
* generation `token`. The claim allows one handover per restart interval: the
|
|
218
|
+
* supervisor may restart this worker again before the relaunch stops it.
|
|
219
|
+
*/
|
|
220
|
+
function handOverReceiver(target, token) {
|
|
221
|
+
try {
|
|
222
|
+
if (claimBackgroundListenRestart(target)) relaunchSupervisor(token);
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
130
225
|
function configDirectory(path, create) {
|
|
131
226
|
let info;
|
|
132
227
|
try {
|
|
@@ -157,9 +252,51 @@ function files(target, create = false) {
|
|
|
157
252
|
directory,
|
|
158
253
|
state: join(directory, "state.json"),
|
|
159
254
|
stop: join(directory, "stop.json"),
|
|
160
|
-
supervisor: join(directory, "supervisor.json")
|
|
255
|
+
supervisor: join(directory, "supervisor.json"),
|
|
256
|
+
startFailure: join(directory, "start-failure.json"),
|
|
257
|
+
ownerStop: join(directory, "owner-stop.json")
|
|
161
258
|
};
|
|
162
259
|
}
|
|
260
|
+
/** A diagnostic only: an unreadable record is reported as none, never as an error. */
|
|
261
|
+
function readStartFailure(path) {
|
|
262
|
+
try {
|
|
263
|
+
const value = readMailJson(path, 1024);
|
|
264
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
265
|
+
const row = value;
|
|
266
|
+
if (row.version !== 1 || !Number.isSafeInteger(row.at) || Number(row.at) < 0 || Number(row.at) > 864e13 || typeof row.reason !== "string" || row.failureCode !== null && typeof row.failureCode !== "string") return null;
|
|
267
|
+
return {
|
|
268
|
+
at: Number(row.at),
|
|
269
|
+
reason: startFailureReasons.includes(row.reason) ? row.reason : "unrecognized",
|
|
270
|
+
failureCode: savedFailureCode(row.failureCode)
|
|
271
|
+
};
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function recordStartFailure(paths, reason, failureCode) {
|
|
277
|
+
try {
|
|
278
|
+
writeMailJson(paths.startFailure, {
|
|
279
|
+
version: 1,
|
|
280
|
+
at: Date.now(),
|
|
281
|
+
reason,
|
|
282
|
+
failureCode
|
|
283
|
+
});
|
|
284
|
+
} catch {}
|
|
285
|
+
}
|
|
286
|
+
/** Whether an owner asked generation `token` to stop; unreadable means no. */
|
|
287
|
+
function ownerStopped(paths, token) {
|
|
288
|
+
try {
|
|
289
|
+
const value = readMailJson(paths.ownerStop, 512);
|
|
290
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && typeof value.token === "string" && String(value.token).toLowerCase() === token;
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function clearStartFailure(paths) {
|
|
296
|
+
try {
|
|
297
|
+
removeMailFile(paths.startFailure);
|
|
298
|
+
} catch {}
|
|
299
|
+
}
|
|
163
300
|
function readSupervisor(path) {
|
|
164
301
|
const value = readMailJson(path, 2048);
|
|
165
302
|
if (value === null) return null;
|
|
@@ -264,7 +401,15 @@ function backgroundListenStatus(target) {
|
|
|
264
401
|
const paths = files(target);
|
|
265
402
|
const worker = status(readState(paths.state));
|
|
266
403
|
const supervisor = readSupervisor(paths.supervisor);
|
|
267
|
-
|
|
404
|
+
const saved = readStartFailure(paths.startFailure);
|
|
405
|
+
if (!supervisor) return saved && startFailureCurrent(worker) ? {
|
|
406
|
+
...worker,
|
|
407
|
+
lastStartFailure: saved,
|
|
408
|
+
detail: statusDetail({
|
|
409
|
+
...worker,
|
|
410
|
+
lastStartFailure: saved
|
|
411
|
+
})
|
|
412
|
+
} : worker;
|
|
268
413
|
const ownership = owner(supervisor);
|
|
269
414
|
const age = Date.now() - supervisor.updatedAt;
|
|
270
415
|
const fresh = age >= 0 && age < STALE_AFTER_MS;
|
|
@@ -272,9 +417,14 @@ function backgroundListenStatus(target) {
|
|
|
272
417
|
const reason = supervisor.phase === "failed" ? "failed" : supervisor.phase === "stopped" ? "stopped" : ownership === "gone" ? "exited" : ownership === "unknown" ? "unverifiable" : !fresh ? "stale" : worker.healthy ? null : "restarting";
|
|
273
418
|
const healthy = active && reason === null;
|
|
274
419
|
const failureCode = healthy ? null : supervisor.failureCode ?? worker.failureCode;
|
|
420
|
+
const phase = supervisor.phase === "failed" ? "failed" : reason === "restarting" ? "reconnecting" : worker.phase;
|
|
421
|
+
const lastStartFailure = saved && startFailureCurrent({
|
|
422
|
+
healthy,
|
|
423
|
+
phase
|
|
424
|
+
}) ? saved : null;
|
|
275
425
|
return {
|
|
276
426
|
...worker,
|
|
277
|
-
phase
|
|
427
|
+
phase,
|
|
278
428
|
healthy,
|
|
279
429
|
reason,
|
|
280
430
|
failureCode,
|
|
@@ -283,8 +433,11 @@ function backgroundListenStatus(target) {
|
|
|
283
433
|
detail: statusDetail({
|
|
284
434
|
healthy,
|
|
285
435
|
reason,
|
|
286
|
-
failureCode
|
|
287
|
-
|
|
436
|
+
failureCode,
|
|
437
|
+
phase,
|
|
438
|
+
lastStartFailure
|
|
439
|
+
}),
|
|
440
|
+
...lastStartFailure ? { lastStartFailure } : {}
|
|
288
441
|
};
|
|
289
442
|
}
|
|
290
443
|
/**
|
|
@@ -328,6 +481,10 @@ async function stopBackgroundListen(target, expectedToken) {
|
|
|
328
481
|
if (state && !supervisor && (state.phase === "stopped" || state.phase === "failed" || owner(state) === "gone")) return status(state);
|
|
329
482
|
if (supervisor && owner(supervisor) === "unknown") throw new ListenStateError("Background supervisor ownership cannot be verified; no stop was requested.");
|
|
330
483
|
if (!supervisor && state && owner(state) !== "verified" && !state.identity.startsWith("darwin:")) throw new ListenStateError("Background listener ownership cannot be verified; no stop was requested.");
|
|
484
|
+
writeMailJson(paths.ownerStop, {
|
|
485
|
+
version: 1,
|
|
486
|
+
token
|
|
487
|
+
});
|
|
331
488
|
writeMailJson(paths.stop, { token });
|
|
332
489
|
const deadline = Date.now() + 5e3;
|
|
333
490
|
for (;;) {
|
|
@@ -399,7 +556,8 @@ async function runBackgroundListen(options) {
|
|
|
399
556
|
return;
|
|
400
557
|
}
|
|
401
558
|
relaunched = true;
|
|
402
|
-
(options.relaunchSupervisor
|
|
559
|
+
if (options.relaunchSupervisor) options.relaunchSupervisor();
|
|
560
|
+
else relaunchSupervisor(token);
|
|
403
561
|
};
|
|
404
562
|
const tick = () => {
|
|
405
563
|
try {
|
|
@@ -483,8 +641,11 @@ async function runBackgroundListen(options) {
|
|
|
483
641
|
if (failure) throw failure.error;
|
|
484
642
|
}
|
|
485
643
|
/** Relaunch this worker's own command detached, with --background restored. */
|
|
486
|
-
function relaunchSupervisor() {
|
|
487
|
-
const env = {
|
|
644
|
+
function relaunchSupervisor(token) {
|
|
645
|
+
const env = {
|
|
646
|
+
...process.env,
|
|
647
|
+
[BACKGROUND_LISTEN_RELAUNCH_ENV]: token
|
|
648
|
+
};
|
|
488
649
|
delete env[BACKGROUND_LISTEN_TOKEN_ENV];
|
|
489
650
|
delete env[BACKGROUND_LISTEN_TARGET_ENV];
|
|
490
651
|
delete env[SUPERVISOR_WORKER_ENV];
|
|
@@ -580,6 +741,7 @@ async function runBackgroundListenSupervisor(options = {}) {
|
|
|
580
741
|
};
|
|
581
742
|
process.on("uncaughtException", crashed);
|
|
582
743
|
process.on("unhandledRejection", crashed);
|
|
744
|
+
let workerReceiving = false;
|
|
583
745
|
const heartbeat = setInterval(() => {
|
|
584
746
|
try {
|
|
585
747
|
if (stopRequested()) {
|
|
@@ -587,6 +749,10 @@ async function runBackgroundListenSupervisor(options = {}) {
|
|
|
587
749
|
child?.kill("SIGTERM");
|
|
588
750
|
}
|
|
589
751
|
tryPublish();
|
|
752
|
+
if (workerReceiving) try {
|
|
753
|
+
const worker = readState(paths.state);
|
|
754
|
+
if (worker?.token === token && worker.phase === "receiving" && status(worker).healthy) clearStartFailure(paths);
|
|
755
|
+
} catch {}
|
|
590
756
|
} catch {
|
|
591
757
|
cancel();
|
|
592
758
|
}
|
|
@@ -618,14 +784,16 @@ async function runBackgroundListenSupervisor(options = {}) {
|
|
|
618
784
|
if (!message || typeof message !== "object") return;
|
|
619
785
|
const row = message;
|
|
620
786
|
if (row.type !== "primitive-listen-state" || row.token !== token) return;
|
|
787
|
+
if (typeof row.phase === "string") workerReceiving = row.phase === "receiving";
|
|
621
788
|
if (row.phase === "receiving") {
|
|
622
789
|
if (readyAt === null) readyAt = Date.now();
|
|
790
|
+
clearStartFailure(paths);
|
|
623
791
|
if (record.failureCode !== null) {
|
|
624
792
|
record.failureCode = null;
|
|
625
793
|
tryPublish();
|
|
626
794
|
}
|
|
627
795
|
}
|
|
628
|
-
if (row.phase === "failed") reported =
|
|
796
|
+
if (row.phase === "failed") reported = reportedFailureCode(row.failureCode) ?? "receiving-failed";
|
|
629
797
|
try {
|
|
630
798
|
if (process.connected) process.send?.(message, () => {});
|
|
631
799
|
} catch {}
|
|
@@ -643,6 +811,7 @@ async function runBackgroundListenSupervisor(options = {}) {
|
|
|
643
811
|
}));
|
|
644
812
|
});
|
|
645
813
|
child = void 0;
|
|
814
|
+
workerReceiving = false;
|
|
646
815
|
if (stopped || stopRequested()) break;
|
|
647
816
|
let worker = null;
|
|
648
817
|
try {
|
|
@@ -650,7 +819,8 @@ async function runBackgroundListenSupervisor(options = {}) {
|
|
|
650
819
|
} catch {}
|
|
651
820
|
const recorded = worker?.token === token && worker.phase === "failed" ? worker.failureCode : null;
|
|
652
821
|
const failureCode = reported ?? recorded ?? (exit.code === 0 && exit.signal === null ? "receiving-failed" : "crashed");
|
|
653
|
-
|
|
822
|
+
const known = knownFailureCode(failureCode);
|
|
823
|
+
if (known === null || HOLDING_FAILURE_CODES.includes(known)) {
|
|
654
824
|
record.phase = "failed";
|
|
655
825
|
record.failureCode = failureCode;
|
|
656
826
|
tryPublish();
|
|
@@ -702,6 +872,59 @@ async function stopOrphanedWorker(paths, token) {
|
|
|
702
872
|
await setTimeout$1(50);
|
|
703
873
|
}
|
|
704
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* Prepare a start that a worker relaunched for its own generation `token`:
|
|
877
|
+
* its supervisor was lost, or runs another CLI build. Nothing starts once the
|
|
878
|
+
* owner stopped that generation, or while its supervisor is stopped or holds
|
|
879
|
+
* a failure. A supervisor of that generation that is alive while its
|
|
880
|
+
* receiver is not healthy is stopped first, and this waits until its process
|
|
881
|
+
* has exited, so its cleanup cannot touch the replacement. Anything else is
|
|
882
|
+
* left to the ordinary start that follows.
|
|
883
|
+
*/
|
|
884
|
+
async function prepareRelaunch(paths, target, token, timeout, signal) {
|
|
885
|
+
const supervisor = readSupervisor(paths.supervisor);
|
|
886
|
+
if (ownerStopped(paths, token) || supervisor?.token === token && (supervisor.phase === "stopped" || supervisor.phase === "failed" && supervisor.failureCode !== "crashed")) throw relaunchCancelled();
|
|
887
|
+
if (supervisor?.token !== token || supervisor.phase !== "starting" && supervisor.phase !== "running" || owner(supervisor) === "gone" || backgroundListenStatus(target).healthy) return;
|
|
888
|
+
writeMailJson(paths.stop, { token });
|
|
889
|
+
const deadline = Date.now() + timeout;
|
|
890
|
+
let exited = false;
|
|
891
|
+
try {
|
|
892
|
+
while (Date.now() < deadline) {
|
|
893
|
+
if (owner(supervisor) === "gone") {
|
|
894
|
+
exited = true;
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
await setTimeout$1(50, void 0, { signal });
|
|
898
|
+
}
|
|
899
|
+
} finally {
|
|
900
|
+
if (!exited) try {
|
|
901
|
+
if (stopToken(paths.stop) === token && !ownerStopped(paths, token)) removeMailFile(paths.stop);
|
|
902
|
+
} catch {}
|
|
903
|
+
}
|
|
904
|
+
recordStartFailure(paths, "existing-kept", null);
|
|
905
|
+
throw new ListenStateError("The supervisor this relaunch was to replace did not stop, so it was left running. Stop it, then start again.");
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* A start refused because a healthy receiver runs with another options
|
|
909
|
+
* fingerprint. Earlier releases put their version in it, so a receiver one
|
|
910
|
+
* of them started differs even when the command is the same. From here the
|
|
911
|
+
* two causes look alike, and stopping it is the remedy for both. The stop
|
|
912
|
+
* must reach the same connection, so the flags that chose it are named, never
|
|
913
|
+
* their values.
|
|
914
|
+
*/
|
|
915
|
+
function keptWithOtherOptions(threadId, argv) {
|
|
916
|
+
const given = (flag) => argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
917
|
+
const stop = [
|
|
918
|
+
`primitive listen --notify-session ${threadId}`,
|
|
919
|
+
...given("--api-key") ? ["--api-key <the same key>"] : [],
|
|
920
|
+
...given("--api-base-url") ? ["--api-base-url <the same URL>"] : [],
|
|
921
|
+
"--stop"
|
|
922
|
+
].join(" ");
|
|
923
|
+
return new ListenStateError(`A background listener for this session is still running, started by an older version of the CLI or with different receiving options, so it was kept. To restart it with this command, run \`${stop}\` with the same profile and environment, then run this command again.`);
|
|
924
|
+
}
|
|
925
|
+
function relaunchCancelled() {
|
|
926
|
+
return new ListenStateError("The receiver this relaunch was for was stopped or holds a failure, so nothing was started.");
|
|
927
|
+
}
|
|
705
928
|
/** Spawn the same CLI entrypoint. Overrides travel only in the child's environment. */
|
|
706
929
|
async function startBackgroundListen(options) {
|
|
707
930
|
const configuration = configurationFingerprint(options.configuration);
|
|
@@ -719,12 +942,19 @@ async function startBackgroundListen(options) {
|
|
|
719
942
|
}
|
|
720
943
|
}
|
|
721
944
|
try {
|
|
945
|
+
const relaunchValue = process.env[BACKGROUND_LISTEN_RELAUNCH_ENV];
|
|
946
|
+
const relaunchFor = relaunchValue === void 0 ? null : mailId(relaunchValue);
|
|
947
|
+
if (relaunchFor !== null) await prepareRelaunch(paths, options, relaunchFor, timeout, options.signal);
|
|
722
948
|
const prior = readState(paths.state);
|
|
723
949
|
const priorSupervisor = readSupervisor(paths.supervisor);
|
|
724
950
|
if (priorSupervisor && ["starting", "running"].includes(priorSupervisor.phase) && owner(priorSupervisor) !== "gone") {
|
|
725
951
|
const current = backgroundListenStatus(options);
|
|
726
|
-
if (!current.healthy)
|
|
727
|
-
|
|
952
|
+
if (!current.healthy) {
|
|
953
|
+
if (current.reason !== "restarting") recordStartFailure(paths, "existing-kept", current.failureCode);
|
|
954
|
+
throw new ListenStateError("Existing supervisor ownership is stale or unverifiable. Inspect status before restarting.");
|
|
955
|
+
}
|
|
956
|
+
if (prior?.configuration !== configuration) throw keptWithOtherOptions(options.threadId, options.argv);
|
|
957
|
+
if (!startFailureCurrent(current)) clearStartFailure(paths);
|
|
728
958
|
return {
|
|
729
959
|
started: false,
|
|
730
960
|
status: current
|
|
@@ -732,17 +962,25 @@ async function startBackgroundListen(options) {
|
|
|
732
962
|
}
|
|
733
963
|
if (prior && prior.phase !== "stopped" && prior.phase !== "failed" && owner(prior) !== "gone") {
|
|
734
964
|
const current = status(prior);
|
|
735
|
-
if (!current.healthy)
|
|
965
|
+
if (!current.healthy) {
|
|
966
|
+
recordStartFailure(paths, "existing-kept", current.failureCode);
|
|
967
|
+
throw new ListenStateError("Existing listener ownership is stale or unverifiable; the worker was retained. Inspect status or request a stop before restarting.");
|
|
968
|
+
}
|
|
736
969
|
if (!prior.detached) throw new ListenStateError("An existing foreground listener owns this session. Stop it before restarting with --background.");
|
|
737
970
|
if (!priorSupervisor) {
|
|
738
|
-
if (prior.configuration !== configuration) throw
|
|
971
|
+
if (prior.configuration !== configuration) throw keptWithOtherOptions(options.threadId, options.argv);
|
|
972
|
+
if (!startFailureCurrent(current)) clearStartFailure(paths);
|
|
739
973
|
return {
|
|
740
974
|
started: false,
|
|
741
975
|
status: current
|
|
742
976
|
};
|
|
743
977
|
}
|
|
744
|
-
if (!await stopOrphanedWorker(paths, prior.token))
|
|
978
|
+
if (!await stopOrphanedWorker(paths, prior.token)) {
|
|
979
|
+
recordStartFailure(paths, "existing-kept", null);
|
|
980
|
+
throw new ListenStateError("The existing background listener lost its supervisor and did not stop. Stop it, then start again.");
|
|
981
|
+
}
|
|
745
982
|
}
|
|
983
|
+
if (relaunchFor !== null && ownerStopped(paths, relaunchFor)) throw relaunchCancelled();
|
|
746
984
|
const token = randomUUID();
|
|
747
985
|
const env = {
|
|
748
986
|
...process.env,
|
|
@@ -755,6 +993,7 @@ async function startBackgroundListen(options) {
|
|
|
755
993
|
configuration
|
|
756
994
|
})
|
|
757
995
|
};
|
|
996
|
+
delete env[BACKGROUND_LISTEN_RELAUNCH_ENV];
|
|
758
997
|
const argv = [];
|
|
759
998
|
for (let index = 0; index < options.argv.length; index++) {
|
|
760
999
|
const arg = options.argv[index];
|
|
@@ -782,30 +1021,49 @@ async function startBackgroundListen(options) {
|
|
|
782
1021
|
]
|
|
783
1022
|
});
|
|
784
1023
|
let startupFailureCode = null;
|
|
1024
|
+
let supervisorFailureCode = null;
|
|
1025
|
+
let decision = "unverifiable";
|
|
1026
|
+
let keptReason = "timed-out";
|
|
1027
|
+
let outcome = null;
|
|
1028
|
+
const holds = (supervisor) => child.exitCode === null && child.signalCode === null && supervisor?.token === token && supervisor.pid === child.pid && (supervisor.phase === "starting" || supervisor.phase === "running") && owner(supervisor) !== "gone";
|
|
1029
|
+
const confirm = (waited) => {
|
|
1030
|
+
const current = readState(paths.state);
|
|
1031
|
+
if (current && current.token !== token && current.phase !== "stopped" && current.phase !== "failed" && owner(current) !== "gone") return "ownership-changed";
|
|
1032
|
+
const mine = current?.token === token ? current : null;
|
|
1033
|
+
if (mine && (mine.configuration !== configuration || !mine.detached)) return "options-changed";
|
|
1034
|
+
const supervisor = readSupervisor(paths.supervisor);
|
|
1035
|
+
const own = supervisor?.token === token ? supervisor : null;
|
|
1036
|
+
supervisorFailureCode = own?.failureCode ?? null;
|
|
1037
|
+
if (mine && own && (waited === "ready" || mine.phase === "receiving" || mine.phase === "reconnecting") && backgroundListenStatus(options).healthy) return "started";
|
|
1038
|
+
if (holds(supervisor)) return waited === "ready" ? "started" : "kept";
|
|
1039
|
+
if (own?.phase === "failed") return own.failureCode === "crashed" ? "supervisor-exited" : "worker-failed";
|
|
1040
|
+
if (own?.phase === "stopped") return "cancelled";
|
|
1041
|
+
if (child.exitCode !== null || child.signalCode !== null) return "supervisor-exited";
|
|
1042
|
+
return waited === "ready" ? "ownership-changed" : "timed-out";
|
|
1043
|
+
};
|
|
785
1044
|
try {
|
|
786
|
-
await new Promise((resolve
|
|
787
|
-
const timer = setTimeout(() => finish(
|
|
788
|
-
const abort = () => finish(
|
|
789
|
-
const exit = () => finish(
|
|
790
|
-
const error = () => finish(
|
|
1045
|
+
const waited = await new Promise((resolve) => {
|
|
1046
|
+
const timer = setTimeout(() => finish("timed-out"), timeout);
|
|
1047
|
+
const abort = () => finish("cancelled");
|
|
1048
|
+
const exit = () => finish("supervisor-exited");
|
|
1049
|
+
const error = () => finish("spawn-failed");
|
|
791
1050
|
const message = (value) => {
|
|
792
1051
|
if (!value || typeof value !== "object") return;
|
|
793
1052
|
const next = value;
|
|
794
1053
|
if (next.type !== "primitive-listen-state" || next.token !== token) return;
|
|
795
|
-
if (next.phase === "receiving" || next.phase === "reconnecting") finish();
|
|
1054
|
+
if (next.phase === "receiving" || next.phase === "reconnecting") finish("ready");
|
|
796
1055
|
else if (next.phase === "failed") {
|
|
797
1056
|
startupFailureCode = knownFailureCode(next.failureCode);
|
|
798
|
-
|
|
1057
|
+
finish("worker-failed");
|
|
799
1058
|
}
|
|
800
1059
|
};
|
|
801
|
-
const finish = (
|
|
1060
|
+
const finish = (outcome) => {
|
|
802
1061
|
clearTimeout(timer);
|
|
803
1062
|
options.signal?.removeEventListener("abort", abort);
|
|
804
1063
|
child.off("exit", exit);
|
|
805
1064
|
child.off("error", error);
|
|
806
1065
|
child.off("message", message);
|
|
807
|
-
|
|
808
|
-
else resolve();
|
|
1066
|
+
resolve(outcome);
|
|
809
1067
|
};
|
|
810
1068
|
child.once("exit", exit);
|
|
811
1069
|
child.once("error", error);
|
|
@@ -813,20 +1071,49 @@ async function startBackgroundListen(options) {
|
|
|
813
1071
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
814
1072
|
if (options.signal?.aborted) abort();
|
|
815
1073
|
});
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (
|
|
819
|
-
|
|
820
|
-
started: true,
|
|
821
|
-
status: backgroundListenStatus(options)
|
|
822
|
-
};
|
|
1074
|
+
outcome = waited;
|
|
1075
|
+
if (waited === "timed-out") decision = "timed-out";
|
|
1076
|
+
if (waited === "ready") await options.beforeStartupCheck?.();
|
|
1077
|
+
decision = waited === "ready" || waited === "timed-out" ? confirm(waited) : waited;
|
|
823
1078
|
} catch {
|
|
1079
|
+
if ((outcome === "ready" || outcome === "timed-out") && child.exitCode === null && child.signalCode === null) {
|
|
1080
|
+
decision = "kept";
|
|
1081
|
+
keptReason = "unverifiable";
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
if (decision === "started") {
|
|
1086
|
+
clearStartFailure(paths);
|
|
1087
|
+
return {
|
|
1088
|
+
started: true,
|
|
1089
|
+
status: backgroundListenStatus(options)
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
if (decision === "kept") {
|
|
1093
|
+
recordStartFailure(paths, keptReason, supervisorFailureCode);
|
|
1094
|
+
try {
|
|
1095
|
+
const now = readState(paths.state);
|
|
1096
|
+
if (now?.token === token && now.phase === "receiving") {
|
|
1097
|
+
clearStartFailure(paths);
|
|
1098
|
+
return {
|
|
1099
|
+
started: true,
|
|
1100
|
+
status: backgroundListenStatus(options)
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
} catch {}
|
|
1104
|
+
const guidance = supervisorFailureCode ? ` ${failureGuidance(supervisorFailureCode)}` : "";
|
|
1105
|
+
throw new ListenStateError(`${keptReason === "timed-out" ? "Background listener was not ready before the startup timeout." : "Background listener startup could not be confirmed because its records could not be read."} Its supervisor is still running and keeps the receiver, restarting it if it exits, so it was not stopped. Inspect status, or stop it before starting again.${guidance}`);
|
|
1106
|
+
}
|
|
824
1107
|
writeMailJson(paths.stop, { token });
|
|
825
1108
|
const until = Date.now() + 2e3;
|
|
826
1109
|
while (Date.now() < until && child.exitCode === null && child.signalCode === null) await setTimeout$1(50);
|
|
827
|
-
|
|
1110
|
+
let current = null;
|
|
1111
|
+
try {
|
|
1112
|
+
current = readState(paths.state);
|
|
1113
|
+
} catch {}
|
|
828
1114
|
const unconfirmed = current?.token === token && owner(current) !== "gone" && current.phase !== "stopped" && current.phase !== "failed" || child.exitCode === null && child.signalCode === null;
|
|
829
1115
|
const code = startupFailureCode ?? (current?.token === token ? current.failureCode : null);
|
|
1116
|
+
recordStartFailure(paths, decision, code ?? supervisorFailureCode);
|
|
830
1117
|
const guidance = code ? unconfirmed && code === "receiving-failed" ? "" : failureGuidance(code) : unconfirmed ? "" : "Run the same command without --background for details.";
|
|
831
1118
|
throw new ListenStateError(`${unconfirmed ? "Background listener did not become ready; a stop was requested, but worker termination is unconfirmed. Inspect status before retrying." : "Background listener did not become ready and stopped."}${guidance ? ` ${guidance}` : ""}`);
|
|
832
1119
|
} finally {
|
|
@@ -837,4 +1124,4 @@ async function startBackgroundListen(options) {
|
|
|
837
1124
|
release();
|
|
838
1125
|
}
|
|
839
1126
|
}
|
|
840
|
-
export {
|
|
1127
|
+
export { backgroundListenStatus as a, runBackgroundListen as c, stopBackgroundListen as d, verifyBackgroundListenTarget as f, backgroundListenRestartable as i, runBackgroundListenSupervisor as l, BACKGROUND_LISTEN_TOKEN_ENV as n, backgroundListenToken as o, backgroundListenConfiguration as r, claimBackgroundListenRestart as s, BACKGROUND_LISTEN_RELAUNCH_ENV as t, startBackgroundListen as u };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { B as parseOwnerMemberAddress, F as agentProfilesDirectory, I as connectedAgentIdentity, L as connectedApiBaseUrl, M as AgentConnectionSetupError, N as agentProfileDirectory, P as agentProfileName, R as loadConnectedAgentProfile, U as saveConnectedAgentProfile, V as profileAlreadyConnectedMessage, W as PrimitiveApiClient, b as loadCliCredentials, j as AGENT_PROFILE_ENV, z as parseConnectedAgentProfile } from "./cli-config-Ms_0vNjq.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Dn as listSentEmails, Fn as putContact, Gn as searchEmails, Ht as getEmail, Jn as sendEmail, Lt as getAgentContactPolicy, Nn as putAgentContact, Pn as putAgentContactPolicy, Rt as getContact, Tt as deleteContact, fn as listDomains, hr as __exportAll, ln as listContacts, on as listAgentConnections, rn as inviteAgentConnection, sn as listAgentContacts, wt as deleteAgentContact } from "./repeats-Dj_o6c-X-CP0h4alF.js";
|
|
3
|
+
import { It as detectAgentRuntime, en as refreshStoredCliCredentials, jt as defaultConnectionName, kt as cliInvocation, tn as refuseOrgSelectionForProfile } from "./api-command-CRjIK1QL.js";
|
|
4
4
|
import { _ as acquireListenLock, b as listenLockHeld, c as mailTime, d as readMailJson, f as removeMailFile, g as ListenStateError, h as writeMailJson, i as mailId, l as privateMailDirectory, o as mailObject, p as syncMailDirectory, r as mailAddress, s as mailString } from "./shared-mail-files-BAQSGxxz.js";
|
|
5
|
-
import { S as readBundledConnectSkill, a as NotificationRetryError, d as SESSION_UUID, f as connectNativeSession, m as NativeSessionDisconnectedError, p as defaultSessionSocket, t as notificationScope, y as installConnectSkill } from "./notify-session-
|
|
6
|
-
import {
|
|
5
|
+
import { S as readBundledConnectSkill, a as NotificationRetryError, d as SESSION_UUID, f as connectNativeSession, m as NativeSessionDisconnectedError, p as defaultSessionSocket, t as notificationScope, y as installConnectSkill } from "./notify-session-DTyg-sfS.js";
|
|
6
|
+
import { a as backgroundListenStatus, d as stopBackgroundListen } from "./listen-background-DyBYca28.js";
|
|
7
7
|
import { t as readSharedMailOwner } from "./shared-mail-watch-BAPMsFNv.js";
|
|
8
8
|
import { withoutOrgSelection } from "./oclif/global-org-flag.js";
|
|
9
|
-
import { o as scopedChatSenderTrust } from "./scoped-chat-
|
|
9
|
+
import { o as scopedChatSenderTrust } from "./scoped-chat-CvqdlWuX.js";
|
|
10
10
|
import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
12
|
import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { B as wakeReadCommand, tt as wakeInteractionSentence, z as skillFirstLine } from "./api-command-
|
|
1
|
+
import { Mt as downloadEmailAttachmentPart, Q as parseWebhookEvent, X as isEmailReceivedEvent, Z as isTrustedSender, c as classifySignalContent } from "./repeats-Dj_o6c-X-CP0h4alF.js";
|
|
2
|
+
import { B as wakeReadCommand, tt as wakeInteractionSentence, z as skillFirstLine } from "./api-command-CRjIK1QL.js";
|
|
3
3
|
import { _ as acquireListenLock, g as ListenStateError, y as listenIdentity } from "./shared-mail-files-BAQSGxxz.js";
|
|
4
4
|
import { closeSync, copyFileSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, opendirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { createHash, randomUUID, scryptSync } from "node:crypto";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { F as readThreadContext,
|
|
1
|
+
import { en as getSentEmail } from "../repeats-Dj_o6c-X-CP0h4alF.js";
|
|
2
|
+
import { F as readThreadContext, Qt as createAuthenticatedCliApiClient } from "../api-command-CRjIK1QL.js";
|
|
3
3
|
import { AUTO_SIGNAL_KIND_ENV, AUTO_WORKING_RENEW_MS, autoSignalsDisabled, clearWorkingSending, markWorkingSending, readAutoClaim, readWorkingLease, recordRenewer, stopWorkingLease, writeWorkingLease } from "./auto-signals.js";
|
|
4
4
|
import { _ as acquireListenLock } from "../shared-mail-files-BAQSGxxz.js";
|
|
5
|
-
import { t as sendSignal } from "../signal-command-
|
|
5
|
+
import { t as sendSignal } from "../signal-command-CPxRPbHK.js";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
// src/oclif/auto-signal-worker.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as isPlainChatReply, o as scopedChatSenderTrust } from "../scoped-chat-
|
|
1
|
+
import { n as isPlainChatReply, o as scopedChatSenderTrust } from "../scoped-chat-CvqdlWuX.js";
|
|
2
2
|
import { d as readMailJson, h as writeMailJson, i as mailId, l as privateMailDirectory, r as mailAddress } from "../shared-mail-files-BAQSGxxz.js";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|