@bridge4dev/runner 0.33.0 → 0.34.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.
@@ -1800,11 +1800,39 @@ class ClaudeSession {
1800
1800
  break;
1801
1801
  }
1802
1802
  case 'assistant': {
1803
+ // Ticket #186. A subagent's own prose is NOT this session's answer.
1804
+ //
1805
+ // The SDK's own note — «by default, only tool_use/tool_result blocks
1806
+ // from subagents are emitted» — covers SYNCHRONOUS Task subagents. A
1807
+ // BACKGROUND agent's closing report is delivered regardless, as an
1808
+ // ordinary top-level assistant message that merely carries
1809
+ // `parent_tool_use_id` (probe against 0.3.226, 2026-08-08):
1810
+ //
1811
+ // {type:'assistant', parent_tool_use_id:'toolu_…',
1812
+ // subagent_type:'general-purpose', task_description:'…',
1813
+ // message:{content:[{type:'text', text:'…'}]}}
1814
+ //
1815
+ // Session 50eb77b9 put two of those — 46 655 and 16 368 characters of
1816
+ // internal survey — into the feed four seconds after an
1817
+ // AskUserQuestion card, while the main agent was blocked on the very
1818
+ // tool call that card belongs to and could not have written a word.
1819
+ //
1820
+ // Only text and thinking are dropped. `tool_use` stays, and so does
1821
+ // the paired `tool_result` in `case 'user'` below: the dashboard
1822
+ // resolves a result by toolUseId and falls back to the last unfinished
1823
+ // row, so filtering one arm without the other sticks a subagent's
1824
+ // result onto a stranger's row (that fallback has misfired before).
1825
+ //
1826
+ // The test is «is a string», not «is not null»: an emitter that
1827
+ // omits the field must read as the main loop. Treating a missing
1828
+ // field as a subagent would blank the transcript of every session on
1829
+ // that CLI, which is a far worse failure than one leaked report.
1830
+ const fromSubagent = typeof msg.parent_tool_use_id === 'string';
1803
1831
  for (const block of msg.message.content) {
1804
- if (block.type === 'text' && block.text.trim()) {
1832
+ if (block.type === 'text' && !fromSubagent && block.text.trim()) {
1805
1833
  this.emit({ type: 'message', role: 'assistant', text: truncate(block.text) });
1806
1834
  }
1807
- else if (block.type === 'thinking' && block.thinking.trim()) {
1835
+ else if (block.type === 'thinking' && !fromSubagent && block.thinking.trim()) {
1808
1836
  this.emit({ type: 'thinking', text: truncate(block.thinking, 8_000) });
1809
1837
  }
1810
1838
  else if (block.type === 'tool_use') {
@@ -246,6 +246,30 @@ export declare class Supervisor {
246
246
  * rewound (ticket #126).
247
247
  */
248
248
  private park;
249
+ /**
250
+ * The agent is producing output while this runner still says a human is
251
+ * expected — put `lastReported` back to RUNNING (ticket #185).
252
+ *
253
+ * This is not cosmetics on top of the API's own repair. While `lastReported`
254
+ * lies, two things break here and only here:
255
+ *
256
+ * - `isParkable()` reads WAITING_INPUT with no open question as «idle», so
257
+ * `ensureCapacity` may KILL a live agent process to hand its slot to
258
+ * another session;
259
+ * - `isBillable()` counts only RUNNING/STARTING, so `agentActiveMs` stops
260
+ * growing and the time budget never fires for work that is really happening.
261
+ *
262
+ * It happens for real: a background subagent's report wakes a new turn inside
263
+ * the agent process (SDK 0.3.226 emits a second `system:init` and a `result`
264
+ * carrying `origin: {kind:'task-notification'}`), and nothing here reports a
265
+ * turn the runner did not start. Compaction does the same, and so does a
266
+ * question this runner withdraws by itself.
267
+ *
268
+ * An open question is the one thing that must survive: there, WAITING_INPUT
269
+ * means a tool call is parked on a person, and the agent narrating around its
270
+ * own question must not take the card off the screen.
271
+ */
272
+ private noteAgentIsWorking;
249
273
  private forwardEvent;
250
274
  /**
251
275
  * The dashboard's answer to a parked question (session 12).
@@ -22,6 +22,12 @@ import { availableModes, MODE_REFUSED_TEXT } from './adapters/types.js';
22
22
  /** Refusals shared by every checkpoint command (ticket #126). */
23
23
  const CHECKPOINTS_OFF = 'Restore points are switched off on this server ([checkpoints] enabled = false)';
24
24
  const AGENT_BUSY = 'The agent is still working — stop the turn first';
25
+ /**
26
+ * Adapter events that exist only because the agent is producing output right
27
+ * now (ticket #185). `message` is deliberately absent — a user echo carries the
28
+ * same type and proves nothing about the agent — and is handled at the call site.
29
+ */
30
+ const AGENT_OUTPUT_EVENTS = new Set(['tool', 'thinking']);
25
31
  /**
26
32
  * The project's git policy, lifted out of a session descriptor (session 18).
27
33
  *
@@ -1046,8 +1052,47 @@ export class Supervisor {
1046
1052
  }
1047
1053
  running.session.stop('session_parked');
1048
1054
  }
1055
+ /**
1056
+ * The agent is producing output while this runner still says a human is
1057
+ * expected — put `lastReported` back to RUNNING (ticket #185).
1058
+ *
1059
+ * This is not cosmetics on top of the API's own repair. While `lastReported`
1060
+ * lies, two things break here and only here:
1061
+ *
1062
+ * - `isParkable()` reads WAITING_INPUT with no open question as «idle», so
1063
+ * `ensureCapacity` may KILL a live agent process to hand its slot to
1064
+ * another session;
1065
+ * - `isBillable()` counts only RUNNING/STARTING, so `agentActiveMs` stops
1066
+ * growing and the time budget never fires for work that is really happening.
1067
+ *
1068
+ * It happens for real: a background subagent's report wakes a new turn inside
1069
+ * the agent process (SDK 0.3.226 emits a second `system:init` and a `result`
1070
+ * carrying `origin: {kind:'task-notification'}`), and nothing here reports a
1071
+ * turn the runner did not start. Compaction does the same, and so does a
1072
+ * question this runner withdraws by itself.
1073
+ *
1074
+ * An open question is the one thing that must survive: there, WAITING_INPUT
1075
+ * means a tool call is parked on a person, and the agent narrating around its
1076
+ * own question must not take the card off the screen.
1077
+ */
1078
+ noteAgentIsWorking(running) {
1079
+ if (running.openQuestions.size > 0)
1080
+ return;
1081
+ if (running.stopRequested || running.parkRequested || running.budgetSpent)
1082
+ return;
1083
+ if (running.lastReported !== 'WAITING_INPUT' && running.lastReported !== 'REVIEW')
1084
+ return;
1085
+ running.lastReported = 'RUNNING';
1086
+ this.syncBudgetClock(running);
1087
+ this.reportStatus(running.descriptor.id, 'RUNNING', {});
1088
+ }
1049
1089
  forwardEvent(running, event) {
1050
1090
  const { descriptor } = running;
1091
+ // Anything below that is the agent talking means the agent is working. Read
1092
+ // before the switch so every such case gets it, including the ones added
1093
+ // after this line was written.
1094
+ if (event.type === 'message' ? event.role === 'assistant' : AGENT_OUTPUT_EVENTS.has(event.type))
1095
+ this.noteAgentIsWorking(running);
1051
1096
  switch (event.type) {
1052
1097
  case 'provider_session': {
1053
1098
  running.descriptor = { ...descriptor, providerSessionId: event.providerSessionId };
@@ -2431,6 +2476,21 @@ export class Supervisor {
2431
2476
  error: 'The agent could not compact right now — wait for the current turn to finish',
2432
2477
  });
2433
2478
  }
2479
+ // Ticket #185. Compaction is a whole agent turn — `/compact` goes in
2480
+ // as an ordinary user message — and until now nobody said so: the
2481
+ // session read «Ваш ход» for the entire squeeze, and the dashboard
2482
+ // papered over it with `compactingSince`, a variable that exists only
2483
+ // in the tab that pressed the button. A second tab, a teammate or a
2484
+ // reload saw a session waiting for a person who had nothing to do.
2485
+ //
2486
+ // It is also not only cosmetic here: a session reading WAITING_INPUT
2487
+ // with no open question is parkable, and parking it would kill the
2488
+ // compaction mid-flight.
2489
+ if (running.lastReported !== 'RUNNING') {
2490
+ running.lastReported = 'RUNNING';
2491
+ this.syncBudgetClock(running);
2492
+ this.reportStatus(running.descriptor.id, 'RUNNING', {});
2493
+ }
2434
2494
  return void reply({ ok: true, result: { started: true } });
2435
2495
  }
2436
2496
  case 'git_status': {
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const RUNNER_VERSION = "0.33.0";
1
+ export declare const RUNNER_VERSION = "0.34.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in sync with package.json by the release script (manual for now).
2
- export const RUNNER_VERSION = '0.33.0';
2
+ export const RUNNER_VERSION = '0.34.0';
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge4dev/runner",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "DevBridge dev runner — connects a dev server to DevBridge and runs agent sessions (Claude Code / Codex)",
5
5
  "homepage": "https://bridge4.dev",
6
6
  "license": "MIT",
@@ -37,16 +37,16 @@
37
37
  "prepublishOnly": "tsc"
38
38
  },
39
39
  "dependencies": {
40
- "@anthropic-ai/claude-agent-sdk": "^0.3.218",
41
- "smol-toml": "^1.7.0",
42
- "ws": "^8.21.1",
43
- "zod": "^3.24.0"
40
+ "@anthropic-ai/claude-agent-sdk": "^0.3.226",
41
+ "smol-toml": "^1.7.1",
42
+ "ws": "^8.21.3",
43
+ "zod": "^3.25.76"
44
44
  },
45
45
  "devDependencies": {
46
- "@types/node": "^22.10.2",
46
+ "@types/node": "^22.20.1",
47
47
  "@types/ws": "^8.18.1",
48
- "tsx": "^4.19.2",
49
- "typescript": "~5.7.2",
50
- "vitest": "^2.1.9"
48
+ "tsx": "^4.23.11",
49
+ "typescript": "~5.7.3",
50
+ "vitest": "^3.2.7"
51
51
  }
52
52
  }