@bridge4dev/runner 0.59.1 → 0.61.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.
@@ -116,6 +116,28 @@ export declare function installIsWritable(packageDir?: string | null): boolean;
116
116
  * that would work is recoverable, disappearing from the user's server is not.
117
117
  */
118
118
  export declare function isSupervisedProcess(env?: NodeJS.ProcessEnv): boolean;
119
+ /**
120
+ * The `hello` pair that decides whether the card draws a «Restart runner»
121
+ * button (#396, plan `workflow-stage-restart` R13).
122
+ *
123
+ * The same shape as `selfUpdate` / `selfUpdateBlocked`, and for the same
124
+ * reason: an older runner drops a command it cannot parse without answering, so
125
+ * the dashboard must gate on an announced capability rather than on a version.
126
+ *
127
+ * Only `isSupervisedProcess()`, and NOT `selfUpdatable()`: replacing the
128
+ * package needs an installed, writable npm package, restarting needs only
129
+ * something that will start us again. A daemon run from a source checkout under
130
+ * systemd — the dogfood box — restarts perfectly well and must get the button.
131
+ *
132
+ * Lives here rather than beside the other capabilities in `index.ts` because
133
+ * that file exports nothing and is the CLI entry point: a key spelled wrong
134
+ * there is caught by nobody, and `capabilities` is an untyped bag on both ends.
135
+ */
136
+ export declare function restartCapability(env?: NodeJS.ProcessEnv): {
137
+ restart: true;
138
+ } | {
139
+ restartBlocked: 'unsupervised';
140
+ };
119
141
  /**
120
142
  * Same origin as the API we are paired with, and it really is a tarball.
121
143
  *
@@ -148,6 +148,28 @@ export function installIsWritable(packageDir = resolveInstalledPackageDir()) {
148
148
  export function isSupervisedProcess(env = process.env) {
149
149
  return typeof env['INVOCATION_ID'] === 'string' && env['INVOCATION_ID'].length > 0;
150
150
  }
151
+ /**
152
+ * The `hello` pair that decides whether the card draws a «Restart runner»
153
+ * button (#396, plan `workflow-stage-restart` R13).
154
+ *
155
+ * The same shape as `selfUpdate` / `selfUpdateBlocked`, and for the same
156
+ * reason: an older runner drops a command it cannot parse without answering, so
157
+ * the dashboard must gate on an announced capability rather than on a version.
158
+ *
159
+ * Only `isSupervisedProcess()`, and NOT `selfUpdatable()`: replacing the
160
+ * package needs an installed, writable npm package, restarting needs only
161
+ * something that will start us again. A daemon run from a source checkout under
162
+ * systemd — the dogfood box — restarts perfectly well and must get the button.
163
+ *
164
+ * Lives here rather than beside the other capabilities in `index.ts` because
165
+ * that file exports nothing and is the CLI entry point: a key spelled wrong
166
+ * there is caught by nobody, and `capabilities` is an untyped bag on both ends.
167
+ */
168
+ export function restartCapability(env = process.env) {
169
+ // The literal string matters: the dashboard renders the explanatory sentence
170
+ // for exactly this value and silently draws nothing for any other.
171
+ return isSupervisedProcess(env) ? { restart: true } : { restartBlocked: 'unsupervised' };
172
+ }
151
173
  /**
152
174
  * Same origin as the API we are paired with, and it really is a tarball.
153
175
  *
@@ -39,6 +39,20 @@ export interface SupervisorOptions {
39
39
  * reports the new version only after the next manual restart.
40
40
  */
41
41
  onRestartRequested?: (outcome: SelfUpdateOutcome) => void;
42
+ /**
43
+ * Called after a `runner_restart` the machine agreed to, once the reply is on
44
+ * the wire (#396). Separate from `onRestartRequested` rather than folded into
45
+ * it: that one is typed on `SelfUpdateOutcome` and its handler logs the
46
+ * version it is restarting INTO, which a plain restart has no answer for.
47
+ */
48
+ onRestartCommanded?: (note: string | null) => void;
49
+ /**
50
+ * Build an adapter for an agent that was not on PATH when the daemon started
51
+ * (#395). Returning `null` means «still not here», and the session is refused
52
+ * in the same words as before. Only the entry point can supply this: it owns
53
+ * the config the adapters are built from.
54
+ */
55
+ makeAdapter?: (agent: 'CLAUDE' | 'CODEX') => AgentAdapter | null;
42
56
  /**
43
57
  * Local ceiling on concurrent agent processes, from the runner's own config
44
58
  * (layer 1). When set it wins over the API's number if it is stricter — the
@@ -216,6 +230,8 @@ export interface SupervisorOptions {
216
230
  * `BACKGROUND_TASKS_HEARTBEAT_MS`. Real one is five minutes.
217
231
  */
