devchain-cli 0.22.1 → 0.23.1

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 (249) hide show
  1. package/dist/drizzle/0077_familiar_morbius.sql +6 -0
  2. package/dist/drizzle/0078_preserve_legacy_connection_ownership.sql +180 -0
  3. package/dist/drizzle/0079_premium_forgotten_one.sql +18 -0
  4. package/dist/drizzle/0080_lethal_thunderbolt_ross.sql +49 -0
  5. package/dist/drizzle/0081_early_kronos.sql +58 -0
  6. package/dist/drizzle/0082_huge_black_queen.sql +14 -0
  7. package/dist/drizzle/meta/0077_snapshot.json +6957 -0
  8. package/dist/drizzle/meta/0078_snapshot.json +6957 -0
  9. package/dist/drizzle/meta/0079_snapshot.json +7093 -0
  10. package/dist/drizzle/meta/0080_snapshot.json +7241 -0
  11. package/dist/drizzle/meta/0081_snapshot.json +7482 -0
  12. package/dist/drizzle/meta/0082_snapshot.json +7571 -0
  13. package/dist/drizzle/meta/_journal.json +42 -0
  14. package/dist/node_modules/@devchain/shared/tsconfig.tsbuildinfo +1 -1
  15. package/dist/server/common/errors/error-types.d.ts +7 -0
  16. package/dist/server/common/errors/error-types.js +7 -1
  17. package/dist/server/common/errors/error-types.js.map +1 -1
  18. package/dist/server/modules/epic-time/controllers/agent-time-buffer.controller.d.ts +9 -0
  19. package/dist/server/modules/epic-time/controllers/agent-time-buffer.controller.js +90 -0
  20. package/dist/server/modules/epic-time/controllers/agent-time-buffer.controller.js.map +1 -0
  21. package/dist/server/modules/epic-time/controllers/external-estimate-log.controller.d.ts +12 -0
  22. package/dist/server/modules/epic-time/controllers/external-estimate-log.controller.js +322 -0
  23. package/dist/server/modules/epic-time/controllers/external-estimate-log.controller.js.map +1 -0
  24. package/dist/server/modules/epic-time/epic-time-store.module.d.ts +2 -0
  25. package/dist/server/modules/epic-time/epic-time-store.module.js +23 -0
  26. package/dist/server/modules/epic-time/epic-time-store.module.js.map +1 -0
  27. package/dist/server/modules/epic-time/epic-time.module.js +10 -6
  28. package/dist/server/modules/epic-time/epic-time.module.js.map +1 -1
  29. package/dist/server/modules/epic-time/models/epic-time-daily-allocator.d.ts +30 -0
  30. package/dist/server/modules/epic-time/models/epic-time-daily-allocator.js +125 -0
  31. package/dist/server/modules/epic-time/models/epic-time-daily-allocator.js.map +1 -0
  32. package/dist/server/modules/epic-time/models/epic-time-local-day.d.ts +9 -0
  33. package/dist/server/modules/epic-time/models/epic-time-local-day.js +164 -0
  34. package/dist/server/modules/epic-time/models/epic-time-local-day.js.map +1 -0
  35. package/dist/server/modules/epic-time/models/epic-time.models.d.ts +147 -0
  36. package/dist/server/modules/epic-time/models/epic-time.models.js +2 -0
  37. package/dist/server/modules/epic-time/models/epic-time.models.js.map +1 -1
  38. package/dist/server/modules/epic-time/services/agent-time-accounting.service.d.ts +1 -0
  39. package/dist/server/modules/epic-time/services/agent-time-accounting.service.js +31 -8
  40. package/dist/server/modules/epic-time/services/agent-time-accounting.service.js.map +1 -1
  41. package/dist/server/modules/epic-time/services/epic-estimate-logging.service.d.ts +37 -0
  42. package/dist/server/modules/epic-time/services/epic-estimate-logging.service.js +716 -0
  43. package/dist/server/modules/epic-time/services/epic-estimate-logging.service.js.map +1 -0
  44. package/dist/server/modules/epic-time/services/epic-time.service.d.ts +13 -3
  45. package/dist/server/modules/epic-time/services/epic-time.service.js +162 -69
  46. package/dist/server/modules/epic-time/services/epic-time.service.js.map +1 -1
  47. package/dist/server/modules/epic-time/services/epic-time.store.d.ts +50 -3
  48. package/dist/server/modules/epic-time/services/epic-time.store.js +705 -146
  49. package/dist/server/modules/epic-time/services/epic-time.store.js.map +1 -1
  50. package/dist/server/modules/epics/controllers/epics.controller.d.ts +10 -1
  51. package/dist/server/modules/epics/controllers/epics.controller.js +108 -2
  52. package/dist/server/modules/epics/controllers/epics.controller.js.map +1 -1
  53. package/dist/server/modules/epics/epics.module.js +8 -2
  54. package/dist/server/modules/epics/epics.module.js.map +1 -1
  55. package/dist/server/modules/epics/models/epic-relations.models.d.ts +34 -0
  56. package/dist/server/modules/epics/models/epic-relations.models.js +16 -0
  57. package/dist/server/modules/epics/models/epic-relations.models.js.map +1 -0
  58. package/dist/server/modules/epics/services/epic-relation-target-resolver.d.ts +8 -0
  59. package/dist/server/modules/epics/services/epic-relation-target-resolver.js +37 -0
  60. package/dist/server/modules/epics/services/epic-relation-target-resolver.js.map +1 -0
  61. package/dist/server/modules/epics/services/epic-relations.service.d.ts +52 -0
  62. package/dist/server/modules/epics/services/epic-relations.service.js +135 -0
  63. package/dist/server/modules/epics/services/epic-relations.service.js.map +1 -0
  64. package/dist/server/modules/epics/services/epics.service.d.ts +9 -2
  65. package/dist/server/modules/epics/services/epics.service.js +115 -11
  66. package/dist/server/modules/epics/services/epics.service.js.map +1 -1
  67. package/dist/server/modules/events/catalog/broadcast-registry.js +16 -0
  68. package/dist/server/modules/events/catalog/broadcast-registry.js.map +1 -1
  69. package/dist/server/modules/events/catalog/epic.relations.invalidated.d.ts +12 -0
  70. package/dist/server/modules/events/catalog/epic.relations.invalidated.js +13 -0
  71. package/dist/server/modules/events/catalog/epic.relations.invalidated.js.map +1 -0
  72. package/dist/server/modules/events/catalog/epic.time.scope.invalidated.d.ts +12 -0
  73. package/dist/server/modules/events/catalog/epic.time.scope.invalidated.js +13 -0
  74. package/dist/server/modules/events/catalog/epic.time.scope.invalidated.js.map +1 -0
  75. package/dist/server/modules/events/catalog/index.d.ts +27 -4
  76. package/dist/server/modules/events/catalog/index.js +6 -0
  77. package/dist/server/modules/events/catalog/index.js.map +1 -1
  78. package/dist/server/modules/events/catalog/integration.connection.created.d.ts +4 -1
  79. package/dist/server/modules/events/catalog/integration.connection.created.js +5 -2
  80. package/dist/server/modules/events/catalog/integration.connection.created.js.map +1 -1
  81. package/dist/server/modules/events/catalog/integration.connection.deleted.d.ts +4 -1
  82. package/dist/server/modules/events/catalog/integration.connection.deleted.js +5 -2
  83. package/dist/server/modules/events/catalog/integration.connection.deleted.js.map +1 -1
  84. package/dist/server/modules/events/catalog/integration.connection.legacy.d.ts +85 -0
  85. package/dist/server/modules/events/catalog/integration.connection.legacy.js +41 -0
  86. package/dist/server/modules/events/catalog/integration.connection.legacy.js.map +1 -0
  87. package/dist/server/modules/events/catalog/integration.connection.updated.d.ts +4 -1
  88. package/dist/server/modules/events/catalog/integration.connection.updated.js +5 -2
  89. package/dist/server/modules/events/catalog/integration.connection.updated.js.map +1 -1
  90. package/dist/server/modules/events/services/committed-event-replay.d.ts +10 -0
  91. package/dist/server/modules/events/services/committed-event-replay.js +30 -0
  92. package/dist/server/modules/events/services/committed-event-replay.js.map +1 -0
  93. package/dist/server/modules/events/services/committed-event.store.d.ts +1 -0
  94. package/dist/server/modules/events/services/committed-event.store.js +12 -7
  95. package/dist/server/modules/events/services/committed-event.store.js.map +1 -1
  96. package/dist/server/modules/events/services/durable-event-registry.service.d.ts +9 -2
  97. package/dist/server/modules/events/services/durable-event-registry.service.js.map +1 -1
  98. package/dist/server/modules/external-integrations/adapters/clickup-external-task.provider.d.ts +7 -0
  99. package/dist/server/modules/external-integrations/adapters/clickup-external-task.provider.js +158 -50
  100. package/dist/server/modules/external-integrations/adapters/clickup-external-task.provider.js.map +1 -1
  101. package/dist/server/modules/external-integrations/adapters/jira-external-task.provider.d.ts +4 -0
  102. package/dist/server/modules/external-integrations/adapters/jira-external-task.provider.js +73 -33
  103. package/dist/server/modules/external-integrations/adapters/jira-external-task.provider.js.map +1 -1
  104. package/dist/server/modules/external-integrations/connections/integration-connections.controller.d.ts +6 -3
  105. package/dist/server/modules/external-integrations/connections/integration-connections.controller.js +67 -12
  106. package/dist/server/modules/external-integrations/connections/integration-connections.controller.js.map +1 -1
  107. package/dist/server/modules/external-integrations/connections/integration-connections.service.d.ts +35 -3
  108. package/dist/server/modules/external-integrations/connections/integration-connections.service.js +178 -17
  109. package/dist/server/modules/external-integrations/connections/integration-connections.service.js.map +1 -1
  110. package/dist/server/modules/external-integrations/connections/managed-subtask-sync.controller.d.ts +6 -3
  111. package/dist/server/modules/external-integrations/connections/managed-subtask-sync.controller.js +51 -11
  112. package/dist/server/modules/external-integrations/connections/managed-subtask-sync.controller.js.map +1 -1
  113. package/dist/server/modules/external-integrations/external-integrations.module.js +6 -1
  114. package/dist/server/modules/external-integrations/external-integrations.module.js.map +1 -1
  115. package/dist/server/modules/external-integrations/models/external-edit-session.models.d.ts +1 -0
  116. package/dist/server/modules/external-integrations/models/external-provider.models.d.ts +6 -2
  117. package/dist/server/modules/external-integrations/models/external-provider.models.js +2 -3
  118. package/dist/server/modules/external-integrations/models/external-provider.models.js.map +1 -1
  119. package/dist/server/modules/external-integrations/models/external-time-mutation.models.d.ts +33 -2
  120. package/dist/server/modules/external-integrations/models/external-time-mutation.models.js +14 -0
  121. package/dist/server/modules/external-integrations/models/external-time-mutation.models.js.map +1 -1
  122. package/dist/server/modules/external-integrations/my-work/external-edit-session.service.d.ts +10 -9
  123. package/dist/server/modules/external-integrations/my-work/external-edit-session.service.js +81 -46
  124. package/dist/server/modules/external-integrations/my-work/external-edit-session.service.js.map +1 -1
  125. package/dist/server/modules/external-integrations/my-work/external-my-work.controller.d.ts +22 -20
  126. package/dist/server/modules/external-integrations/my-work/external-my-work.controller.js +136 -64
  127. package/dist/server/modules/external-integrations/my-work/external-my-work.controller.js.map +1 -1
  128. package/dist/server/modules/external-integrations/my-work/external-my-work.openapi.d.ts +7 -0
  129. package/dist/server/modules/external-integrations/my-work/external-my-work.openapi.js +289 -4
  130. package/dist/server/modules/external-integrations/my-work/external-my-work.openapi.js.map +1 -1
  131. package/dist/server/modules/external-integrations/my-work/external-my-work.service.d.ts +12 -7
  132. package/dist/server/modules/external-integrations/my-work/external-my-work.service.js +114 -54
  133. package/dist/server/modules/external-integrations/my-work/external-my-work.service.js.map +1 -1
  134. package/dist/server/modules/external-integrations/my-work/external-time-mutation.service.d.ts +19 -6
  135. package/dist/server/modules/external-integrations/my-work/external-time-mutation.service.js +346 -46
  136. package/dist/server/modules/external-integrations/my-work/external-time-mutation.service.js.map +1 -1
  137. package/dist/server/modules/external-integrations/sessions/external-edit-session.store.d.ts +2 -1
  138. package/dist/server/modules/external-integrations/sessions/external-edit-session.store.js +3 -2
  139. package/dist/server/modules/external-integrations/sessions/external-edit-session.store.js.map +1 -1
  140. package/dist/server/modules/external-integrations/sessions/external-time-mutation.store.d.ts +6 -1
  141. package/dist/server/modules/external-integrations/sessions/external-time-mutation.store.js +19 -0
  142. package/dist/server/modules/external-integrations/sessions/external-time-mutation.store.js.map +1 -1
  143. package/dist/server/modules/external-integrations/sessions/provider-operation-gate.d.ts +15 -2
  144. package/dist/server/modules/external-integrations/sessions/provider-operation-gate.js +33 -6
  145. package/dist/server/modules/external-integrations/sessions/provider-operation-gate.js.map +1 -1
  146. package/dist/server/modules/external-integrations/subscribers/external-subtask-sync.subscriber.d.ts +13 -6
  147. package/dist/server/modules/external-integrations/subscribers/external-subtask-sync.subscriber.js +212 -75
  148. package/dist/server/modules/external-integrations/subscribers/external-subtask-sync.subscriber.js.map +1 -1
  149. package/dist/server/modules/external-integrations/subscribers/managed-subtask-content.d.ts +13 -4
  150. package/dist/server/modules/external-integrations/subscribers/managed-subtask-content.js +98 -0
  151. package/dist/server/modules/external-integrations/subscribers/managed-subtask-content.js.map +1 -1
  152. package/dist/server/modules/external-integrations/subscribers/managed-subtask-sync-health.service.d.ts +10 -4
  153. package/dist/server/modules/external-integrations/subscribers/managed-subtask-sync-health.service.js +57 -18
  154. package/dist/server/modules/external-integrations/subscribers/managed-subtask-sync-health.service.js.map +1 -1
  155. package/dist/server/modules/mcp/dtos/mcp.dto.d.ts +139 -0
  156. package/dist/server/modules/mcp/dtos/mcp.dto.js +41 -1
  157. package/dist/server/modules/mcp/dtos/mcp.dto.js.map +1 -1
  158. package/dist/server/modules/mcp/services/handlers/epic-context.d.ts +2 -0
  159. package/dist/server/modules/mcp/services/handlers/epic-tools.d.ts +4 -0
  160. package/dist/server/modules/mcp/services/handlers/epic-tools.js +275 -0
  161. package/dist/server/modules/mcp/services/handlers/epic-tools.js.map +1 -1
  162. package/dist/server/modules/mcp/services/mcp-tool-binding.registry.d.ts +2 -1
  163. package/dist/server/modules/mcp/services/mcp-tool-binding.registry.js +13 -8
  164. package/dist/server/modules/mcp/services/mcp-tool-binding.registry.js.map +1 -1
  165. package/dist/server/modules/mcp/services/testing/mcp-tool-binding-registry.fixture.d.ts +1 -1
  166. package/dist/server/modules/mcp/services/testing/mcp-tool-binding-registry.fixture.js +1 -1
  167. package/dist/server/modules/mcp/services/testing/mcp-tool-binding-registry.fixture.js.map +1 -1
  168. package/dist/server/modules/mcp/tool-descriptors/binding-types.d.ts +2 -0
  169. package/dist/server/modules/mcp/tool-descriptors/binding-types.js.map +1 -1
  170. package/dist/server/modules/mcp/tool-descriptors/epic.bindings.js +6 -0
  171. package/dist/server/modules/mcp/tool-descriptors/epic.bindings.js.map +1 -1
  172. package/dist/server/modules/mcp/tool-descriptors/epic.metadata.js +132 -0
  173. package/dist/server/modules/mcp/tool-descriptors/epic.metadata.js.map +1 -1
  174. package/dist/server/modules/projects/controllers/projects.controller.js +1 -4
  175. package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
  176. package/dist/server/modules/projects/projects.module.js +2 -0
  177. package/dist/server/modules/projects/projects.module.js.map +1 -1
  178. package/dist/server/modules/projects/services/projects.service.d.ts +4 -1
  179. package/dist/server/modules/projects/services/projects.service.js +23 -2
  180. package/dist/server/modules/projects/services/projects.service.js.map +1 -1
  181. package/dist/server/modules/providers/adapters/claude.adapter.d.ts +4 -2
  182. package/dist/server/modules/providers/adapters/claude.adapter.js +31 -0
  183. package/dist/server/modules/providers/adapters/claude.adapter.js.map +1 -1
  184. package/dist/server/modules/providers/services/provider-mcp-ensure.service.d.ts +7 -0
  185. package/dist/server/modules/providers/services/provider-mcp-ensure.service.js +69 -4
  186. package/dist/server/modules/providers/services/provider-mcp-ensure.service.js.map +1 -1
  187. package/dist/server/modules/session-reader/data/pricing.json +452 -56
  188. package/dist/server/modules/session-reader/services/session-reader.service.d.ts +2 -0
  189. package/dist/server/modules/session-reader/services/session-reader.service.js +85 -9
  190. package/dist/server/modules/session-reader/services/session-reader.service.js.map +1 -1
  191. package/dist/server/modules/session-reader/services/transcript-path-validator.service.js +8 -2
  192. package/dist/server/modules/session-reader/services/transcript-path-validator.service.js.map +1 -1
  193. package/dist/server/modules/session-reader/services/transcript-watcher.service.js +44 -17
  194. package/dist/server/modules/session-reader/services/transcript-watcher.service.js.map +1 -1
  195. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.d.ts +1 -0
  196. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js +1 -0
  197. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js.map +1 -1
  198. package/dist/server/modules/sessions/services/session-runtime/session-launch-pipeline.service.js +11 -1
  199. package/dist/server/modules/sessions/services/session-runtime/session-launch-pipeline.service.js.map +1 -1
  200. package/dist/server/modules/sessions/services/sessions.service.d.ts +3 -1
  201. package/dist/server/modules/sessions/services/sessions.service.js +36 -6
  202. package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
  203. package/dist/server/modules/sessions/sessions.module.js +2 -0
  204. package/dist/server/modules/sessions/sessions.module.js.map +1 -1
  205. package/dist/server/modules/sessions/utils/claude-config.d.ts +4 -1
  206. package/dist/server/modules/sessions/utils/claude-config.js +265 -60
  207. package/dist/server/modules/sessions/utils/claude-config.js.map +1 -1
  208. package/dist/server/modules/storage/db/schema.d.ts +1109 -98
  209. package/dist/server/modules/storage/db/schema.js +141 -3
  210. package/dist/server/modules/storage/db/schema.js.map +1 -1
  211. package/dist/server/modules/storage/db/sqlite-json.d.ts +1 -0
  212. package/dist/server/modules/storage/db/sqlite-json.js +4 -0
  213. package/dist/server/modules/storage/db/sqlite-json.js.map +1 -1
  214. package/dist/server/modules/storage/interfaces/storage.interface.d.ts +58 -10
  215. package/dist/server/modules/storage/interfaces/storage.interface.js.map +1 -1
  216. package/dist/server/modules/storage/local/delegates/epic.delegate.d.ts +30 -5
  217. package/dist/server/modules/storage/local/delegates/epic.delegate.js +718 -14
  218. package/dist/server/modules/storage/local/delegates/epic.delegate.js.map +1 -1
  219. package/dist/server/modules/storage/local/delegates/external-estimate-log.delegate.d.ts +52 -0
  220. package/dist/server/modules/storage/local/delegates/external-estimate-log.delegate.js +659 -0
  221. package/dist/server/modules/storage/local/delegates/external-estimate-log.delegate.js.map +1 -0
  222. package/dist/server/modules/storage/local/delegates/external-managed-subtask.delegate.d.ts +2 -1
  223. package/dist/server/modules/storage/local/delegates/external-managed-subtask.delegate.js +17 -4
  224. package/dist/server/modules/storage/local/delegates/external-managed-subtask.delegate.js.map +1 -1
  225. package/dist/server/modules/storage/local/delegates/integration.delegate.d.ts +23 -8
  226. package/dist/server/modules/storage/local/delegates/integration.delegate.js +238 -52
  227. package/dist/server/modules/storage/local/delegates/integration.delegate.js.map +1 -1
  228. package/dist/server/modules/storage/local/delegates/project.delegate.d.ts +2 -0
  229. package/dist/server/modules/storage/local/delegates/project.delegate.js +180 -130
  230. package/dist/server/modules/storage/local/delegates/project.delegate.js.map +1 -1
  231. package/dist/server/modules/storage/local/local-storage.service.d.ts +45 -10
  232. package/dist/server/modules/storage/local/local-storage.service.js +99 -15
  233. package/dist/server/modules/storage/local/local-storage.service.js.map +1 -1
  234. package/dist/server/modules/storage/models/domain.models.d.ts +170 -0
  235. package/dist/server/modules/storage/models/domain.models.js +9 -0
  236. package/dist/server/modules/storage/models/domain.models.js.map +1 -1
  237. package/dist/server/templates/teams-dev.json +311 -301
  238. package/dist/server/tsconfig.tsbuildinfo +1 -1
  239. package/dist/server/ui/assets/{ExternalRichEditor-DMtnZIMZ.js → ExternalRichEditor-FwCAalXO.js} +1 -1
  240. package/dist/server/ui/assets/{ReviewDetailPage-BmyiM5IR.js → ReviewDetailPage-D8DVpsJm.js} +1 -1
  241. package/dist/server/ui/assets/{ReviewsPage-CQ87BMwT.js → ReviewsPage-DcWpRFbm.js} +1 -1
  242. package/dist/server/ui/assets/index-CV8ydm7w.css +32 -0
  243. package/dist/server/ui/assets/index-Cwe7LxcW.js +1197 -0
  244. package/dist/server/ui/assets/{useReviewSubscription-DjbEUzt4.js → useReviewSubscription-CYoSg-88.js} +1 -1
  245. package/dist/server/ui/index.html +2 -2
  246. package/dist/templates/teams-dev.json +311 -301
  247. package/package.json +20 -2
  248. package/dist/server/ui/assets/index-BX6Yze-A.js +0 -1177
  249. package/dist/server/ui/assets/index-qoQEv_u4.css +0 -32
