@indigoai-us/hq-cli 5.304.0 → 5.305.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 +12 -0
- package/dist/command-catalog.generated.d.ts +1 -1
- package/dist/command-catalog.generated.js +3 -1
- package/dist/command-registration-plan.js +8 -0
- package/dist/commands/lanes.js +13 -0
- package/dist/commands/mcp-status.js +1 -0
- package/dist/commands/members.js +0 -1
- package/dist/commands/people.d.ts +3 -0
- package/dist/commands/people.js +27 -3
- package/dist/index.js +6 -1
- package/dist/lazy-commands.js +5 -2
- package/dist/lib/aws-sdk-node-warning-output.d.ts +12 -0
- package/dist/lib/aws-sdk-node-warning-output.js +22 -0
- package/dist/lib/company-errors-sdk-warning-flag.d.ts +5 -0
- package/dist/lib/company-errors-sdk-warning-flag.js +8 -0
- package/dist/lib/doctor/checks/work-context.js +2 -2
- package/dist/lib/lanes/keep-alive.js +13 -2
- package/dist/lib/mesh/live/daemon/doctor.js +2 -2
- package/dist/lib/mesh/live/flush.d.ts +2 -2
- package/dist/lib/mesh/live/flush.js +49 -29
- package/dist/lib/mesh/live/spool.d.ts +6 -0
- package/dist/lib/mesh/live/spool.js +203 -4
- package/dist/lib/work-context/bind-best.js +13 -1
- package/dist/main.js +16 -3
- package/dist/utils/self-update.d.ts +4 -0
- package/dist/utils/self-update.js +46 -9
- package/dist/utils/version-check.d.ts +7 -0
- package/dist/utils/version-check.js +30 -1
- package/dist/utils/version-gate.d.ts +28 -10
- package/dist/utils/version-gate.js +159 -29
- package/dist/utils/version-report.d.ts +2 -1
- package/dist/utils/version-report.js +8 -1
- package/package.json +1 -1
- package/dist/lib/work-context/index.d.ts +0 -26
- package/dist/lib/work-context/index.js +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.305.0] — 2026-10-01
|
|
6
|
+
|
|
7
|
+
- Work Mesh project discovery skips projects whose matching local lifecycle state is archived or retired.
|
|
8
|
+
|
|
9
|
+
- `hq lanes show` now treats a newer terminal primary mirror as authoritative when an Outpost owner snapshot is behind, avoiding a misleading refresh warning while preserving warnings for nonterminal conflicts.
|
|
10
|
+
- Self-updates detect an unwritable npm global prefix before install, show the command to repair that install, and report the handled permission state as a rate-limited warning.
|
|
11
|
+
- Keep Work Mesh held events unique by event ID across retries, including interrupted overflow recovery.
|
|
12
|
+
- Added `hq mcp list` as an alias for the existing MCP status command. `hq version` can show cached CLI update guidance on stderr when the default-off `cli.version-update-nudge` flag is enabled. Bare `hq --version` prints only the installed CLI semver and starts no flag or network work.
|
|
13
|
+
- Keep-alive no longer lets a newer terminal lane or a same-profile lane under a distinct lane senior supersede a running lane.
|
|
14
|
+
- With `cli.company-errors-sdk-warning` enabled, `hq people` uses shared active-company guidance when local metadata is missing, and files commands show the AWS SDK Node-version notice only with verbose diagnostics.
|
|
15
|
+
- Remove the unrelated secret-sharing hint from active member listings.
|
|
16
|
+
|
|
5
17
|
## [5.304.0] — 2026-10-01
|
|
6
18
|
- `hq lanes show` skips owner refresh only when a failed Outpost dispatch explicitly recorded `pickup_persisted: false` and no owner heartbeat; it reports `remote_refresh: "skipped_undispatched"` in that case.
|
|
7
19
|
|
|
@@ -3622,7 +3622,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3622
3622
|
readonly subcommands: readonly [{
|
|
3623
3623
|
readonly name: "status";
|
|
3624
3624
|
readonly description: "Show installed MCP packs/servers and their health in BOTH Claude and Codex (PARTIAL/DRIFT flagged; secrets redacted)";
|
|
3625
|
-
readonly aliases: readonly [];
|
|
3625
|
+
readonly aliases: readonly ["list"];
|
|
3626
3626
|
readonly hidden: false;
|
|
3627
3627
|
readonly usage: "[options]";
|
|
3628
3628
|
readonly arguments: readonly [];
|
|
@@ -4674,7 +4674,9 @@ export const COMMAND_CATALOG = [
|
|
|
4674
4674
|
{
|
|
4675
4675
|
"name": "status",
|
|
4676
4676
|
"description": "Show installed MCP packs/servers and their health in BOTH Claude and Codex (PARTIAL/DRIFT flagged; secrets redacted)",
|
|
4677
|
-
"aliases": [
|
|
4677
|
+
"aliases": [
|
|
4678
|
+
"list"
|
|
4679
|
+
],
|
|
4678
4680
|
"hidden": false,
|
|
4679
4681
|
"usage": "[options]",
|
|
4680
4682
|
"arguments": [],
|
|
@@ -85,6 +85,7 @@ export const REGISTRATION_PLAN = [
|
|
|
85
85
|
];
|
|
86
86
|
async function executePlan(program, steps) {
|
|
87
87
|
const parents = new Map([["program", program]]);
|
|
88
|
+
let filesRuntimeConfigured = false;
|
|
88
89
|
for (const step of steps) {
|
|
89
90
|
if (step.type === "group") {
|
|
90
91
|
const group = program.command(step.name, step.hidden ? { hidden: true } : {});
|
|
@@ -94,6 +95,13 @@ async function executePlan(program, steps) {
|
|
|
94
95
|
const parent = parents.get(step.parent);
|
|
95
96
|
if (!parent)
|
|
96
97
|
throw new Error(`Missing command parent '${step.parent}' for '${step.exportName}'`);
|
|
98
|
+
if (step.root === "files" && !filesRuntimeConfigured) {
|
|
99
|
+
// The SDK emits its Node support notice during module evaluation. Set
|
|
100
|
+
// its narrow suppression switch before loading any files command module.
|
|
101
|
+
const { configureAwsSdkNodeVersionWarningOutput } = await import("./lib/aws-sdk-node-warning-output.js");
|
|
102
|
+
configureAwsSdkNodeVersionWarningOutput();
|
|
103
|
+
filesRuntimeConfigured = true;
|
|
104
|
+
}
|
|
97
105
|
const imported = await import(step.module);
|
|
98
106
|
const register = imported[step.exportName];
|
|
99
107
|
if (!register)
|
package/dist/commands/lanes.js
CHANGED
|
@@ -5065,6 +5065,19 @@ async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
|
|
|
5065
5065
|
return syncLaneRecordFromOwner(ctx.hqRoot(), laneId, outpostId, ownerLane, undefined, { allowNonTerminal: true });
|
|
5066
5066
|
}
|
|
5067
5067
|
catch (err) {
|
|
5068
|
+
if (isLanesError(err) && err.error === "outpost_lane_sync_conflict") {
|
|
5069
|
+
const ownerVersion = err.details.owner_version;
|
|
5070
|
+
const primaryVersion = err.details.primary_version;
|
|
5071
|
+
if (typeof ownerVersion === "number" &&
|
|
5072
|
+
typeof primaryVersion === "number" &&
|
|
5073
|
+
ownerVersion < primaryVersion) {
|
|
5074
|
+
const currentPrimary = readLane(ctx.hqRoot(), laneId);
|
|
5075
|
+
if (currentPrimary.version === primaryVersion &&
|
|
5076
|
+
isTerminalLaneState(currentPrimary.state)) {
|
|
5077
|
+
return currentPrimary;
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5068
5081
|
const detail = isLanesError(err)
|
|
5069
5082
|
? `${err.error}: ${err.message}`
|
|
5070
5083
|
: `outpost_lane_sync_unavailable: ${dispatchErrorMessage(err)}`;
|
|
@@ -374,6 +374,7 @@ export function registerMcpCommand(program) {
|
|
|
374
374
|
.description('MCP pack observability (read-only) across Claude + Codex');
|
|
375
375
|
mcp
|
|
376
376
|
.command('status')
|
|
377
|
+
.alias('list')
|
|
377
378
|
.description('Show installed MCP packs/servers and their health in BOTH Claude and Codex (PARTIAL/DRIFT flagged; secrets redacted)')
|
|
378
379
|
.option('--json', 'Emit the structured report as JSON (for CI / scripting)')
|
|
379
380
|
.action((opts) => {
|
package/dist/commands/members.js
CHANGED
|
@@ -672,7 +672,6 @@ export function registerMembersCommand(program) {
|
|
|
672
672
|
const name = m.personName ?? m.personSlug ?? "";
|
|
673
673
|
console.log([email.padEnd(emailW), m.role.padEnd(roleW), name].join(" "));
|
|
674
674
|
}
|
|
675
|
-
console.log(chalk.gray("Share a secret with a member: hq secrets share <path> --with <email>"));
|
|
676
675
|
}
|
|
677
676
|
catch (err) {
|
|
678
677
|
if (err instanceof InviteHttpError) {
|
|
@@ -17,6 +17,9 @@ import { resolveNameToEmail, type PersonRecord } from "../utils/people.js";
|
|
|
17
17
|
export type FetchPeopleRoster = (hqRoot: string, companySlug: string) => Promise<PersonRecord[]>;
|
|
18
18
|
interface PeopleCommandDeps {
|
|
19
19
|
fetchRoster?: FetchPeopleRoster;
|
|
20
|
+
/** Test seam for the shared active-membership ambiguity resolver. */
|
|
21
|
+
resolveMissingManifestMemberships?: () => Promise<void>;
|
|
22
|
+
companySelectionGuidanceEnabled?: () => boolean;
|
|
20
23
|
}
|
|
21
24
|
interface PeopleLookupOpts {
|
|
22
25
|
localOnly?: boolean;
|
package/dist/commands/people.js
CHANGED
|
@@ -19,6 +19,7 @@ import { findHqRoot } from "../utils/manifest.js";
|
|
|
19
19
|
import { manifestPath } from "./cloud-provision.js";
|
|
20
20
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
21
21
|
import { getCompanyUid } from "../utils/vault-api.js";
|
|
22
|
+
import { companyErrorsSdkWarningEnabled } from "../lib/company-errors-sdk-warning-flag.js";
|
|
22
23
|
import { listActiveMembers } from "./members.js";
|
|
23
24
|
import { assertSafeCompanySlug, listCompanyPeople, searchPeople, resolveNameToEmail, companyPeopleDir, } from "../utils/people.js";
|
|
24
25
|
function slugify(value) {
|
|
@@ -108,6 +109,29 @@ export function resolveCompanySlug(hqRoot, explicit) {
|
|
|
108
109
|
throw new Error("Multiple companies found — re-run with --company <slug> to pick one:\n" +
|
|
109
110
|
slugs.map((s) => ` --company ${s}`).join("\n"));
|
|
110
111
|
}
|
|
112
|
+
async function resolvePeopleCompanySlug(hqRoot, explicit, localOnly, deps) {
|
|
113
|
+
try {
|
|
114
|
+
return resolveCompanySlug(hqRoot, explicit);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
const ambiguous = message.includes("companies/manifest.yaml not found") ||
|
|
119
|
+
message.startsWith("Multiple companies found");
|
|
120
|
+
const enabled = (deps.companySelectionGuidanceEnabled ?? companyErrorsSdkWarningEnabled)();
|
|
121
|
+
if (!explicit && !localOnly && enabled && ambiguous) {
|
|
122
|
+
const resolveMemberships = deps.resolveMissingManifestMemberships ??
|
|
123
|
+
(async () => {
|
|
124
|
+
const token = await ensureCognitoToken();
|
|
125
|
+
await getCompanyUid(token, undefined);
|
|
126
|
+
});
|
|
127
|
+
// The shared resolver throws members-style guidance for multiple active
|
|
128
|
+
// companies. A single membership cannot provide a local slug without a
|
|
129
|
+
// manifest, so preserve the existing missing-manifest error in that case.
|
|
130
|
+
await resolveMemberships();
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
111
135
|
/** Resolve the HQ root: explicit override (tests) → cwd walk-up. */
|
|
112
136
|
function resolveHqRoot(opts) {
|
|
113
137
|
return opts.hqRoot ?? findHqRoot();
|
|
@@ -192,7 +216,7 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
192
216
|
try {
|
|
193
217
|
const scope = people.opts();
|
|
194
218
|
const hqRoot = resolveHqRoot(scope);
|
|
195
|
-
const slug =
|
|
219
|
+
const slug = await resolvePeopleCompanySlug(hqRoot, scope.company, opts.localOnly, deps);
|
|
196
220
|
let records = listCompanyPeople(hqRoot, slug);
|
|
197
221
|
if (!opts.localOnly) {
|
|
198
222
|
const roster = await tryFetchRoster(deps.fetchRoster ?? fetchMembershipRoster, hqRoot, slug);
|
|
@@ -223,7 +247,7 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
223
247
|
try {
|
|
224
248
|
const scope = people.opts();
|
|
225
249
|
const hqRoot = resolveHqRoot(scope);
|
|
226
|
-
const slug =
|
|
250
|
+
const slug = await resolvePeopleCompanySlug(hqRoot, scope.company, opts.localOnly, deps);
|
|
227
251
|
const matches = await searchPeopleWithRosterFallback({
|
|
228
252
|
hqRoot,
|
|
229
253
|
slug,
|
|
@@ -255,7 +279,7 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
255
279
|
try {
|
|
256
280
|
const scope = people.opts();
|
|
257
281
|
const hqRoot = resolveHqRoot(scope);
|
|
258
|
-
const slug =
|
|
282
|
+
const slug = await resolvePeopleCompanySlug(hqRoot, scope.company, opts.localOnly, deps);
|
|
259
283
|
const result = await resolvePersonWithRosterFallback({
|
|
260
284
|
hqRoot,
|
|
261
285
|
slug,
|
package/dist/index.js
CHANGED
|
@@ -22,9 +22,14 @@ if (isVersionRequest(process.argv)) {
|
|
|
22
22
|
// compare). `hq version` and `--json` print the labelled HQ/CLI report.
|
|
23
23
|
// Lazy so this path still skips the command graph.
|
|
24
24
|
if (wantsLabelledVersionReport(process.argv)) {
|
|
25
|
-
void import("./
|
|
25
|
+
void import("./lib/flag-registry.js").then(({ kickFlagRegistryReadiness }) => {
|
|
26
|
+
kickFlagRegistryReadiness();
|
|
27
|
+
return import("./utils/version-report.js").then(({ runVersionCommand }) => runVersionCommand(process.argv));
|
|
28
|
+
});
|
|
26
29
|
}
|
|
27
30
|
else {
|
|
31
|
+
// Keep the benchmarked updater fast route limited to the installed semver.
|
|
32
|
+
// Registry checks and flag resolution belong to the regular command path.
|
|
28
33
|
void import("./cli-version.js").then(({ CLI_VERSION }) => {
|
|
29
34
|
process.stdout.write(`${CLI_VERSION}\n`);
|
|
30
35
|
});
|
package/dist/lazy-commands.js
CHANGED
|
@@ -55,8 +55,11 @@ function commandForToken(token) {
|
|
|
55
55
|
* preserve the complete help and unknown-command recovery surfaces.
|
|
56
56
|
*/
|
|
57
57
|
export function findLazyCommand(argv) {
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
let commandIndex = 2;
|
|
59
|
+
while (argv[commandIndex] === "--verbose")
|
|
60
|
+
commandIndex += 1;
|
|
61
|
+
const first = argv[commandIndex];
|
|
62
|
+
const token = first === "help" ? argv[commandIndex + 1] : first;
|
|
60
63
|
if (typeof token !== "string" || token.startsWith("-"))
|
|
61
64
|
return null;
|
|
62
65
|
const command = commandForToken(token);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const AWS_SDK_NODE_VERSION_WARNING_ENV = "AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED";
|
|
2
|
+
export interface AwsSdkWarningOutputOptions {
|
|
3
|
+
argv?: readonly string[];
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Configure the AWS SDK's documented, warning-specific switch before loading
|
|
9
|
+
* the files command and its SDK clients. No other SDK warnings are affected.
|
|
10
|
+
*/
|
|
11
|
+
export declare function configureAwsSdkNodeVersionWarningOutput(options?: AwsSdkWarningOutputOptions): void;
|
|
12
|
+
//# sourceMappingURL=aws-sdk-node-warning-output.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { companyErrorsSdkWarningEnabled } from "./company-errors-sdk-warning-flag.js";
|
|
2
|
+
export const AWS_SDK_NODE_VERSION_WARNING_ENV = "AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED";
|
|
3
|
+
/**
|
|
4
|
+
* Configure the AWS SDK's documented, warning-specific switch before loading
|
|
5
|
+
* the files command and its SDK clients. No other SDK warnings are affected.
|
|
6
|
+
*/
|
|
7
|
+
export function configureAwsSdkNodeVersionWarningOutput(options = {}) {
|
|
8
|
+
const env = options.env ?? process.env;
|
|
9
|
+
const argv = options.argv ?? process.argv;
|
|
10
|
+
const enabled = options.enabled ?? companyErrorsSdkWarningEnabled();
|
|
11
|
+
if (!enabled)
|
|
12
|
+
return;
|
|
13
|
+
const verbose = argv.includes("--verbose");
|
|
14
|
+
const debug = /^(1|true|yes)$/i.test(env.HQ_DEBUG?.trim() ?? "");
|
|
15
|
+
if (verbose || debug) {
|
|
16
|
+
delete env[AWS_SDK_NODE_VERSION_WARNING_ENV];
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
env[AWS_SDK_NODE_VERSION_WARNING_ENV] = "true";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=aws-sdk-node-warning-output.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const COMPANY_ERRORS_SDK_WARNING_FLAG = "cli.company-errors-sdk-warning";
|
|
2
|
+
export declare const COMPANY_ERRORS_SDK_WARNING_FALLBACK = false;
|
|
3
|
+
/** One default-off rollout gate for the two US-163 CLI compatibility fixes. */
|
|
4
|
+
export declare function companyErrorsSdkWarningEnabled(): boolean;
|
|
5
|
+
//# sourceMappingURL=company-errors-sdk-warning-flag.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { resolveProcessFlagGate } from "./flag-registry.js";
|
|
2
|
+
export const COMPANY_ERRORS_SDK_WARNING_FLAG = "cli.company-errors-sdk-warning";
|
|
3
|
+
export const COMPANY_ERRORS_SDK_WARNING_FALLBACK = false;
|
|
4
|
+
/** One default-off rollout gate for the two US-163 CLI compatibility fixes. */
|
|
5
|
+
export function companyErrorsSdkWarningEnabled() {
|
|
6
|
+
return resolveProcessFlagGate(COMPANY_ERRORS_SDK_WARNING_FLAG, {}, () => COMPANY_ERRORS_SDK_WARNING_FALLBACK);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=company-errors-sdk-warning-flag.js.map
|
|
@@ -10,7 +10,7 @@ import { readDeviceConfig, } from "../../work-context/config.js";
|
|
|
10
10
|
import { outboxStats, listOutboxOperations } from "../../work-context/outbox.js";
|
|
11
11
|
import { outboxQuarantineStatusEnabled } from "../../work-context/outbox-quarantine-status-flag.js";
|
|
12
12
|
import { workContextRoot, workContextSessionsDir } from "../../work-context/paths.js";
|
|
13
|
-
import { countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
|
|
13
|
+
import { countDistinctHeldEvents, countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
|
|
14
14
|
import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../../mesh/live/paths.js";
|
|
15
15
|
import { collectDaemonDoctor, formatDaemonDoctor, } from "../../mesh/live/daemon/doctor.js";
|
|
16
16
|
export const WORK_CONTEXT_FAMILY_ID = "work-context";
|
|
@@ -139,7 +139,7 @@ function runChecks(deps) {
|
|
|
139
139
|
const heldPath = workMeshHeldPath(meshRoot);
|
|
140
140
|
const deadLetterPath = workMeshDeadLetterPath(meshRoot);
|
|
141
141
|
const spoolDepth = countJsonlLines(spoolPath);
|
|
142
|
-
const heldDepth =
|
|
142
|
+
const heldDepth = countDistinctHeldEvents(heldPath);
|
|
143
143
|
results.push({
|
|
144
144
|
status: "PASS",
|
|
145
145
|
checkId: "work-context.spool-depth",
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
4
|
import { dropboxInboxHasMessages } from "./dropbox.js";
|
|
5
5
|
import { appendCappedHistory, listQuestionIds, readQuestion, retryUpdateLane } from "./store.js";
|
|
6
|
-
import { isIsoDateTime } from "./types.js";
|
|
6
|
+
import { isTerminalLaneState, isIsoDateTime, } from "./types.js";
|
|
7
7
|
export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
8
8
|
export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
9
9
|
export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG = "lanes.keep-alive-next-check-at";
|
|
@@ -254,7 +254,10 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
|
|
|
254
254
|
return lanes.find((candidate) => {
|
|
255
255
|
if (candidate.lane_id === lane.lane_id || Date.parse(candidate.created_at) <= createdAt)
|
|
256
256
|
return false;
|
|
257
|
-
if (candidate.state === "cancelled" ||
|
|
257
|
+
if (candidate.state === "cancelled" ||
|
|
258
|
+
candidate.state === "killed" ||
|
|
259
|
+
candidate.state === "failed" ||
|
|
260
|
+
(lane.state === "running" && isTerminalLaneState(candidate.state)))
|
|
258
261
|
return false;
|
|
259
262
|
const candidateCompany = companyOf(candidate) ?? null;
|
|
260
263
|
const sameCompany = laneCompany === null
|
|
@@ -270,8 +273,16 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
|
|
|
270
273
|
const sameLaneSenior = (lane.senior.kind === "lane" || lane.senior.kind === "session") &&
|
|
271
274
|
candidate.senior.kind === lane.senior.kind &&
|
|
272
275
|
candidate.senior.id === lane.senior.id;
|
|
276
|
+
const differentKnownSenior = lane.senior.kind === "lane" &&
|
|
277
|
+
candidate.senior.kind === "lane" &&
|
|
278
|
+
typeof lane.senior.id === "string" &&
|
|
279
|
+
lane.senior.id.trim().length > 0 &&
|
|
280
|
+
typeof candidate.senior.id === "string" &&
|
|
281
|
+
candidate.senior.id.trim().length > 0 &&
|
|
282
|
+
candidate.senior.id !== lane.senior.id;
|
|
273
283
|
const sameProfile = lane.worker_profile !== undefined &&
|
|
274
284
|
candidate.worker_profile === lane.worker_profile &&
|
|
285
|
+
!(lane.state === "running" && differentKnownSenior) &&
|
|
275
286
|
!(lineageAware && sameLaneSenior);
|
|
276
287
|
const projectLead = lane.project_id !== undefined &&
|
|
277
288
|
candidate.project_id === lane.project_id &&
|
|
@@ -11,7 +11,7 @@ import { readSessionState } from "../../../work-context/state.js";
|
|
|
11
11
|
import { workContextRoot } from "../../../work-context/paths.js";
|
|
12
12
|
import { outboxQuarantineStatusEnabled } from "../../../work-context/outbox-quarantine-status-flag.js";
|
|
13
13
|
import { formatDroppedPresenceCompany } from "./credentials.js";
|
|
14
|
-
import { countJsonlLines, countUnattributedEvents, } from "../spool.js";
|
|
14
|
+
import { countDistinctHeldEvents, countJsonlLines, countUnattributedEvents, } from "../spool.js";
|
|
15
15
|
import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../paths.js";
|
|
16
16
|
import { daemonDir, daemonPidPath } from "./paths.js";
|
|
17
17
|
import { readEmitState } from "../emit.js";
|
|
@@ -178,7 +178,7 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
178
178
|
const heldPath = workMeshHeldPath(meshRoot);
|
|
179
179
|
const deadPath = workMeshDeadLetterPath(meshRoot);
|
|
180
180
|
const spoolDepth = countJsonlLines(spoolPath);
|
|
181
|
-
const heldCount =
|
|
181
|
+
const heldCount = countDistinctHeldEvents(heldPath);
|
|
182
182
|
const trustedRoots = readDaemonConfig(dir).trustedHqRoots;
|
|
183
183
|
const outboxOperations = listOutboxOperations(ctxRoot);
|
|
184
184
|
const heldWithNoLocalEvidence = readHeldSessions(meshRoot).filter((ref) => {
|
|
@@ -14,7 +14,7 @@ import { type SessionEventsPoster } from "./session-events-client.js";
|
|
|
14
14
|
export declare const HELD_TTL_MS: number;
|
|
15
15
|
/** Re-attempt held events at most this often (spool still flushes every cycle). */
|
|
16
16
|
export declare const HELD_RETRY_INTERVAL_MS: number;
|
|
17
|
-
/** Cap held
|
|
17
|
+
/** Cap distinct held events; oldest overflow is dead-lettered with HELD_OVERFLOW. */
|
|
18
18
|
export declare const HELD_MAX_LINES = 20000;
|
|
19
19
|
/**
|
|
20
20
|
* Re-check an unbound session against the project catalog at most this often.
|
|
@@ -46,7 +46,7 @@ export interface FlushDeps {
|
|
|
46
46
|
lastHeldRetryAtMs?: number;
|
|
47
47
|
/** Invoked when this flush claims held, so the daemon can advance its timestamp. */
|
|
48
48
|
onHeldClaimed?: (atMs: number) => void;
|
|
49
|
-
/** Max
|
|
49
|
+
/** Max distinct eventIds retained in held.jsonl after a flush. Default HELD_MAX_LINES. */
|
|
50
50
|
heldMaxLines?: number;
|
|
51
51
|
/** Optional logger (daemon wires this); used once per overflow trim. */
|
|
52
52
|
log?: (message: string) => void;
|
|
@@ -22,7 +22,7 @@ import { defaultSleep, FLUSH_MAX_ATTEMPTS, fullJitterDelayMs, } from "./backoff.
|
|
|
22
22
|
import { LOCAL_ONLY_FIELDS, stripLocalOnlyFields } from "./format-spool-line.js";
|
|
23
23
|
import { enrichSessionEndEvent } from "./report-summary.js";
|
|
24
24
|
import { SESSION_EVENTS_BATCH_MAX, WRITABLE_CONTEXT_STATUSES, parseSessionEventsBatchAck, } from "./session-events-client.js";
|
|
25
|
-
import { appendDeadLetterLine, appendDroppedReceipt,
|
|
25
|
+
import { appendDeadLetterLine, appendDroppedReceipt, appendHeldLinesIfAbsent, appendSpoolLine, claimHeld, claimSpool, countDistinctHeldEvents, countJsonlLines, recoverOrphanClaims, removeClaimFile, } from "./spool.js";
|
|
26
26
|
import { workMeshHeldPath, workMeshRoot } from "./paths.js";
|
|
27
27
|
/** Bound a replayed session backlog so one flush cannot burst hundreds of events. */
|
|
28
28
|
const SESSION_EVENTS_MAX_PER_SESSION_PER_FLUSH = 25;
|
|
@@ -31,7 +31,7 @@ const MAX_RETRY_AFTER_MS = 5 * 60 * 1000;
|
|
|
31
31
|
export const HELD_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
32
32
|
/** Re-attempt held events at most this often (spool still flushes every cycle). */
|
|
33
33
|
export const HELD_RETRY_INTERVAL_MS = 5 * 60 * 1000;
|
|
34
|
-
/** Cap held
|
|
34
|
+
/** Cap distinct held events; oldest overflow is dead-lettered with HELD_OVERFLOW. */
|
|
35
35
|
export const HELD_MAX_LINES = 20_000;
|
|
36
36
|
/**
|
|
37
37
|
* Re-check an unbound session against the project catalog at most this often.
|
|
@@ -279,11 +279,9 @@ export async function flushSessionEvents(deps) {
|
|
|
279
279
|
state = readSessionState(sessionId, deps.workContextRoot) ?? state;
|
|
280
280
|
if (!allowed) {
|
|
281
281
|
const heldAt = now().toISOString();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
summary.held += 1;
|
|
282
|
+
summary.held += appendHeldLinesIfAbsent(remainingItems.map((item) => heldEnvelope(item.event, "NEEDS_COMPANY", item.heldAt ?? heldAt)), deps.workMeshRoot);
|
|
283
|
+
for (const item of remainingItems)
|
|
285
284
|
markDisposed(item.event);
|
|
286
|
-
}
|
|
287
285
|
continue;
|
|
288
286
|
}
|
|
289
287
|
}
|
|
@@ -306,11 +304,9 @@ export async function flushSessionEvents(deps) {
|
|
|
306
304
|
}
|
|
307
305
|
if (decision.action === "hold") {
|
|
308
306
|
const heldAt = now().toISOString();
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
summary.held += 1;
|
|
307
|
+
summary.held += appendHeldLinesIfAbsent(remainingItems.map((item) => heldEnvelope(item.event, decision.heldReason, item.heldAt ?? heldAt)), deps.workMeshRoot);
|
|
308
|
+
for (const item of remainingItems)
|
|
312
309
|
markDisposed(item.event);
|
|
313
|
-
}
|
|
314
310
|
continue;
|
|
315
311
|
}
|
|
316
312
|
// Post a bounded portion of this session's remaining backlog (after any
|
|
@@ -368,11 +364,9 @@ export async function flushSessionEvents(deps) {
|
|
|
368
364
|
deps.log?.(`work-mesh: session ${sessionId} project missing (HTTP 404); downgraded to needs_project and held events`);
|
|
369
365
|
}
|
|
370
366
|
const heldAt = now().toISOString();
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
summary.held += 1;
|
|
367
|
+
summary.held += appendHeldLinesIfAbsent(originalChunk.map((item) => heldEnvelope(item.event, "NEEDS_PROJECT", item.heldAt ?? heldAt)), deps.workMeshRoot);
|
|
368
|
+
for (const item of originalChunk)
|
|
374
369
|
markDisposed(item.event);
|
|
375
|
-
}
|
|
376
370
|
continue;
|
|
377
371
|
}
|
|
378
372
|
if (!result.retryable) {
|
|
@@ -429,15 +423,17 @@ export async function flushSessionEvents(deps) {
|
|
|
429
423
|
* flusher (`hq mesh flush` beside the daemon) may have appended to a fresh
|
|
430
424
|
* held.jsonl after our claim renamed the old one away.
|
|
431
425
|
* - The overflow claim file is removed only after every dead letter and every
|
|
432
|
-
* retained line is on disk.
|
|
433
|
-
*
|
|
434
|
-
*
|
|
426
|
+
* retained line is on disk. Retained rows are idempotent by eventId, so a
|
|
427
|
+
* retry after an interrupted append is safe; the claim stays until recovery
|
|
428
|
+
* can finish, preserving the append-before-remove ordering.
|
|
435
429
|
*/
|
|
436
430
|
function trimHeldOverflow(meshRoot, maxLines, now, summary) {
|
|
437
431
|
if (maxLines <= 0)
|
|
438
432
|
return 0;
|
|
439
433
|
const heldPath = workMeshHeldPath(meshRoot);
|
|
440
|
-
|
|
434
|
+
const lineCount = countJsonlLines(heldPath);
|
|
435
|
+
const distinctCount = countDistinctHeldEvents(heldPath);
|
|
436
|
+
if (lineCount <= maxLines && lineCount === distinctCount)
|
|
441
437
|
return 0;
|
|
442
438
|
const claim = claimHeld(meshRoot, `${now().getTime()}.overflow`);
|
|
443
439
|
if (!claim.claimPath)
|
|
@@ -447,15 +443,26 @@ function trimHeldOverflow(meshRoot, maxLines, now, summary) {
|
|
|
447
443
|
return 0;
|
|
448
444
|
}
|
|
449
445
|
try {
|
|
450
|
-
|
|
451
|
-
|
|
446
|
+
const alreadyHeldIds = readHeldEventIds(heldPath);
|
|
447
|
+
const seenClaimIds = new Set();
|
|
448
|
+
const uniqueLines = claim.lines.filter((line) => {
|
|
449
|
+
const eventId = heldEventIdOfLine(line);
|
|
450
|
+
if (!eventId)
|
|
451
|
+
return true;
|
|
452
|
+
if (alreadyHeldIds.has(eventId) || seenClaimIds.has(eventId))
|
|
453
|
+
return false;
|
|
454
|
+
seenClaimIds.add(eventId);
|
|
455
|
+
return true;
|
|
456
|
+
});
|
|
457
|
+
if (uniqueLines.length <= maxLines) {
|
|
458
|
+
appendHeldLinesOnce(meshRoot, uniqueLines);
|
|
452
459
|
removeClaimFile(claim.claimPath);
|
|
453
460
|
return 0;
|
|
454
461
|
}
|
|
455
|
-
const overflow =
|
|
462
|
+
const overflow = uniqueLines.length - maxLines;
|
|
456
463
|
// Oldest by heldAt: retries regroup held by session, so file order is not
|
|
457
464
|
// chronological. Unparseable lines sort first (dead-lettered first).
|
|
458
|
-
const ordered =
|
|
465
|
+
const ordered = uniqueLines
|
|
459
466
|
.map((line, idx) => ({ line, idx, atMs: heldAtMs(line) }))
|
|
460
467
|
.sort((a, b) => a.atMs - b.atMs || a.idx - b.idx);
|
|
461
468
|
const toDead = ordered.slice(0, overflow);
|
|
@@ -475,7 +482,7 @@ function trimHeldOverflow(meshRoot, maxLines, now, summary) {
|
|
|
475
482
|
}
|
|
476
483
|
summary.deadLettered += 1;
|
|
477
484
|
}
|
|
478
|
-
|
|
485
|
+
appendHeldLinesOnce(meshRoot, toKeep);
|
|
479
486
|
summary.heldOverflow = overflow;
|
|
480
487
|
removeClaimFile(claim.claimPath);
|
|
481
488
|
return overflow;
|
|
@@ -496,12 +503,25 @@ function heldAtMs(line) {
|
|
|
496
503
|
const ms = Date.parse(heldAt);
|
|
497
504
|
return Number.isFinite(ms) ? ms : 0;
|
|
498
505
|
}
|
|
499
|
-
function
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
506
|
+
function appendHeldLinesOnce(meshRoot, lines) {
|
|
507
|
+
appendHeldLinesIfAbsent(lines, meshRoot);
|
|
508
|
+
}
|
|
509
|
+
function heldEventIdOfLine(line) {
|
|
510
|
+
const parsed = parseLine(line);
|
|
511
|
+
return parsed ? eventIdOf(unwrapHeld(parsed).event) : null;
|
|
512
|
+
}
|
|
513
|
+
function readHeldEventIds(filePath) {
|
|
514
|
+
if (!fs.existsSync(filePath))
|
|
515
|
+
return new Set();
|
|
516
|
+
const ids = new Set();
|
|
517
|
+
for (const line of fs.readFileSync(filePath, "utf8").split("\n")) {
|
|
518
|
+
if (!line.trim())
|
|
519
|
+
continue;
|
|
520
|
+
const id = heldEventIdOfLine(line);
|
|
521
|
+
if (id)
|
|
522
|
+
ids.add(id);
|
|
523
|
+
}
|
|
524
|
+
return ids;
|
|
505
525
|
}
|
|
506
526
|
/**
|
|
507
527
|
* Account a 2xx batch: dead-letter rejected eventIds with their codes;
|
|
@@ -19,6 +19,10 @@ export declare function ensureSpoolFile(filePath: string): void;
|
|
|
19
19
|
export declare function appendJsonlLine(filePath: string, line: string): void;
|
|
20
20
|
export declare function appendSpoolLine(line: string, root?: string): string;
|
|
21
21
|
export declare function appendHeldLine(line: string, root?: string): string;
|
|
22
|
+
/** Append a held envelope only when its eventId is not already held. */
|
|
23
|
+
export declare function appendHeldLineIfAbsent(line: string, root?: string): boolean;
|
|
24
|
+
/** Append a deduplicated held batch under one file lock and one write. */
|
|
25
|
+
export declare function appendHeldLinesIfAbsent(lines: string[], root?: string): number;
|
|
22
26
|
export declare function appendDeadLetterLine(line: string, root?: string): string;
|
|
23
27
|
export declare function appendDroppedReceipt(receipt: Record<string, unknown>, root?: string): string;
|
|
24
28
|
export interface ClaimResult {
|
|
@@ -49,6 +53,8 @@ export declare function recoverOrphanClaims(root: string): {
|
|
|
49
53
|
export declare function removeClaimFile(claimPath: string | null): void;
|
|
50
54
|
export declare function deadLetterNonEmpty(root?: string): boolean;
|
|
51
55
|
export declare function countJsonlLines(filePath: string): number;
|
|
56
|
+
/** Count held eventIds once; legacy rows without an eventId remain line-counted. */
|
|
57
|
+
export declare function countDistinctHeldEvents(filePath: string): number;
|
|
52
58
|
/** Local queues that still lack a usable company attribution. */
|
|
53
59
|
export interface UnattributedEventCounts {
|
|
54
60
|
spool: number;
|