squadrant 0.13.5 → 0.14.1
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 +49 -7
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +70 -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
|
}
|
|
@@ -534,6 +537,8 @@ function reduce(rec, ev, now) {
|
|
|
534
537
|
case "task.turn.completed":
|
|
535
538
|
if (rec.state === "blocked")
|
|
536
539
|
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
540
|
+
if (rec.pendingTool)
|
|
541
|
+
return stampAttempt(base, {}, now);
|
|
537
542
|
return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0 };
|
|
538
543
|
case "task.delta":
|
|
539
544
|
return stampAttempt(base, {}, now);
|
|
@@ -1128,18 +1133,21 @@ async function* readFromCursor(opts) {
|
|
|
1128
1133
|
}
|
|
1129
1134
|
async function mailboxStats(stateRoot, project) {
|
|
1130
1135
|
const file = logPath(stateRoot, project);
|
|
1136
|
+
const rotated = await listRotatedOldestFirst(stateRoot, project);
|
|
1131
1137
|
let sizeBytes = 0;
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1138
|
+
for (const f of [file, ...rotated]) {
|
|
1139
|
+
try {
|
|
1140
|
+
sizeBytes += (await fs7.stat(f)).size;
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
if (e.code !== "ENOENT")
|
|
1143
|
+
throw e;
|
|
1144
|
+
}
|
|
1137
1145
|
}
|
|
1138
|
-
const
|
|
1146
|
+
const oldestFile = rotated[0] ?? file;
|
|
1139
1147
|
return {
|
|
1140
1148
|
maxSeq: await readMaxSeq(stateRoot, project),
|
|
1141
1149
|
sizeBytes,
|
|
1142
|
-
oldestEntryAgeMs: await oldestEntryAgeMs(
|
|
1150
|
+
oldestEntryAgeMs: await oldestEntryAgeMs(oldestFile),
|
|
1143
1151
|
rotationCount: rotated.length
|
|
1144
1152
|
};
|
|
1145
1153
|
}
|
|
@@ -1412,13 +1420,14 @@ function projectHealth(input) {
|
|
|
1412
1420
|
for (const c of crews) {
|
|
1413
1421
|
if (TERMINAL.has(c.state))
|
|
1414
1422
|
continue;
|
|
1423
|
+
const undelivered = c.mode === "interactive" && !c.firstTurnConfirmedAt && c.heartbeatBudgetMs != null && now - c.lastHeartbeat > c.heartbeatBudgetMs;
|
|
1415
1424
|
out.push({
|
|
1416
1425
|
kind: "crew",
|
|
1417
1426
|
project,
|
|
1418
1427
|
ref: c.name ?? c.id.slice(0, 8),
|
|
1419
1428
|
state: classifyHealth(c.lastHeartbeat, now, CREW_STALE_MS, CREW_GONE_MS),
|
|
1420
1429
|
lastSeenMs: c.lastHeartbeat,
|
|
1421
|
-
detail: c.state
|
|
1430
|
+
detail: undelivered ? `undelivered (${c.state})` : c.state
|
|
1422
1431
|
});
|
|
1423
1432
|
}
|
|
1424
1433
|
return out;
|
|
@@ -1638,6 +1647,7 @@ function buildContext(opts) {
|
|
|
1638
1647
|
opencodeBridge: null,
|
|
1639
1648
|
cmuxEventsBridge: null,
|
|
1640
1649
|
telegramBridge: void 0,
|
|
1650
|
+
lifecycleSources: opts.lifecycleSources ?? [],
|
|
1641
1651
|
broadcast: () => {
|
|
1642
1652
|
},
|
|
1643
1653
|
schedulePromotion: () => {
|
|
@@ -1969,6 +1979,14 @@ var CaptainDelivery = class {
|
|
|
1969
1979
|
return { deferred: true };
|
|
1970
1980
|
}
|
|
1971
1981
|
}
|
|
1982
|
+
/** Read-only. Never mutates — safe to poll from the snapshot assembler every tick. */
|
|
1983
|
+
stats() {
|
|
1984
|
+
let maxDeferCount = 0;
|
|
1985
|
+
for (const c of this.deferCounts.values())
|
|
1986
|
+
if (c > maxDeferCount)
|
|
1987
|
+
maxDeferCount = c;
|
|
1988
|
+
return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers };
|
|
1989
|
+
}
|
|
1972
1990
|
};
|
|
1973
1991
|
|
|
1974
1992
|
// packages/core/dist/daemon/delivery-loop.js
|
|
@@ -2008,11 +2026,12 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
2008
2026
|
}
|
|
2009
2027
|
};
|
|
2010
2028
|
if (!daemonCmux) {
|
|
2011
|
-
return { defaultNotify, deliveryTick: void 0 };
|
|
2029
|
+
return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0 };
|
|
2012
2030
|
}
|
|
2013
2031
|
const cmux2 = daemonCmux;
|
|
2014
2032
|
const cfg = loadConfig();
|
|
2015
2033
|
const deliveries = /* @__PURE__ */ new Map();
|
|
2034
|
+
const deliveryStats = (project) => deliveries.get(project)?.stats();
|
|
2016
2035
|
const sessionStartMs = Date.now();
|
|
2017
2036
|
let delivering = false;
|
|
2018
2037
|
const deliveryCore = async () => {
|
|
@@ -2095,7 +2114,7 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
2095
2114
|
delivering = false;
|
|
2096
2115
|
}
|
|
2097
2116
|
};
|
|
2098
|
-
return { defaultNotify, deliveryTick };
|
|
2117
|
+
return { defaultNotify, deliveryTick, deliveryStats };
|
|
2099
2118
|
}
|
|
2100
2119
|
|
|
2101
2120
|
// packages/core/dist/daemon/gates.js
|
|
@@ -2275,7 +2294,7 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
2275
2294
|
const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
|
|
2276
2295
|
const { daemonCmux } = ctx;
|
|
2277
2296
|
const probes = createProbes(ctx);
|
|
2278
|
-
const { defaultNotify, deliveryTick: initialDeliveryTick } = createDelivery(ctx, daemonCmux);
|
|
2297
|
+
const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats } = createDelivery(ctx, daemonCmux);
|
|
2279
2298
|
const baseNotify = opts.notify ?? defaultNotify;
|
|
2280
2299
|
const notify = ctx.telegramBridge ? async (args) => {
|
|
2281
2300
|
await baseNotify(args);
|
|
@@ -2349,7 +2368,8 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
2349
2368
|
mailbox: await mailboxStats(stateRoot, project),
|
|
2350
2369
|
lastAckedSeq: cursor?.lastAckedSeq ?? 0,
|
|
2351
2370
|
storeByState: storeStats.byState,
|
|
2352
|
-
corruptCount: storeStats.corruptCount
|
|
2371
|
+
corruptCount: storeStats.corruptCount,
|
|
2372
|
+
deferral: deliveryStats(project)
|
|
2353
2373
|
};
|
|
2354
2374
|
}));
|
|
2355
2375
|
return {
|
|
@@ -2360,6 +2380,8 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
2360
2380
|
lastSweepAt: ctx.lastSweepAt.value,
|
|
2361
2381
|
sweepCadenceMs: opts.sweepMs ?? 3e4,
|
|
2362
2382
|
log: gatherLogStats(logPath2, now, SNAPSHOT_LOG_WINDOW_MS),
|
|
2383
|
+
telegram: ctx.telegramBridge ? { configured: true, ...ctx.telegramBridge.health() } : { configured: false, polling: false, lastSuccessfulPollAt: null, lastError: null, lastErrorAt: null },
|
|
2384
|
+
lifecycleSources: ctx.lifecycleSources.map((s) => ({ name: s.name, ...s.health?.() ?? { active: true, error: null } })),
|
|
2363
2385
|
health: buildHealth(),
|
|
2364
2386
|
projects,
|
|
2365
2387
|
results: gatherResults(resultsDir)
|
|
@@ -2978,6 +3000,9 @@ function createTelegramBridge(opts) {
|
|
|
2978
3000
|
const configRoot = opts.configRoot ?? path9.join(os3.homedir(), ".config", "squadrant");
|
|
2979
3001
|
const pollMs = cfg.pollMs ?? 1e3;
|
|
2980
3002
|
let running = false;
|
|
3003
|
+
let lastSuccessfulPollAt = null;
|
|
3004
|
+
let lastError = null;
|
|
3005
|
+
let lastErrorAt = null;
|
|
2981
3006
|
function persistOffset(next) {
|
|
2982
3007
|
const s = loadState(stateRoot);
|
|
2983
3008
|
s.offset = next;
|
|
@@ -3276,7 +3301,10 @@ function createTelegramBridge(opts) {
|
|
|
3276
3301
|
await handleUpdate(u);
|
|
3277
3302
|
persistOffset(u.update_id + 1);
|
|
3278
3303
|
}
|
|
3304
|
+
lastSuccessfulPollAt = Date.now();
|
|
3279
3305
|
} catch (e) {
|
|
3306
|
+
lastError = e.message;
|
|
3307
|
+
lastErrorAt = Date.now();
|
|
3280
3308
|
log(`telegram inbound poll failed: ${e.message}`);
|
|
3281
3309
|
}
|
|
3282
3310
|
if (running)
|
|
@@ -3297,6 +3325,9 @@ function createTelegramBridge(opts) {
|
|
|
3297
3325
|
void deliverOutbound(project, ev).catch((e) => {
|
|
3298
3326
|
log(`telegram outbound failed project=${project}: ${e.message}`);
|
|
3299
3327
|
});
|
|
3328
|
+
},
|
|
3329
|
+
health() {
|
|
3330
|
+
return { polling: running, lastSuccessfulPollAt, lastError, lastErrorAt };
|
|
3300
3331
|
}
|
|
3301
3332
|
};
|
|
3302
3333
|
}
|
|
@@ -3568,17 +3599,24 @@ var CodexAppServerSource = class {
|
|
|
3568
3599
|
deps;
|
|
3569
3600
|
/** taskId → last reported snapshot (for snapshot() liveness floor). */
|
|
3570
3601
|
cache = /* @__PURE__ */ new Map();
|
|
3602
|
+
active = false;
|
|
3571
3603
|
start(deps) {
|
|
3572
3604
|
this.deps = deps;
|
|
3605
|
+
this.active = true;
|
|
3573
3606
|
}
|
|
3574
3607
|
stop() {
|
|
3575
3608
|
this.deps = void 0;
|
|
3576
3609
|
this.cache.clear();
|
|
3610
|
+
this.active = false;
|
|
3577
3611
|
}
|
|
3578
3612
|
/** Returns the last-reported snapshot for a known crew (liveness floor). */
|
|
3579
3613
|
snapshot(taskId) {
|
|
3580
3614
|
return this.cache.get(taskId);
|
|
3581
3615
|
}
|
|
3616
|
+
/** Read-only source health (B4). Purely push-driven — never errors on its own. */
|
|
3617
|
+
health() {
|
|
3618
|
+
return { active: this.active, error: null };
|
|
3619
|
+
}
|
|
3582
3620
|
/**
|
|
3583
3621
|
* Feed a ControlEvent from CodexInteractiveDriver into this source.
|
|
3584
3622
|
* The daemon wires: emit = (ev) => { source.observe(ev); handle(ev); }
|
|
@@ -4950,6 +4988,8 @@ var CmuxStoreSource = class {
|
|
|
4950
4988
|
debounceTimer;
|
|
4951
4989
|
/** taskId → last reported snapshot (for snapshot() liveness floor). */
|
|
4952
4990
|
cache = /* @__PURE__ */ new Map();
|
|
4991
|
+
active = false;
|
|
4992
|
+
lastError = null;
|
|
4953
4993
|
constructor(opts = {}) {
|
|
4954
4994
|
this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
|
|
4955
4995
|
this.debounceMs = opts.debounceMs ?? 50;
|
|
@@ -4965,10 +5005,13 @@ var CmuxStoreSource = class {
|
|
|
4965
5005
|
}
|
|
4966
5006
|
start(deps) {
|
|
4967
5007
|
this.deps = deps;
|
|
5008
|
+
this.active = true;
|
|
5009
|
+
this.lastError = null;
|
|
4968
5010
|
this.scan();
|
|
4969
5011
|
try {
|
|
4970
5012
|
this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced());
|
|
4971
5013
|
} catch (e) {
|
|
5014
|
+
this.lastError = e.message;
|
|
4972
5015
|
this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
|
|
4973
5016
|
}
|
|
4974
5017
|
}
|
|
@@ -4981,11 +5024,16 @@ var CmuxStoreSource = class {
|
|
|
4981
5024
|
this.stopWatcher = void 0;
|
|
4982
5025
|
this.deps = void 0;
|
|
4983
5026
|
this.cache.clear();
|
|
5027
|
+
this.active = false;
|
|
4984
5028
|
}
|
|
4985
5029
|
/** Returns the last-reported snapshot for a known crew (liveness floor). */
|
|
4986
5030
|
snapshot(taskId) {
|
|
4987
5031
|
return this.cache.get(taskId);
|
|
4988
5032
|
}
|
|
5033
|
+
/** Read-only source health (B4 — dashboard visibility into which sources are up). */
|
|
5034
|
+
health() {
|
|
5035
|
+
return { active: this.active, error: this.lastError };
|
|
5036
|
+
}
|
|
4989
5037
|
// ── private ─────────────────────────────────────────────────────────────────
|
|
4990
5038
|
scheduleDebounced() {
|
|
4991
5039
|
if (this.debounceTimer !== void 0) {
|
|
@@ -5172,6 +5220,7 @@ var NativeHookSource = class {
|
|
|
5172
5220
|
deps;
|
|
5173
5221
|
/** taskId → last-reported snapshot, for snapshot() liveness floor. */
|
|
5174
5222
|
cache = /* @__PURE__ */ new Map();
|
|
5223
|
+
active = false;
|
|
5175
5224
|
constructor(opts = {}) {
|
|
5176
5225
|
this.hookInstall = opts.hookInstall ?? {};
|
|
5177
5226
|
this.log = opts.log ?? (() => {
|
|
@@ -5179,15 +5228,21 @@ var NativeHookSource = class {
|
|
|
5179
5228
|
}
|
|
5180
5229
|
start(deps) {
|
|
5181
5230
|
this.deps = deps;
|
|
5231
|
+
this.active = true;
|
|
5182
5232
|
}
|
|
5183
5233
|
stop() {
|
|
5184
5234
|
this.deps = void 0;
|
|
5185
5235
|
this.cache.clear();
|
|
5236
|
+
this.active = false;
|
|
5186
5237
|
}
|
|
5187
5238
|
/** Returns the last-reported snapshot for a known crew (liveness floor poll). */
|
|
5188
5239
|
snapshot(taskId) {
|
|
5189
5240
|
return this.cache.get(taskId);
|
|
5190
5241
|
}
|
|
5242
|
+
/** Read-only source health (B4). Purely push-driven — never errors on its own. */
|
|
5243
|
+
health() {
|
|
5244
|
+
return { active: this.active, error: null };
|
|
5245
|
+
}
|
|
5191
5246
|
/**
|
|
5192
5247
|
* Install squadrant-owned hooks into ~/.claude/settings.json.
|
|
5193
5248
|
* Idempotent — safe to call on every project init or crew spawn.
|
|
@@ -5359,6 +5414,7 @@ function startSquadrantd(opts = {}) {
|
|
|
5359
5414
|
ctx.codexDriver = codexDriver;
|
|
5360
5415
|
ctx.opencodeBridge = opencodeBridge;
|
|
5361
5416
|
ctx.cmuxEventsBridge = cmuxEventsBridge;
|
|
5417
|
+
ctx.lifecycleSources = [cmuxStoreSource, nativeHookSource, codexAppServerSource];
|
|
5362
5418
|
const tgCfg = loadConfig().telegram;
|
|
5363
5419
|
ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(tgCfg, stateRoot, log) : void 0);
|
|
5364
5420
|
ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
|