@loicngr/kobo 1.11.36 → 1.11.38

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.
Files changed (112) hide show
  1. package/AGENTS.md +18 -1
  2. package/CHANGELOG.md +8 -0
  3. package/dist/server/index.js +20 -24
  4. package/dist/server/routes/dev-server.js +2 -1
  5. package/dist/server/routes/pull-requests.js +5 -4
  6. package/dist/server/routes/workspaces.js +161 -109
  7. package/dist/server/services/agent/engines/claude-code/engine.js +11 -0
  8. package/dist/server/services/agent/engines/codex/client.js +1 -0
  9. package/dist/server/services/agent/engines/codex/engine.js +47 -10
  10. package/dist/server/services/agent/engines/codex/event-mapper.js +8 -2
  11. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +21 -8
  12. package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +30 -3
  13. package/dist/server/services/agent/event-router.js +1 -1
  14. package/dist/server/services/agent/orchestrator.js +219 -78
  15. package/dist/server/services/agent/session-controller.js +12 -2
  16. package/dist/server/services/auto-loop-service.js +60 -6
  17. package/dist/server/services/change-source-branch-service.js +36 -4
  18. package/dist/server/services/cron-service.js +19 -0
  19. package/dist/server/services/db-backup-service.js +30 -0
  20. package/dist/server/services/dev-server-service.js +167 -38
  21. package/dist/server/services/image-service.js +25 -28
  22. package/dist/server/services/pr-checkout-service.js +34 -5
  23. package/dist/server/services/pr-watcher-service.js +5 -3
  24. package/dist/server/services/quota-backoff-service.js +25 -8
  25. package/dist/server/services/wakeup-service.js +23 -3
  26. package/dist/server/services/websocket-service.js +10 -2
  27. package/dist/server/services/workspace-service.js +8 -4
  28. package/dist/server/services/worktree-purge-service.js +3 -7
  29. package/dist/server/utils/agent-stop-result.js +12 -0
  30. package/dist/server/utils/git-ops.js +8 -1
  31. package/dist/server/utils/git-worktree-fingerprint.js +86 -0
  32. package/dist/server/utils/safe-path.js +21 -0
  33. package/dist/server/utils/script-runner.js +32 -9
  34. package/dist/server/utils/workspace-lifecycle-guard.js +27 -0
  35. package/package.json +1 -1
  36. package/src/client/dist/pwa/assets/ActivityFeed-BzzvcOmq.js +8 -0
  37. package/src/client/dist/pwa/assets/{ActivityFeed-Bakgvay6.css → ActivityFeed-Chsw1Mh7.css} +1 -1
  38. package/src/client/dist/pwa/assets/{ChangelogPage-BFGYQ6eY.js → ChangelogPage-D8563008.js} +1 -1
  39. package/src/client/dist/pwa/assets/ComparisonPanel-Bd0X08sg.js +2 -0
  40. package/src/client/dist/pwa/assets/ComparisonPanel-CRRaDs2D.css +1 -0
  41. package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +1 -0
  42. package/src/client/dist/pwa/assets/CreatePage-Bon8qIka.js +2 -0
  43. package/src/client/dist/pwa/assets/DashboardPage-D8g7_r4S.css +1 -0
  44. package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +1 -0
  45. package/src/client/dist/pwa/assets/{DiffViewer-DSlSAIiW.js → DiffViewer-BpsLNxnj.js} +3 -3
  46. package/src/client/dist/pwa/assets/{DocumentsPanel-C1eXTw_Q.js → DocumentsPanel-BJsJ0fNs.js} +1 -1
  47. package/src/client/dist/pwa/assets/{DrawerToggleButton-BxYpUb4y.js → DrawerToggleButton-BVLO5K5d.js} +1 -1
  48. package/src/client/dist/pwa/assets/{GitPanel-DnBWSdVp.js → GitPanel-Cal_wzC0.js} +3 -3
  49. package/src/client/dist/pwa/assets/{HealthPage-rX1TiaP9.js → HealthPage-RFZS7Lao.js} +1 -1
  50. package/src/client/dist/pwa/assets/{MainLayout-Ceuh5uJj.js → MainLayout-vvyiRcql.js} +2 -2
  51. package/src/client/dist/pwa/assets/{QBtnDropdown-K7syiyyH.js → QBtnDropdown-X8oNU5Mb.js} +1 -1
  52. package/src/client/dist/pwa/assets/{QDrawer-DapzE8DH.js → QDrawer-Cij17skX.js} +1 -1
  53. package/src/client/dist/pwa/assets/{QSelect-Bodml89k.js → QSelect-xd-AlGo_.js} +1 -1
  54. package/src/client/dist/pwa/assets/{QTooltip-K-9OdDfq.js → QTooltip-CwcCGSWq.js} +1 -1
  55. package/src/client/dist/pwa/assets/{SchedulePanel-CfumTzAa.js → SchedulePanel-CXRUsz6G.js} +1 -1
  56. package/src/client/dist/pwa/assets/{SearchPage-AhZYS-dS.js → SearchPage-DopbgLxh.js} +1 -1
  57. package/src/client/dist/pwa/assets/{SettingsPage-BoHXWvKn.js → SettingsPage-Jx6TnOzR.js} +1 -1
  58. package/src/client/dist/pwa/assets/{SplitWorkspacePage-3wK11mcu.js → SplitWorkspacePage-D7lGFRKs.js} +1 -1
  59. package/src/client/dist/pwa/assets/{TerminalPanel-CKg0BeNK.js → TerminalPanel-DI8DHb9e.js} +1 -1
  60. package/src/client/dist/pwa/assets/{TouchPan-D6ctyPYf.js → TouchPan-B3nrkCY8.js} +1 -1
  61. package/src/client/dist/pwa/assets/{TourReplayButton-Dn4Dy_kH.js → TourReplayButton-DU1UShMY.js} +1 -1
  62. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-DnoabsfK.js → WorkspaceAttentionLabels-C-yN5IX8.js} +1 -1
  63. package/src/client/dist/pwa/assets/{WorkspacePage-DYQrfeYw.css → WorkspacePage-BAC6u9OP.css} +1 -1
  64. package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +4 -0
  65. package/src/client/dist/pwa/assets/{browser-DHpSUK8-.js → browser-f9MHQjfi.js} +1 -1
  66. package/src/client/dist/pwa/assets/{build-path-tree-DtEjsZlh.js → build-path-tree-BWBzP8os.js} +1 -1
  67. package/src/client/dist/pwa/assets/{de-Dt_m-H83.js → de-QkDLrcGq.js} +1 -1
  68. package/src/client/dist/pwa/assets/{documents-B2dg1wRf.js → documents-C2cLnEBQ.js} +1 -1
  69. package/src/client/dist/pwa/assets/{editor.api-Cd1D35oj.js → editor.api-sw146MWN.js} +1 -1
  70. package/src/client/dist/pwa/assets/{efforts-IB1vPjJQ.js → efforts-C5ZZgCH4.js} +1 -1
  71. package/src/client/dist/pwa/assets/{engineFeatures-DYSyDEJe.js → engineFeatures-CAVMDawq.js} +1 -1
  72. package/src/client/dist/pwa/assets/{es-DFg25DCS.js → es-BgQFKlo8.js} +1 -1
  73. package/src/client/dist/pwa/assets/{expand-template-CNAHiesL.js → expand-template-B6xAlAaM.js} +1 -1
  74. package/src/client/dist/pwa/assets/{format-DidwgbbX.js → format-BwWP6Aay.js} +1 -1
  75. package/src/client/dist/pwa/assets/{formatters-BjzyUHlu.js → formatters-DyWtO-nR.js} +1 -1
  76. package/src/client/dist/pwa/assets/{fr-CQ1slpol.js → fr-DAdLWjNM.js} +1 -1
  77. package/src/client/dist/pwa/assets/{freemarker2-jNGWawVw.js → freemarker2-DZ5Hm8UH.js} +1 -1
  78. package/src/client/dist/pwa/assets/{handlebars-DfSqClrW.js → handlebars-BRUGpTA7.js} +1 -1
  79. package/src/client/dist/pwa/assets/{html-ByLX-n8T.js → html-DNOayNNs.js} +1 -1
  80. package/src/client/dist/pwa/assets/i18n-CBez_vip.js +1 -0
  81. package/src/client/dist/pwa/assets/index-VL3fI9a3.js +20 -0
  82. package/src/client/dist/pwa/assets/{it-w09rfaP7.js → it-Bumaewp1.js} +1 -1
  83. package/src/client/dist/pwa/assets/{javascript-Dja1fHfv.js → javascript-C0tDQU3z.js} +1 -1
  84. package/src/client/dist/pwa/assets/{liquid-D315hW6C.js → liquid-CN46O6OF.js} +1 -1
  85. package/src/client/dist/pwa/assets/{mdx-B5yUwpzv.js → mdx-DM-lrr8Q.js} +1 -1
  86. package/src/client/dist/pwa/assets/{monaco-contributions-NfxrU0r3.js → monaco-contributions-CkN7y-8c.js} +1 -1
  87. package/src/client/dist/pwa/assets/{monaco.contribution-D1hXPkDZ.js → monaco.contribution-2BcAOLjc.js} +2 -2
  88. package/src/client/dist/pwa/assets/{network-auth-CARVtbNe.js → network-auth-KYS4snQH.js} +1 -1
  89. package/src/client/dist/pwa/assets/{notify-theme-CKC3-EAZ.js → notify-theme-a91Tcr6o.js} +1 -1
  90. package/src/client/dist/pwa/assets/{pending-diff-open-BlLrQaW7.js → pending-diff-open-DOtELZfB.js} +1 -1
  91. package/src/client/dist/pwa/assets/{permissionModes-DMzFEQhr.js → permissionModes-C-9Crj-c.js} +1 -1
  92. package/src/client/dist/pwa/assets/{project-color-CFjdIj6W.js → project-color-BRe4oyNx.js} +1 -1
  93. package/src/client/dist/pwa/assets/{python-Dc6mnEip.js → python-BRpJG6sZ.js} +1 -1
  94. package/src/client/dist/pwa/assets/{razor-DArVFBGO.js → razor-CehCmvHl.js} +1 -1
  95. package/src/client/dist/pwa/assets/{render-chat-markdown-BTPAjU2g.js → render-chat-markdown-DlbQLZ02.js} +1 -1
  96. package/src/client/dist/pwa/assets/{typescript-COE1vIyB.js → typescript-Dt_8mVuj.js} +1 -1
  97. package/src/client/dist/pwa/assets/{use-tours-C8x2V1w5.js → use-tours-CSVM_54K.js} +1 -1
  98. package/src/client/dist/pwa/assets/{workspace-templates-DHyA_GEW.js → workspace-templates-GCObcjO6.js} +1 -1
  99. package/src/client/dist/pwa/assets/{xml-eYjr4cGR.js → xml-_gOwdzB4.js} +1 -1
  100. package/src/client/dist/pwa/assets/{yaml-UooOcssF.js → yaml-BxeBqpGf.js} +1 -1
  101. package/src/client/dist/pwa/index.html +1 -1
  102. package/src/client/dist/pwa/sw.js +1 -1
  103. package/src/client/dist/pwa/assets/ActivityFeed-BPx-xhFY.js +0 -8
  104. package/src/client/dist/pwa/assets/ComparisonPanel-C6DfjpZZ.js +0 -2
  105. package/src/client/dist/pwa/assets/ComparisonPanel-x1iuICgd.css +0 -1
  106. package/src/client/dist/pwa/assets/CreatePage-DxOyY65u.css +0 -1
  107. package/src/client/dist/pwa/assets/CreatePage-ypEzkAKv.js +0 -2
  108. package/src/client/dist/pwa/assets/DashboardPage-DsVRjzzy.js +0 -1
  109. package/src/client/dist/pwa/assets/DashboardPage-DsWn5cWl.css +0 -1
  110. package/src/client/dist/pwa/assets/WorkspacePage-BI7CW2yT.js +0 -4
  111. package/src/client/dist/pwa/assets/i18n-CnjXQjxy.js +0 -1
  112. package/src/client/dist/pwa/assets/index-DcwcFDrZ.js +0 -20
