@indigoai-us/hq-cli 5.345.166 → 5.345.169
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 +13 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +6 -2
- package/dist/command-catalog.generated.js +7 -2
- package/dist/commands/access.js +2 -0
- package/dist/commands/agents.js +2 -2
- package/dist/commands/core-checkpoint-native.js +30 -10
- package/dist/commands/core-delegate-w1b.js +2 -1
- package/dist/commands/core-dm-bind-native.js +3 -1
- package/dist/commands/dm.d.ts +13 -2
- package/dist/commands/dm.js +42 -9
- package/dist/commands/feedback.js +11 -3
- package/dist/lib/agent-kit/mcp/tools.js +5 -6
- package/dist/lib/doctor/checks/sync-health.d.ts +7 -1
- package/dist/lib/doctor/checks/sync-health.js +67 -2
- package/dist/lib/doctor/fix/apply.d.ts +40 -4
- package/dist/lib/doctor/fix/apply.js +183 -69
- package/dist/lib/doctor/fix/process-identity.d.ts +13 -0
- package/dist/lib/doctor/fix/process-identity.js +34 -0
- package/dist/lib/doctor/fix/refresh-sync-handoff.d.ts +201 -0
- package/dist/lib/doctor/fix/refresh-sync-handoff.js +752 -0
- package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +1 -1
- package/dist/lib/doctor/fix/refresh-sync-outcome.js +13 -0
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +2 -0
- package/dist/lib/notify-recipient.d.ts +15 -0
- package/dist/lib/notify-recipient.js +15 -0
- package/dist/lib/secret-scan/repo-config.d.ts +8 -2
- package/dist/lib/secret-scan/repo-config.js +12 -6
- package/dist/lib/secret-scan/staged.d.ts +6 -0
- package/dist/lib/secret-scan/staged.js +21 -3
- package/dist/sentry-before-send.js +13 -0
- package/dist/utils/breadcrumb-buffer.js +15 -4
- package/dist/utils/feedback-diagnostics.js +5 -2
- package/dist/utils/feedback-logs.d.ts +7 -0
- package/dist/utils/feedback-logs.js +37 -3
- package/dist/utils/scrub-diagnostics.d.ts +4 -0
- package/dist/utils/scrub-diagnostics.js +95 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.169] — 2026-10-24
|
|
6
|
+
- Sentry error reports now redact CloudFront signed-URL params (`Policy`, `Key-Pair-Id`) and the `AWSAccessKeyId` and `GoogleAccessId` ids when they appear as structured keys in event data, for example a parsed query under `extra`. The feedback diagnostics scrubber already redacted them (#2177); the Sentry scrubber caught them only inside strings. The shared structured-key cases now run against the Sentry scrubber too.
|
|
7
|
+
- A debounced automatic checkpoint is now queued for the next checkpoint sibling. Before this, `hq core checkpoint` wrote the `-auto-` thread when the per-session debounce skipped the sibling, but it never queued the payload, so no sibling ever filled the thread in. The payload is now added to the pending queue once, and the next sibling run picks it up. The debounce still limits how many siblings start. When the pending queue goes over its cap of 50, a message on stderr now gives the number of oldest payloads dropped. Feedback a2a5c2c3-5ea0-4e1f-8155-1f39ce6ac750.
|
|
8
|
+
|
|
9
|
+
- `hq feedback` now redacts presigned URL signatures, session tokens, bearer tokens and other credentials from the diagnostics it attaches. Before this fix, recent Sentry breadcrumbs were copied into diagnostics without scrubbing, so an S3 presigned URL's X-Amz-Security-Token could reach the stored feedback row. Diagnostics passed in by callers are scrubbed too, and the inline log tails and the S3 log bundle now also catch bare, URL-encoded, CloudFront, GCS and Azure signed URL parameters.
|
|
10
|
+
- `hq doctor --fix` no longer starts a second background sync pull while a pull it stopped at its time limit is still shutting down. It takes the pull-start lock from another process only when that process has exited or its process id now belongs to a different process.
|
|
11
|
+
- `hq doctor --fix` can now refresh sync journals on drives without hard links (exFAT, FAT, some network drives). `hq doctor` warns, naming the process, when a journal refresh lock has been held for more than 10 minutes.
|
|
12
|
+
- `hq doctor --fix` no longer kills a slow sync journal pull after 90 seconds. A pull that is still running keeps going in the background for up to 10 minutes, one at a time, and `hq doctor` shows that journal as "refresh in progress" until it finishes. It never blocks your own `hq sync`, and it steps aside at once for `hq rescue` and other repairs, keeping what it already downloaded. A large company is no longer reported as stale every day.
|
|
13
|
+
|
|
14
|
+
## [5.345.168] — 2026-10-24
|
|
15
|
+
- `hq dm` now picks the default audience from the recipient when no audience flag is passed. A 1:1 DM to a person (email, `prs_` uid, or a bare name that resolves to a person) sends `audience: "human"`. A 1:1 DM to an agent (`agt_` uid), a group DM and a channel post send `audience: "agent"`, so script and lane messages stay out of people's conversations. Scheduled sends (`--at`, `--in`) follow the same rule. Pass `--for-human` (alias for `--audience human`) to tag any message for people; an explicit `--audience human|agent|both` wins over `--for-human`, with a warning on stderr, and is validated on every path. `hq core delegate send` tags the pickup DM `audience: "both"`, `hq dm-bind post` keeps the channel default (`audience: "agent"`) for its automated status posts, and the agent MCP `hq_dm_send` tool, `hq agents message` and the `hq access` grant request apply the recipient rule. Reading commands (inbox, thread, channel) are unchanged, and `hq dm forward` does not set an audience.
|
|
16
|
+
- `hq scan secrets` no longer exits 2 when a repo's `.gitleaks.toml` allowlist is rejected. In staged and file scans it now prints a fixed warning that quotes no part of the file and scans with no repo allowlist, so a real key is still reported (exit 1) and a commit that replaces a bad allowlist is not blocked. An unknown `--allowlist-rev` still exits 2.
|
|
17
|
+
|
|
5
18
|
## [5.345.166] — 2026-10-24
|
|
6
19
|
- `hq integrations approve` now says why an approved call did not run. When an app refuses it, the CLI prints the app's own reason, such as Slack's refusal to send into a Slack Connect channel. Before this it printed only "failed (rejected)". With an older server it prints the rejection code instead. `hq integrations pending --history` adds a `Why:` line for decided calls that did not run. The approve command printed after a queued call keeps `--company` when you passed it. Before printing, the reason is cleaned of terminal escape codes, credentials and credential-bearing links, and it is capped at 500 characters. Feedback 15e7593c-50e5-4e6a-ae3a-342d618363c0.
|
|
7
20
|
- The bundled sync engine now uses hq-cloud 6.18.101, the current npm `latest`.
|
|
@@ -283,7 +283,7 @@ if ! jq '.status = "sending"' "$MANIFEST" > "$TMP_MANIFEST" || ! mv "$TMP_MANIFE
|
|
|
283
283
|
rm -f "$TMP_MANIFEST"
|
|
284
284
|
die "could not persist sending state; no DM attempted"
|
|
285
285
|
fi
|
|
286
|
-
DM_OUT="$(hq dm "$PRINCIPAL" "$HEADLINE" --prompt-file "$PROMPT" --details-file "$BRIEF")" \
|
|
286
|
+
DM_OUT="$(hq dm "$PRINCIPAL" "$HEADLINE" --audience both --prompt-file "$PROMPT" --details-file "$BRIEF")" \
|
|
287
287
|
|| die "hq dm delivery outcome is uncertain; status is sending, inspect DM history before retrying"
|
|
288
288
|
|
|
289
289
|
EVENT_ID="$(printf '%s' "$DM_OUT" | grep -oE 'eventId[: ]+[A-Za-z0-9_-]+' | head -1 | sed -E 's/eventId[: ]+//' || true)"
|
|
@@ -3987,7 +3987,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3987
3987
|
}];
|
|
3988
3988
|
}, {
|
|
3989
3989
|
readonly name: "dm";
|
|
3990
|
-
readonly description: "Send and read direct messages, and manage connection requests.";
|
|
3990
|
+
readonly description: "Send and read direct messages, and manage connection requests. Messages to a person send to them; messages to an agent, a channel or a group are tagged for agents and folded away from people by default. Pass --for-human or --audience human|agent|both to override.";
|
|
3991
3991
|
readonly aliases: readonly [];
|
|
3992
3992
|
readonly hidden: false;
|
|
3993
3993
|
readonly helpOption: true;
|
|
@@ -4054,7 +4054,11 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4054
4054
|
readonly hidden: false;
|
|
4055
4055
|
}, {
|
|
4056
4056
|
readonly flags: "--audience <human|agent|both>";
|
|
4057
|
-
readonly description: "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both";
|
|
4057
|
+
readonly description: "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both. Default: human for a person, agent for an agent, a channel or a group. Wins over --for-human.";
|
|
4058
|
+
readonly hidden: false;
|
|
4059
|
+
}, {
|
|
4060
|
+
readonly flags: "--for-human";
|
|
4061
|
+
readonly description: "Tag the message for people (alias for --audience human).";
|
|
4058
4062
|
readonly hidden: false;
|
|
4059
4063
|
}, {
|
|
4060
4064
|
readonly flags: "--attach <path>";
|
|
@@ -4827,7 +4827,7 @@ export const COMMAND_CATALOG = [
|
|
|
4827
4827
|
},
|
|
4828
4828
|
{
|
|
4829
4829
|
"name": "dm",
|
|
4830
|
-
"description": "Send and read direct messages, and manage connection requests.",
|
|
4830
|
+
"description": "Send and read direct messages, and manage connection requests. Messages to a person send to them; messages to an agent, a channel or a group are tagged for agents and folded away from people by default. Pass --for-human or --audience human|agent|both to override.",
|
|
4831
4831
|
"aliases": [],
|
|
4832
4832
|
"hidden": false,
|
|
4833
4833
|
"helpOption": true,
|
|
@@ -4908,7 +4908,12 @@ export const COMMAND_CATALOG = [
|
|
|
4908
4908
|
},
|
|
4909
4909
|
{
|
|
4910
4910
|
"flags": "--audience <human|agent|both>",
|
|
4911
|
-
"description": "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both",
|
|
4911
|
+
"description": "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both. Default: human for a person, agent for an agent, a channel or a group. Wins over --for-human.",
|
|
4912
|
+
"hidden": false
|
|
4913
|
+
},
|
|
4914
|
+
{
|
|
4915
|
+
"flags": "--for-human",
|
|
4916
|
+
"description": "Tag the message for people (alias for --audience human).",
|
|
4912
4917
|
"hidden": false
|
|
4913
4918
|
},
|
|
4914
4919
|
{
|
package/dist/commands/access.js
CHANGED
|
@@ -25,6 +25,7 @@ import { listJournals, syncDoctor, } from "@indigoai-us/hq-cloud";
|
|
|
25
25
|
import { listActiveMembers } from "./members.js";
|
|
26
26
|
import { aclLockedBy } from "./acl-lock.js";
|
|
27
27
|
import { buildDmBody } from "./dm.js";
|
|
28
|
+
import { defaultAudienceFor, recipientKindOf } from "../lib/notify-recipient.js";
|
|
28
29
|
import { peekIdToken } from "../utils/id-token.js";
|
|
29
30
|
import { findRecentAccessRequest, formatTimeAgo, recordAccessRequest, } from "../utils/access-requests.js";
|
|
30
31
|
import { recordAccessOutcome } from "../utils/access-outcomes.js";
|
|
@@ -1155,6 +1156,7 @@ async function buildLiveAccessDeps(slug, hqRoot) {
|
|
|
1155
1156
|
prompt: msg.prompt,
|
|
1156
1157
|
details: msg.details,
|
|
1157
1158
|
now: Date.now(),
|
|
1159
|
+
audience: defaultAudienceFor(recipientKindOf(msg.recipient)),
|
|
1158
1160
|
});
|
|
1159
1161
|
const res = await vaultApiFetch({
|
|
1160
1162
|
token,
|
package/dist/commands/agents.js
CHANGED
|
@@ -42,7 +42,7 @@ import { consoleOrigin } from "./integrations-connect.js";
|
|
|
42
42
|
import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSessionTimeoutError, } from "@indigoai-us/hq-cloud/outposts/node";
|
|
43
43
|
import { peekIdToken } from "../utils/id-token.js";
|
|
44
44
|
import { canOfferTeamUpgrade, formatUpgradeRefusal, isPlanGateError, offerUpgradeLink, } from "../utils/plan-gate-error.js";
|
|
45
|
-
import { AGENT_UID_PATTERN, isAgentUid } from "../lib/notify-recipient.js";
|
|
45
|
+
import { AGENT_UID_PATTERN, defaultAudienceFor, isAgentUid } from "../lib/notify-recipient.js";
|
|
46
46
|
import { AGENT_MODEL_PROVIDERS, modelSelectionErrorMessage, readAgentModelCatalog, validateAgentModelSelection, } from "../lib/agents/model-catalog.js";
|
|
47
47
|
import { fetchCompanyPricingPlan, isUsageOnly, usageOnlyBoxSentence, usageOnlyCardRequiredLines, } from "../lib/billing/usage-only.js";
|
|
48
48
|
import { BUNDLED_USAGE_CARD, fetchUsageCard, } from "../lib/billing/usage-card.js";
|
|
@@ -1139,7 +1139,7 @@ export async function sendAgentDm(token, agentUid, message) {
|
|
|
1139
1139
|
token,
|
|
1140
1140
|
path: "/v1/notify/dm",
|
|
1141
1141
|
method: "POST",
|
|
1142
|
-
body: { toPersonUid: agentUid, body: message },
|
|
1142
|
+
body: { toPersonUid: agentUid, body: message, audience: defaultAudienceFor("agent") },
|
|
1143
1143
|
});
|
|
1144
1144
|
}
|
|
1145
1145
|
/** Read the two-way DM conversation with an agent (most recent `limit`). */
|
|
@@ -1206,8 +1206,30 @@ function appendPending(pendingPath, payload) {
|
|
|
1206
1206
|
// runs once the queue is already over cap, where newest-wins is the point.
|
|
1207
1207
|
const kept = lines.slice(-MAX_PENDING_PAYLOADS);
|
|
1208
1208
|
fs.writeFileSync(pendingPath, `${kept.join("\n")}\n`);
|
|
1209
|
+
// Say so instead of dropping silently: a wedged sibling shows up here.
|
|
1210
|
+
const dropped = lines.length - kept.length;
|
|
1211
|
+
printError(`checkpoint: pending queue over cap (${MAX_PENDING_PAYLOADS}); dropped ${dropped} oldest payload(s)`);
|
|
1209
1212
|
return kept.length;
|
|
1210
1213
|
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Queue a payload for the next sibling run and record its ontology touches.
|
|
1216
|
+
* Shared by the busy-sibling path and the debounced automatic path, so both
|
|
1217
|
+
* hand their thread to the next run instead of leaving it unfilled.
|
|
1218
|
+
*/
|
|
1219
|
+
async function queuePayload(liveRoot, payload) {
|
|
1220
|
+
const siblingRoot = path.join(liveRoot, "workspace", "checkpoints", "sibling");
|
|
1221
|
+
fs.mkdirSync(siblingRoot, { recursive: true });
|
|
1222
|
+
const depth = appendPending(path.join(siblingRoot, "pending.jsonl"), payload);
|
|
1223
|
+
try {
|
|
1224
|
+
const { recordQueuedTouches } = await import("../lib/checkpoint-ontology/queued-touches.js");
|
|
1225
|
+
recordQueuedTouches(liveRoot, payload.company, payload.files, new Date());
|
|
1226
|
+
}
|
|
1227
|
+
catch {
|
|
1228
|
+
// Queued touches only help a later ontology plan; they must never fail
|
|
1229
|
+
// the checkpoint itself.
|
|
1230
|
+
}
|
|
1231
|
+
return depth;
|
|
1232
|
+
}
|
|
1211
1233
|
/**
|
|
1212
1234
|
* Take ownership of the queued payloads by renaming the queue aside.
|
|
1213
1235
|
*
|
|
@@ -1369,15 +1391,7 @@ async function startSibling(liveRoot, input, threadPath, backend) {
|
|
|
1369
1391
|
const activePid = existingSiblingPid(lockPath, Date.now());
|
|
1370
1392
|
fs.mkdirSync(siblingRoot, { recursive: true });
|
|
1371
1393
|
if (activePid !== null) {
|
|
1372
|
-
const depth =
|
|
1373
|
-
try {
|
|
1374
|
-
const { recordQueuedTouches } = await import("../lib/checkpoint-ontology/queued-touches.js");
|
|
1375
|
-
recordQueuedTouches(liveRoot, payload.company, payload.files, new Date());
|
|
1376
|
-
}
|
|
1377
|
-
catch {
|
|
1378
|
-
// Queued touches only help a later ontology plan; they must never fail
|
|
1379
|
-
// the checkpoint itself.
|
|
1380
|
-
}
|
|
1394
|
+
const depth = await queuePayload(liveRoot, payload);
|
|
1381
1395
|
printResult(`checkpoint: sibling busy — payload queued (${depth} pending)`);
|
|
1382
1396
|
return;
|
|
1383
1397
|
}
|
|
@@ -1579,7 +1593,13 @@ async function runCheckpoint(options, command, group, preResolvedHqRoot) {
|
|
|
1579
1593
|
printResult(`checkpoint: sibling disabled (${SIBLING_SETTING})`);
|
|
1580
1594
|
}
|
|
1581
1595
|
else if (autoDebounced) {
|
|
1582
|
-
|
|
1596
|
+
// feedback_a2a5c2c3: the debounce limits how many siblings start, but
|
|
1597
|
+
// the thread still needs filling. Queue it once; the next sibling run
|
|
1598
|
+
// claims it. The debounce stamp is not refreshed, so the window still
|
|
1599
|
+
// closes on schedule and the next automatic checkpoint starts a run.
|
|
1600
|
+
const depth = await queuePayload(liveRoot, siblingPayload(input, threadPath, "none"));
|
|
1601
|
+
printResult("checkpoint: sibling skipped (automatic checkpoint; one ran for this session recently)" +
|
|
1602
|
+
` — payload queued (${depth} pending)`);
|
|
1583
1603
|
}
|
|
1584
1604
|
else if (backend === "none") {
|
|
1585
1605
|
printResult("checkpoint: sibling disabled (backend none)");
|
|
@@ -1079,7 +1079,8 @@ function runSend(args, context) {
|
|
|
1079
1079
|
catch {
|
|
1080
1080
|
return fail("send", "could not persist sending state; no DM attempted");
|
|
1081
1081
|
}
|
|
1082
|
-
|
|
1082
|
+
// The pickup DM is read by the person and acted on by their agent.
|
|
1083
|
+
const dm = runHq(["dm", principal, headline, "--audience", "both", "--prompt-file", prompt, "--details-file", brief], context);
|
|
1083
1084
|
if (dm.code !== 0)
|
|
1084
1085
|
return fail("send", "hq dm delivery outcome is uncertain; status is sending, inspect DM history before retrying");
|
|
1085
1086
|
const event = /eventId[: ]+([A-Za-z0-9_-]+)/.exec(dm.stdout ?? "")?.[1] ?? "";
|
|
@@ -579,6 +579,8 @@ async function post(d, args) {
|
|
|
579
579
|
const versionError = requireCliVersion(d);
|
|
580
580
|
if (versionError)
|
|
581
581
|
return versionError;
|
|
582
|
+
// An automated status post keeps the channel default (agent audience); only a
|
|
583
|
+
// reply to a message a person wrote is for humans.
|
|
582
584
|
const call = ["dm", ch, ...(root ? ["--thread", root] : []), cliMessage];
|
|
583
585
|
const sent = d.runHq(call, { ...d.env, HQ_ROOT: d.root });
|
|
584
586
|
if (sent.code !== 0)
|
|
@@ -594,7 +596,7 @@ async function post(d, args) {
|
|
|
594
596
|
const mentions = mentionsJson(d, sid, names);
|
|
595
597
|
if (!mentions.length)
|
|
596
598
|
return fail("no mention resolved to a channel member id — not posting an update that notifies nobody");
|
|
597
|
-
const payload = { body: postedMessage, mentions };
|
|
599
|
+
const payload = { body: postedMessage, mentions, audience: "agent" };
|
|
598
600
|
if (root)
|
|
599
601
|
payload.rootEventId = root;
|
|
600
602
|
try {
|
package/dist/commands/dm.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { defaultAudienceFor, recipientKindOf, type DmRecipientKind } from "../lib/notify-recipient.js";
|
|
3
|
+
export { defaultAudienceFor, recipientKindOf, type DmRecipientKind };
|
|
2
4
|
import { type DmAttachmentWire } from "./dm-attach.js";
|
|
3
5
|
export interface DmRecipient {
|
|
4
6
|
toEmail?: string;
|
|
@@ -158,7 +160,8 @@ export declare function resolveMentions(tokens: string[], members: ChannelMember
|
|
|
158
160
|
* Returns null on anything that doesn't match. Pure → unit-testable.
|
|
159
161
|
*/
|
|
160
162
|
export declare function parseDuration(input: string): number | null;
|
|
161
|
-
type DmAudience
|
|
163
|
+
import type { DmAudience } from "../lib/notify-recipient.js";
|
|
164
|
+
export type { DmAudience };
|
|
162
165
|
export interface DmSendBody {
|
|
163
166
|
toEmail?: string;
|
|
164
167
|
toPersonUid?: string;
|
|
@@ -233,6 +236,15 @@ export declare function fetchChannels(token: string): Promise<ChannelSummary[]>;
|
|
|
233
236
|
* post itself passes. Used to resolve `@name` mentions into participant uids.
|
|
234
237
|
*/
|
|
235
238
|
export declare function fetchChannelMembers(token: string, channelId: string): Promise<ChannelMember[]>;
|
|
239
|
+
/**
|
|
240
|
+
* Resolve the audience for a send. The default comes from the recipient kind
|
|
241
|
+
* (`defaultAudienceFor`); `--for-human` maps to `human`; an explicit
|
|
242
|
+
* `--audience` wins over `--for-human` (with a warning) and is validated.
|
|
243
|
+
*/
|
|
244
|
+
export declare function resolveSendAudience(opts: {
|
|
245
|
+
audience?: string;
|
|
246
|
+
forHuman?: boolean;
|
|
247
|
+
}, recipientKind: DmRecipientKind, warn?: (msg: string) => void): string;
|
|
236
248
|
/** One incoming DM as returned by GET /v1/notify/inbox. */
|
|
237
249
|
export interface DmInboxEvent {
|
|
238
250
|
eventId: string;
|
|
@@ -411,5 +423,4 @@ export declare function formatChannelTranscript(messages: ChannelMessageItem[],
|
|
|
411
423
|
export declare function agentCallerUid(token: string, machine?: () => boolean): string | null;
|
|
412
424
|
export declare const AGENT_THREAD_UNAVAILABLE: string;
|
|
413
425
|
export declare function registerDmCommand(program: Command): void;
|
|
414
|
-
export {};
|
|
415
426
|
//# sourceMappingURL=dm.d.ts.map
|
package/dist/commands/dm.js
CHANGED
|
@@ -5,7 +5,8 @@ import { resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.
|
|
|
5
5
|
import { isMachineIdentity } from "@indigoai-us/hq-cloud";
|
|
6
6
|
import { peekIdToken } from "../utils/id-token.js";
|
|
7
7
|
import { redactAgentInboxItem as redactInboxCredentials } from "../lib/agent-kit/inbox-redact.js";
|
|
8
|
-
import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
|
|
8
|
+
import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN, defaultAudienceFor, recipientKindOf, } from "../lib/notify-recipient.js";
|
|
9
|
+
export { defaultAudienceFor, recipientKindOf };
|
|
9
10
|
import { dmForwardCliEnabled } from "./dm-forward-flag.js";
|
|
10
11
|
import { attachmentScopeForChannel, formatUploadedAttachments, personalScopeForCaller, uploadDmAttachments, validateAttachmentPaths, } from "./dm-attach.js";
|
|
11
12
|
import { ChannelNotFoundError, suggestChannels, withChannelSuggestions, } from "./channel-suggest.js";
|
|
@@ -555,7 +556,7 @@ async function runConnectionAction(action, identifier) {
|
|
|
555
556
|
* the server's idempotent participant-key dedupe) a group channel, then posts
|
|
556
557
|
* the message into it. Reuses the same vault API client + auth as the 1:1 path.
|
|
557
558
|
*/
|
|
558
|
-
async function runGroupSend(recipients, message) {
|
|
559
|
+
async function runGroupSend(recipients, message, audience) {
|
|
559
560
|
try {
|
|
560
561
|
const participants = [];
|
|
561
562
|
for (const r of recipients) {
|
|
@@ -601,7 +602,7 @@ async function runGroupSend(recipients, message) {
|
|
|
601
602
|
token,
|
|
602
603
|
path: `/v1/notify/channels/${encodeURIComponent(channelId)}/messages`,
|
|
603
604
|
method: "POST",
|
|
604
|
-
body: { body },
|
|
605
|
+
body: { body, ...(audience ? { audience } : {}) },
|
|
605
606
|
});
|
|
606
607
|
if (!sendRes.ok) {
|
|
607
608
|
const err = (await sendRes.json().catch(() => ({})));
|
|
@@ -810,11 +811,41 @@ async function runChannelSend(channelName, message, opts) {
|
|
|
810
811
|
process.exit(1);
|
|
811
812
|
}
|
|
812
813
|
}
|
|
813
|
-
|
|
814
|
+
function explicitAudience(opts) {
|
|
815
|
+
const explicit = opts.audience !== undefined && opts.audience !== "" ? opts.audience : undefined;
|
|
816
|
+
if (explicit !== undefined && !DM_AUDIENCE_VALUES.includes(explicit)) {
|
|
817
|
+
throw new Error("Invalid --audience '" + explicit + "': must be human, agent, or both.");
|
|
818
|
+
}
|
|
819
|
+
return explicit;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Resolve the audience for a send. The default comes from the recipient kind
|
|
823
|
+
* (`defaultAudienceFor`); `--for-human` maps to `human`; an explicit
|
|
824
|
+
* `--audience` wins over `--for-human` (with a warning) and is validated.
|
|
825
|
+
*/
|
|
826
|
+
export function resolveSendAudience(opts, recipientKind, warn = (m) => console.error(chalk.yellow(m))) {
|
|
827
|
+
const explicit = explicitAudience(opts);
|
|
828
|
+
if (explicit !== undefined) {
|
|
829
|
+
if (opts.forHuman) {
|
|
830
|
+
warn("Both --audience " + explicit + " and --for-human were passed; using --audience " + explicit + ".");
|
|
831
|
+
}
|
|
832
|
+
return explicit;
|
|
833
|
+
}
|
|
834
|
+
return opts.forHuman ? "human" : defaultAudienceFor(recipientKind);
|
|
835
|
+
}
|
|
836
|
+
async function runDmSend(recipient, message, rawOpts) {
|
|
837
|
+
// Validate an explicit --audience before any network call; the default is
|
|
838
|
+
// applied once the recipient kind is known.
|
|
839
|
+
explicitAudience(rawOpts);
|
|
840
|
+
const opts = { ...rawOpts };
|
|
841
|
+
const withAudience = (kind) => ({
|
|
842
|
+
...rawOpts,
|
|
843
|
+
audience: resolveSendAudience(rawOpts, kind),
|
|
844
|
+
});
|
|
814
845
|
// --channel <name> is an explicit channel target; the positional carries the
|
|
815
846
|
// message (recipient slot), e.g. `hq dm --channel vyg-dev "hello"`.
|
|
816
847
|
if (opts.channel !== undefined) {
|
|
817
|
-
await runChannelSend(opts.channel, message ?? recipient,
|
|
848
|
+
await runChannelSend(opts.channel, message ?? recipient, withAudience("channel"));
|
|
818
849
|
return;
|
|
819
850
|
}
|
|
820
851
|
if (recipient === undefined) {
|
|
@@ -837,7 +868,7 @@ async function runDmSend(recipient, message, opts) {
|
|
|
837
868
|
process.exit(1);
|
|
838
869
|
return;
|
|
839
870
|
}
|
|
840
|
-
await runGroupSend(group, message);
|
|
871
|
+
await runGroupSend(group, message, withAudience("group").audience);
|
|
841
872
|
return;
|
|
842
873
|
}
|
|
843
874
|
// A bare name or #hash addresses a named DM channel.
|
|
@@ -849,12 +880,13 @@ async function runDmSend(recipient, message, opts) {
|
|
|
849
880
|
? null
|
|
850
881
|
: matchContactByName(await fetchContacts(await ensureCognitoToken()), channelName);
|
|
851
882
|
if (!person?.personUid) {
|
|
852
|
-
await runChannelSend(channelName, message,
|
|
883
|
+
await runChannelSend(channelName, message, withAudience("channel"));
|
|
853
884
|
return;
|
|
854
885
|
}
|
|
855
886
|
console.log(chalk.dim(`Resolved '${channelName}' to person ${person.displayName || person.email || person.personUid} (${person.personUid}). Use '#${channelName}' or --channel ${channelName} to post to a channel instead.`));
|
|
856
887
|
recipient = person.personUid;
|
|
857
888
|
}
|
|
889
|
+
Object.assign(opts, withAudience(recipientKindOf(recipient)));
|
|
858
890
|
if (opts.thread !== undefined) {
|
|
859
891
|
console.error(chalk.red("--thread is only supported for channel messages (hq dm <channel> --thread <eventId|last> \"…\")."));
|
|
860
892
|
process.exit(1);
|
|
@@ -1697,7 +1729,7 @@ async function runDmChannel(target, opts) {
|
|
|
1697
1729
|
export function registerDmCommand(program) {
|
|
1698
1730
|
const dm = program
|
|
1699
1731
|
.command("dm")
|
|
1700
|
-
.description("Send and read direct messages, and manage connection requests.");
|
|
1732
|
+
.description("Send and read direct messages, and manage connection requests. Messages to a person send to them; messages to an agent, a channel or a group are tagged for agents and folded away from people by default. Pass --for-human or --audience human|agent|both to override.");
|
|
1701
1733
|
dm
|
|
1702
1734
|
.command("send [recipient] [message]", { isDefault: true, hidden: true })
|
|
1703
1735
|
.description('Send a direct message. RECIPIENT can be a person (email, personUid, or agentUid), a GROUP DM (comma-separated: "a@x.com,b@y.com"), or one of your DM CHANNELS by name — bare (hq dm vyg-dev "hi"), hash form (hq dm "#vyg-dev" "hi"), or via --channel (hq dm --channel vyg-dev "hi"). A person receives a DM as an HQ Sync notification; an agent receives it in its durable box inbox. If you aren\'t connected yet, it sends a connection request that holds your message. See your channels with `hq channels`.\n\nCHANNEL MENTIONS: HQ mentions are structured — the server never reads `@name` out of your text, so a channel message has to carry the participants alongside the body. When you write `@name` in a channel message, this command looks the name up in the channel roster (case-insensitive on display name and slug/handle; quote multi-word names as @"Jacob Posel") and sends it as a real mention, which notifies people and wakes agents. If a name is unknown or matches more than one member, the send FAILS with the candidates instead of posting something that mentions nobody. A bare "@" or an email address in your prose is never treated as a mention. Pass --no-mentions to post the text literally.\n\nCHANNEL THREADS: `hq dm <channel> --thread <eventId> "…"` replies under an existing root (the request carries rootEventId). `--thread last` replies to your most recent top-level message in that channel. Copy a root eventId from `hq dm channel <name>`, which prints each root\'s id and indents replies underneath.')
|
|
@@ -1710,7 +1742,8 @@ export function registerDmCommand(program) {
|
|
|
1710
1742
|
.option("--details-file <path>", "Read the details from a file (1:1 DMs; channel sends also supported)")
|
|
1711
1743
|
.option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
|
|
1712
1744
|
.option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
|
|
1713
|
-
.option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both")
|
|
1745
|
+
.option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both. Default: human for a person, agent for an agent, a channel or a group. Wins over --for-human.")
|
|
1746
|
+
.option("--for-human", "Tag the message for people (alias for --audience human).")
|
|
1714
1747
|
.option("--attach <path>", "Upload a local file and attach it to the message (repeatable, up to 5 files, 25 MB each). 1:1 DMs and named channels; the file lands in the conversation's vault scope and the recipient can open it. Not available for group DMs or scheduled sends.", (value, previous = []) => [...previous, value])
|
|
1715
1748
|
.action(async (recipient, message, opts) => {
|
|
1716
1749
|
await runDmSend(recipient, message, opts);
|
|
@@ -3,6 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import { ensureCognitoToken, loadCachedTokens } from "../utils/cognito-session.js";
|
|
4
4
|
import { assertVaultApiRequestSupported, vaultApiFetch, } from "../utils/vault-api.js";
|
|
5
5
|
import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
|
|
6
|
+
import { scrubDiagnosticsString, scrubDiagnosticsValue } from "../utils/scrub-diagnostics.js";
|
|
6
7
|
import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
|
|
7
8
|
import { FEEDBACK_LOGS_MAX_TOTAL_BYTES, FEEDBACK_LOGS_MIN_USEFUL_BYTES, collectFeedbackLogs, feedbackLogsEnabled, } from "../utils/feedback-logs.js";
|
|
8
9
|
import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
|
|
@@ -79,8 +80,10 @@ export function resolveFeedbackReporter(env = process.env, isTTY = process.stdin
|
|
|
79
80
|
function buildFeedbackRequestBody(opts) {
|
|
80
81
|
const requestBody = {
|
|
81
82
|
type: opts.type,
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
// Title and body are free text, and /hq-bug puts tool output in the body,
|
|
84
|
+
// so they get the same credential scrub as diagnostics.
|
|
85
|
+
title: scrubDiagnosticsString(opts.title),
|
|
86
|
+
body: scrubDiagnosticsString(opts.body),
|
|
84
87
|
diagnostics: opts.diagnostics,
|
|
85
88
|
};
|
|
86
89
|
if (opts.reporterMark) {
|
|
@@ -157,7 +160,12 @@ export async function submitFeedback(opts) {
|
|
|
157
160
|
// Keep submitFeedback safe for direct callers as well as the command path,
|
|
158
161
|
// which invokes the same validator before uploading any attachments.
|
|
159
162
|
validateFeedbackContent(opts.title, opts.body);
|
|
160
|
-
|
|
163
|
+
// Scrub here, at the one point every submission passes through. Callers may
|
|
164
|
+
// hand in their own diagnostics (the command path collects them before
|
|
165
|
+
// uploads, and adds latestVersion afterwards), and a caller-supplied blob
|
|
166
|
+
// previously skipped redaction entirely. The scrubbed copy is the object
|
|
167
|
+
// attachDebugLogs later mutates, so inline logs still land in the request.
|
|
168
|
+
const diagnostics = scrubDiagnosticsValue(opts.diagnostics ?? collectDiagnostics());
|
|
161
169
|
const reporterMark = opts.type === "bug" && resolveProcessFlagGate(FEEDBACK_REPORTER_KIND_FLAG, { fallback: FEEDBACK_REPORTER_KIND_FLAG_FALLBACK }, () => FEEDBACK_REPORTER_KIND_FLAG_FALLBACK)
|
|
162
170
|
? resolveFeedbackReporter(process.env, process.stdin.isTTY === true, opts.reporterOverride)
|
|
163
171
|
: undefined;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { CLI_VERSION } from "../../../cli-version.js";
|
|
19
19
|
import { peekIdToken } from "../../../utils/id-token.js";
|
|
20
|
-
import { isRecipientUid } from "../../../lib/notify-recipient.js";
|
|
20
|
+
import { defaultAudienceFor, isAgentUid, isRecipientUid } from "../../../lib/notify-recipient.js";
|
|
21
21
|
import { redactAgentInboxItem } from "../inbox-redact.js";
|
|
22
22
|
import { markInboxDone, mergeAgentInbox, readDoneIds, summarizeInboxEntry } from "../inbox-state.js";
|
|
23
23
|
import { AttachmentCompanyUnresolvedError, fetchChatAttachment } from "./attachments.js";
|
|
@@ -214,13 +214,13 @@ export function buildAgentMcpTools(clients) {
|
|
|
214
214
|
},
|
|
215
215
|
{
|
|
216
216
|
name: "hq_dm_send",
|
|
217
|
-
description: "Send an HQ message as this agent to a person (email or prs_…), an agent (agt_…), or a channel or group DM (chn_…). When replying to an inbox item that has a channelId, pass that channelId as `to` so the answer lands in the conversation it came from. Use audience='agent' for progress/status messages that only agents need to see; audience='human' for messages a person must read; audience='both' when unsure.",
|
|
217
|
+
description: "Send an HQ message as this agent to a person (email or prs_…), an agent (agt_…), or a channel or group DM (chn_…). When replying to an inbox item that has a channelId, pass that channelId as `to` so the answer lands in the conversation it came from. Use audience='agent' for progress/status messages that only agents need to see; audience='human' for messages a person must read; audience='both' when unsure. Default: human for a person, agent for an agent, a channel or a group.",
|
|
218
218
|
inputSchema: {
|
|
219
219
|
type: "object",
|
|
220
220
|
properties: {
|
|
221
221
|
to: { type: "string", description: "Email, prs_… or agt_… uid, or a chn_… channel id" },
|
|
222
222
|
message: { type: "string" },
|
|
223
|
-
audience: { type: "string", enum: ["human", "agent", "both"], description: "Who this message is for (default: human)" },
|
|
223
|
+
audience: { type: "string", enum: ["human", "agent", "both"], description: "Who this message is for (default: human for a person, agent for an agent, channel or group)" },
|
|
224
224
|
},
|
|
225
225
|
required: ["to", "message"],
|
|
226
226
|
additionalProperties: false,
|
|
@@ -238,7 +238,7 @@ export function buildAgentMcpTools(clients) {
|
|
|
238
238
|
const token = await clients.getToken();
|
|
239
239
|
const res = await clients.apiJson(token, `/v1/notify/channels/${encodeURIComponent(to)}/messages`, {
|
|
240
240
|
method: "POST",
|
|
241
|
-
body: { body: message,
|
|
241
|
+
body: { body: message, audience: audience ?? defaultAudienceFor("channel") },
|
|
242
242
|
});
|
|
243
243
|
if (res.status < 200 || res.status >= 300) {
|
|
244
244
|
return errorResult(`Channel send failed (${res.status}): ${describeError(res.body)}`);
|
|
@@ -252,8 +252,7 @@ export function buildAgentMcpTools(clients) {
|
|
|
252
252
|
: (() => {
|
|
253
253
|
throw new McpToolInputError('"to" must be an email, prs_… or agt_… uid, or a chn_… channel id');
|
|
254
254
|
})();
|
|
255
|
-
|
|
256
|
-
body.audience = audience;
|
|
255
|
+
body.audience = audience ?? defaultAudienceFor(isAgentUid(to) ? "agent" : "person");
|
|
257
256
|
const token = await clients.getToken();
|
|
258
257
|
const res = await clients.apiJson(token, "/v1/notify/dm", { method: "POST", body });
|
|
259
258
|
if (res.status < 200 || res.status >= 300) {
|
|
@@ -116,6 +116,13 @@ export interface SyncHealthDeps {
|
|
|
116
116
|
* since a re-upload would exit 0 without uploading.
|
|
117
117
|
*/
|
|
118
118
|
manifestUploadsDisabled?: () => boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Start time (epoch ms) of a live background `hq doctor --fix` pull for
|
|
121
|
+
* this refresh target (a company slug, or `personal`), or null. A journal
|
|
122
|
+
* whose refresh is still running is reported "refresh in progress" rather
|
|
123
|
+
* than stale (feedback cd56a501).
|
|
124
|
+
*/
|
|
125
|
+
refreshInProgress?: (fixTarget: string) => number | null;
|
|
119
126
|
}
|
|
120
127
|
/**
|
|
121
128
|
* True when `slug` is a filesystem-safe company folder name. Rejects empty
|
|
@@ -148,7 +155,6 @@ export declare function isPersonalJournalSlug(slug: string): boolean;
|
|
|
148
155
|
export declare function journalSlugFromCheckId(checkId: string): string | null;
|
|
149
156
|
/** The versions/sync check family. Registered in `createDefaultRegistry`. */
|
|
150
157
|
export declare const syncHealthFamily: CheckFamily;
|
|
151
|
-
/** Run every versions/sync check. A thrown check degrades to UNKNOWN. */
|
|
152
158
|
export declare function checkSyncHealth(context: CheckContext, rawDeps?: SyncHealthDeps): CheckResult[];
|
|
153
159
|
/** True when `a` > `b` for plain X.Y.Z versions. Non-numeric parts compare 0. */
|
|
154
160
|
export declare function semverGt(a: string, b: string): boolean;
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { defaultSyncRuntimeResults } from "./sync-runtime.js";
|
|
27
27
|
import * as fs from "node:fs";
|
|
28
|
+
import { DEFAULT_OWNER_IDENTITY } from "../fix/process-identity.js";
|
|
29
|
+
import { REFRESH_SPAWN_LOCK_WARN_MS, inspectRefreshSpawnLock, refreshHandoffStartedAt, } from "../fix/refresh-sync-handoff.js";
|
|
28
30
|
import * as path from "node:path";
|
|
29
31
|
// Namespace import, deliberately: hq-cloud is an ESM package, so a static
|
|
30
32
|
// NAMED import of an export the installed version lacks is a link-time
|
|
@@ -63,6 +65,8 @@ function withDefaults(deps, hqRoot) {
|
|
|
63
65
|
companySyncResolvable: deps.companySyncResolvable ??
|
|
64
66
|
((slug, lastSync) => !companyRecordedNotResolved(hqRoot, slug, deps.now().getTime(), lastSync)),
|
|
65
67
|
manifestUploadsDisabled: deps.manifestUploadsDisabled ?? (() => manifestUploadsDisabled()),
|
|
68
|
+
refreshInProgress: deps.refreshInProgress ??
|
|
69
|
+
((fixTarget) => refreshHandoffStartedAt(hqRoot, fixTarget, deps.now().getTime())),
|
|
66
70
|
};
|
|
67
71
|
}
|
|
68
72
|
/**
|
|
@@ -294,6 +298,36 @@ export const syncHealthFamily = {
|
|
|
294
298
|
run: (context) => Promise.resolve(checkSyncHealth(context)),
|
|
295
299
|
};
|
|
296
300
|
/** Run every versions/sync check. A thrown check degrades to UNKNOWN. */
|
|
301
|
+
/**
|
|
302
|
+
* WARN when the `hq doctor --fix` spawn lock has been held for over
|
|
303
|
+
* {@link REFRESH_SPAWN_LOCK_WARN_MS}, naming the holder pid, so a stalled or
|
|
304
|
+
* suspended holder is visible. Report only: the lock is never removed here.
|
|
305
|
+
*/
|
|
306
|
+
function refreshSpawnLockResult(hqRoot, nowMs) {
|
|
307
|
+
let lock;
|
|
308
|
+
try {
|
|
309
|
+
lock = inspectRefreshSpawnLock(hqRoot, nowMs, DEFAULT_OWNER_IDENTITY);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
if (!lock || lock.heldMs === null || lock.heldMs <= REFRESH_SPAWN_LOCK_WARN_MS)
|
|
315
|
+
return [];
|
|
316
|
+
// Left by a claimer that has exited: nothing to stop, and the next
|
|
317
|
+
// `hq doctor --fix` releases it.
|
|
318
|
+
if (lock.ownerGone)
|
|
319
|
+
return [];
|
|
320
|
+
const minutes = Math.floor(lock.heldMs / 60_000);
|
|
321
|
+
return [
|
|
322
|
+
{
|
|
323
|
+
status: "WARN",
|
|
324
|
+
checkId: "sync.refresh.spawn-lock",
|
|
325
|
+
reasonCode: "refresh-lock-held",
|
|
326
|
+
message: `The hq doctor --fix refresh lock has been held for ${minutes} minutes by process ${lock.pid}. No new journal refresh can start until it is released.`,
|
|
327
|
+
remediation: `If process ${lock.pid} is stuck or suspended, resume or stop it; the lock is released when it exits.`,
|
|
328
|
+
},
|
|
329
|
+
];
|
|
330
|
+
}
|
|
297
331
|
export function checkSyncHealth(context, rawDeps = DEFAULT_DEPS) {
|
|
298
332
|
const deps = withDefaults(rawDeps, context.hqRoot);
|
|
299
333
|
try {
|
|
@@ -301,6 +335,7 @@ export function checkSyncHealth(context, rawDeps = DEFAULT_DEPS) {
|
|
|
301
335
|
...versionResults(context, deps),
|
|
302
336
|
...updateAvailabilityResult(context, deps),
|
|
303
337
|
...journalAndManifestResults(deps),
|
|
338
|
+
...refreshSpawnLockResult(context.hqRoot, deps.now().getTime()),
|
|
304
339
|
...deps.runtime(deps.now()),
|
|
305
340
|
];
|
|
306
341
|
}
|
|
@@ -528,7 +563,7 @@ function journalResults(deps, journals) {
|
|
|
528
563
|
if (entry.slug === PERSONAL_SCOPE_SLUG) {
|
|
529
564
|
results.push(leftoverPersonalJournalResult(entry));
|
|
530
565
|
if (!hasPersonalVaultJournal) {
|
|
531
|
-
results.push(journalFreshnessResult(entry, PERSONAL_VAULT_SCOPE_SLUG, nowMs));
|
|
566
|
+
results.push(withRefreshInProgress(deps, journalFreshnessResult(entry, PERSONAL_VAULT_SCOPE_SLUG, nowMs), "personal"));
|
|
532
567
|
}
|
|
533
568
|
continue;
|
|
534
569
|
}
|
|
@@ -570,10 +605,40 @@ function journalResults(deps, journals) {
|
|
|
570
605
|
results.push(notOnThisMachineResult(entry, checkId));
|
|
571
606
|
continue;
|
|
572
607
|
}
|
|
573
|
-
results.push(freshness);
|
|
608
|
+
results.push(withRefreshInProgress(deps, freshness, isPersonalJournalSlug(entry.slug) ? "personal" : entry.slug));
|
|
574
609
|
}
|
|
575
610
|
return results;
|
|
576
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* A stale or never-synced journal whose `hq doctor --fix` pull is still
|
|
614
|
+
* running in the background is UNTESTED "refresh in progress", not WARN.
|
|
615
|
+
* The pull records lastSync only when it finishes, so until then the journal
|
|
616
|
+
* looks stale even though the repair is under way. Once the process is gone
|
|
617
|
+
* the record no longer counts and the WARN comes back, so a pull that died is
|
|
618
|
+
* retried by the next `--fix`.
|
|
619
|
+
*/
|
|
620
|
+
function withRefreshInProgress(deps, result, fixTarget) {
|
|
621
|
+
if (result.status !== "WARN")
|
|
622
|
+
return result;
|
|
623
|
+
if (result.reasonCode !== "stale-threshold" && result.reasonCode !== "never-synced")
|
|
624
|
+
return result;
|
|
625
|
+
let startedAtMs;
|
|
626
|
+
try {
|
|
627
|
+
startedAtMs = deps.refreshInProgress(fixTarget);
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return result;
|
|
631
|
+
}
|
|
632
|
+
if (startedAtMs === null)
|
|
633
|
+
return result;
|
|
634
|
+
return {
|
|
635
|
+
status: "UNTESTED",
|
|
636
|
+
checkId: result.checkId,
|
|
637
|
+
reasonCode: "refresh-in-progress",
|
|
638
|
+
...(result.target ? { target: result.target } : {}),
|
|
639
|
+
message: `${result.message} hq doctor --fix is refreshing it in the background (started ${new Date(startedAtMs).toISOString()}); it passes when that pull finishes.`,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
577
642
|
function companySyncResolvable(deps, slug, lastSync) {
|
|
578
643
|
try {
|
|
579
644
|
return deps.companySyncResolvable(slug, lastSync);
|