@indigoai-us/hq-cli 5.296.0 → 5.297.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 +4 -0
- package/dist/commands/mesh.js +3 -1
- package/dist/lib/daemon/handoff.d.ts +1 -0
- package/dist/lib/daemon/handoff.js +18 -2
- package/dist/lib/daemon/legacy.d.ts +7 -0
- package/dist/lib/daemon/legacy.js +6 -0
- package/dist/lib/daemon/lock.js +1 -1
- package/dist/lib/daemon/run.js +57 -2
- package/dist/lib/daemon/supervisor.d.ts +2 -0
- package/dist/lib/daemon/supervisor.js +26 -0
- package/dist/lib/doctor/checks/work-context.js +5 -1
- package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/doctor.js +10 -0
- package/dist/lib/work-context/outbox-quarantine-status-flag.d.ts +4 -0
- package/dist/lib/work-context/outbox-quarantine-status-flag.js +7 -0
- package/dist/lib/work-context/outbox.d.ts +1 -0
- package/dist/lib/work-context/outbox.js +7 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.297.0] — 2026-09-30
|
|
6
|
+
- When enabled by hq-flags, show the quarantined work-context outbox count and latest reason in hq doctor and mesh daemon status.
|
|
7
|
+
- `hq daemon` now exits after shutdown when handles remain open, and its mesh service waits when another live mesh daemon owns the process lock.
|
|
8
|
+
|
|
5
9
|
## [5.296.0] — 2026-09-30
|
|
6
10
|
- Upgrade varlock to 1.21.1. `hq run` skips Varlock cache setup and reports malformed env files promptly without sending them to Sentry.
|
|
7
11
|
- Record bounded actor and trigger history for lane worker starts and expose it through `hq lanes show --json`. Preserve history through conflict merges and envelope repair, and record answered-question callers for immediate and queued resumes.
|
package/dist/commands/mesh.js
CHANGED
|
@@ -2129,7 +2129,9 @@ async function runDaemonInstall(opts) {
|
|
|
2129
2129
|
// `hq daemon` runs mesh on this machine: installing a second unit would run it twice.
|
|
2130
2130
|
// An explicit --hq-root is verified the same way as below and saved for the daemon.
|
|
2131
2131
|
const explicitRoot = opts.hqRoot ? resolveInstallHqRoot({ explicit: opts.hqRoot }).root : undefined;
|
|
2132
|
-
const handedOff = opts.systemUser
|
|
2132
|
+
const handedOff = opts.systemUser
|
|
2133
|
+
? null
|
|
2134
|
+
: handOffInstallHere(["mesh"], process.env, { hqRoot: explicitRoot, ifStale: opts.ifStale });
|
|
2133
2135
|
if (handedOff) {
|
|
2134
2136
|
console.log(opts.json ? JSON.stringify({ ok: true, action: "hq-daemon", reinstalled: false, message: handedOff }) : handedOff);
|
|
2135
2137
|
return;
|
|
@@ -51,5 +51,6 @@ export declare function handOffInstall(opts: {
|
|
|
51
51
|
/** handOffInstall for this machine and user, for the older install commands. */
|
|
52
52
|
export declare function handOffInstallHere(services: readonly ServiceName[], env?: NodeJS.ProcessEnv, opts?: {
|
|
53
53
|
hqRoot?: string;
|
|
54
|
+
ifStale?: boolean;
|
|
54
55
|
}): string | null;
|
|
55
56
|
//# sourceMappingURL=handoff.d.ts.map
|
|
@@ -17,6 +17,15 @@ import { daemonPaths } from "./paths.js";
|
|
|
17
17
|
import { writeControlRequest } from "./requests.js";
|
|
18
18
|
/** Statuses where the daemon itself owns the unit's process. */
|
|
19
19
|
const OWNED = new Set(["running", "backoff", "exited", "idle", "stopped"]);
|
|
20
|
+
const RECENT_HOSTED_DAEMON_MS = 5 * 60_000;
|
|
21
|
+
/** The updater's postinstall may run in the short gap before the desktop app relaunches its daemon. */
|
|
22
|
+
function recentlyHostedDaemon(paths) {
|
|
23
|
+
if (!fs.existsSync(paths.statePath))
|
|
24
|
+
return false;
|
|
25
|
+
const state = JSON.parse(fs.readFileSync(paths.statePath, "utf8"));
|
|
26
|
+
const age = Date.now() - (state.updatedAt ?? 0);
|
|
27
|
+
return state.host === "desktop" && age >= 0 && age <= RECENT_HOSTED_DAEMON_MS;
|
|
28
|
+
}
|
|
20
29
|
/** The live daemon's state, or null when no daemon is running. */
|
|
21
30
|
function liveState(paths, alive) {
|
|
22
31
|
if (runningDaemonPid(paths.lockPath, alive) === null)
|
|
@@ -124,14 +133,21 @@ export function handOffInstall(opts) {
|
|
|
124
133
|
export function handOffInstallHere(services, env = process.env, opts = {}) {
|
|
125
134
|
const home = os.homedir();
|
|
126
135
|
const paths = daemonPaths(home, env);
|
|
136
|
+
const config = readDaemonConfig(paths.configPath);
|
|
137
|
+
if (opts.ifStale && services.every((service) => config.disable.includes(service))) {
|
|
138
|
+
return `hq daemon has ${services.join(", ")} disabled, so the older service will not be installed.`;
|
|
139
|
+
}
|
|
127
140
|
const installed = daemonUnitInstalled({
|
|
128
141
|
platform: process.platform,
|
|
129
142
|
home,
|
|
130
143
|
windowsTaskExists: (task) => spawnSync("schtasks.exe", ["/Query", "/TN", task], { encoding: "utf8", windowsHide: true }).status === 0,
|
|
131
144
|
}) || hostedDaemonRunning(paths);
|
|
132
|
-
if (!installed)
|
|
145
|
+
if (!installed) {
|
|
146
|
+
if (opts.ifStale && services.includes("mesh") && recentlyHostedDaemon(paths)) {
|
|
147
|
+
return `hq daemon recently hosted mesh on this machine, so the older mesh service will not be installed during its restart.`;
|
|
148
|
+
}
|
|
133
149
|
return null;
|
|
134
|
-
|
|
150
|
+
}
|
|
135
151
|
const machineType = detectMachineType(undefined, {
|
|
136
152
|
platform: process.platform,
|
|
137
153
|
env,
|
|
@@ -32,4 +32,11 @@ export declare function findLegacyUnits(deps: LegacyProbeDeps): LegacyUnit[];
|
|
|
32
32
|
* own checks, because `hq daemon migrate` will never take them over.
|
|
33
33
|
*/
|
|
34
34
|
export declare function legacyOwnerLookup(units: readonly LegacyUnit[]): (unitId: string) => string | null;
|
|
35
|
+
/** A daemon-managed macOS mesh service takes over the old LaunchAgent on startup. */
|
|
36
|
+
export declare function legacyMeshUnitsToRetire(units: readonly LegacyUnit[], opts: {
|
|
37
|
+
platform: NodeJS.Platform;
|
|
38
|
+
managed: boolean;
|
|
39
|
+
hosted: boolean;
|
|
40
|
+
meshEnabled: boolean;
|
|
41
|
+
}): LegacyUnit[];
|
|
35
42
|
//# sourceMappingURL=legacy.d.ts.map
|
|
@@ -122,4 +122,10 @@ export function legacyOwnerLookup(units) {
|
|
|
122
122
|
return hit ? describeLegacyUnit(hit) : null;
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
+
/** A daemon-managed macOS mesh service takes over the old LaunchAgent on startup. */
|
|
126
|
+
export function legacyMeshUnitsToRetire(units, opts) {
|
|
127
|
+
if (opts.platform !== "darwin" || (!opts.managed && !opts.hosted) || !opts.meshEnabled)
|
|
128
|
+
return [];
|
|
129
|
+
return units.filter((unit) => unit.service === "mesh" && unit.kind === "launchd" && unit.migratable);
|
|
130
|
+
}
|
|
125
131
|
//# sourceMappingURL=legacy.js.map
|
package/dist/lib/daemon/lock.js
CHANGED
|
@@ -43,7 +43,7 @@ export function processCommandLine(pid, deps = defaultCommandLineDeps()) {
|
|
|
43
43
|
"-Command",
|
|
44
44
|
`(Get-CimInstance Win32_Process -Filter "ProcessId=${Math.trunc(pid)}").CommandLine`,
|
|
45
45
|
])
|
|
46
|
-
: deps.run("ps", ["-o", "
|
|
46
|
+
: deps.run("ps", ["-ww", "-o", "args=", "-p", String(pid)]);
|
|
47
47
|
if (r.status !== 0)
|
|
48
48
|
return null;
|
|
49
49
|
const line = r.stdout.trim();
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -19,9 +19,12 @@ import { botsRoot } from "../bot/paths.js";
|
|
|
19
19
|
import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
|
|
20
20
|
import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
|
|
21
21
|
import { SUPERVISOR_HANDOFF_FLAG, readDaemonFlag } from "./flag.js";
|
|
22
|
-
import { findLegacyUnits, legacyOwnerLookup } from "./legacy.js";
|
|
22
|
+
import { findLegacyUnits, legacyMeshUnitsToRetire, legacyOwnerLookup } from "./legacy.js";
|
|
23
23
|
import { readLegacySyncEnv } from "./legacy-sync-env.js";
|
|
24
|
-
import { acquireDaemonLock, releaseDaemonLock } from "./lock.js";
|
|
24
|
+
import { acquireDaemonLock, isProcessAlive, processCommandLine, releaseDaemonLock } from "./lock.js";
|
|
25
|
+
import { adoptLegacyUnits } from "./migrate.js";
|
|
26
|
+
import { daemonDir as meshDaemonDir, daemonPidPath as meshDaemonPidPath } from "../mesh/live/daemon/paths.js";
|
|
27
|
+
import { readPidLock } from "../mesh/live/daemon/pid-lock.js";
|
|
25
28
|
import { daemonRootRefusal } from "./install.js";
|
|
26
29
|
import { detectMachineType } from "./machine-type.js";
|
|
27
30
|
import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from "./paths.js";
|
|
@@ -332,6 +335,54 @@ export async function runDaemon(opts) {
|
|
|
332
335
|
}
|
|
333
336
|
return legacyCache.units;
|
|
334
337
|
};
|
|
338
|
+
const legacyMesh = legacyMeshUnitsToRetire(legacyUnits(), {
|
|
339
|
+
platform,
|
|
340
|
+
managed: opts.managed === true,
|
|
341
|
+
hosted: opts.host !== undefined,
|
|
342
|
+
meshEnabled: enabledServices(machineType, initial).includes("mesh"),
|
|
343
|
+
});
|
|
344
|
+
if (legacyMesh.length > 0) {
|
|
345
|
+
try {
|
|
346
|
+
const uid = process.getuid?.() ?? 0;
|
|
347
|
+
const result = adoptLegacyUnits(legacyMesh, {
|
|
348
|
+
run: (command, args) => {
|
|
349
|
+
const r = spawnSync(command, args, { encoding: "utf8", windowsHide: true, timeout: 15_000 });
|
|
350
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
351
|
+
},
|
|
352
|
+
uid,
|
|
353
|
+
isRoot: uid === 0,
|
|
354
|
+
adoptedDir: path.join(paths.root, "adopted"),
|
|
355
|
+
recordPath: path.join(paths.root, "adopted.json"),
|
|
356
|
+
systemAdoptedDir: "/var/lib/hq/daemon/adopted",
|
|
357
|
+
systemRecordPath: "/var/lib/hq/daemon/adopted.json",
|
|
358
|
+
systemUnitDir: "/etc/systemd/system",
|
|
359
|
+
});
|
|
360
|
+
for (const unit of result.adopted)
|
|
361
|
+
log("info", `retired legacy ${unit.unitId} unit ${unit.name}; hq daemon now owns mesh`);
|
|
362
|
+
for (const failure of result.failed)
|
|
363
|
+
log("warn", `could not retire legacy ${failure.unit.unitId} unit ${failure.unit.name}: ${failure.error}`);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
log("error", `could not retire legacy mesh units: ${err.message}`);
|
|
367
|
+
}
|
|
368
|
+
legacyCache = null;
|
|
369
|
+
}
|
|
370
|
+
const meshOwnerReason = () => {
|
|
371
|
+
const lockPath = meshDaemonPidPath(meshDaemonDir(undefined, home, env));
|
|
372
|
+
const owner = readPidLock(lockPath);
|
|
373
|
+
if (!owner || owner.pid === process.pid || !isProcessAlive(owner.pid))
|
|
374
|
+
return undefined;
|
|
375
|
+
const command = processCommandLine(owner.pid, {
|
|
376
|
+
platform,
|
|
377
|
+
run: (file, args) => {
|
|
378
|
+
const r = spawnSync(file, args, { encoding: "utf8", windowsHide: true, timeout: 15_000 });
|
|
379
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
if (command && !/\bmesh\s+daemon\s+run\b/.test(command))
|
|
383
|
+
return undefined;
|
|
384
|
+
return `mesh daemon already running (pid ${owner.pid}); waiting for it to exit`;
|
|
385
|
+
};
|
|
335
386
|
let warnedMissingPin = null;
|
|
336
387
|
const daemonSyncRunnerPids = new Set();
|
|
337
388
|
const plan = () => {
|
|
@@ -554,6 +605,7 @@ export async function runDaemon(opts) {
|
|
|
554
605
|
return undefined;
|
|
555
606
|
}
|
|
556
607
|
},
|
|
608
|
+
meshOwnerReason,
|
|
557
609
|
cliVersion: CLI_VERSION,
|
|
558
610
|
adopt,
|
|
559
611
|
});
|
|
@@ -603,6 +655,9 @@ export async function runDaemon(opts) {
|
|
|
603
655
|
log("info", "all services stopped");
|
|
604
656
|
releaseDaemonLock(paths.lockPath);
|
|
605
657
|
resolve(exitCode);
|
|
658
|
+
// Pipes, sockets and unrelated timers can outlive a clean shutdown.
|
|
659
|
+
// Keep restart and signal exits bounded just like the handoff path.
|
|
660
|
+
setTimeout(() => process.exit(exitCode), 5_000).unref();
|
|
606
661
|
});
|
|
607
662
|
};
|
|
608
663
|
/** Exit for a restart but leave the services running for the next daemon to adopt. */
|
|
@@ -107,6 +107,8 @@ export interface SupervisorDeps {
|
|
|
107
107
|
requestPollMs?: number;
|
|
108
108
|
/** Read hq-cloud's lease status after it exits with the lease-conflict code. */
|
|
109
109
|
syncOwnerBlockReason?: () => string | undefined;
|
|
110
|
+
/** A separate mesh daemon currently owns its pid lock after our mesh child exits. */
|
|
111
|
+
meshOwnerReason?: () => string | undefined;
|
|
110
112
|
/** The running CLI's version; part of the key of units that run `hq` commands. */
|
|
111
113
|
cliVersion?: string;
|
|
112
114
|
/** Services to take over from the previous daemon instead of starting them. */
|
|
@@ -27,6 +27,7 @@ export const BACKOFF_BASE_MS = 1_000;
|
|
|
27
27
|
export const BACKOFF_MAX_MS = 60_000;
|
|
28
28
|
export const HEALTHY_RUN_MS = 60_000;
|
|
29
29
|
export const STOP_ESCALATION_MS = 10_000;
|
|
30
|
+
const MESH_OWNER_RECHECK_MS = 15_000;
|
|
30
31
|
/** After a handoff, how long a replaced unit must stay up before the next one is replaced. */
|
|
31
32
|
export const HANDOFF_STAGGER_MS = 15_000;
|
|
32
33
|
/** How long a draining unit may take to finish its work before it gets SIGTERM. */
|
|
@@ -294,6 +295,31 @@ export function createSupervisor(deps) {
|
|
|
294
295
|
emit();
|
|
295
296
|
return;
|
|
296
297
|
}
|
|
298
|
+
const meshOwnerReason = slot.plan.service === "mesh" ? deps.meshOwnerReason?.() : undefined;
|
|
299
|
+
if (meshOwnerReason) {
|
|
300
|
+
const waitForOwner = (reason) => {
|
|
301
|
+
slot.state.status = "waiting";
|
|
302
|
+
slot.state.reason = reason;
|
|
303
|
+
slot.state.nextRunAt = now() + MESH_OWNER_RECHECK_MS;
|
|
304
|
+
deps.log("warn", `${reason}; retrying mesh ownership check in ${MESH_OWNER_RECHECK_MS}ms`);
|
|
305
|
+
slot.timer = setT(() => {
|
|
306
|
+
slot.timer = undefined;
|
|
307
|
+
const current = deps.meshOwnerReason?.();
|
|
308
|
+
if (current)
|
|
309
|
+
waitForOwner(current);
|
|
310
|
+
else {
|
|
311
|
+
slot.attempts = 0;
|
|
312
|
+
delete slot.state.reason;
|
|
313
|
+
delete slot.state.nextRunAt;
|
|
314
|
+
startLong(slot);
|
|
315
|
+
}
|
|
316
|
+
emit();
|
|
317
|
+
}, MESH_OWNER_RECHECK_MS);
|
|
318
|
+
};
|
|
319
|
+
waitForOwner(meshOwnerReason);
|
|
320
|
+
emit();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
297
323
|
deps.log("warn", `${slot.plan.id} exited (code=${code ?? "null"} signal=${signal ?? "none"})`);
|
|
298
324
|
if (ranFor >= HEALTHY_RUN_MS)
|
|
299
325
|
slot.attempts = 0;
|
|
@@ -8,6 +8,7 @@ import * as os from "node:os";
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { readDeviceConfig, } from "../../work-context/config.js";
|
|
10
10
|
import { outboxStats, listOutboxOperations } from "../../work-context/outbox.js";
|
|
11
|
+
import { outboxQuarantineStatusEnabled } from "../../work-context/outbox-quarantine-status-flag.js";
|
|
11
12
|
import { workContextRoot, workContextSessionsDir } from "../../work-context/paths.js";
|
|
12
13
|
import { countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
|
|
13
14
|
import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../../mesh/live/paths.js";
|
|
@@ -87,11 +88,14 @@ function runChecks(deps) {
|
|
|
87
88
|
target: path.join(root, "outbox"),
|
|
88
89
|
message: `Outbox queued depth=${stats.depth} (acked=${stats.acked}).`,
|
|
89
90
|
});
|
|
91
|
+
const showQuarantineReason = stats.quarantined > 0 && outboxQuarantineStatusEnabled();
|
|
90
92
|
results.push({
|
|
91
93
|
status: stats.quarantined > 0 ? "WARN" : "PASS",
|
|
92
94
|
checkId: "work-context.outbox-quarantined",
|
|
93
95
|
target: path.join(root, "outbox"),
|
|
94
|
-
message: `Outbox quarantined count=${stats.quarantined}
|
|
96
|
+
message: `Outbox quarantined count=${stats.quarantined}${showQuarantineReason
|
|
97
|
+
? ` (last reason=${stats.lastQuarantineReason ?? "unknown"})`
|
|
98
|
+
: ""}.`,
|
|
95
99
|
});
|
|
96
100
|
// Default-company mode + migration capability (US-017B)
|
|
97
101
|
try {
|
|
@@ -25,6 +25,8 @@ export interface DaemonDoctorReport {
|
|
|
25
25
|
/** Events in any local queue still marked no-company / NEEDS_COMPANY / NONE. */
|
|
26
26
|
unattributedEvents: UnattributedEventCounts;
|
|
27
27
|
outboxDepth: number;
|
|
28
|
+
outboxQuarantined?: number;
|
|
29
|
+
outboxLastQuarantineReason?: string;
|
|
28
30
|
lastFlushAt?: string;
|
|
29
31
|
lastFlushResult?: DaemonStateFile["lastFlushResult"];
|
|
30
32
|
unhealthy: boolean;
|
|
@@ -9,6 +9,7 @@ import { describeCognitoTokenSource, formatCognitoSubject, loadMachineCachedToke
|
|
|
9
9
|
import { listOutboxOperations, outboxStats, } from "../../../work-context/outbox.js";
|
|
10
10
|
import { readSessionState } from "../../../work-context/state.js";
|
|
11
11
|
import { workContextRoot } from "../../../work-context/paths.js";
|
|
12
|
+
import { outboxQuarantineStatusEnabled } from "../../../work-context/outbox-quarantine-status-flag.js";
|
|
12
13
|
import { formatDroppedPresenceCompany } from "./credentials.js";
|
|
13
14
|
import { countJsonlLines, countUnattributedEvents, } from "../spool.js";
|
|
14
15
|
import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../paths.js";
|
|
@@ -258,6 +259,12 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
258
259
|
deadLetterCount,
|
|
259
260
|
unattributedEvents,
|
|
260
261
|
outboxDepth: outbox.depth,
|
|
262
|
+
...(outbox.quarantined > 0 && outboxQuarantineStatusEnabled()
|
|
263
|
+
? {
|
|
264
|
+
outboxQuarantined: outbox.quarantined,
|
|
265
|
+
outboxLastQuarantineReason: outbox.lastQuarantineReason ?? undefined,
|
|
266
|
+
}
|
|
267
|
+
: {}),
|
|
261
268
|
lastFlushAt: state?.lastFlushAt,
|
|
262
269
|
lastFlushResult: state?.lastFlushResult,
|
|
263
270
|
unhealthy: unhealthyReasons.length > 0,
|
|
@@ -302,6 +309,9 @@ export function formatDaemonDoctor(report) {
|
|
|
302
309
|
// receive-only (direct) mode.
|
|
303
310
|
lines.push(`spool depth: ${report.spoolDepth}`, `held: ${report.heldCount}${report.heldCount > 0 ? " (needs_company; not sent as NONE)" : ""}`, `held with no local evidence: ${report.heldWithNoLocalEvidence}`, `outbox depth: ${report.outboxDepth}`);
|
|
304
311
|
}
|
|
312
|
+
if (report.outboxQuarantined !== undefined) {
|
|
313
|
+
lines.push(`outbox quarantined: ${report.outboxQuarantined} (last reason=${report.outboxLastQuarantineReason ?? "unknown"})`);
|
|
314
|
+
}
|
|
305
315
|
if (report.emitMode !== "legacy" && report.heldCount > 0) {
|
|
306
316
|
lines.push(`held: ${report.heldCount} (needs_company; not sent as NONE)`);
|
|
307
317
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const OUTBOX_QUARANTINE_STATUS_FLAG = "agents.work-context-outbox-quarantine-status";
|
|
2
|
+
export declare const OUTBOX_QUARANTINE_STATUS_FALLBACK = false;
|
|
3
|
+
export declare function outboxQuarantineStatusEnabled(): boolean;
|
|
4
|
+
//# sourceMappingURL=outbox-quarantine-status-flag.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { resolveProcessFlagGate } from "../flag-registry.js";
|
|
2
|
+
export const OUTBOX_QUARANTINE_STATUS_FLAG = "agents.work-context-outbox-quarantine-status";
|
|
3
|
+
export const OUTBOX_QUARANTINE_STATUS_FALLBACK = false;
|
|
4
|
+
export function outboxQuarantineStatusEnabled() {
|
|
5
|
+
return resolveProcessFlagGate(OUTBOX_QUARANTINE_STATUS_FLAG, { fallback: OUTBOX_QUARANTINE_STATUS_FALLBACK }, () => OUTBOX_QUARANTINE_STATUS_FALLBACK);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=outbox-quarantine-status-flag.js.map
|
|
@@ -118,6 +118,7 @@ export interface OutboxStats {
|
|
|
118
118
|
quarantined: number;
|
|
119
119
|
queued: number;
|
|
120
120
|
acked: number;
|
|
121
|
+
lastQuarantineReason: string | null;
|
|
121
122
|
}
|
|
122
123
|
export declare function outboxStats(root: string): OutboxStats;
|
|
123
124
|
export type DeliverFn = (op: OutboxOperation) => Promise<{
|
|
@@ -458,11 +458,17 @@ export function listOutboxOperations(root, deps = {}) {
|
|
|
458
458
|
}
|
|
459
459
|
export function outboxStats(root) {
|
|
460
460
|
const ops = listOutboxOperations(root);
|
|
461
|
+
const quarantinedOps = ops.filter((o) => o.delivery === "quarantined");
|
|
462
|
+
const latestQuarantine = quarantinedOps.reduce((latest, op) => !latest || op.updatedAt > latest.updatedAt ||
|
|
463
|
+
(op.updatedAt === latest.updatedAt && op.operationId > latest.operationId)
|
|
464
|
+
? op
|
|
465
|
+
: latest, null);
|
|
461
466
|
return {
|
|
462
467
|
depth: ops.filter((o) => o.delivery === "queued").length,
|
|
463
|
-
quarantined:
|
|
468
|
+
quarantined: quarantinedOps.length,
|
|
464
469
|
queued: ops.filter((o) => o.delivery === "queued").length,
|
|
465
470
|
acked: ops.filter((o) => o.delivery === "acked").length,
|
|
471
|
+
lastQuarantineReason: latestQuarantine?.lastErrorCode ?? null,
|
|
466
472
|
};
|
|
467
473
|
}
|
|
468
474
|
function isOutboxDue(op, nowIso) {
|