@indigoai-us/hq-cli 5.161.0 → 5.163.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/CHANGELOG.md +27 -2
- package/dist/command-catalog.generated.d.ts +188 -0
- package/dist/command-catalog.generated.js +235 -0
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +6 -1
- package/dist/commands/agent-enroll.js +30 -3
- package/dist/commands/agents.js +15 -22
- package/dist/commands/bot.js +56 -6
- package/dist/commands/daemon.d.ts +53 -0
- package/dist/commands/daemon.js +554 -0
- package/dist/commands/lanes.js +10 -1
- package/dist/commands/meetings.js +18 -16
- package/dist/commands/mesh.js +9 -0
- package/dist/lib/daemon/catalog.d.ts +85 -0
- package/dist/lib/daemon/catalog.js +274 -0
- package/dist/lib/daemon/config.d.ts +26 -0
- package/dist/lib/daemon/config.js +85 -0
- package/dist/lib/daemon/handoff.d.ts +53 -0
- package/dist/lib/daemon/handoff.js +136 -0
- package/dist/lib/daemon/install.d.ts +76 -0
- package/dist/lib/daemon/install.js +299 -0
- package/dist/lib/daemon/legacy.d.ts +35 -0
- package/dist/lib/daemon/legacy.js +120 -0
- package/dist/lib/daemon/lock.d.ts +47 -0
- package/dist/lib/daemon/lock.js +138 -0
- package/dist/lib/daemon/machine-type.d.ts +48 -0
- package/dist/lib/daemon/machine-type.js +139 -0
- package/dist/lib/daemon/migrate.d.ts +61 -0
- package/dist/lib/daemon/migrate.js +238 -0
- package/dist/lib/daemon/paths.d.ts +22 -0
- package/dist/lib/daemon/paths.js +42 -0
- package/dist/lib/daemon/remote-control.d.ts +35 -0
- package/dist/lib/daemon/remote-control.js +47 -0
- package/dist/lib/daemon/requests.d.ts +24 -0
- package/dist/lib/daemon/requests.js +63 -0
- package/dist/lib/daemon/run.d.ts +41 -0
- package/dist/lib/daemon/run.js +319 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +523 -0
- package/dist/lib/daemon/updater.d.ts +39 -0
- package/dist/lib/daemon/updater.js +103 -0
- package/dist/lib/flag-registry.d.ts +7 -1
- package/dist/lib/flag-registry.js +8 -0
- package/dist/lib/lanes/delivery.d.ts +5 -3
- package/dist/lib/lanes/delivery.js +14 -6
- package/dist/lib/lanes/spawn.d.ts +5 -0
- package/dist/lib/lanes/spawn.js +25 -3
- package/dist/lib/lanes/types.d.ts +11 -2
- package/dist/lib/lanes/types.js +11 -0
- package/dist/lib/lanes/watcher.d.ts +2 -2
- package/dist/lib/lanes/watcher.js +2 -2
- package/dist/lib/mesh/live/backoff.d.ts +2 -2
- package/dist/lib/mesh/live/backoff.js +16 -3
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -2
- package/dist/lib/mesh/live/daemon/run.js +5 -2
- package/dist/lib/mesh/live/flush.d.ts +3 -2
- package/dist/lib/mesh/live/flush.js +34 -7
- package/dist/lib/mesh/live/session-events-client.d.ts +2 -0
- package/dist/lib/mesh/live/session-events-client.js +25 -0
- package/dist/lib/service-manager/systemd.d.ts +2 -2
- package/dist/lib/service-manager/systemd.js +1 -1
- package/dist/lib/service-manager/types.d.ts +2 -0
- package/dist/main.d.ts +6 -0
- package/dist/main.js +53 -13
- package/dist/utils/cognito-session.d.ts +4 -0
- package/dist/utils/cognito-session.js +11 -10
- package/dist/utils/plan-gate-error.d.ts +27 -13
- package/dist/utils/plan-gate-error.js +133 -58
- package/dist/utils/self-update.d.ts +9 -0
- package/dist/utils/self-update.js +15 -2
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One run of the `updater` service (`hq daemon service self-update`), which
|
|
3
|
+
* replaces the boxes' `hq-cli-update.timer` and `*-codex-update.timer`:
|
|
4
|
+
*
|
|
5
|
+
* 1. Update the hq CLI through the shared self-update path (same install
|
|
6
|
+
* routing, lock and convergence check as every other CLI update).
|
|
7
|
+
* 2. Update Codex when it is installed as a global npm package and behind.
|
|
8
|
+
* 3. After a CLI update, ask the daemon to restart itself so the supervisor and
|
|
9
|
+
* every service run the new version.
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import semver from "semver";
|
|
13
|
+
export const CODEX_PACKAGE = "@openai/codex";
|
|
14
|
+
export async function runDaemonUpdater(deps) {
|
|
15
|
+
let ok = true;
|
|
16
|
+
const cli = await deps.updateCli();
|
|
17
|
+
// The restart stops this process too, so it is requested only once all the
|
|
18
|
+
// updater's work (including a global npm install of Codex) is done.
|
|
19
|
+
let restart = false;
|
|
20
|
+
switch (cli.action) {
|
|
21
|
+
case "updated":
|
|
22
|
+
deps.log(`hq CLI updated to ${cli.latest ?? "the latest version"}; the daemon restarts onto it once Codex is checked`);
|
|
23
|
+
restart = true;
|
|
24
|
+
break;
|
|
25
|
+
case "update-failed":
|
|
26
|
+
case "update-ineffective":
|
|
27
|
+
deps.log(`hq CLI update to ${cli.latest ?? "latest"} did not take effect (${cli.action})`);
|
|
28
|
+
ok = false;
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
deps.log(`hq CLI: ${cli.action === "current" ? "up to date" : cli.action}`);
|
|
32
|
+
}
|
|
33
|
+
if (!updateCodex(deps))
|
|
34
|
+
ok = false;
|
|
35
|
+
if (restart)
|
|
36
|
+
deps.requestDaemonRestart();
|
|
37
|
+
return { ok };
|
|
38
|
+
}
|
|
39
|
+
/** Update Codex when it is a global npm install and behind; false when that update failed. */
|
|
40
|
+
function updateCodex(deps) {
|
|
41
|
+
const installed = deps.codex.installedVersion();
|
|
42
|
+
if (installed === null)
|
|
43
|
+
return true;
|
|
44
|
+
const latest = deps.codex.latestVersion();
|
|
45
|
+
if (latest === null || !semver.valid(installed) || !semver.valid(latest) || !semver.gt(latest, installed)) {
|
|
46
|
+
deps.log(`Codex ${installed}: nothing to update`);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const result = deps.codex.install();
|
|
50
|
+
if (result.ok) {
|
|
51
|
+
deps.log(`Codex updated ${installed} -> ${latest}`);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
deps.log(`Codex update ${installed} -> ${latest} failed: ${result.detail ?? "unknown error"}`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The variables for the updater's own CLI update. Every supervised service
|
|
59
|
+
* gets HQ_NO_UPDATE_CHECK=1 so it never updates itself; the updater is the one
|
|
60
|
+
* process that must, so it drops that opt-out.
|
|
61
|
+
*/
|
|
62
|
+
export function updaterEnv(vars) {
|
|
63
|
+
const copy = { ...vars };
|
|
64
|
+
delete copy.HQ_NO_UPDATE_CHECK;
|
|
65
|
+
return copy;
|
|
66
|
+
}
|
|
67
|
+
function npm(args) {
|
|
68
|
+
const r = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", args, {
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
timeout: 10 * 60_000,
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
shell: process.platform === "win32",
|
|
73
|
+
});
|
|
74
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
75
|
+
}
|
|
76
|
+
/** Codex through the global npm install, the way the boxes install it. */
|
|
77
|
+
export function npmCodexUpdater() {
|
|
78
|
+
return {
|
|
79
|
+
installedVersion: () => {
|
|
80
|
+
const r = npm(["ls", "-g", CODEX_PACKAGE, "--depth=0", "--json"]);
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(r.stdout);
|
|
83
|
+
return parsed.dependencies?.[CODEX_PACKAGE]?.version ?? null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
latestVersion: () => {
|
|
90
|
+
const r = npm(["view", CODEX_PACKAGE, "version"]);
|
|
91
|
+
const v = r.status === 0 ? r.stdout.trim() : "";
|
|
92
|
+
return v || null;
|
|
93
|
+
},
|
|
94
|
+
install: () => {
|
|
95
|
+
const r = npm(["install", "-g", `${CODEX_PACKAGE}@latest`]);
|
|
96
|
+
if (r.status === 0)
|
|
97
|
+
return { ok: true };
|
|
98
|
+
const tail = r.stderr.trim().split("\n").slice(-3).join(" ").slice(0, 400);
|
|
99
|
+
return { ok: false, detail: tail || `npm exited ${r.status ?? "by signal"}` };
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=updater.js.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* keep a short-lived, offline CLI alive until the client's network deadline.
|
|
7
7
|
*/
|
|
8
8
|
import { type WorkerOptions } from "node:worker_threads";
|
|
9
|
-
import { type FlagClient, type FlagLookupOptions } from "@indigoai-us/hq-flags-client";
|
|
9
|
+
import { type FlagClient, type FlagLookupOptions, type FlagSnapshot } from "@indigoai-us/hq-flags-client";
|
|
10
10
|
/** Dedicated endpoint configuration; this is intentionally not HQ_PRO_API_URL. */
|
|
11
11
|
export declare const FLAG_REGISTRY_ENDPOINT_ENV = "HQ_FLAGS_API_URL";
|
|
12
12
|
/** The synchronous capability that command gate call sites need. */
|
|
@@ -38,6 +38,12 @@ export declare function kickFlagRegistryReadiness(dependencies?: FlagRegistryDep
|
|
|
38
38
|
export declare function resolveFlagGate(reader: FlagReader | null | undefined, flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean): boolean;
|
|
39
39
|
/** Resolve a process gate against the shared snapshot reader, when configured. */
|
|
40
40
|
export declare function resolveProcessFlagGate(flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The registry snapshot held by this process, or null when none has arrived
|
|
43
|
+
* (no endpoint configured, offline, or still fetching). Callers that must
|
|
44
|
+
* distinguish "unreachable" from "off" read this instead of a gate.
|
|
45
|
+
*/
|
|
46
|
+
export declare function currentFlagSnapshot(): FlagSnapshot | null;
|
|
41
47
|
/** Test-only reset for process-global readiness state. */
|
|
42
48
|
export declare function _resetFlagRegistryForTests(): void;
|
|
43
49
|
export {};
|
|
@@ -83,6 +83,14 @@ export function resolveFlagGate(reader, flagKey, lookup, legacyFallback) {
|
|
|
83
83
|
export function resolveProcessFlagGate(flagKey, lookup, legacyFallback) {
|
|
84
84
|
return resolveFlagGate(processReader, flagKey, lookup, legacyFallback);
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The registry snapshot held by this process, or null when none has arrived
|
|
88
|
+
* (no endpoint configured, offline, or still fetching). Callers that must
|
|
89
|
+
* distinguish "unreachable" from "off" read this instead of a gate.
|
|
90
|
+
*/
|
|
91
|
+
export function currentFlagSnapshot() {
|
|
92
|
+
return heldSnapshot;
|
|
93
|
+
}
|
|
86
94
|
/** Test-only reset for process-global readiness state. */
|
|
87
95
|
export function _resetFlagRegistryForTests() {
|
|
88
96
|
heldSnapshot = null;
|
|
@@ -57,7 +57,9 @@ export type ResumeCommandState = (typeof RESUME_COMMAND_STATES)[number];
|
|
|
57
57
|
export declare function isResumeCommandState(state: LaneState): state is ResumeCommandState;
|
|
58
58
|
export declare const LANE_DELIVERY_MECHANISMS: readonly ["dropbox", "resume", "none"];
|
|
59
59
|
export type LaneDeliveryMechanism = (typeof LANE_DELIVERY_MECHANISMS)[number];
|
|
60
|
-
export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id">;
|
|
60
|
+
export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair">;
|
|
61
|
+
/** Only the terminal result of a failed automatic envelope repair is recoverable. */
|
|
62
|
+
export declare function isFailedEnvelopeRepairLane(lane: DeliveryLane): boolean;
|
|
61
63
|
/**
|
|
62
64
|
* `dropbox`: a process is (or is about to be) alive to drain the inbox.
|
|
63
65
|
* `resume`: the process has exited, the provider can resume, the state allows
|
|
@@ -120,7 +122,7 @@ export type LaunchTrigger = {
|
|
|
120
122
|
/** `hq lanes resume`: the operator's explicit opt-in to continue an exited lane. */
|
|
121
123
|
kind: "resume";
|
|
122
124
|
/** Lane state at the moment of the command; recorded as `last_resume.resumed_from`. */
|
|
123
|
-
from: ResumeCommandState;
|
|
125
|
+
from: ResumeCommandState | "failed";
|
|
124
126
|
text?: string;
|
|
125
127
|
message_id?: string;
|
|
126
128
|
};
|
|
@@ -147,5 +149,5 @@ export declare function resumeCommandRefusal(lane: DeliveryLane): {
|
|
|
147
149
|
*/
|
|
148
150
|
export declare function planLaneLaunch(lane: DeliveryLane, trigger: LaunchTrigger): LaunchPlan;
|
|
149
151
|
/** Lane states a launch may claim `running` from, per trigger. */
|
|
150
|
-
export declare function claimableStatesFor(trigger: LaunchTrigger): readonly LaneState[];
|
|
152
|
+
export declare function claimableStatesFor(trigger: LaunchTrigger, lane?: DeliveryLane): readonly LaneState[];
|
|
151
153
|
//# sourceMappingURL=delivery.d.ts.map
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import * as fs from "node:fs";
|
|
21
21
|
import * as path from "node:path";
|
|
22
22
|
import { LanesError } from "./errors.js";
|
|
23
|
-
import { parseLaneProvider, } from "./types.js";
|
|
23
|
+
import { parseLaneProvider, ENVELOPE_INVALID_REASON, } from "./types.js";
|
|
24
24
|
/** Which providers can continue an exited worker from a recorded session id. */
|
|
25
25
|
export const PROVIDER_SESSION_RESUME = {
|
|
26
26
|
claude: true,
|
|
@@ -67,6 +67,13 @@ export function isResumeCommandState(state) {
|
|
|
67
67
|
return RESUME_COMMAND_STATES.includes(state);
|
|
68
68
|
}
|
|
69
69
|
export const LANE_DELIVERY_MECHANISMS = ["dropbox", "resume", "none"];
|
|
70
|
+
/** Only the terminal result of a failed automatic envelope repair is recoverable. */
|
|
71
|
+
export function isFailedEnvelopeRepairLane(lane) {
|
|
72
|
+
return (lane.state === "failed" &&
|
|
73
|
+
lane.terminal_reason === ENVELOPE_INVALID_REASON &&
|
|
74
|
+
lane.envelope_repair?.attempted === true &&
|
|
75
|
+
lane.envelope_repair.succeeded === false);
|
|
76
|
+
}
|
|
70
77
|
/**
|
|
71
78
|
* `dropbox`: a process is (or is about to be) alive to drain the inbox.
|
|
72
79
|
* `resume`: the process has exited, the provider can resume, the state allows
|
|
@@ -77,7 +84,7 @@ export function laneDeliveryMechanism(lane) {
|
|
|
77
84
|
if (lane.state === "queued" || lane.state === "running")
|
|
78
85
|
return "dropbox";
|
|
79
86
|
if (providerSupportsSessionResume(lane.provider) &&
|
|
80
|
-
isResumableLaneState(lane.state) &&
|
|
87
|
+
(isResumableLaneState(lane.state) || isFailedEnvelopeRepairLane(lane)) &&
|
|
81
88
|
hasRecordedSessionId(lane)) {
|
|
82
89
|
return "resume";
|
|
83
90
|
}
|
|
@@ -275,7 +282,7 @@ export const LAUNCH_TRIGGERS = ["admit", "answered_questions", "message", "resum
|
|
|
275
282
|
* without a resume path, no recorded session. Null when the resume can go.
|
|
276
283
|
*/
|
|
277
284
|
export function resumeCommandRefusal(lane) {
|
|
278
|
-
if (!isResumeCommandState(lane.state)) {
|
|
285
|
+
if (!isResumeCommandState(lane.state) && !isFailedEnvelopeRepairLane(lane)) {
|
|
279
286
|
return {
|
|
280
287
|
error: "lane_not_resumable",
|
|
281
288
|
message: `Lane is ${lane.state}. hq lanes resume continues a lane in ${RESUME_COMMAND_STATES.join(", ")}; ${lane.state === "awaiting_input"
|
|
@@ -337,16 +344,17 @@ export function planLaneLaunch(lane, trigger) {
|
|
|
337
344
|
}
|
|
338
345
|
}
|
|
339
346
|
/** Lane states a launch may claim `running` from, per trigger. */
|
|
340
|
-
export function claimableStatesFor(trigger) {
|
|
347
|
+
export function claimableStatesFor(trigger, lane) {
|
|
348
|
+
const envelopeRepairFailed = lane !== undefined && isFailedEnvelopeRepairLane(lane) && hasRecordedSessionId(lane);
|
|
341
349
|
switch (trigger.kind) {
|
|
342
350
|
case "admit":
|
|
343
351
|
return ["queued"];
|
|
344
352
|
case "answered_questions":
|
|
345
353
|
return ["awaiting_input"];
|
|
346
354
|
case "message":
|
|
347
|
-
return RESUMABLE_LANE_STATES;
|
|
355
|
+
return envelopeRepairFailed ? [...RESUMABLE_LANE_STATES, "failed"] : RESUMABLE_LANE_STATES;
|
|
348
356
|
case "resume":
|
|
349
|
-
return RESUME_COMMAND_STATES;
|
|
357
|
+
return envelopeRepairFailed ? [...RESUME_COMMAND_STATES, "failed"] : RESUME_COMMAND_STATES;
|
|
350
358
|
default: {
|
|
351
359
|
const _exhaustive = trigger;
|
|
352
360
|
return _exhaustive;
|
|
@@ -192,6 +192,11 @@ export declare function resumePrompt(input: {
|
|
|
192
192
|
answers?: AnsweredQuestionLine[];
|
|
193
193
|
/** `hq lanes resume`: the state the operator resumed the lane from. */
|
|
194
194
|
resumedFrom?: string;
|
|
195
|
+
/** Exact validator errors recorded after the prior envelope repair failed. */
|
|
196
|
+
envelopeRepairErrors?: {
|
|
197
|
+
first_error: string;
|
|
198
|
+
second_error?: string;
|
|
199
|
+
};
|
|
195
200
|
}): string;
|
|
196
201
|
/** Spec §4: the one repair turn. Schema, parser error, raw text, JSON only. */
|
|
197
202
|
export declare function envelopeRepairPrompt(input: {
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -26,7 +26,7 @@ import { copySessionFiles, planSessionCopy } from "./move-account.js";
|
|
|
26
26
|
import { ensureSessionHome } from "./session-relink.js";
|
|
27
27
|
import { hasEarlierEligibleQueuedLane, laneAdmissionCapReason, withLaneAdmissionLock, } from "./admission.js";
|
|
28
28
|
import { resolveAdmissionHost } from "./admission-host.js";
|
|
29
|
-
import { LANE_ENVELOPE_JSON_SCHEMA, parseLaneProvider, } from "./types.js";
|
|
29
|
+
import { LANE_ENVELOPE_JSON_SCHEMA, ENVELOPE_INVALID_REASON, parseLaneProvider, } from "./types.js";
|
|
30
30
|
export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
31
31
|
/** Written by reconcile before a resume spawn; the worker prompt names this path. */
|
|
32
32
|
export const ANSWERED_QUESTIONS_FILE = "answered-questions.json";
|
|
@@ -996,6 +996,16 @@ export function resumePrompt(input) {
|
|
|
996
996
|
...(lines.length > 0 ? ["Answers:", ...lines] : []),
|
|
997
997
|
].join("\n"));
|
|
998
998
|
}
|
|
999
|
+
if (input.envelopeRepairErrors !== undefined) {
|
|
1000
|
+
const errors = [input.envelopeRepairErrors.first_error, input.envelopeRepairErrors.second_error]
|
|
1001
|
+
.filter((error) => typeof error === "string" && error.length > 0);
|
|
1002
|
+
parts.push([
|
|
1003
|
+
`The previous envelope repair failed validation. The next envelope slot is ${input.envelopePath}; write a new valid envelope there.`,
|
|
1004
|
+
"Exact validation errors recorded in envelope_repair:",
|
|
1005
|
+
...errors.map((error, index) => `- envelope_repair.${index === 0 ? "first_error" : "second_error"}: ${JSON.stringify(error)}`),
|
|
1006
|
+
"For an ask question, include non-empty id, question, and context fields.",
|
|
1007
|
+
].join("\n"));
|
|
1008
|
+
}
|
|
999
1009
|
// Every resume turn carries the authorization, including a textless
|
|
1000
1010
|
// `hq lanes resume`: a pre-upgrade session's brief never had the paragraph.
|
|
1001
1011
|
if (input.operatorMessage !== undefined ||
|
|
@@ -1028,6 +1038,7 @@ export function envelopeRepairPrompt(input) {
|
|
|
1028
1038
|
`The file you wrote at ${input.envelopePath} is not a valid hq-lane-envelope.v1. ` +
|
|
1029
1039
|
`This is the single repair turn: rewrite that file so it validates, and do nothing else.`,
|
|
1030
1040
|
`Parser error: ${input.error}`,
|
|
1041
|
+
"For an ask question, include non-empty id, question, and context fields.",
|
|
1031
1042
|
"Schema:",
|
|
1032
1043
|
JSON.stringify(LANE_ENVELOPE_JSON_SCHEMA, null, 2),
|
|
1033
1044
|
"Your previous output was:",
|
|
@@ -1723,6 +1734,16 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1723
1734
|
answers: (trigger.kind === "answered_questions" || pendingResume?.trigger === "answered_questions") && existsSync(answersPath)
|
|
1724
1735
|
? readAnsweredQuestionLines(answersPath)
|
|
1725
1736
|
: undefined,
|
|
1737
|
+
envelopeRepairErrors: lane.terminal_reason === ENVELOPE_INVALID_REASON &&
|
|
1738
|
+
lane.envelope_repair?.attempted === true &&
|
|
1739
|
+
lane.envelope_repair.succeeded === false
|
|
1740
|
+
? {
|
|
1741
|
+
first_error: lane.envelope_repair.first_error,
|
|
1742
|
+
...(lane.envelope_repair.second_error !== undefined
|
|
1743
|
+
? { second_error: lane.envelope_repair.second_error }
|
|
1744
|
+
: {}),
|
|
1745
|
+
}
|
|
1746
|
+
: undefined,
|
|
1726
1747
|
}),
|
|
1727
1748
|
}
|
|
1728
1749
|
: undefined;
|
|
@@ -1805,7 +1826,6 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1805
1826
|
}
|
|
1806
1827
|
// Claim running before spawning. Two callers that both observe queued must
|
|
1807
1828
|
// not both start processes; the CAS loser returns without spawn.
|
|
1808
|
-
const claimable = claimableStatesFor(trigger);
|
|
1809
1829
|
const nowIso = (deps.now ?? (() => new Date()))().toISOString();
|
|
1810
1830
|
let claimed = false;
|
|
1811
1831
|
const claimedRecord = retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => {
|
|
@@ -1816,7 +1836,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1816
1836
|
}
|
|
1817
1837
|
return current;
|
|
1818
1838
|
}
|
|
1819
|
-
if (!
|
|
1839
|
+
if (!claimableStatesFor(trigger, current).includes(current.state)) {
|
|
1820
1840
|
throw new LanesError("spawn_state_mismatch", `Cannot mark ${current.lane_id} running from ${current.state}`, { lane_id: current.lane_id, state: current.state, trigger: trigger.kind });
|
|
1821
1841
|
}
|
|
1822
1842
|
claimed = true;
|
|
@@ -1826,6 +1846,8 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1826
1846
|
queued_reason: undefined,
|
|
1827
1847
|
queued_at: undefined,
|
|
1828
1848
|
admission_reserved_at: undefined,
|
|
1849
|
+
terminal_reason: undefined,
|
|
1850
|
+
terminal_details: undefined,
|
|
1829
1851
|
// An account move's reservation ends with the claim.
|
|
1830
1852
|
pending_resume: undefined,
|
|
1831
1853
|
account: resolved.id,
|
|
@@ -7,6 +7,8 @@ export declare const LANE_SCHEMA_VERSION: "hq-lane.v1";
|
|
|
7
7
|
export declare const QUESTION_SCHEMA_VERSION: "hq-lane-question.v1";
|
|
8
8
|
export declare const ENVELOPE_SCHEMA_VERSION: "hq-lane-envelope.v1";
|
|
9
9
|
export declare const CONFIG_SCHEMA_VERSION: "hq-lanes-config.v1";
|
|
10
|
+
/** Lane terminal reason for an envelope still invalid after its one repair turn. */
|
|
11
|
+
export declare const ENVELOPE_INVALID_REASON: "envelope_invalid";
|
|
10
12
|
/**
|
|
11
13
|
* The envelope contract as a JSON schema. `parseLaneEnvelope` is the runtime
|
|
12
14
|
* check; this is the text handed to a worker on the repair turn (spec §4) so
|
|
@@ -36,6 +38,11 @@ export declare const LANE_ENVELOPE_JSON_SCHEMA: {
|
|
|
36
38
|
readonly additionalProperties: false;
|
|
37
39
|
readonly required: readonly ["question", "context", "options"];
|
|
38
40
|
readonly properties: {
|
|
41
|
+
readonly id: {
|
|
42
|
+
readonly type: "string";
|
|
43
|
+
readonly minLength: 1;
|
|
44
|
+
readonly pattern: "\\S";
|
|
45
|
+
};
|
|
39
46
|
readonly question: {
|
|
40
47
|
readonly type: "string";
|
|
41
48
|
};
|
|
@@ -444,7 +451,7 @@ export interface AccountHistoryEntry {
|
|
|
444
451
|
export interface PendingResume {
|
|
445
452
|
session_id: string;
|
|
446
453
|
trigger: "answered_questions" | "message" | "resume";
|
|
447
|
-
resumed_from?: "interrupted" | "done" | "blocked";
|
|
454
|
+
resumed_from?: "interrupted" | "done" | "blocked" | "failed";
|
|
448
455
|
text?: string;
|
|
449
456
|
message_id?: string;
|
|
450
457
|
/** False when the command passed --no-compact; the deferred spawn honours it. */
|
|
@@ -471,7 +478,7 @@ export interface LaneResumeRecord {
|
|
|
471
478
|
* `hq lanes resume` only: the state the lane was in when the operator
|
|
472
479
|
* resumed it (`interrupted`, `done` or `blocked`) and the text they sent.
|
|
473
480
|
*/
|
|
474
|
-
resumed_from?: "interrupted" | "done" | "blocked";
|
|
481
|
+
resumed_from?: "interrupted" | "done" | "blocked" | "failed";
|
|
475
482
|
text?: string;
|
|
476
483
|
/** False when the continued turn explicitly skipped compaction. */
|
|
477
484
|
compact?: boolean;
|
|
@@ -555,6 +562,8 @@ export interface QuestionRecord {
|
|
|
555
562
|
answer_history?: QuestionAnswer[];
|
|
556
563
|
}
|
|
557
564
|
export interface EnvelopeQuestion {
|
|
565
|
+
/** Optional for compatibility with existing v1 envelopes; repair prompts request it. */
|
|
566
|
+
id?: string;
|
|
558
567
|
question: string;
|
|
559
568
|
context: string;
|
|
560
569
|
options: string[];
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -8,6 +8,8 @@ export const LANE_SCHEMA_VERSION = "hq-lane.v1";
|
|
|
8
8
|
export const QUESTION_SCHEMA_VERSION = "hq-lane-question.v1";
|
|
9
9
|
export const ENVELOPE_SCHEMA_VERSION = "hq-lane-envelope.v1";
|
|
10
10
|
export const CONFIG_SCHEMA_VERSION = "hq-lanes-config.v1";
|
|
11
|
+
/** Lane terminal reason for an envelope still invalid after its one repair turn. */
|
|
12
|
+
export const ENVELOPE_INVALID_REASON = "envelope_invalid";
|
|
11
13
|
/**
|
|
12
14
|
* The envelope contract as a JSON schema. `parseLaneEnvelope` is the runtime
|
|
13
15
|
* check; this is the text handed to a worker on the repair turn (spec §4) so
|
|
@@ -30,6 +32,7 @@ export const LANE_ENVELOPE_JSON_SCHEMA = {
|
|
|
30
32
|
additionalProperties: false,
|
|
31
33
|
required: ["question", "context", "options"],
|
|
32
34
|
properties: {
|
|
35
|
+
id: { type: "string", minLength: 1, pattern: "\\S" },
|
|
33
36
|
question: { type: "string" },
|
|
34
37
|
context: { type: "string" },
|
|
35
38
|
options: { type: "array", items: { type: "string" } },
|
|
@@ -357,6 +360,14 @@ export function parseLaneEnvelope(raw) {
|
|
|
357
360
|
context: requireString(item.context, `questions[${index}].context`),
|
|
358
361
|
options,
|
|
359
362
|
};
|
|
363
|
+
if (item.id !== undefined) {
|
|
364
|
+
if (typeof item.id !== "string" || item.id.trim().length === 0) {
|
|
365
|
+
throw new LaneSchemaError(`Expected non-empty string for questions[${index}].id`, {
|
|
366
|
+
field: `questions[${index}].id`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
parsed.id = item.id;
|
|
370
|
+
}
|
|
360
371
|
if (item.recommended !== undefined) {
|
|
361
372
|
parsed.recommended = requireString(item.recommended, `questions[${index}].recommended`);
|
|
362
373
|
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* Queue admission of the next lane when a slot frees is owned by the
|
|
32
32
|
* periodic reconcile pass and is out of scope here.
|
|
33
33
|
*/
|
|
34
|
-
import { type LaneEnvelope, type LaneRecord, type LaneState } from "./types.js";
|
|
34
|
+
import { ENVELOPE_INVALID_REASON, type LaneEnvelope, type LaneRecord, type LaneState } from "./types.js";
|
|
35
35
|
import { type QuotaSignal } from "./quota.js";
|
|
36
36
|
import { type ProviderRejection } from "./provider-rejection.js";
|
|
37
37
|
import type { AdmissionHost } from "./admission.js";
|
|
@@ -50,7 +50,7 @@ export declare const WATCHER_HEARTBEAT_MS: number;
|
|
|
50
50
|
export declare const WATCHER_OUT_MAX_BYTES: number;
|
|
51
51
|
export declare const WATCHER_DECISION_EVENTS: readonly ["liveness_transition", "silence_deadline", "envelope_observed", "envelope_invalid", "envelope_repair", "classification", "terminal_write", "mesh_event", "mesh_event_failed"];
|
|
52
52
|
/** Terminal reason when the envelope failed to parse after the repair turn (spec §4). */
|
|
53
|
-
export
|
|
53
|
+
export { ENVELOPE_INVALID_REASON };
|
|
54
54
|
export type WatcherDecisionEvent = (typeof WATCHER_DECISION_EVENTS)[number];
|
|
55
55
|
export declare const WATCHER_SIGNAL_KINDS: readonly ["progress", "terminal", "silence"];
|
|
56
56
|
export type WatcherSignalKind = (typeof WATCHER_SIGNAL_KINDS)[number];
|
|
@@ -35,7 +35,7 @@ import * as fs from "node:fs";
|
|
|
35
35
|
import * as path from "node:path";
|
|
36
36
|
import { LanesError } from "./errors.js";
|
|
37
37
|
import { raiseEnvelopeQuestions } from "./questions.js";
|
|
38
|
-
import { isTerminalLaneState, parseLaneEnvelope, } from "./types.js";
|
|
38
|
+
import { isTerminalLaneState, ENVELOPE_INVALID_REASON, parseLaneEnvelope, } from "./types.js";
|
|
39
39
|
import { laneStories, storyStatusForEnvelope } from "./stories.js";
|
|
40
40
|
import { readLane, retryUpdateLane } from "./store.js";
|
|
41
41
|
import { readWorkerRunResult } from "./delivery.js";
|
|
@@ -70,7 +70,7 @@ export const WATCHER_DECISION_EVENTS = [
|
|
|
70
70
|
"mesh_event_failed",
|
|
71
71
|
];
|
|
72
72
|
/** Terminal reason when the envelope failed to parse after the repair turn (spec §4). */
|
|
73
|
-
export
|
|
73
|
+
export { ENVELOPE_INVALID_REASON };
|
|
74
74
|
export const WATCHER_SIGNAL_KINDS = ["progress", "terminal", "silence"];
|
|
75
75
|
export function parseWatcherSignalKind(value) {
|
|
76
76
|
if (typeof value === "string" &&
|
|
@@ -6,11 +6,11 @@ export declare const BACKOFF_BASE_MS = 1000;
|
|
|
6
6
|
export declare const BACKOFF_CAP_MS = 60000;
|
|
7
7
|
export declare const FLUSH_MAX_ATTEMPTS = 5;
|
|
8
8
|
export type RandomFn = () => number;
|
|
9
|
-
export type SleepFn = (ms: number) => Promise<void>;
|
|
9
|
+
export type SleepFn = (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
10
10
|
export declare function fullJitterDelayMs(attempt: number, opts?: {
|
|
11
11
|
baseMs?: number;
|
|
12
12
|
capMs?: number;
|
|
13
13
|
random?: RandomFn;
|
|
14
14
|
}): number;
|
|
15
|
-
export declare function defaultSleep(ms: number): Promise<void>;
|
|
15
|
+
export declare function defaultSleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
16
16
|
//# sourceMappingURL=backoff.d.ts.map
|
|
@@ -12,9 +12,22 @@ export function fullJitterDelayMs(attempt, opts = {}) {
|
|
|
12
12
|
const exp = Math.min(cap, base * 2 ** attempt);
|
|
13
13
|
return Math.floor(random() * exp);
|
|
14
14
|
}
|
|
15
|
-
export async function defaultSleep(ms) {
|
|
16
|
-
if (ms <= 0)
|
|
15
|
+
export async function defaultSleep(ms, signal) {
|
|
16
|
+
if (ms <= 0 || signal?.aborted)
|
|
17
17
|
return;
|
|
18
|
-
await new Promise((resolve) =>
|
|
18
|
+
await new Promise((resolve) => {
|
|
19
|
+
const onAbort = () => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
finish();
|
|
22
|
+
};
|
|
23
|
+
const finish = () => {
|
|
24
|
+
signal?.removeEventListener("abort", onAbort);
|
|
25
|
+
resolve();
|
|
26
|
+
};
|
|
27
|
+
const timer = setTimeout(finish, ms);
|
|
28
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
29
|
+
if (signal?.aborted)
|
|
30
|
+
onAbort();
|
|
31
|
+
});
|
|
19
32
|
}
|
|
20
33
|
//# sourceMappingURL=backoff.js.map
|
|
@@ -45,8 +45,8 @@ export interface DaemonRunDeps {
|
|
|
45
45
|
timers?: TimerHost;
|
|
46
46
|
fetchCredentials?: CredentialsFetcher;
|
|
47
47
|
mqttConnect?: MqttConnectFn;
|
|
48
|
-
/** Injected flush (tests). */
|
|
49
|
-
flush?: () => Promise<FlushSummary>;
|
|
48
|
+
/** Injected flush (tests). Receives the daemon shutdown signal. */
|
|
49
|
+
flush?: (signal?: AbortSignal) => Promise<FlushSummary>;
|
|
50
50
|
/** Injected outbox replay (tests). */
|
|
51
51
|
replayOutbox?: () => Promise<{
|
|
52
52
|
delivered: number;
|
|
@@ -330,8 +330,9 @@ export async function runMeshDaemon(deps = {}) {
|
|
|
330
330
|
};
|
|
331
331
|
/** In-memory held re-attempt throttle (survives watch-triggered flushes). */
|
|
332
332
|
let lastHeldRetryAtMs = 0;
|
|
333
|
+
const shutdownController = new AbortController();
|
|
333
334
|
const flushFn = deps.flush ??
|
|
334
|
-
(async () => {
|
|
335
|
+
(async (signal) => {
|
|
335
336
|
const t = await getToken();
|
|
336
337
|
const poster = deps.poster ?? createSessionEventsPoster({
|
|
337
338
|
token: t,
|
|
@@ -341,6 +342,7 @@ export async function runMeshDaemon(deps = {}) {
|
|
|
341
342
|
workMeshRoot: meshRoot,
|
|
342
343
|
workContextRoot: ctxRoot,
|
|
343
344
|
poster,
|
|
345
|
+
signal,
|
|
344
346
|
heldRetryIntervalMs: HELD_RETRY_INTERVAL_MS,
|
|
345
347
|
lastHeldRetryAtMs,
|
|
346
348
|
onHeldClaimed: (atMs) => {
|
|
@@ -628,7 +630,7 @@ export async function runMeshDaemon(deps = {}) {
|
|
|
628
630
|
const at = now().getTime();
|
|
629
631
|
noteHookSessionsFromSpoolFile(workMeshSpoolPath(meshRoot), lastHookEventAt, at);
|
|
630
632
|
noteHookSessionsFromSpoolFile(workMeshHeldPath(meshRoot), lastHookEventAt, at);
|
|
631
|
-
const summary = await flushFn();
|
|
633
|
+
const summary = await flushFn(shutdownController.signal);
|
|
632
634
|
try {
|
|
633
635
|
const replay = await replayFn();
|
|
634
636
|
const delivered = replay.delivered ?? 0;
|
|
@@ -794,6 +796,7 @@ export async function runMeshDaemon(deps = {}) {
|
|
|
794
796
|
if (stopped)
|
|
795
797
|
return;
|
|
796
798
|
stopped = true;
|
|
799
|
+
shutdownController.abort();
|
|
797
800
|
log(dir, "daemon stopping");
|
|
798
801
|
if (debounceHandle !== null)
|
|
799
802
|
timers.clearTimeout(debounceHandle);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Flush spool + held session events (US-008).
|
|
3
3
|
*
|
|
4
4
|
* Claim-by-rename → group by sessionId → read work-context state →
|
|
5
|
-
* drop untracked / hold unverified / strip local-only → POST
|
|
6
|
-
*
|
|
5
|
+
* drop untracked / hold unverified / strip local-only → POST at most 25 events
|
|
6
|
+
* per session per flush. Throttles honor Retry-After and preserve the backlog.
|
|
7
7
|
*
|
|
8
8
|
* Orphan `*.claimed` files from a prior crash are recovered first.
|
|
9
9
|
* 2xx responses are accounted per eventId (accepted / duplicates / rejected).
|
|
@@ -32,6 +32,7 @@ export interface FlushDeps {
|
|
|
32
32
|
workMeshRoot: string;
|
|
33
33
|
workContextRoot: string;
|
|
34
34
|
poster: SessionEventsPoster;
|
|
35
|
+
signal?: AbortSignal;
|
|
35
36
|
now?: () => Date;
|
|
36
37
|
sleep?: SleepFn;
|
|
37
38
|
random?: RandomFn;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Flush spool + held session events (US-008).
|
|
3
3
|
*
|
|
4
4
|
* Claim-by-rename → group by sessionId → read work-context state →
|
|
5
|
-
* drop untracked / hold unverified / strip local-only → POST
|
|
6
|
-
*
|
|
5
|
+
* drop untracked / hold unverified / strip local-only → POST at most 25 events
|
|
6
|
+
* per session per flush. Throttles honor Retry-After and preserve the backlog.
|
|
7
7
|
*
|
|
8
8
|
* Orphan `*.claimed` files from a prior crash are recovered first.
|
|
9
9
|
* 2xx responses are accounted per eventId (accepted / duplicates / rejected).
|
|
@@ -21,6 +21,10 @@ import { LOCAL_ONLY_FIELDS, stripLocalOnlyFields } from "./format-spool-line.js"
|
|
|
21
21
|
import { SESSION_EVENTS_BATCH_MAX, WRITABLE_CONTEXT_STATUSES, parseSessionEventsBatchAck, } from "./session-events-client.js";
|
|
22
22
|
import { appendDeadLetterLine, appendDroppedReceipt, appendHeldLine, appendSpoolLine, claimHeld, claimSpool, countJsonlLines, ensureSpoolFile, recoverOrphanClaims, removeClaimFile, } from "./spool.js";
|
|
23
23
|
import { workMeshHeldPath, workMeshRoot } from "./paths.js";
|
|
24
|
+
/** Bound a replayed session backlog so one flush cannot burst hundreds of events. */
|
|
25
|
+
const SESSION_EVENTS_MAX_PER_SESSION_PER_FLUSH = 25;
|
|
26
|
+
/** Never keep a flush blocked on a Retry-After value for more than five minutes. */
|
|
27
|
+
const MAX_RETRY_AFTER_MS = 5 * 60 * 1000;
|
|
24
28
|
export const HELD_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
25
29
|
/** Re-attempt held events at most this often (spool still flushes every cycle). */
|
|
26
30
|
export const HELD_RETRY_INTERVAL_MS = 5 * 60 * 1000;
|
|
@@ -126,18 +130,32 @@ async function postBatchWithRetries(poster, body, deps) {
|
|
|
126
130
|
let lastStatus = 0;
|
|
127
131
|
let lastBody = {};
|
|
128
132
|
for (let attempt = 0; attempt < deps.maxAttempts; attempt += 1) {
|
|
133
|
+
if (deps.signal?.aborted) {
|
|
134
|
+
return { ok: false, retryable: true, status: lastStatus, body: lastBody };
|
|
135
|
+
}
|
|
129
136
|
const res = await poster(body);
|
|
130
137
|
lastStatus = res.status;
|
|
131
138
|
lastBody = res.body;
|
|
132
139
|
if (res.ok)
|
|
133
140
|
return { ok: true, body: res.body };
|
|
141
|
+
if (res.status === 429 || res.status === 503) {
|
|
142
|
+
const retryAfterMs = typeof res.retryAfterMs === "number" &&
|
|
143
|
+
Number.isFinite(res.retryAfterMs) &&
|
|
144
|
+
res.retryAfterMs >= 0
|
|
145
|
+
? Math.min(res.retryAfterMs, MAX_RETRY_AFTER_MS)
|
|
146
|
+
: 0;
|
|
147
|
+
const jitterMs = fullJitterDelayMs(attempt, { random: deps.random });
|
|
148
|
+
await deps.sleep(Math.min(MAX_RETRY_AFTER_MS, Math.max(retryAfterMs, jitterMs)), deps.signal);
|
|
149
|
+
// A throttled batch stays queued for a later flush; do not hot-loop it.
|
|
150
|
+
return { ok: false, retryable: true, status: res.status, body: res.body };
|
|
151
|
+
}
|
|
134
152
|
if (!res.retryable) {
|
|
135
153
|
return { ok: false, retryable: false, status: res.status, body: res.body };
|
|
136
154
|
}
|
|
137
155
|
if (attempt + 1 >= deps.maxAttempts)
|
|
138
156
|
break;
|
|
139
157
|
const delay = fullJitterDelayMs(attempt, { random: deps.random });
|
|
140
|
-
await deps.sleep(delay);
|
|
158
|
+
await deps.sleep(delay, deps.signal);
|
|
141
159
|
}
|
|
142
160
|
return { ok: false, retryable: true, status: lastStatus, body: lastBody };
|
|
143
161
|
}
|
|
@@ -230,7 +248,9 @@ export async function flushSessionEvents(deps) {
|
|
|
230
248
|
pendingRestore.delete(event);
|
|
231
249
|
};
|
|
232
250
|
const loggedProject404 = new Set();
|
|
233
|
-
for (const [sessionId, items] of bySession) {
|
|
251
|
+
sessionLoop: for (const [sessionId, items] of bySession) {
|
|
252
|
+
if (deps.signal?.aborted)
|
|
253
|
+
break sessionLoop;
|
|
234
254
|
let state = readSessionState(sessionId, deps.workContextRoot);
|
|
235
255
|
if (state?.companyConfidence === "device_default" &&
|
|
236
256
|
deps.authorizeDeviceDefaultTenant) {
|
|
@@ -273,11 +293,13 @@ export async function flushSessionEvents(deps) {
|
|
|
273
293
|
}
|
|
274
294
|
continue;
|
|
275
295
|
}
|
|
276
|
-
// Post
|
|
277
|
-
|
|
296
|
+
// Post a bounded portion of this session's backlog. Untouched events
|
|
297
|
+
// remain in pendingRestore and are put back on the spool below.
|
|
298
|
+
const flushItems = items.slice(0, SESSION_EVENTS_MAX_PER_SESSION_PER_FLUSH);
|
|
299
|
+
const stripped = flushItems.map((item) => stripLocalOnlyFields(item.event));
|
|
278
300
|
for (let i = 0; i < stripped.length; i += SESSION_EVENTS_BATCH_MAX) {
|
|
279
301
|
const chunk = stripped.slice(i, i + SESSION_EVENTS_BATCH_MAX);
|
|
280
|
-
const originalChunk =
|
|
302
|
+
const originalChunk = flushItems.slice(i, i + SESSION_EVENTS_BATCH_MAX);
|
|
281
303
|
const body = {
|
|
282
304
|
contractVersion: 1,
|
|
283
305
|
companyUid: decision.companyUid,
|
|
@@ -296,6 +318,7 @@ export async function flushSessionEvents(deps) {
|
|
|
296
318
|
maxAttempts,
|
|
297
319
|
sleep,
|
|
298
320
|
random,
|
|
321
|
+
signal: deps.signal,
|
|
299
322
|
});
|
|
300
323
|
if (result.ok) {
|
|
301
324
|
accountBatchSuccess(result.body, originalChunk, deps.workMeshRoot, now, summary, markDisposed);
|
|
@@ -347,6 +370,10 @@ export async function flushSessionEvents(deps) {
|
|
|
347
370
|
summary.restored += 1;
|
|
348
371
|
markDisposed(item.event);
|
|
349
372
|
}
|
|
373
|
+
if (result.status === 429 || result.status === 503)
|
|
374
|
+
break;
|
|
375
|
+
if (deps.signal?.aborted)
|
|
376
|
+
break sessionLoop;
|
|
350
377
|
}
|
|
351
378
|
}
|
|
352
379
|
}
|