@loicngr/kobo 2.1.4 → 2.1.6

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 (173) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CONFIGURATION.md +11 -3
  3. package/dist/server/index.js +24 -91
  4. package/dist/server/routes/git.js +6 -7
  5. package/dist/server/routes/settings.js +21 -11
  6. package/dist/server/routes/workspaces.js +84 -74
  7. package/dist/server/services/agent/engines/claude-code/event-mapper.js +33 -0
  8. package/dist/server/services/agent/engines/codex/engine.js +10 -1
  9. package/dist/server/services/agent/engines/codex/event-mapper.js +2 -0
  10. package/dist/server/services/agent/integration-mcp.js +4 -3
  11. package/dist/server/services/agent/orchestrator.js +1 -1
  12. package/dist/server/services/agent/streaming-batcher.js +14 -3
  13. package/dist/server/services/engine-handoff-service.js +120 -24
  14. package/dist/server/services/pr-checkout-service.js +90 -53
  15. package/dist/server/services/pr-watcher-service.js +13 -7
  16. package/dist/server/services/retention/worker.js +40 -0
  17. package/dist/server/services/review-service.js +8 -4
  18. package/dist/server/services/session-event-metrics.js +43 -0
  19. package/dist/server/services/session-handoff-service.js +6 -4
  20. package/dist/server/services/templates-service.js +67 -49
  21. package/dist/server/services/terminal-service.js +45 -14
  22. package/dist/server/services/terminal-websocket-service.js +143 -0
  23. package/dist/server/services/wakeup-service.js +23 -21
  24. package/dist/server/services/workspace-service.js +3 -44
  25. package/dist/server/services/workspace-template-service.js +16 -18
  26. package/dist/server/services/worktree-purge-service.js +1 -4
  27. package/dist/server/services/ws-events-retention-service.js +197 -104
  28. package/dist/server/services/ws-events-retention-worker-service.js +112 -0
  29. package/dist/server/utils/atomic-json-file.js +23 -0
  30. package/dist/server/utils/git-ops.js +262 -60
  31. package/dist/shared/codex-models.js +20 -3
  32. package/dist/shared/models.js +12 -0
  33. package/package.json +6 -6
  34. package/src/client/dist/pwa/assets/ActivityFeed-BO1mKMJ0.js +1 -0
  35. package/src/client/dist/pwa/assets/ActivityFeed-ZTxXVmNM.css +1 -0
  36. package/src/client/dist/pwa/assets/ChangelogPage-BXNz7PGb.js +1 -0
  37. package/src/client/dist/pwa/assets/ChangelogPage-DVyx9ZjV.css +1 -0
  38. package/src/client/dist/pwa/assets/ComparisonPanel-ByVbqYCs.css +1 -0
  39. package/src/client/dist/pwa/assets/ComparisonPanel-CfmJHeH_.js +2 -0
  40. package/src/client/dist/pwa/assets/CreatePage-6_AP5zso.js +2 -0
  41. package/src/client/dist/pwa/assets/CreatePage-CMyl2j8Z.css +1 -0
  42. package/src/client/dist/pwa/assets/DashboardPage-BKCrV6u0.js +1 -0
  43. package/src/client/dist/pwa/assets/DashboardPage-CtaHaVdf.css +1 -0
  44. package/src/client/dist/pwa/assets/DiffViewer-BMzrb9fB.js +8 -0
  45. package/src/client/dist/pwa/assets/DocumentsPanel-CqGg38Oi.js +1 -0
  46. package/src/client/dist/pwa/assets/{DrawerToggleButton-Bh4FXdtJ.js → DrawerToggleButton-DKytIBlk.js} +1 -1
  47. package/src/client/dist/pwa/assets/{GitPanel-C4wOcZV2.css → GitPanel-C3blk_Pd.css} +1 -1
  48. package/src/client/dist/pwa/assets/GitPanel-DRzhrs_V.js +3 -0
  49. package/src/client/dist/pwa/assets/HealthPage-DT8FGrKA.css +1 -0
  50. package/src/client/dist/pwa/assets/HealthPage-DWotmfkh.js +1 -0
  51. package/src/client/dist/pwa/assets/MainLayout-DkSavoiV.css +1 -0
  52. package/src/client/dist/pwa/assets/MainLayout-fRVZ01LN.js +2 -0
  53. package/src/client/dist/pwa/assets/QBanner-mzrnn82R.js +1 -0
  54. package/src/client/dist/pwa/assets/{QBtnDropdown-DVbxGTNb.js → QBtnDropdown-BGGvQIvH.js} +1 -1
  55. package/src/client/dist/pwa/assets/QChip-Chu7r6RZ.js +1 -0
  56. package/src/client/dist/pwa/assets/QExpansionItem-C2XXHOHk.js +1 -0
  57. package/src/client/dist/pwa/assets/QItemSection-CR3Kfibl.js +1 -0
  58. package/src/client/dist/pwa/assets/QList-DnezW6gW.js +1 -0
  59. package/src/client/dist/pwa/assets/QMarkupTable-wuDf50Oh.js +1 -0
  60. package/src/client/dist/pwa/assets/QScrollArea-BnmQ9US7.js +1 -0
  61. package/src/client/dist/pwa/assets/QSelect-BskTzVCM.js +1 -0
  62. package/src/client/dist/pwa/assets/{QTooltip-BLhkNT4s.js → QTooltip-CO84H0cq.js} +1 -1
  63. package/src/client/dist/pwa/assets/SchedulePanel-CBM_GpS3.css +1 -0
  64. package/src/client/dist/pwa/assets/SchedulePanel-CPirTYw5.js +1 -0
  65. package/src/client/dist/pwa/assets/SearchPage-CnT0-Hpe.js +1 -0
  66. package/src/client/dist/pwa/assets/{SettingsPage-BYdLXJEJ.css → SettingsPage-CpykRKq2.css} +1 -1
  67. package/src/client/dist/pwa/assets/SettingsPage-DCMZL7GO.js +9 -0
  68. package/src/client/dist/pwa/assets/SplitWorkspacePage-DwWsD7f5.js +1 -0
  69. package/src/client/dist/pwa/assets/{TerminalPanel-D2j5AA8S.js → TerminalPanel-C4dYabx6.js} +2 -2
  70. package/src/client/dist/pwa/assets/{TerminalPanel-DbVZk0vD.css → TerminalPanel-Dz9EE-Ph.css} +1 -1
  71. package/src/client/dist/pwa/assets/ToolCallItem-CjXWiKJc.js +8 -0
  72. package/src/client/dist/pwa/assets/ToolCallItem-DhjdG8vp.css +1 -0
  73. package/src/client/dist/pwa/assets/{TouchPan-C0EqFcrr.js → TouchPan-6fI8xFPq.js} +1 -1
  74. package/src/client/dist/pwa/assets/{TourReplayButton-DlMvPblb.js → TourReplayButton-DhbNI6O5.js} +1 -1
  75. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CZxW7QSq.js +1 -0
  76. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-DSw8Upcp.css +1 -0
  77. package/src/client/dist/pwa/assets/WorkspacePage-B0wCQdev.js +4 -0
  78. package/src/client/dist/pwa/assets/{WorkspacePage--5HsnMQ1.css → WorkspacePage-BnW-gp3u.css} +1 -1
  79. package/src/client/dist/pwa/assets/{browser-DNEbLF-4.js → browser-CTkGHrN5.js} +1 -1
  80. package/src/client/dist/pwa/assets/build-path-tree-Bd50EZMW.js +1 -0
  81. package/src/client/dist/pwa/assets/custom-sounds-DoPDmcY5.js +1 -0
  82. package/src/client/dist/pwa/assets/de-BPtzBQu0.js +17 -0
  83. package/src/client/dist/pwa/assets/defineProperty-BbfpZ9Tg.js +1 -0
  84. package/src/client/dist/pwa/assets/{editor.api-PENN0io6.js → editor.api-CKcTKXcM.js} +1 -1
  85. package/src/client/dist/pwa/assets/efforts-CDsBbk8o.js +9 -0
  86. package/src/client/dist/pwa/assets/es-BoNOfG0w.js +17 -0
  87. package/src/client/dist/pwa/assets/expand-template-D6zFPzzB.js +1 -0
  88. package/src/client/dist/pwa/assets/{format-CVI7mr7G.js → format-MtzcnR3z.js} +7 -7
  89. package/src/client/dist/pwa/assets/formatters-DCjBZ47F.js +1 -0
  90. package/src/client/dist/pwa/assets/fr-Bu92fK5U.js +17 -0
  91. package/src/client/dist/pwa/assets/{freemarker2-BxN_Aq6C.js → freemarker2-JGUTjYvR.js} +1 -1
  92. package/src/client/dist/pwa/assets/{handlebars-C9UHXtO1.js → handlebars-BWStl2Hc.js} +1 -1
  93. package/src/client/dist/pwa/assets/{html-CTH9wSJr.js → html-CjGCxUEe.js} +1 -1
  94. package/src/client/dist/pwa/assets/i18n-DKj-umtn.js +19 -0
  95. package/src/client/dist/pwa/assets/i18n-DtmbeJu8.js +1 -0
  96. package/src/client/dist/pwa/assets/index-BPmUmc6D.js +2 -0
  97. package/src/client/dist/pwa/assets/it-CrA4Q7oc.js +17 -0
  98. package/src/client/dist/pwa/assets/{javascript-WT4BS8p4.js → javascript-C_jd0j2i.js} +1 -1
  99. package/src/client/dist/pwa/assets/latest-request-CBPb8UWQ.js +1 -0
  100. package/src/client/dist/pwa/assets/{liquid-D6dfe-gU.js → liquid-DMyQVGkp.js} +1 -1
  101. package/src/client/dist/pwa/assets/{mdx-B8pNXNeC.js → mdx-DeW5qzOU.js} +1 -1
  102. package/src/client/dist/pwa/assets/{monaco-contributions-COLfslyi.js → monaco-contributions-Be_o6s3m.js} +1 -1
  103. package/src/client/dist/pwa/assets/monaco.contribution-VUGNbVME.js +2 -0
  104. package/src/client/dist/pwa/assets/network-auth-CSx0jS5X.js +1 -0
  105. package/src/client/dist/pwa/assets/{notify-theme-7MsG-MyN.js → notify-theme-uhSG2r09.js} +1 -1
  106. package/src/client/dist/pwa/assets/{permissionModes-B-ciuUbz.js → permissionModes-Cc5VaOdC.js} +1 -1
  107. package/src/client/dist/pwa/assets/preload-helper-DjzSQ1t3.js +1 -0
  108. package/src/client/dist/pwa/assets/{project-color-Bo6iEcqN.js → project-color-BQbOnYBN.js} +1 -1
  109. package/src/client/dist/pwa/assets/{python-D2gb6Jjz.js → python-C1v3VovI.js} +1 -1
  110. package/src/client/dist/pwa/assets/{razor-dksrCQF2.js → razor-DMEmKSdO.js} +1 -1
  111. package/src/client/dist/pwa/assets/{render-chat-markdown-px_6WDIX.js → render-chat-markdown-CafzCcmu.js} +1 -1
  112. package/src/client/dist/pwa/assets/{typescript-DTPyRTrW.js → typescript-rKyiSQID.js} +1 -1
  113. package/src/client/dist/pwa/assets/use-checkbox-BqDMQ6x7.js +1 -0
  114. package/src/client/dist/pwa/assets/use-search-index-status-BL3cb04x.js +1 -0
  115. package/src/client/dist/pwa/assets/{use-tours-b00RruX_.js → use-tours-Dum-bOyQ.js} +2 -2
  116. package/src/client/dist/pwa/assets/use-worktree-restore-BQyoz3Qa.js +1 -0
  117. package/src/client/dist/pwa/assets/workspace-templates-YRwFjkUh.js +1 -0
  118. package/src/client/dist/pwa/assets/{xml-mxUZCM4r.js → xml-U_pDcywM.js} +1 -1
  119. package/src/client/dist/pwa/assets/{yaml-BuN8sUWC.js → yaml-CRCd_ET9.js} +1 -1
  120. package/src/client/dist/pwa/index.html +6 -3
  121. package/src/client/dist/pwa/sw.js +1 -1
  122. package/src/client/dist/pwa/assets/ActivityFeed-DCmiJP-z.css +0 -1
  123. package/src/client/dist/pwa/assets/ActivityFeed-DdGARu8o.js +0 -8
  124. package/src/client/dist/pwa/assets/ChangelogPage-CGenjiI5.js +0 -1
  125. package/src/client/dist/pwa/assets/ChangelogPage-DKJkFiU_.css +0 -1
  126. package/src/client/dist/pwa/assets/ComparisonPanel-CRRaDs2D.css +0 -1
  127. package/src/client/dist/pwa/assets/ComparisonPanel-aXgjEulK.js +0 -2
  128. package/src/client/dist/pwa/assets/CreatePage-B9C9J9Pw.css +0 -1
  129. package/src/client/dist/pwa/assets/CreatePage-BaB_30xJ.js +0 -2
  130. package/src/client/dist/pwa/assets/DashboardPage-DsbwmO2W.js +0 -1
  131. package/src/client/dist/pwa/assets/DashboardPage-XiCj24i2.css +0 -1
  132. package/src/client/dist/pwa/assets/DiffViewer--dUMf7b3.js +0 -8
  133. package/src/client/dist/pwa/assets/DocumentsPanel-DTjEUYUd.js +0 -1
  134. package/src/client/dist/pwa/assets/GitPanel-PUPLj0M8.js +0 -3
  135. package/src/client/dist/pwa/assets/HealthPage-BHgucifz.js +0 -1
  136. package/src/client/dist/pwa/assets/HealthPage-CFZ-tdKK.css +0 -1
  137. package/src/client/dist/pwa/assets/MainLayout-CNmwCnDD.css +0 -1
  138. package/src/client/dist/pwa/assets/MainLayout-CQVLnTX3.js +0 -2
  139. package/src/client/dist/pwa/assets/QBanner-XE0EMe9L.js +0 -1
  140. package/src/client/dist/pwa/assets/QChip-Bt_JvnHb.js +0 -1
  141. package/src/client/dist/pwa/assets/QExpansionItem-CxEBY3jY.js +0 -1
  142. package/src/client/dist/pwa/assets/QItemSection-Cf6OZEH4.js +0 -1
  143. package/src/client/dist/pwa/assets/QList-Z90BspT4.js +0 -1
  144. package/src/client/dist/pwa/assets/QMarkupTable-Bwjknbmw.js +0 -1
  145. package/src/client/dist/pwa/assets/QSelect-CMzJDhi-.js +0 -1
  146. package/src/client/dist/pwa/assets/SchedulePanel-BYKSFoFj.css +0 -1
  147. package/src/client/dist/pwa/assets/SchedulePanel-DTVwe7sX.js +0 -1
  148. package/src/client/dist/pwa/assets/SearchPage-CtrR7dNr.js +0 -1
  149. package/src/client/dist/pwa/assets/SettingsPage-Bl_RNTGi.js +0 -9
  150. package/src/client/dist/pwa/assets/SplitWorkspacePage-Bac_Pzmm.js +0 -1
  151. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BAk5l4Ik.js +0 -1
  152. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CrLP_I5S.css +0 -1
  153. package/src/client/dist/pwa/assets/WorkspacePage-FScR5YAD.js +0 -4
  154. package/src/client/dist/pwa/assets/build-path-tree-C0b61nxI.js +0 -1
  155. package/src/client/dist/pwa/assets/custom-sounds-DE4JLdGG.js +0 -1
  156. package/src/client/dist/pwa/assets/de-DgZ9jKq_.js +0 -17
  157. package/src/client/dist/pwa/assets/documents-CALwmao-.js +0 -1
  158. package/src/client/dist/pwa/assets/efforts-Detdm-FH.js +0 -9
  159. package/src/client/dist/pwa/assets/es-zvlgzw9H.js +0 -17
  160. package/src/client/dist/pwa/assets/expand-template-ZIVLPdW2.js +0 -1
  161. package/src/client/dist/pwa/assets/formatters-C_zcfJBt.js +0 -1
  162. package/src/client/dist/pwa/assets/fr-BICzq59I.js +0 -17
  163. package/src/client/dist/pwa/assets/i18n-DG_vChZ3.js +0 -1
  164. package/src/client/dist/pwa/assets/index-fuLr-ZVa.js +0 -20
  165. package/src/client/dist/pwa/assets/it-BIGArQrS.js +0 -17
  166. package/src/client/dist/pwa/assets/monaco.contribution-CDhuQbzi.js +0 -2
  167. package/src/client/dist/pwa/assets/network-auth-QouPCx6A.js +0 -1
  168. package/src/client/dist/pwa/assets/pending-diff-open-CX24u9gW.js +0 -1
  169. package/src/client/dist/pwa/assets/pinia-BdFjQEqa.js +0 -1
  170. package/src/client/dist/pwa/assets/use-checkbox-Bh9EQhkV.js +0 -1
  171. package/src/client/dist/pwa/assets/use-search-index-status-_nCyACc7.js +0 -1
  172. package/src/client/dist/pwa/assets/use-worktree-restore-BDOZc9fk.js +0 -1
  173. package/src/client/dist/pwa/assets/workspace-templates-DV9Njz0T.js +0 -1
