@indigoai-us/hq-cli 5.191.0 → 5.192.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 +18 -3
- package/dist/command-catalog.generated.js +24 -3
- package/dist/commands/agents.d.ts +3 -0
- package/dist/commands/agents.js +95 -3
- package/dist/commands/report.js +44 -1
- package/dist/lib/agents/model-catalog.d.ts +61 -0
- package/dist/lib/agents/model-catalog.js +275 -0
- package/dist/lib/report-publish.d.ts +3 -2
- package/dist/lib/report-publish.js +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.192.0] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq agents models` lists cloud-agent catalog entries by provider and supports JSON output.
|
|
10
|
+
Provision and update validate model IDs against the catalog. They use the cached
|
|
11
|
+
catalog when the endpoint is unavailable, and rely on server validation if no cache exists.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `hq report adopt` checks whether scheduled jobs are enabled for the company before creating a cloud job, and explains when the server rejects a disabled job.
|
|
16
|
+
|
|
5
17
|
## [5.191.0] — 2026-09-25
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
@@ -3845,6 +3845,21 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3845
3845
|
readonly description: "Company slug (resolves to companyUid)";
|
|
3846
3846
|
}];
|
|
3847
3847
|
readonly subcommands: readonly [{
|
|
3848
|
+
readonly name: "models";
|
|
3849
|
+
readonly description: "List the model catalog available to cloud agents";
|
|
3850
|
+
readonly aliases: readonly [];
|
|
3851
|
+
readonly hidden: false;
|
|
3852
|
+
readonly usage: "[options]";
|
|
3853
|
+
readonly arguments: readonly [];
|
|
3854
|
+
readonly options: readonly [{
|
|
3855
|
+
readonly flags: "--provider <provider>";
|
|
3856
|
+
readonly description: "Filter to one provider: codex, grok, or claude";
|
|
3857
|
+
}, {
|
|
3858
|
+
readonly flags: "--json";
|
|
3859
|
+
readonly description: "Print the stable catalog JSON shape";
|
|
3860
|
+
}];
|
|
3861
|
+
readonly subcommands: readonly [];
|
|
3862
|
+
}, {
|
|
3848
3863
|
readonly name: "message";
|
|
3849
3864
|
readonly description: "Send a message to an agent and wait for its reply";
|
|
3850
3865
|
readonly aliases: readonly ["msg"];
|
|
@@ -3939,7 +3954,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3939
3954
|
readonly description: "Runtime: agents-v2 (recommended) | codex | grok | claude (accepted for script compatibility; use agents-v2 for new Claude agents)";
|
|
3940
3955
|
}, {
|
|
3941
3956
|
readonly flags: "--model <model>";
|
|
3942
|
-
readonly description: "Brain model
|
|
3957
|
+
readonly description: "Brain model from `hq agents models`";
|
|
3943
3958
|
}, {
|
|
3944
3959
|
readonly flags: "--auth-mode <mode>";
|
|
3945
3960
|
readonly description: "Auth: subscription | apiKey (default subscription)";
|
|
@@ -4074,7 +4089,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4074
4089
|
}, {
|
|
4075
4090
|
readonly name: "config";
|
|
4076
4091
|
readonly description: "Update an agent's runtime config (model / reasoning effort / service tier), or migrate its brain/runtime provider with --provider (DESTRUCTIVE: terminates + reprovisions the box)";
|
|
4077
|
-
readonly aliases: readonly [];
|
|
4092
|
+
readonly aliases: readonly ["update"];
|
|
4078
4093
|
readonly hidden: false;
|
|
4079
4094
|
readonly usage: "[options] <agentUid>";
|
|
4080
4095
|
readonly arguments: readonly [{
|
|
@@ -4087,7 +4102,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4087
4102
|
readonly description: "Company slug (resolves to companyUid)";
|
|
4088
4103
|
}, {
|
|
4089
4104
|
readonly flags: "--model <model>";
|
|
4090
|
-
readonly description: "
|
|
4105
|
+
readonly description: "Model id from `hq agents models`";
|
|
4091
4106
|
}, {
|
|
4092
4107
|
readonly flags: "--effort <effort>";
|
|
4093
4108
|
readonly description: "Reasoning effort: minimal | low | medium | high | xhigh";
|
|
@@ -4961,6 +4961,25 @@ export const COMMAND_CATALOG = [
|
|
|
4961
4961
|
}
|
|
4962
4962
|
],
|
|
4963
4963
|
"subcommands": [
|
|
4964
|
+
{
|
|
4965
|
+
"name": "models",
|
|
4966
|
+
"description": "List the model catalog available to cloud agents",
|
|
4967
|
+
"aliases": [],
|
|
4968
|
+
"hidden": false,
|
|
4969
|
+
"usage": "[options]",
|
|
4970
|
+
"arguments": [],
|
|
4971
|
+
"options": [
|
|
4972
|
+
{
|
|
4973
|
+
"flags": "--provider <provider>",
|
|
4974
|
+
"description": "Filter to one provider: codex, grok, or claude"
|
|
4975
|
+
},
|
|
4976
|
+
{
|
|
4977
|
+
"flags": "--json",
|
|
4978
|
+
"description": "Print the stable catalog JSON shape"
|
|
4979
|
+
}
|
|
4980
|
+
],
|
|
4981
|
+
"subcommands": []
|
|
4982
|
+
},
|
|
4964
4983
|
{
|
|
4965
4984
|
"name": "message",
|
|
4966
4985
|
"description": "Send a message to an agent and wait for its reply",
|
|
@@ -5092,7 +5111,7 @@ export const COMMAND_CATALOG = [
|
|
|
5092
5111
|
},
|
|
5093
5112
|
{
|
|
5094
5113
|
"flags": "--model <model>",
|
|
5095
|
-
"description": "Brain model
|
|
5114
|
+
"description": "Brain model from `hq agents models`"
|
|
5096
5115
|
},
|
|
5097
5116
|
{
|
|
5098
5117
|
"flags": "--auth-mode <mode>",
|
|
@@ -5268,7 +5287,9 @@ export const COMMAND_CATALOG = [
|
|
|
5268
5287
|
{
|
|
5269
5288
|
"name": "config",
|
|
5270
5289
|
"description": "Update an agent's runtime config (model / reasoning effort / service tier), or migrate its brain/runtime provider with --provider (DESTRUCTIVE: terminates + reprovisions the box)",
|
|
5271
|
-
"aliases": [
|
|
5290
|
+
"aliases": [
|
|
5291
|
+
"update"
|
|
5292
|
+
],
|
|
5272
5293
|
"hidden": false,
|
|
5273
5294
|
"usage": "[options] <agentUid>",
|
|
5274
5295
|
"arguments": [
|
|
@@ -5285,7 +5306,7 @@ export const COMMAND_CATALOG = [
|
|
|
5285
5306
|
},
|
|
5286
5307
|
{
|
|
5287
5308
|
"flags": "--model <model>",
|
|
5288
|
-
"description": "
|
|
5309
|
+
"description": "Model id from `hq agents models`"
|
|
5289
5310
|
},
|
|
5290
5311
|
{
|
|
5291
5312
|
"flags": "--effort <effort>",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { Command } from "commander";
|
|
30
30
|
import { type TerminalSessionPayload } from "@indigoai-us/hq-cloud/outposts/node";
|
|
31
|
+
import { type AgentModelProvider, type AgentModelCatalog } from "../lib/agents/model-catalog.js";
|
|
31
32
|
import { type BillingErrorPayload } from "../utils/billing-gate.js";
|
|
32
33
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
33
34
|
export declare const VALID_EFFORTS: Set<string>;
|
|
@@ -341,6 +342,8 @@ export declare function formatJobsTable(jobs: AgentJobView[]): string;
|
|
|
341
342
|
export declare function formatPauseResult(result: PauseJobResult): string;
|
|
342
343
|
/** Human-readable "hot-applied to the running box" vs "saved for next launch". */
|
|
343
344
|
export declare function appliedHint(applied: boolean): string;
|
|
345
|
+
/** Stable human table shared by the all-provider and provider-filtered forms. */
|
|
346
|
+
export declare function formatAgentModelsTable(catalog: AgentModelCatalog, selectedProvider?: AgentModelProvider): string;
|
|
344
347
|
/** Agent uid shape (`agt_…`). */
|
|
345
348
|
/** One message in a DM thread, as returned by `GET /v1/notify/thread`. */
|
|
346
349
|
export interface DmThreadMessage {
|
package/dist/commands/agents.js
CHANGED
|
@@ -38,6 +38,7 @@ import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSe
|
|
|
38
38
|
import { peekIdToken } from "../utils/id-token.js";
|
|
39
39
|
import { canOfferTeamUpgrade, formatUpgradeRefusal, isPlanGateError, offerUpgradeLink, } from "../utils/plan-gate-error.js";
|
|
40
40
|
import { AGENT_UID_PATTERN, isAgentUid } from "../lib/notify-recipient.js";
|
|
41
|
+
import { AGENT_MODEL_PROVIDERS, modelSelectionErrorMessage, readAgentModelCatalog, validateAgentModelSelection, } from "../lib/agents/model-catalog.js";
|
|
41
42
|
import { confirmChargeOrExit, formatUsd, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
|
|
42
43
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
43
44
|
export const VALID_EFFORTS = new Set([
|
|
@@ -57,11 +58,12 @@ export const VALID_AUTH_MODES = new Set(["subscription", "apiKey"]);
|
|
|
57
58
|
export const VALID_AGENT_SIZE_KEYS = new Set(["basic", "power", "dev"]);
|
|
58
59
|
// Keep cloud agent create slugs aligned with hq-pro-agents POST /v1/agents.
|
|
59
60
|
const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
60
|
-
const CLAUDE_PROVISION_COMMAND = "hq agents provision <name> --provider agents-v2 --model
|
|
61
|
+
const CLAUDE_PROVISION_COMMAND = "hq agents provision <name> --provider agents-v2 --model <model-id>";
|
|
61
62
|
const CLAUDE_API_KEY_ARGS = "--auth-mode apiKey --api-key-env <VAR>";
|
|
62
63
|
function claudeProvisionGuidance() {
|
|
63
64
|
return [
|
|
64
65
|
"The account must be enabled for Claude. Subscription auth is the default.",
|
|
66
|
+
"Use `hq agents models` to see the current model ids.",
|
|
65
67
|
`Create an agent with: ${CLAUDE_PROVISION_COMMAND}`,
|
|
66
68
|
`For API-key auth, add: ${CLAUDE_API_KEY_ARGS}`,
|
|
67
69
|
"The CLI accepts --provider claude for script compatibility, but the server rejects new agents that use it.",
|
|
@@ -863,6 +865,50 @@ export function appliedHint(applied) {
|
|
|
863
865
|
? "applied to the running box now"
|
|
864
866
|
: "saved — takes effect on the agent's next launch";
|
|
865
867
|
}
|
|
868
|
+
async function validateAgentModelOption(token, chosenProvider, modelId) {
|
|
869
|
+
const result = await readAgentModelCatalog(token);
|
|
870
|
+
if (!result.ok) {
|
|
871
|
+
if (result.reason === "unavailable") {
|
|
872
|
+
console.error(chalk.yellow("model validation skipped: catalog unavailable (the server still validates)."));
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
throw new Error(`Could not read the model catalog (HTTP ${result.status}). Check your hq login and try again.`);
|
|
876
|
+
}
|
|
877
|
+
if (result.source === "cache") {
|
|
878
|
+
console.error(chalk.yellow(`Model catalog unavailable; validating against the cached catalog from ${new Date(result.fetchedAt).toISOString()}.`));
|
|
879
|
+
}
|
|
880
|
+
const selection = validateAgentModelSelection(result.catalog, modelId, chosenProvider);
|
|
881
|
+
if (!selection.ok) {
|
|
882
|
+
throw new Error(modelSelectionErrorMessage(modelId, selection));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/** Stable human table shared by the all-provider and provider-filtered forms. */
|
|
886
|
+
export function formatAgentModelsTable(catalog, selectedProvider) {
|
|
887
|
+
const providers = selectedProvider ? [selectedProvider] : AGENT_MODEL_PROVIDERS;
|
|
888
|
+
const sections = providers.map((provider) => {
|
|
889
|
+
const view = catalog.providers[provider];
|
|
890
|
+
const columns = ["ID", "LABEL", "DEFAULT", "NEW", "AVAILABILITY"];
|
|
891
|
+
const rows = view.models.map((model) => [
|
|
892
|
+
model.id,
|
|
893
|
+
model.label,
|
|
894
|
+
model.id === view.defaultModel ? "yes" : "",
|
|
895
|
+
model.isNew ? "yes" : "",
|
|
896
|
+
model.selectable ? "available" : "not yet on agents",
|
|
897
|
+
]);
|
|
898
|
+
const widths = columns.map((column, index) => Math.max(column.length, ...rows.map((row) => row[index].length)));
|
|
899
|
+
const render = (cells) => cells.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd();
|
|
900
|
+
return [
|
|
901
|
+
chalk.bold(provider.toUpperCase()),
|
|
902
|
+
render(columns),
|
|
903
|
+
...rows.map(render),
|
|
904
|
+
].join("\n");
|
|
905
|
+
});
|
|
906
|
+
return sections.join("\n\n");
|
|
907
|
+
}
|
|
908
|
+
function catalogUnavailableMessage() {
|
|
909
|
+
return ("Model catalog is unavailable and no cached copy exists. " +
|
|
910
|
+
"Check your connection and try again after model-catalog.serve is enabled.");
|
|
911
|
+
}
|
|
866
912
|
/**
|
|
867
913
|
* Resolve an agent reference to its `agt_` uid. An `agt_…` value is used
|
|
868
914
|
* directly; anything else is treated as a slug or display name and resolved
|
|
@@ -1097,6 +1143,8 @@ export function registerAgentsCommand(program) {
|
|
|
1097
1143
|
bySubcommand: {
|
|
1098
1144
|
message: { capability: "agents:use", routeAvailable: false },
|
|
1099
1145
|
thread: { capability: "agents:use", routeAvailable: false },
|
|
1146
|
+
// The catalog read endpoint is JWT-only; hq-pro has no keyed parallel.
|
|
1147
|
+
models: { capability: "agents:use", routeAvailable: false },
|
|
1100
1148
|
// Interactive terminals are for humans: hq-pro serves no keyed
|
|
1101
1149
|
// `/v1/keys/agents/{uid}/terminal` route, so machine keys fail closed.
|
|
1102
1150
|
terminal: { capability: "agents:use", routeAvailable: false },
|
|
@@ -1112,6 +1160,43 @@ export function registerAgentsCommand(program) {
|
|
|
1112
1160
|
// `hq agents --company acme list`.
|
|
1113
1161
|
const companyOf = (sub) => sub.opts().company ??
|
|
1114
1162
|
agents.opts().company;
|
|
1163
|
+
agents
|
|
1164
|
+
.command("models")
|
|
1165
|
+
.description("List the model catalog available to cloud agents")
|
|
1166
|
+
.option("--provider <provider>", "Filter to one provider: codex, grok, or claude")
|
|
1167
|
+
.option("--json", "Print the stable catalog JSON shape")
|
|
1168
|
+
.action(async (opts) => {
|
|
1169
|
+
try {
|
|
1170
|
+
const token = (await resolveVaultCredential()).token;
|
|
1171
|
+
const result = await readAgentModelCatalog(token);
|
|
1172
|
+
if (!result.ok) {
|
|
1173
|
+
if (result.reason === "request_failed") {
|
|
1174
|
+
throw new Error(`Could not read the model catalog (HTTP ${result.status}). Check your hq login and try again.`);
|
|
1175
|
+
}
|
|
1176
|
+
console.error(chalk.red(catalogUnavailableMessage()));
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
if (result.source === "cache") {
|
|
1180
|
+
console.error(chalk.yellow(`Model catalog unavailable; showing cached catalog from ${new Date(result.fetchedAt).toISOString()}.`));
|
|
1181
|
+
}
|
|
1182
|
+
const providers = opts.provider
|
|
1183
|
+
? { [opts.provider]: result.catalog.providers[opts.provider] }
|
|
1184
|
+
: result.catalog.providers;
|
|
1185
|
+
if (opts.json) {
|
|
1186
|
+
// Stable shape: { generatedAt, providers }. A provider filter selects
|
|
1187
|
+
// one key but never changes or flattens the response envelope.
|
|
1188
|
+
process.stdout.write(JSON.stringify({ generatedAt: result.catalog.generatedAt, providers }, null, 2) + "\n");
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
console.log(formatAgentModelsTable(result.catalog, opts.provider));
|
|
1192
|
+
}
|
|
1193
|
+
catch (err) {
|
|
1194
|
+
fail(err);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
agents.commands.find((command) => command.name() === "models")
|
|
1198
|
+
?.options.find((option) => option.long === "--provider")
|
|
1199
|
+
?.choices(AGENT_MODEL_PROVIDERS);
|
|
1115
1200
|
agents
|
|
1116
1201
|
.command("message <agent> <message...>")
|
|
1117
1202
|
.alias("msg")
|
|
@@ -1269,7 +1354,7 @@ export function registerAgentsCommand(program) {
|
|
|
1269
1354
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1270
1355
|
.option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
|
|
1271
1356
|
.option("--provider <provider>", "Runtime: agents-v2 (recommended) | codex | grok | claude (accepted for script compatibility; use agents-v2 for new Claude agents)")
|
|
1272
|
-
.option("--model <model>", "Brain model
|
|
1357
|
+
.option("--model <model>", "Brain model from `hq agents models`")
|
|
1273
1358
|
.option("--auth-mode <mode>", "Auth: subscription | apiKey (default subscription)", "subscription")
|
|
1274
1359
|
.option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
|
|
1275
1360
|
.option("--slack-bot-token <xoxb-token>", "Reuse an existing Slack bot token; HQ_SLACK_BOT_TOKEN or --slack-tokens-stdin avoid shell history")
|
|
@@ -1334,6 +1419,9 @@ export function registerAgentsCommand(program) {
|
|
|
1334
1419
|
}
|
|
1335
1420
|
const token = (await resolveVaultCredential()).token;
|
|
1336
1421
|
const companyUid = await getCompanyUid(token, companyOf(this));
|
|
1422
|
+
if (opts.model !== undefined) {
|
|
1423
|
+
await validateAgentModelOption(token, provider, opts.model);
|
|
1424
|
+
}
|
|
1337
1425
|
const idempotencyKey = `hq-cli-${randomUUID()}`;
|
|
1338
1426
|
const sizeKey = parseEnumOption(opts.size, VALID_AGENT_SIZE_KEYS, "--size");
|
|
1339
1427
|
const shouldChooseInteractively = !sizeKey && process.stdin.isTTY === true;
|
|
@@ -1636,9 +1724,10 @@ export function registerAgentsCommand(program) {
|
|
|
1636
1724
|
});
|
|
1637
1725
|
agents
|
|
1638
1726
|
.command("config <agentUid>")
|
|
1727
|
+
.alias("update")
|
|
1639
1728
|
.description("Update an agent's runtime config (model / reasoning effort / service tier), or migrate its brain/runtime provider with --provider (DESTRUCTIVE: terminates + reprovisions the box)")
|
|
1640
1729
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1641
|
-
.option("--model <model>", "
|
|
1730
|
+
.option("--model <model>", "Model id from `hq agents models`")
|
|
1642
1731
|
.option("--effort <effort>", "Reasoning effort: minimal | low | medium | high | xhigh")
|
|
1643
1732
|
.option("--tier <tier>", "Service tier (speed): default | priority")
|
|
1644
1733
|
.option("--provider <provider>", "Migrate brain/runtime provider (codex | grok | claude | agents-v2). DESTRUCTIVE: terminates and reprovisions the box; requires --model and --yes.")
|
|
@@ -1707,6 +1796,9 @@ export function registerAgentsCommand(program) {
|
|
|
1707
1796
|
process.exit(1);
|
|
1708
1797
|
}
|
|
1709
1798
|
const token = (await resolveVaultCredential()).token;
|
|
1799
|
+
if (opts.model !== undefined) {
|
|
1800
|
+
await validateAgentModelOption(token, opts.provider?.trim().toLowerCase(), opts.model);
|
|
1801
|
+
}
|
|
1710
1802
|
const result = await patchAgentRuntimeConfig(token, agentUid, patch);
|
|
1711
1803
|
if (opts.json) {
|
|
1712
1804
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
package/dist/commands/report.js
CHANGED
|
@@ -9,6 +9,7 @@ import { adoptReport, buildReportTarball, conventionReportDir, deployOrgContext,
|
|
|
9
9
|
const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL ?? "https://api.indigo-hq.com";
|
|
10
10
|
const REPORT_CHANNEL_STATE_FILE = "report-channels.json";
|
|
11
11
|
const MAX_REPORT_DISCOVERY_CHANNELS = 50;
|
|
12
|
+
const JOB_EXECUTION_DISABLED_MESSAGE = "Scheduled jobs are turned off for this company. A company owner can turn them back on.";
|
|
12
13
|
function reportPath(channelId, reportId) {
|
|
13
14
|
return `/v1/notify/channels/${encodeURIComponent(channelId)}/reports/${encodeURIComponent(reportId)}`;
|
|
14
15
|
}
|
|
@@ -414,10 +415,39 @@ async function cancelOwnedJob(token, agentUid, jobId) {
|
|
|
414
415
|
if (!res.ok && res.status !== 404)
|
|
415
416
|
throw new ReportPublishError("Could not remove the report job.");
|
|
416
417
|
}
|
|
418
|
+
async function requireScheduledJobsEnabled(token, agentUid) {
|
|
419
|
+
let executionEnabled;
|
|
420
|
+
try {
|
|
421
|
+
const res = await vaultApiFetch({
|
|
422
|
+
token,
|
|
423
|
+
path: `/v1/agents/${encodeURIComponent(agentUid)}/jobs`,
|
|
424
|
+
method: "GET",
|
|
425
|
+
});
|
|
426
|
+
if (!res.ok) {
|
|
427
|
+
console.debug("Scheduled job preflight was unavailable; proceeding with job creation.", { status: res.status });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const body = await res.json();
|
|
431
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
432
|
+
executionEnabled = body.executionEnabled;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
console.debug("Scheduled job preflight failed; proceeding with job creation.", error);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (executionEnabled === false) {
|
|
440
|
+
throw new ReportPublishError(JOB_EXECUTION_DISABLED_MESSAGE, "job-execution-disabled");
|
|
441
|
+
}
|
|
442
|
+
if (executionEnabled !== true) {
|
|
443
|
+
console.debug("Scheduled job preflight did not return a boolean executionEnabled; proceeding with job creation.");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
417
446
|
async function adoptAction(reportId, options) {
|
|
418
447
|
const token = await ensureCognitoToken();
|
|
419
448
|
const report = await resolveReport(token, reportId, options.channel);
|
|
420
449
|
const updated = await adoptReport(report, {
|
|
450
|
+
preflightJobCreation: () => requireScheduledJobsEnabled(token, report.ownerAgentUid),
|
|
421
451
|
cancelJob: (jobId) => cancelOwnedJob(token, report.ownerAgentUid, jobId),
|
|
422
452
|
createJob: async (input) => {
|
|
423
453
|
const res = await vaultApiFetch({
|
|
@@ -426,8 +456,21 @@ async function adoptAction(reportId, options) {
|
|
|
426
456
|
method: "POST",
|
|
427
457
|
body: input,
|
|
428
458
|
});
|
|
429
|
-
if (!res.ok)
|
|
459
|
+
if (!res.ok) {
|
|
460
|
+
if (res.status === 409) {
|
|
461
|
+
let rejection;
|
|
462
|
+
try {
|
|
463
|
+
rejection = await res.json();
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// A non-JSON conflict response is still the generic install failure.
|
|
467
|
+
}
|
|
468
|
+
if (rejection?.code === "JOB_EXECUTION_DISABLED") {
|
|
469
|
+
throw new ReportPublishError(JOB_EXECUTION_DISABLED_MESSAGE, "job-execution-disabled");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
430
472
|
throw new ReportPublishError("Could not install the report job.");
|
|
473
|
+
}
|
|
431
474
|
const body = await res.json();
|
|
432
475
|
const jobId = body.job?.jobId ?? body.jobId;
|
|
433
476
|
if (!jobId)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare const AGENT_MODEL_PROVIDERS: readonly ["codex", "grok", "claude"];
|
|
2
|
+
export type AgentModelProvider = (typeof AGENT_MODEL_PROVIDERS)[number];
|
|
3
|
+
export interface AgentCatalogModel {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
visibility: string;
|
|
7
|
+
isNew: boolean;
|
|
8
|
+
authModes: string[];
|
|
9
|
+
efforts: string[];
|
|
10
|
+
selectable: boolean;
|
|
11
|
+
unavailableReason?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface AgentCatalogProvider {
|
|
14
|
+
models: AgentCatalogModel[];
|
|
15
|
+
defaultModel: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AgentModelCatalog {
|
|
18
|
+
providers: Record<AgentModelProvider, AgentCatalogProvider>;
|
|
19
|
+
generatedAt: string;
|
|
20
|
+
}
|
|
21
|
+
export type AgentModelCatalogResult = {
|
|
22
|
+
ok: true;
|
|
23
|
+
catalog: AgentModelCatalog;
|
|
24
|
+
etag: string;
|
|
25
|
+
fetchedAt: number;
|
|
26
|
+
source: "network" | "cache";
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
reason: "unavailable";
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: "request_failed";
|
|
33
|
+
status: number;
|
|
34
|
+
};
|
|
35
|
+
export interface ReadAgentModelCatalogOptions {
|
|
36
|
+
baseUrl?: string;
|
|
37
|
+
cacheFile?: string;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
request?: (headers: Record<string, string>, signal: AbortSignal) => Promise<Response>;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
/** Use the same effective state directory as the CLI's person-token cache. */
|
|
43
|
+
export declare function agentModelCatalogCacheFile(baseUrl?: string, home?: string, env?: NodeJS.ProcessEnv): string;
|
|
44
|
+
/** Parse the documented response without constraining forward-compatible string values. */
|
|
45
|
+
export declare function parseAgentModelCatalog(value: unknown): AgentModelCatalog | null;
|
|
46
|
+
/** Read the live endpoint, conditionally refresh it, and fall back to the last good cache. */
|
|
47
|
+
export declare function readAgentModelCatalog(token: string, options?: ReadAgentModelCatalogOptions): Promise<AgentModelCatalogResult>;
|
|
48
|
+
export type ModelSelectionResult = {
|
|
49
|
+
ok: true;
|
|
50
|
+
} | {
|
|
51
|
+
ok: false;
|
|
52
|
+
reason: "unknown" | "not_selectable";
|
|
53
|
+
provider?: AgentModelProvider;
|
|
54
|
+
suggestions: string[];
|
|
55
|
+
};
|
|
56
|
+
/** Check an explicit model against the selected provider or inferred agents-v2 brain. */
|
|
57
|
+
export declare function validateAgentModelSelection(catalog: AgentModelCatalog, modelId: string, chosenProvider?: string): ModelSelectionResult;
|
|
58
|
+
export declare function modelSelectionErrorMessage(modelId: string, result: Extract<ModelSelectionResult, {
|
|
59
|
+
ok: false;
|
|
60
|
+
}>): string;
|
|
61
|
+
//# sourceMappingURL=model-catalog.d.ts.map
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { DEFAULT_VAULT_API_URL, personTokenCacheFile } from "../../utils/cognito-session.js";
|
|
6
|
+
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
7
|
+
export const AGENT_MODEL_PROVIDERS = ["codex", "grok", "claude"];
|
|
8
|
+
const MODEL_CATALOG_REQUEST_TIMEOUT_MS = 15_000;
|
|
9
|
+
/** Use the same effective state directory as the CLI's person-token cache. */
|
|
10
|
+
export function agentModelCatalogCacheFile(baseUrl = DEFAULT_VAULT_API_URL, home = os.homedir(), env = process.env) {
|
|
11
|
+
const scope = createHash("sha256").update(baseUrl).digest("hex").slice(0, 16);
|
|
12
|
+
return path.join(path.dirname(personTokenCacheFile(home, env)), `agent-model-catalog-${scope}.json`);
|
|
13
|
+
}
|
|
14
|
+
/** Parse the documented response without constraining forward-compatible string values. */
|
|
15
|
+
export function parseAgentModelCatalog(value) {
|
|
16
|
+
if (!isRecord(value) || !isRecord(value.providers))
|
|
17
|
+
return null;
|
|
18
|
+
if (typeof value.generatedAt !== "string" || !Number.isFinite(Date.parse(value.generatedAt))) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const providers = {};
|
|
22
|
+
for (const provider of AGENT_MODEL_PROVIDERS) {
|
|
23
|
+
const rawProvider = value.providers[provider];
|
|
24
|
+
if (!isRecord(rawProvider) ||
|
|
25
|
+
!Array.isArray(rawProvider.models) ||
|
|
26
|
+
typeof rawProvider.defaultModel !== "string") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const models = [];
|
|
30
|
+
for (const rawModel of rawProvider.models) {
|
|
31
|
+
if (!isRecord(rawModel) ||
|
|
32
|
+
typeof rawModel.id !== "string" ||
|
|
33
|
+
!/^[A-Za-z0-9._-]{1,128}$/.test(rawModel.id) ||
|
|
34
|
+
typeof rawModel.label !== "string" ||
|
|
35
|
+
typeof rawModel.visibility !== "string" ||
|
|
36
|
+
typeof rawModel.isNew !== "boolean" ||
|
|
37
|
+
!isStringArray(rawModel.authModes) ||
|
|
38
|
+
!isStringArray(rawModel.efforts) ||
|
|
39
|
+
typeof rawModel.selectable !== "boolean" ||
|
|
40
|
+
(rawModel.unavailableReason !== undefined &&
|
|
41
|
+
rawModel.unavailableReason !== null &&
|
|
42
|
+
typeof rawModel.unavailableReason !== "string")) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
models.push({
|
|
46
|
+
id: rawModel.id,
|
|
47
|
+
label: rawModel.label,
|
|
48
|
+
visibility: rawModel.visibility,
|
|
49
|
+
isNew: rawModel.isNew,
|
|
50
|
+
authModes: rawModel.authModes,
|
|
51
|
+
efforts: rawModel.efforts,
|
|
52
|
+
selectable: rawModel.selectable,
|
|
53
|
+
...(rawModel.unavailableReason !== undefined
|
|
54
|
+
? { unavailableReason: rawModel.unavailableReason }
|
|
55
|
+
: {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!models.some((model) => model.id === rawProvider.defaultModel && model.selectable)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
providers[provider] = { models, defaultModel: rawProvider.defaultModel };
|
|
62
|
+
}
|
|
63
|
+
return { providers, generatedAt: value.generatedAt };
|
|
64
|
+
}
|
|
65
|
+
/** Read the live endpoint, conditionally refresh it, and fall back to the last good cache. */
|
|
66
|
+
export async function readAgentModelCatalog(token, options = {}) {
|
|
67
|
+
const baseUrl = options.baseUrl ?? DEFAULT_VAULT_API_URL;
|
|
68
|
+
const cacheFile = options.cacheFile ?? agentModelCatalogCacheFile(baseUrl);
|
|
69
|
+
const now = options.now ?? Date.now;
|
|
70
|
+
const signal = AbortSignal.timeout(options.timeoutMs ?? MODEL_CATALOG_REQUEST_TIMEOUT_MS);
|
|
71
|
+
const cached = readCache(cacheFile);
|
|
72
|
+
const headers = cached
|
|
73
|
+
? { "If-None-Match": cached.etag }
|
|
74
|
+
: {};
|
|
75
|
+
let response;
|
|
76
|
+
try {
|
|
77
|
+
response = options.request
|
|
78
|
+
? await options.request(headers, signal)
|
|
79
|
+
: await vaultApiFetch({ token, baseUrl, path: "/v1/agents/models", headers, signal });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return cached ? fromCache(cached) : { ok: false, reason: "unavailable" };
|
|
83
|
+
}
|
|
84
|
+
if (response.status === 304) {
|
|
85
|
+
if (!cached)
|
|
86
|
+
return { ok: false, reason: "unavailable" };
|
|
87
|
+
const refreshed = {
|
|
88
|
+
...cached,
|
|
89
|
+
etag: response.headers.get("etag") ?? cached.etag,
|
|
90
|
+
fetchedAt: now(),
|
|
91
|
+
};
|
|
92
|
+
if (isStrongEtag(refreshed.etag))
|
|
93
|
+
writeCache(cacheFile, refreshed);
|
|
94
|
+
return fromCache(refreshed, "network");
|
|
95
|
+
}
|
|
96
|
+
if (response.status === 404 || response.status === 429 || response.status >= 500) {
|
|
97
|
+
return cached ? fromCache(cached) : { ok: false, reason: "unavailable" };
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
return { ok: false, reason: "request_failed", status: response.status };
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const catalog = parseAgentModelCatalog(await response.json());
|
|
104
|
+
const etag = response.headers.get("etag");
|
|
105
|
+
if (!catalog || !etag || !isStrongEtag(etag)) {
|
|
106
|
+
return cached ? fromCache(cached) : { ok: false, reason: "unavailable" };
|
|
107
|
+
}
|
|
108
|
+
const entry = {
|
|
109
|
+
version: 1,
|
|
110
|
+
catalog,
|
|
111
|
+
etag,
|
|
112
|
+
fetchedAt: now(),
|
|
113
|
+
};
|
|
114
|
+
writeCache(cacheFile, entry);
|
|
115
|
+
return { ok: true, ...entry, source: "network" };
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return cached ? fromCache(cached) : { ok: false, reason: "unavailable" };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Check an explicit model against the selected provider or inferred agents-v2 brain. */
|
|
122
|
+
export function validateAgentModelSelection(catalog, modelId, chosenProvider) {
|
|
123
|
+
const provider = isCatalogProvider(chosenProvider) ? chosenProvider : undefined;
|
|
124
|
+
const inferred = providerFromModelId(modelId);
|
|
125
|
+
const providers = provider
|
|
126
|
+
? [provider]
|
|
127
|
+
: inferred
|
|
128
|
+
? [inferred]
|
|
129
|
+
: [...AGENT_MODEL_PROVIDERS];
|
|
130
|
+
const rows = providers.flatMap((name) => catalog.providers[name].models.map((model) => ({ provider: name, model })));
|
|
131
|
+
const exact = rows.find(({ model }) => model.id.toLowerCase() === modelId.toLowerCase());
|
|
132
|
+
if (exact?.model.selectable)
|
|
133
|
+
return { ok: true };
|
|
134
|
+
const suggestions = closestSelectableModels(modelId, rows.filter(({ model }) => model.selectable).map(({ model }) => model.id));
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
reason: exact ? "not_selectable" : "unknown",
|
|
138
|
+
...(providers.length === 1 ? { provider: providers[0] } : {}),
|
|
139
|
+
suggestions,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function modelSelectionErrorMessage(modelId, result) {
|
|
143
|
+
const scope = result.provider ? `the ${result.provider} catalog` : "the model catalog";
|
|
144
|
+
const problem = result.reason === "not_selectable" ? "not selectable in" : "not listed in";
|
|
145
|
+
const suggestion = result.suggestions.length
|
|
146
|
+
? ` Close matches: ${result.suggestions.join(", ")}.`
|
|
147
|
+
: "";
|
|
148
|
+
const command = result.provider
|
|
149
|
+
? ` Run \`hq agents models --provider ${result.provider}\` to see available models.`
|
|
150
|
+
: " Run `hq agents models` to see available models.";
|
|
151
|
+
return `Model '${modelId}' is ${problem} ${scope}.${suggestion}${command}`;
|
|
152
|
+
}
|
|
153
|
+
function providerFromModelId(modelId) {
|
|
154
|
+
const normalized = modelId.toLowerCase();
|
|
155
|
+
if (normalized.startsWith("gpt-"))
|
|
156
|
+
return "codex";
|
|
157
|
+
if (normalized.startsWith("grok-"))
|
|
158
|
+
return "grok";
|
|
159
|
+
if (normalized.startsWith("claude-"))
|
|
160
|
+
return "claude";
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
function closestSelectableModels(modelId, candidates) {
|
|
164
|
+
const needle = modelId.toLowerCase();
|
|
165
|
+
return candidates
|
|
166
|
+
.map((id) => ({
|
|
167
|
+
id,
|
|
168
|
+
distance: editDistance(needle, id.toLowerCase()),
|
|
169
|
+
prefix: commonPrefixLength(needle, id.toLowerCase()),
|
|
170
|
+
}))
|
|
171
|
+
.filter(({ distance, id }) => distance <= Math.max(2, Math.floor(needle.length / 4)) ||
|
|
172
|
+
id.toLowerCase().startsWith(needle))
|
|
173
|
+
.sort((a, b) => a.distance - b.distance || b.prefix - a.prefix || a.id.localeCompare(b.id))
|
|
174
|
+
.slice(0, 3)
|
|
175
|
+
.map(({ id }) => id);
|
|
176
|
+
}
|
|
177
|
+
function editDistance(left, right) {
|
|
178
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
179
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
180
|
+
const current = [i];
|
|
181
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
182
|
+
current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1));
|
|
183
|
+
}
|
|
184
|
+
previous = current;
|
|
185
|
+
}
|
|
186
|
+
return previous[right.length];
|
|
187
|
+
}
|
|
188
|
+
function commonPrefixLength(left, right) {
|
|
189
|
+
let index = 0;
|
|
190
|
+
while (index < left.length && index < right.length && left[index] === right[index])
|
|
191
|
+
index += 1;
|
|
192
|
+
return index;
|
|
193
|
+
}
|
|
194
|
+
function readCache(file) {
|
|
195
|
+
let serialized;
|
|
196
|
+
try {
|
|
197
|
+
serialized = fs.readFileSync(file, "utf8");
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (isMissingFile(error))
|
|
201
|
+
return null;
|
|
202
|
+
console.warn("Unable to read the model catalog cache; continuing without cached data.");
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
let value;
|
|
206
|
+
try {
|
|
207
|
+
value = JSON.parse(serialized);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
console.warn("Ignoring an invalid model catalog cache file.");
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
if (!isRecord(value) || value.version !== 1 || !isStrongEtag(value.etag)) {
|
|
214
|
+
console.warn("Ignoring an invalid model catalog cache file.");
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (typeof value.fetchedAt !== "number" || !Number.isFinite(value.fetchedAt)) {
|
|
218
|
+
console.warn("Ignoring an invalid model catalog cache file.");
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const catalog = parseAgentModelCatalog(value.catalog);
|
|
222
|
+
if (!catalog) {
|
|
223
|
+
console.warn("Ignoring an invalid model catalog cache file.");
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return { version: 1, catalog, etag: value.etag, fetchedAt: value.fetchedAt };
|
|
227
|
+
}
|
|
228
|
+
function writeCache(file, cache) {
|
|
229
|
+
const temporary = `${file}.${process.pid}.${cache.fetchedAt}.tmp`;
|
|
230
|
+
try {
|
|
231
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
232
|
+
fs.writeFileSync(temporary, JSON.stringify(cache), { encoding: "utf8", mode: 0o600 });
|
|
233
|
+
fs.renameSync(temporary, file);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
console.warn("Unable to save the model catalog cache; offline use may be unavailable.");
|
|
237
|
+
try {
|
|
238
|
+
fs.rmSync(temporary, { force: true });
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (!isMissingFile(error)) {
|
|
242
|
+
console.warn("Unable to remove a temporary model catalog cache file.");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function fromCache(cache, source = "cache") {
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
catalog: cache.catalog,
|
|
251
|
+
etag: cache.etag,
|
|
252
|
+
fetchedAt: cache.fetchedAt,
|
|
253
|
+
source,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function isStrongEtag(value) {
|
|
257
|
+
return typeof value === "string" &&
|
|
258
|
+
value.length >= 2 &&
|
|
259
|
+
value.startsWith('"') &&
|
|
260
|
+
value.endsWith('"') &&
|
|
261
|
+
!/[\r\n]/.test(value);
|
|
262
|
+
}
|
|
263
|
+
function isMissingFile(error) {
|
|
264
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
265
|
+
}
|
|
266
|
+
function isCatalogProvider(value) {
|
|
267
|
+
return value !== undefined && AGENT_MODEL_PROVIDERS.includes(value);
|
|
268
|
+
}
|
|
269
|
+
function isStringArray(value) {
|
|
270
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
271
|
+
}
|
|
272
|
+
function isRecord(value) {
|
|
273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=model-catalog.js.map
|
|
@@ -106,6 +106,7 @@ export declare function ownerFailureNotice(input: {
|
|
|
106
106
|
body: string;
|
|
107
107
|
} | null;
|
|
108
108
|
export interface ReportAdoptDependencies {
|
|
109
|
+
preflightJobCreation(): Promise<void>;
|
|
109
110
|
createJob(input: {
|
|
110
111
|
prompt: string;
|
|
111
112
|
schedule: {
|
|
@@ -124,8 +125,8 @@ export interface ReportAdoptDependencies {
|
|
|
124
125
|
patch(report: ChannelReport, patch: ReportPatch): Promise<ChannelReport>;
|
|
125
126
|
}
|
|
126
127
|
export declare class ReportPublishError extends Error {
|
|
127
|
-
readonly code: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "revision-conflict";
|
|
128
|
-
constructor(message: string, code?: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "revision-conflict");
|
|
128
|
+
readonly code: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "job-execution-disabled" | "revision-conflict";
|
|
129
|
+
constructor(message: string, code?: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "job-execution-disabled" | "revision-conflict");
|
|
129
130
|
}
|
|
130
131
|
/** A deliberately boring, token-safe failure reason stored on the report. */
|
|
131
132
|
export declare function plainFailureReason(error: unknown): string;
|
|
@@ -41,7 +41,7 @@ export class ReportPublishError extends Error {
|
|
|
41
41
|
/** A deliberately boring, token-safe failure reason stored on the report. */
|
|
42
42
|
export function plainFailureReason(error) {
|
|
43
43
|
if (error instanceof ReportPublishError) {
|
|
44
|
-
if (error.code === "source-invalid" || error.code === "job-failed" || error.code === "revision-conflict")
|
|
44
|
+
if (error.code === "source-invalid" || error.code === "job-failed" || error.code === "job-execution-disabled" || error.code === "revision-conflict")
|
|
45
45
|
return error.message.slice(0, 240);
|
|
46
46
|
if (error.code === "already-running")
|
|
47
47
|
return "already running";
|
|
@@ -231,9 +231,11 @@ export async function adoptReport(report, deps) {
|
|
|
231
231
|
throw new ReportPublishError("Report schedule is missing.", "source-invalid");
|
|
232
232
|
}
|
|
233
233
|
try {
|
|
234
|
+
const cron = reportJobCron(schedule);
|
|
235
|
+
if (cron)
|
|
236
|
+
await deps.preflightJobCreation();
|
|
234
237
|
if (report.jobId)
|
|
235
238
|
await deps.cancelJob(report.jobId);
|
|
236
|
-
const cron = reportJobCron(schedule);
|
|
237
239
|
if (!cron) {
|
|
238
240
|
return deps.patch(report, { jobId: null, jobState: "adopted", jobStateReason: null });
|
|
239
241
|
}
|