@indigoai-us/hq-cli 5.345.38 → 5.345.39
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 +8 -0
- package/dist/command-catalog.generated.d.ts +43 -0
- package/dist/command-catalog.generated.js +54 -0
- package/dist/commands/core-dm-bind-native.js +5 -4
- package/dist/commands/db-destroy.d.ts +15 -0
- package/dist/commands/db-destroy.js +143 -0
- package/dist/commands/db-dump.d.ts +9 -0
- package/dist/commands/db-dump.js +19 -11
- package/dist/commands/db.js +2 -0
- package/dist/commands/files-browse.js +1 -1
- package/dist/commands/lanes-message.js +14 -5
- package/dist/commands/lanes-resume.js +62 -14
- package/dist/commands/lanes.js +21 -4
- package/dist/commands/report.js +5 -1
- package/dist/lib/db/control-plane.d.ts +10 -0
- package/dist/lib/db/control-plane.js +18 -0
- package/dist/lib/deploy-api.js +2 -0
- package/dist/lib/doctor/fix/apply.d.ts +2 -0
- package/dist/lib/doctor/fix/apply.js +52 -1
- package/dist/lib/flag-registry.d.ts +1 -1
- package/dist/lib/flag-registry.js +20 -7
- package/dist/lib/lanes/accounts.js +24 -2
- package/dist/lib/lanes/delivery.d.ts +5 -2
- package/dist/lib/lanes/delivery.js +14 -5
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/reconcile.js +49 -5
- package/dist/lib/lanes/spawn.d.ts +7 -3
- package/dist/lib/lanes/spawn.js +97 -5
- package/dist/lib/lanes/store.js +54 -20
- package/dist/lib/lanes/story-ledger.d.ts +1 -1
- package/dist/lib/lanes/story-ledger.js +4 -4
- package/dist/lib/lanes/types.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.39] — 2026-10-07
|
|
6
|
+
- Cap each `hq doctor --fix` invocation at two company journal refreshes and persist a rotating cursor so pending journals continue on later runs.
|
|
7
|
+
- Add `hq db destroy --company <slug> --app <app>` to permanently delete an app's remote database. It requires typing the app id (or `--confirm <app>` when not interactive), offers `hq db dump` first (`--dump-first` / `--no-dump`), and only company owners and admins can run it.
|
|
8
|
+
- Lane story ledger sync now clears resolved sync errors and reads the company's cached feature gate before registry startup finishes.
|
|
9
|
+
- Keep quota-limited keep-alive resumes recoverable, preserve operator-triggered work while waiting for reset, and retry when an eligible account is available.
|
|
10
|
+
- Allow terminal worker lanes to be reparented to a live senior in the same company, while refusing running workers.
|
|
11
|
+
- Add request timeouts to native DM, file download, report deployment, and deploy API requests.
|
|
12
|
+
|
|
5
13
|
## [5.345.38] — 2026-10-07
|
|
6
14
|
- Windows CI parity suites now derive their shell-subprocess time budget from a warm-up measured on the same runner instead of a fixed 7-second deadline.
|
|
7
15
|
|
|
@@ -2050,6 +2050,49 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
2050
2050
|
readonly hidden: false;
|
|
2051
2051
|
}];
|
|
2052
2052
|
readonly subcommands: readonly [];
|
|
2053
|
+
}, {
|
|
2054
|
+
readonly name: "destroy";
|
|
2055
|
+
readonly description: "Permanently delete an app's remote database (owners and admins only; cannot be undone)";
|
|
2056
|
+
readonly aliases: readonly [];
|
|
2057
|
+
readonly hidden: false;
|
|
2058
|
+
readonly helpOption: true;
|
|
2059
|
+
readonly helpCommand: false;
|
|
2060
|
+
readonly usage: "[options]";
|
|
2061
|
+
readonly helpText: readonly [{
|
|
2062
|
+
readonly position: "after";
|
|
2063
|
+
readonly text: "\nThis deletes the app's database immediately. It cannot be undone.\nInteractive: you are asked to type the app id and whether to dump first.\nNon-interactive: pass --confirm <app> and either --dump-first or --no-dump.\nExample: hq db destroy --company acme --app todo --confirm todo --dump-first";
|
|
2064
|
+
}];
|
|
2065
|
+
readonly arguments: readonly [];
|
|
2066
|
+
readonly options: readonly [{
|
|
2067
|
+
readonly flags: "--company <slug>";
|
|
2068
|
+
readonly description: "Company slug";
|
|
2069
|
+
readonly hidden: false;
|
|
2070
|
+
}, {
|
|
2071
|
+
readonly flags: "--app <app>";
|
|
2072
|
+
readonly description: "App id";
|
|
2073
|
+
readonly hidden: false;
|
|
2074
|
+
}, {
|
|
2075
|
+
readonly flags: "--confirm <app>";
|
|
2076
|
+
readonly description: "Type the app id again to confirm (required when not interactive)";
|
|
2077
|
+
readonly hidden: false;
|
|
2078
|
+
}, {
|
|
2079
|
+
readonly flags: "--yes";
|
|
2080
|
+
readonly description: "Skip the dump question; the app id confirmation is still required";
|
|
2081
|
+
readonly hidden: false;
|
|
2082
|
+
}, {
|
|
2083
|
+
readonly flags: "--dump-first";
|
|
2084
|
+
readonly description: "Run hq db dump before destroying (non-interactive)";
|
|
2085
|
+
readonly hidden: false;
|
|
2086
|
+
}, {
|
|
2087
|
+
readonly flags: "--no-dump";
|
|
2088
|
+
readonly description: "Do not dump before destroying (non-interactive)";
|
|
2089
|
+
readonly hidden: false;
|
|
2090
|
+
}, {
|
|
2091
|
+
readonly flags: "--out <file>";
|
|
2092
|
+
readonly description: "Dump output file (default: <app>.sql)";
|
|
2093
|
+
readonly hidden: false;
|
|
2094
|
+
}];
|
|
2095
|
+
readonly subcommands: readonly [];
|
|
2053
2096
|
}];
|
|
2054
2097
|
}, {
|
|
2055
2098
|
readonly name: "api-keys";
|
|
@@ -2459,6 +2459,60 @@ export const COMMAND_CATALOG = [
|
|
|
2459
2459
|
}
|
|
2460
2460
|
],
|
|
2461
2461
|
"subcommands": []
|
|
2462
|
+
},
|
|
2463
|
+
{
|
|
2464
|
+
"name": "destroy",
|
|
2465
|
+
"description": "Permanently delete an app's remote database (owners and admins only; cannot be undone)",
|
|
2466
|
+
"aliases": [],
|
|
2467
|
+
"hidden": false,
|
|
2468
|
+
"helpOption": true,
|
|
2469
|
+
"helpCommand": false,
|
|
2470
|
+
"usage": "[options]",
|
|
2471
|
+
"helpText": [
|
|
2472
|
+
{
|
|
2473
|
+
"position": "after",
|
|
2474
|
+
"text": "\nThis deletes the app's database immediately. It cannot be undone.\nInteractive: you are asked to type the app id and whether to dump first.\nNon-interactive: pass --confirm <app> and either --dump-first or --no-dump.\nExample: hq db destroy --company acme --app todo --confirm todo --dump-first"
|
|
2475
|
+
}
|
|
2476
|
+
],
|
|
2477
|
+
"arguments": [],
|
|
2478
|
+
"options": [
|
|
2479
|
+
{
|
|
2480
|
+
"flags": "--company <slug>",
|
|
2481
|
+
"description": "Company slug",
|
|
2482
|
+
"hidden": false
|
|
2483
|
+
},
|
|
2484
|
+
{
|
|
2485
|
+
"flags": "--app <app>",
|
|
2486
|
+
"description": "App id",
|
|
2487
|
+
"hidden": false
|
|
2488
|
+
},
|
|
2489
|
+
{
|
|
2490
|
+
"flags": "--confirm <app>",
|
|
2491
|
+
"description": "Type the app id again to confirm (required when not interactive)",
|
|
2492
|
+
"hidden": false
|
|
2493
|
+
},
|
|
2494
|
+
{
|
|
2495
|
+
"flags": "--yes",
|
|
2496
|
+
"description": "Skip the dump question; the app id confirmation is still required",
|
|
2497
|
+
"hidden": false
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
"flags": "--dump-first",
|
|
2501
|
+
"description": "Run hq db dump before destroying (non-interactive)",
|
|
2502
|
+
"hidden": false
|
|
2503
|
+
},
|
|
2504
|
+
{
|
|
2505
|
+
"flags": "--no-dump",
|
|
2506
|
+
"description": "Do not dump before destroying (non-interactive)",
|
|
2507
|
+
"hidden": false
|
|
2508
|
+
},
|
|
2509
|
+
{
|
|
2510
|
+
"flags": "--out <file>",
|
|
2511
|
+
"description": "Dump output file (default: <app>.sql)",
|
|
2512
|
+
"hidden": false
|
|
2513
|
+
}
|
|
2514
|
+
],
|
|
2515
|
+
"subcommands": []
|
|
2462
2516
|
}
|
|
2463
2517
|
]
|
|
2464
2518
|
},
|
|
@@ -51,6 +51,7 @@ export const createDmBindDeps = (root, cwd, env, now, home = env.HOME || os.home
|
|
|
51
51
|
}),
|
|
52
52
|
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
53
53
|
});
|
|
54
|
+
const DM_BIND_API_TIMEOUT_MS = 15_000;
|
|
54
55
|
const fail = (message, code = 2) => utilityError(`hq-dm-bind: ${message}`, code);
|
|
55
56
|
const sessionDirectory = (d, sid) => path.join(d.root, "workspace/sessions", sid);
|
|
56
57
|
const filePath = (d, sid, name) => path.join(sessionDirectory(d, sid), name);
|
|
@@ -192,13 +193,13 @@ async function fetchRoster(d, ch) {
|
|
|
192
193
|
const config = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/config.json"), "utf8"));
|
|
193
194
|
const token = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/cognito-tokens.json"), "utf8")).idToken;
|
|
194
195
|
const headers = { Authorization: `Bearer ${token}` };
|
|
195
|
-
const response = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels`, { headers });
|
|
196
|
+
const response = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels`, { headers, signal: AbortSignal.timeout(DM_BIND_API_TIMEOUT_MS) });
|
|
196
197
|
const channels = await response.json();
|
|
197
198
|
const rows = Array.isArray(channels) ? channels : channels.channels ?? [];
|
|
198
199
|
const hit = rows.find((entry) => entry.channelId === ch || slug(entry.name) === slug(ch) || entry.slug === slug(ch));
|
|
199
200
|
if (!hit?.channelId)
|
|
200
201
|
return { stdout: "", stderr: "" };
|
|
201
|
-
const membersResponse = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels/${encodeURIComponent(String(hit.channelId))}/members`, { headers });
|
|
202
|
+
const membersResponse = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels/${encodeURIComponent(String(hit.channelId))}/members`, { headers, signal: AbortSignal.timeout(DM_BIND_API_TIMEOUT_MS) });
|
|
202
203
|
const members = await membersResponse.json();
|
|
203
204
|
const list = Array.isArray(members) ? members : members.members ?? [];
|
|
204
205
|
const lines = list.flatMap((member) => {
|
|
@@ -331,14 +332,14 @@ async function sendHuman(d, ch, payload) {
|
|
|
331
332
|
const config = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/config.json"), "utf8"));
|
|
332
333
|
const token = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/cognito-tokens.json"), "utf8")).idToken;
|
|
333
334
|
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
334
|
-
const channelsResponse = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels`, { headers });
|
|
335
|
+
const channelsResponse = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels`, { headers, signal: AbortSignal.timeout(DM_BIND_API_TIMEOUT_MS) });
|
|
335
336
|
const channels = await channelsResponse.json();
|
|
336
337
|
const rows = Array.isArray(channels) ? channels : channels.channels ?? [];
|
|
337
338
|
const hit = rows.find((entry) => entry.channelId === ch || slug(entry.name) === slug(ch) || entry.slug === slug(ch));
|
|
338
339
|
if (!hit?.channelId)
|
|
339
340
|
throw new Error(`channel not found: ${ch}`);
|
|
340
341
|
const response = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels/${encodeURIComponent(String(hit.channelId))}/messages`, {
|
|
341
|
-
method: "POST", headers, body: JSON.stringify(payload),
|
|
342
|
+
method: "POST", headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(DM_BIND_API_TIMEOUT_MS),
|
|
342
343
|
});
|
|
343
344
|
const text = await response.text();
|
|
344
345
|
if (!response.ok)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq db destroy — permanently delete one app's remote database.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { type DbRemoteCommandDeps } from "./db-remote-deps.js";
|
|
6
|
+
export type DestroyRoleCheck = "allowed" | "denied" | "unknown";
|
|
7
|
+
export interface DbDestroyIo {
|
|
8
|
+
isInteractive: () => boolean;
|
|
9
|
+
ask: (question: string) => Promise<string>;
|
|
10
|
+
checkRole: (deps: DbRemoteCommandDeps, companyUid: string) => Promise<DestroyRoleCheck>;
|
|
11
|
+
}
|
|
12
|
+
export declare function defaultDestroyIo(): DbDestroyIo;
|
|
13
|
+
export declare function describeDestroyError(error: unknown): string | null;
|
|
14
|
+
export declare function registerDbDestroyCommand(db: Command, depsFactory?: () => DbRemoteCommandDeps, ioFactory?: () => DbDestroyIo): void;
|
|
15
|
+
//# sourceMappingURL=db-destroy.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq db destroy — permanently delete one app's remote database.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import * as readline from "node:readline";
|
|
6
|
+
import { safeErrorMessage } from "../lib/db/redact.js";
|
|
7
|
+
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
8
|
+
import { dumpAppDatabase, describeSessionGap } from "./db-dump.js";
|
|
9
|
+
import { controlPlaneClient, defaultRemoteDeps, describeRemoteError, } from "./db-remote-deps.js";
|
|
10
|
+
const NOT_ADMIN = "Only company owners and admins can destroy an app database.";
|
|
11
|
+
async function defaultCheckRole(deps, companyUid) {
|
|
12
|
+
if (process.env.HQ_DB_MOCK_CONTROL_PLANE === "1")
|
|
13
|
+
return "unknown";
|
|
14
|
+
try {
|
|
15
|
+
const [{ listActiveMembers }, { resolveCallerPersonUid }] = await Promise.all([
|
|
16
|
+
import("./members.js"),
|
|
17
|
+
import("../utils/vault-api.js"),
|
|
18
|
+
]);
|
|
19
|
+
const token = await deps.getAccessToken();
|
|
20
|
+
const [personUid, members] = await Promise.all([
|
|
21
|
+
resolveCallerPersonUid(token),
|
|
22
|
+
listActiveMembers(token, companyUid),
|
|
23
|
+
]);
|
|
24
|
+
const me = members.find((m) => m.personUid === personUid);
|
|
25
|
+
if (!me)
|
|
26
|
+
return "unknown";
|
|
27
|
+
return me.role === "owner" || me.role === "admin" ? "allowed" : "denied";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function defaultDestroyIo() {
|
|
34
|
+
return {
|
|
35
|
+
isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
36
|
+
ask: (question) => new Promise((resolve) => {
|
|
37
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
38
|
+
rl.question(question, (answer) => {
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve(answer);
|
|
41
|
+
});
|
|
42
|
+
}),
|
|
43
|
+
checkRole: defaultCheckRole,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function describeDestroyError(error) {
|
|
47
|
+
const status = error.status;
|
|
48
|
+
if (status === 403)
|
|
49
|
+
return NOT_ADMIN;
|
|
50
|
+
if (status === 404)
|
|
51
|
+
return "This app has no database attached, so there is nothing to destroy.";
|
|
52
|
+
return describeRemoteError(error);
|
|
53
|
+
}
|
|
54
|
+
function fail(message) {
|
|
55
|
+
console.error(chalk.red("Error:"), message);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
export function registerDbDestroyCommand(db, depsFactory = defaultRemoteDeps, ioFactory = defaultDestroyIo) {
|
|
59
|
+
db.command("destroy")
|
|
60
|
+
.description("Permanently delete an app's remote database (owners and admins only; cannot be undone)")
|
|
61
|
+
.requiredOption("--company <slug>", "Company slug")
|
|
62
|
+
.requiredOption("--app <app>", "App id")
|
|
63
|
+
.option("--confirm <app>", "Type the app id again to confirm (required when not interactive)")
|
|
64
|
+
.option("--yes", "Skip the dump question; the app id confirmation is still required")
|
|
65
|
+
.option("--dump-first", "Run hq db dump before destroying (non-interactive)")
|
|
66
|
+
.option("--no-dump", "Do not dump before destroying (non-interactive)")
|
|
67
|
+
.option("--out <file>", "Dump output file (default: <app>.sql)")
|
|
68
|
+
.addHelpText("after", "\nThis deletes the app's database immediately. It cannot be undone.\n" +
|
|
69
|
+
"Interactive: you are asked to type the app id and whether to dump first.\n" +
|
|
70
|
+
"Non-interactive: pass --confirm <app> and either --dump-first or --no-dump.\n" +
|
|
71
|
+
"Example: hq db destroy --company acme --app todo --confirm todo --dump-first")
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
try {
|
|
74
|
+
const io = ioFactory();
|
|
75
|
+
const app = String(opts.app ?? "").trim();
|
|
76
|
+
const interactive = io.isInteractive();
|
|
77
|
+
const dumpFlagGiven = opts.dumpFirst === true || opts.dump === false;
|
|
78
|
+
if (opts.dumpFirst && opts.dump === false) {
|
|
79
|
+
fail("Use either --dump-first or --no-dump, not both.");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const deps = depsFactory();
|
|
83
|
+
const resolved = await deps.resolveCompany(String(opts.company ?? "").trim());
|
|
84
|
+
if ((await io.checkRole(deps, resolved.companyUid)) === "denied") {
|
|
85
|
+
fail(NOT_ADMIN);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.error(chalk.yellow(`Warning: this deletes the database for app ${app} in ${resolved.companySlug} immediately. It cannot be undone.`));
|
|
89
|
+
let confirmed = opts.confirm;
|
|
90
|
+
if (confirmed === undefined) {
|
|
91
|
+
if (!interactive) {
|
|
92
|
+
fail(`Confirmation required. Re-run with --confirm ${app}.`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
confirmed = await io.ask(`Type the app id (${app}) to confirm: `);
|
|
96
|
+
}
|
|
97
|
+
if (confirmed.trim() !== app) {
|
|
98
|
+
fail("The confirmation did not match the app id. Nothing was deleted.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let dumpFirst = opts.dumpFirst === true;
|
|
102
|
+
if (!dumpFlagGiven) {
|
|
103
|
+
if (interactive && !opts.yes) {
|
|
104
|
+
const answer = (await io.ask(`Dump the database to ${opts.out ?? `${app}.sql`} first? [Y/n] `))
|
|
105
|
+
.trim()
|
|
106
|
+
.toLowerCase();
|
|
107
|
+
dumpFirst = answer === "" || answer === "y" || answer === "yes";
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.error("No dump was taken. Pass --dump-first to save a copy before destroying.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (dumpFirst) {
|
|
114
|
+
try {
|
|
115
|
+
const summary = await dumpAppDatabase(deps, {
|
|
116
|
+
companyUid: resolved.companyUid,
|
|
117
|
+
appId: app,
|
|
118
|
+
out: opts.out,
|
|
119
|
+
});
|
|
120
|
+
console.log(`Wrote ${summary.tables} table(s) and ${summary.rows} row(s) to ${summary.outPath}.`);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (isPlanGateError(error))
|
|
124
|
+
throw error;
|
|
125
|
+
fail(`The dump failed, so the database was not destroyed. ${describeSessionGap(error) ?? describeRemoteError(error) ?? safeErrorMessage(error)}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const result = await controlPlaneClient(deps).destroyApp({
|
|
130
|
+
companyUid: resolved.companyUid,
|
|
131
|
+
appId: app,
|
|
132
|
+
});
|
|
133
|
+
const what = result.mode === "schema" ? "schema and roles dropped" : "database cluster deleted";
|
|
134
|
+
console.log(`Destroyed the database for app ${app}: ${what}.`);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (isPlanGateError(error))
|
|
138
|
+
throw error;
|
|
139
|
+
fail(describeDestroyError(error) ?? safeErrorMessage(error));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=db-destroy.js.map
|
|
@@ -4,5 +4,14 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { type DbRemoteCommandDeps } from "./db-remote-deps.js";
|
|
6
6
|
export declare function describeSessionGap(error: unknown): string | null;
|
|
7
|
+
export declare function dumpAppDatabase(deps: DbRemoteCommandDeps, input: {
|
|
8
|
+
companyUid: string;
|
|
9
|
+
appId: string;
|
|
10
|
+
out?: string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
tables: number;
|
|
13
|
+
rows: number;
|
|
14
|
+
outPath: string;
|
|
15
|
+
}>;
|
|
7
16
|
export declare function registerDbDumpCommand(db: Command, depsFactory?: () => DbRemoteCommandDeps): void;
|
|
8
17
|
//# sourceMappingURL=db-dump.d.ts.map
|
package/dist/commands/db-dump.js
CHANGED
|
@@ -14,6 +14,22 @@ export function describeSessionGap(error) {
|
|
|
14
14
|
}
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
|
+
export async function dumpAppDatabase(deps, input) {
|
|
18
|
+
const conn = await openRemoteAppConnection(remoteEngineDeps(deps), {
|
|
19
|
+
companyUid: input.companyUid,
|
|
20
|
+
appId: input.appId,
|
|
21
|
+
});
|
|
22
|
+
const outPath = input.out ?? `${input.appId}.sql`;
|
|
23
|
+
const fd = fs.openSync(outPath, "w");
|
|
24
|
+
try {
|
|
25
|
+
const summary = await dumpRemoteSchema(conn, (text) => fs.writeSync(fd, text));
|
|
26
|
+
return { tables: summary.tables, rows: summary.rows, outPath };
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
fs.closeSync(fd);
|
|
30
|
+
await conn.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
17
33
|
export function registerDbDumpCommand(db, depsFactory = defaultRemoteDeps) {
|
|
18
34
|
db.command("dump")
|
|
19
35
|
.description("Export an app's remote database schema and data to a SQL file")
|
|
@@ -24,20 +40,12 @@ export function registerDbDumpCommand(db, depsFactory = defaultRemoteDeps) {
|
|
|
24
40
|
try {
|
|
25
41
|
const deps = depsFactory();
|
|
26
42
|
const resolved = await deps.resolveCompany(String(opts.company ?? "").trim());
|
|
27
|
-
const
|
|
43
|
+
const summary = await dumpAppDatabase(deps, {
|
|
28
44
|
companyUid: resolved.companyUid,
|
|
29
45
|
appId: opts.app,
|
|
46
|
+
out: opts.out,
|
|
30
47
|
});
|
|
31
|
-
|
|
32
|
-
const fd = fs.openSync(outPath, "w");
|
|
33
|
-
try {
|
|
34
|
-
const summary = await dumpRemoteSchema(conn, (text) => fs.writeSync(fd, text));
|
|
35
|
-
console.log(`Wrote ${summary.tables} table(s) and ${summary.rows} row(s) to ${outPath}.`);
|
|
36
|
-
}
|
|
37
|
-
finally {
|
|
38
|
-
fs.closeSync(fd);
|
|
39
|
-
await conn.close();
|
|
40
|
-
}
|
|
48
|
+
console.log(`Wrote ${summary.tables} table(s) and ${summary.rows} row(s) to ${summary.outPath}.`);
|
|
41
49
|
}
|
|
42
50
|
catch (error) {
|
|
43
51
|
if (isPlanGateError(error))
|
package/dist/commands/db.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* hq db — vault database commands (local + remote tiers).
|
|
3
3
|
*/
|
|
4
|
+
import { registerDbDestroyCommand } from "./db-destroy.js";
|
|
4
5
|
import { registerDbDumpCommand } from "./db-dump.js";
|
|
5
6
|
import { registerDbMigrateCommand } from "./db-migrate.js";
|
|
6
7
|
import { registerDbProvisionCommand } from "./db-provision.js";
|
|
@@ -20,5 +21,6 @@ export function registerDbCommand(program) {
|
|
|
20
21
|
registerDbProvisionCommand(db);
|
|
21
22
|
registerDbUsageCommand(db);
|
|
22
23
|
registerDbDumpCommand(db);
|
|
24
|
+
registerDbDestroyCommand(db);
|
|
23
25
|
}
|
|
24
26
|
//# sourceMappingURL=db.js.map
|
|
@@ -778,7 +778,7 @@ export function createCompanyPresignClient(input) {
|
|
|
778
778
|
}
|
|
779
779
|
throw new Error(message);
|
|
780
780
|
}
|
|
781
|
-
const dl = await fetch(first.url);
|
|
781
|
+
const dl = await fetch(first.url, { signal: AbortSignal.timeout(120_000) });
|
|
782
782
|
if (!dl.ok) {
|
|
783
783
|
throw new Error(`Failed to download '${key}' (HTTP ${dl.status})`);
|
|
784
784
|
}
|
|
@@ -426,7 +426,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
426
426
|
// This invocation won the race to classify the dead worker, so the
|
|
427
427
|
// watcher callback did not own recovery. Fold any earlier queued
|
|
428
428
|
// messages into this resume to keep one provider turn and one delivery.
|
|
429
|
-
if (laneDeliveryMechanism(lane) === "resume" && dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
429
|
+
if (laneDeliveryMechanism(lane, { forMessage: true }) === "resume" && dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
430
430
|
const queuedMessages = drainDropbox(hqRoot, laneId);
|
|
431
431
|
if (queuedMessages.length > 0) {
|
|
432
432
|
claimedMessages = queuedMessages;
|
|
@@ -496,7 +496,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
496
496
|
const awaitingInputFallback = lane.state === "awaiting_input";
|
|
497
497
|
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
498
498
|
// `none` refuses before anything is written: no inbox append, no spawn.
|
|
499
|
-
const mechanism = laneDeliveryMechanism(lane);
|
|
499
|
+
const mechanism = laneDeliveryMechanism(lane, { forMessage: true });
|
|
500
500
|
if (mechanism === "none") {
|
|
501
501
|
const message = deliveryRefusalReason(lane);
|
|
502
502
|
throw new LanesError("lane_not_accepting_messages", message, {
|
|
@@ -542,8 +542,10 @@ export function createLaneMessageHandlers(deps) {
|
|
|
542
542
|
.map((reason) => reason.quota_reset_at)
|
|
543
543
|
.filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)))
|
|
544
544
|
.sort((a, b) => Date.parse(a) - Date.parse(b));
|
|
545
|
+
if (resetTimes.length === 0)
|
|
546
|
+
throw err;
|
|
545
547
|
quotaHoldDeferred = {
|
|
546
|
-
|
|
548
|
+
quota_reset_at: resetTimes[0],
|
|
547
549
|
...(lane.model !== undefined ? { model: lane.model } : {}),
|
|
548
550
|
};
|
|
549
551
|
automaticAccountSelection = undefined;
|
|
@@ -674,6 +676,13 @@ export function createLaneMessageHandlers(deps) {
|
|
|
674
676
|
queued_details: undefined,
|
|
675
677
|
queued_at: at,
|
|
676
678
|
admission_reserved_at: undefined,
|
|
679
|
+
...(quotaHoldDeferred?.quota_reset_at !== undefined ? {
|
|
680
|
+
quota_reset_at: quotaHoldDeferred.quota_reset_at,
|
|
681
|
+
keep_alive_next_eligible_at: quotaHoldDeferred.quota_reset_at,
|
|
682
|
+
keep_alive_resume_count: 0,
|
|
683
|
+
keep_alive_last_resume_at: undefined,
|
|
684
|
+
keep_alive_exhausted_at: undefined,
|
|
685
|
+
} : {}),
|
|
677
686
|
pending_resume: {
|
|
678
687
|
session_id: sessionId,
|
|
679
688
|
trigger: "message",
|
|
@@ -925,7 +934,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
925
934
|
}
|
|
926
935
|
// Every accepted message moves the compaction reference forward.
|
|
927
936
|
const next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
928
|
-
...resetKeepAliveProgress(current),
|
|
937
|
+
...(queuedOnQuotaHold ? current : resetKeepAliveProgress(current)),
|
|
929
938
|
last_message_at: at,
|
|
930
939
|
}));
|
|
931
940
|
// What this command can claim when it returns: the message is durably
|
|
@@ -1050,7 +1059,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
1050
1059
|
? "done_before_envelope"
|
|
1051
1060
|
: lane.state === "awaiting_input"
|
|
1052
1061
|
? "awaiting_input_requires_question_answer"
|
|
1053
|
-
: laneDeliveryMechanism(lane) !== "resume"
|
|
1062
|
+
: laneDeliveryMechanism(lane, { forMessage: true }) !== "resume"
|
|
1054
1063
|
? `no_resume_path_for_${lane.state}`
|
|
1055
1064
|
: undefined);
|
|
1056
1065
|
if (refusal !== undefined) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { BALANCE_MOVABLE_STATES } from "../lib/lanes/claude-balance.js";
|
|
2
2
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
3
|
-
import { appendCappedHistory, providerSessionPortable, providerSupportsSessionResume, readLane, resumeCommandRefusal, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
|
|
3
|
+
import { appendCappedHistory, isFailedQuotaKeepAliveLane, providerSessionPortable, providerSupportsSessionResume, readLane, resumeCommandRefusal, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
|
|
4
4
|
import { parseCliEffort } from "../lib/lanes/effort.js";
|
|
5
5
|
import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
|
|
6
6
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
7
7
|
import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
|
|
8
8
|
import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice } from "../lib/lanes/story-switch.js";
|
|
9
|
-
import { spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
9
|
+
import { markSpawnFailure, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
10
10
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, hasKeepAliveProgress, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
11
11
|
export function createLaneResumeHandlers(deps) {
|
|
12
12
|
const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, automaticBalanceMoveHistoryReason, authorizeFailedResume, authorizeResumeProfileRepin, casUpdateLane, dispatchOutpostLaneCommand, laneIsHostedHere, laneWithRecoveredProviderSession, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
|
|
@@ -219,7 +219,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
219
219
|
}
|
|
220
220
|
const refusal = queuedProfileRecovery || lane.state === "awaiting_input"
|
|
221
221
|
? null
|
|
222
|
-
: resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
|
|
222
|
+
: resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane) });
|
|
223
223
|
if (refusal !== null) {
|
|
224
224
|
throw new LanesError(refusal.error, refusal.message, {
|
|
225
225
|
lane_id: laneId,
|
|
@@ -284,12 +284,29 @@ export function createLaneResumeHandlers(deps) {
|
|
|
284
284
|
});
|
|
285
285
|
return;
|
|
286
286
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
287
|
+
let quotaHoldDeferred;
|
|
288
|
+
let automaticAccountSelection = undefined;
|
|
289
|
+
if (opts.account === undefined) {
|
|
290
|
+
try {
|
|
291
|
+
automaticAccountSelection = await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, {
|
|
292
|
+
repairUnavailableAccount: true,
|
|
293
|
+
...(refreshProfileSettings ? { refreshProfileSettings: true } : {}),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
const reasons = err instanceof LanesError && err.error === "claude_account_balance_no_eligible_account" &&
|
|
298
|
+
Array.isArray(err.details.account_reasons)
|
|
299
|
+
? err.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason))
|
|
300
|
+
: [];
|
|
301
|
+
const resets = reasons.filter((reason) => reason.reason === "quota_held")
|
|
302
|
+
.map((reason) => reason.quota_reset_at)
|
|
303
|
+
.filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)))
|
|
304
|
+
.sort((a, b) => Date.parse(a) - Date.parse(b));
|
|
305
|
+
if (resets.length === 0)
|
|
306
|
+
throw err;
|
|
307
|
+
quotaHoldDeferred = { quota_reset_at: resets[0], account_reasons: reasons };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
293
310
|
const profileRefreshAccount = opts.account === undefined && refreshProfileSettings && resumeProfile !== undefined &&
|
|
294
311
|
lane.account_source !== undefined && lane.account_source !== "cli" &&
|
|
295
312
|
resumeProfile.profile.meta.provider === lane.provider
|
|
@@ -451,8 +468,37 @@ export function createLaneResumeHandlers(deps) {
|
|
|
451
468
|
: {}),
|
|
452
469
|
});
|
|
453
470
|
let queuedOnAdmission = false;
|
|
471
|
+
let queuedOnQuotaHold = false;
|
|
454
472
|
let resumed;
|
|
455
|
-
if (
|
|
473
|
+
if (quotaHoldDeferred !== undefined) {
|
|
474
|
+
const sessionId = lane.provider_session_id;
|
|
475
|
+
if (!sessionId)
|
|
476
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this resume`, {
|
|
477
|
+
lane_id: laneId,
|
|
478
|
+
provider: lane.provider,
|
|
479
|
+
});
|
|
480
|
+
const quotaHoldAt = new Date(Date.parse(at) + 1).toISOString();
|
|
481
|
+
const pendingResume = {
|
|
482
|
+
session_id: sessionId,
|
|
483
|
+
trigger: "resume",
|
|
484
|
+
caller: ctx.env.HQ_LANE_ID?.trim() || "user",
|
|
485
|
+
resumed_from: resumedFrom,
|
|
486
|
+
...(opts.fromFailed === true ? { from_failed: true } : {}),
|
|
487
|
+
...(failedResumeExpectedSenior !== undefined ? { expected_senior: failedResumeExpectedSenior } : {}),
|
|
488
|
+
...(deliveryText !== undefined ? { text: deliveryText } : {}),
|
|
489
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
490
|
+
compact: opts.compact !== false,
|
|
491
|
+
at: quotaHoldAt,
|
|
492
|
+
};
|
|
493
|
+
resumed = markSpawnFailure(hqRoot, laneId, "account_quota_exhausted", {
|
|
494
|
+
provider: lane.provider,
|
|
495
|
+
account: lane.account ?? null,
|
|
496
|
+
quota_reset_at: quotaHoldDeferred.quota_reset_at,
|
|
497
|
+
account_reasons: quotaHoldDeferred.account_reasons,
|
|
498
|
+
}, { resumeTrigger: "resume", pendingResume, at: quotaHoldAt });
|
|
499
|
+
queuedOnQuotaHold = true;
|
|
500
|
+
}
|
|
501
|
+
else if (move?.queued) {
|
|
456
502
|
resumed = lane;
|
|
457
503
|
}
|
|
458
504
|
else {
|
|
@@ -570,7 +616,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
570
616
|
if (deliveryText !== undefined) {
|
|
571
617
|
// An operator text moves the compaction reference forward, as a message does.
|
|
572
618
|
next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
573
|
-
...resetKeepAliveProgress(current),
|
|
619
|
+
...(queuedOnQuotaHold ? current : resetKeepAliveProgress(current)),
|
|
574
620
|
last_message_at: at,
|
|
575
621
|
}));
|
|
576
622
|
}
|
|
@@ -587,9 +633,11 @@ export function createLaneResumeHandlers(deps) {
|
|
|
587
633
|
delivery: "resume",
|
|
588
634
|
delivery_status: move?.queued
|
|
589
635
|
? "queued_on_target_account"
|
|
590
|
-
:
|
|
591
|
-
? "
|
|
592
|
-
:
|
|
636
|
+
: queuedOnQuotaHold
|
|
637
|
+
? "queued_on_quota_hold"
|
|
638
|
+
: queuedOnAdmission
|
|
639
|
+
? "queued_on_admission"
|
|
640
|
+
: "worker_spawned",
|
|
593
641
|
account_move: move ? accountMoveSummary(move) : null,
|
|
594
642
|
resumed_session_id: resumed.last_resume?.session_id ?? null,
|
|
595
643
|
last_resume: next.last_resume ?? null,
|