@bridge4dev/runner 0.45.1 → 0.46.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/index.js CHANGED
@@ -145,6 +145,14 @@ function runnerCapabilities(apiUrlOverride) {
145
145
  resumeEpoch: true,
146
146
  /** Session 8: understands `maxSessions` and runs sessions side by side. */
147
147
  parallelSessions: true,
148
+ /**
149
+ * 0.46.0: reports which sessions are actually holding a seat here, so the
150
+ * API stops having to guess the occupancy of this machine from its own
151
+ * rows. Announced rather than inferred from the version, because the API
152
+ * runs a compatibility sweep for runners that cannot say it — and that
153
+ * sweep must switch off the moment this one can.
154
+ */
155
+ sessionSlots: true,
148
156
  /**
149
157
  * Session 9: can update itself on command. Reported as a capability rather
150
158
  * than inferred from the version, because the dashboard must not offer a
@@ -234,6 +242,13 @@ function runnerCapabilities(apiUrlOverride) {
234
242
  * anything the previous version could not.
235
243
  */
236
244
  gitRefs: true,
245
+ /**
246
+ * 0.46.0: умеет выложить файл из рабочей копии в хранилище платформы
247
+ * (`fs_publish`). Без флага дашборд не рисует кнопку «Опубликовать», а
248
+ * объясняет, что раннер надо обновить — кнопка, чей фрейм старый раннер
249
+ * молча уронит, хуже отсутствующей кнопки.
250
+ */
251
+ filePublish: true,
237
252
  /**
238
253
  * Session 14: reads `.devbridge/project.json`. Announced even when
239
254
  * verification is switched off — «this machine will not run recipes» and
@@ -1485,6 +1485,45 @@ export type RunnerFrame = {
1485
1485
  | {
1486
1486
  type: 'verify_report';
1487
1487
  report: Record<string, unknown>;
1488
+ }
1489
+ /**
1490
+ * How many seats this machine is REALLY holding, and which sessions hold them
1491
+ * (runner 0.46.0).
1492
+ *
1493
+ * Until this frame existed the occupancy of a dev server had two independent
1494
+ * answers — rows in the API's database, and entries in this process's map —
1495
+ * and nothing but a reconnect ever compared them. Every path that ended a
1496
+ * session on paper without releasing the entry silently burned a seat, and
1497
+ * the first symptom was a user being refused a session the dashboard had just
1498
+ * offered (31.08.2026).
1499
+ *
1500
+ * The ids, not only the count: a number tells the API that it disagrees, the
1501
+ * ids tell it WHICH sessions to lay to rest. That turns `reconcile` from
1502
+ * something that happens once per reconnect into something that happens
1503
+ * whenever the two sides drift.
1504
+ */
1505
+ | {
1506
+ type: 'session_slots';
1507
+ /**
1508
+ * Every session this runner still tracks, parked or not.
1509
+ *
1510
+ * This is the reconciliation list: «here is what I still know about».
1511
+ * The API compares it with its own rows and lays to rest anything it has
1512
+ * already buried.
1513
+ */
1514
+ sessionIds: string[];
1515
+ /**
1516
+ * The subset that a new session would actually have to wait for.
1517
+ *
1518
+ * Not the same question, and conflating them is a bug in both
1519
+ * directions. `ensureCapacity` PARKS an idle session (REVIEW or
1520
+ * WAITING_INPUT with no open question) to make room, so its seat is
1521
+ * available on demand and must not be counted as taken. What is left —
1522
+ * a mid-turn session, or one holding an unanswered question card — is
1523
+ * what really refuses the next one.
1524
+ */
1525
+ blockingIds: string[];
1526
+ maxSessions: number;
1488
1527
  } | {
1489
1528
  type: 'pong';
1490
1529
  };
package/dist/protocol.js CHANGED
@@ -369,7 +369,7 @@ export const GatewayFrameSchema = z.discriminatedUnion('type', [
369
369
  // git_branches, update_from_base, git_push, git_refs, workspace_state,
370
370
  // recipe_state, verify_start, verify_status, verify_cancel,
371
371
  // preview_checkout, preview_stop, propose_commit_message, git_stage,
372
- // git_unstage, git_discard, git_pull, git_merge_abort.
372
+ // git_unstage, git_discard, git_pull, git_merge_abort, fs_publish.
373
373
  name: z.string().min(1).max(64),
374
374
  workspaceId: z.string().optional(),
375
375
  sessionId: z.string().optional(),
@@ -123,6 +123,9 @@ export declare class Supervisor {
123
123
  private readonly verify;
124
124
  private readonly verifyReports;
125
125
  constructor(ws: RunnerWsClient, opts: SupervisorOptions);
126
+ /** How often the seat report re-derives the truth. See the constructor. */
127
+ private static readonly SLOTS_REPORT_INTERVAL_MS;
128
+ private readonly slotsTimer;
126
129
  /**
127
130
  * Push every unacked verdict at the API.
128
131
  *
@@ -132,6 +135,38 @@ export declare class Supervisor {
132
135
  */
133
136
  private flushVerifyReports;
134
137
  get activeSessionIds(): string[];
138
+ /** The last set of seat-holders we told the API about, to send only changes. */
139
+ private lastPublishedSlots;
140
+ /**
141
+ * Tell the API which sessions are actually holding a seat here (0.46.0).
142
+ *
143
+ * The seat count is a property of THIS process, and until now the API could
144
+ * only guess it from its own rows. The guess was wrong whenever a session
145
+ * ended on paper without releasing its entry, and wrong in the direction that
146
+ * costs a person their session: the dashboard offers a seat, the create
147
+ * passes, this runner refuses, the session dies in fifty milliseconds.
148
+ *
149
+ * Called from `reportStatus` — the choke point every seat change already
150
+ * passes through — and from the interval in the constructor, which is the net
151
+ * under any path that forgets. Both are safe to call as often as they like:
152
+ * this compares what it is about to say against what it last said and returns
153
+ * without touching the socket when nothing moved.
154
+ *
155
+ * Two lists, because two different questions are being asked and answering
156
+ * both with one number is wrong in both directions:
157
+ *
158
+ * - `sessionIds` — everything still tracked here, parked or not. This is the
159
+ * reconciliation list, and it must be generous: a session the API has
160
+ * buried needs laying to rest whatever state it is in on this side.
161
+ * - `blockingIds` — what a new session would really have to wait for.
162
+ * `ensureCapacity` parks an idle REVIEW or WAITING_INPUT session to make
163
+ * room, so those seats are available on demand.
164
+ *
165
+ * Reporting the second as the first would have the API stop live REVIEW
166
+ * sessions; reporting the first as the second would call a machine full while
167
+ * it had room. Both were caught by the independent QA review of this change.
168
+ */
169
+ private publishSlots;
135
170
  private onFrame;
136
171
  private startSession;
137
172
  /**
@@ -570,7 +605,61 @@ export declare class Supervisor {
570
605
  private isMidTurn;
571
606
  /** The three live setters, in the order that lets an explicit pick win. */
572
607
  private applyLiveSettings;
608
+ /**
609
+ * Let go of a session the API says no longer exists, so its files can go.
610
+ *
611
+ * `purge_session` and `clean` used to refuse outright while ANY entry for the
612
+ * id was in the map. That reads as caution and behaves as a deadlock: the
613
+ * frame arrives precisely because the API has already deleted the row, so
614
+ * nothing will ever come along to release the entry, the worktree stays on
615
+ * disk, and the retry runs until the purge ages out of its window. On
616
+ * production one such tombstone was still failing two hours later, on a
617
+ * machine that was refusing new sessions for exactly the seat it held.
618
+ *
619
+ * The API is the authority on whether a session exists. If it says gone, the
620
+ * honest answer is to end it here too — the same teardown a reconnect
621
+ * performs for a session missing from `hello_ack` — and only then report that
622
+ * we could not finish, if the process really will not go.
623
+ *
624
+ * Returns true when nothing is holding the id any more.
625
+ */
626
+ private releaseForCleanup;
627
+ /**
628
+ * How long cleanup waits for a stopped agent process to actually be gone.
629
+ *
630
+ * Deliberately a small slice of the API's ten-second command budget: removing
631
+ * the worktree still has to happen after this, under the repository lock, and
632
+ * a purge that times out on the wire is reported as a failure even when it
633
+ * succeeded here. Three seconds is enough for an ordinary exit; anything
634
+ * slower is better answered honestly, so the drain comes back and finds the
635
+ * entry already gone.
636
+ */
637
+ private static readonly CLEANUP_RELEASE_MS;
573
638
  private stopSession;
639
+ /**
640
+ * The ONE way a session ends on this runner.
641
+ *
642
+ * A terminal status is two facts, not one: the API is told the session is
643
+ * over, AND this machine stops holding a seat for it. They used to be
644
+ * separate lines at eight call sites, and three of them wrote only the first
645
+ * — `settleTurnStatus`'s failed-turn branch, `forwardEvent`'s `case 'error'`
646
+ * and `runApiRetry`. Each left a `RunningSession` in the map with a live
647
+ * `session` handle and `stopRequested === false`, which is precisely what
648
+ * `liveSessionCount` counts. The seat was then held for a session the API had
649
+ * already buried, until the next reconnect — weeks, on a healthy runner.
650
+ *
651
+ * On production (31.08.2026) that arithmetic refused a fourth session on a
652
+ * machine whose database said one was running, and the dashboard had offered
653
+ * the seat a moment earlier. `launchCrashed` had the rule right all along
654
+ * («an entry left in the map would hold one of the runner's few slots»); it
655
+ * just could not be the only place that knew it.
656
+ *
657
+ * Deliberately NOT folded into `reportStatus`: that method is also how a
658
+ * session reaches REVIEW, WAITING_INPUT and RUNNING, and a teardown hidden
659
+ * inside it would be invisible at exactly the call sites that must not tear
660
+ * anything down. The name says what it does.
661
+ */
662
+ private finishSession;
574
663
  private reconcile;
575
664
  /**
576
665
  * Housekeeping for the journal directory. Safe to call any time: live