@@ -353,6 +353,7 @@ export function createClaudeCodeEngine() {
353
353
  return;
354
354
  console.warn(`[claude-engine] Compaction still reported active ${COMPACTION_STALL_TIMEOUT_MS}ms after it started — resuming the liveness deadline.`);
355
355
  isCompacting = false;
356
+ safeEmit({ kind: 'session:compacting', active: false });
356
357
  reevaluateLivenessPause();
357
358
  if (activeSubagentTaskIds.size > 0 && !subagentStallTimer) {
358
359
  // Resume declined because a subagent is still tracked — but the
@@ -468,6 +469,16 @@ export function createClaudeCodeEngine() {
468
469
  clearCompactionStallTimer();
469
470
  }
470
471
  }
472
+ // Actual foreground output proves work resumed even if the SDK
473
+ // omitted its trailing status/compact_boundary message. Tool results
474
+ // and background progress can drain during compaction, so exclude them.
475
+ if (isCompacting &&
476
+ !('parent_tool_use_id' in msg && msg.parent_tool_use_id != null) &&
477
+ events.some((ev) => ev.kind === 'message:text' || ev.kind === 'message:thinking' || ev.kind === 'tool:call')) {
478
+ isCompacting = false;
479
+ clearCompactionStallTimer();
480
+ safeEmit({ kind: 'session:compacting', active: false });
481
+ }
471
482
  for (const ev of events) {
472
483
  if (ev.kind === 'tool:call')
473
484
  pendingToolCallIds.add(ev.toolCallId);
@@ -15,6 +15,7 @@ export function createAppServerClient(opts) {
15
15
  onNotification: opts.onNotification ?? (() => { }),
16
16
  onServerRequest: opts.onServerRequest ?? (() => { }),
17
17
  onError: opts.onError,
18
+ onDisconnect: opts.onDisconnect,
18
19
  });
19
20
  return {
20
21
  peer,
@@ -11,6 +11,7 @@ import { buildResponseForResolve, handleServerRequest } from './server-requests.
11
11
  import { spawnAppServer } from './spawn.js';
12
12
  /** Long enough for normal tool work, short enough to recover a lost turn event. */
13
13
  export const CODEX_TURN_IDLE_TIMEOUT_MS = 120_000;
14
+ export const CODEX_TOOL_IDLE_TIMEOUT_MS = 30 * 60_000;
14
15
  export const CODEX_GRACEFUL_INTERRUPT_TIMEOUT_MS = 3_000;
15
16
  /** Grace given to a SIGTERM before the child is killed outright. */
16
17
  const CODEX_FORCE_KILL_TIMEOUT_MS = 3_000;
@@ -24,6 +25,35 @@ export const CODEX_SUBAGENT_STALL_TIMEOUT_MS = 10 * 60_000;
24
25
  function wait(ms) {
25
26
  return new Promise((resolve) => setTimeout(resolve, ms));
26
27
  }
28
+ /** A sent signal is only a request; resolve true exclusively on confirmed exit. */
29
+ function signalAndWaitForExit(child, signal, timeoutMs) {
30
+ if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined)
31
+ return Promise.resolve(true);
32
+ return new Promise((resolve, reject) => {
33
+ const cleanup = () => {
34
+ clearTimeout(timer);
35
+ child.removeListener('exit', onExit);
36
+ };
37
+ const onExit = () => {
38
+ cleanup();
39
+ resolve(true);
40
+ };
41
+ const timer = setTimeout(() => {
42
+ cleanup();
43
+ resolve(false);
44
+ }, timeoutMs);
45
+ timer.unref?.();
46
+ // Install before signalling so an immediate exit cannot be missed.
47
+ child.once('exit', onExit);
48
+ try {
49
+ child.kill(signal);
50
+ }
51
+ catch (err) {
52
+ cleanup();
53
+ reject(err);
54
+ }
55
+ });
56
+ }
27
57
  class CodexTurnTimeoutError extends Error {
28
58
  constructor() {
29
59
  super('Codex stopped reporting activity for this turn');
@@ -77,7 +107,17 @@ export function createCodexEngine() {
77
107
  }
78
108
  };
79
109
  const streamingBatcher = createStreamingBatcher(emitDirect);
80
- const safeEmit = (ev) => streamingBatcher.push(ev);
110
+ const pendingToolCalls = new Set();
111
+ const safeEmit = (ev) => {
112
+ if (ev.kind === 'tool:call')
113
+ pendingToolCalls.add(ev.toolCallId);
114
+ if (ev.kind === 'tool:result')
115
+ pendingToolCalls.delete(ev.toolCallId);
116
+ if (ev.kind === 'tool:call' || ev.kind === 'tool:result') {
117
+ turnLiveness.setTimeoutMs(pendingToolCalls.size ? CODEX_TOOL_IDLE_TIMEOUT_MS : CODEX_TURN_IDLE_TIMEOUT_MS);
118
+ }
119
+ streamingBatcher.push(ev);
120
+ };
81
121
  let rejectChildFailure;
82
122
  const childFailurePromise = new Promise((_resolve, reject) => {
83
123
  rejectChildFailure = reject;
@@ -324,6 +364,7 @@ export function createCodexEngine() {
324
364
  autoApprove: (toolName, payload) => isWorkspacePermissionAllowed(options.workspaceId, { engine: 'codex', toolName, payload }),
325
365
  });
326
366
  },
367
+ onDisconnect: rejectChildFailure,
327
368
  onError(err) {
328
369
  console.error('[codex] JSON-RPC transport error:', err);
329
370
  rejectTurnDone(err);
@@ -428,6 +469,7 @@ export function createCodexEngine() {
428
469
  }
429
470
  }
430
471
  pendingByCallId.clear();
472
+ pendingToolCalls.clear();
431
473
  client.close();
432
474
  try {
433
475
  child.kill('SIGTERM');
@@ -515,15 +557,10 @@ export function createCodexEngine() {
515
557
  // stop, the archive, the delete and Kōbō's own shutdown, holding the
516
558
  // worktree and its share of the model quota, with nothing left in
517
559
  // this process tracking it.
518
- if (child.exitCode === null && child.signalCode === null) {
519
- child.kill('SIGTERM');
520
- const exited = await Promise.race([
521
- new Promise((resolve) => child.once('exit', () => resolve(true))),
522
- wait(CODEX_FORCE_KILL_TIMEOUT_MS).then(() => false),
523
- ]);
524
- if (!exited) {
525
- console.warn('[codex] app-server ignored SIGTERM — sending SIGKILL');
526
- child.kill('SIGKILL');
560
+ if (!(await signalAndWaitForExit(child, 'SIGTERM', CODEX_FORCE_KILL_TIMEOUT_MS))) {
561
+ console.warn('[codex] app-server ignored SIGTERM — sending SIGKILL');
562
+ if (!(await signalAndWaitForExit(child, 'SIGKILL', CODEX_FORCE_KILL_TIMEOUT_MS))) {
563
+ throw new Error('Codex app-server did not exit after SIGKILL');
527
564
  }
528
565
  }
529
566
  },
@@ -179,7 +179,7 @@ export function handleItemStarted(item, state) {
179
179
  return events;
180
180
  }
181
181
  if (item.type === 'contextCompaction') {
182
- events.push({ kind: 'session:compacted' });
182
+ events.push({ kind: 'session:compacting', active: true });
183
183
  return events;
184
184
  }
185
185
  return events;
@@ -189,6 +189,8 @@ export function handleItemStarted(item, state) {
189
189
  */
190
190
  export function handleItemCompleted(item, state) {
191
191
  const events = [];
192
+ if (item.type === 'contextCompaction')
193
+ return [{ kind: 'session:compacted' }];
192
194
  if (item.type === 'agentMessage') {
193
195
  const text = item.text ?? '';
194
196
  const messageId = scopedId(state, item.id);
@@ -302,13 +304,17 @@ export function handleItemCompleted(item, state) {
302
304
  }
303
305
  if (item.type === 'plan') {
304
306
  // Codex's `plan` is a markdown blob, not a structured list — parse bullets.
307
+ const toolCallId = scopedId(state, item.id);
305
308
  events.push({
306
309
  kind: 'tool:call',
307
310
  messageId: '',
308
- toolCallId: scopedId(state, item.id),
311
+ toolCallId,
309
312
  name: 'TodoWrite',
310
313
  input: { todos: parseCodexPlanText(item.text) },
311
314
  });
315
+ // The plan has already completed. Close the synthetic update so both the
316
+ // tool card and the engine's idle watchdog see no outstanding operation.
317
+ events.push({ kind: 'tool:result', toolCallId, output: null, isError: false });
312
318
  return events;
313
319
  }
314
320
  if (item.type === 'error') {
@@ -19,6 +19,7 @@ export class JsonRpcTimeoutError extends Error {
19
19
  }
20
20
  export function createJsonRpcPeer(opts) {
21
21
  let nextId = 1;
22
+ let closedError;
22
23
  const pending = new Map();
23
24
  /** Remove a pending slot and clear its deadline in one place. */
24
25
  const takePending = (id) => {
@@ -30,10 +31,21 @@ export function createJsonRpcPeer(opts) {
30
31
  pending.delete(id);
31
32
  return slot;
32
33
  };
34
+ const rejectPending = (err) => {
35
+ if (closedError)
36
+ return;
37
+ closedError = err;
38
+ for (const id of pending.keys())
39
+ takePending(id)?.reject(err);
40
+ };
33
41
  const transport = createJsonRpcTransport({
34
42
  stdin: opts.stdin,
35
43
  stdout: opts.stdout,
36
44
  onError: opts.onError ?? (() => { }),
45
+ onDisconnect(err) {
46
+ rejectPending(err);
47
+ opts.onDisconnect?.(err);
48
+ },
37
49
  onMessage(msg) {
38
50
  // Response to one of our requests
39
51
  if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
@@ -61,6 +73,8 @@ export function createJsonRpcPeer(opts) {
61
73
  });
62
74
  return {
63
75
  request(method, params, timeoutMs) {
76
+ if (closedError)
77
+ return Promise.reject(closedError);
64
78
  const id = nextId++;
65
79
  const deadlineMs = timeoutMs ?? opts.defaultRequestTimeoutMs ?? DEFAULT_JSONRPC_REQUEST_TIMEOUT_MS;
66
80
  return new Promise((resolve, reject) => {
@@ -73,7 +87,12 @@ export function createJsonRpcPeer(opts) {
73
87
  slot.timer.unref?.();
74
88
  }
75
89
  pending.set(id, slot);
76
- transport.send({ jsonrpc: '2.0', id, method, params });
90
+ try {
91
+ transport.send({ jsonrpc: '2.0', id, method, params });
92
+ }
93
+ catch (err) {
94
+ takePending(id)?.reject(err instanceof Error ? err : new Error(String(err)));
95
+ }
77
96
  });
78
97
  },
79
98
  notify(method, params) {
@@ -86,14 +105,8 @@ export function createJsonRpcPeer(opts) {
86
105
  transport.send({ jsonrpc: '2.0', id, error: { code, message } });
87
106
  },
88
107
  close() {
108
+ rejectPending(new Error('peer closed'));
89
109
  transport.close();
90
- const err = new Error('peer closed');
91
- for (const slot of pending.values()) {
92
- if (slot.timer)
93
- clearTimeout(slot.timer);
94
- slot.reject(err);
95
- }
96
- pending.clear();
97
110
  },
98
111
  };
99
112
  }
@@ -1,5 +1,17 @@
1
1
  export function createJsonRpcTransport(opts) {
2
2
  let buffer = '';
3
+ let closed = false;
4
+ const fail = (err) => {
5
+ if (closed)
6
+ return;
7
+ closed = true;
8
+ opts.onDisconnect?.(err);
9
+ opts.onError(err);
10
+ };
11
+ // Keep listeners installed through teardown: late pipe errors must stay handled.
12
+ opts.stdin.on('error', fail);
13
+ opts.stdout.on('error', fail);
14
+ opts.stdout.on('end', () => fail(new Error('JSON-RPC transport closed')));
3
15
  opts.stdout.setEncoding?.('utf-8');
4
16
  opts.stdout.on('data', (chunk) => {
5
17
  buffer += chunk;
@@ -19,13 +31,28 @@ export function createJsonRpcTransport(opts) {
19
31
  }
20
32
  }
21
33
  });
22
- opts.stdout.on('error', opts.onError);
23
34
  return {
24
35
  send(msg) {
25
- opts.stdin.write(`${JSON.stringify(msg)}\n`);
36
+ if (closed)
37
+ throw new Error('JSON-RPC transport closed');
38
+ try {
39
+ opts.stdin.write(`${JSON.stringify(msg)}\n`);
40
+ }
41
+ catch (err) {
42
+ fail(err instanceof Error ? err : new Error(String(err)));
43
+ throw err;
44
+ }
26
45
  },
27
46
  close() {
28
- opts.stdin.end();
47
+ if (closed)
48
+ return;
49
+ closed = true;
50
+ try {
51
+ opts.stdin.end();
52
+ }
53
+ catch {
54
+ /* Already disconnected. */
55
+ }
29
56
  },
30
57
  };
31
58
  }
@@ -3,7 +3,7 @@ export function routeEvent(workspaceId, agentSessionId, event) {
3
3
  // The compaction "in progress" signal is a transient live indicator — deliver
4
4
  // it once and never persist/replay it. The persistent trace is `session:compacted`.
5
5
  if (event.kind === 'session:compacting') {
6
- emitEphemeral(workspaceId, 'agent:event', event);
6
+ emitEphemeral(workspaceId, 'agent:event', event, agentSessionId);
7
7
  return;
8
8
  }
9
9
  if (event.kind === 'mcp:status' && event.status !== 'error') {