@@ -6,7 +6,7 @@ import { getSearchIndexStatus, searchEvents } from '../services/search-service.j
6
6
  import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
7
7
  import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
8
8
  import { withGitRepoLock } from '../utils/git-repo-lock.js';
9
- import { isWorkspaceLifecycleBusy, workspaceLifecycleReason } from '../utils/workspace-lifecycle-guard.js';
9
+ import { assertWorkspaceLifecycleAvailable, isWorkspaceLifecycleBusy, workspaceLifecycleReason, } from '../utils/workspace-lifecycle-guard.js';
10
10
  import autoLoopMessagesRoutes from './auto-loop-messages.js';
11
11
  const execFileAsync = promisify(execFileCb);
12
12
  import fs from 'node:fs';
@@ -73,7 +73,11 @@ function realPathOrResolved(p) {
73
73
  }
74
74
  }
75
75
  function workspaceErrorStatus(err) {
76
- return err instanceof AgentStopError || err instanceof WorkspaceLifecycleBusyError ? 409 : 500;
76
+ return err instanceof AgentStopError ||
77
+ err instanceof WorkspaceLifecycleBusyError ||
78
+ err instanceof terminalService.TerminalStopError
79
+ ? 409
80
+ : 500;
77
81
  }
78
82
  /** Hono sub-router for workspace CRUD, tasks, agents, Git and PR creation. */
