@getpaseo/server 0.1.59 → 0.1.60

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 (273) hide show
  1. package/dist/scripts/dev-runner.js +26 -7
  2. package/dist/scripts/dev-runner.js.map +1 -1
  3. package/dist/server/client/daemon-client-runtime-metrics.d.ts +39 -0
  4. package/dist/server/client/daemon-client-runtime-metrics.d.ts.map +1 -0
  5. package/dist/server/client/daemon-client-runtime-metrics.js +173 -0
  6. package/dist/server/client/daemon-client-runtime-metrics.js.map +1 -0
  7. package/dist/server/client/daemon-client.d.ts +58 -9
  8. package/dist/server/client/daemon-client.d.ts.map +1 -1
  9. package/dist/server/client/daemon-client.js +151 -10
  10. package/dist/server/client/daemon-client.js.map +1 -1
  11. package/dist/server/server/agent/agent-manager.d.ts +55 -48
  12. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  13. package/dist/server/server/agent/agent-manager.js +541 -331
  14. package/dist/server/server/agent/agent-manager.js.map +1 -1
  15. package/dist/server/server/agent/agent-metadata-generator.d.ts +3 -2
  16. package/dist/server/server/agent/agent-metadata-generator.d.ts.map +1 -1
  17. package/dist/server/server/agent/agent-metadata-generator.js +31 -16
  18. package/dist/server/server/agent/agent-metadata-generator.js.map +1 -1
  19. package/dist/server/server/agent/agent-projections.d.ts +2 -1
  20. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  21. package/dist/server/server/agent/agent-projections.js +29 -1
  22. package/dist/server/server/agent/agent-projections.js.map +1 -1
  23. package/dist/server/server/agent/agent-sdk-types.d.ts +9 -5
  24. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  25. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  26. package/dist/server/server/agent/agent-storage.d.ts +76 -69
  27. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  28. package/dist/server/server/agent/agent-storage.js +13 -6
  29. package/dist/server/server/agent/agent-storage.js.map +1 -1
  30. package/dist/server/server/agent/agent-stream-coalescer.d.ts +41 -0
  31. package/dist/server/server/agent/agent-stream-coalescer.d.ts.map +1 -0
  32. package/dist/server/server/agent/agent-stream-coalescer.js +166 -0
  33. package/dist/server/server/agent/agent-stream-coalescer.js.map +1 -0
  34. package/dist/server/server/agent/agent-timeline-store-types.d.ts +54 -0
  35. package/dist/server/server/agent/agent-timeline-store-types.d.ts.map +1 -0
  36. package/dist/server/server/agent/agent-timeline-store-types.js +2 -0
  37. package/dist/server/server/agent/agent-timeline-store-types.js.map +1 -0
  38. package/dist/server/server/agent/agent-timeline-store.d.ts +32 -0
  39. package/dist/server/server/agent/agent-timeline-store.d.ts.map +1 -0
  40. package/dist/server/server/agent/agent-timeline-store.js +245 -0
  41. package/dist/server/server/agent/agent-timeline-store.js.map +1 -0
  42. package/dist/server/server/agent/mcp-server.d.ts +12 -1
  43. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  44. package/dist/server/server/agent/mcp-server.js +276 -65
  45. package/dist/server/server/agent/mcp-server.js.map +1 -1
  46. package/dist/server/server/agent/mcp-shared.d.ts +196 -152
  47. package/dist/server/server/agent/mcp-shared.d.ts.map +1 -1
  48. package/dist/server/server/agent/mcp-shared.js +40 -42
  49. package/dist/server/server/agent/mcp-shared.js.map +1 -1
  50. package/dist/server/server/agent/model-resolver.d.ts.map +1 -1
  51. package/dist/server/server/agent/model-resolver.js +3 -1
  52. package/dist/server/server/agent/model-resolver.js.map +1 -1
  53. package/dist/server/server/agent/prompt-attachments.d.ts +6 -0
  54. package/dist/server/server/agent/prompt-attachments.d.ts.map +1 -0
  55. package/dist/server/server/agent/prompt-attachments.js +31 -0
  56. package/dist/server/server/agent/prompt-attachments.js.map +1 -0
  57. package/dist/server/server/agent/provider-launch-config.d.ts +12 -10
  58. package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -1
  59. package/dist/server/server/agent/provider-launch-config.js +34 -0
  60. package/dist/server/server/agent/provider-launch-config.js.map +1 -1
  61. package/dist/server/server/agent/provider-manifest.d.ts +1 -0
  62. package/dist/server/server/agent/provider-manifest.d.ts.map +1 -1
  63. package/dist/server/server/agent/provider-manifest.js +22 -1
  64. package/dist/server/server/agent/provider-manifest.js.map +1 -1
  65. package/dist/server/server/agent/provider-registry.d.ts +5 -2
  66. package/dist/server/server/agent/provider-registry.d.ts.map +1 -1
  67. package/dist/server/server/agent/provider-registry.js +20 -9
  68. package/dist/server/server/agent/provider-registry.js.map +1 -1
  69. package/dist/server/server/agent/provider-snapshot-manager.d.ts +17 -5
  70. package/dist/server/server/agent/provider-snapshot-manager.d.ts.map +1 -1
  71. package/dist/server/server/agent/provider-snapshot-manager.js +150 -61
  72. package/dist/server/server/agent/provider-snapshot-manager.js.map +1 -1
  73. package/dist/server/server/agent/providers/acp-agent.d.ts +8 -4
  74. package/dist/server/server/agent/providers/acp-agent.d.ts.map +1 -1
  75. package/dist/server/server/agent/providers/acp-agent.js +73 -8
  76. package/dist/server/server/agent/providers/acp-agent.js.map +1 -1
  77. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +2 -2
  78. package/dist/server/server/agent/providers/claude-agent.d.ts +1 -1
  79. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  80. package/dist/server/server/agent/providers/claude-agent.js +8 -7
  81. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  82. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +37 -4
  83. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  84. package/dist/server/server/agent/providers/codex-app-server-agent.js +61 -31
  85. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  86. package/dist/server/server/agent/providers/copilot-acp-agent.d.ts.map +1 -1
  87. package/dist/server/server/agent/providers/copilot-acp-agent.js +3 -2
  88. package/dist/server/server/agent/providers/copilot-acp-agent.js.map +1 -1
  89. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +64 -0
  90. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts.map +1 -0
  91. package/dist/server/server/agent/providers/mock-load-test-agent.js +585 -0
  92. package/dist/server/server/agent/providers/mock-load-test-agent.js.map +1 -0
  93. package/dist/server/server/agent/providers/opencode-agent.d.ts +19 -4
  94. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  95. package/dist/server/server/agent/providers/opencode-agent.js +227 -118
  96. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  97. package/dist/server/server/agent/providers/pi-direct-agent.d.ts +69 -0
  98. package/dist/server/server/agent/providers/pi-direct-agent.d.ts.map +1 -0
  99. package/dist/server/server/agent/providers/pi-direct-agent.js +1177 -0
  100. package/dist/server/server/agent/providers/pi-direct-agent.js.map +1 -0
  101. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +7 -4
  102. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  103. package/dist/server/server/agent-attention-policy.d.ts +13 -13
  104. package/dist/server/server/agent-attention-policy.d.ts.map +1 -1
  105. package/dist/server/server/agent-attention-policy.js +20 -36
  106. package/dist/server/server/agent-attention-policy.js.map +1 -1
  107. package/dist/server/server/bootstrap.d.ts +6 -0
  108. package/dist/server/server/bootstrap.d.ts.map +1 -1
  109. package/dist/server/server/bootstrap.js +113 -11
  110. package/dist/server/server/bootstrap.js.map +1 -1
  111. package/dist/server/server/chat/chat-rpc-schemas.d.ts +44 -44
  112. package/dist/server/server/chat/chat-types.d.ts +6 -6
  113. package/dist/server/server/checkout-diff-manager.d.ts +0 -1
  114. package/dist/server/server/checkout-diff-manager.d.ts.map +1 -1
  115. package/dist/server/server/checkout-diff-manager.js +6 -4
  116. package/dist/server/server/checkout-diff-manager.js.map +1 -1
  117. package/dist/server/server/config.d.ts.map +1 -1
  118. package/dist/server/server/config.js +1 -0
  119. package/dist/server/server/config.js.map +1 -1
  120. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  121. package/dist/server/server/file-explorer/service.js +2 -1
  122. package/dist/server/server/file-explorer/service.js.map +1 -1
  123. package/dist/server/server/loop/rpc-schemas.d.ts +392 -392
  124. package/dist/server/server/loop-service.d.ts +52 -52
  125. package/dist/server/server/paseo-worktree-archive-service.d.ts +41 -0
  126. package/dist/server/server/paseo-worktree-archive-service.d.ts.map +1 -0
  127. package/dist/server/server/paseo-worktree-archive-service.js +137 -0
  128. package/dist/server/server/paseo-worktree-archive-service.js.map +1 -0
  129. package/dist/server/server/paseo-worktree-service.d.ts +24 -0
  130. package/dist/server/server/paseo-worktree-service.d.ts.map +1 -0
  131. package/dist/server/server/paseo-worktree-service.js +94 -0
  132. package/dist/server/server/paseo-worktree-service.js.map +1 -0
  133. package/dist/server/server/path-utils.d.ts +1 -0
  134. package/dist/server/server/path-utils.d.ts.map +1 -1
  135. package/dist/server/server/path-utils.js +9 -0
  136. package/dist/server/server/path-utils.js.map +1 -1
  137. package/dist/server/server/persisted-config.d.ts +73 -73
  138. package/dist/server/server/persistence-hooks.d.ts.map +1 -1
  139. package/dist/server/server/persistence-hooks.js +3 -0
  140. package/dist/server/server/persistence-hooks.js.map +1 -1
  141. package/dist/server/server/resolve-worktree-creation-intent.d.ts +30 -0
  142. package/dist/server/server/resolve-worktree-creation-intent.d.ts.map +1 -0
  143. package/dist/server/server/resolve-worktree-creation-intent.js +163 -0
  144. package/dist/server/server/resolve-worktree-creation-intent.js.map +1 -0
  145. package/dist/server/server/schedule/rpc-schemas.d.ts +192 -192
  146. package/dist/server/server/schedule/service.d.ts +1 -1
  147. package/dist/server/server/schedule/service.d.ts.map +1 -1
  148. package/dist/server/server/schedule/types.d.ts +44 -44
  149. package/dist/server/server/script-health-monitor.d.ts +39 -0
  150. package/dist/server/server/script-health-monitor.d.ts.map +1 -0
  151. package/dist/server/server/script-health-monitor.js +158 -0
  152. package/dist/server/server/script-health-monitor.js.map +1 -0
  153. package/dist/server/server/script-proxy.d.ts +40 -0
  154. package/dist/server/server/script-proxy.d.ts.map +1 -0
  155. package/dist/server/server/script-proxy.js +245 -0
  156. package/dist/server/server/script-proxy.js.map +1 -0
  157. package/dist/server/server/script-route-branch-handler.d.ts +10 -0
  158. package/dist/server/server/script-route-branch-handler.d.ts.map +1 -0
  159. package/dist/server/server/script-route-branch-handler.js +45 -0
  160. package/dist/server/server/script-route-branch-handler.js.map +1 -0
  161. package/dist/server/server/script-status-projection.d.ts +29 -0
  162. package/dist/server/server/script-status-projection.d.ts.map +1 -0
  163. package/dist/server/server/script-status-projection.js +133 -0
  164. package/dist/server/server/script-status-projection.js.map +1 -0
  165. package/dist/server/server/session.d.ts +77 -13
  166. package/dist/server/server/session.d.ts.map +1 -1
  167. package/dist/server/server/session.js +1290 -548
  168. package/dist/server/server/session.js.map +1 -1
  169. package/dist/server/server/websocket-server.d.ts +27 -3
  170. package/dist/server/server/websocket-server.d.ts.map +1 -1
  171. package/dist/server/server/websocket-server.js +112 -29
  172. package/dist/server/server/websocket-server.js.map +1 -1
  173. package/dist/server/server/workspace-archive-service.d.ts +8 -0
  174. package/dist/server/server/workspace-archive-service.d.ts.map +1 -0
  175. package/dist/server/server/workspace-archive-service.js +17 -0
  176. package/dist/server/server/workspace-archive-service.js.map +1 -0
  177. package/dist/server/server/workspace-git-metadata.d.ts +24 -0
  178. package/dist/server/server/workspace-git-metadata.d.ts.map +1 -0
  179. package/dist/server/server/workspace-git-metadata.js +78 -0
  180. package/dist/server/server/workspace-git-metadata.js.map +1 -0
  181. package/dist/server/server/workspace-git-service.d.ts +104 -5
  182. package/dist/server/server/workspace-git-service.d.ts.map +1 -1
  183. package/dist/server/server/workspace-git-service.js +442 -56
  184. package/dist/server/server/workspace-git-service.js.map +1 -1
  185. package/dist/server/server/workspace-reconciliation-service.d.ts +54 -0
  186. package/dist/server/server/workspace-reconciliation-service.d.ts.map +1 -0
  187. package/dist/server/server/workspace-reconciliation-service.js +176 -0
  188. package/dist/server/server/workspace-reconciliation-service.js.map +1 -0
  189. package/dist/server/server/workspace-registry-bootstrap.d.ts.map +1 -1
  190. package/dist/server/server/workspace-registry-bootstrap.js +4 -3
  191. package/dist/server/server/workspace-registry-bootstrap.js.map +1 -1
  192. package/dist/server/server/workspace-registry.d.ts +8 -8
  193. package/dist/server/server/workspace-registry.test-helpers.d.ts +37 -0
  194. package/dist/server/server/workspace-registry.test-helpers.d.ts.map +1 -0
  195. package/dist/server/server/workspace-registry.test-helpers.js +121 -0
  196. package/dist/server/server/workspace-registry.test-helpers.js.map +1 -0
  197. package/dist/server/server/workspace-script-runtime-store.d.ts +28 -0
  198. package/dist/server/server/workspace-script-runtime-store.d.ts.map +1 -0
  199. package/dist/server/server/workspace-script-runtime-store.js +78 -0
  200. package/dist/server/server/workspace-script-runtime-store.js.map +1 -0
  201. package/dist/server/server/workspace-service-env.d.ts +17 -0
  202. package/dist/server/server/workspace-service-env.d.ts.map +1 -0
  203. package/dist/server/server/workspace-service-env.js +80 -0
  204. package/dist/server/server/workspace-service-env.js.map +1 -0
  205. package/dist/server/server/workspace-service-port-registry.d.ts +19 -0
  206. package/dist/server/server/workspace-service-port-registry.d.ts.map +1 -0
  207. package/dist/server/server/workspace-service-port-registry.js +59 -0
  208. package/dist/server/server/workspace-service-port-registry.js.map +1 -0
  209. package/dist/server/server/worktree-bootstrap.d.ts +55 -10
  210. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
  211. package/dist/server/server/worktree-bootstrap.js +290 -112
  212. package/dist/server/server/worktree-bootstrap.js.map +1 -1
  213. package/dist/server/server/worktree-core.d.ts +25 -0
  214. package/dist/server/server/worktree-core.d.ts.map +1 -0
  215. package/dist/server/server/worktree-core.js +75 -0
  216. package/dist/server/server/worktree-core.js.map +1 -0
  217. package/dist/server/server/worktree-errors.d.ts +12 -0
  218. package/dist/server/server/worktree-errors.d.ts.map +1 -0
  219. package/dist/server/server/worktree-errors.js +31 -0
  220. package/dist/server/server/worktree-errors.js.map +1 -0
  221. package/dist/server/server/worktree-session.d.ts +56 -70
  222. package/dist/server/server/worktree-session.d.ts.map +1 -1
  223. package/dist/server/server/worktree-session.js +176 -251
  224. package/dist/server/server/worktree-session.js.map +1 -1
  225. package/dist/server/services/github-service.d.ts +225 -0
  226. package/dist/server/services/github-service.d.ts.map +1 -0
  227. package/dist/server/services/github-service.js +1381 -0
  228. package/dist/server/services/github-service.js.map +1 -0
  229. package/dist/server/shared/messages.d.ts +29408 -12268
  230. package/dist/server/shared/messages.d.ts.map +1 -1
  231. package/dist/server/shared/messages.js +391 -65
  232. package/dist/server/shared/messages.js.map +1 -1
  233. package/dist/server/terminal/shell-integration/zsh/.zshenv +17 -0
  234. package/dist/server/terminal/shell-integration/zsh/paseo-integration.zsh +32 -0
  235. package/dist/server/terminal/terminal-manager.d.ts +9 -0
  236. package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
  237. package/dist/server/terminal/terminal-manager.js +27 -0
  238. package/dist/server/terminal/terminal-manager.js.map +1 -1
  239. package/dist/server/terminal/terminal-output-coalescer.d.ts +30 -0
  240. package/dist/server/terminal/terminal-output-coalescer.d.ts.map +1 -0
  241. package/dist/server/terminal/terminal-output-coalescer.js +55 -0
  242. package/dist/server/terminal/terminal-output-coalescer.js.map +1 -0
  243. package/dist/server/terminal/terminal.d.ts +32 -1
  244. package/dist/server/terminal/terminal.d.ts.map +1 -1
  245. package/dist/server/terminal/terminal.js +397 -17
  246. package/dist/server/terminal/terminal.js.map +1 -1
  247. package/dist/server/utils/checkout-git.d.ts +63 -10
  248. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  249. package/dist/server/utils/checkout-git.js +321 -229
  250. package/dist/server/utils/checkout-git.js.map +1 -1
  251. package/dist/server/utils/promise-timeout.d.ts +9 -0
  252. package/dist/server/utils/promise-timeout.d.ts.map +1 -0
  253. package/dist/server/utils/promise-timeout.js +25 -0
  254. package/dist/server/utils/promise-timeout.js.map +1 -0
  255. package/dist/server/utils/script-hostname.d.ts +8 -0
  256. package/dist/server/utils/script-hostname.d.ts.map +1 -0
  257. package/dist/server/utils/script-hostname.js +14 -0
  258. package/dist/server/utils/script-hostname.js.map +1 -0
  259. package/dist/server/utils/string-command-shell.d.ts +10 -0
  260. package/dist/server/utils/string-command-shell.d.ts.map +1 -0
  261. package/dist/server/utils/string-command-shell.js +21 -0
  262. package/dist/server/utils/string-command-shell.js.map +1 -0
  263. package/dist/server/utils/worktree.d.ts +54 -7
  264. package/dist/server/utils/worktree.d.ts.map +1 -1
  265. package/dist/server/utils/worktree.js +434 -129
  266. package/dist/server/utils/worktree.js.map +1 -1
  267. package/dist/src/terminal/shell-integration/zsh/.zshenv +17 -0
  268. package/dist/src/terminal/shell-integration/zsh/paseo-integration.zsh +32 -0
  269. package/package.json +11 -14
  270. package/dist/server/server/agent/providers/pi-acp-agent.d.ts +0 -28
  271. package/dist/server/server/agent/providers/pi-acp-agent.d.ts.map +0 -1
  272. package/dist/server/server/agent/providers/pi-acp-agent.js +0 -302
  273. package/dist/server/server/agent/providers/pi-acp-agent.js.map +0 -1
