@bridge4dev/runner 0.58.2 → 0.59.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/dist/adapters/claude.js +10 -3
- package/dist/supervisor.d.ts +53 -0
- package/dist/supervisor.js +91 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/adapters/claude.js
CHANGED
|
@@ -1798,10 +1798,17 @@ class ClaudeSession {
|
|
|
1798
1798
|
// as work. The id is forgotten when that task settles instead, which is the
|
|
1799
1799
|
// moment it stops being able to come back.
|
|
1800
1800
|
this.turnEpoch += 1;
|
|
1801
|
-
// Straight through `flushTasks` rather than an empty frame of its own
|
|
1802
|
-
//
|
|
1803
|
-
//
|
|
1801
|
+
// Straight through `flushTasks` rather than an empty frame of its own, and
|
|
1802
|
+
// with the de-duplication disarmed for this one frame (plan
|
|
1803
|
+
// `workflow-mode-fixes` S1 p.10). The turn's counters usually changed, but
|
|
1804
|
+
// not always: a task started in an earlier turn and still running, and a
|
|
1805
|
+
// turn that started nothing new, produce exactly the frame the last turn
|
|
1806
|
+
// ended on — and the end of a turn is the moment the API and the tray most
|
|
1807
|
+
// need to hear what is still running, whether or not it is news. The
|
|
1808
|
+
// supervisor de-duplicates the COUNT on its own side; this frame is the
|
|
1809
|
+
// tray's freshness, not the database's.
|
|
1804
1810
|
this.taskPublishedAt = 0;
|
|
1811
|
+
this.lastTaskFingerprint = '';
|
|
1805
1812
|
this.flushTasks();
|
|
1806
1813
|
}
|
|
1807
1814
|
/**
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -210,7 +210,30 @@ export interface SupervisorOptions {
|
|
|
210
210
|
readScopeHold?: (unit: string) => ScopeHold | null;
|
|
211
211
|
/** #398 S4: the heartbeat for the memory frame. Real one is a minute. */
|
|
212
212
|
sessionLimitsHeartbeatMs?: number;
|
|
213
|
+
/**
|
|
214
|
+
* S1 of plan `workflow-mode-fixes`: how often a resting session re-asserts
|
|
215
|
+
* a background count it still holds — a test seam over
|
|
216
|
+
* `BACKGROUND_TASKS_HEARTBEAT_MS`. Real one is five minutes.
|
|
217
|
+
*/
|
|
218
|
+
backgroundTasksHeartbeatMs?: number;
|
|
213
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* How often a resting session says «I still hold N background tasks» — the
|
|
222
|
+
* same `session_status` frame, the same status, sent again (plan
|
|
223
|
+
* `workflow-mode-fixes` S1 p.9).
|
|
224
|
+
*
|
|
225
|
+
* The count is a level the API stamps on arrival, and every reader over there
|
|
226
|
+
* — the workflow gate, its reminder, the «Agents» badge, the Inbox — believes
|
|
227
|
+
* it for an hour from that stamp. A task that simply runs (a forty-minute
|
|
228
|
+
* build) changes nothing in the tray, so nothing else would ever send the
|
|
229
|
+
* count again, and the platform would stop believing a live task after an
|
|
230
|
+
* hour. This is the frame that keeps it believed for as long as it is true.
|
|
231
|
+
*
|
|
232
|
+
* Mirrored by value in `@devbridge/shared` (`constants/dev-sessions.ts`),
|
|
233
|
+
* which this package cannot import; `supervisor.test.ts` holds the two copies
|
|
234
|
+
* together and keeps this well inside the API's trust window.
|
|
235
|
+
*/
|
|
236
|
+
export declare const BACKGROUND_TASKS_HEARTBEAT_MS: number;
|
|
214
237
|
export declare class Supervisor {
|
|
215
238
|
private readonly ws;
|
|
216
239
|
private readonly opts;
|
|
@@ -438,6 +461,14 @@ export declare class Supervisor {
|
|
|
438
461
|
private publishSlots;
|
|
439
462
|
/** The heartbeat window actually used — the constant, or a test's own. */
|
|
440
463
|
private readonly hostLoadHeartbeatMs;
|
|
464
|
+
/** S1 p.9: how often a held background count is said again. */
|
|
465
|
+
private readonly backgroundTasksHeartbeatMs;
|
|
466
|
+
/**
|
|
467
|
+
* The API has answered this socket's `hello` (S1 p.9). Frames sent before
|
|
468
|
+
* that are closed with 4002; the reconnect path re-sends every status in
|
|
469
|
+
* `reconcile` anyway, so the heartbeat has nothing to say until then.
|
|
470
|
+
*/
|
|
471
|
+
private helloAcked;
|
|
441
472
|
private readonly hostLoadTimer;
|
|
442
473
|
/** #398 S2: the stall detector's clock, and the deadline it enforces. */
|
|
443
474
|
private readonly stallTimer;
|
|
@@ -655,6 +686,28 @@ export declare class Supervisor {
|
|
|
655
686
|
*/
|
|
656
687
|
private idleSessionCeiling;
|
|
657
688
|
private publishSessionLimits;
|
|
689
|
+
/**
|
|
690
|
+
* Re-assert the background count of every resting session that still holds
|
|
691
|
+
* one (S1 p.9) — the same `session_status`, the same status, sent again.
|
|
692
|
+
*
|
|
693
|
+
* Only at rest, and only in the two statuses somebody over there reads the
|
|
694
|
+
* age of the number in: `REVIEW` and `WAITING_INPUT`. NOT in
|
|
695
|
+
* `WAITING_PERMISSION` — after a denied card `lastReported` stays there
|
|
696
|
+
* until the turn ends while the API has already healed its row to `RUNNING`,
|
|
697
|
+
* and repeating the old status would put the row back to «waiting for
|
|
698
|
+
* permission» with no card behind it, a fresh `statusChangedAt` and a
|
|
699
|
+
* needless nudge of the workflow engine. Not mid-turn either: a turn ends
|
|
700
|
+
* with a status frame of its own.
|
|
701
|
+
*
|
|
702
|
+
* Straight through `reportStatus`, past the equality check in
|
|
703
|
+
* `setBackgroundTasks`: that one exists to stay quiet when nothing changed,
|
|
704
|
+
* and «nothing changed» is exactly what this frame is for. Measured from the
|
|
705
|
+
* last status frame of ANY kind — every one carries the count — so a session
|
|
706
|
+
* that just reported is not reported twice, and a reconnect (which re-sends
|
|
707
|
+
* every tracked session's status in `reconcile`) restarts the five minutes
|
|
708
|
+
* by itself.
|
|
709
|
+
*/
|
|
710
|
+
private publishBackgroundHeartbeats;
|
|
658
711
|
private publishHostLoad;
|
|
659
712
|
private onFrame;
|
|
660
713
|
private startSession;
|
package/dist/supervisor.js
CHANGED
|
@@ -80,6 +80,23 @@ function freshLevels() {
|
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
const LAUNCH_REFUSED = { ok: false, reason: 'refused' };
|
|
83
|
+
/**
|
|
84
|
+
* How often a resting session says «I still hold N background tasks» — the
|
|
85
|
+
* same `session_status` frame, the same status, sent again (plan
|
|
86
|
+
* `workflow-mode-fixes` S1 p.9).
|
|
87
|
+
*
|
|
88
|
+
* The count is a level the API stamps on arrival, and every reader over there
|
|
89
|
+
* — the workflow gate, its reminder, the «Agents» badge, the Inbox — believes
|
|
90
|
+
* it for an hour from that stamp. A task that simply runs (a forty-minute
|
|
91
|
+
* build) changes nothing in the tray, so nothing else would ever send the
|
|
92
|
+
* count again, and the platform would stop believing a live task after an
|
|
93
|
+
* hour. This is the frame that keeps it believed for as long as it is true.
|
|
94
|
+
*
|
|
95
|
+
* Mirrored by value in `@devbridge/shared` (`constants/dev-sessions.ts`),
|
|
96
|
+
* which this package cannot import; `supervisor.test.ts` holds the two copies
|
|
97
|
+
* together and keeps this well inside the API's trust window.
|
|
98
|
+
*/
|
|
99
|
+
export const BACKGROUND_TASKS_HEARTBEAT_MS = 5 * 60 * 1000;
|
|
83
100
|
export class Supervisor {
|
|
84
101
|
ws;
|
|
85
102
|
opts;
|
|
@@ -177,6 +194,9 @@ export class Supervisor {
|
|
|
177
194
|
ws.on('frame', (frame) => {
|
|
178
195
|
void this.onFrame(frame).catch((error) => log.error('supervisor: frame handler failed', { type: frame.type, error: String(error) }));
|
|
179
196
|
});
|
|
197
|
+
ws.on('close', () => {
|
|
198
|
+
this.helloAcked = false;
|
|
199
|
+
});
|
|
180
200
|
/**
|
|
181
201
|
* The seat report is on a heartbeat, not only on the events that change it.
|
|
182
202
|
*
|
|
@@ -233,8 +253,14 @@ export class Supervisor {
|
|
|
233
253
|
* actually moved (or the heartbeat came due).
|
|
234
254
|
*/
|
|
235
255
|
this.hostLoadHeartbeatMs = opts.hostLoadHeartbeatMs ?? HOST_LOAD_HEARTBEAT_MS;
|
|
256
|
+
this.backgroundTasksHeartbeatMs =
|
|
257
|
+
opts.backgroundTasksHeartbeatMs ?? BACKGROUND_TASKS_HEARTBEAT_MS;
|
|
236
258
|
this.hostLoadTimer = setInterval(() => {
|
|
237
259
|
this.publishHostLoad();
|
|
260
|
+
// The fourth thing only a tick can notice (S1 p.9): a background task
|
|
261
|
+
// that is simply still running produces no event, and the API's belief
|
|
262
|
+
// in the count runs out an hour after it was last said.
|
|
263
|
+
this.publishBackgroundHeartbeats();
|
|
238
264
|
// Same cadence, same reason: what a session's cgroup is going through is
|
|
239
265
|
// something that happens to it, and only a tick can notice (#387).
|
|
240
266
|
this.watchSessionCages();
|
|
@@ -641,6 +667,14 @@ export class Supervisor {
|
|
|
641
667
|
}
|
|
642
668
|
/** The heartbeat window actually used — the constant, or a test's own. */
|
|
643
669
|
hostLoadHeartbeatMs;
|
|
670
|
+
/** S1 p.9: how often a held background count is said again. */
|
|
671
|
+
backgroundTasksHeartbeatMs;
|
|
672
|
+
/**
|
|
673
|
+
* The API has answered this socket's `hello` (S1 p.9). Frames sent before
|
|
674
|
+
* that are closed with 4002; the reconnect path re-sends every status in
|
|
675
|
+
* `reconcile` anyway, so the heartbeat has nothing to say until then.
|
|
676
|
+
*/
|
|
677
|
+
helloAcked = false;
|
|
644
678
|
hostLoadTimer;
|
|
645
679
|
/** #398 S2: the stall detector's clock, and the deadline it enforces. */
|
|
646
680
|
stallTimer;
|
|
@@ -1531,6 +1565,58 @@ export class Supervisor {
|
|
|
1531
1565
|
this.lastLimitsSentAt = measuredAt;
|
|
1532
1566
|
}
|
|
1533
1567
|
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Re-assert the background count of every resting session that still holds
|
|
1570
|
+
* one (S1 p.9) — the same `session_status`, the same status, sent again.
|
|
1571
|
+
*
|
|
1572
|
+
* Only at rest, and only in the two statuses somebody over there reads the
|
|
1573
|
+
* age of the number in: `REVIEW` and `WAITING_INPUT`. NOT in
|
|
1574
|
+
* `WAITING_PERMISSION` — after a denied card `lastReported` stays there
|
|
1575
|
+
* until the turn ends while the API has already healed its row to `RUNNING`,
|
|
1576
|
+
* and repeating the old status would put the row back to «waiting for
|
|
1577
|
+
* permission» with no card behind it, a fresh `statusChangedAt` and a
|
|
1578
|
+
* needless nudge of the workflow engine. Not mid-turn either: a turn ends
|
|
1579
|
+
* with a status frame of its own.
|
|
1580
|
+
*
|
|
1581
|
+
* Straight through `reportStatus`, past the equality check in
|
|
1582
|
+
* `setBackgroundTasks`: that one exists to stay quiet when nothing changed,
|
|
1583
|
+
* and «nothing changed» is exactly what this frame is for. Measured from the
|
|
1584
|
+
* last status frame of ANY kind — every one carries the count — so a session
|
|
1585
|
+
* that just reported is not reported twice, and a reconnect (which re-sends
|
|
1586
|
+
* every tracked session's status in `reconcile`) restarts the five minutes
|
|
1587
|
+
* by itself.
|
|
1588
|
+
*/
|
|
1589
|
+
publishBackgroundHeartbeats() {
|
|
1590
|
+
// `helloAcked` and not merely `connected`: the socket is open from the
|
|
1591
|
+
// `open` event, but the gateway answers any frame that reaches it before
|
|
1592
|
+
// its own `hello` bookkeeping is done with a close (4002) — a window of
|
|
1593
|
+
// tens of milliseconds on every reconnect that a tick can land in.
|
|
1594
|
+
if (!this.ws.connected || !this.helloAcked)
|
|
1595
|
+
return;
|
|
1596
|
+
const now = Date.now();
|
|
1597
|
+
for (const running of this.sessions.values()) {
|
|
1598
|
+
if (running.backgroundTasks <= 0)
|
|
1599
|
+
continue;
|
|
1600
|
+
// Only a session with a process behind it. The count is zeroed when the
|
|
1601
|
+
// process is found gone, but not on every path that loses one: a
|
|
1602
|
+
// relaunch that crashed leaves the entry with `session = null` and the
|
|
1603
|
+
// count it was holding, and re-asserting that would be the platform
|
|
1604
|
+
// confirming work nobody is doing.
|
|
1605
|
+
if (!running.session)
|
|
1606
|
+
continue;
|
|
1607
|
+
if (running.lastReported !== 'REVIEW' && running.lastReported !== 'WAITING_INPUT')
|
|
1608
|
+
continue;
|
|
1609
|
+
const sinceLastSent = now - (running.statusSentAt ?? 0);
|
|
1610
|
+
// A backwards clock step is «due», not «early» — same allowance as the
|
|
1611
|
+
// host-load heartbeat, for the same reason.
|
|
1612
|
+
if (sinceLastSent < this.backgroundTasksHeartbeatMs && sinceLastSent >= 0)
|
|
1613
|
+
continue;
|
|
1614
|
+
this.reportStatus(running.descriptor.id, running.lastReported, {
|
|
1615
|
+
costUsd: running.costUsd,
|
|
1616
|
+
activeMs: Supervisor.spentMs(running),
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1534
1620
|
publishHostLoad() {
|
|
1535
1621
|
const sample = (this.opts.readHostLoad ?? readHostLoad)();
|
|
1536
1622
|
if (!sample)
|
|
@@ -1549,6 +1635,7 @@ export class Supervisor {
|
|
|
1549
1635
|
async onFrame(frame) {
|
|
1550
1636
|
switch (frame.type) {
|
|
1551
1637
|
case 'hello_ack':
|
|
1638
|
+
this.helloAcked = true;
|
|
1552
1639
|
// A new connection knows nothing about the seats we reported to the
|
|
1553
1640
|
// last one — the API keeps that beside the socket, not in the database,
|
|
1554
1641
|
// because it is only true while the socket is. Forget what we told the
|
|
@@ -6978,6 +7065,10 @@ export class Supervisor {
|
|
|
6978
7065
|
// would mean it goes on claiming background work forever.
|
|
6979
7066
|
...(running ? { backgroundTasks: running.backgroundTasks } : {}),
|
|
6980
7067
|
});
|
|
7068
|
+
// …and the count's age starts here (S1 p.9): the background heartbeat is
|
|
7069
|
+
// due five minutes after the last frame that carried it, whichever it was.
|
|
7070
|
+
if (running)
|
|
7071
|
+
running.statusSentAt = Date.now();
|
|
6981
7072
|
// Every seat change is accompanied by a status report — a session starting,
|
|
6982
7073
|
// parking, ending. Hooking the seat report here rather than at each of those
|
|
6983
7074
|
// is the whole point: «remember to also tell the API» is the rule that had
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.59.1";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED