squadrant 0.13.4 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -286
- package/dist/index.js +72 -7
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +91 -14
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
package/dist/squadrantd.js
CHANGED
|
@@ -33,7 +33,9 @@ function assembleDaemonSnapshot(input, now) {
|
|
|
33
33
|
ageMs: input.lastSweepAt == null ? null : now - input.lastSweepAt,
|
|
34
34
|
cadenceMs: input.sweepCadenceMs
|
|
35
35
|
},
|
|
36
|
-
log: input.log
|
|
36
|
+
log: input.log,
|
|
37
|
+
telegram: input.telegram,
|
|
38
|
+
lifecycleSources: input.lifecycleSources
|
|
37
39
|
},
|
|
38
40
|
tier1: input.health,
|
|
39
41
|
tier2: {
|
|
@@ -45,7 +47,8 @@ function assembleDaemonSnapshot(input, now) {
|
|
|
45
47
|
lastAckedSeq: p.lastAckedSeq,
|
|
46
48
|
behind: Math.max(0, p.mailbox.maxSeq - p.lastAckedSeq)
|
|
47
49
|
},
|
|
48
|
-
store: { byState: p.storeByState, corruptCount: p.corruptCount }
|
|
50
|
+
store: { byState: p.storeByState, corruptCount: p.corruptCount },
|
|
51
|
+
deferral: p.deferral ?? { maxDeferCount: 0, stuck: false }
|
|
49
52
|
})),
|
|
50
53
|
results: input.results
|
|
51
54
|
}
|
|
@@ -1128,18 +1131,21 @@ async function* readFromCursor(opts) {
|
|
|
1128
1131
|
}
|
|
1129
1132
|
async function mailboxStats(stateRoot, project) {
|
|
1130
1133
|
const file = logPath(stateRoot, project);
|
|
1134
|
+
const rotated = await listRotatedOldestFirst(stateRoot, project);
|
|
1131
1135
|
let sizeBytes = 0;
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1136
|
+
for (const f of [file, ...rotated]) {
|
|
1137
|
+
try {
|
|
1138
|
+
sizeBytes += (await fs7.stat(f)).size;
|
|
1139
|
+
} catch (e) {
|
|
1140
|
+
if (e.code !== "ENOENT")
|
|
1141
|
+
throw e;
|
|
1142
|
+
}
|
|
1137
1143
|
}
|
|
1138
|
-
const
|
|
1144
|
+
const oldestFile = rotated[0] ?? file;
|
|
1139
1145
|
return {
|
|
1140
1146
|
maxSeq: await readMaxSeq(stateRoot, project),
|
|
1141
1147
|
sizeBytes,
|
|
1142
|
-
oldestEntryAgeMs: await oldestEntryAgeMs(
|
|
1148
|
+
oldestEntryAgeMs: await oldestEntryAgeMs(oldestFile),
|
|
1143
1149
|
rotationCount: rotated.length
|
|
1144
1150
|
};
|
|
1145
1151
|
}
|
|
@@ -1412,13 +1418,14 @@ function projectHealth(input) {
|
|
|
1412
1418
|
for (const c of crews) {
|
|
1413
1419
|
if (TERMINAL.has(c.state))
|
|
1414
1420
|
continue;
|
|
1421
|
+
const undelivered = c.mode === "interactive" && !c.firstTurnConfirmedAt && c.heartbeatBudgetMs != null && now - c.lastHeartbeat > c.heartbeatBudgetMs;
|
|
1415
1422
|
out.push({
|
|
1416
1423
|
kind: "crew",
|
|
1417
1424
|
project,
|
|
1418
1425
|
ref: c.name ?? c.id.slice(0, 8),
|
|
1419
1426
|
state: classifyHealth(c.lastHeartbeat, now, CREW_STALE_MS, CREW_GONE_MS),
|
|
1420
1427
|
lastSeenMs: c.lastHeartbeat,
|
|
1421
|
-
detail: c.state
|
|
1428
|
+
detail: undelivered ? `undelivered (${c.state})` : c.state
|
|
1422
1429
|
});
|
|
1423
1430
|
}
|
|
1424
1431
|
return out;
|
|
@@ -1638,6 +1645,7 @@ function buildContext(opts) {
|
|
|
1638
1645
|
opencodeBridge: null,
|
|
1639
1646
|
cmuxEventsBridge: null,
|
|
1640
1647
|
telegramBridge: void 0,
|
|
1648
|
+
lifecycleSources: opts.lifecycleSources ?? [],
|
|
1641
1649
|
broadcast: () => {
|
|
1642
1650
|
},
|
|
1643
1651
|
schedulePromotion: () => {
|
|
@@ -1969,6 +1977,14 @@ var CaptainDelivery = class {
|
|
|
1969
1977
|
return { deferred: true };
|
|
1970
1978
|
}
|
|
1971
1979
|
}
|
|
1980
|
+
/** Read-only. Never mutates — safe to poll from the snapshot assembler every tick. */
|
|
1981
|
+
stats() {
|
|
1982
|
+
let maxDeferCount = 0;
|
|
1983
|
+
for (const c of this.deferCounts.values())
|
|
1984
|
+
if (c > maxDeferCount)
|
|
1985
|
+
maxDeferCount = c;
|
|
1986
|
+
return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers };
|
|
1987
|
+
}
|
|
1972
1988
|
};
|
|
1973
1989
|
|
|
1974
1990
|
// packages/core/dist/daemon/delivery-loop.js
|
|
@@ -2008,11 +2024,12 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
2008
2024
|
}
|
|
2009
2025
|
};
|
|
2010
2026
|
if (!daemonCmux) {
|
|
2011
|
-
return { defaultNotify, deliveryTick: void 0 };
|
|
2027
|
+
return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0 };
|
|
2012
2028
|
}
|
|
2013
2029
|
const cmux2 = daemonCmux;
|
|
2014
2030
|
const cfg = loadConfig();
|
|
2015
2031
|
const deliveries = /* @__PURE__ */ new Map();
|
|
2032
|
+
const deliveryStats = (project) => deliveries.get(project)?.stats();
|
|
2016
2033
|
const sessionStartMs = Date.now();
|
|
2017
2034
|
let delivering = false;
|
|
2018
2035
|
const deliveryCore = async () => {
|
|
@@ -2095,7 +2112,7 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
2095
2112
|
delivering = false;
|
|
2096
2113
|
}
|
|
2097
2114
|
};
|
|
2098
|
-
return { defaultNotify, deliveryTick };
|
|
2115
|
+
return { defaultNotify, deliveryTick, deliveryStats };
|
|
2099
2116
|
}
|
|
2100
2117
|
|
|
2101
2118
|
// packages/core/dist/daemon/gates.js
|
|
@@ -2275,7 +2292,7 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
2275
2292
|
const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
|
|
2276
2293
|
const { daemonCmux } = ctx;
|
|
2277
2294
|
const probes = createProbes(ctx);
|
|
2278
|
-
const { defaultNotify, deliveryTick: initialDeliveryTick } = createDelivery(ctx, daemonCmux);
|
|
2295
|
+
const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats } = createDelivery(ctx, daemonCmux);
|
|
2279
2296
|
const baseNotify = opts.notify ?? defaultNotify;
|
|
2280
2297
|
const notify = ctx.telegramBridge ? async (args) => {
|
|
2281
2298
|
await baseNotify(args);
|
|
@@ -2349,7 +2366,8 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
2349
2366
|
mailbox: await mailboxStats(stateRoot, project),
|
|
2350
2367
|
lastAckedSeq: cursor?.lastAckedSeq ?? 0,
|
|
2351
2368
|
storeByState: storeStats.byState,
|
|
2352
|
-
corruptCount: storeStats.corruptCount
|
|
2369
|
+
corruptCount: storeStats.corruptCount,
|
|
2370
|
+
deferral: deliveryStats(project)
|
|
2353
2371
|
};
|
|
2354
2372
|
}));
|
|
2355
2373
|
return {
|
|
@@ -2360,6 +2378,8 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
2360
2378
|
lastSweepAt: ctx.lastSweepAt.value,
|
|
2361
2379
|
sweepCadenceMs: opts.sweepMs ?? 3e4,
|
|
2362
2380
|
log: gatherLogStats(logPath2, now, SNAPSHOT_LOG_WINDOW_MS),
|
|
2381
|
+
telegram: ctx.telegramBridge ? { configured: true, ...ctx.telegramBridge.health() } : { configured: false, polling: false, lastSuccessfulPollAt: null, lastError: null, lastErrorAt: null },
|
|
2382
|
+
lifecycleSources: ctx.lifecycleSources.map((s) => ({ name: s.name, ...s.health?.() ?? { active: true, error: null } })),
|
|
2363
2383
|
health: buildHealth(),
|
|
2364
2384
|
projects,
|
|
2365
2385
|
results: gatherResults(resultsDir)
|
|
@@ -2978,6 +2998,9 @@ function createTelegramBridge(opts) {
|
|
|
2978
2998
|
const configRoot = opts.configRoot ?? path9.join(os3.homedir(), ".config", "squadrant");
|
|
2979
2999
|
const pollMs = cfg.pollMs ?? 1e3;
|
|
2980
3000
|
let running = false;
|
|
3001
|
+
let lastSuccessfulPollAt = null;
|
|
3002
|
+
let lastError = null;
|
|
3003
|
+
let lastErrorAt = null;
|
|
2981
3004
|
function persistOffset(next) {
|
|
2982
3005
|
const s = loadState(stateRoot);
|
|
2983
3006
|
s.offset = next;
|
|
@@ -3276,7 +3299,10 @@ function createTelegramBridge(opts) {
|
|
|
3276
3299
|
await handleUpdate(u);
|
|
3277
3300
|
persistOffset(u.update_id + 1);
|
|
3278
3301
|
}
|
|
3302
|
+
lastSuccessfulPollAt = Date.now();
|
|
3279
3303
|
} catch (e) {
|
|
3304
|
+
lastError = e.message;
|
|
3305
|
+
lastErrorAt = Date.now();
|
|
3280
3306
|
log(`telegram inbound poll failed: ${e.message}`);
|
|
3281
3307
|
}
|
|
3282
3308
|
if (running)
|
|
@@ -3297,6 +3323,9 @@ function createTelegramBridge(opts) {
|
|
|
3297
3323
|
void deliverOutbound(project, ev).catch((e) => {
|
|
3298
3324
|
log(`telegram outbound failed project=${project}: ${e.message}`);
|
|
3299
3325
|
});
|
|
3326
|
+
},
|
|
3327
|
+
health() {
|
|
3328
|
+
return { polling: running, lastSuccessfulPollAt, lastError, lastErrorAt };
|
|
3300
3329
|
}
|
|
3301
3330
|
};
|
|
3302
3331
|
}
|
|
@@ -3568,17 +3597,24 @@ var CodexAppServerSource = class {
|
|
|
3568
3597
|
deps;
|
|
3569
3598
|
/** taskId → last reported snapshot (for snapshot() liveness floor). */
|
|
3570
3599
|
cache = /* @__PURE__ */ new Map();
|
|
3600
|
+
active = false;
|
|
3571
3601
|
start(deps) {
|
|
3572
3602
|
this.deps = deps;
|
|
3603
|
+
this.active = true;
|
|
3573
3604
|
}
|
|
3574
3605
|
stop() {
|
|
3575
3606
|
this.deps = void 0;
|
|
3576
3607
|
this.cache.clear();
|
|
3608
|
+
this.active = false;
|
|
3577
3609
|
}
|
|
3578
3610
|
/** Returns the last-reported snapshot for a known crew (liveness floor). */
|
|
3579
3611
|
snapshot(taskId) {
|
|
3580
3612
|
return this.cache.get(taskId);
|
|
3581
3613
|
}
|
|
3614
|
+
/** Read-only source health (B4). Purely push-driven — never errors on its own. */
|
|
3615
|
+
health() {
|
|
3616
|
+
return { active: this.active, error: null };
|
|
3617
|
+
}
|
|
3582
3618
|
/**
|
|
3583
3619
|
* Feed a ControlEvent from CodexInteractiveDriver into this source.
|
|
3584
3620
|
* The daemon wires: emit = (ev) => { source.observe(ev); handle(ev); }
|
|
@@ -4395,6 +4431,27 @@ function parseDraftFromScreen(screen) {
|
|
|
4395
4431
|
}
|
|
4396
4432
|
return "";
|
|
4397
4433
|
}
|
|
4434
|
+
function hasModalOptionList(screen) {
|
|
4435
|
+
if (!screen)
|
|
4436
|
+
return false;
|
|
4437
|
+
const lines = screen.split(/\r?\n/);
|
|
4438
|
+
const HR_RE = /^\s*─{10,}\s*$/;
|
|
4439
|
+
let bottomHR = -1;
|
|
4440
|
+
let topHR = -1;
|
|
4441
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
4442
|
+
if (HR_RE.test(lines[i])) {
|
|
4443
|
+
if (bottomHR === -1)
|
|
4444
|
+
bottomHR = i;
|
|
4445
|
+
else {
|
|
4446
|
+
topHR = i;
|
|
4447
|
+
break;
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
if (topHR === -1)
|
|
4452
|
+
return false;
|
|
4453
|
+
return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
|
|
4454
|
+
}
|
|
4398
4455
|
function readInputBoxRaw(screen, opts) {
|
|
4399
4456
|
if (!screen)
|
|
4400
4457
|
return null;
|
|
@@ -4639,6 +4696,8 @@ function createCmuxDriver() {
|
|
|
4639
4696
|
const draft = parseDraftFromScreen(screen);
|
|
4640
4697
|
if (draft === null)
|
|
4641
4698
|
throw new DeferDelivery(null);
|
|
4699
|
+
if (hasModalOptionList(screen))
|
|
4700
|
+
throw new DeferDelivery(null);
|
|
4642
4701
|
if (draft === "") {
|
|
4643
4702
|
await deliver();
|
|
4644
4703
|
return;
|
|
@@ -4927,6 +4986,8 @@ var CmuxStoreSource = class {
|
|
|
4927
4986
|
debounceTimer;
|
|
4928
4987
|
/** taskId → last reported snapshot (for snapshot() liveness floor). */
|
|
4929
4988
|
cache = /* @__PURE__ */ new Map();
|
|
4989
|
+
active = false;
|
|
4990
|
+
lastError = null;
|
|
4930
4991
|
constructor(opts = {}) {
|
|
4931
4992
|
this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
|
|
4932
4993
|
this.debounceMs = opts.debounceMs ?? 50;
|
|
@@ -4942,10 +5003,13 @@ var CmuxStoreSource = class {
|
|
|
4942
5003
|
}
|
|
4943
5004
|
start(deps) {
|
|
4944
5005
|
this.deps = deps;
|
|
5006
|
+
this.active = true;
|
|
5007
|
+
this.lastError = null;
|
|
4945
5008
|
this.scan();
|
|
4946
5009
|
try {
|
|
4947
5010
|
this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced());
|
|
4948
5011
|
} catch (e) {
|
|
5012
|
+
this.lastError = e.message;
|
|
4949
5013
|
this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
|
|
4950
5014
|
}
|
|
4951
5015
|
}
|
|
@@ -4958,11 +5022,16 @@ var CmuxStoreSource = class {
|
|
|
4958
5022
|
this.stopWatcher = void 0;
|
|
4959
5023
|
this.deps = void 0;
|
|
4960
5024
|
this.cache.clear();
|
|
5025
|
+
this.active = false;
|
|
4961
5026
|
}
|
|
4962
5027
|
/** Returns the last-reported snapshot for a known crew (liveness floor). */
|
|
4963
5028
|
snapshot(taskId) {
|
|
4964
5029
|
return this.cache.get(taskId);
|
|
4965
5030
|
}
|
|
5031
|
+
/** Read-only source health (B4 — dashboard visibility into which sources are up). */
|
|
5032
|
+
health() {
|
|
5033
|
+
return { active: this.active, error: this.lastError };
|
|
5034
|
+
}
|
|
4966
5035
|
// ── private ─────────────────────────────────────────────────────────────────
|
|
4967
5036
|
scheduleDebounced() {
|
|
4968
5037
|
if (this.debounceTimer !== void 0) {
|
|
@@ -5149,6 +5218,7 @@ var NativeHookSource = class {
|
|
|
5149
5218
|
deps;
|
|
5150
5219
|
/** taskId → last-reported snapshot, for snapshot() liveness floor. */
|
|
5151
5220
|
cache = /* @__PURE__ */ new Map();
|
|
5221
|
+
active = false;
|
|
5152
5222
|
constructor(opts = {}) {
|
|
5153
5223
|
this.hookInstall = opts.hookInstall ?? {};
|
|
5154
5224
|
this.log = opts.log ?? (() => {
|
|
@@ -5156,15 +5226,21 @@ var NativeHookSource = class {
|
|
|
5156
5226
|
}
|
|
5157
5227
|
start(deps) {
|
|
5158
5228
|
this.deps = deps;
|
|
5229
|
+
this.active = true;
|
|
5159
5230
|
}
|
|
5160
5231
|
stop() {
|
|
5161
5232
|
this.deps = void 0;
|
|
5162
5233
|
this.cache.clear();
|
|
5234
|
+
this.active = false;
|
|
5163
5235
|
}
|
|
5164
5236
|
/** Returns the last-reported snapshot for a known crew (liveness floor poll). */
|
|
5165
5237
|
snapshot(taskId) {
|
|
5166
5238
|
return this.cache.get(taskId);
|
|
5167
5239
|
}
|
|
5240
|
+
/** Read-only source health (B4). Purely push-driven — never errors on its own. */
|
|
5241
|
+
health() {
|
|
5242
|
+
return { active: this.active, error: null };
|
|
5243
|
+
}
|
|
5168
5244
|
/**
|
|
5169
5245
|
* Install squadrant-owned hooks into ~/.claude/settings.json.
|
|
5170
5246
|
* Idempotent — safe to call on every project init or crew spawn.
|
|
@@ -5336,6 +5412,7 @@ function startSquadrantd(opts = {}) {
|
|
|
5336
5412
|
ctx.codexDriver = codexDriver;
|
|
5337
5413
|
ctx.opencodeBridge = opencodeBridge;
|
|
5338
5414
|
ctx.cmuxEventsBridge = cmuxEventsBridge;
|
|
5415
|
+
ctx.lifecycleSources = [cmuxStoreSource, nativeHookSource, codexAppServerSource];
|
|
5339
5416
|
const tgCfg = loadConfig().telegram;
|
|
5340
5417
|
ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(tgCfg, stateRoot, log) : void 0);
|
|
5341
5418
|
ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
|