218
232
  backgroundTasksHeartbeatMs?: number;
233
+ /** #348: the compaction net, as a test seam over `COMPACTION_WATCHDOG_MS`. */
234
+ compactionWatchdogMs?: number;
219
235
  }
220
236
  /**
221
237
  * How often a resting session says «I still hold N background tasks» — the
@@ -260,6 +276,8 @@ export declare class Supervisor {
260
276
  private static readonly EMPTY_TURN_SETTLE_MS;
261
277
  /** The window actually used — the constant, or a test's own shorter one. */
262
278
  private readonly emptyTurnSettleMs;
279
+ /** The compaction net actually used — the constant, or a test's own. */
280
+ private readonly compactionWatchdogMs;
263
281
  private readonly rateLimitsResendMs;
264
282
  /** The stop-settle window actually used — the constant, or a test's own. */
265
283
  private readonly stopSettleMs;
@@ -305,8 +323,31 @@ export declare class Supervisor {
305
323
  * which of the two is running.
306
324
  */
307
325
  private installInFlight;
326
+ /**
327
+ * A `runner_restart` has been agreed to and the exit is coming (#396).
328
+ *
329
+ * Never cleared: the process has ~1.5 s left, and everything it must refuse in
330
+ * that window it must refuse for good.
331
+ */
332
+ private restarting;
308
333
  /** A restore-point collection is running; a second reconnect must not start another (#388). */
309
334
  private checkpointGcInFlight;
335
+ /**
336
+ * Sessions the current `hello_ack` names, while `reconcile` is walking them
337
+ * (#392).
338
+ *
339
+ * The unacked replay at the top of `reconcile` sends every journal's
340
+ * leftovers before a single session is registered, and the acks come back
341
+ * while the loop is still awaiting an earlier session's worktree. The ack
342
+ * handler deletes the journal of a session it does not know once nothing is
343
+ * unacked — which used to be exactly the journal the loop was about to
344
+ * restore from: its open cards, its queued messages, its anchor, gone a
345
+ * moment before they were read. Membership here is what «this session is
346
+ * known» means until the loop reaches it.
347
+ */
348
+ private readonly reconciling;
349
+ /** How long a dashboard-requested compaction may go unreported before the session is handed back. */
350
+ private static readonly COMPACTION_WATCHDOG_MS;
310
351
  /** Session 14: one project-recipe run per machine, and its verdict queue. */
311
352
  private readonly verify;
312
353
  private readonly verifyReports;
@@ -322,6 +363,21 @@ export declare class Supervisor {
322
363
  * sent — the next `hello_ack` or the hourly tick carries the same fact, and
323
364
  * the measurement behind it is cached, so retrying is nearly free.
324
365
  */
366
+ /**
367
+ * The adapter for this agent — built now if it was not there at boot (#395).
368
+ *
369
+ * `opts.adapters` is assembled once, when the daemon starts, from what was on
370
+ * PATH at that moment. That is the SAME frozen snapshot `capabilities.agents`
371
+ * was, one layer down: press «Install Codex» on a running machine and the
372
+ * measurement updates, the card lights up, the API lets the session through —
373
+ * and this map still has no CODEX in it, so the session is created and dies
374
+ * at once. Asking the factory closes the last of the three lists.
375
+ *
376
+ * Built at most once per process and remembered: preparing Codex's isolated
377
+ * home clones ~90 MB on a machine that has never run it, and that is a price
378
+ * to pay on the first session, not on every one.
379
+ */
380
+ private adapterFor;
325
381
  /**
326
382
  * Why an install cannot start right now, in words for the person who pressed.
327
383
  *
@@ -877,6 +933,26 @@ export declare class Supervisor {
877
933
  * chance, and the alternative there is a card that stays clickable forever.
878
934
  */
879
935
  private withdrawOpenQuestions;
936
+ /**
937
+ * Close out the asks a PREVIOUS process of this runner left open (#392).
938
+ *
939
+ * `withdrawOpenQuestions` above reads `running.openQuestions`, and that set
940
+ * is memory: a process that died without reaching `shutdown()` — killed,
941
+ * crashed, machine rebooted — took it along, and the restore used to create
942
+ * a fresh empty set and write only «Runner reconnected». The card in the
943
+ * browser is closed by one thing, a `question_resolved` with its askId in
944
+ * the feed, so it stayed answerable for ever, and everything typed into it
945
+ * was lost (the 07.09.2026 session in the ticket: six restarts, zero
946
+ * tombstones).
947
+ *
948
+ * The journal is the set that survived: `SessionJournal.append` notes every
949
+ * card this runner published and every resolution it published, whoever
950
+ * wrote it. That is also the «already closed» check the plan asks to name:
951
+ * a graceful `shutdown()` sends its `question_resolved` through the same
952
+ * `append`, so by the time a restore runs, a card closed that way is no
953
+ * longer in the set — one tombstone per card, never two.
954
+ */
955
+ private withdrawJournaledQuestions;
880
956
  /**
881
957
  * Deliver messages that were held because every slot was taken.
882
958
  *
@@ -983,6 +1059,23 @@ export declare class Supervisor {
983
1059
  * phantom can never fire after the turn it belonged to has closed properly.
984
1060
  */
985
1061
  private clearEmptyTurn;
1062
+ /**
1063
+ * A compaction is over for a reason other than the agent saying so (#348):
1064
+ * the turn it rode on ended, the process died, a new process was launched.
1065
+ * The feed gets its closing line so the pair is complete, and the runner's
1066
+ * own state is dropped so the NEXT compaction opens a new pair — left set,
1067
+ * every later start would be read as a repeat and written nowhere (QA S4).
1068
+ */
1069
+ private endCompaction;
1070
+ /** A compaction has begun: say so in the feed, once, and remember who started it (#348). */
1071
+ private openCompaction;
1072
+ /**
1073
+ * The net under a dashboard-requested compaction (#348) — see
1074
+ * `RunningSession.compactionWatchdog`. Fires only into a session that has
1075
+ * said nothing since: every agent event and every ending clears it.
1076
+ */
1077
+ private armCompactionWatchdog;
1078
+ private clearCompactionWatchdog;
986
1079
  /**
987
1080
  * Record how many subagents are alive, and say so when it matters (#236).
988
1081
  *
@@ -1024,8 +1117,9 @@ export declare class Supervisor {
1024
1117
  * It happens for real: a background subagent's report wakes a new turn inside
1025
1118
  * the agent process (SDK 0.3.226 emits a second `system:init` and a `result`
1026
1119
  * carrying `origin: {kind:'task-notification'}`), and nothing here reports a
1027
- * turn the runner did not start. Compaction does the same, and so does a
1028
- * question this runner withdraws by itself.
1120
+ * turn the runner did not start. So does a question this runner withdraws by
1121
+ * itself. (A compaction used to be the third case; `compact_context` reports
1122
+ * RUNNING itself now, and #348 gave the compaction lines of its own.)
1029
1123
  *
1030
1124
  * An open question is the one thing that must survive: there, WAITING_INPUT
1031
1125
  * means a tool call is parked on a person, and the agent narrating around its
@@ -1376,7 +1470,34 @@ export declare class Supervisor {
1376
1470
  * anything down. The name says what it does.
1377
1471
  */
1378
1472
  private finishSession;
1473
+ /**
1474
+ * The terminal status a reconnect should replay for this session instead
1475
+ * of restoring it, or null (QA-96 F1).
1476
+ *
1477
+ * Only a terminal status from THIS life of the session. A resumed session
1478
+ * carries a higher epoch, and replaying the FAILED it was resumed from would
1479
+ * kill it again the moment the runner reconnects. A journal written by a
1480
+ * runner from before session 13 could hold a `DONE` — it is no longer a
1481
+ * status this runner may report, and replaying one would close the session
1482
+ * for the user. Drop it and let the session be picked back up like any
1483
+ * other. Pure: reads the journal and decides, so the pre-registration pass
1484
+ * and the loop answer the question the same way.
1485
+ */
1486
+ private terminalStatusToReplay;
1487
+ /**
1488
+ * Build and register the entry for a session a runner restart interrupted,
1489
+ * and close the cards its dead process left open (#392).
1490
+ *
1491
+ * Synchronous on purpose, and called for EVERY such session before the
1492
+ * reconnect awaits anything: registered, the session is known (a message
1493
+ * for it waits in its queue behind the `starting` gate of #401 instead of
1494
+ * coming back as `session_unknown`), and its cards go dark first thing —
1495
+ * above the line about the restart, so a person reading down sees the card
1496
+ * close before they read why.
1497
+ */
1498
+ private registerRestoredSession;
1379
1499
  private reconcile;
1500
+ private reconcileKnown;
1380
1501
  /**
1381
1502
  * Housekeeping for the journal directory. Safe to call any time: live
1382
1503
  * sessions are skipped and a journal still holding unacked events survives