@@ -3,7 +3,11 @@ import { resolve } from "node:path";
3
3
  import { stat } from "node:fs/promises";
4
4
  import { AGENT_LIFECYCLE_STATUSES, } from "../../shared/agent-lifecycle.js";
5
5
  import { z } from "zod";
6
+ import { InMemoryAgentTimelineStore, } from "./agent-timeline-store.js";
7
+ import { AGENT_STREAM_COALESCE_DEFAULT_WINDOW_MS, AgentStreamCoalescer, } from "./agent-stream-coalescer.js";
6
8
  import { getAgentProviderDefinition } from "./provider-manifest.js";
9
+ const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3000;
10
+ const INTERRUPT_SESSION_TIMEOUT_MS = 2000;
7
11
  export { AGENT_LIFECYCLE_STATUSES };
8
12
  const SYSTEM_ERROR_PREFIX = "[System Error]";
9
13
  function attachPersistenceCwd(handle, cwd) {
@@ -18,7 +22,6 @@ function attachPersistenceCwd(handle, cwd) {
18
22
  },
19
23
  };
20
24
  }
21
- const DEFAULT_TIMELINE_FETCH_LIMIT = 200;
22
25
  const BUSY_STATUSES = ["initializing", "running"];
23
26
  const AgentIdSchema = z.string().uuid();
