@indigoai-us/hq-cli 5.345.156 → 5.345.160
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 +255 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +535 -10
- package/dist/command-catalog.generated.js +678 -31
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/access.d.ts +6 -0
- package/dist/commands/access.js +18 -1
- package/dist/commands/acl-lock.d.ts +41 -0
- package/dist/commands/acl-lock.js +28 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +9 -4
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/agents.js +5 -11
- package/dist/commands/billing-sponsor.d.ts +4 -4
- package/dist/commands/billing-sponsor.js +11 -11
- package/dist/commands/billing-usage.d.ts +22 -8
- package/dist/commands/billing-usage.js +57 -22
- package/dist/commands/billing.d.ts +7 -7
- package/dist/commands/billing.js +13 -13
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud-provision.js +2 -2
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +157 -17
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +12 -0
- package/dist/commands/crm.js +83 -6
- package/dist/commands/db-remote-deps.js +12 -1
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +71 -8
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/meetings.d.ts +4 -4
- package/dist/commands/meetings.js +17 -8
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +21 -0
- package/dist/commands/secrets.js +114 -14
- package/dist/commands/signals.js +17 -5
- package/dist/commands/skill.js +6 -6
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/commands/whoami.js +1 -1
- package/dist/hook-engine-path.js +13 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
- package/dist/lib/billing/plan-limit-refusals.js +26 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
- package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
- package/dist/lib/billing/plan-lock.d.ts +22 -6
- package/dist/lib/billing/plan-lock.js +58 -30
- package/dist/lib/billing/usage-cost.d.ts +24 -2
- package/dist/lib/billing/usage-cost.js +103 -3
- package/dist/lib/billing/usage-only.d.ts +37 -1
- package/dist/lib/billing/usage-only.js +89 -4
- package/dist/lib/billing/usage-pause.d.ts +27 -0
- package/dist/lib/billing/usage-pause.js +50 -0
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.d.ts +14 -0
- package/dist/lib/hook-engine/resolve.js +55 -1
- package/dist/lib/index-render/company-knowledge.js +1 -3
- package/dist/lib/index-render/projects.js +1 -3
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- package/dist/utils/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
package/dist/lib/lanes/store.js
CHANGED
|
@@ -24,7 +24,13 @@ export const LANES_STORE_REL = path.join("workspace", "lanes");
|
|
|
24
24
|
export const LANE_RECORDS_DIR = "lanes";
|
|
25
25
|
export const QUESTION_RECORDS_DIR = "questions";
|
|
26
26
|
export const PENDING_INDEX_DIR = path.join("questions", "index", "pending");
|
|
27
|
-
const
|
|
27
|
+
const DEFAULT_LOCK_STALE_MS = 30_000;
|
|
28
|
+
/** Lock age after which any holder is presumed gone. HQ_LANES_LOCK_STALE_MS overrides it. */
|
|
29
|
+
function lockStaleMs(env = process.env) {
|
|
30
|
+
const raw = env.HQ_LANES_LOCK_STALE_MS;
|
|
31
|
+
const parsed = raw === undefined ? NaN : Number(raw);
|
|
32
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_LOCK_STALE_MS;
|
|
33
|
+
}
|
|
28
34
|
const LOCK_RETRY_MS = 10;
|
|
29
35
|
const LOCK_TIMEOUT_MS = 5_000;
|
|
30
36
|
function defaultStoreDeps() {
|
|
@@ -153,16 +159,104 @@ function wait(ms) {
|
|
|
153
159
|
function lockPathFor(recordPath) {
|
|
154
160
|
return `${recordPath}.lock`;
|
|
155
161
|
}
|
|
156
|
-
function
|
|
162
|
+
function lockOwnerAlive(pid) {
|
|
163
|
+
try {
|
|
164
|
+
process.kill(pid, 0);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
return err.code !== "ESRCH";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function sameFile(a, b) {
|
|
172
|
+
return a.ino === b.ino && a.dev === b.dev;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Remove a record lock that no live process holds, and report whether it was
|
|
176
|
+
* removed. A lock is stale when it was written on this host and its owner pid
|
|
177
|
+
* no longer exists, or when it is older than the stale window. Age is the
|
|
178
|
+
* larger of the timestamp age and the file mtime age, so a future or
|
|
179
|
+
* unparseable timestamp still ages out through mtime.
|
|
180
|
+
*
|
|
181
|
+
* Removal is atomic: the lock is renamed to a unique name, which only one
|
|
182
|
+
* reclaimer can win. If the renamed file is not the lock that was inspected
|
|
183
|
+
* (another process reclaimed it and took a fresh lock in between), the fresh
|
|
184
|
+
* lock is linked back into place and nothing is reclaimed.
|
|
185
|
+
*/
|
|
186
|
+
function recordLockIsStale(lockPath, deps) {
|
|
187
|
+
const statSync = deps.statSync ?? ((p) => fs.statSync(p));
|
|
188
|
+
const linkSync = deps.linkSync ?? fs.linkSync.bind(fs);
|
|
189
|
+
const before = statSync(lockPath);
|
|
190
|
+
const raw = deps.readFileSync(lockPath, "utf8");
|
|
191
|
+
const [pidLine, tsLine, hostLine] = raw.split("\n");
|
|
192
|
+
const pid = pidLine ? Number(pidLine) : NaN;
|
|
193
|
+
const ts = tsLine ? Number(tsLine) : NaN;
|
|
194
|
+
const pidKnown = Number.isSafeInteger(pid) && pid > 0;
|
|
195
|
+
// A pid is only meaningful on the host that wrote it. A lock with a foreign
|
|
196
|
+
// or missing host falls back to the age rule.
|
|
197
|
+
const sameHost = hostLine !== undefined && hostLine.trim() !== "" && hostLine.trim() === deps.hostname();
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const mtimeAge = now - before.mtimeMs;
|
|
200
|
+
const age = Number.isFinite(ts) ? Math.max(now - ts, mtimeAge) : mtimeAge;
|
|
201
|
+
const stale = (pidKnown && sameHost && pid !== process.pid && !lockOwnerAlive(pid)) ||
|
|
202
|
+
age > lockStaleMs();
|
|
203
|
+
if (!stale)
|
|
204
|
+
return false;
|
|
205
|
+
const parked = `${lockPath}.stale.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
206
|
+
try {
|
|
207
|
+
deps.renameSync(lockPath, parked);
|
|
208
|
+
}
|
|
209
|
+
catch (renameErr) {
|
|
210
|
+
// Another reclaimer won the rename, or the holder released it.
|
|
211
|
+
if (renameErr.code === "ENOENT")
|
|
212
|
+
return false;
|
|
213
|
+
throw renameErr;
|
|
214
|
+
}
|
|
215
|
+
const removeParked = () => {
|
|
216
|
+
try {
|
|
217
|
+
deps.unlinkSync(parked);
|
|
218
|
+
}
|
|
219
|
+
catch (unlinkErr) {
|
|
220
|
+
if (unlinkErr.code !== "ENOENT")
|
|
221
|
+
throw unlinkErr;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
let reclaimed;
|
|
225
|
+
try {
|
|
226
|
+
reclaimed = sameFile(statSync(parked), before) && deps.readFileSync(parked, "utf8") === raw;
|
|
227
|
+
if (!reclaimed) {
|
|
228
|
+
// The parked file is a live holder's fresh lock. Put it back.
|
|
229
|
+
try {
|
|
230
|
+
linkSync(parked, lockPath);
|
|
231
|
+
}
|
|
232
|
+
catch (linkErr) {
|
|
233
|
+
if (linkErr.code !== "EEXIST")
|
|
234
|
+
throw linkErr;
|
|
235
|
+
process.stderr.write(`lanes store: could not restore displaced lock ${lockPath}; a newer lock already exists\n`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
removeParked();
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
removeParked();
|
|
244
|
+
return reclaimed;
|
|
245
|
+
}
|
|
246
|
+
function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
157
247
|
const lockPath = lockPathFor(recordPath);
|
|
248
|
+
const statSync = deps.statSync ?? ((p) => fs.statSync(p));
|
|
249
|
+
const fstatSync = deps.fstatSync ?? ((fd) => fs.fstatSync(fd));
|
|
158
250
|
ensureDir(path.dirname(recordPath), deps);
|
|
159
251
|
const started = Date.now();
|
|
160
252
|
while (true) {
|
|
161
253
|
let fd;
|
|
162
254
|
try {
|
|
163
255
|
fd = deps.openSync(lockPath, "wx");
|
|
256
|
+
let held;
|
|
164
257
|
try {
|
|
165
|
-
fs.writeSync(fd, Buffer.from(`${process.pid}\n${Date.now()}\n`, "utf8"));
|
|
258
|
+
fs.writeSync(fd, Buffer.from(`${process.pid}\n${Date.now()}\n${deps.hostname()}\n`, "utf8"));
|
|
259
|
+
held = fstatSync(fd);
|
|
166
260
|
}
|
|
167
261
|
catch (err) {
|
|
168
262
|
deps.closeSync(fd);
|
|
@@ -180,7 +274,10 @@ function withRecordLock(recordPath, deps, fn) {
|
|
|
180
274
|
process.stderr.write(`lanes store: lock close failed for ${lockPath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
181
275
|
}
|
|
182
276
|
try {
|
|
183
|
-
|
|
277
|
+
// Unlink only our own lock. If it was reclaimed and replaced while
|
|
278
|
+
// fn ran, the file at lockPath belongs to the successor.
|
|
279
|
+
if (sameFile(statSync(lockPath), held))
|
|
280
|
+
deps.unlinkSync(lockPath);
|
|
184
281
|
}
|
|
185
282
|
catch (err) {
|
|
186
283
|
const code = err.code;
|
|
@@ -194,24 +291,12 @@ function withRecordLock(recordPath, deps, fn) {
|
|
|
194
291
|
const code = err.code;
|
|
195
292
|
if (code !== "EEXIST")
|
|
196
293
|
throw err;
|
|
197
|
-
if (Date.now() - started >
|
|
294
|
+
if (Date.now() - started > timeoutMs) {
|
|
198
295
|
throw new LanesError("store_lock_timeout", `Timed out waiting for lock ${lockPath}`, { path: lockPath });
|
|
199
296
|
}
|
|
200
297
|
try {
|
|
201
|
-
|
|
202
|
-
const tsLine = raw.split("\n")[1];
|
|
203
|
-
const ts = tsLine ? Number(tsLine) : NaN;
|
|
204
|
-
if (Number.isFinite(ts) && Date.now() - ts > LOCK_STALE_MS) {
|
|
205
|
-
try {
|
|
206
|
-
deps.unlinkSync(lockPath);
|
|
207
|
-
}
|
|
208
|
-
catch (unlinkErr) {
|
|
209
|
-
const unlinkCode = unlinkErr.code;
|
|
210
|
-
if (unlinkCode !== "ENOENT")
|
|
211
|
-
throw unlinkErr;
|
|
212
|
-
}
|
|
298
|
+
if (recordLockIsStale(lockPath, deps))
|
|
213
299
|
continue;
|
|
214
|
-
}
|
|
215
300
|
}
|
|
216
301
|
catch (statErr) {
|
|
217
302
|
const statCode = statErr.code;
|
|
@@ -223,6 +308,10 @@ function withRecordLock(recordPath, deps, fn) {
|
|
|
223
308
|
}
|
|
224
309
|
}
|
|
225
310
|
}
|
|
311
|
+
/** Test seam: run withRecordLock with overridden deps and a shorter timeout. */
|
|
312
|
+
export function withRecordLockForTests(recordPath, overrides, fn, timeoutMs) {
|
|
313
|
+
return withRecordLock(recordPath, { ...defaultStoreDeps(), ...overrides }, fn, timeoutMs);
|
|
314
|
+
}
|
|
226
315
|
/** Run a lane-owned file read/modify/write transaction under its sidecar lock. */
|
|
227
316
|
export function withLaneRecordFileLock(recordPath, fn) {
|
|
228
317
|
return withRecordLock(recordPath, defaultStoreDeps(), fn);
|
|
@@ -1029,6 +1029,8 @@ export interface LaneEnvelope {
|
|
|
1029
1029
|
fingerprint?: string | null;
|
|
1030
1030
|
}
|
|
1031
1031
|
export interface AccountConfig {
|
|
1032
|
+
/** Company slug or UID owning this account. Untagged accounts are personal. */
|
|
1033
|
+
company?: string;
|
|
1032
1034
|
home: string;
|
|
1033
1035
|
/** Optional account home overrides keyed by the owning host's hostname. */
|
|
1034
1036
|
hosts?: Record<string, AccountHostHome>;
|
package/dist/lib/lanes/watch.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { loopControllerIsAlive, loopStatus } from "./loop.js";
|
|
4
|
+
import { laneRunDir } from "./spawn.js";
|
|
1
5
|
import { expandLocalLaneIdPrefix, listQuestionIds, readLane, readQuestion, } from "./store.js";
|
|
2
6
|
import { LaneNotFoundError, LanesError } from "./errors.js";
|
|
3
7
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
@@ -118,6 +122,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
118
122
|
while (true) {
|
|
119
123
|
polls += 1;
|
|
120
124
|
const records = [];
|
|
125
|
+
const activeLoops = new Set();
|
|
121
126
|
for (const laneId of watchedIds) {
|
|
122
127
|
let record = null;
|
|
123
128
|
try {
|
|
@@ -131,9 +136,27 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
131
136
|
continue;
|
|
132
137
|
}
|
|
133
138
|
records.push(record);
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
if (record.loop_mode === true) {
|
|
140
|
+
try {
|
|
141
|
+
const runDir = laneRunDir(hqRoot, record.lane_id);
|
|
142
|
+
// loopStatus defaults missing metadata to waiting; that is not
|
|
143
|
+
// evidence of a controller after a failed admission or launch.
|
|
144
|
+
if (fs.existsSync(path.join(runDir, "loop", "loop.json"))) {
|
|
145
|
+
const status = loopStatus(runDir, record.lane_id);
|
|
146
|
+
if (status.state !== "stopped" && status.state !== "failed" && loopControllerIsAlive(runDir))
|
|
147
|
+
activeLoops.add(record.lane_id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
unreadable("loop", laneId, err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// A phase can be done while its loop controller awaits more work.
|
|
155
|
+
const terminal = isTerminalLaneState(record.state) && !activeLoops.has(record.lane_id);
|
|
156
|
+
const stateKey = `${record.state}:${terminal}`;
|
|
157
|
+
if (previousStates.get(laneId) !== stateKey) {
|
|
158
|
+
emit(`lane ${record.lane_id}: state=${record.state}${terminal ? " (terminal)" : ""}`);
|
|
159
|
+
previousStates.set(laneId, stateKey);
|
|
137
160
|
}
|
|
138
161
|
}
|
|
139
162
|
const keepAliveWaiting = new Set();
|
|
@@ -175,7 +198,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
175
198
|
}
|
|
176
199
|
if (deps.readAccountStatuses !== undefined) {
|
|
177
200
|
const accountLanes = records.filter((record) => record.account !== undefined &&
|
|
178
|
-
(!isTerminalLaneState(record.state) || keepAliveWaiting.has(record.lane_id)));
|
|
201
|
+
(!isTerminalLaneState(record.state) || activeLoops.has(record.lane_id) || keepAliveWaiting.has(record.lane_id)));
|
|
179
202
|
let statuses;
|
|
180
203
|
try {
|
|
181
204
|
statuses = await deps.readAccountStatuses(accountLanes);
|
|
@@ -242,7 +265,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
242
265
|
previousAccountLogin.set(key, status.logged_in);
|
|
243
266
|
}
|
|
244
267
|
}
|
|
245
|
-
if (records.every((record) => isTerminalLaneState(record.state) && !keepAliveWaiting.has(record.lane_id))) {
|
|
268
|
+
if (records.every((record) => isTerminalLaneState(record.state) && !activeLoops.has(record.lane_id) && !keepAliveWaiting.has(record.lane_id))) {
|
|
246
269
|
return { polls, lines, suppressed };
|
|
247
270
|
}
|
|
248
271
|
await sleep(intervalMs);
|
|
@@ -12,6 +12,7 @@ import { isMainModule } from "../../utils/is-main-module.js";
|
|
|
12
12
|
import { runWatcherLoop } from "./watcher.js";
|
|
13
13
|
import { spawnEnvelopeRepairViaCurrentCli, } from "./envelope-repair-dispatch.js";
|
|
14
14
|
import { LanesError } from "./errors.js";
|
|
15
|
+
import { liveNodeBinary } from "./node-binary.js";
|
|
15
16
|
import { openMeshTransport } from "../mesh/client.js";
|
|
16
17
|
import { CompanySelectionError } from "../../utils/company-selection-error.js";
|
|
17
18
|
export function watcherMainDeps(hqRoot, laneId, env = process.env, repairDispatchDeps = {}) {
|
|
@@ -21,7 +22,7 @@ export function watcherMainDeps(hqRoot, laneId, env = process.env, repairDispatc
|
|
|
21
22
|
spawnRepair: (root, lane, input) => spawnEnvelopeRepairViaCurrentCli(root, lane, input, { ...repairDispatchDeps, env }),
|
|
22
23
|
deliverLaneSeniorMessage: async (seniorLaneId, text) => deliverLaneSeniorMessage(cliEntry, seniorLaneId, text),
|
|
23
24
|
recoverQueuedMessages: async () => {
|
|
24
|
-
const result = spawnSync(
|
|
25
|
+
const result = spawnSync(liveNodeBinary(), [cliEntry, "lanes", "_recover-queued-messages", laneId], {
|
|
25
26
|
env,
|
|
26
27
|
encoding: "utf8",
|
|
27
28
|
timeout: 120_000,
|
|
@@ -34,7 +35,7 @@ export function watcherMainDeps(hqRoot, laneId, env = process.env, repairDispatc
|
|
|
34
35
|
},
|
|
35
36
|
scheduleTerminalKeepAlive: async ({ laneId: scheduledLaneId, version, dueAt }) => {
|
|
36
37
|
await new Promise((resolve, reject) => {
|
|
37
|
-
const child = spawn(
|
|
38
|
+
const child = spawn(liveNodeBinary(), [
|
|
38
39
|
cliEntry,
|
|
39
40
|
"lanes",
|
|
40
41
|
"_keep-alive-terminal-trigger",
|
|
@@ -54,7 +55,7 @@ export function watcherMainDeps(hqRoot, laneId, env = process.env, repairDispatc
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
function deliverLaneSeniorMessage(cliEntry, laneId, text) {
|
|
57
|
-
const result = spawnSync(
|
|
58
|
+
const result = spawnSync(liveNodeBinary(), [cliEntry, "lanes", "message", laneId, "--text", text], {
|
|
58
59
|
env: process.env,
|
|
59
60
|
encoding: "utf8",
|
|
60
61
|
timeout: 120_000,
|
|
@@ -201,6 +201,8 @@ export interface WatcherLoopDeps {
|
|
|
201
201
|
progressEventMinIntervalMs?: number;
|
|
202
202
|
/** Bound a hung mesh request so watcher liveness polling continues. */
|
|
203
203
|
meshRequestTimeoutMs?: number;
|
|
204
|
+
/** Bound on waiting for an older run's watcher to release the claim. */
|
|
205
|
+
claimWaitMs?: number;
|
|
204
206
|
/** Spec §4 repair spawn. Default: `spawnEnvelopeRepair`. */
|
|
205
207
|
spawnRepair?: (hqRoot: string, lane: LaneRecord, input: EnvelopeRepairSpawnInput) => number;
|
|
206
208
|
/** Rebind and respawn after a provider quota or authentication refusal. */
|
|
@@ -247,6 +249,8 @@ export interface WatcherLoopDeps {
|
|
|
247
249
|
export declare function emitMeshSoft(event: string, logDecision: (record: Record<string, unknown>) => void, at: string, fn: (signal: AbortSignal) => Promise<void>, timeoutMs?: number): Promise<boolean>;
|
|
248
250
|
/** Question ids an ask envelope raised, as `raiseEnvelopeQuestions` mints them. */
|
|
249
251
|
export declare function envelopeQuestionIds(laneId: string, envelopeRef: string, envelope: LaneEnvelope, questionIndexes?: readonly number[]): string[];
|
|
252
|
+
/** How long a new phase watcher waits for an older phase's watcher to let go. */
|
|
253
|
+
export declare const WATCHER_CLAIM_WAIT_MS = 60000;
|
|
250
254
|
/** Detached and synchronous classifiers share one process-identity claim. */
|
|
251
255
|
export declare function runWatcherLoop(hqRoot: string, laneId: string, deps?: WatcherLoopDeps): Promise<void>;
|
|
252
256
|
export type EnvelopeRepairDecision = {
|
|
@@ -825,7 +825,7 @@ async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, l
|
|
|
825
825
|
written = false;
|
|
826
826
|
}
|
|
827
827
|
if (written) {
|
|
828
|
-
return
|
|
828
|
+
return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
829
829
|
if (current.status_gate?.turn_id !== gate.turn_id || current.status_gate.consecutive_misses === 0)
|
|
830
830
|
return current;
|
|
831
831
|
return { ...current, status_gate: { ...current.status_gate, consecutive_misses: 0 } };
|
|
@@ -837,7 +837,7 @@ async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, l
|
|
|
837
837
|
path: gate.status_path,
|
|
838
838
|
at,
|
|
839
839
|
identified_by: target.identified_by ?? "juniors",
|
|
840
|
-
}, resumeText);
|
|
840
|
+
}, resumeText, (current) => followupRunMoved(hqRoot, lane.lane_id, deps, current));
|
|
841
841
|
const miss = updated.status_gate_history?.find((entry) => entry.event === "status_missed" && entry.turn_id === gate.turn_id);
|
|
842
842
|
if (miss) {
|
|
843
843
|
logDecision({
|
|
@@ -876,7 +876,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
876
876
|
if (resolution.stale.length === 0) {
|
|
877
877
|
if (lane.blocker_freshness_state === undefined)
|
|
878
878
|
return lane;
|
|
879
|
-
return
|
|
879
|
+
return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => current.blocker_freshness_state === undefined
|
|
880
880
|
? current
|
|
881
881
|
: { ...current, blocker_freshness_state: undefined });
|
|
882
882
|
}
|
|
@@ -895,7 +895,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
895
895
|
? findBlockerFreshnessSessionInbox(hqRoot, lane.senior.id)
|
|
896
896
|
: { reason: "The session senior has no session id" }
|
|
897
897
|
: undefined;
|
|
898
|
-
let updated =
|
|
898
|
+
let updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
899
899
|
deliverEscalation = false;
|
|
900
900
|
deliverFallback = false;
|
|
901
901
|
retryUndeliverable = false;
|
|
@@ -1014,7 +1014,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
1014
1014
|
kind: "blocker_stale_escalation_undeliverable",
|
|
1015
1015
|
text: `${escalationText} The session senior inbox is unavailable (${reason}); this escalation is recorded in the lane inbox for its next turn.`,
|
|
1016
1016
|
});
|
|
1017
|
-
updated =
|
|
1017
|
+
updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1018
1018
|
const state = current.blocker_freshness_state;
|
|
1019
1019
|
if (state === undefined)
|
|
1020
1020
|
return current;
|
|
@@ -1037,7 +1037,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
1037
1037
|
const message = error instanceof Error ? error.message : String(error);
|
|
1038
1038
|
process.stderr.write(`hq lanes watcher: blocker freshness fallback delivery failed for ${lane.lane_id}: ${message}\n`);
|
|
1039
1039
|
logDecision({ event: "blocker_stale_escalation_fallback_failed", at, turn_id: turnId, fingerprint, error: message });
|
|
1040
|
-
updated =
|
|
1040
|
+
updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1041
1041
|
const state = current.blocker_freshness_state;
|
|
1042
1042
|
const failureReason = `${undeliverableReason ?? `Session ${lane.senior.id ?? "unknown"} has no monitor-session inbox`}; lane inbox fallback failed: ${message}`;
|
|
1043
1043
|
return {
|
|
@@ -1080,7 +1080,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
1080
1080
|
}
|
|
1081
1081
|
logDecision({ event: "blocker_stale_escalation_delivered", at, turn_id: turnId, senior_kind: lane.senior.kind });
|
|
1082
1082
|
if (retryUndeliverable) {
|
|
1083
|
-
updated =
|
|
1083
|
+
updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1084
1084
|
const state = current.blocker_freshness_state;
|
|
1085
1085
|
const failedFallbackStillOpen = (current.status_gate_history ?? []).some((entry) => entry.event === "blocker_stale_escalation_undeliverable" &&
|
|
1086
1086
|
entry.fingerprint === fingerprint && entry.delivery_failed === true &&
|
|
@@ -1104,7 +1104,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
1104
1104
|
const message = error instanceof Error ? error.message : String(error);
|
|
1105
1105
|
process.stderr.write(`hq lanes watcher: blocker freshness escalation delivery failed for ${lane.lane_id}: ${message}\n`);
|
|
1106
1106
|
logDecision({ event: "blocker_stale_escalation_delivery_failed", at, turn_id: turnId, error: message });
|
|
1107
|
-
updated =
|
|
1107
|
+
updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1108
1108
|
const state = current.blocker_freshness_state;
|
|
1109
1109
|
if (state?.escalated_fingerprint !== fingerprint)
|
|
1110
1110
|
return current;
|
|
@@ -1151,7 +1151,7 @@ async function settleKrFreshness(hqRoot, lane, manifestTurnId, at, deps, logDeci
|
|
|
1151
1151
|
if (stalled.length === 0) {
|
|
1152
1152
|
if (lane.kr_freshness_state === undefined)
|
|
1153
1153
|
return lane;
|
|
1154
|
-
return
|
|
1154
|
+
return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => current.kr_freshness_state === undefined
|
|
1155
1155
|
? current
|
|
1156
1156
|
: { ...current, kr_freshness_state: undefined });
|
|
1157
1157
|
}
|
|
@@ -1159,7 +1159,7 @@ async function settleKrFreshness(hqRoot, lane, manifestTurnId, at, deps, logDeci
|
|
|
1159
1159
|
const names = stalled.map((kpi) => kpi.name);
|
|
1160
1160
|
const turnId = manifestTurnId ?? lane.status_gate?.turn_id ?? `unknown-${at}`;
|
|
1161
1161
|
const resumeText = `These KPIs in ${resolution.statusPath} were not re-measured in the last ${resolution.windowHours}h: ${names.join(", ")}. Measure each one now, update value and measured_at, then end the turn.`;
|
|
1162
|
-
return
|
|
1162
|
+
return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1163
1163
|
const history = current.status_gate_history ?? [];
|
|
1164
1164
|
if (history.some((entry) => entry.event === "kr_stalled" && entry.turn_id === turnId))
|
|
1165
1165
|
return current;
|
|
@@ -1297,6 +1297,38 @@ function defaultSleep(ms) {
|
|
|
1297
1297
|
});
|
|
1298
1298
|
}
|
|
1299
1299
|
const WATCHER_LOOP_OWNER_FILE = "watcher-loop.pid";
|
|
1300
|
+
/** Run identity (spawn manifest envelope path) of the current claimant. */
|
|
1301
|
+
const WATCHER_LOOP_RUN_FILE = "watcher-loop.run";
|
|
1302
|
+
/** How long a new phase watcher waits for an older phase's watcher to let go. */
|
|
1303
|
+
export const WATCHER_CLAIM_WAIT_MS = 60_000;
|
|
1304
|
+
const WATCHER_CLAIM_RETRY_MS = 250;
|
|
1305
|
+
/** Identifies one worker run of a lane; changes on every resume. */
|
|
1306
|
+
function watcherRunId(runDir) {
|
|
1307
|
+
try {
|
|
1308
|
+
return readSpawnManifest(runDir).envelope_path ?? "";
|
|
1309
|
+
}
|
|
1310
|
+
catch {
|
|
1311
|
+
return "";
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/** True when the lane has moved on to a run this watcher does not own. */
|
|
1315
|
+
function followupRunMoved(hqRoot, laneId, deps, current) {
|
|
1316
|
+
if (deps.followupRun === undefined)
|
|
1317
|
+
return false;
|
|
1318
|
+
return current.state === "running" || watcherRunId(laneRunDir(hqRoot, laneId)) !== deps.followupRun;
|
|
1319
|
+
}
|
|
1320
|
+
/** Watcher lane write that is a no-op once the lane belongs to a newer run. */
|
|
1321
|
+
function updateLaneFollowup(hqRoot, laneId, deps, updater) {
|
|
1322
|
+
return retryUpdateLane(hqRoot, laneId, "watcher", (current) => followupRunMoved(hqRoot, laneId, deps, current) ? current : updater(current));
|
|
1323
|
+
}
|
|
1324
|
+
function readClaimRunId(runDir) {
|
|
1325
|
+
try {
|
|
1326
|
+
return fs.readFileSync(path.join(runDir, WATCHER_LOOP_RUN_FILE), "utf8").trim();
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1300
1332
|
function claimWatcherLoop(hqRoot, laneId, deps) {
|
|
1301
1333
|
const runDir = laneRunDir(hqRoot, laneId);
|
|
1302
1334
|
const ownerPath = path.join(runDir, WATCHER_LOOP_OWNER_FILE);
|
|
@@ -1310,6 +1342,7 @@ function claimWatcherLoop(hqRoot, laneId, deps) {
|
|
|
1310
1342
|
}
|
|
1311
1343
|
if (existing !== null)
|
|
1312
1344
|
fs.unlinkSync(ownerPath);
|
|
1345
|
+
fs.writeFileSync(path.join(runDir, WATCHER_LOOP_RUN_FILE), `${watcherRunId(runDir)}\n`, "utf8");
|
|
1313
1346
|
writePidRecord(ownerPath, {
|
|
1314
1347
|
pid: process.pid,
|
|
1315
1348
|
starttime: readStarttime(process.pid) ?? readProcStarttime(process.pid) ?? undefined,
|
|
@@ -1319,7 +1352,11 @@ function claimWatcherLoop(hqRoot, laneId, deps) {
|
|
|
1319
1352
|
});
|
|
1320
1353
|
if (owner === null)
|
|
1321
1354
|
return null;
|
|
1355
|
+
let released = false;
|
|
1322
1356
|
return () => {
|
|
1357
|
+
if (released)
|
|
1358
|
+
return;
|
|
1359
|
+
released = true;
|
|
1323
1360
|
withLaneHireLocks(hqRoot, [laneId], () => {
|
|
1324
1361
|
try {
|
|
1325
1362
|
const current = fs.statSync(ownerPath);
|
|
@@ -1391,7 +1428,7 @@ async function maybeContinueAfterMissingEnvelopeRepair(hqRoot, lane, envelope, e
|
|
|
1391
1428
|
}
|
|
1392
1429
|
if (!enabled)
|
|
1393
1430
|
return lane;
|
|
1394
|
-
const queued =
|
|
1431
|
+
const queued = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1395
1432
|
if (current.version !== lane.version ||
|
|
1396
1433
|
(current.state !== "done" && current.state !== "blocked") ||
|
|
1397
1434
|
current.last_envelope?.ref !== envelopeRef ||
|
|
@@ -1421,17 +1458,59 @@ async function maybeContinueAfterMissingEnvelopeRepair(hqRoot, lane, envelope, e
|
|
|
1421
1458
|
}
|
|
1422
1459
|
/** Detached and synchronous classifiers share one process-identity claim. */
|
|
1423
1460
|
export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
1424
|
-
|
|
1425
|
-
if (release === null)
|
|
1426
|
-
|
|
1461
|
+
let release = claimWatcherLoop(hqRoot, laneId, deps);
|
|
1462
|
+
if (release === null) {
|
|
1463
|
+
// A live claimant from an older run (e.g. the previous phase's watcher
|
|
1464
|
+
// still finishing its post-terminal mesh call) lets go soon. Wait for it
|
|
1465
|
+
// instead of leaving this run unwatched. A claimant on the same run is a
|
|
1466
|
+
// true duplicate and this watcher exits at once.
|
|
1467
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
1468
|
+
const now = deps.now ?? (() => Date.now());
|
|
1469
|
+
const runDir = laneRunDir(hqRoot, laneId);
|
|
1470
|
+
const myRun = watcherRunId(runDir);
|
|
1471
|
+
const waitMs = deps.claimWaitMs ?? WATCHER_CLAIM_WAIT_MS;
|
|
1472
|
+
const started = now();
|
|
1473
|
+
let claimantRun = readClaimRunId(runDir);
|
|
1474
|
+
while (release === null && claimantRun !== myRun && now() - started < waitMs) {
|
|
1475
|
+
await sleep(WATCHER_CLAIM_RETRY_MS);
|
|
1476
|
+
release = claimWatcherLoop(hqRoot, laneId, deps);
|
|
1477
|
+
if (release === null)
|
|
1478
|
+
claimantRun = readClaimRunId(runDir);
|
|
1479
|
+
}
|
|
1480
|
+
if (release === null) {
|
|
1481
|
+
let claimantPid = null;
|
|
1482
|
+
try {
|
|
1483
|
+
claimantPid = readPidRecord(path.join(runDir, WATCHER_LOOP_OWNER_FILE))?.pid ?? null;
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
claimantPid = null;
|
|
1487
|
+
}
|
|
1488
|
+
const record = {
|
|
1489
|
+
event: "watcher_claim_lost",
|
|
1490
|
+
at: new Date(now()).toISOString(),
|
|
1491
|
+
claimant_pid: claimantPid,
|
|
1492
|
+
claimant_run: claimantRun,
|
|
1493
|
+
run: myRun,
|
|
1494
|
+
waited_ms: now() - started,
|
|
1495
|
+
};
|
|
1496
|
+
try {
|
|
1497
|
+
writeWatcherDecision(deps.watcherOutPath ?? watcherOutLogPath(hqRoot, laneId), record, deps.watcherOutMaxBytes ?? WATCHER_OUT_MAX_BYTES);
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
// Logging is best effort; stderr below still records the loss.
|
|
1501
|
+
}
|
|
1502
|
+
process.stderr.write(`hq lanes watcher: claim lost for ${laneId} to pid ${claimantPid ?? "unknown"}\n`);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1427
1506
|
try {
|
|
1428
|
-
await runWatcherLoopOwned(hqRoot, laneId, deps);
|
|
1507
|
+
await runWatcherLoopOwned(hqRoot, laneId, deps, release);
|
|
1429
1508
|
}
|
|
1430
1509
|
finally {
|
|
1431
1510
|
release();
|
|
1432
1511
|
}
|
|
1433
1512
|
}
|
|
1434
|
-
async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
1513
|
+
async function runWatcherLoopOwned(hqRoot, laneId, deps, releaseClaim = () => { }) {
|
|
1435
1514
|
const sleep = deps.sleep ?? defaultSleep;
|
|
1436
1515
|
const now = deps.now ?? (() => Date.now());
|
|
1437
1516
|
const isAlive = deps.isAlive ?? isPidAlive;
|
|
@@ -1459,6 +1538,7 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1459
1538
|
const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
|
|
1460
1539
|
const storyStatusSuppressions = new Set();
|
|
1461
1540
|
const meshRequestTimeoutMs = deps.meshRequestTimeoutMs ?? 10_000;
|
|
1541
|
+
const ownRun = watcherRunId(runDir);
|
|
1462
1542
|
const initialLane = readLaneSoft(hqRoot, laneId);
|
|
1463
1543
|
if (mesh && initialLane) {
|
|
1464
1544
|
// Every armed watcher refreshes the server descriptor. This migrates old
|
|
@@ -1889,6 +1969,12 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1889
1969
|
state: updated.state,
|
|
1890
1970
|
version: updated.version,
|
|
1891
1971
|
});
|
|
1972
|
+
// The lane is classified and this watcher stops polling after this
|
|
1973
|
+
// tick. Let go of the single-owner claim before the slow mesh and
|
|
1974
|
+
// follow-up awaits, so a watcher armed for the next phase can claim.
|
|
1975
|
+
if (tick.done)
|
|
1976
|
+
releaseClaim();
|
|
1977
|
+
const followupDeps = tick.done ? { ...deps, followupRun: ownRun } : deps;
|
|
1892
1978
|
// Spec §8 terminal rows: done → lane_done + story review; blocked →
|
|
1893
1979
|
// lane_blocked; ask → lane_ask with the queue ids. After the state
|
|
1894
1980
|
// write, so a mesh failure can never leave the lane unclassified.
|
|
@@ -1914,9 +2000,9 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1914
2000
|
}
|
|
1915
2001
|
}
|
|
1916
2002
|
if (terminalWrite.changed) {
|
|
1917
|
-
updated = await settleManagerStatusGate(hqRoot, updated, manifest.turn_id, tick.emit.at,
|
|
1918
|
-
updated = await settleBlockerFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at,
|
|
1919
|
-
updated = await settleKrFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at,
|
|
2003
|
+
updated = await settleManagerStatusGate(hqRoot, updated, manifest.turn_id, tick.emit.at, followupDeps, logDecision);
|
|
2004
|
+
updated = await settleBlockerFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, followupDeps, mesh, logDecision);
|
|
2005
|
+
updated = await settleKrFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, followupDeps, logDecision);
|
|
1920
2006
|
}
|
|
1921
2007
|
const managerStatusFollowupQueued = manifest.turn_id !== undefined &&
|
|
1922
2008
|
updated.status_gate_history?.some((entry) => "turn_id" in entry && entry.turn_id === manifest.turn_id &&
|
|
@@ -1924,11 +2010,14 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1924
2010
|
(entry.event === "blocker_stale" && entry.resume_queued === true) ||
|
|
1925
2011
|
(entry.event === "kr_stalled" && entry.resume_queued === true))) === true;
|
|
1926
2012
|
if (terminalWrite.changed && !managerStatusFollowupQueued) {
|
|
1927
|
-
updated = await maybeContinueAfterMissingEnvelopeRepair(hqRoot, updated, effectiveEnvelope ?? null, latest?.ref, tick.emit.at,
|
|
2013
|
+
updated = await maybeContinueAfterMissingEnvelopeRepair(hqRoot, updated, effectiveEnvelope ?? null, latest?.ref, tick.emit.at, followupDeps, logDecision);
|
|
1928
2014
|
}
|
|
1929
2015
|
if (terminalWrite.changed && deps.recoverQueuedMessages && !managerStatusFollowupQueued && updated.state !== "queued") {
|
|
1930
2016
|
try {
|
|
1931
|
-
if (
|
|
2017
|
+
if (followupRunMoved(hqRoot, laneId, followupDeps, readLane(hqRoot, laneId))) {
|
|
2018
|
+
logDecision({ event: "message_recovery_skipped", at: tick.emit.at, reason: "newer_run" });
|
|
2019
|
+
}
|
|
2020
|
+
else if (dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
1932
2021
|
await deps.recoverQueuedMessages(updated);
|
|
1933
2022
|
logDecision({
|
|
1934
2023
|
event: "message_recovery",
|
package/dist/lib/mesh/api.d.ts
CHANGED
|
@@ -176,6 +176,38 @@ export declare function putProjectView(token: string, companyUid: string, projec
|
|
|
176
176
|
}): Promise<MeshProjectView>;
|
|
177
177
|
/** Register the project genesis thread and deterministic project channel. */
|
|
178
178
|
export declare function registerMeshProject(token: string, companyUid: string, projectId: string, channelId?: string): Promise<MeshProjectRegistration>;
|
|
179
|
+
export declare const PROJECT_LIFECYCLE_STATUSES: readonly ["active", "completed", "archived"];
|
|
180
|
+
export type MeshProjectLifecycleStatus = (typeof PROJECT_LIFECYCLE_STATUSES)[number];
|
|
181
|
+
/** One row of the server's PROJECT_LIFECYCLE record for a project. */
|
|
182
|
+
export interface MeshProjectLifecycle {
|
|
183
|
+
projectId: string;
|
|
184
|
+
status: MeshProjectLifecycleStatus;
|
|
185
|
+
/** OCC version of the lifecycle row (not the Board version). */
|
|
186
|
+
version: number;
|
|
187
|
+
updatedAt: string;
|
|
188
|
+
updatedBy: string;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* List every lifecycle row for a company (follows the 25-row server pages).
|
|
192
|
+
* A project with no row has never left `active` and is not listed.
|
|
193
|
+
*/
|
|
194
|
+
export declare function listProjectLifecycles(token: string, companyUid: string): Promise<MeshProjectLifecycle[]>;
|
|
195
|
+
/** The lifecycle row for one project, or null when it has never been changed. */
|
|
196
|
+
export declare function getProjectLifecycle(token: string, companyUid: string, projectId: string): Promise<MeshProjectLifecycle | null>;
|
|
197
|
+
/**
|
|
198
|
+
* PATCH /v1/work-mesh/projects/{id}/lifecycle. `expectedVersion` is the
|
|
199
|
+
* current lifecycle row version (0 when the project has no row yet). The
|
|
200
|
+
* server answers 409 BOARD_NONEMPTY when archiving a Board that still has
|
|
201
|
+
* stories unless `confirmNonempty` is set, and a plain 409 on a version
|
|
202
|
+
* mismatch. Nothing is deleted: stories, folder, channel, members and
|
|
203
|
+
* history stay; the project only leaves the active list and the organize
|
|
204
|
+
* candidates.
|
|
205
|
+
*/
|
|
206
|
+
export declare function setProjectLifecycle(token: string, companyUid: string, projectId: string, change: {
|
|
207
|
+
status: MeshProjectLifecycleStatus;
|
|
208
|
+
expectedVersion: number;
|
|
209
|
+
confirmNonempty?: boolean;
|
|
210
|
+
}): Promise<MeshProjectLifecycle>;
|
|
179
211
|
export declare function pairDmUidsFromInbox(inbox: unknown): string[];
|
|
180
212
|
export interface WarmCacheResult {
|
|
181
213
|
directory: boolean;
|