@indigoai-us/hq-cli 5.345.177 → 5.345.179
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 +50 -0
- package/dist/command-catalog.generated.d.ts +79 -1
- package/dist/command-catalog.generated.js +96 -1
- package/dist/commands/bot-jobs.d.ts +11 -9
- package/dist/commands/bot-jobs.js +56 -21
- package/dist/commands/bot.d.ts +13 -0
- package/dist/commands/bot.js +230 -21
- package/dist/commands/cloud.d.ts +49 -0
- package/dist/commands/cloud.js +155 -15
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +15 -4
- package/dist/commands/dm-attach.js +15 -3
- package/dist/commands/integrations-core.d.ts +2 -0
- package/dist/commands/integrations-core.js +1 -0
- package/dist/commands/library-add.d.ts +9 -0
- package/dist/commands/library-add.js +19 -2
- package/dist/commands/run.js +9 -1
- package/dist/commands/sources.d.ts +89 -0
- package/dist/commands/sources.js +363 -1
- package/dist/lib/bot/config.d.ts +2 -0
- package/dist/lib/bot/fleet-default-flag.d.ts +31 -0
- package/dist/lib/bot/fleet-default-flag.js +127 -0
- package/dist/lib/bot/fleet-local.d.ts +30 -1
- package/dist/lib/bot/fleet-local.js +86 -11
- package/dist/lib/bot/index.d.ts +1 -0
- package/dist/lib/bot/index.js +1 -0
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
- package/dist/lib/bot/local-config.js +1 -1
- package/dist/lib/bot/local-jobs.d.ts +40 -4
- package/dist/lib/bot/local-jobs.js +72 -5
- package/dist/lib/bot/run.d.ts +15 -0
- package/dist/lib/bot/run.js +61 -0
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +7 -2
- package/dist/lib/daemon/config.d.ts +2 -1
- package/dist/lib/daemon/config.js +4 -3
- package/dist/lib/daemon/handoff.d.ts +4 -0
- package/dist/lib/daemon/handoff.js +6 -2
- package/dist/lib/daemon/install.d.ts +70 -2
- package/dist/lib/daemon/install.js +185 -27
- package/dist/lib/daemon/legacy.d.ts +9 -3
- package/dist/lib/daemon/legacy.js +11 -4
- package/dist/lib/daemon/migrate.js +1 -1
- package/dist/lib/daemon/run.d.ts +3 -2
- package/dist/lib/daemon/run.js +10 -6
- package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
- package/dist/lib/doctor/checks/anywhere.js +35 -6
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
- package/dist/lib/hot-channels/adapters/attio.js +374 -0
- package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
- package/dist/lib/hot-channels/adapters/gmail.js +413 -0
- package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
- package/dist/lib/hot-channels/adapters/index.js +39 -0
- package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
- package/dist/lib/hot-channels/adapters/linear.js +321 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
- package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
- package/dist/lib/hot-channels/adapters/meetings.js +523 -0
- package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
- package/dist/lib/hot-channels/adapters/slack.js +277 -0
- package/dist/lib/hot-channels/alert.d.ts +78 -0
- package/dist/lib/hot-channels/alert.js +179 -0
- package/dist/lib/hot-channels/audience.d.ts +15 -0
- package/dist/lib/hot-channels/audience.js +25 -0
- package/dist/lib/hot-channels/contract.d.ts +140 -0
- package/dist/lib/hot-channels/contract.js +20 -0
- package/dist/lib/hot-channels/cursors.d.ts +41 -0
- package/dist/lib/hot-channels/cursors.js +84 -0
- package/dist/lib/hot-channels/index.d.ts +10 -0
- package/dist/lib/hot-channels/index.js +10 -0
- package/dist/lib/hot-channels/landing.d.ts +83 -0
- package/dist/lib/hot-channels/landing.js +175 -0
- package/dist/lib/hot-channels/ledger.d.ts +101 -0
- package/dist/lib/hot-channels/ledger.js +186 -0
- package/dist/lib/hot-channels/loop.d.ts +74 -0
- package/dist/lib/hot-channels/loop.js +219 -0
- package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
- package/dist/lib/hot-channels/registry.d.ts +28 -0
- package/dist/lib/hot-channels/registry.js +121 -0
- package/dist/lib/hot-channels/run.d.ts +137 -0
- package/dist/lib/hot-channels/run.js +324 -0
- package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
- package/dist/lib/hot-channels/signals-pass.js +581 -0
- package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
- package/dist/lib/hot-channels/sync-lock.js +224 -0
- package/dist/lib/install/hook-target.d.ts +6 -2
- package/dist/lib/install/hook-target.js +9 -4
- package/dist/lib/lanes/loop.d.ts +10 -0
- package/dist/lib/lanes/loop.js +18 -1
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +26 -6
- package/dist/run/hq-plugin.d.ts +3 -0
- package/dist/run/hq-plugin.js +18 -0
- package/package.json +7 -7
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One hot-channel sync at a time per person per company. The bot's scheduled
|
|
3
|
+
* run and a manual `hq sources sync` can fire together; the index files are
|
|
4
|
+
* read-modify-write, so two runs at once can lose rows and process the same
|
|
5
|
+
* event twice. The lock is `sources/_runs/@{personKey}/.lock`, created with
|
|
6
|
+
* O_EXCL and holding the pid, host, start time and last heartbeat.
|
|
7
|
+
*
|
|
8
|
+
* A lock is stale when its holder on this host is no longer alive, or when its
|
|
9
|
+
* last heartbeat is older than `SYNC_LOCK_STALE_MS`. The holder heartbeats the
|
|
10
|
+
* lock while it runs, so a long signals pass is never taken over mid-write.
|
|
11
|
+
* Takeover runs under an O_EXCL `.takeover` guard: the guard holder renames the
|
|
12
|
+
* stale file to a unique name, checks it moved the file it judged stale, and
|
|
13
|
+
* every contender then competes on the O_EXCL create of a fresh lock.
|
|
14
|
+
* Release removes the file only while it still names this holder.
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import * as fs from "node:fs/promises";
|
|
18
|
+
import * as os from "node:os";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { assertNotIntegrationSecretsPath, personDirName } from "./cursors.js";
|
|
21
|
+
/** A lock whose last heartbeat is older than this is stale. */
|
|
22
|
+
export const SYNC_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
23
|
+
/** How often a held lock is refreshed; well under the stale limit. */
|
|
24
|
+
export const SYNC_LOCK_HEARTBEAT_MS = 60 * 1000;
|
|
25
|
+
export const SYNC_LOCK_BUSY_REASON = "another sync is running";
|
|
26
|
+
/**
|
|
27
|
+
* The run no longer holds its sync lock (another run took it over, for example
|
|
28
|
+
* after this machine slept past the stale limit). The run stops before its
|
|
29
|
+
* next write.
|
|
30
|
+
*/
|
|
31
|
+
export class SyncLockLostError extends Error {
|
|
32
|
+
constructor() {
|
|
33
|
+
super("sync lock lost to another run; stopped before the next write");
|
|
34
|
+
this.name = "SyncLockLostError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function syncLockPath(vaultRoot, personKey) {
|
|
38
|
+
const file = path.join(vaultRoot, "sources", "_runs", personDirName(personKey), ".lock");
|
|
39
|
+
assertNotIntegrationSecretsPath(file);
|
|
40
|
+
return file;
|
|
41
|
+
}
|
|
42
|
+
function serialize(body) {
|
|
43
|
+
return `${JSON.stringify(body)}\n`;
|
|
44
|
+
}
|
|
45
|
+
async function tryCreate(file, body) {
|
|
46
|
+
try {
|
|
47
|
+
await fs.writeFile(file, serialize(body), { encoding: "utf8", flag: "wx" });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err.code === "EEXIST")
|
|
52
|
+
return false;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function readBody(file) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
59
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err.code === "ENOENT")
|
|
63
|
+
return "missing";
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** True when `pid` names a live process on this host. EPERM means it exists. */
|
|
68
|
+
export function pidAlive(pid) {
|
|
69
|
+
try {
|
|
70
|
+
process.kill(pid, 0);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return err.code === "EPERM";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function isStale(file, nowMs, staleMs, alive) {
|
|
78
|
+
const body = await readBody(file);
|
|
79
|
+
if (body === "missing")
|
|
80
|
+
return "missing";
|
|
81
|
+
if (body && typeof body.pid === "number" && body.host === os.hostname() && !alive(body.pid))
|
|
82
|
+
return true;
|
|
83
|
+
const stamp = body?.heartbeatAt ?? body?.startedAt;
|
|
84
|
+
let at = typeof stamp === "string" ? Date.parse(stamp) : Number.NaN;
|
|
85
|
+
if (Number.isNaN(at)) {
|
|
86
|
+
try {
|
|
87
|
+
at = (await fs.stat(file)).mtimeMs;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return "missing";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return nowMs - at >= staleMs;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Take the sync lock, or return null when a live run holds it. A stale lock is
|
|
97
|
+
* moved aside and taken over once; a race lost during takeover returns null.
|
|
98
|
+
*/
|
|
99
|
+
export async function acquireSyncLock(vaultRoot, personKey, options = {}) {
|
|
100
|
+
const now = options.now ?? (() => new Date());
|
|
101
|
+
const staleMs = options.staleMs ?? SYNC_LOCK_STALE_MS;
|
|
102
|
+
const heartbeatMs = options.heartbeatMs ?? SYNC_LOCK_HEARTBEAT_MS;
|
|
103
|
+
const alive = options.pidAlive ?? pidAlive;
|
|
104
|
+
const file = syncLockPath(vaultRoot, personKey);
|
|
105
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
106
|
+
const startedAt = now().toISOString();
|
|
107
|
+
const body = { pid: process.pid, host: os.hostname(), startedAt, heartbeatAt: startedAt };
|
|
108
|
+
const ownsFile = async () => {
|
|
109
|
+
const current = await readBody(file);
|
|
110
|
+
return (current !== null &&
|
|
111
|
+
current !== "missing" &&
|
|
112
|
+
current.pid === body.pid &&
|
|
113
|
+
current.host === body.host &&
|
|
114
|
+
current.startedAt === body.startedAt);
|
|
115
|
+
};
|
|
116
|
+
const held = () => {
|
|
117
|
+
let released = false;
|
|
118
|
+
let timer;
|
|
119
|
+
const heartbeat = async () => {
|
|
120
|
+
if (released || !(await ownsFile()))
|
|
121
|
+
return;
|
|
122
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
123
|
+
await fs.writeFile(tmp, serialize({ ...body, heartbeatAt: now().toISOString() }), "utf8");
|
|
124
|
+
await fs.rename(tmp, file);
|
|
125
|
+
};
|
|
126
|
+
if (heartbeatMs > 0) {
|
|
127
|
+
timer = setInterval(() => {
|
|
128
|
+
heartbeat().catch(() => undefined);
|
|
129
|
+
}, heartbeatMs);
|
|
130
|
+
timer.unref();
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
file,
|
|
134
|
+
assertHeld: async () => {
|
|
135
|
+
if (released || !(await ownsFile()))
|
|
136
|
+
throw new SyncLockLostError();
|
|
137
|
+
},
|
|
138
|
+
heartbeat,
|
|
139
|
+
release: async () => {
|
|
140
|
+
released = true;
|
|
141
|
+
if (timer)
|
|
142
|
+
clearInterval(timer);
|
|
143
|
+
if (await ownsFile())
|
|
144
|
+
await fs.rm(file, { force: true });
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
if (await tryCreate(file, body))
|
|
149
|
+
return held();
|
|
150
|
+
const stale = await isStale(file, now().getTime(), staleMs, alive);
|
|
151
|
+
if (stale === false)
|
|
152
|
+
return null;
|
|
153
|
+
if (stale === true && !(await takeOverStale(file, now, staleMs, alive)))
|
|
154
|
+
return null;
|
|
155
|
+
return (await tryCreate(file, body)) ? held() : null;
|
|
156
|
+
}
|
|
157
|
+
/** A takeover guard older than this belongs to a contender that died mid-takeover. */
|
|
158
|
+
const TAKEOVER_GUARD_STALE_MS = 60 * 1000;
|
|
159
|
+
/**
|
|
160
|
+
* Move a stale lock out of the way. Only the contender that O_EXCL-creates the
|
|
161
|
+
* `.takeover` guard may touch the lock file, so two contenders can never both
|
|
162
|
+
* remove it. Under the guard the lock is re-read, renamed to a unique name,
|
|
163
|
+
* and the moved file is compared with what was read: if it changed in between
|
|
164
|
+
* (its holder released and a new run took the lock), it is put back. Returns
|
|
165
|
+
* false when this contender should give up.
|
|
166
|
+
*/
|
|
167
|
+
async function takeOverStale(file, now, staleMs, alive) {
|
|
168
|
+
const guard = `${file}.takeover`;
|
|
169
|
+
const takeGuard = async () => {
|
|
170
|
+
try {
|
|
171
|
+
await fs.writeFile(guard, `${process.pid}\n`, { encoding: "utf8", flag: "wx" });
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
if (err.code !== "EEXIST")
|
|
176
|
+
throw err;
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
if (!(await takeGuard())) {
|
|
181
|
+
let age = 0;
|
|
182
|
+
try {
|
|
183
|
+
age = now().getTime() - (await fs.stat(guard)).mtimeMs;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (age < TAKEOVER_GUARD_STALE_MS)
|
|
189
|
+
return false;
|
|
190
|
+
const staleGuard = `${guard}.stale-${randomUUID()}`;
|
|
191
|
+
await fs.rename(guard, staleGuard).catch(() => undefined);
|
|
192
|
+
await fs.rm(staleGuard, { force: true });
|
|
193
|
+
if (!(await takeGuard()))
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const seen = await fs.readFile(file, "utf8").catch(() => null);
|
|
198
|
+
if (seen === null)
|
|
199
|
+
return true;
|
|
200
|
+
if ((await isStale(file, now().getTime(), staleMs, alive)) !== true)
|
|
201
|
+
return false;
|
|
202
|
+
const aside = `${file}.stale-${process.pid}-${randomUUID()}`;
|
|
203
|
+
try {
|
|
204
|
+
await fs.rename(file, aside);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
if (err.code === "ENOENT")
|
|
208
|
+
return true;
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
const moved = await fs.readFile(aside, "utf8").catch(() => null);
|
|
212
|
+
if (moved !== seen) {
|
|
213
|
+
await fs.link(aside, file).catch(() => undefined);
|
|
214
|
+
await fs.rm(aside, { force: true });
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
await fs.rm(aside, { force: true });
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
await fs.rm(guard, { force: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=sync-lock.js.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type HookTarget } from './claude-targets.js';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* True when this user's hq daemon unit (launchd plist, systemd unit, or the
|
|
4
|
+
* per-user Windows Scheduled Task) exists. On Windows the shared task an older
|
|
5
|
+
* install registered counts only when it runs as this user.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean, taskName?: () => string, legacyTaskIsMine?: () => boolean): boolean;
|
|
4
8
|
/**
|
|
5
9
|
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
6
10
|
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
@@ -7,15 +7,20 @@ import * as fs from 'node:fs';
|
|
|
7
7
|
import * as path from 'node:path';
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
9
|
import { realpathOrSelf } from '../../commands/mcp-registration.js';
|
|
10
|
-
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT,
|
|
10
|
+
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, SYSTEM_UNIT_DIR, currentWindowsIdentity, legacyWindowsTask, windowsTaskName } from '../daemon/install.js';
|
|
11
|
+
import { defaultRunCommand } from '../service-manager/launchd.js';
|
|
11
12
|
import { readDaemonConfig } from '../daemon/config.js';
|
|
12
13
|
import { daemonPaths } from '../daemon/paths.js';
|
|
13
14
|
import { registrationEngine } from '../hook-engine/registration.js';
|
|
14
15
|
import { shimPath } from './claude-targets.js';
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* True when this user's hq daemon unit (launchd plist, systemd unit, or the
|
|
18
|
+
* per-user Windows Scheduled Task) exists. On Windows the shared task an older
|
|
19
|
+
* install registered counts only when it runs as this user.
|
|
20
|
+
*/
|
|
21
|
+
export function isDaemonInstalled(home, platform = process.platform, queryTask = (task) => spawnSync('schtasks.exe', ['/Query', '/TN', task], { encoding: 'utf8', windowsHide: true }).status === 0, taskName = () => windowsTaskName(currentWindowsIdentity()), legacyTaskIsMine = () => legacyWindowsTask(defaultRunCommand, currentWindowsIdentity()).state === 'mine') {
|
|
17
22
|
if (platform === 'win32')
|
|
18
|
-
return queryTask(
|
|
23
|
+
return queryTask(taskName()) || legacyTaskIsMine();
|
|
19
24
|
const candidates = platform === 'darwin'
|
|
20
25
|
? [path.join(home, 'Library', 'LaunchAgents', `${DAEMON_LAUNCHD_LABEL}.plist`)]
|
|
21
26
|
: platform === 'linux'
|
package/dist/lib/lanes/loop.d.ts
CHANGED
|
@@ -222,6 +222,16 @@ export declare function inspectLoopRecovery(runDir: string): {
|
|
|
222
222
|
kind: "pending" | "hold" | "protected" | "empty";
|
|
223
223
|
reason?: string;
|
|
224
224
|
};
|
|
225
|
+
/**
|
|
226
|
+
* Budget for an envelope that carries an absolute deadline. The deadline is
|
|
227
|
+
* stamped when the phase is routed, so the budget is the time between enqueue
|
|
228
|
+
* and that deadline, counted from when the lane starts the envelope. Time the
|
|
229
|
+
* envelope spent waiting in the queue does not count against it. Without a
|
|
230
|
+
* usable queued_at the remaining time from start is used, as before.
|
|
231
|
+
* A deadline at or before its enqueue still gets a 1 s execution window.
|
|
232
|
+
* An unparsable deadline returns undefined so the caller uses the phase budget.
|
|
233
|
+
*/
|
|
234
|
+
export declare function absoluteDeadlineBudget(deadline: string, queuedAt: string | undefined, startedAtMs: number): number | undefined;
|
|
225
235
|
/** Reset only after an explicit, durable disposition; this never starts work. */
|
|
226
236
|
export declare function resetLoopAfterDisposition(runDir: string, laneId: string, decision: string, hasPending: boolean): void;
|
|
227
237
|
/** Run queued phases serially. Sessions are isolated by story and provider. */
|
package/dist/lib/lanes/loop.js
CHANGED
|
@@ -985,6 +985,23 @@ function phaseDeadlineBudget(envelope, defaultSeconds) {
|
|
|
985
985
|
const configured = Number(process.env.HQ_LANE_LOOP_PHASE_DEADLINE_SECS);
|
|
986
986
|
return Number.isFinite(configured) && configured > 0 ? configured : defaultSeconds;
|
|
987
987
|
}
|
|
988
|
+
/**
|
|
989
|
+
* Budget for an envelope that carries an absolute deadline. The deadline is
|
|
990
|
+
* stamped when the phase is routed, so the budget is the time between enqueue
|
|
991
|
+
* and that deadline, counted from when the lane starts the envelope. Time the
|
|
992
|
+
* envelope spent waiting in the queue does not count against it. Without a
|
|
993
|
+
* usable queued_at the remaining time from start is used, as before.
|
|
994
|
+
* A deadline at or before its enqueue still gets a 1 s execution window.
|
|
995
|
+
* An unparsable deadline returns undefined so the caller uses the phase budget.
|
|
996
|
+
*/
|
|
997
|
+
export function absoluteDeadlineBudget(deadline, queuedAt, startedAtMs) {
|
|
998
|
+
const deadlineMs = Date.parse(deadline);
|
|
999
|
+
if (!Number.isFinite(deadlineMs))
|
|
1000
|
+
return undefined;
|
|
1001
|
+
const queuedMs = typeof queuedAt === "string" ? Date.parse(queuedAt) : Number.NaN;
|
|
1002
|
+
const fromMs = Number.isFinite(queuedMs) && queuedMs <= startedAtMs ? queuedMs : startedAtMs;
|
|
1003
|
+
return Math.max(1, Math.round((deadlineMs - fromMs) / 1000));
|
|
1004
|
+
}
|
|
988
1005
|
function persistStallCount(runDir, laneId, envelopeId, count) {
|
|
989
1006
|
const status = loopStatus(runDir, laneId);
|
|
990
1007
|
persistStatus(runDir, laneId, { stall_counts: { ...status.stall_counts, [envelopeId]: count } });
|
|
@@ -1121,7 +1138,7 @@ export async function runLoopLane(options) {
|
|
|
1121
1138
|
const absoluteDeadline = envelope.deadline;
|
|
1122
1139
|
const budget = record.deadline_budget_seconds ?? firstDeadline.get(record.envelope_id) ??
|
|
1123
1140
|
(typeof absoluteDeadline === "string"
|
|
1124
|
-
?
|
|
1141
|
+
? absoluteDeadlineBudget(absoluteDeadline, record.queued_at, startedAtMs) ?? phaseDeadlineBudget(envelope, defaultDeadlineSeconds)
|
|
1125
1142
|
: phaseDeadlineBudget(envelope, defaultDeadlineSeconds));
|
|
1126
1143
|
firstDeadline.set(record.envelope_id, budget);
|
|
1127
1144
|
record.deadline_budget_seconds = budget;
|
|
@@ -17,6 +17,7 @@ export interface WorkCacheSubscriber {
|
|
|
17
17
|
export { AGENT_INBOX_PUSH_STATE_BASENAME, SYNC_WAKE_STATE_BASENAME } from "../paths.js";
|
|
18
18
|
export type SyncWakeVaults = Record<string, {
|
|
19
19
|
wakeSequence: number;
|
|
20
|
+
head?: number;
|
|
20
21
|
}>;
|
|
21
22
|
export declare function startWorkCacheSubscriber(opts: {
|
|
22
23
|
meshRoot: string;
|
|
@@ -15,6 +15,7 @@ const INBOX_PUSH_HEARTBEAT_MS = 5_000;
|
|
|
15
15
|
const SYNC_WAKE_KEYS = new Set(["v", "type", "vault", "head", "rootId"]);
|
|
16
16
|
const SYNC_WAKE_VAULT = /^(?:prs|cmp|agt)_[0-9A-Za-z]+$/;
|
|
17
17
|
const SYNC_WAKE_ROOT = /^[0-9a-f]{64}$/i;
|
|
18
|
+
const SYNC_WAKE_MAX_BYTES = 4_096;
|
|
18
19
|
function parseSyncWakeEnvelope(payload) {
|
|
19
20
|
let parsed;
|
|
20
21
|
try {
|
|
@@ -38,7 +39,7 @@ function parseSyncWakeEnvelope(payload) {
|
|
|
38
39
|
Number(message.head) > 0 &&
|
|
39
40
|
(message.rootId === null ||
|
|
40
41
|
(typeof message.rootId === "string" && SYNC_WAKE_ROOT.test(message.rootId)));
|
|
41
|
-
return valid ? { vault: message.vault } : null;
|
|
42
|
+
return valid ? { vault: message.vault, head: Number(message.head) } : null;
|
|
42
43
|
}
|
|
43
44
|
function readVaultSequences(raw) {
|
|
44
45
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -47,9 +48,11 @@ function readVaultSequences(raw) {
|
|
|
47
48
|
for (const [vault, entry] of Object.entries(raw)) {
|
|
48
49
|
if (!SYNC_WAKE_VAULT.test(vault) || !entry || typeof entry !== "object")
|
|
49
50
|
continue;
|
|
50
|
-
const sequence = entry
|
|
51
|
+
const { wakeSequence: sequence, head } = entry;
|
|
51
52
|
if (Number.isSafeInteger(sequence) && Number(sequence) >= 0) {
|
|
52
|
-
vaults[vault] =
|
|
53
|
+
vaults[vault] = Number.isSafeInteger(head) && Number(head) >= 0
|
|
54
|
+
? { wakeSequence: Number(sequence), head: Number(head) }
|
|
55
|
+
: { wakeSequence: Number(sequence) };
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
return vaults;
|
|
@@ -85,6 +88,9 @@ function readWakeSequence(meshRoot, basename, label, onInfo) {
|
|
|
85
88
|
};
|
|
86
89
|
}
|
|
87
90
|
}
|
|
91
|
+
function withoutHeads(vaults) {
|
|
92
|
+
return Object.fromEntries(Object.entries(vaults).map(([vault, entry]) => [vault, { wakeSequence: entry.wakeSequence }]));
|
|
93
|
+
}
|
|
88
94
|
function writeWakeState(meshRoot, basename, state, durable) {
|
|
89
95
|
fs.mkdirSync(meshRoot, { recursive: true, mode: 0o700 });
|
|
90
96
|
const rootInfo = fs.lstatSync(meshRoot);
|
|
@@ -162,15 +168,27 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
162
168
|
timers.clearTimeout(heartbeatTimer);
|
|
163
169
|
heartbeatTimer = null;
|
|
164
170
|
};
|
|
171
|
+
let syncWakeHeadsOmitted = false;
|
|
165
172
|
const persistState = (basename, label, connected, wakeSequence, identity, durable = true, vaults) => {
|
|
166
173
|
try {
|
|
167
|
-
|
|
174
|
+
const state = {
|
|
168
175
|
connected,
|
|
169
176
|
heartbeatAtMs: now().getTime(),
|
|
170
177
|
wakeSequence,
|
|
171
178
|
...(connected && actorUid && identity ? { [identity]: actorUid } : {}),
|
|
172
179
|
...(vaults ? { vaults } : {}),
|
|
173
|
-
}
|
|
180
|
+
};
|
|
181
|
+
if (vaults) {
|
|
182
|
+
const oversized = Buffer.byteLength(`${JSON.stringify(state)}\n`) > SYNC_WAKE_MAX_BYTES;
|
|
183
|
+
if (oversized) {
|
|
184
|
+
state.vaults = withoutHeads(vaults);
|
|
185
|
+
if (!syncWakeHeadsOmitted) {
|
|
186
|
+
opts.onInfo?.(`${label} state over ${SYNC_WAKE_MAX_BYTES} bytes; vault heads omitted`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
syncWakeHeadsOmitted = oversized;
|
|
190
|
+
}
|
|
191
|
+
writeWakeState(opts.meshRoot, basename, state, durable);
|
|
174
192
|
}
|
|
175
193
|
catch (error) {
|
|
176
194
|
const name = error instanceof Error ? error.name : "UnknownError";
|
|
@@ -318,8 +336,10 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
318
336
|
return;
|
|
319
337
|
}
|
|
320
338
|
syncWakeSequence += 1;
|
|
339
|
+
const previous = syncWakeVaults[envelope.vault];
|
|
321
340
|
syncWakeVaults[envelope.vault] = {
|
|
322
|
-
wakeSequence: (
|
|
341
|
+
wakeSequence: (previous?.wakeSequence ?? 0) + 1,
|
|
342
|
+
head: Math.max(previous?.head ?? 0, envelope.head),
|
|
323
343
|
};
|
|
324
344
|
persistSyncState(true);
|
|
325
345
|
return;
|
package/dist/run/hq-plugin.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ export interface PluginState {
|
|
|
18
18
|
}>;
|
|
19
19
|
loadedSecretsByName: Map<string, string>;
|
|
20
20
|
}
|
|
21
|
+
/** True when the varlock item has an `# @optional` decorator. */
|
|
22
|
+
export declare function itemIsOptional(item: unknown): boolean;
|
|
23
|
+
export declare function isSkippableOptionalSecretError(code: string | undefined): boolean;
|
|
21
24
|
export declare function installHqPlugin(graph: any, opts: InstallHqPluginOpts): PluginState;
|
|
22
25
|
export declare function prewarmHqSecrets(graph: any, opts: InstallHqPluginOpts, state: PluginState): Promise<void>;
|
|
23
26
|
//# sourceMappingURL=hq-plugin.d.ts.map
|
package/dist/run/hq-plugin.js
CHANGED
|
@@ -16,6 +16,15 @@ function normalizeCacheTtlMs(secret) {
|
|
|
16
16
|
? secret.cacheTtlMs
|
|
17
17
|
: 0;
|
|
18
18
|
}
|
|
19
|
+
const SKIPPABLE_OPTIONAL_CODES = new Set(['forbidden', 'not_found', 'not_returned']);
|
|
20
|
+
/** True when the varlock item has an `# @optional` decorator. */
|
|
21
|
+
export function itemIsOptional(item) {
|
|
22
|
+
const decorators = item?.effectiveDecorators;
|
|
23
|
+
return Boolean(decorators && Object.prototype.hasOwnProperty.call(decorators, 'optional'));
|
|
24
|
+
}
|
|
25
|
+
export function isSkippableOptionalSecretError(code) {
|
|
26
|
+
return Boolean(code && SKIPPABLE_OPTIONAL_CODES.has(code));
|
|
27
|
+
}
|
|
19
28
|
export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
20
29
|
const pluginState = {
|
|
21
30
|
schemaCompanySlug: null,
|
|
@@ -63,6 +72,10 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
|
63
72
|
}
|
|
64
73
|
const err = pluginState.errorsByName.get(secretName);
|
|
65
74
|
if (err) {
|
|
75
|
+
// `# @optional`: missing ACL or a missing secret leaves the var unset.
|
|
76
|
+
if (itemIsOptional(this._ownerItem) && isSkippableOptionalSecretError(err.code)) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
66
79
|
if (err.code === 'forbidden') {
|
|
67
80
|
throw new ResolutionError(`No read permission for secret "${secretName}" — ask an admin to share it via \`hq secrets share ${secretName} --with <you> --permission read\``);
|
|
68
81
|
}
|
|
@@ -85,13 +98,18 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
|
85
98
|
if (inMemory != null) {
|
|
86
99
|
return inMemory;
|
|
87
100
|
}
|
|
101
|
+
if (itemIsOptional(this._ownerItem)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
88
104
|
throw new ResolutionError(`Secret "${secretName}" was not returned by vault after server authorization`);
|
|
89
105
|
},
|
|
90
106
|
};
|
|
91
107
|
// Captured during process(parent); used by resolve() to fall back to the var key.
|
|
92
108
|
_ownerKey;
|
|
109
|
+
_ownerItem;
|
|
93
110
|
process(parent) {
|
|
94
111
|
super.process(parent);
|
|
112
|
+
this._ownerItem = parent;
|
|
95
113
|
if (parent != null && typeof parent.key === 'string') {
|
|
96
114
|
this._ownerKey = parent.key;
|
|
97
115
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.179",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
|
|
115
115
|
},
|
|
116
116
|
"optionalDependencies": {
|
|
117
|
-
"@indigoai-us/hq-hook-win32-x64": "5.345.
|
|
118
|
-
"@indigoai-us/hq-hook-win32-arm64": "5.345.
|
|
119
|
-
"@indigoai-us/hq-hook-darwin-x64": "5.345.
|
|
120
|
-
"@indigoai-us/hq-hook-darwin-arm64": "5.345.
|
|
121
|
-
"@indigoai-us/hq-hook-linux-x64": "5.345.
|
|
122
|
-
"@indigoai-us/hq-hook-linux-arm64": "5.345.
|
|
117
|
+
"@indigoai-us/hq-hook-win32-x64": "5.345.179",
|
|
118
|
+
"@indigoai-us/hq-hook-win32-arm64": "5.345.179",
|
|
119
|
+
"@indigoai-us/hq-hook-darwin-x64": "5.345.179",
|
|
120
|
+
"@indigoai-us/hq-hook-darwin-arm64": "5.345.179",
|
|
121
|
+
"@indigoai-us/hq-hook-linux-x64": "5.345.179",
|
|
122
|
+
"@indigoai-us/hq-hook-linux-arm64": "5.345.179"
|
|
123
123
|
}
|
|
124
124
|
}
|