79
83
  const app = new Hono();
@@ -381,7 +385,7 @@ app.post('/:id/engine-handoff-preview', migrationGuard, async (c) => {
381
385
  const engine = listEngines().find((item) => item.id === body.engine);
382
386
  if (!engine)
383
387
  return c.json({ error: `Unknown engine '${body.engine ?? ''}'` }, 400);
384
- return c.json({ handoff: buildEngineHandoff(workspace, workspace.engine, engine.id) });
388
+ return c.json({ handoff: await buildEngineHandoff(workspace, workspace.engine, engine.id) });
385
389
  }
386
390
  catch (err) {
387
391
  return c.json({ error: err instanceof Error ? err.message : 'Failed to build engine handoff' }, 500);
@@ -414,7 +418,7 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
414
418
  const effort = typeof body.reasoningEffort === 'string' ? body.reasoningEffort : 'auto';
415
419
  const handoff = typeof body.handoff === 'string' && body.handoff.trim()
416
420
  ? body.handoff.trim().slice(0, 30_000)
417
- : buildEngineHandoff(workspace, workspace.engine, body.engine);
421
+ : await buildEngineHandoff(workspace, workspace.engine, body.engine);
418
422
  if (workspaceService.getWorkspace(workspace.id)?.status === 'compacting') {
419
423
  return c.json({
420
424
  code: 'compacting',
@@ -1914,13 +1918,19 @@ app.post('/:id/pending-wakeup', async (c) => {
1914
1918
  if (rawMode !== 'fresh' && rawMode !== 'resume') {
1915
1919
  return c.json({ error: "mode must be 'fresh' or 'resume'" }, 400);
1916
1920
  }
1921
+ const workspace = workspaceService.getWorkspace(id);
1922
+ if (!workspace)
1923
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
1924
+ if (workspace.archivedAt || workspace.worktreePurgedAt) {
1925
+ return c.json({ error: 'Restore the workspace before scheduling a wakeup' }, 409);
1926
+ }
1927
+ assertWorkspaceLifecycleAvailable(id);
1917
1928
  // 'resume' pins the active session so the wakeup resumes that conversation;
1918
1929
  // 'fresh' (default) — or 'resume' with no active session — leaves it unpinned
1919
1930
  // so the wakeup fires a brand-new session running `prompt`. This is what makes
1920
1931
  // manual scheduling on an idle workspace possible (no more hard 409).
1921
1932
  const agentSessionId = rawMode === 'resume' ? (agentManager.getActiveSessionId(id) ?? undefined) : undefined;
1922
- wakeupService.schedule(id, delaySeconds, prompt, reason, agentSessionId);
1923
- const pending = wakeupService.getPending(id);
1933
+ const pending = wakeupService.schedule(id, delaySeconds, prompt, reason, agentSessionId);
1924
1934
  return c.json({ ok: true, pending });
1925
1935
  }
1926
1936
  catch (err) {
@@ -2408,8 +2418,34 @@ app.get('/:id/events', (c) => {
2408
2418
  // optional: scope to a session view. Session views also include
2409
2419
  // workspace-level rows where session_id IS NULL (legacy/pre-session items).
2410
2420
  const session = c.req.query('session');
2421
+ // Optional, narrow history to one child. Nested tool calls keep their
2422
+ // parent origin, so match both that origin and their own toolCallId.
2423
+ const subagentToolCallId = c.req.query('subagentToolCallId');
2424
+ const subagentThreadIds = (c.req.query('subagentThreadIds') ?? '')
2425
+ .split(',')
2426
+ .map((threadId) => threadId.trim())
2427
+ .filter(Boolean)
2428
+ .slice(0, 50);
2411
2429
  const parsedLimit = parseInt(c.req.query('limit') ?? '100', 10);
2412
2430
  const limit = Math.max(1, Math.min(Number.isFinite(parsedLimit) ? parsedLimit : 100, 500));
2431
+ const sessionClause = session ? ' AND (session_id = ? OR session_id IS NULL)' : '';
2432
+ const sessionParams = session ? [session] : [];
2433
+ const originPayload = "CASE WHEN json_valid(payload) THEN payload ELSE '{}' END";
2434
+ const originClauses = [];
2435
+ const originParams = [];
2436
+ if (subagentToolCallId) {
2437
+ originClauses.push(`(json_extract(${originPayload}, '$.origin.toolCallId') = ? OR json_extract(${originPayload}, '$.toolCallId') = ?)`);
2438
+ originParams.push(subagentToolCallId, subagentToolCallId);
2439
+ }
2440
+ if (subagentThreadIds.length > 0) {
2441
+ originClauses.push(`json_extract(${originPayload}, '$.origin.threadId') IN (${subagentThreadIds.map(() => '?').join(', ')})`);
2442
+ originParams.push(...subagentThreadIds);
2443
+ }
2444
+ const originClause = originClauses.length
2445
+ ? ` AND type = 'agent:event' AND json_extract(${originPayload}, '$.origin.kind') = 'subagent' AND (${originClauses.join(' OR ')})`
2446
+ : '';
2447
+ const scopedClause = `${sessionClause}${originClause}`;
2448
+ const scopedParams = [...sessionParams, ...originParams];
2413
2449
  const db = getDb();
2414
2450
  let rows;
2415
2451
  if (around) {
@@ -2417,13 +2453,9 @@ app.get('/:id/events', (c) => {
2417
2453
  if (!targetRow)
2418
2454
  return c.json({ events: [], hasMore: false });
2419
2455
  const halfWindow = Math.floor(limit / 2);
2420
- rows = session
2421
- ? db
2422
- .prepare('SELECT * FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL) AND rowid BETWEEN ? AND ? ORDER BY rowid ASC')
2423
- .all(id, session, targetRow.rowid - halfWindow, targetRow.rowid + halfWindow)
2424
- : db
2425
- .prepare('SELECT * FROM ws_events WHERE workspace_id = ? AND rowid BETWEEN ? AND ? ORDER BY rowid ASC')
2426
- .all(id, targetRow.rowid - halfWindow, targetRow.rowid + halfWindow);
2456
+ rows = db
2457
+ .prepare(`SELECT * FROM ws_events WHERE workspace_id = ?${scopedClause} AND rowid BETWEEN ? AND ? ORDER BY rowid ASC`)
2458
+ .all(id, ...scopedParams, targetRow.rowid - halfWindow, targetRow.rowid + halfWindow);
2427
2459
  }
2428
2460
  else if (before) {
2429
2461
  // Get the rowid of the cursor event
@@ -2431,25 +2463,17 @@ app.get('/:id/events', (c) => {
2431
2463
  if (!cursorRow) {
2432
2464
  return c.json({ events: [], hasMore: false });
2433
2465
  }
2434
- rows = session
2435
- ? db
2436
- .prepare('SELECT * FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL) AND rowid < ? ORDER BY rowid DESC LIMIT ?')
2437
- .all(id, session, cursorRow.rowid, limit)
2438
- : db
2439
- .prepare('SELECT * FROM ws_events WHERE workspace_id = ? AND rowid < ? ORDER BY rowid DESC LIMIT ?')
2440
- .all(id, cursorRow.rowid, limit);
2466
+ rows = db
2467
+ .prepare(`SELECT * FROM ws_events WHERE workspace_id = ?${scopedClause} AND rowid < ? ORDER BY rowid DESC LIMIT ?`)
2468
+ .all(id, ...scopedParams, cursorRow.rowid, limit);
2441
2469
  }
2442
2470
  else {
2443
2471
  // No cursor — return events. When filtering by session, we want the
2444
2472
  // MOST RECENT events of that session first (so the feed renders from
2445
2473
  // the end), reversed to chronological order below.
2446
- rows = session
2447
- ? db
2448
- .prepare('SELECT * FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL) ORDER BY rowid DESC LIMIT ?')
2449
- .all(id, session, limit)
2450
- : db
2451
- .prepare('SELECT * FROM ws_events WHERE workspace_id = ? ORDER BY rowid DESC LIMIT ?')
2452
- .all(id, limit);
2474
+ rows = db
2475
+ .prepare(`SELECT * FROM ws_events WHERE workspace_id = ?${scopedClause} ORDER BY rowid DESC LIMIT ?`)
2476
+ .all(id, ...scopedParams, limit);
2453
2477
  }
2454
2478
  // Reverse to chronological order (we queried DESC for "before" pagination,
2455
2479
  // or for the "session + no cursor" case where we fetched the newest first).
@@ -2478,13 +2502,9 @@ app.get('/:id/events', (c) => {
2478
2502
  if (before || around) {
2479
2503
  const firstRow = db.prepare('SELECT rowid FROM ws_events WHERE id = ?').get(rows[0].id);
2480
2504
  if (firstRow) {
2481
- const older = session
2482
- ? db
2483
- .prepare('SELECT 1 as c FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL) AND rowid < ? LIMIT 1')
2484
- .get(id, session, firstRow.rowid)
2485
- : db
2486
- .prepare('SELECT 1 as c FROM ws_events WHERE workspace_id = ? AND rowid < ? LIMIT 1')
2487
- .get(id, firstRow.rowid);
2505
+ const older = db
2506
+ .prepare(`SELECT 1 as c FROM ws_events WHERE workspace_id = ?${scopedClause} AND rowid < ? LIMIT 1`)
2507
+ .get(id, ...scopedParams, firstRow.rowid);
2488
2508
  hasMore = older !== undefined;
2489
2509
  }
2490
2510
  }
@@ -2492,11 +2512,9 @@ app.get('/:id/events', (c) => {
2492
2512
  // `LIMIT 1 OFFSET <page size>` answers "is there anything past this
2493
2513
  // page?" by reading one row, where COUNT(*) walked every event the
2494
2514
  // workspace ever produced — on each page, synchronously.
2495
- const beyond = session
2496
- ? db
2497
- .prepare('SELECT 1 as c FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL) LIMIT 1 OFFSET ?')
2498
- .get(id, session, rows.length)
2499
- : db.prepare('SELECT 1 as c FROM ws_events WHERE workspace_id = ? LIMIT 1 OFFSET ?').get(id, rows.length);
2515
+ const beyond = db
2516
+ .prepare(`SELECT 1 as c FROM ws_events WHERE workspace_id = ?${scopedClause} LIMIT 1 OFFSET ?`)
2517
+ .get(id, ...scopedParams, rows.length);
2500
2518
  hasMore = beyond !== undefined;
2501
2519
  }
2502
2520
  }
@@ -3018,12 +3036,7 @@ app.post('/:id/archive', migrationGuard, async (c) => {
3018
3036
  console.error(`[workspaces] stopDevServer during archive failed: ${message}`);
3019
3037
  throw err;
3020
3038
  }
3021
- try {
3022
- terminalService.destroyTerminal(id);
3023
- }
3024
- catch {
3025
- // Terminal may not exist — ignore
3026
- }
3039
+ await terminalService.destroyTerminal(id);
3027
3040
  const updated = workspaceService.archiveWorkspace(id);
3028
3041
  wsService.emitEphemeral(id, 'workspace:archived', { workspace: updated });
3029
3042
  // Run the project's archive script (best-effort — never blocks the archive).
@@ -3140,12 +3153,7 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
3140
3153
  console.error(`[workspaces] stopDevServer during delete failed for '${workspace.name}':`, err);
3141
3154
  throw err;
3142
3155
  }
3143
- try {
3144
- terminalService.destroyTerminal(workspace.id);
3145
- }
3146
- catch {
3147
- // Terminal may not exist — ignore
3148
- }
3156
+ await terminalService.destroyTerminal(workspace.id);
3149
3157
  // Collected best-effort warnings: the DB deletion always proceeds, but
3150
3158
  // side-effects (worktree, local/remote branches) can fail independently.
3151
3159
  // We surface a user-friendly message per failure so the UI can show a
@@ -3178,7 +3186,7 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
3178
3186
  // Delete local branch if requested
3179
3187
  if (opts.deleteLocalBranch) {
3180
3188
  try {
3181
- gitOps.deleteLocalBranch(workspace.projectPath, workspace.workingBranch);
3189
+ await gitOps.deleteLocalBranchAsync(workspace.projectPath, workspace.workingBranch);
3182
3190
  }
3183
3191
  catch (err) {
3184
3192
  const message = err instanceof Error ? err.message : String(err);
@@ -3191,7 +3199,7 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
3191
3199
  // Delete remote branch if requested
3192
3200
  if (opts.deleteRemoteBranch) {
3193
3201
  try {
3194
- gitOps.deleteRemoteBranch(workspace.projectPath, workspace.workingBranch);
3202
+ await gitOps.deleteRemoteBranchAsync(workspace.projectPath, workspace.workingBranch);
3195
3203
  }
3196
3204
  catch (err) {
3197
3205
  const message = err instanceof Error ? err.message : String(err);
@@ -3405,7 +3413,7 @@ app.get('/:id/diff', async (c) => {
3405
3413
  if (!from || !to) {
3406
3414
  return c.json({ error: 'mode=commits requires from and to query params' }, 400);
3407
3415
  }
3408
- if (!gitOps.commitExists(worktreePath, to)) {
3416
+ if (!(await gitOps.commitExistsAsync(worktreePath, to))) {
3409
3417
  return c.json({ error: `Invalid commit ref '${to}'` }, 400);
3410
3418
  }
3411
3419
  // `to` is strict (400 above). `from` is intentionally lenient: any ref it
@@ -3413,8 +3421,8 @@ app.get('/:id/diff', async (c) => {
3413
3421
  // only unresolved `from` the UI ever sends is a root commit's `<sha>^`
3414
3422
  // (single-commit diff of the first commit); the compare dialog otherwise
3415
3423
  // only offers refs that resolve.
3416
- const fromRef = gitOps.commitExists(worktreePath, from) ? from : gitOps.EMPTY_TREE_SHA;
3417
- const files = gitOps.getChangedFilesBetween(worktreePath, fromRef, to);
3424
+ const fromRef = (await gitOps.commitExistsAsync(worktreePath, from)) ? from : gitOps.EMPTY_TREE_SHA;
3425
+ const files = await gitOps.getChangedFilesBetweenAsync(worktreePath, fromRef, to);
3418
3426
  c.header('Cache-Control', 'no-store');
3419
3427
  return c.json({ files, mode: 'commits', from: fromRef, to });
3420
3428
  }
@@ -3426,8 +3434,8 @@ app.get('/:id/diff', async (c) => {
3426
3434
  await gitOps.fetchSourceBranchAsync(worktreePath, workspace.sourceBranch);
3427
3435
  }
3428
3436
  const files = mode === 'unpushed'
3429
- ? gitOps.getUnpushedChangedFiles(worktreePath, workspace.workingBranch)
3430
- : gitOps.getChangedFiles(worktreePath, workspace.sourceBranch, includeUntracked);
3437
+ ? await gitOps.getUnpushedChangedFilesAsync(worktreePath, workspace.workingBranch)
3438
+ : await gitOps.getChangedFilesAsync(worktreePath, workspace.sourceBranch, includeUntracked);
3431
3439
  c.header('Cache-Control', 'no-store');
3432
3440
  return c.json({
3433
3441
  files,
@@ -3446,7 +3454,7 @@ app.get('/:id/diff', async (c) => {
3446
3454
  // - `branch` → sourceBranch
3447
3455
  // - `unpushed` → origin/<workingBranch>
3448
3456
  // `modified` is always the current worktree content.
3449
- app.get('/:id/diff-file', (c) => {
3457
+ app.get('/:id/diff-file', async (c) => {
3450
3458
  try {
3451
3459
  const id = c.req.param('id');
3452
3460
  const filePath = c.req.query('path');
@@ -3472,14 +3480,16 @@ app.get('/:id/diff-file', (c) => {
3472
3480
  if (!from || !to) {
3473
3481
  return c.json({ error: 'mode=commits requires from and to query params' }, 400);
3474
3482
  }
3475
- if (!gitOps.commitExists(worktreePath, to)) {
3483
+ if (!(await gitOps.commitExistsAsync(worktreePath, to))) {
3476
3484
  return c.json({ error: `Invalid commit ref '${to}'` }, 400);
3477
3485
  }
3478
3486
  // `from` is lenient (empty-tree fallback) for a root commit's `<sha>^` —
3479
3487
  // see the matching note in the `/diff` commits branch above.
3480
- const fromRef = gitOps.commitExists(worktreePath, from) ? from : gitOps.EMPTY_TREE_SHA;
3481
- const original = gitOps.getFileAtRef(worktreePath, fromRef, filePath);
3482
- const modified = gitOps.getFileAtRef(worktreePath, to, filePath);
3488
+ const fromRef = (await gitOps.commitExistsAsync(worktreePath, from)) ? from : gitOps.EMPTY_TREE_SHA;
3489
+ const [original, modified] = await Promise.all([
3490
+ gitOps.getFileAtRefAsync(worktreePath, fromRef, filePath),
3491
+ gitOps.getFileAtRefAsync(worktreePath, to, filePath),
3492
+ ]);
3483
3493
  c.header('Cache-Control', 'no-store');
3484
3494
  return c.json({
3485
3495
  original: original ?? '',
@@ -3491,7 +3501,7 @@ app.get('/:id/diff-file', (c) => {
3491
3501
  });
3492
3502
  }
3493
3503
  const baseRef = mode === 'unpushed' ? `origin/${workspace.workingBranch}` : workspace.sourceBranch;
3494
- const original = gitOps.getFileAtRef(worktreePath, baseRef, filePath);
3504
+ const original = await gitOps.getFileAtRefAsync(worktreePath, baseRef, filePath);
3495
3505
  const modified = gitOps.getFileContent(worktreePath, filePath);
3496
3506
  c.header('Cache-Control', 'no-store');
3497
3507
  return c.json({
@@ -3555,7 +3565,7 @@ app.post('/:id/rollback-file', async (c) => {
3555
3565
  // GET /api/workspaces/:id/branch-divergence?limit=50
3556
3566
  // Returns commits on the working branch ahead of `origin/<sourceBranch>`
3557
3567
  // (`ahead`) and commits on `origin/<sourceBranch>` not yet on the working
3558
- // branch (`behind`). Refreshes `origin/<sourceBranch>` synchronously so the
3568
+ // branch (`behind`). Awaits refreshing `origin/<sourceBranch>` so the
3559
3569
  // counts and lists are never computed against a stale local source ref.
3560
3570
  app.get('/:id/branch-divergence', async (c) => {
3561
3571
  try {
@@ -3569,8 +3579,8 @@ app.get('/:id/branch-divergence', async (c) => {
3569
3579
  const limit = Math.min(Math.max(1, Number.isNaN(parsed) ? 50 : parsed), 200);
3570
3580
  const worktreePath = workspace.worktreePath;
3571
3581
  await gitOps.fetchSourceBranchAsync(worktreePath, workspace.sourceBranch);
3572
- const ahead = gitOps.listBranchCommits(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
3573
- const behind = gitOps.listCommitsBehind(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
3582
+ const ahead = await gitOps.listBranchCommitsAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
3583
+ const behind = await gitOps.listCommitsBehindAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
3574
3584
  c.header('Cache-Control', 'no-store');
3575
3585
  return c.json({
3576
3586
  ahead,
@@ -3586,7 +3596,7 @@ app.get('/:id/branch-divergence', async (c) => {
3586
3596
  });
3587
3597
  // GET /api/workspaces/:id/commits?limit=50 — list commits between
3588
3598
  // `origin/<sourceBranch>` and HEAD, each tagged with whether it's already
3589
- // pushed to origin/<branch>. Refreshes `origin/<sourceBranch>` synchronously
3599
+ // pushed to origin/<branch>. Awaits refreshing `origin/<sourceBranch>`
3590
3600
  // so the list is not computed against a stale local source ref.
3591
3601
  app.get('/:id/commits', async (c) => {
3592
3602
  try {
@@ -3599,7 +3609,7 @@ app.get('/:id/commits', async (c) => {
3599
3609
  const limit = Math.min(Math.max(1, parseInt(limitRaw ?? '50', 10) || 50), 200);
3600
3610
  const worktreePath = workspace.worktreePath;
3601
3611
  await gitOps.fetchSourceBranchAsync(worktreePath, workspace.sourceBranch);
3602
- const commits = gitOps.listBranchCommits(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
3612
+ const commits = await gitOps.listBranchCommitsAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
3603
3613
  c.header('Cache-Control', 'no-store');
3604
3614
  return c.json({ commits, sourceBranch: workspace.sourceBranch, workingBranch: workspace.workingBranch });
3605
3615
  }
@@ -3609,13 +3619,13 @@ app.get('/:id/commits', async (c) => {
3609
3619
  }
3610
3620
  });
3611
3621
  // GET /:id/working-tree-files — list uncommitted working-tree files (read-only)
3612
- app.get('/:id/working-tree-files', (c) => {
3622
+ app.get('/:id/working-tree-files', async (c) => {
3613
3623
  try {
3614
3624
  const id = c.req.param('id');
3615
3625
  const workspace = workspaceService.getWorkspace(id);
3616
3626
  if (!workspace)
3617
3627
  return c.json({ error: `Workspace '${id}' not found` }, 404);
3618
- const files = gitOps.getWorkingTreeFiles(workspace.worktreePath);
3628
+ const files = await gitOps.getWorkingTreeFilesAsync(workspace.worktreePath);
3619
3629
  return c.json({ files });
3620
3630
  }
3621
3631
  catch (err) {
@@ -3954,7 +3964,7 @@ app.post('/:id/git/commit-with-agent', migrationGuard, async (c) => {
3954
3964
  const workspace = workspaceService.getWorkspace(id);
3955
3965
  if (!workspace)
3956
3966
  return c.json({ error: `Workspace '${id}' not found` }, 404);
3957
- const workingTree = gitOps.getWorkingTreeStatus(workspace.worktreePath);
3967
+ const workingTree = await gitOps.getWorkingTreeStatusAsync(workspace.worktreePath);
3958
3968
  if (workingTree.staged + workingTree.modified + workingTree.untracked === 0) {
3959
3969
  return c.json({ error: 'No uncommitted changes to commit' }, 400);
3960
3970
  }
@@ -4372,8 +4382,8 @@ app.post('/:id/open-pr', async (c) => {
4372
4382
  return c.json({ ok: true, prNumber, prUrl, messageSent: false });
4373
4383
  }
4374
4384
  // Build context and render the PR prompt template
4375
- const commits = gitOps.getCommitsBetween(worktreePath, workspace.sourceBranch, workspace.workingBranch);
4376
- const diffStats = gitOps.getDiffStatsBetween(worktreePath, workspace.sourceBranch, workspace.workingBranch);
4385
+ const commits = await gitOps.getCommitsBetweenAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch);
4386
+ const diffStats = await gitOps.getDiffStatsBetweenAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch);
4377
4387
  const tasks = workspaceService.listTasks(workspace.id);
4378
4388
  const rendered = renderPrTemplate(effective.prPromptTemplate, {
4379
4389
  workspace,
@@ -94,6 +94,8 @@ const TASK_UPDATED_TERMINAL_STATUS = {
94
94
  };
95
95
  export function createMapperState() {
96
96
  return {
97
+ childMessageOrigins: new Map(),
98
+ childToolOrigins: new Map(),
97
99
  sessionStartedEmitted: false,
98
100
  openMessages: new Map(),
99
101
  sawErrorResult: false,
@@ -147,6 +149,37 @@ function tryEmitQuotaError(state, events, message) {
147
149
  * `state` as needed.
148
150
  */
149
151
  export function mapSdkMessage(msg, state) {
152
+ const parsed = msg;
153
+ const parentId = typeof parsed.parent_tool_use_id === 'string' ? parsed.parent_tool_use_id : undefined;
154
+ const origin = parentId ? { kind: 'subagent', toolCallId: parentId } : undefined;
155
+ const streamed = parsed.event;
156
+ if (origin && streamed?.type === 'message_start' && streamed.message?.id) {
157
+ state.childMessageOrigins.set(streamed.message.id, origin);
158
+ }
159
+ const events = mapSdkMessagePayload(msg, state);
160
+ return events.map((event) => {
161
+ if (event.kind === 'message:text' || event.kind === 'message:thinking' || event.kind === 'message:end') {
162
+ const eventOrigin = origin ?? state.childMessageOrigins.get(event.messageId);
163
+ if (origin)
164
+ state.childMessageOrigins.set(event.messageId, origin);
165
+ if (event.kind === 'message:end')
166
+ state.childMessageOrigins.delete(event.messageId);
167
+ return eventOrigin ? { ...event, origin: eventOrigin } : event;
168
+ }
169
+ if (event.kind === 'tool:call') {
170
+ if (origin)
171
+ state.childToolOrigins.set(event.toolCallId, origin);
172
+ return origin ? { ...event, origin } : event;
173
+ }
174
+ if (event.kind === 'tool:result') {
175
+ const eventOrigin = origin ?? state.childToolOrigins.get(event.toolCallId);
176
+ state.childToolOrigins.delete(event.toolCallId);
177
+ return eventOrigin ? { ...event, origin: eventOrigin } : event;
178
+ }
179
+ return event;
180
+ });
181
+ }
182
+ function mapSdkMessagePayload(msg, state) {
150
183
  // Treat as a generic record — the SDK discriminated union is too broad to
151
184
  // narrow per branch here.
152
185
  const parsed = msg;
@@ -276,7 +276,16 @@ export function createCodexEngine() {
276
276
  }
277
277
  // Child output remains visible, but its failures/compaction/markers
278
278
  // cannot control the parent session or its auto-loop lifecycle.
279
- return map(state).filter((event) => event.kind !== 'error' && !event.kind.startsWith('session:'));
279
+ return map(state)
280
+ .filter((event) => event.kind !== 'error' && !event.kind.startsWith('session:'))
281
+ .map((event) => {
282
+ const origin = { kind: 'subagent', threadId: notification.threadId };
283
+ if (event.kind === 'message:text' || event.kind === 'message:thinking' || event.kind === 'message:end')
284
+ return { ...event, origin };
285
+ if (event.kind === 'tool:call' || event.kind === 'tool:result')
286
+ return { ...event, origin };
287
+ return event;
288
+ });
280
289
  };
281
290
  const itemKey = (threadId, turnId, itemId) => JSON.stringify([threadId, turnId, itemId]);
282
291
  const client = createAppServerClient({
@@ -134,6 +134,7 @@ export function handleItemStarted(item, state) {
134
134
  events.push({
135
135
  kind: 'subagent:progress',
136
136
  toolCallId,
137
+ threadIds: item.receiverThreadIds,
137
138
  status: 'running',
138
139
  description: item.prompt ?? item.tool,
139
140
  taskType: item.tool,
@@ -271,6 +272,7 @@ export function handleItemCompleted(item, state) {
271
272
  events.push({
272
273
  kind: 'subagent:progress',
273
274
  toolCallId,
275
+ threadIds: item.receiverThreadIds,
274
276
  status: hasActiveAgent ? 'running' : 'done',
275
277
  description: item.prompt ?? item.tool,
276
278
  taskType: item.tool,
@@ -14,10 +14,11 @@ export function hasIntegrationMcpConfig(integration, selectedKey) {
14
14
  return Boolean(readClaudeMcpEntry((name) => key ? name === key : integration === 'notion' ? name === 'notion' : /sentry/i.test(name)));
15
15
  }
16
16
  /** Resolve at every launch/resume; never write credentials into a project file. */
17
- export function buildIntegrationMcpServers(settings, onUnavailable) {
17
+ export function buildIntegrationMcpServers(settings, engineId, onUnavailable) {
18
18
  const servers = [];
19
- // Fresh names avoid Codex recursively merging stale native config fields.
20
- const launch = nanoid(12);
19
+ // Codex needs fresh names to avoid recursively merging stale native config.
20
+ // Claude needs stable tool names to preserve its prompt cache across resumes.
21
+ const launch = engineId === 'codex' ? nanoid(12) : 'managed';
21
22
  for (const integration of ['notion', 'sentry']) {
22
23
  if (!settings[`${integration}Enabled`])
23
24
  continue;
@@ -1385,7 +1385,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
1385
1385
  // enforce read-only plan mode here, not only through its prompt.
1386
1386
  const readOnly = getReviewReturn(workspaceId, agentSessionId) !== null;
1387
1387
  const unavailableIntegrations = [];
1388
- const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), (name) => unavailableIntegrations.push(name));
1388
+ const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), engine.id, (name) => unavailableIntegrations.push(name));
1389
1389
  const options = {
1390
1390
  workspaceId,
1391
1391
  workingDir,
@@ -8,19 +8,30 @@ export function createStreamingBatcher(emit, options = {}) {
8
8
  return;
9
9
  clearTimeout(entry.timer);
10
10
  pending.delete(messageId);
11
- emit({ kind: 'message:text', messageId, text: entry.text, streaming: true });
11
+ emit({
12
+ kind: 'message:text',
13
+ messageId,
14
+ text: entry.text,
15
+ streaming: true,
16
+ ...(entry.origin ? { origin: entry.origin } : {}),
17
+ });
12
18
  };
13
19
  return {
14
20
  push(event) {
15
21
  if (event.kind === 'message:text' && event.streaming) {
16
- const entry = pending.get(event.messageId);
22
+ let entry = pending.get(event.messageId);
23
+ if (entry &&
24
+ (entry.origin?.toolCallId !== event.origin?.toolCallId || entry.origin?.threadId !== event.origin?.threadId)) {
25
+ flush(event.messageId);
26
+ entry = undefined;
27
+ }
17
28
  if (entry) {
18
29
  entry.text += event.text;
19
30
  return;
20
31
  }
21
32
  const timer = setTimeout(() => flush(event.messageId), windowMs);
22
33
  timer.unref?.();
23
- pending.set(event.messageId, { text: event.text, timer });
34
+ pending.set(event.messageId, { text: event.text, origin: event.origin, timer });
24
35
  return;
25
36
  }
26
37
  if (event.kind === 'message:end')