@taskforcehq/taskforce 0.3.328 → 0.3.329

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 (264) hide show
  1. package/dist/Taskforce.module.css +233 -34
  2. package/dist/TaskforceCore.js +237 -230
  3. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +1 -1
  4. package/dist/cli.js +34 -0
  5. package/dist/cliCloudTargetGuard.d.ts +5 -0
  6. package/dist/cliCloudTargetGuard.js +11 -0
  7. package/dist/components/features/AnnotatedAttachmentWorkspace.js +24 -8
  8. package/dist/components/features/TaskSettings.d.ts +12 -1
  9. package/dist/components/features/TaskSettings.js +147 -40
  10. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +1 -1
  11. package/dist/components/features/planning/PlanningModule.js +2 -5
  12. package/dist/components/features/planning/PlanningTaskOrderList.d.ts +1 -1
  13. package/dist/components/features/planning/PlanningTaskOrderList.js +17 -6
  14. package/dist/components/features/planning/planningTaskArrangement.d.ts +1 -0
  15. package/dist/components/features/planning/planningTaskArrangement.js +5 -5
  16. package/dist/components/features/taskforceAgentEditorModel.js +3 -1
  17. package/dist/components/task/TaskCard.d.ts +5 -0
  18. package/dist/components/task/TaskCard.js +65 -10
  19. package/dist/components/task/TaskImageReviews.js +14 -5
  20. package/dist/components/task/TaskKanban.d.ts +6 -1
  21. package/dist/components/task/TaskKanban.js +152 -6
  22. package/dist/components/task/TaskList.d.ts +5 -0
  23. package/dist/components/task/TaskList.js +3 -3
  24. package/dist/components/ui/PasswordInput.d.ts +6 -0
  25. package/dist/components/ui/PasswordInput.js +11 -0
  26. package/dist/components/ui/TopNoticeLayer.js +1 -1
  27. package/dist/components/views/AppShellHeader.js +32 -11
  28. package/dist/components/views/PlanComparisonPage.js +12 -0
  29. package/dist/components/views/StandaloneLayout.js +7 -31
  30. package/dist/components/views/WidgetView.js +3 -3
  31. package/dist/components/views/panels/PlanningDrawer.js +5 -12
  32. package/dist/components/views/standalone/modals/AccountSettingsModal.js +26 -17
  33. package/dist/config/envSchema.js +12 -0
  34. package/dist/config/localServiceRuntimeContract.d.ts +13 -0
  35. package/dist/config/localServiceRuntimeContract.js +135 -0
  36. package/dist/core/AiProfileService.js +46 -8
  37. package/dist/core/DeletedTaskLifecycleService.d.ts +1 -0
  38. package/dist/core/DeletedTaskLifecycleService.js +37 -6
  39. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +3 -2
  40. package/dist/core/TaskAttachmentLinksDurableMutationService.js +33 -9
  41. package/dist/core/Taskforce.d.ts +62 -2
  42. package/dist/core/Taskforce.js +635 -32
  43. package/dist/core/types.d.ts +6 -0
  44. package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +1 -1
  45. package/dist/hooks/sync/useDurableTaskCreateMutation.js +1 -1
  46. package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +1 -0
  47. package/dist/hooks/sync/useDurableTaskMetadataMutation.js +1 -0
  48. package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +1 -0
  49. package/dist/hooks/tasks/taskHttpMutation.js +1 -0
  50. package/dist/hooks/tasks/useCloudTaskSearch.d.ts +12 -0
  51. package/dist/hooks/tasks/useCloudTaskSearch.js +71 -0
  52. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.d.ts +16 -0
  53. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.js +197 -0
  54. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +16 -3
  55. package/dist/hooks/tasks/useTaskEditorNavigation.js +72 -13
  56. package/dist/hooks/tasks/useTaskFormActions.d.ts +6 -0
  57. package/dist/hooks/tasks/useTaskFormActions.js +65 -12
  58. package/dist/hooks/ui/useSettingsModel.d.ts +24 -2
  59. package/dist/hooks/ui/useSettingsModel.js +13 -2
  60. package/dist/hooks/ui/useSettingsPersistence.d.ts +12 -1
  61. package/dist/hooks/ui/useSettingsPersistence.js +74 -47
  62. package/dist/hooks/useFilterSort.d.ts +7 -3
  63. package/dist/hooks/useFilterSort.js +15 -5
  64. package/dist/hooks/useTaskData.d.ts +10 -1
  65. package/dist/hooks/useTaskData.js +216 -7
  66. package/dist/hooks/useTaskMutations.d.ts +10 -2
  67. package/dist/hooks/useTaskMutations.js +223 -43
  68. package/dist/hooks/useTaskforce.d.ts +37 -9
  69. package/dist/hooks/useTaskforce.js +349 -39
  70. package/dist/hooks/useUiNotice.d.ts +4 -0
  71. package/dist/hooks/useUiNotice.js +4 -0
  72. package/dist/mcp/adminWorkspaceRegistrar.js +3 -3
  73. package/dist/mcp/documentAssetRegistrar.js +44 -35
  74. package/dist/mcp/documentHelpers.d.ts +1 -0
  75. package/dist/mcp/documentHelpers.js +7 -3
  76. package/dist/mcp/runtime.js +172 -42
  77. package/dist/mcp/taskPlanningRegistrar.js +41 -38
  78. package/dist/mcp/toolCallCompatibility.js +38 -1
  79. package/dist/mcp/toolCatalog.d.ts +1 -0
  80. package/dist/migrations/taskSchemaMigrations.js +12 -0
  81. package/dist/runtime/appGateDefinitions.d.ts +30 -6
  82. package/dist/runtime/appGateDefinitions.js +6 -6
  83. package/dist/runtime/appGates.d.ts +12 -13
  84. package/dist/runtime/appGates.js +29 -13
  85. package/dist/runtime/nodeAppGates.d.ts +8 -0
  86. package/dist/runtime/nodeAppGates.js +27 -0
  87. package/dist/server/cloudCollectionRead.d.ts +1 -1
  88. package/dist/server/cloudCollectionRead.js +2 -2
  89. package/dist/server/index.d.ts +3 -0
  90. package/dist/server/index.js +15 -3
  91. package/dist/server/localServiceLease.d.ts +3 -0
  92. package/dist/server/localServiceLease.js +9 -1
  93. package/dist/server/localWorkspaceSyncServerRuntime.js +10 -3
  94. package/dist/server/rateLimit.d.ts +1 -0
  95. package/dist/server/rateLimit.js +4 -0
  96. package/dist/server/realtimeSyncWs.js +36 -12
  97. package/dist/server/routes/admin.js +1 -0
  98. package/dist/server/routes/agentRoles.js +3 -2
  99. package/dist/server/routes/agentSkills.js +3 -2
  100. package/dist/server/routes/agents.js +118 -16
  101. package/dist/server/routes/annotatedAttachments.js +3 -2
  102. package/dist/server/routes/auth.js +3 -1
  103. package/dist/server/routes/durableTaskHttpRouters.d.ts +9 -2
  104. package/dist/server/routes/localService.js +2 -0
  105. package/dist/server/routes/primitives.d.ts +2 -0
  106. package/dist/server/routes/syncAuxRoutes.js +5 -2
  107. package/dist/server/routes/syncPullApplyRoutes.js +23 -4
  108. package/dist/server/routes/syncV3FeedRoutes.js +18 -16
  109. package/dist/server/routes/syncV3LocalCaptureRoutes.js +28 -2
  110. package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +1 -0
  111. package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +26 -0
  112. package/dist/server/routes/syncV3OperationRoutes.js +10 -0
  113. package/dist/server/routes/tasks.js +103 -11
  114. package/dist/server/routes/workspaces.js +43 -1
  115. package/dist/server/workspaceDeletionAdmission.d.ts +23 -0
  116. package/dist/server/workspaceDeletionAdmission.js +50 -0
  117. package/dist/service/localServiceCli.js +16 -0
  118. package/dist/service/localServiceClientLifecycle.js +36 -10
  119. package/dist/services/codexAppServerModelGateway.d.ts +112 -0
  120. package/dist/services/codexAppServerModelGateway.js +438 -0
  121. package/dist/services/taskforceAgentMcpBridge.d.ts +7 -1
  122. package/dist/services/taskforceAgentMcpBridge.js +23 -3
  123. package/dist/shared/taskforceAgentDefinition.js +7 -1
  124. package/dist/shared/taskforceAgentModels.d.ts +1 -0
  125. package/dist/shared/taskforceAgentModels.js +30 -1
  126. package/dist/storage/postgresAdapter.d.ts +20 -3
  127. package/dist/storage/postgresAdapter.js +106 -14
  128. package/dist/storage/postgresWorker.js +13 -4
  129. package/dist/storage/postgresWorkerProtocol.d.ts +29 -0
  130. package/dist/storage/postgresWorkerProtocol.js +30 -0
  131. package/dist/sync/cloudSyncApi.d.ts +4 -1
  132. package/dist/sync/cloudSyncApi.js +7 -2
  133. package/dist/sync/contentSyncState.d.ts +1 -1
  134. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +1 -0
  135. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +7 -1
  136. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +6 -2
  137. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +20 -2
  138. package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +4 -0
  139. package/dist/sync/engine/workspaceSyncEngineLocalServices.js +11 -0
  140. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +11 -0
  141. package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +2 -1
  142. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +31 -4
  143. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +3 -1
  144. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +25 -8
  145. package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +1 -1
  146. package/dist/sync/engine/workspaceSyncEngineTransfers.js +8 -3
  147. package/dist/sync/syncService.d.ts +2 -0
  148. package/dist/sync/syncService.js +4 -1
  149. package/dist/sync/v3/combinedFeedCapability.js +4 -0
  150. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +2 -0
  151. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +11 -1
  152. package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +2 -0
  153. package/dist/sync/v3/durableTaskChecklistMutationRouter.js +4 -0
  154. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +1 -0
  155. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +30 -8
  156. package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +5 -3
  157. package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +27 -2
  158. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +2 -0
  159. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +47 -7
  160. package/dist/sync/v3/localTaskCreateOutboxService.js +15 -6
  161. package/dist/sync/v3/localTaskMetadataClient.d.ts +1 -0
  162. package/dist/sync/v3/localTaskMetadataClient.js +1 -0
  163. package/dist/sync/v3/localTaskMetadataOutboxHandler.js +5 -1
  164. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -0
  165. package/dist/sync/v3/localTaskMetadataOutboxService.js +7 -0
  166. package/dist/sync/v3/localTaskRestoreOutboxService.js +14 -6
  167. package/dist/sync/v3/localTaskStatusClient.d.ts +1 -0
  168. package/dist/sync/v3/localTaskStatusClient.js +6 -1
  169. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -0
  170. package/dist/sync/v3/localTaskStatusOutboxService.js +54 -13
  171. package/dist/sync/v3/syncDurabilityStore.d.ts +7 -0
  172. package/dist/sync/v3/syncDurabilityStore.js +31 -11
  173. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +6 -4
  174. package/dist/sync/v3/taskAttachmentLinksMutationService.js +19 -10
  175. package/dist/sync/v3/taskChecklistMutationService.d.ts +4 -2
  176. package/dist/sync/v3/taskChecklistMutationService.js +4 -0
  177. package/dist/sync/v3/taskCreateMutationService.d.ts +2 -2
  178. package/dist/sync/v3/taskCreateMutationService.js +7 -4
  179. package/dist/sync/v3/taskMetadataMutationService.d.ts +3 -0
  180. package/dist/sync/v3/taskMetadataMutationService.js +4 -0
  181. package/dist/sync/v3/taskRestoreMutationService.d.ts +2 -2
  182. package/dist/sync/v3/taskRestoreMutationService.js +7 -4
  183. package/dist/sync/v3/taskStatusMutationService.d.ts +2 -0
  184. package/dist/sync/v3/taskStatusMutationService.js +4 -0
  185. package/dist/sync/v3/workflowAssignmentSync.d.ts +1 -0
  186. package/dist/sync/v3/workflowAssignmentSync.js +3 -0
  187. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +1 -1
  188. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +13 -3
  189. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +11 -0
  190. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +27 -10
  191. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +2 -1
  192. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +52 -10
  193. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +9 -1
  194. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +8 -3
  195. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +51 -10
  196. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +7 -0
  197. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +30 -18
  198. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +9 -4
  199. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +98 -47
  200. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +10 -0
  201. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +58 -21
  202. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +3 -1
  203. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +18 -0
  204. package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +6 -0
  205. package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +1 -1
  206. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +1 -0
  207. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +23 -10
  208. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +3 -0
  209. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +64 -13
  210. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +9 -0
  211. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +26 -0
  212. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +15 -5
  213. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +50 -14
  214. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +2 -1
  215. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +11 -3
  216. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +2 -0
  217. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +2 -0
  218. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +19 -7
  219. package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +9 -2
  220. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.d.ts +7 -0
  221. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.js +37 -0
  222. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.d.ts +6 -0
  223. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.js +9 -0
  224. package/dist/sync/workspacePullFeed.js +2 -1
  225. package/dist/sync/workspaceRepair.d.ts +3 -0
  226. package/dist/sync/workspaceRepair.js +67 -6
  227. package/dist/types.d.ts +47 -1
  228. package/dist/types.js +6 -0
  229. package/dist/ui/assets/{AiIdentityRosterCard-MZs7lVED.js → AiIdentityRosterCard-BZM4aQnM.js} +1 -1
  230. package/dist/ui/assets/AiProfilesModule-BbAEWtHH.js +1 -0
  231. package/dist/ui/assets/AnnotatedAttachmentWorkspace-B68Wg-RV.js +3 -0
  232. package/dist/ui/assets/AnnotatedAttachmentWorkspace-TzYu7pkG.css +1 -0
  233. package/dist/ui/assets/{AssetTraySortControl-Kf1pelSy.js → AssetTraySortControl-C8Rztyal.js} +1 -1
  234. package/dist/ui/assets/{ContextAttachmentManager-B8boOs6Q.js → ContextAttachmentManager-Cj3c7X9y.js} +2 -2
  235. package/dist/ui/assets/{DocumentWorkspace-DXH4XUsB.js → DocumentWorkspace-B5uvzems.js} +1 -1
  236. package/dist/ui/assets/{EntityActivityTimeline-BtSTq0ON.js → EntityActivityTimeline-BVFvCB5R.js} +1 -1
  237. package/dist/ui/assets/PlanningModule-B5DWQOGz.js +1 -0
  238. package/dist/ui/assets/PlansPage-AY4cGWco.js +1 -0
  239. package/dist/ui/assets/TaskContextUpload-DZxTVesW.js +1 -0
  240. package/dist/ui/assets/TaskSettings-BeWAuyZj.js +12 -0
  241. package/dist/ui/assets/{TaskforceAgentsModule-BazdUwxZ.js → TaskforceAgentsModule-BifCYtjR.js} +6 -6
  242. package/dist/ui/assets/{WorkflowManagerModule-DWFspC-o.js → WorkflowManagerModule-DEr5di_r.js} +1 -1
  243. package/dist/ui/assets/index-I5zhdtgt.js +7 -0
  244. package/dist/ui/assets/index-VRFsx3wz.css +1 -0
  245. package/dist/ui/assets/{vendor-icons-BkFLXavV.js → vendor-icons-D9Lpw-j4.js} +1 -1
  246. package/dist/ui/index.html +3 -3
  247. package/dist/utils/taskActivity.d.ts +3 -2
  248. package/dist/utils/taskActivity.js +19 -2
  249. package/dist/utils/taskEditorReconciliation.d.ts +40 -0
  250. package/dist/utils/taskEditorReconciliation.js +217 -0
  251. package/dist/utils/taskNormalization.d.ts +1 -0
  252. package/dist/utils/taskNormalization.js +39 -0
  253. package/dist/utils/taskSearch.js +2 -2
  254. package/dist/utils/uiNotice.d.ts +4 -0
  255. package/package.json +16 -4
  256. package/dist/ui/assets/AiProfilesModule-C3i0LOxe.js +0 -1
  257. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +0 -1
  258. package/dist/ui/assets/AnnotatedAttachmentWorkspace-DVZk5Tln.js +0 -3
  259. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +0 -1
  260. package/dist/ui/assets/PlansPage-ChqgxALc.js +0 -1
  261. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +0 -1
  262. package/dist/ui/assets/TaskSettings-g4ECKNOz.js +0 -12
  263. package/dist/ui/assets/index-BUplSsv_.js +0 -7
  264. package/dist/ui/assets/index-gz2EXuoK.css +0 -1
