Skip to content

Commit be3abea

Browse files
committed
Hold fleet updates to one row each and let bursts settle
Running a real fleet showed both halves of the noise boundary leaking. A parallel dispatch arrives as one store change per lane, so six lanes printed six dispatch lines instead of the one decision they were; the report now settles briefly before observing, and six lanes read as "6 dispatched". Long worker summaries also wrapped, costing two rows an update, so a line is clipped to the width a transcript row has.
1 parent 7e2fd2b commit be3abea

4 files changed

Lines changed: 47 additions & 3 deletions

File tree

src/subagent/fleet-report.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,23 @@ describe("observeFleet", () => {
104104
expect(busy.updates).toEqual([]);
105105
});
106106

107+
test("an update is one row — a long outcome is clipped, never wrapped", () => {
108+
const seeded = observeFleet(createFleetWatch(), [lane({ id: "api" })], T0).watch;
109+
const { updates } = observeFleet(
110+
seeded,
111+
[
112+
lane({
113+
id: "api",
114+
status: "done",
115+
report: "Rewired the reporter, added the digest, wired the poll, and updated every affected test in the suite.",
116+
}),
117+
],
118+
T0 + 1000,
119+
);
120+
expect(updates[0]!.length).toBeLessThanOrEqual(76);
121+
expect(updates[0]).toContain("…");
122+
});
123+
107124
test("a dozen lanes landing at once collapse into one tally", () => {
108125
const before = Array.from({ length: 12 }, (_, i) => lane({ id: `l${i}` }));
109126
const seeded = observeFleet(createFleetWatch(), before, T0).watch;

src/subagent/fleet-report.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ export function createFleetWatch(): FleetWatch {
5656
const COALESCE_ABOVE = 3;
5757

5858
/** Enough of an outcome to judge it; past this the operator opens the lane. */
59-
const OUTCOME_CHARS = 72;
59+
const OUTCOME_CHARS = 56;
60+
61+
/**
62+
* One update is one row. A line that wraps doubles the cost of every update on
63+
* screen, which is how a report meant to be glanced at turns into a scroll.
64+
*/
65+
const MAX_UPDATE_CHARS = 76;
6066

6167
const PREFIX = "fleet";
6268

@@ -67,6 +73,13 @@ const PREFIX = "fleet";
6773
*/
6874
export const FLEET_STALL_POLL_MS = 5_000;
6975

76+
/**
77+
* A parallel dispatch lands as one store change per lane, so observing each
78+
* one on its own turns a single decision into a line per lane. Settling first
79+
* is what lets the tally do its job.
80+
*/
81+
export const FLEET_REPORT_SETTLE_MS = 400;
82+
7083
/** Lanes named in a digest before it starts counting instead of listing. */
7184
const DIGEST_NAMED_LANES = 4;
7285

@@ -177,7 +190,10 @@ export function observeFleet(
177190
lines.push(`${idleSummary(lanes)} — nothing running`);
178191
}
179192

180-
return { watch, updates: lines.map((line) => `${PREFIX} · ${line}`) };
193+
return {
194+
watch,
195+
updates: lines.map((line) => clip(`${PREFIX} · ${line}`, MAX_UPDATE_CHARS)),
196+
};
181197
}
182198

183199
function tally(changes: readonly Change[]): string {

src/subagent/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { createSubAgentSessionStore } from "./session-store.js";
1010
export {
1111
createFleetWatch,
1212
fleetDigest,
13+
FLEET_REPORT_SETTLE_MS,
1314
FLEET_STALL_POLL_MS,
1415
observeFleet,
1516
type FleetLane,

src/tui/runner.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ import {
121121
createFleetWatch,
122122
createSubAgentSessionStore,
123123
fleetDigest,
124+
FLEET_REPORT_SETTLE_MS,
124125
FLEET_STALL_POLL_MS,
125126
observeFleet,
126127
taskToolDefinition,
@@ -2212,7 +2213,15 @@ export async function runTUI(initialConfig: Config): Promise<number> {
22122213
fleetWatch = observation.watch;
22132214
for (const update of observation.updates) surfaceSystemNotice(host.shell, update);
22142215
};
2215-
const unsubscribeFleetReport = subAgentSessions.subscribe(reportFleet);
2216+
let fleetSettle: ReturnType<typeof setTimeout> | null = null;
2217+
const unsubscribeFleetReport = subAgentSessions.subscribe(() => {
2218+
if (fleetSettle !== null) return;
2219+
fleetSettle = setTimeout(() => {
2220+
fleetSettle = null;
2221+
reportFleet();
2222+
}, FLEET_REPORT_SETTLE_MS);
2223+
if (typeof fleetSettle.unref === "function") fleetSettle.unref();
2224+
});
22162225
const fleetStallPoll = setInterval(reportFleet, FLEET_STALL_POLL_MS);
22172226
if (typeof fleetStallPoll.unref === "function") fleetStallPoll.unref();
22182227

@@ -2317,6 +2326,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
23172326

23182327
await host.waitUntilExit();
23192328
clearInterval(fleetStallPoll);
2329+
if (fleetSettle !== null) clearTimeout(fleetSettle);
23202330
unsubscribeFleetReport();
23212331
// Quitting mid-stream is an abnormal end for the in-flight cycle: nothing
23222332
// downstream delivers its terminal event once the app is gone.

0 commit comments

Comments
 (0)