@indigoai-us/hq-cli 5.345.31 → 5.345.33
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 +11 -0
- package/dist/command-catalog.generated.d.ts +100 -3
- package/dist/command-catalog.generated.js +121 -3
- package/dist/commands/db-dump.d.ts +8 -0
- package/dist/commands/db-dump.js +50 -0
- package/dist/commands/db-provision.d.ts +3 -10
- package/dist/commands/db-provision.js +26 -47
- package/dist/commands/db-remote-deps.d.ts +18 -0
- package/dist/commands/db-remote-deps.js +59 -0
- package/dist/commands/db-sql.d.ts +2 -1
- package/dist/commands/db-sql.js +30 -5
- package/dist/commands/db-usage.d.ts +7 -0
- package/dist/commands/db-usage.js +28 -0
- package/dist/commands/db.js +4 -0
- package/dist/commands/files.js +33 -5
- package/dist/commands/integrations-core.d.ts +40 -0
- package/dist/commands/integrations-core.js +90 -0
- package/dist/commands/integrations.d.ts +2 -2
- package/dist/commands/integrations.js +27 -3
- package/dist/commands/lanes-message.js +40 -8
- package/dist/commands/lanes-questions.d.ts +6 -1
- package/dist/commands/lanes-questions.js +19 -7
- package/dist/commands/lanes-resume.js +17 -6
- package/dist/commands/lanes-shared.d.ts +6 -1
- package/dist/commands/lanes.js +74 -1
- package/dist/commands/mesh.js +150 -26
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +68 -604
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/db/control-plane.d.ts +32 -0
- package/dist/lib/db/control-plane.js +50 -0
- package/dist/lib/db/redact.d.ts +4 -0
- package/dist/lib/db/redact.js +17 -0
- package/dist/lib/db/remote-engine.d.ts +67 -0
- package/dist/lib/db/remote-engine.js +165 -0
- package/dist/lib/db/usage-format.d.ts +4 -0
- package/dist/lib/db/usage-format.js +41 -0
- package/dist/lib/lanes/keep-alive.d.ts +12 -0
- package/dist/lib/lanes/keep-alive.js +25 -6
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/project-board.d.ts +17 -0
- package/dist/lib/mesh/project-board.js +28 -0
- package/dist/lib/mesh/story-status.d.ts +16 -0
- package/dist/lib/mesh/story-status.js +32 -7
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +61 -23
- package/dist/utils/version-check.d.ts +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-gate.d.ts +21 -14
- package/dist/utils/version-gate.js +119 -28
- package/package.json +4 -1
package/dist/commands/db-sql.js
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
import { Option } from "commander";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import fs from "node:fs";
|
|
10
|
+
import { safeErrorMessage } from "../lib/db/redact.js";
|
|
11
|
+
import { formatRemoteResults, openRemoteAppConnection, runRemoteSql, } from "../lib/db/remote-engine.js";
|
|
10
12
|
import { formatSqlResult, runLocalSql } from "../lib/db/sql.js";
|
|
13
|
+
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
14
|
+
import { defaultRemoteDeps, describeRemoteError, remoteEngineDeps, } from "./db-remote-deps.js";
|
|
15
|
+
import { describeSessionGap } from "./db-dump.js";
|
|
11
16
|
function readSqlArg(sqlArgs, stdinFlag) {
|
|
12
17
|
if (stdinFlag || sqlArgs.length === 0) {
|
|
13
18
|
if (process.stdin.isTTY && sqlArgs.length === 0) {
|
|
@@ -17,10 +22,11 @@ function readSqlArg(sqlArgs, stdinFlag) {
|
|
|
17
22
|
}
|
|
18
23
|
return sqlArgs.join(" ");
|
|
19
24
|
}
|
|
20
|
-
export function registerDbSqlCommand(db) {
|
|
25
|
+
export function registerDbSqlCommand(db, remoteDepsFactory = defaultRemoteDeps) {
|
|
21
26
|
db.command("sql")
|
|
22
|
-
.description("Run SQL against the company vault DB (local; read-only by default
|
|
27
|
+
.description("Run SQL against the company vault DB (local) or an app's remote database (--app); read-only by default")
|
|
23
28
|
.requiredOption("--company <slug>", "Company slug (tenant scope; required)")
|
|
29
|
+
.option("--app <app>", "Run against this app's remote database schema")
|
|
24
30
|
.option("--write", "Allow write/DDL statements (discouraged for ad-hoc DDL)", false)
|
|
25
31
|
.addOption(new Option("--remote", "Remote SQL is not available in this CLI build yet; local queries work without --remote").hideHelp())
|
|
26
32
|
.option("--db-path <path>", "Dangerous: open a non-canonical DB path (denied unless --allow-cross-company-path)")
|
|
@@ -37,13 +43,17 @@ export function registerDbSqlCommand(db) {
|
|
|
37
43
|
process.exitCode = 1;
|
|
38
44
|
return;
|
|
39
45
|
}
|
|
40
|
-
if (opts.remote) {
|
|
41
|
-
console.error(chalk.red("Error: Remote SQL
|
|
46
|
+
if (opts.remote && !opts.app) {
|
|
47
|
+
console.error(chalk.red("Error: Remote SQL needs --app <app>. Local queries work without --remote."));
|
|
42
48
|
process.exitCode = 1;
|
|
43
49
|
return;
|
|
44
50
|
}
|
|
45
51
|
const sql = readSqlArg(sqlParts, !!opts.stdin).trim();
|
|
46
52
|
const format = opts.format === "table" ? "table" : "jsonl";
|
|
53
|
+
if (opts.app) {
|
|
54
|
+
await runRemote(remoteDepsFactory(), company, opts.app, sql, !!opts.write);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
47
57
|
const result = runLocalSql({
|
|
48
58
|
company,
|
|
49
59
|
sql,
|
|
@@ -56,9 +66,24 @@ export function registerDbSqlCommand(db) {
|
|
|
56
66
|
console.log(formatSqlResult(result, format));
|
|
57
67
|
}
|
|
58
68
|
catch (error) {
|
|
59
|
-
|
|
69
|
+
if (isPlanGateError(error))
|
|
70
|
+
throw error;
|
|
71
|
+
console.error(chalk.red("Error:"), describeSessionGap(error) ?? describeRemoteError(error) ?? safeErrorMessage(error));
|
|
60
72
|
process.exitCode = 1;
|
|
61
73
|
}
|
|
62
74
|
});
|
|
63
75
|
}
|
|
76
|
+
async function runRemote(deps, company, app, sql, write) {
|
|
77
|
+
const resolved = await deps.resolveCompany(company);
|
|
78
|
+
const conn = await openRemoteAppConnection(remoteEngineDeps(deps), {
|
|
79
|
+
companyUid: resolved.companyUid,
|
|
80
|
+
appId: app,
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
console.log(formatRemoteResults(await runRemoteSql(conn, sql, write)));
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await conn.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
64
89
|
//# sourceMappingURL=db-sql.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq db usage — month-to-date remote database usage against both caps.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { type DbRemoteCommandDeps } from "./db-remote-deps.js";
|
|
6
|
+
export declare function registerDbUsageCommand(db: Command, depsFactory?: () => DbRemoteCommandDeps): void;
|
|
7
|
+
//# sourceMappingURL=db-usage.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq db usage — month-to-date remote database usage against both caps.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { safeErrorMessage } from "../lib/db/redact.js";
|
|
6
|
+
import { formatUsage } from "../lib/db/usage-format.js";
|
|
7
|
+
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
8
|
+
import { controlPlaneClient, defaultRemoteDeps, describeRemoteError, } from "./db-remote-deps.js";
|
|
9
|
+
export function registerDbUsageCommand(db, depsFactory = defaultRemoteDeps) {
|
|
10
|
+
db.command("usage")
|
|
11
|
+
.description("Show this month's remote database usage against the compute and storage caps")
|
|
12
|
+
.requiredOption("--company <slug>", "Company slug")
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
try {
|
|
15
|
+
const deps = depsFactory();
|
|
16
|
+
const resolved = await deps.resolveCompany(String(opts.company ?? "").trim());
|
|
17
|
+
const usage = await controlPlaneClient(deps).usage(resolved.companyUid);
|
|
18
|
+
console.log(formatUsage(usage));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (isPlanGateError(error))
|
|
22
|
+
throw error;
|
|
23
|
+
console.error(chalk.red("Error:"), describeRemoteError(error) ?? safeErrorMessage(error));
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=db-usage.js.map
|
package/dist/commands/db.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* hq db — vault database commands (local + remote tiers).
|
|
3
3
|
*/
|
|
4
|
+
import { registerDbDumpCommand } from "./db-dump.js";
|
|
4
5
|
import { registerDbMigrateCommand } from "./db-migrate.js";
|
|
5
6
|
import { registerDbProvisionCommand } from "./db-provision.js";
|
|
6
7
|
import { registerDbSqlCommand } from "./db-sql.js";
|
|
7
8
|
import { registerDbStatusCommand } from "./db-status.js";
|
|
9
|
+
import { registerDbUsageCommand } from "./db-usage.js";
|
|
8
10
|
/**
|
|
9
11
|
* Register the `hq db` command group and its subcommands.
|
|
10
12
|
*/
|
|
@@ -16,5 +18,7 @@ export function registerDbCommand(program) {
|
|
|
16
18
|
registerDbSqlCommand(db);
|
|
17
19
|
registerDbMigrateCommand(db);
|
|
18
20
|
registerDbProvisionCommand(db);
|
|
21
|
+
registerDbUsageCommand(db);
|
|
22
|
+
registerDbDumpCommand(db);
|
|
19
23
|
}
|
|
20
24
|
//# sourceMappingURL=db.js.map
|
package/dist/commands/files.js
CHANGED
|
@@ -64,7 +64,9 @@ function formatAclPatternConflict(prefix, serverMessage) {
|
|
|
64
64
|
const sharedPattern = `${privatePattern}*`;
|
|
65
65
|
const detail = serverMessage ? `${serverMessage}. ` : "";
|
|
66
66
|
return (`${detail}A private folder '${privatePattern}' and shared folder glob ` +
|
|
67
|
-
`'${sharedPattern}' cannot both exist.
|
|
67
|
+
`'${sharedPattern}' cannot both exist. If the shared glob has no grants, run ` +
|
|
68
|
+
`hq files unshare '${sharedPattern}'. If it has grants, remove them first with ` +
|
|
69
|
+
`hq files unshare '${sharedPattern}' --with <principal>.`);
|
|
68
70
|
}
|
|
69
71
|
/**
|
|
70
72
|
* POST /files/{companyUid}/share-session — mint a browser-launch share
|
|
@@ -265,8 +267,8 @@ export function registerFilesCommand(program) {
|
|
|
265
267
|
});
|
|
266
268
|
files
|
|
267
269
|
.command("unshare <prefix>")
|
|
268
|
-
.description("Remove a file access grant
|
|
269
|
-
.
|
|
270
|
+
.description("Remove a named file access grant, or remove an empty ACL row without --with.")
|
|
271
|
+
.option("--with <principal>", "Person uid, agent uid, email address, group id, or '@all' to remove the grant")
|
|
270
272
|
.action(async (prefix, opts) => {
|
|
271
273
|
try {
|
|
272
274
|
// Unshare MUTATES access with no preview. Anchored content paths remain
|
|
@@ -279,13 +281,13 @@ export function registerFilesCommand(program) {
|
|
|
279
281
|
? { companySlug: files.opts().company }
|
|
280
282
|
: {}),
|
|
281
283
|
});
|
|
282
|
-
const principal = opts.with;
|
|
284
|
+
const principal = opts.with ?? "";
|
|
283
285
|
const isAll = principal === "@all";
|
|
284
286
|
const isEmail = !isAll && EMAIL_PATTERN.test(principal);
|
|
285
287
|
const isGroup = !isAll && GROUP_ID_PATTERN.test(principal);
|
|
286
288
|
const isAgent = !isAll && !isEmail && !isGroup && AGENT_UID_PATTERN.test(principal);
|
|
287
289
|
const isPerson = !isAll && !isEmail && !isGroup && !isAgent && PERSON_UID_PATTERN.test(principal);
|
|
288
|
-
if (!isAll && !isEmail && !isGroup && !isPerson && !isAgent) {
|
|
290
|
+
if (opts.with && !isAll && !isEmail && !isGroup && !isPerson && !isAgent) {
|
|
289
291
|
console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, a person uid matching prs_<alphanumeric>, an agent uid matching agt_<alphanumeric>, or a group id matching grp_<alphanumeric>`));
|
|
290
292
|
process.exit(1);
|
|
291
293
|
}
|
|
@@ -313,6 +315,32 @@ export function registerFilesCommand(program) {
|
|
|
313
315
|
explicit: explicitCompanySlug,
|
|
314
316
|
anchorSlug: companyRootSlug,
|
|
315
317
|
});
|
|
318
|
+
if (!opts.with) {
|
|
319
|
+
const res = await vaultApiFetch({
|
|
320
|
+
token,
|
|
321
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl?prefix=${encodeURIComponent(canonicalPrefix)}`,
|
|
322
|
+
method: "DELETE",
|
|
323
|
+
});
|
|
324
|
+
const body = await res.json().catch(() => ({}));
|
|
325
|
+
if (res.status === 404 && body.code === "ACL_NOT_FOUND") {
|
|
326
|
+
console.log(chalk.dim(`Nothing to remove — '${canonicalPrefix}' has no ACL row.`));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!res.ok) {
|
|
330
|
+
if (res.status === 403) {
|
|
331
|
+
console.error(chalk.red("Not authorized to remove this ACL row"));
|
|
332
|
+
await offerAccessLadder(canonicalPrefix, {
|
|
333
|
+
company: files.opts().company,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
console.error(chalk.red(body.error ?? "Unable to remove ACL row"));
|
|
338
|
+
}
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
console.log(chalk.green(`Removed empty ACL pattern '${canonicalPrefix}'. It now follows its parent folder's access, so anyone who can see that parent can see this folder.`));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
316
344
|
const res = await vaultApiFetch({
|
|
317
345
|
token,
|
|
318
346
|
path: `/files/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
@@ -225,6 +225,46 @@ export declare function toolPrefixForProvider(provider: string): string;
|
|
|
225
225
|
*/
|
|
226
226
|
export declare function fetchAdminSurface(token: string, companyUid: string): Promise<AdminSurface>;
|
|
227
227
|
export declare function fetchConnections(token: string, companyUid: string): Promise<AdminConnection[]>;
|
|
228
|
+
/**
|
|
229
|
+
* One app the caller can use right now through the HQ gateway. Field names
|
|
230
|
+
* follow the bot `company_apps` projection (name, provider, domain, status),
|
|
231
|
+
* so session context and bot context describe an app the same way. No
|
|
232
|
+
* endpoint URLs, credentials, scopes, or creator identities.
|
|
233
|
+
*/
|
|
234
|
+
export interface UsableApp {
|
|
235
|
+
connectionId: string;
|
|
236
|
+
/** Display name, stripped of control characters and capped in length. */
|
|
237
|
+
name: string;
|
|
238
|
+
provider: string;
|
|
239
|
+
domain: string | null;
|
|
240
|
+
status: string;
|
|
241
|
+
/** Bare provider slug, e.g. `notion`. */
|
|
242
|
+
providerArg: string;
|
|
243
|
+
/**
|
|
244
|
+
* The flag that resolves to exactly this connection in `hq integrations
|
|
245
|
+
* tools|call`: `--provider <slug>` when no other active row shares the
|
|
246
|
+
* provider, else `--connection <id>`.
|
|
247
|
+
*/
|
|
248
|
+
selector: string;
|
|
249
|
+
}
|
|
250
|
+
export interface UsableAppsResult {
|
|
251
|
+
companyUid: string;
|
|
252
|
+
/**
|
|
253
|
+
* False when the server ignored `view=summary` (an older deploy). Its rows
|
|
254
|
+
* carry no `viewerAccess`, so nothing can be confirmed usable and `apps` is
|
|
255
|
+
* empty rather than guessed.
|
|
256
|
+
*/
|
|
257
|
+
viewerAccessKnown: boolean;
|
|
258
|
+
apps: UsableApp[];
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The apps the caller may use, from `GET /v1/integrations/admin?view=summary`.
|
|
262
|
+
* A row counts only when all of these hold, mirroring the gateway's checks:
|
|
263
|
+
* the server's per-caller ACL decision (`viewerAccess.usable`) is true, the
|
|
264
|
+
* status is one the gateway serves, and the gateway has a provider registered
|
|
265
|
+
* for it. Fails closed: a row without `viewerAccess` is not listed.
|
|
266
|
+
*/
|
|
267
|
+
export declare function fetchUsableApps(token: string, companyUid: string): Promise<UsableAppsResult>;
|
|
228
268
|
/**
|
|
229
269
|
* Resolve one connection by `--connection acct_…` or `--provider linear`
|
|
230
270
|
* (matches `factory:<slug>`, bare provider ids, and an installation's human
|
|
@@ -355,6 +355,96 @@ export async function fetchAdminSurface(token, companyUid) {
|
|
|
355
355
|
export async function fetchConnections(token, companyUid) {
|
|
356
356
|
return (await fetchAdminSurface(token, companyUid)).connections;
|
|
357
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* Statuses the gateway serves. `needs-attention` is derived by the summary
|
|
360
|
+
* view from a stored `connected` row whose token renewal is failing, so the
|
|
361
|
+
* gateway still dispatches to it.
|
|
362
|
+
*/
|
|
363
|
+
const GATEWAY_SERVED_STATUSES = new Set(["connected", "needs-attention"]);
|
|
364
|
+
/**
|
|
365
|
+
* Providers hq-pro registers statically (`createIntegrationProviderRegistry`).
|
|
366
|
+
* Every other provider is registered per company from a factory installation
|
|
367
|
+
* keyed by its `runtimeProviderId`; a connection with neither is refused by the
|
|
368
|
+
* gateway as `ProviderUnsupported`.
|
|
369
|
+
*/
|
|
370
|
+
const STATIC_GATEWAY_PROVIDERS = new Set(["google-workspace", "linear", "slack"]);
|
|
371
|
+
const MAX_APP_NAME_LENGTH = 60;
|
|
372
|
+
/**
|
|
373
|
+
* Display names are set by whoever installed the app and end up in agent
|
|
374
|
+
* context, so keep them to one short line of printable text.
|
|
375
|
+
*/
|
|
376
|
+
function safeAppName(raw, fallback) {
|
|
377
|
+
const cleaned = (raw ?? "")
|
|
378
|
+
.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, " ")
|
|
379
|
+
.replace(/\s+/g, " ")
|
|
380
|
+
.trim();
|
|
381
|
+
if (!cleaned)
|
|
382
|
+
return fallback;
|
|
383
|
+
const chars = Array.from(cleaned);
|
|
384
|
+
return chars.length > MAX_APP_NAME_LENGTH
|
|
385
|
+
? `${chars.slice(0, MAX_APP_NAME_LENGTH - 1).join("").trimEnd()}…`
|
|
386
|
+
: cleaned;
|
|
387
|
+
}
|
|
388
|
+
/** Case-insensitive code-point order, so the list reads the same in every locale. */
|
|
389
|
+
function compareNames(a, b) {
|
|
390
|
+
const x = a.toLowerCase();
|
|
391
|
+
const y = b.toLowerCase();
|
|
392
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
393
|
+
}
|
|
394
|
+
function gatewayHasProvider(row) {
|
|
395
|
+
if (STATIC_GATEWAY_PROVIDERS.has(row.provider))
|
|
396
|
+
return true;
|
|
397
|
+
return row.installation?.runtimeProviderId === row.provider;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The apps the caller may use, from `GET /v1/integrations/admin?view=summary`.
|
|
401
|
+
* A row counts only when all of these hold, mirroring the gateway's checks:
|
|
402
|
+
* the server's per-caller ACL decision (`viewerAccess.usable`) is true, the
|
|
403
|
+
* status is one the gateway serves, and the gateway has a provider registered
|
|
404
|
+
* for it. Fails closed: a row without `viewerAccess` is not listed.
|
|
405
|
+
*/
|
|
406
|
+
export async function fetchUsableApps(token, companyUid) {
|
|
407
|
+
const res = await vaultApiFetch({
|
|
408
|
+
token,
|
|
409
|
+
path: "/v1/integrations/admin",
|
|
410
|
+
query: { companyUid, view: "summary" },
|
|
411
|
+
});
|
|
412
|
+
if (!res.ok)
|
|
413
|
+
await raiseForResponse(res, "Failed to list integrations");
|
|
414
|
+
const data = (await res.json());
|
|
415
|
+
const viewerAccessKnown = data.view === "summary";
|
|
416
|
+
const rows = Array.isArray(data.connections) ? data.connections : [];
|
|
417
|
+
// `--provider` picks the first non-revoked row with that provider, whoever
|
|
418
|
+
// may use it, so a shared provider needs `--connection` to be exact.
|
|
419
|
+
const activeByProvider = new Map();
|
|
420
|
+
for (const row of rows) {
|
|
421
|
+
if (row.status === "revoked")
|
|
422
|
+
continue;
|
|
423
|
+
const key = bareProvider(row.provider).toLowerCase();
|
|
424
|
+
activeByProvider.set(key, (activeByProvider.get(key) ?? 0) + 1);
|
|
425
|
+
}
|
|
426
|
+
const apps = viewerAccessKnown
|
|
427
|
+
? rows
|
|
428
|
+
.filter((row) => row.viewerAccess?.usable === true &&
|
|
429
|
+
GATEWAY_SERVED_STATUSES.has(row.status) &&
|
|
430
|
+
gatewayHasProvider(row))
|
|
431
|
+
.map((row) => {
|
|
432
|
+
const providerArg = bareProvider(row.provider);
|
|
433
|
+
const shared = (activeByProvider.get(providerArg.toLowerCase()) ?? 0) > 1;
|
|
434
|
+
return {
|
|
435
|
+
connectionId: row.id,
|
|
436
|
+
name: safeAppName(row.installation?.displayName, providerArg),
|
|
437
|
+
provider: row.provider,
|
|
438
|
+
domain: typeof row.domain === "string" && row.domain ? row.domain : null,
|
|
439
|
+
status: row.status,
|
|
440
|
+
providerArg,
|
|
441
|
+
selector: shared ? `--connection ${row.id}` : `--provider ${providerArg}`,
|
|
442
|
+
};
|
|
443
|
+
})
|
|
444
|
+
.sort((a, b) => compareNames(a.name, b.name))
|
|
445
|
+
: [];
|
|
446
|
+
return { companyUid: data.companyUid ?? companyUid, viewerAccessKnown, apps };
|
|
447
|
+
}
|
|
358
448
|
/**
|
|
359
449
|
* Resolve one connection by `--connection acct_…` or `--provider linear`
|
|
360
450
|
* (matches `factory:<slug>`, bare provider ids, and an installation's human
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
*/
|
|
52
52
|
import { Command } from "commander";
|
|
53
53
|
import { type PendingApproval } from "./integrations-api.js";
|
|
54
|
-
export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
55
|
-
export type { AdminConnection } from "./integrations-core.js";
|
|
54
|
+
export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
55
|
+
export type { AdminConnection, UsableApp, UsableAppsResult } from "./integrations-core.js";
|
|
56
56
|
/** Wait for a decided queue item without resending the approve request. */
|
|
57
57
|
export declare function waitForApprovalTerminal(token: string, companyUid: string, queueId: string, options?: {
|
|
58
58
|
timeoutMs?: number;
|
|
@@ -54,12 +54,12 @@ import chalk from "chalk";
|
|
|
54
54
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
55
55
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
56
56
|
import { redactSensitiveUrl, redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
|
|
57
|
-
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
57
|
+
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, fetchUsableApps, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
58
58
|
import { registerConnectCommands } from "./integrations-connect.js";
|
|
59
59
|
import { registerImportCommands } from "./integrations-import.js";
|
|
60
60
|
import { registerManageCommands } from "./integrations-manage.js";
|
|
61
61
|
import { fetchPendingApprovals } from "./integrations-api.js";
|
|
62
|
-
export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
62
|
+
export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
63
63
|
/**
|
|
64
64
|
* Gateway metadata is additive and may be absent when talking to an older
|
|
65
65
|
* gateway. Parse it defensively so an unknown or malformed value never breaks
|
|
@@ -220,9 +220,33 @@ export function registerIntegrationsCommand(program) {
|
|
|
220
220
|
.description("List the company's connected apps")
|
|
221
221
|
.option("--company <slug>", "Company slug, e.g. indigo (defaults to your single active company)")
|
|
222
222
|
.option("--json", "Machine-readable output")
|
|
223
|
+
.option("--usable", "Only apps you can use right now (connected and shared with you), with the --provider value to call them")
|
|
224
|
+
.option("--no-login", "Fail instead of opening a browser sign-in when there is no HQ session")
|
|
223
225
|
.action(async (opts) => {
|
|
224
|
-
const token = await ensureCognitoIdToken();
|
|
226
|
+
const token = await ensureCognitoIdToken(opts.login === false ? { interactive: false } : {});
|
|
225
227
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
228
|
+
if (opts.usable) {
|
|
229
|
+
const result = await fetchUsableApps(token, companyUid);
|
|
230
|
+
if (opts.json) {
|
|
231
|
+
printJson(result);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!result.viewerAccessKnown) {
|
|
235
|
+
console.log("This HQ server cannot report which apps are shared with you yet. Run `hq integrations list` to see every connected app.");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (result.apps.length === 0) {
|
|
239
|
+
console.log("No connected apps are available to you in this company. Run `hq integrations list` to see what is connected, or ask an admin to share one.");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
for (const app of result.apps) {
|
|
243
|
+
const where = app.domain ? ` (${app.domain})` : "";
|
|
244
|
+
const flag = app.status === "needs-attention" ? chalk.yellow(" needs attention") : "";
|
|
245
|
+
console.log(`${chalk.bold(app.name)}${chalk.dim(where)}${flag}`);
|
|
246
|
+
console.log(chalk.dim(` hq integrations tools ${app.selector}`));
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
226
250
|
const connections = await fetchConnections(token, companyUid);
|
|
227
251
|
if (opts.json) {
|
|
228
252
|
printJson(connections);
|
|
@@ -11,7 +11,8 @@ import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, w
|
|
|
11
11
|
import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
|
|
12
12
|
import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice } from "../lib/lanes/story-switch.js";
|
|
13
13
|
import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
14
|
-
import { keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
14
|
+
import { findRetiredLaneEntry, keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
15
|
+
import { isTerminalLaneState } from "../lib/lanes/types.js";
|
|
15
16
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
16
17
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
17
18
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, callerMatchesSessionSenior, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
@@ -136,6 +137,14 @@ export function createLaneMessageHandlers(deps) {
|
|
|
136
137
|
...(snapshot.hire_grant === undefined ? {} : { hire_grant: snapshot.hire_grant }),
|
|
137
138
|
};
|
|
138
139
|
}
|
|
140
|
+
function activeRetiredRegistryWarning(hqRoot, lane) {
|
|
141
|
+
if (lane.keep_alive !== true || isTerminalLaneState(lane.state))
|
|
142
|
+
return undefined;
|
|
143
|
+
const entry = findRetiredLaneEntry(hqRoot, lane.lane_id);
|
|
144
|
+
if (entry === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
return `The active keep-alive lane record takes precedence over ${entry.path} retired[${entry.index}] (lane ${entry.lane}, retired_at ${entry.retired_at ?? "not recorded"}); remove the stale registry row from workspace/lane-briefs/juniors.json.`;
|
|
147
|
+
}
|
|
139
148
|
async function messageLaneExclusion(ctx, lane, grantContext) {
|
|
140
149
|
if (lane.state === "killed" || lane.state === "cancelled") {
|
|
141
150
|
return { lane, exclusion: lane.state };
|
|
@@ -172,6 +181,14 @@ export function createLaneMessageHandlers(deps) {
|
|
|
172
181
|
lanes = [...merged.values()];
|
|
173
182
|
}
|
|
174
183
|
let exclusion = keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware, companyForLane);
|
|
184
|
+
let retiredRegistryWarning = exclusion === "retired"
|
|
185
|
+
? activeRetiredRegistryWarning(ctx.hqRoot(), currentLane)
|
|
186
|
+
: undefined;
|
|
187
|
+
if (retiredRegistryWarning !== undefined) {
|
|
188
|
+
exclusion = keepAliveSupersededBy(currentLane, lanes, lineageAware, companyForLane) === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: "superseded";
|
|
191
|
+
}
|
|
175
192
|
if (exclusion === "superseded" && currentLane.state !== "done" && currentLane.state !== "interrupted") {
|
|
176
193
|
const runningSupersededMessageDeliveryEnabled = await readLanesRegistryFlag(LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, { company: companyForLane(currentLane), env: ctx.env });
|
|
177
194
|
if (runningSupersededMessageDeliveryEnabled) {
|
|
@@ -203,6 +220,14 @@ export function createLaneMessageHandlers(deps) {
|
|
|
203
220
|
lanes = latestLanes;
|
|
204
221
|
retiredLaneIds = loadRetiredLaneIds(ctx.hqRoot());
|
|
205
222
|
exclusion = keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware, companyForLane);
|
|
223
|
+
retiredRegistryWarning = exclusion === "retired"
|
|
224
|
+
? activeRetiredRegistryWarning(ctx.hqRoot(), currentLane)
|
|
225
|
+
: undefined;
|
|
226
|
+
if (retiredRegistryWarning !== undefined) {
|
|
227
|
+
exclusion = keepAliveSupersededBy(currentLane, lanes, lineageAware, companyForLane) === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: "superseded";
|
|
230
|
+
}
|
|
206
231
|
if (exclusion === "superseded" && currentLane.state !== "done" && currentLane.state !== "interrupted") {
|
|
207
232
|
const callerLaneId = dispatchedContextBound ? dispatchedCallerId : undefined;
|
|
208
233
|
const callerSessionId = dispatchedContextBound
|
|
@@ -222,6 +247,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
222
247
|
? { supersededMessageSessionId: currentLane.senior.id }
|
|
223
248
|
: {}),
|
|
224
249
|
supersededMessageWarning: `A newer lead supersedes this ${currentLane.state} lane; message delivery is allowed while the lane remains active.`,
|
|
250
|
+
...(retiredRegistryWarning === undefined ? {} : { retiredRegistryWarning }),
|
|
225
251
|
};
|
|
226
252
|
}
|
|
227
253
|
}
|
|
@@ -234,6 +260,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
234
260
|
return {
|
|
235
261
|
lane: currentLane,
|
|
236
262
|
exclusion: undefined,
|
|
263
|
+
...(retiredRegistryWarning === undefined ? {} : { retiredRegistryWarning }),
|
|
237
264
|
...(!dispatchedContextAuthorized && ctx.env.HQ_LANE_ID?.trim()
|
|
238
265
|
? { supersededMessageCallerId: ctx.env.HQ_LANE_ID.trim() }
|
|
239
266
|
: {}),
|
|
@@ -245,7 +272,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
245
272
|
: {}),
|
|
246
273
|
};
|
|
247
274
|
}
|
|
248
|
-
return { lane: currentLane, exclusion };
|
|
275
|
+
return { lane: currentLane, exclusion, ...(retiredRegistryWarning === undefined ? {} : { retiredRegistryWarning }) };
|
|
249
276
|
}
|
|
250
277
|
function recordMessageUndeliverable(ctx, lane, reason, messageCount) {
|
|
251
278
|
writeWatcherDecision(watcherOutLogPath(ctx.hqRoot(), lane.lane_id), {
|
|
@@ -450,19 +477,23 @@ export function createLaneMessageHandlers(deps) {
|
|
|
450
477
|
if (opts.account !== undefined && !keepCurrentAccount && !providerSessionPortable(lane.provider)) {
|
|
451
478
|
throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
|
|
452
479
|
}
|
|
453
|
-
const
|
|
454
|
-
? await deps.
|
|
455
|
-
:
|
|
456
|
-
if (
|
|
457
|
-
const
|
|
480
|
+
const pendingQuestions = lane.state === "awaiting_input"
|
|
481
|
+
? await deps.pendingQuestionsForLane(ctx, lane)
|
|
482
|
+
: [];
|
|
483
|
+
if (pendingQuestions.length > 0) {
|
|
484
|
+
const companySlug = lane.company?.slug ?? lane.mesh_company ?? "<company-slug>";
|
|
485
|
+
const instructions = pendingQuestions.map(({ question_id, audience }) => `question ${question_id} awaits ${audience.kind}:${audience.id}; answer it with hq lanes questions answer ${question_id} --company ${companySlug} --freeform <answer>`);
|
|
486
|
+
const message = `Lane ${laneId} is awaiting input. ${instructions.join("; ")}`;
|
|
458
487
|
throw new LanesError("lane_not_accepting_messages", message, {
|
|
459
488
|
lane_id: laneId,
|
|
460
489
|
state: lane.state,
|
|
490
|
+
pending_questions: pendingQuestions,
|
|
491
|
+
answer_commands: pendingQuestions.map(({ question_id }) => `hq lanes questions answer ${question_id} --company ${companySlug} --freeform <answer>`),
|
|
461
492
|
provider: lane.provider,
|
|
462
493
|
message,
|
|
463
494
|
});
|
|
464
495
|
}
|
|
465
|
-
const awaitingInputFallback = lane.state === "awaiting_input"
|
|
496
|
+
const awaitingInputFallback = lane.state === "awaiting_input";
|
|
466
497
|
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
467
498
|
// `none` refuses before anything is written: no inbox append, no spawn.
|
|
468
499
|
const mechanism = laneDeliveryMechanism(lane);
|
|
@@ -915,6 +946,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
915
946
|
: "worker_spawned";
|
|
916
947
|
const warning = [
|
|
917
948
|
eligibility.supersededMessageWarning,
|
|
949
|
+
eligibility.retiredRegistryWarning,
|
|
918
950
|
mechanism === "dropbox" && drainHook !== null && !drainHook.registered
|
|
919
951
|
? `The drain hook is not registered in ${drainHook.path}; the running worker will not receive this message. It stays in the inbox and is delivered on the lane's next start.`
|
|
920
952
|
: mechanism === "dropbox" && next.stalled === true
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { type QuestionBody, type LaneRecord, type QuestionRecord } from "../lib/lanes/index.js";
|
|
1
|
+
import { type QuestionBody, type LaneRecord, type QuestionRecord, type SeniorRef } from "../lib/lanes/index.js";
|
|
2
2
|
import type { Ctx, FallbackQuestionCancellationResult, LaneQuestionsHandlerDeps } from "./lanes-shared.js";
|
|
3
3
|
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
4
4
|
export declare function createLaneQuestionsHandlers(deps: LaneQuestionsHandlerDeps): {
|
|
5
5
|
hasPendingQuestionForLane: (ctx: Ctx, lane: LaneRecord) => Promise<boolean>;
|
|
6
|
+
pendingQuestionsForLane: (ctx: Ctx, lane: LaneRecord) => Promise<Array<{
|
|
7
|
+
question_id: string;
|
|
8
|
+
asked_at: string;
|
|
9
|
+
audience: SeniorRef;
|
|
10
|
+
}>>;
|
|
6
11
|
cancelFallbackQuestionsForLane: (ctx: Ctx, lane: LaneRecord, trigger: "message" | "resume", at: string) => Promise<FallbackQuestionCancellationResult>;
|
|
7
12
|
runQuestionsList: (ctx: Ctx, opts: {
|
|
8
13
|
allHosts?: boolean;
|
|
@@ -488,21 +488,32 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
488
488
|
}
|
|
489
489
|
return { cancelled_question_ids: cancelledQuestionIds, failures };
|
|
490
490
|
}
|
|
491
|
-
async function
|
|
491
|
+
async function pendingQuestionsForLane(ctx, lane) {
|
|
492
492
|
const hqRoot = ctx.hqRoot();
|
|
493
|
-
const local = loadAllQuestions(hqRoot).
|
|
493
|
+
const local = loadAllQuestions(hqRoot).filter((question) => question.lane_id === lane.lane_id &&
|
|
494
494
|
question.status === "pending" &&
|
|
495
495
|
questionAudienceMatchesSenior(resolveAudience(hqRoot, question, ctx), lane, ctx));
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
const localRows = local.map((question) => ({
|
|
497
|
+
question_id: question.question_id,
|
|
498
|
+
asked_at: question.asked_at,
|
|
499
|
+
audience: resolveAudience(hqRoot, question, ctx),
|
|
500
|
+
}));
|
|
498
501
|
if (lane.host.kind !== "outpost" || laneIsHostedHere(ctx, lane))
|
|
499
|
-
return
|
|
502
|
+
return localRows;
|
|
500
503
|
// Reuse the owner lookup used by questions list/answer. An unreachable
|
|
501
504
|
// owner leaves no answerable question, so the message/resume path can recover.
|
|
502
505
|
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
|
|
503
|
-
|
|
506
|
+
const remoteRows = lookup.questions.filter(({ question, lane: ownerLane, audience }) => ownerLane.lane_id === lane.lane_id &&
|
|
504
507
|
question.status === "pending" &&
|
|
505
|
-
questionAudienceMatchesSenior(audience, ownerLane, ctx))
|
|
508
|
+
questionAudienceMatchesSenior(audience, ownerLane, ctx)).map(({ question, audience }) => ({
|
|
509
|
+
question_id: question.question_id,
|
|
510
|
+
asked_at: question.asked_at,
|
|
511
|
+
audience,
|
|
512
|
+
}));
|
|
513
|
+
return [...localRows, ...remoteRows];
|
|
514
|
+
}
|
|
515
|
+
async function hasPendingQuestionForLane(ctx, lane) {
|
|
516
|
+
return (await pendingQuestionsForLane(ctx, lane)).length > 0;
|
|
506
517
|
}
|
|
507
518
|
function syncPrimaryQuestionDelivery(hqRoot, owner) {
|
|
508
519
|
const deliveredAt = owner.question.delivered_at;
|
|
@@ -991,6 +1002,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
991
1002
|
}
|
|
992
1003
|
return {
|
|
993
1004
|
hasPendingQuestionForLane,
|
|
1005
|
+
pendingQuestionsForLane,
|
|
994
1006
|
cancelFallbackQuestionsForLane,
|
|
995
1007
|
runQuestionsList,
|
|
996
1008
|
runQuestionsShow,
|
|
@@ -181,9 +181,20 @@ export function createLaneResumeHandlers(deps) {
|
|
|
181
181
|
opts.compact === false)) {
|
|
182
182
|
throw new LanesError("worker_profile_recovery_options_invalid", "Queued worker-profile recovery accepts only --worker; rerun hq lanes resume after reconcile admits the lane", { lane_id: laneId });
|
|
183
183
|
}
|
|
184
|
-
const
|
|
185
|
-
? await deps.
|
|
186
|
-
:
|
|
184
|
+
const pendingQuestions = lane.state === "awaiting_input"
|
|
185
|
+
? await deps.pendingQuestionsForLane(ctx, lane)
|
|
186
|
+
: [];
|
|
187
|
+
if (pendingQuestions.length > 0) {
|
|
188
|
+
const companySlug = lane.company?.slug ?? lane.mesh_company ?? "<company-slug>";
|
|
189
|
+
const instructions = pendingQuestions.map(({ question_id, audience }) => `question ${question_id} awaits ${audience.kind}:${audience.id}; answer it with hq lanes questions answer ${question_id} --company ${companySlug} --freeform <answer>`);
|
|
190
|
+
const message = `Lane ${laneId} is awaiting input. ${instructions.join("; ")}`;
|
|
191
|
+
throw new LanesError("lane_pending_questions", message, {
|
|
192
|
+
lane_id: laneId,
|
|
193
|
+
state: lane.state,
|
|
194
|
+
pending_questions: pendingQuestions,
|
|
195
|
+
answer_commands: pendingQuestions.map(({ question_id }) => `hq lanes questions answer ${question_id} --company ${companySlug} --freeform <answer>`),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
187
198
|
const projectId = opts.project?.trim() || lane.project_id;
|
|
188
199
|
const storyId = opts.story?.trim() || lane.story_id;
|
|
189
200
|
if (opts.story !== undefined && !opts.story.trim()) {
|
|
@@ -206,7 +217,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
206
217
|
throw new LanesError("profile_provider_changed", `Worker profile now selects ${effectiveProvider}; this lane runs ${lane.provider}; create a new lane`, { lane_id: lane.lane_id, lane_provider: lane.provider, profile_provider: effectiveProvider });
|
|
207
218
|
}
|
|
208
219
|
}
|
|
209
|
-
const refusal = queuedProfileRecovery ||
|
|
220
|
+
const refusal = queuedProfileRecovery || lane.state === "awaiting_input"
|
|
210
221
|
? null
|
|
211
222
|
: resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
|
|
212
223
|
if (refusal !== null) {
|
|
@@ -510,7 +521,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
510
521
|
trigger: {
|
|
511
522
|
kind: "resume",
|
|
512
523
|
from: resumedFrom,
|
|
513
|
-
...(resumedFrom === "awaiting_input" &&
|
|
524
|
+
...(resumedFrom === "awaiting_input" && pendingQuestions.length === 0 ? { allowAwaitingInput: true } : {}),
|
|
514
525
|
...(opts.fromFailed === true ? { fromFailed: true } : {}),
|
|
515
526
|
...(failedResumeExpectedSenior !== undefined
|
|
516
527
|
? { expectedSenior: failedResumeExpectedSenior }
|
|
@@ -527,7 +538,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
527
538
|
}, hireGrantContext);
|
|
528
539
|
}, ctx.sleep);
|
|
529
540
|
}
|
|
530
|
-
const questionCancellation = resumedFrom === "awaiting_input" &&
|
|
541
|
+
const questionCancellation = resumedFrom === "awaiting_input" && pendingQuestions.length === 0
|
|
531
542
|
? await deps.cancelFallbackQuestionsForLane(ctx, lane, "resume", at)
|
|
532
543
|
: null;
|
|
533
544
|
if (deliveryText !== undefined && messageId !== undefined) {
|