@bridge4dev/runner 0.60.0 → 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.
@@ -2180,6 +2180,65 @@ class ClaudeSession {
2180
2180
  conversationAnchor() {
2181
2181
  return this.lastMessageUuid;
2182
2182
  }
2183
+ /** A compaction the CLI has announced and not yet ended (#348). */
2184
+ compacting = false;
2185
+ /**
2186
+ * `system:status` — the CLI's own word on what it is doing (#348).
2187
+ *
2188
+ * Read the way the CLI's own remote client reads it (`noteInbound`):
2189
+ * «compacting» opens a compaction, `requesting` says nothing about one (the
2190
+ * summary itself is a model request, and this status arrives DURING a
2191
+ * compaction), and any other status closes it — with `compact_result` when
2192
+ * the CLI says how it went, and as «skipped» when it does not: a PreCompact
2193
+ * hook that said no, or a reactive compaction that found nothing to fold,
2194
+ * both end on a bare `status: null`. Left unclosed, that start line would
2195
+ * keep the dashboard saying «compacting» until the turn ended, minutes later.
2196
+ *
2197
+ * De-duplicated here as well as in the supervisor: the CLI re-says
2198
+ * «compacting» every thirty seconds while it waits on a precomputed summary.
2199
+ *
2200
+ * No `standalone` on this agent: `/compact` is a turn, and its `result`
2201
+ * settles the status the ordinary way.
2202
+ */
2203
+ noteStatus(msg) {
2204
+ const status = msg.status;
2205
+ if (status === 'requesting')
2206
+ return;
2207
+ if (status === 'compacting') {
2208
+ if (this.compacting)
2209
+ return;
2210
+ this.compacting = true;
2211
+ this.emit({ type: 'compaction', phase: 'started' });
2212
+ return;
2213
+ }
2214
+ // An end is only an end of something that began: without a start on
2215
+ // record there is nothing to close, and the second of two endings (a
2216
+ // `compact_result` after a `compact_boundary`) must not open a second
2217
+ // pair. A mode change also travels on this message — `{status: null,
2218
+ // permissionMode}` on every `setPermissionMode` in CLI 2.1.271 — and says
2219
+ // nothing about a compaction at all.
2220
+ if (!this.compacting)
2221
+ return;
2222
+ const compactResult = msg.compact_result;
2223
+ if (compactResult === undefined && msg.permissionMode !== undefined)
2224
+ return;
2225
+ this.compacting = false;
2226
+ if (compactResult === 'failed') {
2227
+ this.emit({
2228
+ type: 'compaction',
2229
+ phase: 'finished',
2230
+ ok: false,
2231
+ error: maskString(String(msg.compact_error ?? 'unknown')).slice(0, 300),
2232
+ });
2233
+ return;
2234
+ }
2235
+ this.emit({
2236
+ type: 'compaction',
2237
+ phase: 'finished',
2238
+ ok: true,
2239
+ ...(compactResult === 'success' ? {} : { skipped: true }),
2240
+ });
2241
+ }
2183
2242
  /**
2184
2243
  * `/compact` — the CLI's own command, delivered as ordinary user input.
2185
2244
  *
@@ -2296,19 +2355,15 @@ class ClaudeSession {
2296
2355
  this.refreshUsage();
2297
2356
  }
2298
2357
  else if (msg.subtype === 'status') {
2299
- const status = msg.status;
2300
- const compactResult = msg.compact_result;
2301
- if (status === 'compacting') {
2302
- this.emit({ type: 'notice', level: 'info', text: 'Compacting the conversation…' });
2303
- }
2304
- else if (compactResult) {
2305
- this.emit({
2306
- type: 'notice',
2307
- level: compactResult === 'failed' ? 'warn' : 'info',
2308
- text: compactResult === 'failed'
2309
- ? `Compaction failed: ${maskString(String(msg.compact_error ?? 'unknown')).slice(0, 300)}`
2310
- : 'Conversation compacted',
2311
- });
2358
+ this.noteStatus(msg);
2359
+ }
2360
+ else if (msg.subtype === 'compact_boundary') {
2361
+ // The summary is in place: this marker is the end of a
2362
+ // compaction whether or not a status said so first (#348) — the
2363
+ // CLI's own remote client reads it the same way.
2364
+ if (this.compacting) {
2365
+ this.compacting = false;
2366
+ this.emit({ type: 'compaction', phase: 'finished', ok: true });
2312
2367
  }
2313
2368
  }
2314
2369
  else if (msg.subtype === 'api_retry') {
@@ -2440,6 +2495,10 @@ class ClaudeSession {
2440
2495
  break;
2441
2496
  }
2442
2497
  case 'result': {
2498
+ // A turn's end is the end of any compaction it carried (#348):
2499
+ // the supervisor closes the pair on `turn_end`, and the flag here
2500
+ // has to agree, or the next «compacting» would be read as a repeat.
2501
+ this.compacting = false;
2443
2502
  this.emit({
2444
2503
  type: 'cost',
2445
2504
  costUsd: msg.total_cost_usd,
@@ -876,6 +876,7 @@ class CodexSession {
876
876
  return false;
877
877
  try {
878
878
  await this.client.request('thread/compact/start', { threadId: this.threadId });
879
+ this.requestedCompaction = true;
879
880
  return true;
880
881
  }
881
882
  catch (error) {
@@ -883,6 +884,14 @@ class CodexSession {
883
884
  return false;
884
885
  }
885
886
  }
887
+ /**
888
+ * A compaction THIS adapter asked for with `thread/compact/start` (#348) —
889
+ * the one kind that runs outside any turn and therefore has nothing to
890
+ * settle the session status behind it. The agent's own compaction inside a
891
+ * turn is not it, even when its item happens to complete before the
892
+ * `turn/started` that announces the turn.
893
+ */
894
+ requestedCompaction = false;
886
895
  stop(reason = 'session_stopped') {
887
896
  if (this.stopped)
888
897
  return;
@@ -1472,8 +1481,19 @@ class CodexSession {
1472
1481
  return;
1473
1482
  }
1474
1483
  case 'contextCompaction': {
1475
- if (done)
1476
- this.notice('info', 'Conversation compacted');
1484
+ // #348: an event, not a notice — see `AgentEvent['compaction']`. A
1485
+ // compaction started from the dashboard runs outside any turn on this
1486
+ // agent (`thread/compact/start` creates none), so nothing will settle
1487
+ // the status behind it; `standalone` tells the supervisor to.
1488
+ if (method === 'item/started') {
1489
+ this.emit({ type: 'compaction', phase: 'started' });
1490
+ return;
1491
+ }
1492
+ if (done) {
1493
+ const standalone = this.requestedCompaction && this.activeTurnId === null;
1494
+ this.requestedCompaction = false;
1495
+ this.emit({ type: 'compaction', phase: 'finished', ok: true, standalone });
1496
+ }
1477
1497
  return;
1478
1498
  }
1479
1499
  case 'webSearch':
@@ -110,6 +110,7 @@ const INVALIDATION_PHRASES = {
110
110
  turn_aborted: 'the turn was interrupted',
111
111
  agent_cancelled: 'the agent withdrew it',
112
112
  budget_spent: 'the session ran out of its allowed working time',
113
+ not_held: 'the agent no longer holds this question',
113
114
  };
114
115
  /** The question was taken away from the user — say why, never fake an answer. */
115
116
  export function invalidationMessage(reason) {
@@ -366,7 +366,16 @@ export interface AgentQuestionAnswer {
366
366
  notes?: string;
367
367
  }
368
368
  /** Why a question was taken away from the user without them answering it. */
369
- export type QuestionInvalidationReason = 'session_stopped' | 'session_parked' | 'runner_restarted' | 'turn_aborted' | 'agent_cancelled' | 'budget_spent';
369
+ export type QuestionInvalidationReason = 'session_stopped' | 'session_parked' | 'runner_restarted' | 'turn_aborted' | 'agent_cancelled' | 'budget_spent'
370
+ /**
371
+ * An answer arrived for a card no live process holds (#392): the card
372
+ * outlived the process that asked, or the ask was withdrawn before the
373
+ * answer got here. The runner does not know which, and says only what it
374
+ * does know. Never handed to an agent — there is no agent holding the ask
375
+ * to hand it to — so its phrase in `INVALIDATION_PHRASES` is for the type
376
+ * checker; the words a person reads are the dashboard's.
377
+ */
378
+ | 'not_held';
370
379
  /** The dashboard's answer to one open ask. */
371
380
  export interface QuestionReply {
372
381
  askId: string;
@@ -502,6 +511,38 @@ export type AgentEvent = {
502
511
  type: 'notice';
503
512
  level: 'info' | 'warn';
504
513
  text: string;
514
+ }
515
+ /**
516
+ * The conversation is being folded into a summary, or just was (#348).
517
+ *
518
+ * Its own event and not a `notice` on purpose: adapter notices are
519
+ * de-duplicated per session on their text (gotcha §148), so «Conversation
520
+ * compacted» reached the feed once and every later compaction was silence.
521
+ * A compaction is a fact the feed has to hear every time — the dashboard's
522
+ * «compacting…» indicator is read off the feed, and a second compaction
523
+ * with no lines is a second compaction the person cannot see end.
524
+ *
525
+ * `standalone` on the ending: `true` when NO turn is going to close behind
526
+ * this compaction, so the supervisor has to settle the session status
527
+ * itself. Codex runs a compaction started from the dashboard outside any
528
+ * turn (`thread/compact/start`); Claude delivers `/compact` as a turn, and
529
+ * that turn's own ending settles the status the ordinary way.
530
+ */
531
+ | {
532
+ type: 'compaction';
533
+ phase: 'started';
534
+ } | {
535
+ type: 'compaction';
536
+ phase: 'finished';
537
+ ok: boolean;
538
+ error?: string;
539
+ standalone?: boolean;
540
+ /**
541
+ * It ended without a summary: the CLI decided not to compact after all
542
+ * (a PreCompact hook said no, or it found nothing worth folding). Not a
543
+ * failure — nothing broke — and not a compaction either.
544
+ */
545
+ skipped?: boolean;
505
546
  } | {
506
547
  type: 'cost';
507
548
  costUsd: number;
package/dist/journal.d.ts CHANGED
@@ -58,6 +58,23 @@ export declare class SessionJournal {
58
58
  anchor: string;
59
59
  providerSessionId: string;
60
60
  } | null;
61
+ /**
62
+ * Every card this journal has published, and whether it is still open —
63
+ * in the order they were asked (#392).
64
+ *
65
+ * The persistent twin of the supervisor's `running.openQuestions`: that set
66
+ * dies with the process, and a process that dies ungracefully never gets to
67
+ * `shutdown()`, which is the only place it withdraws them. This one is
68
+ * rebuilt from the file, so the next process can close what the last one
69
+ * left open — the card in the browser does not know the runner restarted.
70
+ *
71
+ * Closed cards are remembered too, not only dropped: «this card was closed
72
+ * by somebody» is the fact that stops a SECOND tombstone. A queued answer
73
+ * that reaches the next process after a restore has already closed the card
74
+ * must rescue the words and say nothing more about the card — and the only
75
+ * witness that the card was closed is this file.
76
+ */
77
+ private readonly asks;
61
78
  constructor(sessionId: string, dir?: string);
62
79
  private replay;
63
80
  private write;
@@ -86,6 +103,29 @@ export declare class SessionJournal {
86
103
  recordStatus(status: string, extra?: Record<string, unknown>, epoch?: number): void;
87
104
  /** Assign the next seq and persist the event before it is sent. */
88
105
  append(eventType: string, payload: Record<string, unknown>): JournalEvent;
106
+ /**
107
+ * Keep `openAsks` in step with the cards going out through `append` (#392).
108
+ *
109
+ * Here and not in the supervisor, because `append` is the one door every
110
+ * event takes: a card that was published was published through it, and so
111
+ * was every resolution — the adapter's own, `withdrawOpenQuestions`, the
112
+ * `not_open` miss. A set kept beside the callers would need every one of
113
+ * them to remember it, which is exactly how the in-memory set came to be
114
+ * empty at the moment it was needed.
115
+ *
116
+ * A resolution for a card this journal never published writes nothing:
117
+ * there is nothing to close, and remembering strangers would only grow the
118
+ * file.
119
+ */
120
+ private trackAsk;
121
+ /** The cards still waiting on a person, oldest first (#392). */
122
+ openAskIds(): string[];
123
+ /**
124
+ * What this journal knows about one card (#392): `open`, `closed`, or
125
+ * nothing at all — a card asked by a runner from before these lines existed,
126
+ * or one this state directory never saw.
127
+ */
128
+ askState(askId: string): 'open' | 'closed' | undefined;
89
129
  /**
90
130
  * Never reuse a seq the API already stored: after a runner state-dir wipe the
91
131
  * local counter restarts at 1 and every replayed event would collide with an
package/dist/journal.js CHANGED
@@ -21,6 +21,23 @@ export class SessionJournal {
21
21
  lastStatus = null;
22
22
  /** The agent's conversation tip, and the provider session it lives in. */
23
23
  lastAnchor = null;
24
+ /**
25
+ * Every card this journal has published, and whether it is still open —
26
+ * in the order they were asked (#392).
27
+ *
28
+ * The persistent twin of the supervisor's `running.openQuestions`: that set
29
+ * dies with the process, and a process that dies ungracefully never gets to
30
+ * `shutdown()`, which is the only place it withdraws them. This one is
31
+ * rebuilt from the file, so the next process can close what the last one
32
+ * left open — the card in the browser does not know the runner restarted.
33
+ *
34
+ * Closed cards are remembered too, not only dropped: «this card was closed
35
+ * by somebody» is the fact that stops a SECOND tombstone. A queued answer
36
+ * that reaches the next process after a restore has already closed the card
37
+ * must rescue the words and say nothing more about the card — and the only
38
+ * witness that the card was closed is this file.
39
+ */
40
+ asks = new Map();
24
41
  constructor(sessionId, dir = journalDir()) {
25
42
  this.sessionId = sessionId;
26
43
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -50,6 +67,16 @@ export class SessionJournal {
50
67
  });
51
68
  if (parsed.seq >= this.nextSeq)
52
69
  this.nextSeq = parsed.seq + 1;
70
+ // The event lines carry the same fact as `ask_open`/`ask_closed`, and
71
+ // replaying both is harmless: the set is a set.
72
+ this.trackAsk(parsed.eventType, parsed.payload, false);
73
+ }
74
+ else if (parsed.kind === 'ask_open') {
75
+ if (this.asks.get(parsed.askId) !== 'closed')
76
+ this.asks.set(parsed.askId, 'open');
77
+ }
78
+ else if (parsed.kind === 'ask_closed') {
79
+ this.asks.set(parsed.askId, 'closed');
53
80
  }
54
81
  else if (parsed.kind === 'ack') {
55
82
  this.unackedBySeq.delete(parsed.seq);
@@ -107,6 +134,11 @@ export class SessionJournal {
107
134
  if (this.lastAnchor) {
108
135
  lines.push({ kind: 'anchor', ...this.lastAnchor });
109
136
  }
137
+ // AFTER the event lines: an unacked `question` re-written above would
138
+ // otherwise reopen, on the next replay, a card these lines say is closed.
139
+ for (const [askId, state] of this.asks) {
140
+ lines.push({ kind: state === 'open' ? 'ask_open' : 'ask_closed', askId });
141
+ }
110
142
  if (this.lastStatus) {
111
143
  lines.push({
112
144
  kind: 'status',
@@ -182,8 +214,58 @@ export class SessionJournal {
182
214
  const event = { seq: this.nextSeq++, eventType, payload };
183
215
  this.write({ kind: 'event', ...event, ts: new Date().toISOString() });
184
216
  this.unackedBySeq.set(event.seq, event);
217
+ this.trackAsk(eventType, payload, true);
185
218
  return event;
186
219
  }
220
+ /**
221
+ * Keep `openAsks` in step with the cards going out through `append` (#392).
222
+ *
223
+ * Here and not in the supervisor, because `append` is the one door every
224
+ * event takes: a card that was published was published through it, and so
225
+ * was every resolution — the adapter's own, `withdrawOpenQuestions`, the
226
+ * `not_open` miss. A set kept beside the callers would need every one of
227
+ * them to remember it, which is exactly how the in-memory set came to be
228
+ * empty at the moment it was needed.
229
+ *
230
+ * A resolution for a card this journal never published writes nothing:
231
+ * there is nothing to close, and remembering strangers would only grow the
232
+ * file.
233
+ */
234
+ trackAsk(eventType, payload, persist) {
235
+ if (eventType !== 'question' && eventType !== 'question_resolved')
236
+ return;
237
+ const askId = payload['askId'];
238
+ if (typeof askId !== 'string' || !askId)
239
+ return;
240
+ const state = this.asks.get(askId);
241
+ if (eventType === 'question') {
242
+ // A card is asked once; a replayed event line for one already closed
243
+ // (compaction keeps unacked events) must not reopen it.
244
+ if (state !== undefined)
245
+ return;
246
+ this.asks.set(askId, 'open');
247
+ if (persist)
248
+ this.write({ kind: 'ask_open', askId });
249
+ return;
250
+ }
251
+ if (state !== 'open')
252
+ return;
253
+ this.asks.set(askId, 'closed');
254
+ if (persist)
255
+ this.write({ kind: 'ask_closed', askId });
256
+ }
257
+ /** The cards still waiting on a person, oldest first (#392). */
258
+ openAskIds() {
259
+ return [...this.asks].filter(([, state]) => state === 'open').map(([askId]) => askId);
260
+ }
261
+ /**
262
+ * What this journal knows about one card (#392): `open`, `closed`, or
263
+ * nothing at all — a card asked by a runner from before these lines existed,
264
+ * or one this state directory never saw.
265
+ */
266
+ askState(askId) {
267
+ return this.asks.get(askId);
268
+ }
187
269
  /**
188
270
  * Never reuse a seq the API already stored: after a runner state-dir wipe the
189
271
  * local counter restarts at 1 and every replayed event would collide with an
@@ -32,6 +32,20 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
32
32
  * behaviour every runner had before this release.
33
33
  */
34
34
  pausedUntil: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
35
+ /**
36
+ * Subagents the API still believes to be alive in this session (#393).
37
+ *
38
+ * The number is the dead process's last word, kept by the API: a runner
39
+ * restart takes every subagent with it, and the new process seeds its own
40
+ * count at zero — so the descriptor is the only place the number survives
41
+ * long enough to be written down. The API sends it only while it still
42
+ * believes it (its own trust window on the count's age); absent or zero
43
+ * means «nothing to say», and a restore says nothing.
44
+ *
45
+ * `.catch(undefined)` like its neighbours: one malformed value must cost
46
+ * its own session a line at most, never the frame (QA-100 MAJOR-1).
47
+ */
48
+ backgroundTasks: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
35
49
  /**
36
50
  * The newest published version of THIS session's agent — Р13, §4.8.
37
51
  *
@@ -246,6 +260,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
246
260
  } | undefined;
247
261
  workMode?: "DIRECT" | "BRANCH" | undefined;
248
262
  pausedUntil?: string | null | undefined;
263
+ backgroundTasks?: number | undefined;
249
264
  agentLatestVersion?: string | undefined;
250
265
  skipAgentPrompt?: boolean | undefined;
251
266
  branchHint?: string | undefined;
@@ -295,6 +310,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
295
310
  activeMsBase?: number | undefined;
296
311
  extraBudgetMinutes?: number | null | undefined;
297
312
  pausedUntil?: unknown;
313
+ backgroundTasks?: unknown;
298
314
  agentLatestVersion?: unknown;
299
315
  skipAgentPrompt?: unknown;
300
316
  branchHint?: unknown;
@@ -450,6 +466,20 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
450
466
  * behaviour every runner had before this release.
451
467
  */
452
468
  pausedUntil: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
469
+ /**
470
+ * Subagents the API still believes to be alive in this session (#393).
471
+ *
472
+ * The number is the dead process's last word, kept by the API: a runner
473
+ * restart takes every subagent with it, and the new process seeds its own
474
+ * count at zero — so the descriptor is the only place the number survives
475
+ * long enough to be written down. The API sends it only while it still
476
+ * believes it (its own trust window on the count's age); absent or zero
477
+ * means «nothing to say», and a restore says nothing.
478
+ *
479
+ * `.catch(undefined)` like its neighbours: one malformed value must cost
480
+ * its own session a line at most, never the frame (QA-100 MAJOR-1).
481
+ */
482
+ backgroundTasks: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
453
483
  /**
454
484
  * The newest published version of THIS session's agent — Р13, §4.8.
455
485
  *
@@ -664,6 +694,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
664
694
  } | undefined;
665
695
  workMode?: "DIRECT" | "BRANCH" | undefined;
666
696
  pausedUntil?: string | null | undefined;
697
+ backgroundTasks?: number | undefined;
667
698
  agentLatestVersion?: string | undefined;
668
699
  skipAgentPrompt?: boolean | undefined;
669
700
  branchHint?: string | undefined;
@@ -713,6 +744,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
713
744
  activeMsBase?: number | undefined;
714
745
  extraBudgetMinutes?: number | null | undefined;
715
746
  pausedUntil?: unknown;
747
+ backgroundTasks?: unknown;
716
748
  agentLatestVersion?: unknown;
717
749
  skipAgentPrompt?: unknown;
718
750
  branchHint?: unknown;
@@ -759,6 +791,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
759
791
  } | undefined;
760
792
  workMode?: "DIRECT" | "BRANCH" | undefined;
761
793
  pausedUntil?: string | null | undefined;
794
+ backgroundTasks?: number | undefined;
762
795
  agentLatestVersion?: string | undefined;
763
796
  skipAgentPrompt?: boolean | undefined;
764
797
  branchHint?: string | undefined;
@@ -814,6 +847,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
814
847
  activeMsBase?: number | undefined;
815
848
  extraBudgetMinutes?: number | null | undefined;
816
849
  pausedUntil?: unknown;
850
+ backgroundTasks?: unknown;
817
851
  agentLatestVersion?: unknown;
818
852
  skipAgentPrompt?: unknown;
819
853
  branchHint?: unknown;
@@ -883,6 +917,20 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
883
917
  * behaviour every runner had before this release.
884
918
  */
885
919
  pausedUntil: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
920
+ /**
921
+ * Subagents the API still believes to be alive in this session (#393).
922
+ *
923
+ * The number is the dead process's last word, kept by the API: a runner
924
+ * restart takes every subagent with it, and the new process seeds its own
925
+ * count at zero — so the descriptor is the only place the number survives
926
+ * long enough to be written down. The API sends it only while it still
927
+ * believes it (its own trust window on the count's age); absent or zero
928
+ * means «nothing to say», and a restore says nothing.
929
+ *
930
+ * `.catch(undefined)` like its neighbours: one malformed value must cost
931
+ * its own session a line at most, never the frame (QA-100 MAJOR-1).
932
+ */
933
+ backgroundTasks: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
886
934
  /**
887
935
  * The newest published version of THIS session's agent — Р13, §4.8.
888
936
  *
@@ -1097,6 +1145,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1097
1145
  } | undefined;
1098
1146
  workMode?: "DIRECT" | "BRANCH" | undefined;
1099
1147
  pausedUntil?: string | null | undefined;
1148
+ backgroundTasks?: number | undefined;
1100
1149
  agentLatestVersion?: string | undefined;
1101
1150
  skipAgentPrompt?: boolean | undefined;
1102
1151
  branchHint?: string | undefined;
@@ -1146,6 +1195,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1146
1195
  activeMsBase?: number | undefined;
1147
1196
  extraBudgetMinutes?: number | null | undefined;
1148
1197
  pausedUntil?: unknown;
1198
+ backgroundTasks?: unknown;
1149
1199
  agentLatestVersion?: unknown;
1150
1200
  skipAgentPrompt?: unknown;
1151
1201
  branchHint?: unknown;
@@ -1192,6 +1242,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1192
1242
  } | undefined;
1193
1243
  workMode?: "DIRECT" | "BRANCH" | undefined;
1194
1244
  pausedUntil?: string | null | undefined;
1245
+ backgroundTasks?: number | undefined;
1195
1246
  agentLatestVersion?: string | undefined;
1196
1247
  skipAgentPrompt?: boolean | undefined;
1197
1248
  branchHint?: string | undefined;
@@ -1244,6 +1295,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1244
1295
  activeMsBase?: number | undefined;
1245
1296
  extraBudgetMinutes?: number | null | undefined;
1246
1297
  pausedUntil?: unknown;
1298
+ backgroundTasks?: unknown;
1247
1299
  agentLatestVersion?: unknown;
1248
1300
  skipAgentPrompt?: unknown;
1249
1301
  branchHint?: unknown;
package/dist/protocol.js CHANGED
@@ -59,6 +59,20 @@ export const SessionDescriptorSchema = z.object({
59
59
  * behaviour every runner had before this release.
60
60
  */
61
61
  pausedUntil: z.string().max(40).nullable().optional().catch(undefined),
62
+ /**
63
+ * Subagents the API still believes to be alive in this session (#393).
64
+ *
65
+ * The number is the dead process's last word, kept by the API: a runner
66
+ * restart takes every subagent with it, and the new process seeds its own
67
+ * count at zero — so the descriptor is the only place the number survives
68
+ * long enough to be written down. The API sends it only while it still
69
+ * believes it (its own trust window on the count's age); absent or zero
70
+ * means «nothing to say», and a restore says nothing.
71
+ *
72
+ * `.catch(undefined)` like its neighbours: one malformed value must cost
73
+ * its own session a line at most, never the frame (QA-100 MAJOR-1).
74
+ */
75
+ backgroundTasks: z.number().int().min(0).optional().catch(undefined),
62
76
  /**
63
77
  * The newest published version of THIS session's agent — Р13, §4.8.
64
78
  *