@@ -15,12 +15,57 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.EpicTimeStore = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const node_crypto_1 = require("node:crypto");
18
+ const error_types_1 = require("../../../common/errors/error-types");
18
19
  const db_provider_1 = require("../../storage/db/db.provider");
19
20
  const sqlite_raw_1 = require("../../storage/db/sqlite-raw");
20
21
  const transaction_runner_1 = require("../../storage/db/transaction-runner");
21
22
  const TRACKING_STARTED_AT_KEY = 'epicTime.trackingStartedAt';
22
23
  const ACTIVITY_IDLE_TIMEOUT_KEY = 'activity.idleTimeoutMs';
23
24
  const DEFAULT_IDLE_TIMEOUT_MS = 30_000;
25
+ const MILLIS_PER_MINUTE = 60_000;
26
+ const STALE_SNAPSHOT_MESSAGE = 'The captured agent time snapshot changed. Refresh the buffered time and try again.';
27
+ const RESOLVED_SCOPE_CTE = `
28
+ focal(focal_id) AS (
29
+ SELECT CAST(json_each.value AS TEXT) FROM json_each(?)
30
+ ),
31
+ route_chain(focal_id, epic_id) AS (
32
+ SELECT f.focal_id, e.id
33
+ FROM focal f
34
+ JOIN epics e ON e.id = f.focal_id AND e.parent_id IS NULL
35
+ UNION
36
+ SELECT rc.focal_id, source.id
37
+ FROM route_chain rc
38
+ JOIN epics target ON target.id = rc.epic_id
39
+ JOIN epics source
40
+ ON source.parent_id IS NULL AND source.project_id = target.project_id
41
+ JOIN epic_relations rel
42
+ ON rel.type = 'related'
43
+ AND (
44
+ (rel.left_epic_id = source.id AND rel.right_epic_id = target.id
45
+ AND rel.direction = 'left_to_right')
46
+ OR (rel.left_epic_id = target.id AND rel.right_epic_id = source.id
47
+ AND rel.direction = 'right_to_left')
48
+ )
49
+ WHERE NOT EXISTS (
50
+ SELECT 1 FROM external_task_links link WHERE link.epic_id = source.id
51
+ )
52
+ )`;
53
+ function buildBufferSnapshotToken(projectId, agentId, rows) {
54
+ return (0, node_crypto_1.createHash)('sha256')
55
+ .update(JSON.stringify({
56
+ agentId,
57
+ projectId,
58
+ rows: rows.map((row) => ({
59
+ closedAt: row.closed_at,
60
+ durationMs: row.duration_ms,
61
+ id: row.id,
62
+ lastActivityAt: row.last_activity_at,
63
+ startedAt: row.started_at,
64
+ updatedAt: row.updated_at,
65
+ })),
66
+ }))
67
+ .digest('hex');
68
+ }
24
69
  let EpicTimeStore = class EpicTimeStore {
25
70
  rawClient;
26
71
  transactionRunner;
@@ -51,6 +96,12 @@ let EpicTimeStore = class EpicTimeStore {
51
96
  };
52
97
  });