24
27
  function isAgentBusy(status) {
@@ -56,22 +59,31 @@ export class AgentManager {
56
59
  constructor(options) {
57
60
  this.clients = new Map();
58
61
  this.agents = new Map();
62
+ this.timelineStore = new InMemoryAgentTimelineStore();
63
+ this.agentsAwaitingInitialSnapshotPersist = new Set();
64
+ this.sessionEventTails = new Map();
59
65
  this.pendingForegroundRuns = new Map();
60
66
  this.subscribers = new Set();
61
67
  this.previousStatuses = new Map();
62
68
  this.backgroundTasks = new Set();
63
- const maxTimelineItems = options?.maxTimelineItems;
64
- this.maxTimelineItems =
65
- typeof maxTimelineItems === "number" &&
66
- Number.isFinite(maxTimelineItems) &&
67
- maxTimelineItems >= 0
68
- ? Math.floor(maxTimelineItems)
69
- : null;
70
69
  this.idFactory = options?.idFactory ?? (() => randomUUID());
71
70
  this.registry = options?.registry;
71
+ this.durableTimelineStore = options?.durableTimelineStore;
72
72
  this.onAgentAttention = options?.onAgentAttention;
73
73
  this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
74
74
  this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
75
+ this.rescueTimeouts = {
76
+ reloadSessionCloseMs: options.rescueTimeouts?.reloadSessionCloseMs ?? RELOAD_SESSION_CLOSE_TIMEOUT_MS,
77
+ interruptSessionMs: options.rescueTimeouts?.interruptSessionMs ?? INTERRUPT_SESSION_TIMEOUT_MS,
78
+ };
79
+ this.agentStreamCoalescer = new AgentStreamCoalescer({
80
+ windowMs: options.agentStreamCoalesceWindowMs ?? AGENT_STREAM_COALESCE_DEFAULT_WINDOW_MS,
81
+ timers: { setTimeout, clearTimeout },
82
+ onFlush: ({ agentId, item, provider, turnId }) => {
83
+ const event = this.recordAndDispatchTimelineItem(agentId, item, provider, turnId);
84
+ this.notifyForegroundTurnWaiters(agentId, event);
85
+ },
86
+ });
75
87
  if (options?.clients) {
76
88
  for (const [provider, client] of Object.entries(options.clients)) {
77
89
  if (client) {
@@ -102,7 +114,10 @@ export class AgentManager {
102
114
  if (agent.activeForegroundTurnId !== null) {
103
115
  withActiveForegroundTurn++;
104
116
  }
105
- const len = agent.timeline.length;
117
+ if (!this.timelineStore.has(agent.id)) {
118
+ continue;
119
+ }
120
+ const len = this.timelineStore.getItems(agent.id).length;
106
121
  totalItems += len;
107
122
  if (len > maxItemsPerAgent) {
108
123
  maxItemsPerAgent = len;
@@ -282,130 +297,19 @@ export class AgentManager {
282
297
  return agent ? { ...agent } : null;
283
298
  }
284
299
  getTimeline(id) {
285
- const agent = this.requireAgent(id);
286
- return [...agent.timeline];
300
+ this.requireAgent(id);
301
+ return this.timelineStore.getItems(id);
287
302
  }
288
- getTimelineRows(id) {
289
- const agent = this.requireAgent(id);
290
- const { rows } = this.ensureTimelineState(agent);
291
- return rows.map((row) => ({ ...row }));
303
+ async getTimelineRows(id) {
304
+ this.requireAgent(id);
305
+ if (this.durableTimelineStore) {
306
+ return await this.durableTimelineStore.getCommittedRows(id);
307
+ }
308
+ return this.timelineStore.getRows(id);
292
309
  }
293
310
  fetchTimeline(id, options) {
294
- const agent = this.requireAgent(id);
295
- const { rows, epoch, nextSeq, minSeq, maxSeq } = this.ensureTimelineState(agent);
296
- const direction = options?.direction ?? "tail";
297
- const requestedLimit = options?.limit;
298
- const limit = requestedLimit === undefined
299
- ? DEFAULT_TIMELINE_FETCH_LIMIT
300
- : Math.max(0, Math.floor(requestedLimit));
301
- const cursor = options?.cursor;
302
- const window = { minSeq, maxSeq, nextSeq };
303
- if (cursor && cursor.epoch !== epoch) {
304
- return {
305
- epoch,
306
- direction,
307
- reset: true,
308
- staleCursor: true,
309
- gap: false,
310
- window,
311
- hasOlder: false,
312
- hasNewer: false,
313
- rows: rows.map((row) => ({ ...row })),
314
- };
315
- }
316
- const selectAll = limit === 0;
317
- const cloneRows = (items) => items.map((row) => ({ ...row }));
318
- if (direction === "after" && cursor && rows.length > 0 && cursor.seq < minSeq - 1) {
319
- return {
320
- epoch,
321
- direction,
322
- reset: true,
323
- staleCursor: false,
324
- gap: true,
325
- window,
326
- hasOlder: false,
327
- hasNewer: false,
328
- rows: cloneRows(rows),
329
- };
330
- }
331
- if (rows.length === 0) {
332
- return {
333
- epoch,
334
- direction,
335
- reset: false,
336
- staleCursor: false,
337
- gap: false,
338
- window,
339
- hasOlder: false,
340
- hasNewer: false,
341
- rows: [],
342
- };
343
- }
344
- if (direction === "tail") {
345
- const selected = selectAll || limit >= rows.length ? rows : rows.slice(rows.length - limit);
346
- const hasOlder = selected.length > 0 && selected[0].seq > minSeq;
347
- return {
348
- epoch,
349
- direction,
350
- reset: false,
351
- staleCursor: false,
352
- gap: false,
353
- window,
354
- hasOlder,
355
- hasNewer: false,
356
- rows: cloneRows(selected),
357
- };
358
- }
359
- if (direction === "after") {
360
- const baseSeq = cursor?.seq ?? 0;
361
- const startIdx = rows.findIndex((row) => row.seq > baseSeq);
362
- if (startIdx < 0) {
363
- return {
364
- epoch,
365
- direction,
366
- reset: false,
367
- staleCursor: false,
368
- gap: false,
369
- window,
370
- hasOlder: baseSeq >= minSeq,
371
- hasNewer: false,
372
- rows: [],
373
- };
374
- }
375
- const selected = selectAll ? rows.slice(startIdx) : rows.slice(startIdx, startIdx + limit);
376
- const lastSelected = selected[selected.length - 1];
377
- return {
378
- epoch,
379
- direction,
380
- reset: false,
381
- staleCursor: false,
382
- gap: false,
383
- window,
384
- hasOlder: selected[0].seq > minSeq,
385
- hasNewer: Boolean(lastSelected && lastSelected.seq < maxSeq),
386
- rows: cloneRows(selected),
387
- };
388
- }
389
- // direction === "before"
390
- const beforeSeq = cursor?.seq ?? nextSeq;
391
- const endExclusive = rows.findIndex((row) => row.seq >= beforeSeq);
392
- const boundedRows = endExclusive < 0 ? rows : rows.slice(0, endExclusive);
393
- const selected = selectAll || limit >= boundedRows.length
394
- ? boundedRows
395
- : boundedRows.slice(boundedRows.length - limit);
396
- const hasOlder = selected.length > 0 && selected[0].seq > minSeq;
397
- const hasNewer = endExclusive >= 0;
398
- return {
399
- epoch,
400
- direction,
401
- reset: false,
402
- staleCursor: false,
403
- gap: false,
404
- window,
405
- hasOlder,
406
- hasNewer,
407
- rows: cloneRows(selected),
408
- };
311
+ this.requireAgent(id);
312
+ return this.timelineStore.fetch(id, options);
409
313
  }
410
314
  async createAgent(config, agentId, options) {
411
315
  const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent");
@@ -431,6 +335,7 @@ export class AgentManager {
431
335
  const session = await client.createSession(normalizedConfig, launchContext);
432
336
  return this.registerSession(session, normalizedConfig, resolvedAgentId, {
433
337
  labels: options?.labels,
338
+ workspaceId: options?.workspaceId,
434
339
  });
435
340
  }
436
341
  // Reconstruct an agent from provider persistence. Callers should explicitly
@@ -459,16 +364,11 @@ export class AgentManager {
459
364
  // Hot-reload an active agent session with config overrides while preserving
460
365
  // in-memory timeline state.
461
366
  async reloadAgentSession(agentId, overrides) {
462
- let existing = this.requireAgent(agentId);
367
+ let existing = this.requireSessionAgent(agentId);
463
368
  if (this.hasInFlightRun(agentId)) {
464
369
  await this.cancelAgentRun(agentId);
465
- existing = this.requireAgent(agentId);
370
+ existing = this.requireSessionAgent(agentId);
466
371
  }
467
- const timelineState = this.ensureTimelineState(existing);
468
- const preservedTimeline = [...existing.timeline];
469
- const preservedTimelineRows = timelineState.rows.map((row) => ({ ...row }));
470
- const preservedTimelineEpoch = timelineState.epoch;
471
- const preservedTimelineNextSeq = timelineState.nextSeq;
472
372
  const preservedHistoryPrimed = existing.historyPrimed;
473
373
  const preservedLastUsage = existing.lastUsage;
474
374
  const preservedLastError = existing.lastError;
@@ -486,6 +386,7 @@ export class AgentManager {
486
386
  const session = handle
487
387
  ? await client.resumeSession(handle, normalizedConfig, launchContext)
488
388
  : await client.createSession(normalizedConfig, launchContext);
389
+ this.agentStreamCoalescer.flushAndDiscard(agentId);
489
390
  // Remove the existing agent entry before swapping sessions
490
391
  this.agents.delete(agentId);
491
392
  if (existing.unsubscribeSession) {
@@ -497,28 +398,65 @@ export class AgentManager {
497
398
  }
498
399
  existing.foregroundTurnWaiters.clear();
499
400
  this.settlePendingForegroundRun(agentId);
500
- try {
501
- await existing.session.close();
502
- }
503
- catch (error) {
504
- this.logger.warn({ err: error, agentId }, "Failed to close previous session during refresh");
505
- }
401
+ await this.closeReloadedSession(existing.session, agentId);
506
402
  // Preserve existing labels and timeline during reload.
507
403
  return this.registerSession(session, normalizedConfig, agentId, {
508
404
  labels: existing.labels,
509
405
  createdAt: existing.createdAt,
510
406
  updatedAt: existing.updatedAt,
511
407
  lastUserMessageAt: existing.lastUserMessageAt,
512
- timeline: preservedTimeline,
513
- timelineRows: preservedTimelineRows,
514
- timelineEpoch: preservedTimelineEpoch,
515
- timelineNextSeq: preservedTimelineNextSeq,
516
408
  historyPrimed: preservedHistoryPrimed,
517
409
  lastUsage: preservedLastUsage,
518
410
  lastError: preservedLastError,
519
411
  attention: preservedAttention,
520
412
  });
521
413
  }
414
+ async closeReloadedSession(session, agentId) {
415
+ try {
416
+ const result = await this.waitWithTimeout({
417
+ operation: session.close(),
418
+ timeoutMs: this.rescueTimeouts.reloadSessionCloseMs,
419
+ onLateError: (error) => {
420
+ this.logger.warn({ err: error, agentId }, "Previous session close failed after refresh timeout");
421
+ },
422
+ });
423
+ if (result === "timed_out") {
424
+ this.logger.warn({ agentId, timeoutMs: this.rescueTimeouts.reloadSessionCloseMs }, "Timed out closing previous session during refresh");
425
+ }
426
+ }
427
+ catch (error) {
428
+ this.logger.warn({ err: error, agentId }, "Failed to close previous session during refresh");
429
+ }
430
+ }
431
+ async waitWithTimeout(options) {
432
+ let didTimeOut = false;
433
+ let timer = null;
434
+ const operation = options.operation
435
+ .then(() => "completed")
436
+ .catch((error) => {
437
+ if (didTimeOut) {
438
+ options.onLateError?.(error);
439
+ return "timed_out";
440
+ }
441
+ throw error;
442
+ });
443
+ try {
444
+ return await Promise.race([
445
+ operation,
446
+ new Promise((resolve) => {
447
+ timer = setTimeout(() => {
448
+ didTimeOut = true;
449
+ resolve("timed_out");
450
+ }, options.timeoutMs);
451
+ }),
452
+ ]);
453
+ }
454
+ finally {
455
+ if (timer) {
456
+ clearTimeout(timer);
457
+ }
458
+ }
459
+ }
522
460
  async closeAgent(agentId) {
523
461
  const agent = this.requireAgent(agentId);
524
462
  this.logger.trace({
@@ -527,34 +465,11 @@ export class AgentManager {
527
465
  activeForegroundTurnId: agent.activeForegroundTurnId,
528
466
  pendingPermissions: agent.pendingPermissions.size,
529
467
  }, "closeAgent: start");
530
- this.agents.delete(agentId);
531
- // Clean up previousStatus to prevent memory leak
532
- this.previousStatuses.delete(agentId);
533
- if (agent.unsubscribeSession) {
534
- agent.unsubscribeSession();
535
- agent.unsubscribeSession = null;
536
- }
537
- for (const waiter of agent.foregroundTurnWaiters) {
538
- // Wake up the generator so it can exit the await loop
539
- waiter.callback({
540
- type: "turn_canceled",
541
- provider: agent.provider,
542
- reason: "agent closed",
543
- turnId: waiter.turnId,
544
- });
545
- this.settleForegroundTurnWaiter(waiter);
546
- }
547
- agent.foregroundTurnWaiters.clear();
548
- this.settlePendingForegroundRun(agentId);
549
- const session = agent.session;
550
- const closedAgent = {
551
- ...agent,
552
- lifecycle: "closed",
553
- session: null,
554
- activeForegroundTurnId: null,
555
- };
556
- await session.close();
557
- this.emitState(closedAgent);
468
+ const closedAgent = this.prepareAgentForClosure(agent, "agent closed");
469
+ await agent.session.close();
470
+ this.timelineStore.delete(agentId);
471
+ await this.persistSnapshot(closedAgent);
472
+ this.emitClosedAgent(closedAgent, { persist: false });
558
473
  this.logger.trace({ agentId }, "closeAgent: completed");
559
474
  }
560
475
  async archiveAgent(agentId) {
@@ -587,7 +502,7 @@ export class AgentManager {
587
502
  return { archivedAt };
588
503
  }
589
504
  async setAgentMode(agentId, modeId) {
590
- const agent = this.requireAgent(agentId);
505
+ const agent = this.requireSessionAgent(agentId);
591
506
  await agent.session.setMode(modeId);
592
507
  agent.config.modeId = modeId;
593
508
  agent.currentModeId = modeId;
@@ -599,7 +514,7 @@ export class AgentManager {
599
514
  this.emitState(agent);
600
515
  }
601
516
  async setAgentModel(agentId, modelId) {
602
- const agent = this.requireAgent(agentId);
517
+ const agent = this.requireSessionAgent(agentId);
603
518
  const normalizedModelId = typeof modelId === "string" && modelId.trim().length > 0 ? modelId : null;
604
519
  if (agent.session.setModel) {
605
520
  await agent.session.setModel(normalizedModelId);
@@ -612,7 +527,7 @@ export class AgentManager {
612
527
  this.emitState(agent);
613
528
  }
614
529
  async setAgentThinkingOption(agentId, thinkingOptionId) {
615
- const agent = this.requireAgent(agentId);
530
+ const agent = this.requireSessionAgent(agentId);
616
531
  const normalizedThinkingOptionId = typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
617
532
  ? thinkingOptionId
618
533
  : null;
@@ -620,6 +535,12 @@ export class AgentManager {
620
535
  await agent.session.setThinkingOption(normalizedThinkingOptionId);
621
536
  }
622
537
  agent.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined;
538
+ if (agent.runtimeInfo) {
539
+ agent.runtimeInfo = {
540
+ ...agent.runtimeInfo,
541
+ thinkingOptionId: normalizedThinkingOptionId,
542
+ };
543
+ }
623
544
  this.touchUpdatedAt(agent);
624
545
  this.emitState(agent);
625
546
  }
@@ -639,16 +560,21 @@ export class AgentManager {
639
560
  if (!normalizedTitle) {
640
561
  return;
641
562
  }
563
+ if (this.agentsAwaitingInitialSnapshotPersist.has(agent.id) &&
564
+ this.registry &&
565
+ (await this.registry.get(agent.id)) === null) {
566
+ return;
567
+ }
642
568
  this.touchUpdatedAt(agent);
643
569
  await this.persistSnapshot(agent, { title: normalizedTitle });
644
- this.emitState(agent);
570
+ this.emitState(agent, { persist: false });
645
571
  }
646
572
  async setLabels(agentId, labels) {
647
573
  const agent = this.requireAgent(agentId);
648
574
  agent.labels = { ...agent.labels, ...labels };
649
- await this.persistSnapshot(agent);
650
575
  this.touchUpdatedAt(agent);
651
- this.emitState(agent);
576
+ await this.persistSnapshot(agent);
577
+ this.emitState(agent, { persist: false });
652
578
  }
653
579
  notifyAgentState(agentId) {
654
580
  const agent = this.agents.get(agentId);
@@ -663,8 +589,81 @@ export class AgentManager {
663
589
  if (agent.attention.requiresAttention) {
664
590
  agent.attention = { requiresAttention: false };
665
591
  await this.persistSnapshot(agent);
666
- this.emitState(agent);
592
+ this.emitState(agent, { persist: false });
593
+ }
594
+ }
595
+ async archiveSnapshot(agentId, archivedAt) {
596
+ const registry = this.requireRegistry();
597
+ const liveAgent = this.getAgent(agentId);
598
+ if (liveAgent) {
599
+ await this.persistSnapshot(liveAgent, {
600
+ internal: liveAgent.internal,
601
+ });
602
+ }
603
+ const record = await registry.get(agentId);
604
+ if (!record) {
605
+ throw new Error(`Agent not found: ${agentId}`);
606
+ }
607
+ const normalizedStatus = record.lastStatus === "running" || record.lastStatus === "initializing"
608
+ ? "idle"
609
+ : record.lastStatus;
610
+ const nextRecord = {
611
+ ...record,
612
+ archivedAt,
613
+ lastStatus: normalizedStatus,
614
+ requiresAttention: false,
615
+ attentionReason: null,
616
+ attentionTimestamp: null,
617
+ };
618
+ await registry.upsert(nextRecord);
619
+ return nextRecord;
620
+ }
621
+ async unarchiveSnapshot(agentId) {
622
+ const registry = this.requireRegistry();
623
+ const record = await registry.get(agentId);
624
+ if (!record || !record.archivedAt) {
625
+ return false;
626
+ }
627
+ await registry.upsert({
628
+ ...record,
629
+ archivedAt: null,
630
+ });
631
+ if (this.getAgent(agentId)) {
632
+ this.notifyAgentState(agentId);
633
+ }
634
+ return true;
635
+ }
636
+ async unarchiveSnapshotByHandle(handle) {
637
+ const registry = this.requireRegistry();
638
+ const records = await registry.list();
639
+ const matched = records.find((record) => record.persistence?.provider === handle.provider &&
640
+ record.persistence?.sessionId === handle.sessionId);
641
+ if (!matched) {
642
+ return;
667
643
  }
644
+ await this.unarchiveSnapshot(matched.id);
645
+ }
646
+ async updateAgentMetadata(agentId, updates) {
647
+ const liveAgent = this.getAgent(agentId);
648
+ if (liveAgent) {
649
+ if (updates.title) {
650
+ await this.setTitle(agentId, updates.title);
651
+ }
652
+ if (updates.labels) {
653
+ await this.setLabels(agentId, updates.labels);
654
+ }
655
+ return;
656
+ }
657
+ const registry = this.requireRegistry();
658
+ const existing = await registry.get(agentId);
659
+ if (!existing) {
660
+ throw new Error(`Agent not found: ${agentId}`);
661
+ }
662
+ await registry.upsert({
663
+ ...existing,
664
+ ...(updates.title ? { title: updates.title } : {}),
665
+ ...(updates.labels ? { labels: { ...existing.labels, ...updates.labels } } : {}),
666
+ });
668
667
  }
669
668
  async runAgent(agentId, prompt, options) {
670
669
  const events = this.streamAgent(agentId, prompt, options);
@@ -710,14 +709,14 @@ export class AgentManager {
710
709
  };
711
710
  const updatedAt = this.touchUpdatedAt(agent);
712
711
  agent.lastUserMessageAt = updatedAt;
713
- const row = this.recordTimeline(agent, item);
712
+ const row = this.recordTimeline(agentId, item);
714
713
  this.dispatchStream(agentId, {
715
714
  type: "timeline",
716
715
  item,
717
716
  provider: agent.provider,
718
717
  }, {
719
718
  seq: row.seq,
720
- epoch: this.ensureTimelineState(agent).epoch,
719
+ epoch: this.timelineStore.getEpoch(agentId),
721
720
  });
722
721
  if (options?.emitState !== false) {
723
722
  this.emitState(agent);
@@ -726,14 +725,14 @@ export class AgentManager {
726
725
  async appendTimelineItem(agentId, item) {
727
726
  const agent = this.requireAgent(agentId);
728
727
  this.touchUpdatedAt(agent);
729
- const row = this.recordTimeline(agent, item);
728
+ const row = this.recordTimeline(agentId, item);
730
729
  this.dispatchStream(agentId, {
731
730
  type: "timeline",
732
731
  item,
733
732
  provider: agent.provider,
734
733
  }, {
735
734
  seq: row.seq,
736
- epoch: this.ensureTimelineState(agent).epoch,
735
+ epoch: this.timelineStore.getEpoch(agentId),
737
736
  });
738
737
  await this.persistSnapshot(agent);
739
738
  }
@@ -747,7 +746,7 @@ export class AgentManager {
747
746
  });
748
747
  }
749
748
  streamAgent(agentId, prompt, options) {
750
- const existingAgent = this.requireAgent(agentId);
749
+ const existingAgent = this.requireSessionAgent(agentId);
751
750
  this.logger.trace({
752
751
  agentId,
753
752
  lifecycle: existingAgent.lifecycle,
@@ -852,8 +851,11 @@ export class AgentManager {
852
851
  })();
853
852
  return streamForwarder;
854
853
  }
855
- finalizeForegroundTurn(agent) {
854
+ finalizeForegroundTurn(agent, turnId) {
856
855
  const mutableAgent = agent;
856
+ if (turnId) {
857
+ this.rememberFinalizedForegroundTurn(mutableAgent, turnId);
858
+ }
857
859
  mutableAgent.activeForegroundTurnId = null;
858
860
  const terminalError = mutableAgent.lastError;
859
861
  const shouldHoldBusyForReplacement = mutableAgent.pendingReplacement && !terminalError;
@@ -887,8 +889,11 @@ export class AgentManager {
887
889
  !this.hasPendingForegroundRun(agentId)) {
888
890
  return this.streamAgent(agentId, prompt, options);
889
891
  }
890
- const agent = snapshot;
892
+ const agent = this.requireSessionAgent(agentId);
891
893
  agent.pendingReplacement = true;
894
+ agent.lifecycle = "running";
895
+ this.touchUpdatedAt(agent);
896
+ this.emitState(agent);
892
897
  const self = this;
893
898
  return (async function* replaceRunForwarder() {
894
899
  try {
@@ -1026,7 +1031,7 @@ export class AgentManager {
1026
1031
  }
1027
1032
  }
1028
1033
  async cancelAgentRun(agentId) {
1029
- const agent = this.requireAgent(agentId);
1034
+ const agent = this.requireSessionAgent(agentId);
1030
1035
  const pendingRun = this.getPendingForegroundRun(agentId);
1031
1036
  const foregroundTurnId = agent.activeForegroundTurnId;
1032
1037
  const hasForegroundTurn = Boolean(foregroundTurnId);
@@ -1034,12 +1039,7 @@ export class AgentManager {
1034
1039
  if (!hasForegroundTurn && !isAutonomousRunning && !pendingRun) {
1035
1040
  return false;
1036
1041
  }
1037
- try {
1038
- await agent.session.interrupt();
1039
- }
1040
- catch (error) {
1041
- this.logger.error({ err: error, agentId }, "Failed to interrupt session");
1042
- }
1042
+ await this.interruptSession(agent.session, agentId);
1043
1043
  // The interrupt will produce a turn_canceled/turn_failed event via subscribe(),
1044
1044
  // which flows through the session event dispatcher and settles the foreground turn waiter.
1045
1045
  // Wait briefly for the event to propagate if there's an active foreground turn.
@@ -1110,28 +1110,60 @@ export class AgentManager {
1110
1110
  }
1111
1111
  return true;
1112
1112
  }
1113
+ async interruptSession(session, agentId) {
1114
+ try {
1115
+ const result = await this.waitWithTimeout({
1116
+ operation: session.interrupt(),
1117
+ timeoutMs: this.rescueTimeouts.interruptSessionMs,
1118
+ onLateError: (error) => {
1119
+ this.logger.warn({ err: error, agentId }, "Session interrupt failed after timeout during cancel");
1120
+ },
1121
+ });
1122
+ if (result === "timed_out") {
1123
+ this.logger.warn({ agentId, timeoutMs: this.rescueTimeouts.interruptSessionMs }, "Timed out interrupting session during cancel");
1124
+ }
1125
+ }
1126
+ catch (error) {
1127
+ this.logger.error({ err: error, agentId }, "Failed to interrupt session");
1128
+ }
1129
+ }
1113
1130
  getPendingPermissions(agentId) {
1114
- const agent = this.requireAgent(agentId);
1131
+ const agent = this.requireSessionAgent(agentId);
1115
1132
  return Array.from(agent.pendingPermissions.values());
1116
1133
  }
1117
1134
  peekPendingPermission(agent) {
1118
1135
  const iterator = agent.pendingPermissions.values().next();
1119
1136
  return iterator.done ? null : iterator.value;
1120
1137
  }
1138
+ /**
1139
+ * Hydrates the timeline from provider history if the agent's durable
1140
+ * timeline is empty (e.g., imported agents that have provider history
1141
+ * on disk but no persisted timeline rows). No-ops if already hydrated.
1142
+ */
1121
1143
  async hydrateTimelineFromProvider(agentId) {
1122
- const agent = this.requireAgent(agentId);
1123
- await this.hydrateTimeline(agent);
1144
+ const agent = this.requireSessionAgent(agentId);
1145
+ await this.hydrateTimelineFromLegacyProviderHistory(agent);
1124
1146
  }
1125
- getLastAssistantMessage(agentId) {
1147
+ async deleteCommittedTimeline(agentId) {
1148
+ if (!this.durableTimelineStore) {
1149
+ return;
1150
+ }
1151
+ await this.durableTimelineStore.deleteAgent(agentId);
1152
+ }
1153
+ async getLastAssistantMessage(agentId) {
1126
1154
  const agent = this.agents.get(agentId);
1127
1155
  if (!agent) {
1128
1156
  return null;
1129
1157
  }
1130
- return this.getLastAssistantMessageFromTimeline(agent.timeline);
1158
+ return await this.getLastAssistantMessageFromStores(agentId);
1131
1159
  }
1132
1160
  getLastAssistantMessageFromTimeline(timeline) {
1161
+ return this.getLastAssistantMessageSegmentFromTimeline(timeline)?.text ?? null;
1162
+ }
1163
+ getLastAssistantMessageSegmentFromTimeline(timeline) {
1133
1164
  // Collect the last contiguous assistant messages (Claude streams chunks)
1134
1165
  const chunks = [];
1166
+ let startsAtBeginning = false;
1135
1167
  for (let i = timeline.length - 1; i >= 0; i--) {
1136
1168
  const item = timeline[i];
1137
1169
  if (item.type !== "assistant_message") {
@@ -1141,11 +1173,53 @@ export class AgentManager {
1141
1173
  continue;
1142
1174
  }
1143
1175
  chunks.push(item.text);
1176
+ startsAtBeginning = i === 0;
1144
1177
  }
1145
1178
  if (!chunks.length) {
1146
1179
  return null;
1147
1180
  }
1148
- return chunks.reverse().join("");
1181
+ return {
1182
+ text: chunks.reverse().join(""),
1183
+ startsAtBeginning,
1184
+ };
1185
+ }
1186
+ async getLastAssistantMessageFromStores(agentId) {
1187
+ const liveTimeline = this.timelineStore.getItems(agentId);
1188
+ const liveSegment = this.getLastAssistantMessageSegmentFromTimeline(liveTimeline);
1189
+ if (!this.durableTimelineStore) {
1190
+ return liveSegment?.text ?? null;
1191
+ }
1192
+ if (!liveSegment) {
1193
+ return await this.durableTimelineStore.getLastAssistantMessage(agentId);
1194
+ }
1195
+ if (!liveSegment.startsAtBeginning) {
1196
+ return liveSegment.text;
1197
+ }
1198
+ const lastDurableItem = await this.durableTimelineStore.getLastItem(agentId);
1199
+ if (lastDurableItem?.type !== "assistant_message") {
1200
+ return liveSegment.text;
1201
+ }
1202
+ const durableMessage = await this.durableTimelineStore.getLastAssistantMessage(agentId);
1203
+ return durableMessage ? `${durableMessage}${liveSegment.text}` : liveSegment.text;
1204
+ }
1205
+ async getLastItemFromStores(agentId) {
1206
+ const lastLiveItem = this.timelineStore.getLastItem(agentId);
1207
+ if (lastLiveItem) {
1208
+ return lastLiveItem;
1209
+ }
1210
+ if (!this.durableTimelineStore) {
1211
+ return null;
1212
+ }
1213
+ return await this.durableTimelineStore.getLastItem(agentId);
1214
+ }
1215
+ async hasCommittedUserMessageFromStores(agentId, options) {
1216
+ if (this.timelineStore.hasCommittedUserMessage(agentId, options)) {
1217
+ return true;
1218
+ }
1219
+ if (!this.durableTimelineStore) {
1220
+ return false;
1221
+ }
1222
+ return await this.durableTimelineStore.hasCommittedUserMessage(agentId, options);
1149
1223
  }
1150
1224
  async waitForAgentEvent(agentId, options) {
1151
1225
  const snapshot = this.getAgent(agentId);
@@ -1159,7 +1233,7 @@ export class AgentManager {
1159
1233
  return {
1160
1234
  status: snapshot.lifecycle,
1161
1235
  permission: immediatePermission,
1162
- lastMessage: this.getLastAssistantMessage(agentId),
1236
+ lastMessage: await this.getLastAssistantMessage(agentId),
1163
1237
  };
1164
1238
  }
1165
1239
  const initialStatus = snapshot.lifecycle;
@@ -1169,14 +1243,14 @@ export class AgentManager {
1169
1243
  return {
1170
1244
  status: initialStatus,
1171
1245
  permission: null,
1172
- lastMessage: this.getLastAssistantMessage(agentId),
1246
+ lastMessage: await this.getLastAssistantMessage(agentId),
1173
1247
  };
1174
1248
  }
1175
1249
  if (waitForActive && !initialBusy && !hasForegroundTurn) {
1176
1250
  return {
1177
1251
  status: initialStatus,
1178
1252
  permission: null,
1179
- lastMessage: this.getLastAssistantMessage(agentId),
1253
+ lastMessage: await this.getLastAssistantMessage(agentId),
1180
1254
  };
1181
1255
  }
1182
1256
  if (options?.signal?.aborted) {
@@ -1194,6 +1268,7 @@ export class AgentManager {
1194
1268
  Boolean(snapshot.activeForegroundTurnId) ||
1195
1269
  Boolean(pendingForegroundRun?.started);
1196
1270
  let terminalStatusOverride = null;
1271
+ let finished = false;
1197
1272
  // Bug #3 Fix: Declare unsubscribe and abortHandler upfront so cleanup can reference them
1198
1273
  let unsubscribe = null;
1199
1274
  let abortHandler = null;
@@ -1220,12 +1295,20 @@ export class AgentManager {
1220
1295
  }
1221
1296
  };
1222
1297
  const finish = (permission) => {
1298
+ if (finished) {
1299
+ return;
1300
+ }
1301
+ finished = true;
1223
1302
  cleanup();
1224
- resolve({
1225
- status: currentStatus,
1226
- permission,
1227
- lastMessage: this.getLastAssistantMessage(agentId),
1228
- });
1303
+ void this.getLastAssistantMessage(agentId)
1304
+ .then((lastMessage) => {
1305
+ resolve({
1306
+ status: currentStatus,
1307
+ permission,
1308
+ lastMessage,
1309
+ });
1310
+ })
1311
+ .catch(reject);
1229
1312
  };
1230
1313
  // Bug #3 Fix: Set up abort handler BEFORE subscription
1231
1314
  // to ensure cleanup handlers exist before callback can fire
@@ -1285,14 +1368,30 @@ export class AgentManager {
1285
1368
  }
1286
1369
  const initialPersistedTitle = await this.resolveInitialPersistedTitle(resolvedAgentId, config);
1287
1370
  const now = new Date();
1288
- const initialTimeline = options?.timeline ? [...options.timeline] : [];
1289
- const initialTimelineRows = options?.timelineRows?.length
1290
- ? options.timelineRows.map((row) => ({ ...row }))
1291
- : this.buildTimelineRowsFromItems(initialTimeline, options?.timelineNextSeq ?? 1, (options?.updatedAt ?? options?.createdAt ?? now).toISOString());
1292
- const derivedNextSeq = options?.timelineNextSeq ??
1293
- (initialTimelineRows.length
1294
- ? initialTimelineRows[initialTimelineRows.length - 1].seq + 1
1295
- : 1);
1371
+ const explicitTimelineSeed = options?.timeline?.length ||
1372
+ options?.timelineRows?.length ||
1373
+ options?.timelineNextSeq !== undefined
1374
+ ? {
1375
+ items: options?.timeline,
1376
+ rows: options?.timelineRows,
1377
+ nextSeq: options?.timelineNextSeq,
1378
+ timestamp: (options?.updatedAt ?? options?.createdAt ?? now).toISOString(),
1379
+ }
1380
+ : null;
1381
+ const shouldSeedFromDurable = !explicitTimelineSeed &&
1382
+ !this.timelineStore.has(resolvedAgentId) &&
1383
+ this.durableTimelineStore !== undefined;
1384
+ const durableTimelineSeed = shouldSeedFromDurable
1385
+ ? await this.loadCommittedTimelineSeed(resolvedAgentId, now)
1386
+ : null;
1387
+ const durableTimelineHasRows = durableTimelineSeed != null && (durableTimelineSeed.nextSeq ?? 1) > 1;
1388
+ const timelineSeed = explicitTimelineSeed ?? durableTimelineSeed;
1389
+ if (timelineSeed || !this.timelineStore.has(resolvedAgentId)) {
1390
+ this.timelineStore.initialize(resolvedAgentId, timelineSeed ?? { timestamp: now.toISOString() });
1391
+ }
1392
+ if (options?.timelineRows?.length) {
1393
+ this.enqueueDurableTimelineBulkInsert(resolvedAgentId, options.timelineRows);
1394
+ }
1296
1395
  const managed = {
1297
1396
  id: resolvedAgentId,
1298
1397
  provider: config.provider,
@@ -1312,13 +1411,10 @@ export class AgentManager {
1312
1411
  pendingReplacement: false,
1313
1412
  activeForegroundTurnId: null,
1314
1413
  foregroundTurnWaiters: new Set(),
1414
+ finalizedForegroundTurnIds: new Set(),
1315
1415
  unsubscribeSession: null,
1316
- timeline: initialTimeline,
1317
- timelineRows: initialTimelineRows,
1318
- timelineEpoch: options?.timelineEpoch ?? randomUUID(),
1319
- timelineNextSeq: derivedNextSeq,
1320
1416
  persistence: attachPersistenceCwd(session.describePersistence(), config.cwd),
1321
- historyPrimed: options?.historyPrimed ?? false,
1417
+ historyPrimed: options?.historyPrimed ?? durableTimelineHasRows,
1322
1418
  lastUserMessageAt: options?.lastUserMessageAt ?? null,
1323
1419
  lastUsage: options?.lastUsage,
1324
1420
  lastError: options?.lastError,
@@ -1339,36 +1435,99 @@ export class AgentManager {
1339
1435
  this.previousStatuses.set(resolvedAgentId, managed.lifecycle);
1340
1436
  await this.refreshRuntimeInfo(managed);
1341
1437
  await this.persistSnapshot(managed, {
1438
+ workspaceId: options?.workspaceId,
1342
1439
  title: initialPersistedTitle,
1343
1440
  });
1344
- this.emitState(managed);
1441
+ this.emitState(managed, { persist: false });
1345
1442
  await this.refreshSessionState(managed);
1346
1443
  managed.lifecycle = "idle";
1347
- await this.persistSnapshot(managed);
1348
- this.emitState(managed);
1444
+ await this.persistSnapshot(managed, { workspaceId: options?.workspaceId });
1445
+ this.emitState(managed, { persist: false });
1349
1446
  this.subscribeToSession(managed);
1350
1447
  return { ...managed };
1351
1448
  }
1449
+ async loadCommittedTimelineSeed(agentId, now) {
1450
+ if (!this.durableTimelineStore) {
1451
+ return { timestamp: now.toISOString() };
1452
+ }
1453
+ return {
1454
+ nextSeq: (await this.durableTimelineStore.getLatestCommittedSeq(agentId)) + 1,
1455
+ timestamp: now.toISOString(),
1456
+ };
1457
+ }
1458
+ prepareAgentForClosure(agent, cancelReason) {
1459
+ this.agentStreamCoalescer.flushAndDiscard(agent.id);
1460
+ this.agents.delete(agent.id);
1461
+ this.previousStatuses.delete(agent.id);
1462
+ if (agent.unsubscribeSession) {
1463
+ agent.unsubscribeSession();
1464
+ agent.unsubscribeSession = null;
1465
+ }
1466
+ for (const waiter of agent.foregroundTurnWaiters) {
1467
+ waiter.callback({
1468
+ type: "turn_canceled",
1469
+ provider: agent.provider,
1470
+ reason: cancelReason,
1471
+ turnId: waiter.turnId,
1472
+ });
1473
+ this.settleForegroundTurnWaiter(waiter);
1474
+ }
1475
+ agent.foregroundTurnWaiters.clear();
1476
+ this.settlePendingForegroundRun(agent.id);
1477
+ return {
1478
+ ...agent,
1479
+ lifecycle: "closed",
1480
+ session: null,
1481
+ activeForegroundTurnId: null,
1482
+ };
1483
+ }
1484
+ emitClosedAgent(agent, options) {
1485
+ this.emitState(agent, options);
1486
+ }
1352
1487
  subscribeToSession(agent) {
1353
1488
  if (agent.unsubscribeSession) {
1354
1489
  return;
1355
1490
  }
1356
1491
  const agentId = agent.id;
1357
1492
  const unsubscribe = agent.session.subscribe((event) => {
1493
+ this.enqueueSessionEvent(agentId, event);
1494
+ });
1495
+ agent.unsubscribeSession = unsubscribe;
1496
+ }
1497
+ enqueueSessionEvent(agentId, event) {
1498
+ const previous = this.sessionEventTails.get(agentId) ?? Promise.resolve();
1499
+ const next = previous
1500
+ .catch(() => undefined)
1501
+ .then(async () => {
1358
1502
  const current = this.agents.get(agentId);
1359
1503
  if (!current) {
1360
1504
  return;
1361
1505
  }
1362
- this.dispatchSessionEvent(current, event);
1506
+ if (current.session == null) {
1507
+ return;
1508
+ }
1509
+ await this.dispatchSessionEvent(current, event);
1510
+ })
1511
+ .catch((err) => {
1512
+ this.logger.error({ err, agentId, eventType: event.type }, "Failed to process session event");
1513
+ });
1514
+ this.sessionEventTails.set(agentId, next);
1515
+ this.trackBackgroundTask(next);
1516
+ void next.finally(() => {
1517
+ if (this.sessionEventTails.get(agentId) === next) {
1518
+ this.sessionEventTails.delete(agentId);
1519
+ }
1363
1520
  });
1364
- agent.unsubscribeSession = unsubscribe;
1365
1521
  }
1366
- dispatchSessionEvent(agent, event) {
1522
+ async dispatchSessionEvent(agent, event) {
1367
1523
  const turnId = event.turnId;
1368
1524
  const matchingWaiters = turnId == null
1369
1525
  ? []
1370
1526
  : Array.from(agent.foregroundTurnWaiters).filter((waiter) => waiter.turnId === turnId && !waiter.settled);
1371
- this.handleStreamEvent(agent, event);
1527
+ const shouldNotifyWaiters = await this.handleStreamEvent(agent, event);
1528
+ if (!shouldNotifyWaiters) {
1529
+ return;
1530
+ }
1372
1531
  for (const waiter of matchingWaiters) {
1373
1532
  waiter.callback(event);
1374
1533
  if (isTurnTerminalEvent(event)) {
@@ -1383,6 +1542,16 @@ export class AgentManager {
1383
1542
  waiter.settled = true;
1384
1543
  waiter.resolveSettled();
1385
1544
  }
1545
+ rememberFinalizedForegroundTurn(agent, turnId) {
1546
+ agent.finalizedForegroundTurnIds.add(turnId);
1547
+ if (agent.finalizedForegroundTurnIds.size <= 50) {
1548
+ return;
1549
+ }
1550
+ const oldest = agent.finalizedForegroundTurnIds.values().next().value;
1551
+ if (oldest) {
1552
+ agent.finalizedForegroundTurnIds.delete(oldest);
1553
+ }
1554
+ }
1386
1555
  createPendingForegroundRun() {
1387
1556
  let resolveSettled;
1388
1557
  const settledPromise = new Promise((resolve) => {
@@ -1427,31 +1596,6 @@ export class AgentManager {
1427
1596
  }
1428
1597
  return null;
1429
1598
  }
1430
- buildTimelineRowsFromItems(items, startSeq, timestamp) {
1431
- let nextSeq = startSeq;
1432
- return items.map((item) => {
1433
- const row = {
1434
- seq: nextSeq,
1435
- timestamp,
1436
- item,
1437
- };
1438
- nextSeq += 1;
1439
- return row;
1440
- });
1441
- }
1442
- ensureTimelineState(agent) {
1443
- const minSeq = agent.timelineRows.length ? agent.timelineRows[0].seq : 0;
1444
- const maxSeq = agent.timelineRows.length
1445
- ? agent.timelineRows[agent.timelineRows.length - 1].seq
1446
- : 0;
1447
- return {
1448
- rows: agent.timelineRows,
1449
- epoch: agent.timelineEpoch,
1450
- nextSeq: agent.timelineNextSeq,
1451
- minSeq,
1452
- maxSeq,
1453
- };
1454
- }
1455
1599
  async persistSnapshot(agent, options) {
1456
1600
  if (!this.registry) {
1457
1601
  return;
@@ -1460,8 +1604,18 @@ export class AgentManager {
1460
1604
  if (agent.internal) {
1461
1605
  return;
1462
1606
  }
1607
+ if (options?.workspaceId !== undefined) {
1608
+ await this.registry.applySnapshot(agent, options.workspaceId, options);
1609
+ return;
1610
+ }
1463
1611
  await this.registry.applySnapshot(agent, options);
1464
1612
  }
1613
+ requireRegistry() {
1614
+ if (!this.registry) {
1615
+ throw new Error("Agent storage unavailable");
1616
+ }
1617
+ return this.registry;
1618
+ }
1465
1619
  async refreshSessionState(agent) {
1466
1620
  try {
1467
1621
  const modes = await agent.session.getAvailableModes();
@@ -1506,42 +1660,66 @@ export class AgentManager {
1506
1660
  // Keep existing runtimeInfo if refresh fails.
1507
1661
  }
1508
1662
  }
1509
- async hydrateTimeline(agent) {
1663
+ async hydrateTimelineFromLegacyProviderHistory(agent) {
1510
1664
  if (agent.historyPrimed) {
1511
1665
  return;
1512
1666
  }
1513
1667
  agent.historyPrimed = true;
1514
- const canonicalUserMessagesById = new Map(agent.timelineRows.flatMap((row) => {
1515
- if (row.item.type !== "user_message") {
1516
- return [];
1517
- }
1518
- const messageId = normalizeMessageId(row.item.messageId);
1519
- if (!messageId) {
1520
- return [];
1521
- }
1522
- return [[messageId, row.item.text]];
1523
- }));
1668
+ const canonicalUserMessagesById = this.timelineStore.getCanonicalUserMessagesById(agent.id);
1524
1669
  try {
1525
1670
  for await (const event of agent.session.streamHistory()) {
1526
- this.handleStreamEvent(agent, event, {
1527
- fromHistory: true,
1528
- canonicalUserMessagesById: canonicalUserMessagesById.size > 0 ? canonicalUserMessagesById : undefined,
1529
- });
1671
+ if (event.type !== "timeline") {
1672
+ continue;
1673
+ }
1674
+ if (event.item.type === "user_message") {
1675
+ const eventMessageId = normalizeMessageId(event.item.messageId);
1676
+ if (eventMessageId) {
1677
+ const canonicalText = canonicalUserMessagesById.get(eventMessageId);
1678
+ if (canonicalText === event.item.text) {
1679
+ continue;
1680
+ }
1681
+ }
1682
+ }
1683
+ this.recordTimeline(agent.id, event.item);
1530
1684
  }
1531
1685
  }
1532
1686
  catch {
1533
1687
  // ignore history failures
1534
1688
  }
1535
1689
  }
1536
- handleStreamEvent(agent, event, options) {
1690
+ notifyForegroundTurnWaiters(agentId, event) {
1691
+ const turnId = event.turnId;
1692
+ if (turnId == null) {
1693
+ return;
1694
+ }
1695
+ const agent = this.agents.get(agentId);
1696
+ if (!agent) {
1697
+ return;
1698
+ }
1699
+ for (const waiter of agent.foregroundTurnWaiters) {
1700
+ if (waiter.turnId === turnId && !waiter.settled) {
1701
+ waiter.callback(event);
1702
+ }
1703
+ }
1704
+ }
1705
+ async handleStreamEvent(agent, event, options) {
1537
1706
  const eventTurnId = event.turnId;
1538
1707
  const isForegroundEvent = Boolean(eventTurnId && agent.activeForegroundTurnId === eventTurnId);
1708
+ if (eventTurnId &&
1709
+ isTurnTerminalEvent(event) &&
1710
+ agent.finalizedForegroundTurnIds.has(eventTurnId)) {
1711
+ return false;
1712
+ }
1539
1713
  // Only update timestamp for live events, not history replay
1540
1714
  if (!options?.fromHistory) {
1541
1715
  this.touchUpdatedAt(agent);
1716
+ if (this.agentStreamCoalescer.handle(agent.id, event)) {
1717
+ return false;
1718
+ }
1719
+ this.agentStreamCoalescer.flushFor(agent.id);
1542
1720
  }
1543
- let timelineRow = null;
1544
1721
  let shouldDispatchEvent = true;
1722
+ let shouldNotifyWaiters = true;
1545
1723
  switch (event.type) {
1546
1724
  case "thread_started":
1547
1725
  {
@@ -1561,38 +1739,43 @@ export class AgentManager {
1561
1739
  this.emitState(agent);
1562
1740
  break;
1563
1741
  case "timeline":
1564
- // Skip provider-replayed user_message items during history hydration.
1565
- if (options?.fromHistory && event.item.type === "user_message") {
1566
- const eventMessageId = normalizeMessageId(event.item.messageId);
1567
- if (eventMessageId) {
1568
- const canonicalText = options?.canonicalUserMessagesById?.get(eventMessageId);
1569
- if (canonicalText === event.item.text) {
1570
- break;
1742
+ {
1743
+ // Skip provider-replayed user_message items during history hydration.
1744
+ if (options?.fromHistory && event.item.type === "user_message") {
1745
+ const eventMessageId = normalizeMessageId(event.item.messageId);
1746
+ if (eventMessageId) {
1747
+ const canonicalText = options?.canonicalUserMessagesById?.get(eventMessageId);
1748
+ if (canonicalText === event.item.text) {
1749
+ shouldDispatchEvent = false;
1750
+ shouldNotifyWaiters = false;
1751
+ break;
1752
+ }
1571
1753
  }
1572
1754
  }
1573
- }
1574
- // Suppress user_message echoes for the active foreground turn —
1575
- // these are already recorded by recordUserMessage().
1576
- if (!options?.fromHistory && event.item.type === "user_message" && isForegroundEvent) {
1577
- const eventMessageId = normalizeMessageId(event.item.messageId);
1578
- const eventText = event.item.text;
1579
- if (eventMessageId) {
1580
- const alreadyRecorded = agent.timelineRows.some((row) => {
1581
- if (row.item.type !== "user_message") {
1582
- return false;
1583
- }
1584
- const rowMessageId = normalizeMessageId(row.item.messageId);
1585
- return rowMessageId === eventMessageId && row.item.text === eventText;
1586
- });
1587
- if (alreadyRecorded) {
1755
+ // Suppress user_message echoes for the active foreground turn.
1756
+ if (!options?.fromHistory && event.item.type === "user_message" && isForegroundEvent) {
1757
+ const eventMessageId = normalizeMessageId(event.item.messageId);
1758
+ if (eventMessageId &&
1759
+ (await this.hasCommittedUserMessageFromStores(agent.id, {
1760
+ messageId: eventMessageId,
1761
+ text: event.item.text,
1762
+ }))) {
1588
1763
  break;
1589
1764
  }
1590
1765
  }
1591
- }
1592
- timelineRow = this.recordTimeline(agent, event.item);
1593
- if (!options?.fromHistory && event.item.type === "user_message") {
1594
- agent.lastUserMessageAt = new Date();
1595
- this.emitState(agent);
1766
+ if (options?.fromHistory) {
1767
+ this.recordTimeline(agent.id, event.item);
1768
+ shouldDispatchEvent = false;
1769
+ shouldNotifyWaiters = false;
1770
+ break;
1771
+ }
1772
+ this.recordAndDispatchTimelineItem(agent.id, event.item, event.provider, event.turnId);
1773
+ if (event.item.type === "user_message") {
1774
+ agent.lastUserMessageAt = new Date();
1775
+ this.emitState(agent);
1776
+ }
1777
+ shouldDispatchEvent = false;
1778
+ shouldNotifyWaiters = true;
1596
1779
  }
1597
1780
  break;
1598
1781
  case "turn_completed":
@@ -1627,7 +1810,7 @@ export class AgentManager {
1627
1810
  agent.lifecycle = "error";
1628
1811
  }
1629
1812
  agent.lastError = event.error;
1630
- this.appendSystemErrorTimelineMessage(agent, event.provider, this.formatTurnFailedMessage(event), options);
1813
+ await this.appendSystemErrorTimelineMessage(agent, event.provider, this.formatTurnFailedMessage(event), options);
1631
1814
  for (const [requestId] of agent.pendingPermissions) {
1632
1815
  agent.pendingPermissions.delete(requestId);
1633
1816
  if (!options?.fromHistory) {
@@ -1707,19 +1890,29 @@ export class AgentManager {
1707
1890
  break;
1708
1891
  }
1709
1892
  if (!options?.fromHistory && isForegroundEvent && isTurnTerminalEvent(event)) {
1710
- this.finalizeForegroundTurn(agent);
1893
+ this.finalizeForegroundTurn(agent, eventTurnId);
1711
1894
  }
1712
1895
  // Skip dispatching individual stream events during history replay.
1713
1896
  if (!options?.fromHistory && shouldDispatchEvent) {
1714
- this.dispatchStream(agent.id, event, timelineRow
1715
- ? {
1716
- seq: timelineRow.seq,
1717
- epoch: this.ensureTimelineState(agent).epoch,
1718
- }
1719
- : undefined);
1897
+ this.dispatchStream(agent.id, event);
1720
1898
  }
1899
+ return shouldNotifyWaiters;
1900
+ }
1901
+ recordAndDispatchTimelineItem(agentId, item, provider, turnId) {
1902
+ const row = this.recordTimeline(agentId, item);
1903
+ const event = {
1904
+ type: "timeline",
1905
+ item,
1906
+ provider,
1907
+ ...(turnId !== undefined ? { turnId } : {}),
1908
+ };
1909
+ this.dispatchStream(agentId, event, {
1910
+ seq: row.seq,
1911
+ epoch: this.timelineStore.getEpoch(agentId),
1912
+ });
1913
+ return event;
1721
1914
  }
1722
- appendSystemErrorTimelineMessage(agent, provider, message, options) {
1915
+ async appendSystemErrorTimelineMessage(agent, provider, message, options) {
1723
1916
  if (options?.fromHistory) {
1724
1917
  return;
1725
1918
  }
@@ -1728,19 +1921,19 @@ export class AgentManager {
1728
1921
  return;
1729
1922
  }
1730
1923
  const text = `${SYSTEM_ERROR_PREFIX} ${normalized}`;
1731
- const lastItem = agent.timelineRows[agent.timelineRows.length - 1]?.item;
1924
+ const lastItem = await this.getLastItemFromStores(agent.id);
1732
1925
  if (lastItem?.type === "assistant_message" && lastItem.text === text) {
1733
1926
  return;
1734
1927
  }
1735
1928
  const item = { type: "assistant_message", text };
1736
- const row = this.recordTimeline(agent, item);
1929
+ const row = this.recordTimeline(agent.id, item);
1737
1930
  this.dispatchStream(agent.id, {
1738
1931
  type: "timeline",
1739
1932
  item,
1740
1933
  provider,
1741
1934
  }, {
1742
1935
  seq: row.seq,
1743
- epoch: this.ensureTimelineState(agent).epoch,
1936
+ epoch: this.timelineStore.getEpoch(agent.id),
1744
1937
  });
1745
1938
  }
1746
1939
  formatTurnFailedMessage(event) {
@@ -1756,27 +1949,17 @@ export class AgentManager {
1756
1949
  }
1757
1950
  return parts.join("\n\n");
1758
1951
  }
1759
- recordTimeline(agent, item) {
1760
- const timelineState = this.ensureTimelineState(agent);
1761
- const row = {
1762
- seq: timelineState.nextSeq,
1763
- timestamp: new Date().toISOString(),
1764
- item,
1765
- };
1766
- agent.timelineNextSeq = timelineState.nextSeq + 1;
1767
- agent.timeline.push(item);
1768
- timelineState.rows.push(row);
1769
- if (typeof this.maxTimelineItems === "number" &&
1770
- agent.timeline.length > this.maxTimelineItems) {
1771
- const removeCount = agent.timeline.length - this.maxTimelineItems;
1772
- agent.timeline.splice(0, removeCount);
1773
- timelineState.rows.splice(0, removeCount);
1774
- }
1952
+ recordTimeline(agentId, item) {
1953
+ const row = this.timelineStore.append(agentId, item);
1954
+ this.enqueueDurableTimelineAppend(agentId, row);
1775
1955
  return row;
1776
1956
  }
1777
- emitState(agent) {
1957
+ emitState(agent, options) {
1778
1958
  // Keep attention as an edge-triggered unread signal, not a level signal.
1779
1959
  this.checkAndSetAttention(agent);
1960
+ if (options?.persist !== false) {
1961
+ this.enqueueBackgroundPersist(agent);
1962
+ }
1780
1963
  this.syncFeaturesFromSession(agent);
1781
1964
  this.dispatch({
1782
1965
  type: "agent_state",
@@ -1809,7 +1992,6 @@ export class AgentManager {
1809
1992
  attentionTimestamp: new Date(),
1810
1993
  };
1811
1994
  this.broadcastAgentAttention(agent, "finished");
1812
- this.enqueueBackgroundPersist(agent);
1813
1995
  return;
1814
1996
  }
1815
1997
  // Check if agent entered error state
@@ -1820,7 +2002,6 @@ export class AgentManager {
1820
2002
  attentionTimestamp: new Date(),
1821
2003
  };
1822
2004
  this.broadcastAgentAttention(agent, "error");
1823
- this.enqueueBackgroundPersist(agent);
1824
2005
  return;
1825
2006
  }
1826
2007
  }
@@ -1830,6 +2011,27 @@ export class AgentManager {
1830
2011
  });
1831
2012
  this.trackBackgroundTask(task);
1832
2013
  }
2014
+ enqueueDurableTimelineAppend(agentId, row) {
2015
+ if (!this.durableTimelineStore) {
2016
+ return;
2017
+ }
2018
+ const task = this.durableTimelineStore
2019
+ .bulkInsert(agentId, [row])
2020
+ .then(() => undefined)
2021
+ .catch((err) => {
2022
+ this.logger.error({ err, agentId, seq: row.seq, itemType: row.item.type }, "Failed to append timeline row to durable store");
2023
+ });
2024
+ this.trackBackgroundTask(task);
2025
+ }
2026
+ enqueueDurableTimelineBulkInsert(agentId, rows) {
2027
+ if (!this.durableTimelineStore || rows.length === 0) {
2028
+ return;
2029
+ }
2030
+ const task = this.durableTimelineStore.bulkInsert(agentId, rows).catch((err) => {
2031
+ this.logger.error({ err, agentId, rowCount: rows.length }, "Failed to seed durable timeline store");
2032
+ });
2033
+ this.trackBackgroundTask(task);
2034
+ }
1833
2035
  trackBackgroundTask(task) {
1834
2036
  this.backgroundTasks.add(task);
1835
2037
  void task.finally(() => {
@@ -1841,6 +2043,7 @@ export class AgentManager {
1841
2043
  * Used by daemon shutdown paths to avoid unhandled rejections after cleanup.
1842
2044
  */
1843
2045
  async flush() {
2046
+ this.agentStreamCoalescer.flushAll();
1844
2047
  // Drain tasks, including tasks spawned while awaiting.
1845
2048
  while (this.backgroundTasks.size > 0) {
1846
2049
  const pending = Array.from(this.backgroundTasks);
@@ -1915,7 +2118,7 @@ export class AgentManager {
1915
2118
  const client = this.clients.get(normalized.provider);
1916
2119
  if (client) {
1917
2120
  try {
1918
- const models = await client.listModels();
2121
+ const models = await client.listModels({ cwd: normalized.cwd, force: false });
1919
2122
  const defaultModel = models.find((model) => model.isDefault) ?? models[0];
1920
2123
  if (defaultModel) {
1921
2124
  normalized.model = defaultModel.id;
@@ -1959,5 +2162,12 @@ export class AgentManager {
1959
2162
  }
1960
2163
  return agent;
1961
2164
  }
2165
+ requireSessionAgent(id) {
2166
+ const agent = this.requireAgent(id);
2167
+ if (agent.session === null) {
2168
+ throw new Error(`Agent '${agent.id}' has no managed session`);
2169
+ }
2170
+ return agent;
2171
+ }
1962
2172
  }
1963
2173
  //# sourceMappingURL=agent-manager.js.map