@@ -186,6 +186,7 @@ export function registerLocalServiceRoutes(deps) {
186
186
  projectIdentity: context.projectIdentity,
187
187
  dataIdentity: context.ownershipKey,
188
188
  cloudTarget: context.cloudTarget,
189
+ runtimeContract: context.runtimeContract,
189
190
  transports: ['streamable-http'],
190
191
  health: {
191
192
  lifecycle: context.sessions.state(),
@@ -281,6 +282,7 @@ export function registerLocalServiceRoutes(deps) {
281
282
  protocolVersion: context.protocolVersion,
282
283
  ownerPid: context.ownerPid,
283
284
  cloudTarget: context.cloudTarget,
285
+ runtimeContract: context.runtimeContract,
284
286
  sessions: context.sessions.list(),
285
287
  ...(context.diagnostics?.() || {}),
286
288
  });
@@ -5,6 +5,7 @@ import type { LocalSyncCoordinatorRouteServices } from '../../sync/coordinator/l
5
5
  import type { WorkspaceSyncServerDataServices } from '../../sync/engine/workspaceSyncServerDataServices.js';
6
6
  import type { LocalServiceSessionManager } from '../localServiceSessions.js';
7
7
  import type { LocalServiceCloudTarget } from '../../config/localServiceCloudTarget.js';
8
+ import type { LocalServiceRuntimeContract } from '../../config/localServiceRuntimeContract.js';
8
9
  export interface LocalServiceRouteContext {
9
10
  generation: string;
10
11
  protocolVersion: string;
@@ -13,6 +14,7 @@ export interface LocalServiceRouteContext {
13
14
  bootstrapCapability: string;
14
15
  ownershipKey: string;
15
16
  cloudTarget: LocalServiceCloudTarget;
17
+ runtimeContract: LocalServiceRuntimeContract;
16
18
  projectIdentity: string;
17
19
  operationClientId: string;
18
20
  sessions: LocalServiceSessionManager;
@@ -50,8 +50,11 @@ export function registerWorkspaceSyncAuxRoutes({ addRoute, core, context, resolv
50
50
  res.end(JSON.stringify({ success: true }));
51
51
  });
52
52
  // POST /api/taskforce/sync/workspace/repair-startup - Auto-repair workspace asset/document
53
- // metadata on workspace open. Safe fixups re-register metadata for local files and mark
54
- // stale, unreferenced metadata deleted when the file is missing. Never deletes files.
53
+ // metadata on workspace open. Safe fixups re-register metadata for local files and clean up
54
+ // stale links. Does NOT tombstone metadata for files that appear missing locally a single
55
+ // failed existence check at startup can mean the content is only temporarily unavailable
56
+ // (e.g. storage not yet mounted/synced), not deleted; that requires explicit admin
57
+ // confirmation via /api/taskforce/admin/workspace-repair/apply. Never deletes files.
55
58
  // Called once by the sync orchestrator when the workspace becomes active.
56
59
  addRoute('POST', '/api/taskforce/sync/workspace/repair-startup', async (req, res) => {
57
60
  const authConfig = context.authConfig;
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_WORKSPACE_PULL_PAGE_MAX_BYTES, buildWorkspacePullFeed, WorkspacePullEntryTooLargeError, } from '../../sync/workspacePullFeed.js';
2
2
  import { applyWorkspaceSyncLocalBatch } from '../../sync/engine/workspaceSyncEngineLocalApply.js';
3
3
  import { parseJsonBody } from './primitives.js';
4
- import { normalizeWorkspaceSyncV2ProjectionExclusions, parseWorkspaceSyncV2ProjectionExclusions, WORKSPACE_SYNC_V3_COMBINED_V2_EXCLUSIONS, } from '../../sync/v3/workspaceSyncV2ProjectionExclusions.js';
4
+ import { normalizeWorkspaceSyncV2ProjectionExclusions, parseWorkspaceSyncV2ProjectionExclusions, WORKSPACE_SYNC_V3_COMBINED_V2_EXCLUSIONS, workspaceSyncV2ProjectionExclusionsEqual, } from '../../sync/v3/workspaceSyncV2ProjectionExclusions.js';
5
5
  import { resolveWorkspaceSyncV3AttestedV2Exclusions } from '../../sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js';
6
6
  import { requireAuthenticatedSyncRequest, requireEnabledSyncAuth, resolveAuthorizedSyncWorkspace, } from './syncRouteGuards.js';
7
7
  import { completeLocalSyncBrowserPushEchoPermitGroup, createLocalSyncBrowserV2ApplyTransactionHooks, parseLocalSyncBrowserApplyPermitContext, resolveLocalSyncBrowserApplyPermitOperationClass, } from '../../sync/coordinator/localSyncBrowserApplyPermitContext.js';
@@ -301,7 +301,16 @@ export function registerWorkspacePullApplySyncRoutes({ addRoute, core, context,
301
301
  };
302
302
  }
303
303
  if (!change) {
304
- missing.push(request);
304
+ missing.push({
305
+ ...request,
306
+ disposition: request.kind === 'asset'
307
+ && 'assetId' in request
308
+ && (!requestedAsset
309
+ || requestedAsset.deletedAt
310
+ || requestedAsset.storageProvider === 'external')
311
+ ? 'terminal-unavailable'
312
+ : 'retryable',
313
+ });
305
314
  consumed += 1;
306
315
  continue;
307
316
  }
@@ -554,6 +563,16 @@ export function registerWorkspacePullApplySyncRoutes({ addRoute, core, context,
554
563
  : null;
555
564
  let applyResult;
556
565
  if (permitContext && coordinatorServices && permitOperationClass === 'push') {
566
+ const attestedV2ExcludedProjections = v2ExcludedProjections.length > 0
567
+ ? (() => {
568
+ const attested = resolveWorkspaceSyncV3AttestedV2Exclusions({
569
+ coverage: body?.combinedCoverage,
570
+ excludedProjections: v2ExcludedProjections,
571
+ runtimeMode: 'local',
572
+ });
573
+ return attested && workspaceSyncV2ProjectionExclusionsEqual(attested, v2ExcludedProjections) ? attested : null;
574
+ })()
575
+ : [];
557
576
  applyResult = completeLocalSyncBrowserPushEchoPermitGroup({
558
577
  services: coordinatorServices,
559
578
  cookieHeader: req.headers.cookie,
@@ -567,13 +586,13 @@ export function registerWorkspacePullApplySyncRoutes({ addRoute, core, context,
567
586
  || workspaceMembers !== undefined
568
587
  || body?.bootstrapSnapshot !== undefined
569
588
  || allowReferenceNumberReassignment
570
- || v2ExcludedProjections.length > 0) {
589
+ || !attestedV2ExcludedProjections) {
571
590
  throw new Error('Browser push echo apply accepts only authoritative task upserts.');
572
591
  }
573
592
  return dispatchWorkspaceSyncMetadataAtomically(workspaceId, incomingChanges, {
574
593
  allowReferenceNumberReassignment: false,
575
594
  coexistenceDirection: 'local-pull',
576
- v2ExcludedProjections: [],
595
+ v2ExcludedProjections: attestedV2ExcludedProjections,
577
596
  });
578
597
  },
579
598
  });
@@ -5,7 +5,7 @@ import { buildWorkspaceSyncV3InitiativeRepairPage, decodeWorkspaceSyncV3Initiati
5
5
  import { WORKSPACE_SYNC_V3_COVERAGE_PROFILE } from '../../sync/v3/workspaceSyncV3FeedProfile.js';
6
6
  import { isWorkspaceSyncV3InitiativeLegacyPushDisabled } from '../../sync/v3/planningActivation.js';
7
7
  import { WORKSPACE_SYNC_V3_COMBINED_COVERAGE_PROFILE } from '../../sync/v3/workspaceSyncV3CombinedCoverage.js';
8
- import { buildWorkspaceSyncV3CombinedRepairPage, decodeWorkspaceSyncV3CombinedRepairCursor, decodeWorkspaceSyncV3NestedRepairCursor, decodeWorkspaceSyncV3PlanningOrderRepairCursor, decodeWorkspaceSyncV3WorkflowRepairCursor, WorkspaceSyncV3CombinedRepairSnapshotUnavailableError, } from '../../sync/v3/workspaceSyncV3CombinedRepairMaterialization.js';
8
+ import { buildWorkspaceSyncV3CombinedRepairPage, decodeWorkspaceSyncV3CombinedRepairCursor, decodeWorkspaceSyncV3NestedRepairCursor, decodeWorkspaceSyncV3PlanningOrderRepairCursor, decodeWorkspaceSyncV3TaskCoverRepairCursor, decodeWorkspaceSyncV3WorkflowRepairCursor, WorkspaceSyncV3CombinedRepairSnapshotUnavailableError, } from '../../sync/v3/workspaceSyncV3CombinedRepairMaterialization.js';
9
9
  import { isWorkspaceSyncV3CombinedLegacyFenceEnabled } from '../../sync/v3/workspaceSyncV2ProjectionExclusions.js';
10
10
  import { resolveCombinedFeedCapability } from '../../sync/v3/combinedFeedCapability.js';
11
11
  import { resolveWorkspaceSyncV3RegisteredCoverage } from '../../sync/v3/workspaceSyncV3CoverageRegistry.js';
@@ -21,6 +21,17 @@ function json(res, status, body) {
21
21
  res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
22
22
  res.end(JSON.stringify(body));
23
23
  }
24
+ function decodeCombinedRepairCursorForContract(rawCursor, keyring, wire) {
25
+ if (wire.taskCoverProjectionRequired)
26
+ return decodeWorkspaceSyncV3TaskCoverRepairCursor(rawCursor, keyring);
27
+ if (wire.workflowWriterPrerequisitesRequired)
28
+ return decodeWorkspaceSyncV3WorkflowRepairCursor(rawCursor, keyring);
29
+ if (wire.planningOrderWriterPrerequisitesRequired)
30
+ return decodeWorkspaceSyncV3PlanningOrderRepairCursor(rawCursor, keyring);
31
+ if (wire.nestedWriterPrerequisitesRequired)
32
+ return decodeWorkspaceSyncV3NestedRepairCursor(rawCursor, keyring);
33
+ return decodeWorkspaceSyncV3CombinedRepairCursor(rawCursor, keyring);
34
+ }
24
35
  function attestLegacyPushBoundary(body, boundary) {
25
36
  const attested = { ...body, ...boundary };
26
37
  const mutable = attested;
@@ -215,13 +226,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
215
226
  return;
216
227
  }
217
228
  const rawCursor = String(url.searchParams.get('cursor') || '').trim();
218
- const cursor = requestedWire.workflowWriterPrerequisitesRequired
219
- ? decodeWorkspaceSyncV3WorkflowRepairCursor(rawCursor, keyring)
220
- : requestedWire.planningOrderWriterPrerequisitesRequired
221
- ? decodeWorkspaceSyncV3PlanningOrderRepairCursor(rawCursor, keyring)
222
- : requestedWire.nestedWriterPrerequisitesRequired
223
- ? decodeWorkspaceSyncV3NestedRepairCursor(rawCursor, keyring)
224
- : decodeWorkspaceSyncV3CombinedRepairCursor(rawCursor, keyring);
229
+ const cursor = decodeCombinedRepairCursorForContract(rawCursor, keyring, requestedWire);
225
230
  if (rawCursor && !cursor) {
226
231
  json(res, 400, { success: false, code: 'INVALID_SYNC_COMBINED_REPAIR_CURSOR', error: 'Invalid combined repair cursor.' });
227
232
  return;
@@ -231,7 +236,8 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
231
236
  cursorKeyring: keyring, cursor, pageSize: Number.isFinite(limit) ? limit : undefined,
232
237
  nestedCoverage: requestedWire.nestedWriterPrerequisitesRequired,
233
238
  workflowCoverage: requestedWire.workflowWriterPrerequisitesRequired,
234
- planningOrderCoverage: requestedWire.planningOrderWriterPrerequisitesRequired });
239
+ planningOrderCoverage: requestedWire.planningOrderWriterPrerequisitesRequired,
240
+ taskCoverCoverage: requestedWire.taskCoverProjectionRequired });
235
241
  json(res, 200, attestLegacyPushBoundary({ ...page }, {
236
242
  contractVersion: 3,
237
243
  legacyV2Boundary: { cloudWritesFenced: false, excludedProjections: [] },
@@ -328,13 +334,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
328
334
  return;
329
335
  }
330
336
  const repairCursor = coverage.kind === 'combined'
331
- ? (coverage.wire.workflowWriterPrerequisitesRequired
332
- ? decodeWorkspaceSyncV3WorkflowRepairCursor(rawRepairCursor, cursorKeyring)
333
- : coverage.wire.planningOrderWriterPrerequisitesRequired
334
- ? decodeWorkspaceSyncV3PlanningOrderRepairCursor(rawRepairCursor, cursorKeyring)
335
- : coverage.wire.nestedWriterPrerequisitesRequired
336
- ? decodeWorkspaceSyncV3NestedRepairCursor(rawRepairCursor, cursorKeyring)
337
- : decodeWorkspaceSyncV3CombinedRepairCursor(rawRepairCursor, cursorKeyring))
337
+ ? decodeCombinedRepairCursorForContract(rawRepairCursor, cursorKeyring, coverage.wire)
338
338
  : decodeWorkspaceSyncV3InitiativeRepairCursor(rawRepairCursor, cursorKeyring);
339
339
  if (rawRepairCursor && (!repairEnabled || !repairCursor)) {
340
340
  json(res, 400, { success: false, code: 'INVALID_SYNC_V3_REPAIR_CURSOR', error: 'Invalid repair cursor.' });
@@ -353,6 +353,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
353
353
  nestedCoverage: coverage.wire.nestedWriterPrerequisitesRequired,
354
354
  workflowCoverage: coverage.wire.workflowWriterPrerequisitesRequired,
355
355
  planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired,
356
+ taskCoverCoverage: coverage.wire.taskCoverProjectionRequired,
356
357
  coveredLegacyWritesFenced: combinedLegacyPushDisabled })
357
358
  : buildWorkspaceSyncV3InitiativeRepairPage({ core: input.core, workspaceId,
358
359
  cursorKeyring: cursorKeyring, cursor: repairCursor,
@@ -399,6 +400,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
399
400
  nestedCoverage: coverage.wire.nestedWriterPrerequisitesRequired,
400
401
  workflowCoverage: coverage.wire.workflowWriterPrerequisitesRequired,
401
402
  planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired,
403
+ taskCoverCoverage: coverage.wire.taskCoverProjectionRequired,
402
404
  coveredLegacyWritesFenced: combinedLegacyPushDisabled })
403
405
  : buildWorkspaceSyncV3InitiativeRepairPage({ core: input.core, workspaceId,
404
406
  cursorKeyring: cursorKeyring, pageSize: Number.isFinite(limit) ? limit : undefined });
@@ -96,7 +96,14 @@ function parseEnvelope(raw) {
96
96
  };
97
97
  if (operation.domain === 'task' && operation.mutationKind === 'metadata-update') {
98
98
  const saveSource = payload.saveSource;
99
- assertExactKeys(payload, saveSource === undefined ? ['patch'] : ['patch', 'saveSource'], 'operation.payload');
99
+ const expectedDetailRevision = payload.expectedDetailRevision === undefined
100
+ ? undefined
101
+ : requireId(payload.expectedDetailRevision, 'operation.payload.expectedDetailRevision');
102
+ assertExactKeys(payload, [
103
+ 'patch',
104
+ ...(saveSource === undefined ? [] : ['saveSource']),
105
+ ...(expectedDetailRevision === undefined ? [] : ['expectedDetailRevision']),
106
+ ], 'operation.payload');
100
107
  if (saveSource !== undefined && saveSource !== 'manual' && saveSource !== 'autosave') {
101
108
  throw new LocalCaptureRequestError('operation.payload.saveSource must be manual or autosave.');
102
109
  }
@@ -106,6 +113,7 @@ function parseEnvelope(raw) {
106
113
  mutationKind: 'metadata-update',
107
114
  patch: requireRecord(payload.patch, 'operation.payload.patch'),
108
115
  ...(saveSource ? { saveSource } : {}),
116
+ ...(expectedDetailRevision ? { expectedDetailRevision } : {}),
109
117
  };
110
118
  }
111
119
  if (operation.domain === 'task' && operation.mutationKind === 'create') {
@@ -170,11 +178,27 @@ function parseEnvelope(raw) {
170
178
  };
171
179
  }
172
180
  if (operation.domain === 'task' && operation.mutationKind === 'set-status') {
181
+ const expectedDetailRevision = payload.expectedDetailRevision === undefined
182
+ ? undefined
183
+ : requireId(payload.expectedDetailRevision, 'operation.payload.expectedDetailRevision');
184
+ assertExactKeys(payload, [
185
+ 'status',
186
+ 'assigneeIntent',
187
+ 'canceledReason',
188
+ 'metadataPatch',
189
+ ...(expectedDetailRevision === undefined ? [] : ['expectedDetailRevision']),
190
+ ], 'operation.payload');
173
191
  return {
174
192
  ...common,
175
193
  domain: 'task',
176
194
  mutationKind: 'set-status',
177
- payload: validateDurableTaskStatusMutationPayload(payload),
195
+ payload: validateDurableTaskStatusMutationPayload({
196
+ status: payload.status,
197
+ assigneeIntent: payload.assigneeIntent,
198
+ canceledReason: payload.canceledReason,
199
+ metadataPatch: payload.metadataPatch,
200
+ }),
201
+ ...(expectedDetailRevision ? { expectedDetailRevision } : {}),
178
202
  };
179
203
  }
180
204
  if (operation.domain === 'task-comments' && operation.mutationKind === 'append') {
@@ -731,6 +755,7 @@ export function registerWorkspaceSyncV3LocalCaptureRoutes(input) {
731
755
  taskId: envelope.entityId,
732
756
  patch: envelope.patch,
733
757
  saveSource: envelope.saveSource,
758
+ expectedDetailRevision: envelope.expectedDetailRevision,
734
759
  now: acceptedAt,
735
760
  actorRef: 'user',
736
761
  onPostCommitError: input.onPostCommitError,
@@ -786,6 +811,7 @@ export function registerWorkspaceSyncV3LocalCaptureRoutes(input) {
786
811
  identity,
787
812
  taskId: envelope.entityId,
788
813
  payload: envelope.payload,
814
+ expectedDetailRevision: envelope.expectedDetailRevision,
789
815
  now: acceptedAt,
790
816
  actorRef: 'user',
791
817
  onPostCommitError: input.onPostCommitError,
@@ -1,5 +1,6 @@
1
1
  import type { SyncAddRoute, SyncRouteAuthConfig, SyncRouteCore } from './syncRouteTypes.js';
2
2
  import type { LocalSyncCoordinatorRouteServices } from '../../sync/coordinator/localSyncCoordinatorRouteServices.js';
3
+ export declare function revisionConflictDetails(error: unknown): Record<string, unknown> | undefined;
3
4
  export declare function isWorkspaceSyncV3LocalFeedApplyEnabled(env?: NodeJS.ProcessEnv): boolean;
4
5
  export declare function isWorkspaceSyncV3CombinedLocalFeedApplyEnabled(env?: NodeJS.ProcessEnv): boolean;
5
6
  export declare function registerWorkspaceSyncV3LocalFeedApplyRoutes(input: {
@@ -8,6 +8,7 @@ import { resolveCombinedFeedCapability, resolveRequestedCombinedFeedCoverage } f
8
8
  import { parseJsonBody } from './primitives.js';
9
9
  import { applyWorkspaceSyncV3TaskCommentAuditPage, readWorkspaceSyncV3TaskCommentAuditCursor, readWorkspaceSyncV3TaskCommentAuditState, } from '../../sync/v3/workspaceSyncV3TaskCommentAudit.js';
10
10
  import { createLocalSyncBrowserApplyPermitTransactionHooks, parseLocalSyncBrowserApplyPermitContext, } from '../../sync/coordinator/localSyncBrowserApplyPermitContext.js';
11
+ import { WorkspaceSyncRevisionConflictError } from '../../sync/v3/syncDurabilityStore.js';
11
12
  const ROUTE_PATH = '/api/taskforce/sync/v3/local/feed';
12
13
  const TASK_COMMENT_AUDIT_PATH = '/api/taskforce/sync/v3/local/task-comment-audit';
13
14
  const MAX_BODY_BYTES = 8 * 1024 * 1024;
@@ -15,6 +16,29 @@ function writeJson(res, statusCode, body) {
15
16
  res.writeHead(statusCode, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
16
17
  res.end(JSON.stringify(body));
17
18
  }
19
+ export function revisionConflictDetails(error) {
20
+ if (!(error instanceof WorkspaceSyncRevisionConflictError) || !error.details)
21
+ return undefined;
22
+ const details = error.details;
23
+ const coordinates = (value) => {
24
+ if (!value || typeof value !== 'object' || Array.isArray(value))
25
+ return undefined;
26
+ const source = value;
27
+ const sequence = source.sequence ?? source.lastSequence;
28
+ return {
29
+ sequence: sequence == null ? null : String(sequence),
30
+ entityRevision: source.entityRevision == null ? null : String(source.entityRevision),
31
+ lifecycleRevision: source.lifecycleRevision == null ? null : String(source.lifecycleRevision),
32
+ };
33
+ };
34
+ return {
35
+ domain: String(details.domain || ''),
36
+ entityId: String(details.entityId || ''),
37
+ reason: String(details.reason || ''),
38
+ ...(coordinates(details.incoming) ? { incoming: coordinates(details.incoming) } : {}),
39
+ ...(coordinates(details.current) ? { current: coordinates(details.current) } : {}),
40
+ };
41
+ }
18
42
  function workspaceFromUrl(req) {
19
43
  try {
20
44
  return String(new URL(req.url || ROUTE_PATH, 'http://localhost').searchParams.get('workspaceId') || '').trim();
@@ -481,12 +505,14 @@ export function registerWorkspaceSyncV3LocalFeedApplyRoutes(input) {
481
505
  catch (error) {
482
506
  const causeCode = String(error?.code || '').trim();
483
507
  const causeAssetId = String(error?.assetId || '').trim();
508
+ const causeDetails = revisionConflictDetails(error);
484
509
  writeJson(res, 409, {
485
510
  success: false,
486
511
  code: 'WORKSPACE_SYNC_V3_LOCAL_FEED_APPLY_REJECTED',
487
512
  error: String(error?.message || error),
488
513
  ...(causeCode ? { causeCode } : {}),
489
514
  ...(causeAssetId ? { causeAssetId } : {}),
515
+ ...(causeDetails ? { causeDetails } : {}),
490
516
  });
491
517
  }
492
518
  });
@@ -157,9 +157,13 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
157
157
  const payload = requireObject(operation.payload, 'operation.payload');
158
158
  if (operation.domain === 'task' && operation.mutationKind === 'metadata-update') {
159
159
  const saveSource = payload.saveSource;
160
+ const expectedDetailRevision = payload.expectedDetailRevision === undefined
161
+ ? undefined
162
+ : requireId(payload.expectedDetailRevision, 'operation.payload.expectedDetailRevision');
160
163
  assertExactKeys(payload, [
161
164
  'patch',
162
165
  ...(saveSource === undefined ? [] : ['saveSource']),
166
+ ...(expectedDetailRevision === undefined ? [] : ['expectedDetailRevision']),
163
167
  ...(payload.originActor === undefined ? [] : ['originActor']),
164
168
  ], 'operation.payload');
165
169
  if (saveSource !== undefined && saveSource !== 'manual' && saveSource !== 'autosave') {
@@ -173,6 +177,7 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
173
177
  payload: {
174
178
  patch: requireObject(payload.patch, 'operation.payload.patch'),
175
179
  ...(saveSource ? { saveSource } : {}),
180
+ ...(expectedDetailRevision ? { expectedDetailRevision } : {}),
176
181
  ...(originActor ? { originActor } : {}),
177
182
  },
178
183
  } };
@@ -364,6 +369,7 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
364
369
  ...baseKeys,
365
370
  ...(payload.workflowAssignment === undefined ? [] : ['workflowAssignment']),
366
371
  ...(payload.originActor === undefined ? [] : ['originActor']),
372
+ ...(payload.expectedDetailRevision === undefined ? [] : ['expectedDetailRevision']),
367
373
  ], 'operation.payload');
368
374
  const normalizedPayload = validateDurableTaskStatusMutationPayload({
369
375
  status: payload.status,
@@ -382,12 +388,16 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
382
388
  const originActor = payload.originActor === undefined
383
389
  ? undefined
384
390
  : requireOriginActor(payload.originActor);
391
+ const expectedDetailRevision = payload.expectedDetailRevision === undefined
392
+ ? undefined
393
+ : requireId(payload.expectedDetailRevision, 'operation.payload.expectedDetailRevision');
385
394
  return { ...common, operation: {
386
395
  domain: 'task', entityId, mutationKind: 'set-status', baseEntityRevision, baseLifecycleRevision,
387
396
  payload: {
388
397
  ...normalizedPayload,
389
398
  ...(workflowAssignment ? { workflowAssignment } : {}),
390
399
  ...(originActor ? { originActor } : {}),
400
+ ...(expectedDetailRevision ? { expectedDetailRevision } : {}),
391
401
  },
392
402
  } };
393
403
  }
@@ -470,9 +470,66 @@ export function registerTaskRoutes(deps) {
470
470
  res,
471
471
  route: '/api/taskforce/tasks',
472
472
  workspaceId,
473
- load: () => core.listTasks(workspaceId),
473
+ // Local V2 sync still builds an outbound snapshot from these collections.
474
+ // Keep its full graph until that writer is retired; cloud is read-authoritative.
475
+ load: () => (context.authConfig?.runtimeMode === 'cloud' ? 'cloud' : requestRuntimeMode(req)) === 'cloud'
476
+ ? core.listActiveTaskSummaries(workspaceId)
477
+ : core.listTasks(workspaceId),
474
478
  });
475
479
  });
480
+ // GET /api/taskforce/tasks/search - Search lightweight task summaries across lifecycles
481
+ addRoute('GET', '/api/taskforce/tasks/search', async (req, res) => {
482
+ if (!ensureAppAccess(req, res))
483
+ return;
484
+ try {
485
+ const requestUrl = new URL(req.url || '', 'http://localhost');
486
+ const query = String(requestUrl.searchParams.get('query') || '').trim();
487
+ if (!query) {
488
+ throw new TaskforceRuleError('query is required.', 400, 'TASK_SEARCH_QUERY_REQUIRED');
489
+ }
490
+ if (query.length > 200) {
491
+ throw new TaskforceRuleError('query must be 200 characters or fewer.', 400, 'TASK_SEARCH_QUERY_TOO_LONG');
492
+ }
493
+ const scopeInput = String(requestUrl.searchParams.get('scope') || 'all').trim();
494
+ if (!['all', 'active', 'archive'].includes(scopeInput)) {
495
+ throw new TaskforceRuleError('scope must be all, active, or archive.', 400, 'TASK_SEARCH_SCOPE_INVALID');
496
+ }
497
+ const parseBoundedInteger = (name, fallback, max) => {
498
+ const raw = requestUrl.searchParams.get(name);
499
+ if (raw === null || raw === '')
500
+ return fallback;
501
+ const parsed = Number(raw);
502
+ if (!Number.isInteger(parsed) || parsed < (name === 'limit' ? 1 : 0) || parsed > max) {
503
+ throw new TaskforceRuleError(`${name} must be an integer between ${name === 'limit' ? 1 : 0} and ${max}.`, 400, `TASK_SEARCH_${name.toUpperCase()}_INVALID`);
504
+ }
505
+ return parsed;
506
+ };
507
+ const limit = parseBoundedInteger('limit', 50, 50);
508
+ const offset = parseBoundedInteger('offset', 0, 10_000);
509
+ const workspaceId = requestWorkspaceId(req);
510
+ const rows = core.searchTaskSummaries(query, scopeInput, workspaceId, { limit: limit + 1, offset });
511
+ const hasMore = rows.length > limit;
512
+ const matches = rows.slice(0, limit);
513
+ res.writeHead(200, { 'Content-Type': 'application/json' });
514
+ res.end(JSON.stringify({
515
+ matches,
516
+ pagination: {
517
+ limit,
518
+ offset,
519
+ returned: matches.length,
520
+ hasMore,
521
+ nextOffset: hasMore ? offset + matches.length : null,
522
+ },
523
+ }));
524
+ }
525
+ catch (error) {
526
+ const statusCode = resolveErrorStatusCode(error, 400);
527
+ const message = error instanceof Error ? error.message : 'Failed to search tasks.';
528
+ const code = error instanceof TaskforceRuleError ? error.code : error?.code;
529
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
530
+ res.end(JSON.stringify({ error: message, code }));
531
+ }
532
+ });
476
533
  addRoute('GET', '/api/taskforce/task-relationships', async (req, res) => {
477
534
  if (!ensureAppAccess(req, res))
478
535
  return;
@@ -617,6 +674,20 @@ export function registerTaskRoutes(deps) {
617
674
  }));
618
675
  }
619
676
  });
677
+ // GET /api/taskforce/task/:id - Read complete active or archived task detail
678
+ addRoute('GET', '/api/taskforce/task/:id', async (req, res, params) => {
679
+ if (!ensureAppAccess(req, res))
680
+ return;
681
+ const workspaceId = requestWorkspaceId(req);
682
+ const task = core.getTaskDetailSnapshot(params.id, workspaceId);
683
+ if (!task) {
684
+ res.writeHead(404, { 'Content-Type': 'application/json' });
685
+ res.end(JSON.stringify({ error: 'Task not found' }));
686
+ return;
687
+ }
688
+ res.writeHead(200, { 'Content-Type': 'application/json' });
689
+ res.end(JSON.stringify(task));
690
+ });
620
691
  // PATCH /api/taskforce/task/:id - Update a task
621
692
  addRoute('PATCH', '/api/taskforce/task/:id', async (req, res, params) => {
622
693
  if (!ensureAppAccess(req, res))
@@ -625,8 +696,15 @@ export function registerTaskRoutes(deps) {
625
696
  try {
626
697
  const workspaceId = requestWorkspaceId(req);
627
698
  const saveSource = body?.saveSource === 'autosave' ? 'autosave' : 'manual';
699
+ const expectedDetailRevision = typeof body?.expectedDetailRevision === 'string'
700
+ ? body.expectedDetailRevision.trim()
701
+ : null;
702
+ if (body && Object.prototype.hasOwnProperty.call(body, 'expectedDetailRevision')
703
+ && !expectedDetailRevision) {
704
+ throw new TaskforceRuleError('expectedDetailRevision must be a non-empty string.', 400, 'TASK_DETAIL_REVISION_INVALID');
705
+ }
628
706
  const updates = body && typeof body === 'object'
629
- ? Object.fromEntries(Object.entries(body).filter(([key]) => key !== 'saveSource'))
707
+ ? Object.fromEntries(Object.entries(body).filter(([key]) => (key !== 'saveSource' && key !== 'expectedDetailRevision')))
630
708
  : body;
631
709
  const durableRouters = createTaskHttpDurableRouters({ core, context, req, workspaceId });
632
710
  const durableRouter = durableRouters.root;
@@ -658,10 +736,14 @@ export function registerTaskRoutes(deps) {
658
736
  const durableTargetCount = Number(checklistDurable)
659
737
  + Number(attachmentLinksDurable)
660
738
  + Number(Object.keys(remainingUpdates).length > 0);
661
- if (durableTargetCount > 1) {
739
+ if (durableTargetCount > 1 || Boolean(expectedDetailRevision && (checklistDurable || attachmentLinksDurable))) {
662
740
  const aggregate = durableRouters.bulk.updateFields([
663
741
  { id: params.id, ...updates },
664
- ], { saveSource, atomicItemFailures: true });
742
+ ], {
743
+ saveSource,
744
+ atomicItemFailures: true,
745
+ ...(expectedDetailRevision ? { expectedDetailRevision } : {}),
746
+ });
665
747
  const failure = aggregate.errors[0];
666
748
  if (failure) {
667
749
  throw new TaskforceRuleError(failure.message, failure.statusCode, failure.code || 'TASK_UPDATE_FAILED', failure.details);
@@ -671,11 +753,13 @@ export function registerTaskRoutes(deps) {
671
753
  else {
672
754
  updated = Object.keys(remainingUpdates || {}).length > 0
673
755
  ? (durableRouter.enabled
674
- ? durableRouter.update(params.id, remainingUpdates, saveSource)
675
- : core.updateTask(params.id, remainingUpdates, workspaceId, {
676
- actorRef: requestHumanActorRef(req),
677
- saveSource,
678
- }))
756
+ ? durableRouter.update(params.id, remainingUpdates, saveSource, undefined, expectedDetailRevision || undefined)
757
+ : expectedDetailRevision !== null
758
+ ? core.updateTaskIfDetailRevisionMatches(params.id, expectedDetailRevision, remainingUpdates, workspaceId, { actorRef: requestHumanActorRef(req), saveSource })
759
+ : core.updateTask(params.id, remainingUpdates, workspaceId, {
760
+ actorRef: requestHumanActorRef(req),
761
+ saveSource,
762
+ }))
679
763
  : core.getTask(params.id, workspaceId);
680
764
  if (updated && checklistDurable) {
681
765
  const checklistRouter = durableRouters.createChecklistRouter(durableRouters.deriveChildOperationId('task-patch-checklist', 0, params.id));
@@ -711,8 +795,14 @@ export function registerTaskRoutes(deps) {
711
795
  res.end(JSON.stringify({ error: 'Task not found' }));
712
796
  return;
713
797
  }
798
+ const authoritative = core.getTaskDetailSnapshot(params.id, workspaceId);
799
+ if (!authoritative) {
800
+ res.writeHead(404, { 'Content-Type': 'application/json' });
801
+ res.end(JSON.stringify({ error: 'Task not found' }));
802
+ return;
803
+ }
714
804
  res.writeHead(200, { 'Content-Type': 'application/json' });
715
- res.end(JSON.stringify(updated));
805
+ res.end(JSON.stringify(authoritative));
716
806
  }
717
807
  catch (error) {
718
808
  const statusCode = resolveErrorStatusCode(error, 400);
@@ -1291,7 +1381,9 @@ export function registerTaskRoutes(deps) {
1291
1381
  res,
1292
1382
  route: '/api/taskforce/archive',
1293
1383
  workspaceId,
1294
- load: () => core.listArchive(workspaceId),
1384
+ load: () => (context.authConfig?.runtimeMode === 'cloud' ? 'cloud' : requestRuntimeMode(req)) === 'cloud'
1385
+ ? core.listArchiveSummaries(workspaceId)
1386
+ : core.listArchive(workspaceId),
1295
1387
  });
1296
1388
  });
1297
1389
  // POST /api/taskforce/task/:id/complete - Archive as complete
@@ -17,9 +17,11 @@ import { parseJsonBody } from './primitives.js';
17
17
  import { resolveRequestAccess, createSessionTokenCookie, resolveSessionCookieAttributesForRequest, } from '../auth.js';
18
18
  import { isWorkspaceSwitchingEnabledForRuntime } from '../../shared/runtimeContract.js';
19
19
  import { getRouteTimingNow, logSlowBootstrapRoute, requestBootstrapTraceId, resolveErrorStatusCode, serializeJsonWithTiming, setServerTimingHeader } from './shared.js';
20
+ import { WorkspaceDeletionAdmission } from '../workspaceDeletionAdmission.js';
20
21
  // === SECTION: Registration ===
21
22
  export function registerWorkspaceRoutes(deps) {
22
23
  const { addRoute, core, context, ensureAppAccess, auditAuthAction, getObjectStorageClient, ensureWithinDir, sanitizeStorageSegment, sanitizeTaskIdForPath, normalizeDocumentPathInput, } = deps;
24
+ const workspaceDeletionAdmission = new WorkspaceDeletionAdmission();
23
25
  // === SECTION: Storage cleanup helper ===
24
26
  const cleanupDeletedWorkspaceStorage = async (summary) => {
25
27
  const warnings = [];
@@ -231,12 +233,49 @@ export function registerWorkspaceRoutes(deps) {
231
233
  res.end(JSON.stringify({ success: false, error: 'workspaceId is required.', code: 'WORKSPACE_ID_REQUIRED' }));
232
234
  return;
233
235
  }
234
- const resolvedAccess = core.resolveUserAccess(access.userId, targetWorkspaceId, 'member');
236
+ const writeAdmissionError = (decision) => {
237
+ res.setHeader('Retry-After', String(Math.max(1, Math.ceil(decision.retryAfterMs / 1_000))));
238
+ const inProgress = decision.code === 'WORKSPACE_DELETE_IN_PROGRESS';
239
+ res.writeHead(inProgress ? 409 : 429, { 'Content-Type': 'application/json' });
240
+ res.end(JSON.stringify({
241
+ success: false,
242
+ error: inProgress
243
+ ? 'Workspace deletion is already running for this workspace.'
244
+ : 'Workspace deletion was attempted recently. Wait before retrying.',
245
+ code: decision.code,
246
+ retryAfterMs: decision.retryAfterMs,
247
+ }));
248
+ };
249
+ const existingAdmission = workspaceDeletionAdmission.checkExisting(targetWorkspaceId);
250
+ if (existingAdmission) {
251
+ writeAdmissionError(existingAdmission);
252
+ return;
253
+ }
254
+ let resolvedAccess;
255
+ try {
256
+ resolvedAccess = core.resolveUserAccess(access.userId, targetWorkspaceId, 'member');
257
+ }
258
+ catch (error) {
259
+ const statusCode = resolveErrorStatusCode(error, 400);
260
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
261
+ res.end(JSON.stringify({
262
+ success: false,
263
+ error: String(error?.message || 'Unable to resolve workspace access'),
264
+ code: error?.code,
265
+ }));
266
+ return;
267
+ }
235
268
  if (!resolvedAccess || resolvedAccess.role !== 'owner' || resolvedAccess.disabled) {
236
269
  res.writeHead(403, { 'Content-Type': 'application/json' });
237
270
  res.end(JSON.stringify({ success: false, error: 'Only workspace owners can delete this workspace.' }));
238
271
  return;
239
272
  }
273
+ const deletionAdmissionKey = targetWorkspaceId;
274
+ const admission = workspaceDeletionAdmission.acquire(deletionAdmissionKey);
275
+ if (!admission.allowed) {
276
+ writeAdmissionError(admission);
277
+ return;
278
+ }
240
279
  try {
241
280
  const deleted = core.deleteWorkspace({ workspaceId: targetWorkspaceId });
242
281
  const cleanupWarnings = await cleanupDeletedWorkspaceStorage(deleted);
@@ -277,6 +316,9 @@ export function registerWorkspaceRoutes(deps) {
277
316
  code: error?.code
278
317
  }));
279
318
  }
319
+ finally {
320
+ workspaceDeletionAdmission.settle(deletionAdmissionKey);
321
+ }
280
322
  });
281
323
  // === SECTION: Session workspace switch ===
282
324
  // POST /api/taskforce/session/workspace - Switch active workspace in auth session
@@ -0,0 +1,23 @@
1
+ export declare const WORKSPACE_DELETE_RETRY_COOLDOWN_MS = 30000;
2
+ export type WorkspaceDeletionAdmissionDecision = {
3
+ allowed: true;
4
+ } | {
5
+ allowed: false;
6
+ code: 'WORKSPACE_DELETE_IN_PROGRESS' | 'WORKSPACE_DELETE_RETRY_COOLDOWN';
7
+ retryAfterMs: number;
8
+ };
9
+ /**
10
+ * Serializes destructive workspace cleanup inside one server process and
11
+ * throttles immediate retries after an attempt settles. The caller supplies
12
+ * time so the admission policy can be verified without timers.
13
+ */
14
+ export declare class WorkspaceDeletionAdmission {
15
+ private readonly cooldownMs;
16
+ private readonly entries;
17
+ constructor(cooldownMs?: number);
18
+ checkExisting(key: string, now?: number): Exclude<WorkspaceDeletionAdmissionDecision, {
19
+ allowed: true;
20
+ }> | null;
21
+ acquire(key: string, now?: number): WorkspaceDeletionAdmissionDecision;
22
+ settle(key: string, now?: number): void;
23
+ }