53
98
  }
99
+ readActivationSettings() {
100
+ return {
101
+ trackingStartedAt: this.readSetting(TRACKING_STARTED_AT_KEY),
102
+ idleTimeoutMs: this.readIdleTimeoutMs(),
103
+ };
104
+ }
54
105
  listReconciliationSessionIds(trackingStartedAt) {
55
106
  return this.rawClient
56
107
  .prepare(`SELECT session_id
@@ -70,130 +121,194 @@ let EpicTimeStore = class EpicTimeStore {
70
121
  .all(trackingStartedAt).map((row) => row.session_id);
71
122
  }
72
123
  async reconcileSession(sessionId, trackingStartedAt, idleTimeoutMs, now = new Date(), options = {}) {
124
+ return this.transactionRunner.runImmediateQueued(() => this.reconcileSessionCore(sessionId, trackingStartedAt, idleTimeoutMs, now, options));
125
+ }
126
+ reconcileSessionCore(sessionId, trackingStartedAt, idleTimeoutMs, now, options) {
73
127
  const nowIso = now.toISOString();
74
- return this.transactionRunner.runImmediateQueued(() => {
75
- let openSegment = this.loadOpenSegment(sessionId);
76
- const session = this.loadSession(sessionId);
77
- if (!session) {
78
- if (openSegment) {
79
- this.closeSegment(openSegment.id, openSegment.last_activity_at, nowIso);
80
- return {
81
- sessionId,
82
- action: 'closed',
83
- watermark: null,
84
- segmentId: openSegment.id,
85
- };
86
- }
87
- return { sessionId, action: 'noop', watermark: null, segmentId: null };
88
- }
89
- const watermark = this.loadWatermark(sessionId);
90
- const activityAt = session.last_activity_at;
91
- const hasNewActivity = activityAt !== null &&
92
- this.isAfter(activityAt, trackingStartedAt) &&
93
- (!watermark || this.isAfter(activityAt, watermark.last_activity_at));
94
- if (!session.agent_id || !session.project_id || !session.agent_name) {
95
- if (openSegment) {
96
- this.closeSegment(openSegment.id, openSegment.last_activity_at, nowIso);
97
- }
98
- let action = 'noop';
99
- if (hasNewActivity) {
100
- action = 'discarded';
101
- }
102
- else if (openSegment) {
103
- action = 'closed';
104
- }
105
- const watermarkProjectId = session.project_id ?? session.epic_project_id ?? openSegment?.project_id;
106
- if (hasNewActivity && activityAt && watermarkProjectId) {
107
- this.upsertWatermark(session.id, watermarkProjectId, activityAt, nowIso);
108
- }
128
+ let openSegment = this.loadOpenSegment(sessionId);
129
+ const session = this.loadSession(sessionId);
130
+ if (!session) {
131
+ if (openSegment) {
132
+ this.closeSegment(openSegment.id, openSegment.last_activity_at, nowIso);
109
133
  return {
110
134
  sessionId,
111
- action,
112
- watermark: hasNewActivity ? activityAt : (watermark?.last_activity_at ?? null),
113
- segmentId: openSegment?.id ?? null,
135
+ action: 'closed',
136
+ watermark: null,
137
+ segmentId: openSegment.id,
114
138
  };
115
139
  }
140
+ return { sessionId, action: 'noop', watermark: null, segmentId: null };
141
+ }
142
+ const watermark = this.loadWatermark(sessionId);
143
+ const activityAt = session.last_activity_at;
144
+ const hasNewActivity = activityAt !== null &&
145
+ this.isAfter(activityAt, trackingStartedAt) &&
146
+ (!watermark || this.isAfter(activityAt, watermark.last_activity_at));
147
+ if (!session.agent_id || !session.project_id || !session.agent_name) {
148
+ if (openSegment) {
149
+ this.closeSegment(openSegment.id, openSegment.last_activity_at, nowIso);
150
+ }
116
151
  let action = 'noop';
117
- let segmentId = openSegment?.id ?? null;
118
- if (hasNewActivity && activityAt) {
119
- const hadOpenSegment = openSegment !== null;
120
- const epicId = this.resolveAttributionEpic(session);
121
- const priorActivityAt = openSegment?.last_activity_at ?? watermark?.last_activity_at ?? null;
122
- const busyStart = session.activity_state === 'busy' && session.busy_since ? session.busy_since : activityAt;
123
- const continuityStart = priorActivityAt ?? busyStart;
124
- const continuityGapMs = this.elapsedMs(continuityStart, activityAt);
125
- const continuous = continuityGapMs >= 0 && continuityGapMs <= idleTimeoutMs;
126
- const sameSegment = openSegment !== null &&
127
- openSegment.project_id === session.project_id &&
128
- openSegment.agent_id_snapshot === session.agent_id &&
129
- openSegment.epic_id === epicId &&
130
- continuous;
131
- if (sameSegment && openSegment) {
132
- const deltaMs = this.elapsedMs(openSegment.last_activity_at, activityAt);
133
- this.rawClient
134
- .prepare(`UPDATE epic_time_segments
135
- SET last_activity_at = ?, duration_ms = duration_ms + ?, updated_at = ?
136
- WHERE id = ? AND closed_at IS NULL`)
137
- .run(activityAt, deltaMs, nowIso, openSegment.id);
138
- action = 'advanced';
139
- }
140
- else {
141
- if (openSegment) {
142
- this.closeSegment(openSegment.id, openSegment.last_activity_at, nowIso);
143
- openSegment = null;
144
- }
145
- const lowerBound = this.latestTimestamp([
146
- trackingStartedAt,
147
- session.started_at,
148
- watermark?.last_activity_at ?? null,
149
- ]);
150
- const busyStartProvesNewWindow = !watermark || this.isAfter(busyStart, watermark.last_activity_at);
151
- const startedAt = continuous && (hadOpenSegment || busyStartProvesNewWindow)
152
- ? this.latestTimestamp([busyStart, lowerBound])
153
- : activityAt;
154
- const durationMs = Math.max(0, this.elapsedMs(startedAt, activityAt));
155
- segmentId = (0, node_crypto_1.randomUUID)();
156
- this.rawClient
157
- .prepare(`INSERT INTO epic_time_segments
158
- (id, project_id, epic_id, session_id_snapshot, agent_id_snapshot,
159
- agent_name_snapshot, started_at, last_activity_at, closed_at,
160
- duration_ms, created_at, updated_at)
161
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`)
162
- .run(segmentId, session.project_id, epicId, session.id, session.agent_id, session.agent_name, startedAt, activityAt, durationMs, nowIso, nowIso);
163
- action = 'created';
164
- if (epicId === null) {
165
- const pendingClaim = this.applyPendingBufferClaims(session.project_id, session.agent_id, nowIso);
166
- if (pendingClaim.discardedSegmentIds.has(segmentId)) {
167
- action = 'discarded';
168
- segmentId = null;
169
- }
170
- }
171
- }
172
- this.upsertWatermark(session.id, session.project_id, activityAt, nowIso);
173
- openSegment = this.loadOpenSegment(sessionId);
152
+ if (hasNewActivity) {
153
+ action = 'discarded';
174
154
  }
175
- if (openSegment &&
176
- (options.forceCloseOpenSegment ||
177
- this.shouldClose(session, openSegment.last_activity_at, idleTimeoutMs, now))) {
178
- this.closeSegment(openSegment.id, openSegment.last_activity_at, nowIso);
179
- if (action === 'created') {
180
- action = 'created_closed';
181
- }
182
- else if (action === 'advanced') {
183
- action = 'advanced_closed';
184
- }
185
- else {
186
- action = 'closed';
187
- }
188
- segmentId = openSegment.id;
155
+ else if (openSegment) {
156
+ action = 'closed';
157
+ }
158
+ const watermarkProjectId = session.project_id ?? session.epic_project_id ?? openSegment?.project_id;
159
+ if (hasNewActivity && activityAt && watermarkProjectId) {
160
+ this.upsertWatermark(session.id, watermarkProjectId, activityAt, nowIso);
189
161
  }
190
162
  return {
191
163
  sessionId,
192
164
  action,
193
165
  watermark: hasNewActivity ? activityAt : (watermark?.last_activity_at ?? null),
194
- segmentId,
166
+ segmentId: openSegment?.id ?? null,
195
167
  };
196
- });
168
+ }
169
+ let action = 'noop';
170
+ let segmentId = openSegment?.id ?? null;
171
+ if (hasNewActivity && activityAt) {
172
+ const hadOpenSegment = openSegment !== null;
173
+ const epicId = this.resolveAttributionEpic(session);
174
+ const eligibleTeam = epicId === null ? this.resolveEligibleTeam(session) : null;
175
+ const openTeamBatch = eligibleTeam
176
+ ? this.loadOpenTeamBatch(session.project_id, eligibleTeam.team_id)
177
+ : null;
178
+ const priorActivityAt = openSegment?.last_activity_at ?? watermark?.last_activity_at ?? null;
179
+ const busyStart = session.activity_state === 'busy' && session.busy_since ? session.busy_since : activityAt;
180
+ const continuityStart = priorActivityAt ?? busyStart;
181
+ const continuityGapMs = this.elapsedMs(continuityStart, activityAt);
182
+ const continuous = continuityGapMs >= 0 && continuityGapMs <= idleTimeoutMs;
183
+ const sameSegment = openSegment !== null &&
184
+ openSegment.project_id === session.project_id &&
185
+ openSegment.agent_id_snapshot === session.agent_id &&
186
+ openSegment.epic_id === epicId &&
187
+ (eligibleTeam
188
+ ? openTeamBatch !== null && openSegment.team_batch_id === openTeamBatch.id
189
+ : openSegment.team_batch_id === null) &&
190
+ continuous;
191
+ if (sameSegment && openSegment) {
192
+ const deltaMs = this.elapsedMs(openSegment.last_activity_at, activityAt);
193
+ this.rawClient
194
+ .prepare(`UPDATE epic_time_segments
195
+ SET last_activity_at = ?, duration_ms = duration_ms + ?, updated_at = ?
196
+ WHERE id = ? AND closed_at IS NULL`)
197
+ .run(activityAt, deltaMs, nowIso, openSegment.id);
198
+ action = 'advanced';
199
+ }
200
+ else {
201
+ if (openSegment) {
202
+ this.closeSegment(openSegment.id, openSegment.last_activity_at, nowIso);
203
+ openSegment = null;
204
+ }
205
+ const lowerBound = this.latestTimestamp([
206
+ trackingStartedAt,
207
+ session.started_at,
208
+ watermark?.last_activity_at ?? null,
209
+ ]);
210
+ const busyStartProvesNewWindow = !watermark || this.isAfter(busyStart, watermark.last_activity_at);
211
+ const startedAt = continuous && (hadOpenSegment || busyStartProvesNewWindow)
212
+ ? this.latestTimestamp([busyStart, lowerBound])
213
+ : activityAt;
214
+ const durationMs = Math.max(0, this.elapsedMs(startedAt, activityAt));
215
+ const teamBatch = eligibleTeam
216
+ ? (openTeamBatch ??
217
+ this.createOrLoadOpenTeamBatch(session.project_id, eligibleTeam, startedAt, nowIso))
218
+ : null;
219
+ segmentId = (0, node_crypto_1.randomUUID)();
220
+ this.rawClient
221
+ .prepare(`INSERT INTO epic_time_segments
222
+ (id, project_id, epic_id, team_batch_id, attribution_source,
223
+ team_id_snapshot, team_name_snapshot, session_id_snapshot,
224
+ agent_id_snapshot, agent_name_snapshot, started_at, last_activity_at,
225
+ closed_at, duration_ms, created_at, updated_at)
226
+ VALUES (?, ?, ?, ?, 'direct', ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`)
227
+ .run(segmentId, session.project_id, epicId, teamBatch?.id ?? null, eligibleTeam?.team_id ?? null, eligibleTeam?.team_name ?? null, session.id, session.agent_id, session.agent_name, startedAt, activityAt, durationMs, nowIso, nowIso);
228
+ action = 'created';
229
+ if (epicId === null) {
230
+ const pendingClaim = this.applyPendingBufferClaims(session.project_id, session.agent_id, nowIso);
231
+ if (pendingClaim.discardedSegmentIds.has(segmentId)) {
232
+ action = 'discarded';
233
+ segmentId = null;
234
+ }
235
+ }
236
+ }
237
+ this.upsertWatermark(session.id, session.project_id, activityAt, nowIso);
238
+ openSegment = this.loadOpenSegment(sessionId);
239
+ }
240
+ if (openSegment &&
241
+ (options.forceCloseOpenSegment ||
242
+ this.shouldClose(session, openSegment.last_activity_at, idleTimeoutMs, now))) {
243
+ this.closeSegment(openSegment.id, openSegment.last_activity_at, nowIso);
244
+ if (action === 'created') {
245
+ action = 'created_closed';
246
+ }
247
+ else if (action === 'advanced') {
248
+ action = 'advanced_closed';
249
+ }
250
+ else {
251
+ action = 'closed';
252
+ }
253
+ segmentId = openSegment.id;
254
+ }
255
+ return {
256
+ sessionId,
257
+ action,
258
+ watermark: hasNewActivity ? activityAt : (watermark?.last_activity_at ?? null),
259
+ segmentId,
260
+ };
261
+ }
262
+ async processTeamBatches(deliveryKey, idleTimeoutMs, now = new Date()) {
263
+ return this.transactionRunner.runImmediateQueued(() => this.processTeamBatchesCore(deliveryKey, idleTimeoutMs, now));
264
+ }
265
+ processTeamBatchesCore(deliveryKey, idleTimeoutMs, now) {
266
+ const nowIso = now.toISOString();
267
+ const activeAfter = new Date(now.getTime() - idleTimeoutMs).toISOString();
268
+ let sealedBatches = 0;
269
+ let finalizedBatches = 0;
270
+ let cancelledBatches = 0;
271
+ const openBatches = this.loadTeamBatches(false);
272
+ for (const batch of openBatches) {
273
+ if (this.hasActiveEligibleMember(batch, activeAfter)) {
274
+ continue;
275
+ }
276
+ this.rawClient
277
+ .prepare(`UPDATE epic_time_segments
278
+ SET closed_at = COALESCE(closed_at, last_activity_at), updated_at = ?
279
+ WHERE team_batch_id = ?`)
280
+ .run(nowIso, batch.id);
281
+ this.rawClient
282
+ .prepare(`UPDATE epic_time_team_batches
283
+ SET sealed_at = ?, updated_at = ?
284
+ WHERE id = ? AND sealed_at IS NULL`)
285
+ .run(nowIso, nowIso, batch.id);
286
+ this.rawClient
287
+ .prepare(`INSERT OR IGNORE INTO epic_time_team_batch_event_barriers
288
+ (team_batch_id, committed_event_id, created_at)
289
+ SELECT ?, eh.event_id, ?
290
+ FROM event_handlers eh
291
+ INNER JOIN events e ON e.id = eh.event_id
292
+ WHERE eh.delivery_key = ?
293
+ AND eh.status IN ('pending', 'running', 'retry')
294
+ AND e.published_at <= ?`)
295
+ .run(batch.id, nowIso, deliveryKey, nowIso);
296
+ sealedBatches += 1;
297
+ }
298
+ const sealed = this.loadTeamBatches(true);
299
+ for (const batch of sealed) {
300
+ if (this.batchHasPendingBarrier(batch.id, deliveryKey)) {
301
+ continue;
302
+ }
303
+ if (!this.isTeamBatchIdentityCurrent(batch)) {
304
+ this.cancelTeamBatch(batch.id, nowIso);
305
+ cancelledBatches += 1;
306
+ continue;
307
+ }
308
+ this.finalizeTeamBatch(batch, nowIso);
309
+ finalizedBatches += 1;
310
+ }
311
+ return { sealedBatches, finalizedBatches, cancelledBatches };
197
312
  }
198
313
  async recordTaskTouch(touch, now = new Date()) {
199
314
  const nowIso = now.toISOString();
@@ -223,6 +338,150 @@ let EpicTimeStore = class EpicTimeStore {
223
338
  };
224
339
  });
225
340
  }
341
+ listAgentTimeBuffers(projectId) {
342
+ const rows = this.listEligibleBufferRows(projectId, null);
343
+ if (rows.length === 0) {
344
+ return { capturedAt: null, items: [] };
345
+ }
346
+ let capturedAt = rows[0].updated_at;
347
+ let capturedAtMs = this.timestampMs(capturedAt);
348
+ const rowsByAgent = new Map();
349
+ for (const row of rows) {
350
+ const updatedAtMs = this.timestampMs(row.updated_at);
351
+ if (updatedAtMs > capturedAtMs) {
352
+ capturedAt = row.updated_at;
353
+ capturedAtMs = updatedAtMs;
354
+ }
355
+ const agentRows = rowsByAgent.get(row.agent_id_snapshot) ?? [];
356
+ agentRows.push(row);
357
+ rowsByAgent.set(row.agent_id_snapshot, agentRows);
358
+ }
359
+ const items = [];
360
+ for (const [agentId, agentRows] of rowsByAgent) {
361
+ const durationMs = agentRows.reduce((total, row) => total + row.duration_ms, 0);
362
+ let oldest = agentRows[0].last_activity_at;
363
+ let newest = agentRows[0].last_activity_at;
364
+ for (const row of agentRows.slice(1)) {
365
+ if (this.timestampMs(row.last_activity_at) < this.timestampMs(oldest)) {
366
+ oldest = row.last_activity_at;
367
+ }
368
+ if (this.timestampMs(row.last_activity_at) > this.timestampMs(newest)) {
369
+ newest = row.last_activity_at;
370
+ }
371
+ }
372
+ items.push({
373
+ agentId,
374
+ snapshotToken: buildBufferSnapshotToken(projectId, agentId, agentRows),
375
+ minutes: Math.floor(durationMs / MILLIS_PER_MINUTE),
376
+ durationMs,
377
+ segmentCount: agentRows.length,
378
+ oldestActivityAt: oldest,
379
+ newestActivityAt: newest,
380
+ });
381
+ }
382
+ items.sort((left, right) => left.agentId.localeCompare(right.agentId));
383
+ return { capturedAt, items };
384
+ }
385
+ async assignAgentTimeBuffer(input, now = new Date()) {
386
+ const nowIso = now.toISOString();
387
+ return this.transactionRunner.runImmediateQueued(() => {
388
+ const project = this.rawClient
389
+ .prepare(`SELECT workspace_id FROM projects WHERE id = ?`)
390
+ .get(input.projectId);
391
+ if (!project) {
392
+ throw new error_types_1.NotFoundError('Project', input.projectId);
393
+ }
394
+ const agent = this.rawClient
395
+ .prepare(`SELECT id FROM agents WHERE id = ? AND project_id = ?`)
396
+ .get(input.agentId, input.projectId);
397
+ if (!agent) {
398
+ throw new error_types_1.NotFoundError('Agent', input.agentId);
399
+ }
400
+ const epic = this.rawClient
401
+ .prepare(`SELECT id FROM epics WHERE id = ? AND project_id = ?`)
402
+ .get(input.targetEpicId, input.projectId);
403
+ if (!epic) {
404
+ throw new error_types_1.NotFoundError('Epic', input.targetEpicId);
405
+ }
406
+ const rows = this.requireCapturedBufferRows(input);
407
+ const assignRow = this.rawClient.prepare(`UPDATE epic_time_segments
408
+ SET epic_id = ?, updated_at = ?
409
+ WHERE id = ?
410
+ AND epic_id IS NULL
411
+ AND team_batch_id IS NULL
412
+ AND closed_at IS NOT NULL`);
413
+ for (const row of rows) {
414
+ if (assignRow.run(input.targetEpicId, nowIso, row.id).changes !== 1) {
415
+ throw new error_types_1.ConflictError(STALE_SNAPSHOT_MESSAGE);
416
+ }
417
+ }
418
+ return { workspaceId: project.workspace_id };
419
+ });
420
+ }
421
+ async resetAgentTimeBuffer(input) {
422
+ return this.transactionRunner.runImmediateQueued(() => {
423
+ const project = this.rawClient
424
+ .prepare(`SELECT workspace_id FROM projects WHERE id = ?`)
425
+ .get(input.projectId);
426
+ if (!project) {
427
+ throw new error_types_1.NotFoundError('Project', input.projectId);
428
+ }
429
+ const agent = this.rawClient
430
+ .prepare(`SELECT id FROM agents WHERE id = ? AND project_id = ?`)
431
+ .get(input.agentId, input.projectId);
432
+ if (!agent) {
433
+ throw new error_types_1.NotFoundError('Agent', input.agentId);
434
+ }
435
+ const rows = this.requireCapturedBufferRows(input);
436
+ const deleteRow = this.rawClient.prepare(`DELETE FROM epic_time_segments
437
+ WHERE id = ?
438
+ AND epic_id IS NULL
439
+ AND team_batch_id IS NULL
440
+ AND closed_at IS NOT NULL
441
+ AND duration_ms > 0`);
442
+ for (const row of rows) {
443
+ if (deleteRow.run(row.id).changes !== 1) {
444
+ throw new error_types_1.ConflictError(STALE_SNAPSHOT_MESSAGE);
445
+ }
446
+ }
447
+ return { workspaceId: project.workspace_id };
448
+ });
449
+ }
450
+ runTerminationResetSync(input) {
451
+ const now = input.now;
452
+ this.reconcileSessionCore(input.sessionId, input.trackingStartedAt, input.idleTimeoutMs, now, {});
453
+ this.processTeamBatchesCore(input.deliveryKey, input.idleTimeoutMs, now);
454
+ if (!input.agentId) {
455
+ return { workspaceId: null, deletedSegments: 0 };
456
+ }
457
+ const deleted = this.rawClient
458
+ .prepare(`DELETE FROM epic_time_segments
459
+ WHERE id IN (
460
+ SELECT seg.id
461
+ FROM epic_time_segments seg
462
+ INNER JOIN agents a
463
+ ON a.id = seg.agent_id_snapshot AND a.project_id = seg.project_id
464
+ WHERE seg.agent_id_snapshot = ?
465
+ AND seg.epic_id IS NULL
466
+ AND seg.team_batch_id IS NULL
467
+ AND seg.closed_at IS NOT NULL
468
+ AND seg.duration_ms > 0
469
+ )`)
470
+ .run(input.agentId);
471
+ if (deleted.changes === 0) {
472
+ return { workspaceId: null, deletedSegments: 0 };
473
+ }
474
+ const agentProject = this.rawClient
475
+ .prepare(`SELECT p.workspace_id AS workspace_id
476
+ FROM agents a
477
+ INNER JOIN projects p ON p.id = a.project_id
478
+ WHERE a.id = ?`)
479
+ .get(input.agentId);
480
+ return {
481
+ workspaceId: agentProject?.workspace_id ?? null,
482
+ deletedSegments: deleted.changes,
483
+ };
484
+ }
226
485
  getEpicTimeScope(epicId) {
227
486
  const row = this.rawClient
228
487
  .prepare(`SELECT id, parent_id FROM epics WHERE id = ?`)
@@ -238,40 +497,274 @@ let EpicTimeStore = class EpicTimeStore {
238
497
  .prepare(`SELECT id, parent_id FROM epics WHERE id IN (${placeholders})`)
239
498
  .all(...epicIds).map((row) => ({ id: row.id, parentId: row.parent_id }));
240
499
  }
241
- listClosedSegmentsForEpic(epicId, includeDirectChildren) {
242
- const rows = this.rawClient
243
- .prepare(`SELECT s.id, scoped.id AS epic_id, scoped.title AS epic_title,
244
- s.agent_id_snapshot, s.agent_name_snapshot,
245
- s.duration_ms, s.last_activity_at, s.updated_at
246
- FROM epic_time_segments s
247
- INNER JOIN epics scoped ON scoped.id = s.epic_id AND scoped.project_id = s.project_id
248
- WHERE s.closed_at IS NOT NULL
249
- AND (scoped.id = ? OR (? = 1 AND scoped.parent_id = ?))
250
- ORDER BY s.last_activity_at, s.id`)
251
- .all(epicId, includeDirectChildren ? 1 : 0, epicId);
252
- return rows.map((row) => this.mapSummarySegment(row, null, row.epic_id === epicId));
253
- }
254
- listClosedSegmentsForRoots(rootEpicIds) {
255
- if (rootEpicIds.length === 0) {
256
- return [];
500
+ listResolvedScope(focalEpicIds) {
501
+ if (focalEpicIds.length === 0) {
502
+ return { segments: [], routedRootIdsByFocal: new Map() };
257
503
  }
258
- const placeholders = rootEpicIds.map(() => '?').join(', ');
259
504
  const rows = this.rawClient
260
- .prepare(`SELECT roots.id AS root_epic_id, s.id, scoped.id AS epic_id,
261
- scoped.title AS epic_title, s.agent_id_snapshot,
262
- s.agent_name_snapshot, s.duration_ms, s.last_activity_at, s.updated_at
263
- FROM epics roots
264
- INNER JOIN epics scoped
265
- ON (scoped.id = roots.id OR scoped.parent_id = roots.id)
266
- AND scoped.project_id = roots.project_id
267
- INNER JOIN epic_time_segments s
268
- ON s.project_id = roots.project_id
269
- AND s.epic_id = scoped.id
270
- AND s.closed_at IS NOT NULL
271
- WHERE roots.id IN (${placeholders})
272
- ORDER BY roots.id, s.last_activity_at, s.id`)
273
- .all(...rootEpicIds);
274
- return rows.map((row) => this.mapSummarySegment(row, row.root_epic_id, row.epic_id === row.root_epic_id));
505
+ .prepare(`WITH RECURSIVE
506
+ ${RESOLVED_SCOPE_CTE},
507
+ scope(focal_id, epic_id, is_direct, group_epic_id) AS (
508
+ SELECT f.focal_id, e.id, 1, f.focal_id
509
+ FROM focal f
510
+ JOIN epics e ON e.id = f.focal_id
511
+ UNION
512
+ SELECT f.focal_id, child.id, 0, f.focal_id
513
+ FROM focal f
514
+ JOIN epics e ON e.id = f.focal_id AND e.parent_id IS NULL
515
+ JOIN epics child ON child.parent_id = e.id AND child.project_id = e.project_id
516
+ UNION
517
+ SELECT rc.focal_id, rc.epic_id, 0, rc.epic_id
518
+ FROM route_chain rc
519
+ WHERE rc.epic_id <> rc.focal_id
520
+ UNION
521
+ SELECT rc.focal_id, child.id, 0, rc.epic_id
522
+ FROM route_chain rc
523
+ JOIN epics routed ON routed.id = rc.epic_id
524
+ JOIN epics child ON child.parent_id = routed.id AND child.project_id = routed.project_id
525
+ WHERE rc.epic_id <> rc.focal_id
526
+ AND NOT EXISTS (
527
+ SELECT 1 FROM external_task_links link WHERE link.epic_id = child.id
528
+ )
529
+ )
530
+ SELECT DISTINCT scope.focal_id AS root_epic_id,
531
+ seg.id AS segment_id,
532
+ scoped.id AS epic_id, scoped.title AS epic_title,
533
+ seg.agent_id_snapshot, seg.agent_name_snapshot, seg.attribution_source,
534
+ seg.team_id_snapshot, seg.team_name_snapshot,
535
+ seg.duration_ms, seg.last_activity_at, seg.updated_at,
536
+ scope.is_direct AS is_direct,
537
+ scope.group_epic_id AS group_epic_id,
538
+ group_epic.title AS group_epic_title,
539
+ NULL AS routed_epic_id
540
+ FROM scope
541
+ JOIN epics scoped ON scoped.id = scope.epic_id
542
+ LEFT JOIN epics group_epic ON group_epic.id = scope.group_epic_id
543
+ JOIN epic_time_segments seg
544
+ ON seg.epic_id = scoped.id
545
+ AND seg.project_id = scoped.project_id
546
+ AND seg.closed_at IS NOT NULL
547
+ UNION ALL
548
+ SELECT DISTINCT rc.focal_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
549
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, rc.epic_id
550
+ FROM route_chain rc
551
+ WHERE rc.epic_id <> rc.focal_id
552
+ ORDER BY root_epic_id, last_activity_at, segment_id`)
553
+ .all(JSON.stringify(focalEpicIds));
554
+ const segments = [];
555
+ const routedRootIdsByFocal = new Map();
556
+ for (const row of rows) {
557
+ if (row.routed_epic_id !== null) {
558
+ const routed = routedRootIdsByFocal.get(row.root_epic_id) ?? [];
559
+ routed.push(row.routed_epic_id);
560
+ routedRootIdsByFocal.set(row.root_epic_id, routed);
561
+ continue;
562
+ }
563
+ segments.push(this.mapSummarySegment({
564
+ id: row.segment_id,
565
+ epic_id: row.epic_id,
566
+ epic_title: row.epic_title,
567
+ agent_id_snapshot: row.agent_id_snapshot,
568
+ agent_name_snapshot: row.agent_name_snapshot,
569
+ attribution_source: row.attribution_source,
570
+ team_id_snapshot: row.team_id_snapshot,
571
+ team_name_snapshot: row.team_name_snapshot,
572
+ duration_ms: row.duration_ms,
573
+ last_activity_at: row.last_activity_at,
574
+ updated_at: row.updated_at,
575
+ }, row.root_epic_id, row.is_direct === 1, row.group_epic_id, row.group_epic_title));
576
+ }
577
+ for (const routed of routedRootIdsByFocal.values()) {
578
+ routed.sort();
579
+ }
580
+ for (const focalId of focalEpicIds) {
581
+ if (!routedRootIdsByFocal.has(focalId)) {
582
+ routedRootIdsByFocal.set(focalId, []);
583
+ }
584
+ }
585
+ return { segments, routedRootIdsByFocal };
586
+ }
587
+ resolveEligibleTeam(session) {
588
+ if (!session.agent_id || !session.project_id) {
589
+ return null;
590
+ }
591
+ return (this.rawClient
592
+ .prepare(`SELECT t.id AS team_id, t.name AS team_name,
593
+ lead.id AS lead_agent_id, lead.name AS lead_agent_name
594
+ FROM team_members tm
595
+ INNER JOIN teams t ON t.id = tm.team_id AND t.project_id = ?
596
+ INNER JOIN agents lead
597
+ ON lead.id = t.team_lead_agent_id AND lead.project_id = t.project_id
598
+ WHERE tm.agent_id = ?
599
+ AND t.team_lead_agent_id <> tm.agent_id
600
+ AND (
601
+ SELECT COUNT(*)
602
+ FROM team_members memberships
603
+ INNER JOIN teams member_teams
604
+ ON member_teams.id = memberships.team_id
605
+ AND member_teams.project_id = ?
606
+ WHERE memberships.agent_id = tm.agent_id
607
+ ) = 1
608
+ LIMIT 1`)
609
+ .get(session.project_id, session.agent_id, session.project_id) ?? null);
610
+ }
611
+ loadOpenTeamBatch(projectId, teamId) {
612
+ return (this.rawClient
613
+ .prepare(`SELECT id, project_id, team_id_snapshot, team_name_snapshot,
614
+ lead_agent_id_snapshot, lead_agent_name_snapshot, started_at, sealed_at
615
+ FROM epic_time_team_batches
616
+ WHERE project_id = ? AND team_id_snapshot = ? AND sealed_at IS NULL`)
617
+ .get(projectId, teamId) ?? null);
618
+ }
619
+ createOrLoadOpenTeamBatch(projectId, team, startedAt, nowIso) {
620
+ this.rawClient
621
+ .prepare(`INSERT INTO epic_time_team_batches
622
+ (id, project_id, team_id_snapshot, team_name_snapshot,
623
+ lead_agent_id_snapshot, lead_agent_name_snapshot, started_at,
624
+ sealed_at, created_at, updated_at)
625
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
626
+ ON CONFLICT DO NOTHING`)
627
+ .run((0, node_crypto_1.randomUUID)(), projectId, team.team_id, team.team_name, team.lead_agent_id, team.lead_agent_name, startedAt, nowIso, nowIso);
628
+ const batch = this.loadOpenTeamBatch(projectId, team.team_id);
629
+ if (!batch) {
630
+ throw new Error('Unable to create or load the open Epic-time team batch.');
631
+ }
632
+ return batch;
633
+ }
634
+ loadTeamBatches(sealed) {
635
+ return this.rawClient
636
+ .prepare(`SELECT id, project_id, team_id_snapshot, team_name_snapshot,
637
+ lead_agent_id_snapshot, lead_agent_name_snapshot, started_at, sealed_at
638
+ FROM epic_time_team_batches
639
+ WHERE sealed_at IS ${sealed ? 'NOT NULL' : 'NULL'}
640
+ ORDER BY started_at, id`)
641
+ .all();
642
+ }
643
+ hasActiveEligibleMember(batch, activeAfter) {
644
+ return Boolean(this.rawClient
645
+ .prepare(`SELECT 1
646
+ FROM sessions s
647
+ INNER JOIN agents a
648
+ ON a.id = s.agent_id AND a.project_id = ?
649
+ INNER JOIN team_members tm
650
+ ON tm.agent_id = a.id AND tm.team_id = ?
651
+ INNER JOIN teams t
652
+ ON t.id = tm.team_id
653
+ AND t.project_id = ?
654
+ AND t.team_lead_agent_id = ?
655
+ INNER JOIN agents lead
656
+ ON lead.id = t.team_lead_agent_id AND lead.project_id = t.project_id
657
+ WHERE s.status = 'running'
658
+ AND (s.activity_state IS NULL OR s.activity_state <> 'idle')
659
+ AND s.last_activity_at > ?
660
+ AND a.id <> t.team_lead_agent_id
661
+ AND (
662
+ SELECT COUNT(*)
663
+ FROM team_members memberships
664
+ INNER JOIN teams member_teams
665
+ ON member_teams.id = memberships.team_id
666
+ AND member_teams.project_id = ?
667
+ WHERE memberships.agent_id = a.id
668
+ ) = 1
669
+ AND NOT EXISTS (
670
+ SELECT 1 FROM epics bound
671
+ WHERE bound.id = s.epic_id AND bound.project_id = ?
672
+ )
673
+ AND NOT EXISTS (
674
+ SELECT 1 FROM epics assigned
675
+ WHERE assigned.project_id = ? AND assigned.agent_id = a.id
676
+ )
677
+ LIMIT 1`)
678
+ .get(batch.project_id, batch.team_id_snapshot, batch.project_id, batch.lead_agent_id_snapshot, activeAfter, batch.project_id, batch.project_id, batch.project_id));
679
+ }
680
+ batchHasPendingBarrier(batchId, deliveryKey) {
681
+ return Boolean(this.rawClient
682
+ .prepare(`SELECT 1
683
+ FROM epic_time_team_batch_event_barriers barrier
684
+ INNER JOIN event_handlers delivery
685
+ ON delivery.event_id = barrier.committed_event_id
686
+ AND delivery.delivery_key = ?
687
+ WHERE barrier.team_batch_id = ?
688
+ AND delivery.status IN ('pending', 'running', 'retry')
689
+ LIMIT 1`)
690
+ .get(deliveryKey, batchId));
691
+ }
692
+ isTeamBatchIdentityCurrent(batch) {
693
+ const teamStillMatches = Boolean(this.rawClient
694
+ .prepare(`SELECT 1
695
+ FROM teams t
696
+ INNER JOIN agents lead
697
+ ON lead.id = t.team_lead_agent_id AND lead.project_id = t.project_id
698
+ WHERE t.id = ? AND t.project_id = ? AND t.team_lead_agent_id = ?`)
699
+ .get(batch.team_id_snapshot, batch.project_id, batch.lead_agent_id_snapshot));
700
+ if (!teamStillMatches) {
701
+ return false;
702
+ }
703
+ return !this.rawClient
704
+ .prepare(`SELECT 1
705
+ FROM epic_time_segments segment
706
+ LEFT JOIN agents original
707
+ ON original.id = segment.agent_id_snapshot AND original.project_id = segment.project_id
708
+ WHERE segment.team_batch_id = ?
709
+ AND segment.epic_id IS NULL
710
+ AND (
711
+ original.id IS NULL
712
+ OR NOT EXISTS (
713
+ SELECT 1 FROM team_members exact_membership
714
+ WHERE exact_membership.team_id = ?
715
+ AND exact_membership.agent_id = segment.agent_id_snapshot
716
+ )
717
+ OR (
718
+ SELECT COUNT(*)
719
+ FROM team_members memberships
720
+ INNER JOIN teams member_teams
721
+ ON member_teams.id = memberships.team_id
722
+ AND member_teams.project_id = segment.project_id
723
+ WHERE memberships.agent_id = segment.agent_id_snapshot
724
+ ) <> 1
725
+ )
726
+ LIMIT 1`)
727
+ .get(batch.id, batch.team_id_snapshot);
728
+ }
729
+ cancelTeamBatch(batchId, nowIso) {
730
+ this.rawClient
731
+ .prepare(`UPDATE epic_time_segments
732
+ SET team_batch_id = NULL, attribution_source = 'direct',
733
+ team_id_snapshot = NULL, team_name_snapshot = NULL, updated_at = ?
734
+ WHERE team_batch_id = ? AND epic_id IS NULL`)
735
+ .run(nowIso, batchId);
736
+ this.rawClient.prepare(`DELETE FROM epic_time_team_batches WHERE id = ?`).run(batchId);
737
+ }
738
+ finalizeTeamBatch(batch, nowIso) {
739
+ const winner = this.rawClient
740
+ .prepare(`SELECT agent_id_snapshot, SUM(duration_ms) AS duration_ms,
741
+ MIN(started_at) AS earliest_started_at
742
+ FROM epic_time_segments
743
+ WHERE team_batch_id = ? AND epic_id IS NULL
744
+ GROUP BY agent_id_snapshot
745
+ ORDER BY duration_ms DESC, earliest_started_at, agent_id_snapshot
746
+ LIMIT 1`)
747
+ .get(batch.id);
748
+ if (winner) {
749
+ const target = this.rawClient
750
+ .prepare(`SELECT id
751
+ FROM epics
752
+ WHERE project_id = ? AND agent_id = ?
753
+ ORDER BY updated_at DESC, id DESC
754
+ LIMIT 1`)
755
+ .get(batch.project_id, batch.lead_agent_id_snapshot);
756
+ this.rawClient
757
+ .prepare(`DELETE FROM epic_time_segments
758
+ WHERE team_batch_id = ? AND epic_id IS NULL AND agent_id_snapshot <> ?`)
759
+ .run(batch.id, winner.agent_id_snapshot);
760
+ this.rawClient
761
+ .prepare(`UPDATE epic_time_segments
762
+ SET epic_id = ?, agent_id_snapshot = ?, agent_name_snapshot = ?,
763
+ attribution_source = 'team', updated_at = ?
764
+ WHERE team_batch_id = ? AND epic_id IS NULL AND agent_id_snapshot = ?`)
765
+ .run(target?.id ?? null, batch.lead_agent_id_snapshot, batch.lead_agent_name_snapshot, nowIso, batch.id, winner.agent_id_snapshot);
766
+ }
767
+ this.rawClient.prepare(`DELETE FROM epic_time_team_batches WHERE id = ?`).run(batch.id);
275
768
  }
276
769
  loadSession(sessionId) {
277
770
  return (this.rawClient
@@ -284,15 +777,23 @@ let EpicTimeStore = class EpicTimeStore {
284
777
  WHERE s.id = ?`)
285
778
  .get(sessionId) ?? null);
286
779
  }
287
- mapSummarySegment(row, rootEpicId, isDirect) {
780
+ mapSummarySegment(row, rootEpicId, isDirect, groupEpicId, groupEpicTitle) {
781
+ const attributionSource = row.attribution_source === 'team' && row.team_id_snapshot && row.team_name_snapshot
782
+ ? 'team'
783
+ : 'direct';
288
784
  return {
289
785
  id: row.id,
290
786
  rootEpicId,
291
787
  epicId: row.epic_id,
292
788
  epicTitle: row.epic_title,
789
+ groupEpicId,
790
+ groupEpicTitle,
293
791
  isDirect,
294
792
  agentId: row.agent_id_snapshot,
295
793
  agentName: row.agent_name_snapshot,
794
+ attributionSource,
795
+ teamId: attributionSource === 'team' ? row.team_id_snapshot : null,
796
+ teamName: attributionSource === 'team' ? row.team_name_snapshot : null,
296
797
  durationMs: row.duration_ms,
297
798
  lastActivityAt: row.last_activity_at,
298
799
  updatedAt: row.updated_at,
@@ -307,7 +808,9 @@ let EpicTimeStore = class EpicTimeStore {
307
808
  }
308
809
  loadOpenSegment(sessionId) {
309
810
  return (this.rawClient
310
- .prepare(`SELECT id, project_id, epic_id, agent_id_snapshot, last_activity_at, duration_ms
811
+ .prepare(`SELECT id, project_id, epic_id, team_batch_id, attribution_source,
812
+ team_id_snapshot, team_name_snapshot, agent_id_snapshot,
813
+ last_activity_at, duration_ms
311
814
  FROM epic_time_segments
312
815
  WHERE session_id_snapshot = ? AND closed_at IS NULL`)
313
816
  .get(sessionId) ?? null);
@@ -356,6 +859,21 @@ let EpicTimeStore = class EpicTimeStore {
356
859
  WHERE earliest.project_id = s.project_id
357
860
  AND earliest.agent_id_snapshot = s.agent_id_snapshot
358
861
  AND earliest.published_at >= s.started_at
862
+ AND (
863
+ s.team_batch_id IS NULL
864
+ OR EXISTS (
865
+ SELECT 1
866
+ FROM epic_time_team_batches open_batch
867
+ WHERE open_batch.id = s.team_batch_id
868
+ AND open_batch.sealed_at IS NULL
869
+ )
870
+ OR EXISTS (
871
+ SELECT 1
872
+ FROM epic_time_team_batch_event_barriers barrier
873
+ WHERE barrier.team_batch_id = s.team_batch_id
874
+ AND barrier.committed_event_id = earliest.committed_event_id
875
+ )
876
+ )
359
877
  ORDER BY earliest.published_at, earliest.claim_sequence
360
878
  LIMIT 1
361
879
  )
@@ -368,7 +886,21 @@ let EpicTimeStore = class EpicTimeStore {
368
886
  const discardedSegmentIds = new Set();
369
887
  const targetExists = new Map();
370
888
  const updateSegment = this.rawClient.prepare(`UPDATE epic_time_segments
371
- SET epic_id = ?, updated_at = ?
889
+ SET epic_id = ?,
890
+ attribution_source = CASE
891
+ WHEN team_batch_id IS NOT NULL THEN 'direct'
892
+ ELSE attribution_source
893
+ END,
894
+ team_id_snapshot = CASE
895
+ WHEN team_batch_id IS NOT NULL THEN NULL
896
+ ELSE team_id_snapshot
897
+ END,
898
+ team_name_snapshot = CASE
899
+ WHEN team_batch_id IS NOT NULL THEN NULL
900
+ ELSE team_name_snapshot
901
+ END,
902
+ team_batch_id = NULL,
903
+ updated_at = ?
372
904
  WHERE id = ? AND epic_id IS NULL`);
373
905
  const deleteSegment = this.rawClient.prepare(`DELETE FROM epic_time_segments
374
906
  WHERE id = ? AND epic_id IS NULL`);
@@ -395,6 +927,33 @@ let EpicTimeStore = class EpicTimeStore {
395
927
  }
396
928
  return { claimedSegmentIds, discardedSegmentIds };
397
929
  }
930
+ requireCapturedBufferRows(input) {
931
+ const rows = this.listEligibleBufferRows(input.projectId, input.agentId);
932
+ const capturedAtMs = this.timestampMs(input.capturedAt);
933
+ if (rows.length === 0 || rows.some((row) => this.timestampMs(row.updated_at) > capturedAtMs)) {
934
+ throw new error_types_1.ConflictError(STALE_SNAPSHOT_MESSAGE);
935
+ }
936
+ if (buildBufferSnapshotToken(input.projectId, input.agentId, rows) !== input.snapshotToken) {
937
+ throw new error_types_1.ConflictError(STALE_SNAPSHOT_MESSAGE);
938
+ }
939
+ return rows;
940
+ }
941
+ listEligibleBufferRows(projectId, agentId) {
942
+ return this.rawClient
943
+ .prepare(`SELECT seg.id, seg.agent_id_snapshot, seg.duration_ms, seg.started_at,
944
+ seg.last_activity_at, seg.closed_at, seg.updated_at
945
+ FROM epic_time_segments seg
946
+ INNER JOIN agents a
947
+ ON a.id = seg.agent_id_snapshot AND a.project_id = seg.project_id
948
+ WHERE seg.project_id = ?
949
+ AND (? IS NULL OR seg.agent_id_snapshot = ?)
950
+ AND seg.epic_id IS NULL
951
+ AND seg.team_batch_id IS NULL
952
+ AND seg.closed_at IS NOT NULL
953
+ AND seg.duration_ms > 0
954
+ ORDER BY seg.agent_id_snapshot, seg.started_at, seg.id`)
955
+ .all(projectId, agentId, agentId);
956
+ }
398
957
  closeSegment(segmentId, closedAt, nowIso) {
399
958
  this.rawClient
400
959
  .prepare(`UPDATE epic_time_segments