@taskforcehq/taskforce 0.3.327 → 0.3.328

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 (187) hide show
  1. package/dist/Taskforce.module.css +24 -53
  2. package/dist/components/activity/EntityActivityTimeline.js +15 -5
  3. package/dist/components/context/AttachmentPreviewHost.js +1 -1
  4. package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
  5. package/dist/components/context/ContextAttachmentManager.js +116 -20
  6. package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
  7. package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
  8. package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
  9. package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
  10. package/dist/components/features/planning/PlanningModule.js +26 -9
  11. package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
  12. package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
  13. package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
  14. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  15. package/dist/components/task/TaskActionHeader.js +2 -2
  16. package/dist/components/task/TaskCard.js +37 -4
  17. package/dist/components/task/TaskContextUpload.d.ts +3 -0
  18. package/dist/components/task/TaskContextUpload.js +2 -2
  19. package/dist/components/task/TaskForm.d.ts +4 -0
  20. package/dist/components/task/TaskForm.js +32 -7
  21. package/dist/components/task/TaskImageReviews.js +16 -16
  22. package/dist/components/task/TaskStatusActions.js +11 -2
  23. package/dist/components/task/TaskWorkstreamPicker.js +2 -1
  24. package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
  25. package/dist/components/ui/DocumentTypeIcon.js +8 -0
  26. package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
  27. package/dist/components/ui/TaxonomyDropdown.js +30 -3
  28. package/dist/components/views/StandaloneLayout.js +62 -9
  29. package/dist/components/views/WidgetView.js +8 -5
  30. package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
  31. package/dist/components/views/panels/PlanningDrawer.js +48 -44
  32. package/dist/config/envSchema.js +1 -0
  33. package/dist/config/localServiceCloudTarget.d.ts +10 -0
  34. package/dist/config/localServiceCloudTarget.js +76 -0
  35. package/dist/core/AiProfileService.d.ts +1 -0
  36. package/dist/core/AiProfileService.js +2 -1
  37. package/dist/core/AttachmentLinkService.js +7 -4
  38. package/dist/core/DeletedTaskLifecycleService.d.ts +4 -0
  39. package/dist/core/DeletedTaskLifecycleService.js +21 -0
  40. package/dist/core/McpTokenService.d.ts +4 -0
  41. package/dist/core/McpTokenService.js +52 -0
  42. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +5 -0
  43. package/dist/core/TaskAttachmentLinksDurableMutationService.js +20 -11
  44. package/dist/core/Taskforce.d.ts +63 -1
  45. package/dist/core/Taskforce.js +558 -73
  46. package/dist/core/types.d.ts +2 -1
  47. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
  48. package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
  49. package/dist/documentReviews/service.d.ts +4 -0
  50. package/dist/documentReviews/service.js +141 -46
  51. package/dist/hooks/tasks/useTaskFormActions.d.ts +3 -0
  52. package/dist/hooks/tasks/useTaskFormActions.js +33 -7
  53. package/dist/hooks/useTaskforce.d.ts +59 -50
  54. package/dist/hooks/useTaskforce.js +103 -1
  55. package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
  56. package/dist/mcp/canonicalAssetHelpers.js +39 -5
  57. package/dist/mcp/collaborationRegistrar.js +15 -13
  58. package/dist/mcp/documentAssetRegistrar.js +16 -9
  59. package/dist/mcp/localServiceProxy.js +4 -1
  60. package/dist/mcp/runtime.d.ts +16 -0
  61. package/dist/mcp/runtime.js +566 -171
  62. package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
  63. package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
  64. package/dist/mcp/taskPlanningRegistrar.js +42 -34
  65. package/dist/mcp/toolRegistry.js +2 -2
  66. package/dist/migrations/taskSchemaMigrations.js +35 -0
  67. package/dist/runtime/appGateDefinitions.d.ts +2 -2
  68. package/dist/runtime/appGateDefinitions.js +2 -2
  69. package/dist/server/appAccess.d.ts +12 -0
  70. package/dist/server/appAccess.js +21 -0
  71. package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
  72. package/dist/server/authorizedWorkspaceAccess.js +40 -0
  73. package/dist/server/cloudCollectionRead.d.ts +25 -0
  74. package/dist/server/cloudCollectionRead.js +120 -0
  75. package/dist/server/cloudCollectionReadPool.d.ts +52 -0
  76. package/dist/server/cloudCollectionReadPool.js +140 -0
  77. package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
  78. package/dist/server/cloudCollectionReadWorker.js +36 -0
  79. package/dist/server/index.d.ts +3 -11
  80. package/dist/server/index.js +53 -40
  81. package/dist/server/localServiceLease.d.ts +3 -0
  82. package/dist/server/localServiceLease.js +9 -1
  83. package/dist/server/routes/documents.d.ts +5 -2
  84. package/dist/server/routes/documents.js +160 -13
  85. package/dist/server/routes/durableTaskHttpRouters.d.ts +2 -0
  86. package/dist/server/routes/durableTaskHttpRouters.js +2 -2
  87. package/dist/server/routes/localService.js +2 -0
  88. package/dist/server/routes/primitives.d.ts +2 -0
  89. package/dist/server/routes/shared.d.ts +24 -0
  90. package/dist/server/routes/shared.js +114 -18
  91. package/dist/server/routes/sync.js +22 -0
  92. package/dist/server/routes/syncV3FeedRoutes.js +4 -2
  93. package/dist/server/routes/syncV3LocalCaptureRoutes.js +20 -2
  94. package/dist/server/routes/syncV3OperationRoutes.js +17 -2
  95. package/dist/server/routes/tasks.js +199 -89
  96. package/dist/server/routes.js +35 -7
  97. package/dist/service/localServiceCli.js +36 -25
  98. package/dist/service/localServiceClientLifecycle.js +67 -6
  99. package/dist/shared/planningIdentity.d.ts +20 -0
  100. package/dist/shared/planningIdentity.js +79 -0
  101. package/dist/storage/postgresAdapter.js +4 -0
  102. package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
  103. package/dist/storage/postgresRequestDiagnostics.js +24 -0
  104. package/dist/storage/workspaceAssetStore.d.ts +32 -0
  105. package/dist/storage/workspaceAssetStore.js +70 -0
  106. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +16 -0
  107. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +70 -22
  108. package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
  109. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +5 -0
  110. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
  111. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +15 -0
  112. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +26 -3
  113. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
  114. package/dist/sync/syncApplyHandlers.d.ts +3 -1
  115. package/dist/sync/syncApplyHandlers.js +29 -0
  116. package/dist/sync/syncService.js +9 -0
  117. package/dist/sync/taskSyncPayload.d.ts +1 -0
  118. package/dist/sync/taskSyncPayload.js +6 -0
  119. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +1 -0
  120. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +21 -3
  121. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +17 -4
  122. package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
  123. package/dist/sync/v3/localOutboxDispatcher.js +5 -1
  124. package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
  125. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +1 -0
  126. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +15 -3
  127. package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
  128. package/dist/sync/v3/syncDurabilityStore.d.ts +8 -0
  129. package/dist/sync/v3/syncDurabilityStore.js +171 -33
  130. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +1 -0
  131. package/dist/sync/v3/taskAttachmentLinksMutationService.js +18 -2
  132. package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
  133. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +1 -9
  134. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +1 -0
  135. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +7 -1
  136. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +15 -3
  137. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +4 -0
  138. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +29 -6
  139. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +1 -0
  140. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +29 -7
  141. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +62 -7
  142. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +1 -0
  143. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
  144. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
  145. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
  146. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
  147. package/dist/sync/workspacePullFeed.d.ts +6 -1
  148. package/dist/sync/workspacePullFeed.js +172 -161
  149. package/dist/sync/workspaceSyncModel.d.ts +11 -0
  150. package/dist/sync/workspaceSyncModel.js +27 -2
  151. package/dist/types.d.ts +8 -0
  152. package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-MZs7lVED.js} +1 -1
  153. package/dist/ui/assets/{AiProfilesModule-DPxQeF9N.js → AiProfilesModule-C3i0LOxe.js} +1 -1
  154. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DHNYaNg6.js → AnnotatedAttachmentWorkspace-DVZk5Tln.js} +1 -1
  155. package/dist/ui/assets/AssetTraySortControl-Kf1pelSy.js +1 -0
  156. package/dist/ui/assets/ContextAttachmentManager-B8boOs6Q.js +3 -0
  157. package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-DXH4XUsB.js} +4 -4
  158. package/dist/ui/assets/EntityActivityTimeline-BtSTq0ON.js +1 -0
  159. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +1 -0
  160. package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
  161. package/dist/ui/assets/{PlansPage-BVH_NfgT.js → PlansPage-ChqgxALc.js} +1 -1
  162. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +1 -0
  163. package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
  164. package/dist/ui/assets/{TaskSettings-xVsxSa0g.js → TaskSettings-g4ECKNOz.js} +1 -1
  165. package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BazdUwxZ.js} +1 -1
  166. package/dist/ui/assets/WorkflowManagerModule-DWFspC-o.js +1 -0
  167. package/dist/ui/assets/index-BUplSsv_.js +7 -0
  168. package/dist/ui/assets/index-gz2EXuoK.css +1 -0
  169. package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-BkFLXavV.js} +1 -1
  170. package/dist/ui/index.html +3 -3
  171. package/dist/utils/constants.d.ts +2 -2
  172. package/dist/utils/constants.js +1 -1
  173. package/dist/utils/contextAttachmentUpload.d.ts +2 -0
  174. package/dist/utils/contextAttachmentUpload.js +7 -0
  175. package/dist/utils/contextFiles.d.ts +1 -1
  176. package/dist/utils/contextFiles.js +4 -4
  177. package/package.json +6 -1
  178. package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
  179. package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
  180. package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
  181. package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
  182. package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
  183. package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
  184. package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
  185. package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
  186. package/dist/ui/assets/index-BzJlVnNS.css +0 -1
  187. package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
@@ -57,7 +57,7 @@ import { resolveBuildInfo } from '../buildInfo.js';
57
57
  import { resolveRealtimeSyncFlag } from '../../sync/realtimeFeature.js';
58
58
  import { parseJsonBody } from './primitives.js';
59
59
  import { resolveRequestAccess } from '../auth.js';
60
- import { getRouteTimingNow, logSlowBootstrapRoute, requestBootstrapTraceId, requestHeader, requestRuntimeMode, requestUserId, resolveErrorStatusCode, serializeJsonWithTiming, setServerTimingHeader } from './shared.js';
60
+ import { currentPerformanceTraceDiagnostics, getRouteTimingNow, logSlowBootstrapRoute, performanceTraceServerTimingMetrics, requestBootstrapTraceId, requestHeader, requestHumanActorRef, requestRuntimeMode, respondWithPerformanceTraceJson, resolveErrorStatusCode, runWithPerformanceTrace, serializeJsonWithTiming, setPerformanceTraceHeaders, setServerTimingHeader } from './shared.js';
61
61
  import { listBuiltInCustomTaxonomyLibraryPacks } from '../../shared/customTaxonomyLibrary.js';
62
62
  import { getBuiltInTaxonomyLibraryPack, listBuiltInTaxonomyLibraryPacks } from '../../shared/taxonomyLibrary.js';
63
63
  import { shouldUseProvisionalTaskReferences } from '../../utils/taskReferences.js';
@@ -177,7 +177,7 @@ export function registerTaskRoutes(deps) {
177
177
  surface: 'http',
178
178
  tenantId: core.getTenantId(),
179
179
  workspaceId,
180
- actorRef: runtimeMode === 'cloud' ? (String(requestUserId(req) || '').trim() || 'user') : 'user',
180
+ actorRef: requestHumanActorRef(req),
181
181
  cloudClientId: 'http-cloud',
182
182
  ...(operationId ? { createOperationId: () => operationId } : {}),
183
183
  });
@@ -192,7 +192,7 @@ export function registerTaskRoutes(deps) {
192
192
  surface: 'http',
193
193
  tenantId: core.getTenantId(),
194
194
  workspaceId,
195
- actorRef: runtimeMode === 'cloud' ? (String(requestUserId(req) || '').trim() || 'user') : 'user',
195
+ actorRef: requestHumanActorRef(req),
196
196
  cloudClientId: 'http-cloud',
197
197
  ...(operationId ? { createOperationId: () => operationId } : {}),
198
198
  });
@@ -465,9 +465,13 @@ export function registerTaskRoutes(deps) {
465
465
  if (!ensureAppAccess(req, res))
466
466
  return;
467
467
  const workspaceId = requestWorkspaceId(req);
468
- const data = core.listTasks(workspaceId);
469
- res.writeHead(200, { 'Content-Type': 'application/json' });
470
- res.end(JSON.stringify(data));
468
+ respondWithPerformanceTraceJson({
469
+ req,
470
+ res,
471
+ route: '/api/taskforce/tasks',
472
+ workspaceId,
473
+ load: () => core.listTasks(workspaceId),
474
+ });
471
475
  });
472
476
  addRoute('GET', '/api/taskforce/task-relationships', async (req, res) => {
473
477
  if (!ensureAppAccess(req, res))
@@ -480,9 +484,13 @@ export function registerTaskRoutes(deps) {
480
484
  if (!ensureAppAccess(req, res))
481
485
  return;
482
486
  const workspaceId = requestWorkspaceId(req);
483
- const data = core.listDeletedTasks(workspaceId);
484
- res.writeHead(200, { 'Content-Type': 'application/json' });
485
- res.end(JSON.stringify(data));
487
+ respondWithPerformanceTraceJson({
488
+ req,
489
+ res,
490
+ route: '/api/taskforce/deleted',
491
+ workspaceId,
492
+ load: () => core.listDeletedTasks(workspaceId),
493
+ });
486
494
  };
487
495
  // GET /api/taskforce/deleted - List deleted tasks
488
496
  addRoute('GET', '/api/taskforce/deleted', handleListDeletedTasks);
@@ -589,7 +597,7 @@ export function registerTaskRoutes(deps) {
589
597
  const durableRouter = createTaskHttpRootRouter(req, workspaceId);
590
598
  const task = durableRouter.enabled ? durableRouter.create(taskInput) : core.createTask(taskInput, {
591
599
  workspaceId,
592
- actorRef: 'user',
600
+ actorRef: requestHumanActorRef(req),
593
601
  provisionalLocalReference: shouldUseProvisionalTaskReferences(requestRuntimeMode(req))
594
602
  });
595
603
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -630,13 +638,21 @@ export function registerTaskRoutes(deps) {
630
638
  if (hasAttachments && !Array.isArray(updates.attachments)) {
631
639
  throw new TaskforceRuleError('attachments must be an array.', 400, 'TASK_ATTACHMENT_LINKS_INVALID');
632
640
  }
641
+ const hasCardCover = Object.prototype.hasOwnProperty.call(updates || {}, 'cardCoverAssetId');
642
+ if (hasCardCover && updates.cardCoverAssetId !== null
643
+ && (typeof updates.cardCoverAssetId !== 'string' || !updates.cardCoverAssetId.trim())) {
644
+ throw new TaskforceRuleError('cardCoverAssetId must be a non-empty string or null.', 400, 'TASK_CARD_COVER_INVALID');
645
+ }
633
646
  const checklistDurable = hasChecklist && durableRouters.checklist.enabled;
634
- const attachmentLinksDurable = hasAttachments && durableRouters.attachmentLinks.enabled;
647
+ const attachmentLinksDurable = (hasAttachments || hasCardCover) && durableRouters.attachmentLinks.enabled;
635
648
  const rawChecklistItems = hasChecklist ? updates.checklistItems : [];
636
- const rawAttachments = hasAttachments ? updates.attachments : [];
649
+ const rawAttachments = hasAttachments
650
+ ? updates.attachments
651
+ : (core.getTask(params.id, workspaceId)?.attachments || []);
637
652
  const remainingUpdates = checklistDurable || attachmentLinksDurable
638
653
  ? Object.fromEntries(Object.entries(updates).filter(([key]) => (!(checklistDurable && key === 'checklistItems')
639
- && !(attachmentLinksDurable && key === 'attachments'))))
654
+ && !(attachmentLinksDurable && key === 'attachments')
655
+ && !(attachmentLinksDurable && key === 'cardCoverAssetId'))))
640
656
  : updates;
641
657
  let updated;
642
658
  const durableTargetCount = Number(checklistDurable)
@@ -657,7 +673,7 @@ export function registerTaskRoutes(deps) {
657
673
  ? (durableRouter.enabled
658
674
  ? durableRouter.update(params.id, remainingUpdates, saveSource)
659
675
  : core.updateTask(params.id, remainingUpdates, workspaceId, {
660
- actorRef: 'user',
676
+ actorRef: requestHumanActorRef(req),
661
677
  saveSource,
662
678
  }))
663
679
  : core.getTask(params.id, workspaceId);
@@ -679,7 +695,15 @@ export function registerTaskRoutes(deps) {
679
695
  core,
680
696
  workspaceId,
681
697
  });
682
- updated = attachmentLinksRouter.replace({ taskId: params.id, links }).task;
698
+ updated = attachmentLinksRouter.replace({
699
+ taskId: params.id,
700
+ links,
701
+ ...(hasCardCover ? {
702
+ cardCoverAssetId: updates.cardCoverAssetId === null
703
+ ? null
704
+ : String(updates.cardCoverAssetId).trim(),
705
+ } : {}),
706
+ }).task;
683
707
  }
684
708
  }
685
709
  if (!updated) {
@@ -766,43 +790,53 @@ export function registerTaskRoutes(deps) {
766
790
  addRoute('GET', '/api/taskforce/planning/bootstrap', async (req, res) => {
767
791
  if (!ensureAppAccess(req, res))
768
792
  return;
769
- const startedAt = getRouteTimingNow();
770
- const workspaceId = requestWorkspaceId(req);
771
- const traceId = requestBootstrapTraceId(req);
772
- const diagnostics = {};
773
- const data = core.listPlanningBootstrap(workspaceId, diagnostics);
774
- const serializeStartedAt = getRouteTimingNow();
775
- const serialized = serializeJsonWithTiming(data, serializeStartedAt);
776
- const totalDurationMs = Math.max(0, getRouteTimingNow() - startedAt);
777
- const details = diagnostics;
778
- setServerTimingHeader(res, [
779
- { name: 'planning-bootstrap', durationMs: totalDurationMs },
780
- { name: 'planning-bootstrap-initiatives', durationMs: details.initiativeLoadMs ?? 0 },
781
- { name: 'planning-bootstrap-workstreams', durationMs: details.workstreamLoadMs ?? 0 },
782
- { name: 'planning-bootstrap-tasks', durationMs: details.taskSummaryQueryMs ?? 0 },
783
- { name: 'planning-bootstrap-build', durationMs: details.taskSummaryBuildMs ?? 0 },
784
- { name: 'planning-bootstrap-json', durationMs: serialized.durationMs },
785
- ]);
786
- logSlowBootstrapRoute({
787
- route: '/api/taskforce/planning/bootstrap',
788
- workspaceId,
789
- traceId,
790
- durationMs: totalDurationMs,
791
- details: {
792
- initiativeCount: details.initiativeCount ?? 0,
793
- workstreamCount: details.workstreamCount ?? 0,
794
- workstreamLinkedTaskCount: details.workstreamLinkedTaskCount ?? 0,
795
- workstreamTaskSummaryCount: details.workstreamTaskSummaryCount ?? 0,
796
- responseBytes: serialized.byteLength,
797
- initiativeLoadMs: details.initiativeLoadMs ?? 0,
798
- workstreamLoadMs: details.workstreamLoadMs ?? 0,
799
- taskSummaryQueryMs: details.taskSummaryQueryMs ?? 0,
800
- taskSummaryBuildMs: details.taskSummaryBuildMs ?? 0,
801
- jsonSerializeMs: serialized.durationMs,
802
- },
793
+ runWithPerformanceTrace(req, () => {
794
+ const startedAt = getRouteTimingNow();
795
+ const workspaceId = requestWorkspaceId(req);
796
+ const traceId = requestBootstrapTraceId(req);
797
+ const diagnostics = {};
798
+ const data = core.listPlanningBootstrap(workspaceId, diagnostics);
799
+ const serializeStartedAt = getRouteTimingNow();
800
+ const serialized = serializeJsonWithTiming(data, serializeStartedAt);
801
+ const totalDurationMs = Math.max(0, getRouteTimingNow() - startedAt);
802
+ const details = diagnostics;
803
+ const performanceDiagnostics = currentPerformanceTraceDiagnostics();
804
+ setServerTimingHeader(res, [
805
+ { name: 'planning-bootstrap', durationMs: totalDurationMs },
806
+ { name: 'planning-bootstrap-initiatives', durationMs: details.initiativeLoadMs ?? 0 },
807
+ { name: 'planning-bootstrap-workstreams', durationMs: details.workstreamLoadMs ?? 0 },
808
+ { name: 'planning-bootstrap-tasks', durationMs: details.taskSummaryQueryMs ?? 0 },
809
+ { name: 'planning-bootstrap-build', durationMs: details.taskSummaryBuildMs ?? 0 },
810
+ { name: 'planning-bootstrap-json', durationMs: serialized.durationMs },
811
+ ...performanceTraceServerTimingMetrics(performanceDiagnostics),
812
+ ]);
813
+ setPerformanceTraceHeaders(res, performanceDiagnostics, serialized.byteLength);
814
+ logSlowBootstrapRoute({
815
+ route: '/api/taskforce/planning/bootstrap',
816
+ workspaceId,
817
+ traceId,
818
+ durationMs: totalDurationMs,
819
+ details: {
820
+ initiativeCount: details.initiativeCount ?? 0,
821
+ workstreamCount: details.workstreamCount ?? 0,
822
+ workstreamLinkedTaskCount: details.workstreamLinkedTaskCount ?? 0,
823
+ workstreamTaskSummaryCount: details.workstreamTaskSummaryCount ?? 0,
824
+ responseBytes: serialized.byteLength,
825
+ initiativeLoadMs: details.initiativeLoadMs ?? 0,
826
+ workstreamLoadMs: details.workstreamLoadMs ?? 0,
827
+ taskSummaryQueryMs: details.taskSummaryQueryMs ?? 0,
828
+ taskSummaryBuildMs: details.taskSummaryBuildMs ?? 0,
829
+ jsonSerializeMs: serialized.durationMs,
830
+ ...(performanceDiagnostics ? {
831
+ postgresQueryCount: performanceDiagnostics.postgresQueryCount,
832
+ postgresWorkerCallCount: performanceDiagnostics.postgresWorkerCallCount,
833
+ postgresWorkerWaitMs: Math.round(performanceDiagnostics.postgresWorkerWaitMs),
834
+ } : {}),
835
+ },
836
+ });
837
+ res.writeHead(200, { 'Content-Type': 'application/json' });
838
+ res.end(serialized.body);
803
839
  });
804
- res.writeHead(200, { 'Content-Type': 'application/json' });
805
- res.end(serialized.body);
806
840
  });
807
841
  // POST /api/taskforce/initiative - Create initiative
808
842
  addRoute('POST', '/api/taskforce/initiative', async (req, res) => {
@@ -812,7 +846,7 @@ export function registerTaskRoutes(deps) {
812
846
  try {
813
847
  const workspaceId = requestWorkspaceId(req);
814
848
  const router = createInitiativeRouter(req, workspaceId);
815
- const allowed = new Set(['id', 'title', 'description', 'ownerId', 'order', 'attachments', 'createdAt']);
849
+ const allowed = new Set(['id', 'title', 'description', 'ownerId', 'order', 'attachments', 'createdAt', 'icon', 'color']);
816
850
  const unsupported = Object.keys(body || {}).filter((key) => !allowed.has(key));
817
851
  if (router.enabled && unsupported.length > 0) {
818
852
  const error = new Error(`Unsupported initiative fields: ${unsupported.sort().join(', ')}`);
@@ -826,16 +860,28 @@ export function registerTaskRoutes(deps) {
826
860
  error.statusCode = 400;
827
861
  throw error;
828
862
  }
829
- const initiative = router.enabled
830
- ? initiativeResult(router.create(String(body?.id || '').trim() || `initiative-${randomUUID()}`, {
831
- title: body?.title,
832
- description: body?.description ?? null,
833
- ownerId: body?.ownerId ?? null,
834
- order: body?.order ?? null,
835
- attachments: serializeCanonicalPlanningAttachmentLinks(body?.attachments ?? []),
836
- createdAt: String(body?.createdAt || '').trim() || new Date().toISOString(),
837
- }))
838
- : core.createInitiative(body || {}, workspaceId, { actorRef: 'user' });
863
+ const initiative = core.getDatabaseAdapter().transaction(() => {
864
+ const created = router.enabled
865
+ ? initiativeResult(router.create(String(body?.id || '').trim() || `initiative-${randomUUID()}`, {
866
+ title: body?.title,
867
+ description: body?.description ?? null,
868
+ ownerId: body?.ownerId ?? null,
869
+ order: body?.order ?? null,
870
+ attachments: serializeCanonicalPlanningAttachmentLinks(body?.attachments ?? []),
871
+ createdAt: String(body?.createdAt || '').trim() || new Date().toISOString(),
872
+ }))
873
+ : core.createInitiative(body || {}, workspaceId, { actorRef: requestHumanActorRef(req) });
874
+ if (!created)
875
+ throw new Error('Durable initiative capture did not return a provisional initiative.');
876
+ const hasIdentity = Object.prototype.hasOwnProperty.call(body || {}, 'icon')
877
+ || Object.prototype.hasOwnProperty.call(body || {}, 'color');
878
+ return hasIdentity
879
+ ? core.updatePlanningEntityIdentity('initiative', created.id, {
880
+ ...(Object.prototype.hasOwnProperty.call(body || {}, 'icon') ? { icon: body.icon } : {}),
881
+ ...(Object.prototype.hasOwnProperty.call(body || {}, 'color') ? { color: body.color } : {}),
882
+ }, workspaceId)
883
+ : created;
884
+ })();
839
885
  if (!initiative)
840
886
  throw new Error('Durable initiative capture did not return a provisional initiative.');
841
887
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -883,7 +929,7 @@ export function registerTaskRoutes(deps) {
883
929
  };
884
930
  const updated = router.enabled
885
931
  ? initiativeResult(router.updateMetadata(params.id, durablePatch))
886
- : core.updateInitiative(params.id, body || {}, workspaceId, { actorRef: 'user' });
932
+ : core.updateInitiative(params.id, body || {}, workspaceId, { actorRef: requestHumanActorRef(req) });
887
933
  if (!updated)
888
934
  throw new Error('Durable initiative capture did not return a provisional initiative.');
889
935
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -893,6 +939,30 @@ export function registerTaskRoutes(deps) {
893
939
  writeInitiativeMutationError(res, error, 'Failed to update initiative');
894
940
  }
895
941
  });
942
+ addRoute('PATCH', '/api/taskforce/initiative/:id/identity', async (req, res, params) => {
943
+ if (!ensureAppAccess(req, res))
944
+ return;
945
+ const body = await parseJsonBody(req);
946
+ try {
947
+ const workspaceId = requestWorkspaceId(req);
948
+ const source = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
949
+ const keys = Object.keys(source);
950
+ if (keys.length === 0 || keys.some((key) => key !== 'icon' && key !== 'color')) {
951
+ throw new TaskforceRuleError('Planning identity update must contain only icon or color.', 400, 'PLANNING_IDENTITY_INVALID');
952
+ }
953
+ const updated = core.updatePlanningEntityIdentity('initiative', params.id, source, workspaceId);
954
+ res.writeHead(200, { 'Content-Type': 'application/json' });
955
+ res.end(JSON.stringify(updated));
956
+ }
957
+ catch (error) {
958
+ const statusCode = resolveErrorStatusCode(error, 400);
959
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
960
+ res.end(JSON.stringify({
961
+ error: error instanceof Error ? error.message : 'Failed to update initiative identity',
962
+ code: error instanceof TaskforceRuleError ? error.code : undefined,
963
+ }));
964
+ }
965
+ });
896
966
  addRoute('POST', '/api/taskforce/initiative/:id/archive', async (req, res, params) => {
897
967
  if (!ensureAppAccess(req, res))
898
968
  return;
@@ -901,7 +971,7 @@ export function registerTaskRoutes(deps) {
901
971
  const router = createInitiativeRouter(req, workspaceId);
902
972
  const archived = router.enabled
903
973
  ? initiativeResult(router.archive(params.id))
904
- : core.archiveInitiative(params.id, workspaceId, { actorRef: 'user' });
974
+ : core.archiveInitiative(params.id, workspaceId, { actorRef: requestHumanActorRef(req) });
905
975
  if (!archived)
906
976
  throw new Error('Durable initiative capture did not return a provisional initiative.');
907
977
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -919,7 +989,7 @@ export function registerTaskRoutes(deps) {
919
989
  const router = createInitiativeRouter(req, workspaceId);
920
990
  const restored = router.enabled
921
991
  ? initiativeResult(router.unarchive(params.id))
922
- : core.unarchiveInitiative(params.id, workspaceId, { actorRef: 'user' });
992
+ : core.unarchiveInitiative(params.id, workspaceId, { actorRef: requestHumanActorRef(req) });
923
993
  if (!restored)
924
994
  throw new Error('Durable initiative capture did not return a provisional initiative.');
925
995
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -946,7 +1016,7 @@ export function registerTaskRoutes(deps) {
946
1016
  try {
947
1017
  const workspaceId = requestWorkspaceId(req);
948
1018
  const router = createWorkstreamRouter(req, workspaceId);
949
- const allowed = new Set(['id', 'initiativeId', 'title', 'description', 'ownerId', 'order', 'attachments', 'createdAt']);
1019
+ const allowed = new Set(['id', 'initiativeId', 'title', 'description', 'ownerId', 'order', 'attachments', 'createdAt', 'icon', 'color']);
950
1020
  const unsupported = Object.keys(body || {}).filter((key) => !allowed.has(key));
951
1021
  if (router.enabled && unsupported.length > 0) {
952
1022
  const error = new Error(`Unsupported workstream fields: ${unsupported.sort().join(', ')}`);
@@ -960,17 +1030,29 @@ export function registerTaskRoutes(deps) {
960
1030
  error.statusCode = 400;
961
1031
  throw error;
962
1032
  }
963
- const workstream = router.enabled
964
- ? workstreamResult(router.create(String(body?.id || '').trim(), {
965
- initiativeId: body?.initiativeId ?? null,
966
- title: body?.title,
967
- description: body?.description ?? null,
968
- ownerId: body?.ownerId ?? null,
969
- order: body?.order ?? null,
970
- attachments: serializeCanonicalPlanningAttachmentLinks(body?.attachments ?? []),
971
- createdAt: String(body?.createdAt || '').trim(),
972
- }))
973
- : core.createWorkstream(body || {}, workspaceId, { actorRef: 'user' });
1033
+ const workstream = core.getDatabaseAdapter().transaction(() => {
1034
+ const created = router.enabled
1035
+ ? workstreamResult(router.create(String(body?.id || '').trim(), {
1036
+ initiativeId: body?.initiativeId ?? null,
1037
+ title: body?.title,
1038
+ description: body?.description ?? null,
1039
+ ownerId: body?.ownerId ?? null,
1040
+ order: body?.order ?? null,
1041
+ attachments: serializeCanonicalPlanningAttachmentLinks(body?.attachments ?? []),
1042
+ createdAt: String(body?.createdAt || '').trim(),
1043
+ }))
1044
+ : core.createWorkstream(body || {}, workspaceId, { actorRef: requestHumanActorRef(req) });
1045
+ if (!created)
1046
+ throw new Error('Durable workstream capture did not return a provisional workstream.');
1047
+ const hasIdentity = Object.prototype.hasOwnProperty.call(body || {}, 'icon')
1048
+ || Object.prototype.hasOwnProperty.call(body || {}, 'color');
1049
+ return hasIdentity
1050
+ ? core.updatePlanningEntityIdentity('workstream', created.id, {
1051
+ ...(Object.prototype.hasOwnProperty.call(body || {}, 'icon') ? { icon: body.icon } : {}),
1052
+ ...(Object.prototype.hasOwnProperty.call(body || {}, 'color') ? { color: body.color } : {}),
1053
+ }, workspaceId)
1054
+ : created;
1055
+ })();
974
1056
  if (!workstream)
975
1057
  throw new Error('Durable workstream capture did not return a provisional workstream.');
976
1058
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -1018,7 +1100,7 @@ export function registerTaskRoutes(deps) {
1018
1100
  };
1019
1101
  const updated = router.enabled
1020
1102
  ? workstreamResult(router.updateMetadata(params.id, durablePatch))
1021
- : core.updateWorkstream(params.id, body || {}, workspaceId, { actorRef: 'user' });
1103
+ : core.updateWorkstream(params.id, body || {}, workspaceId, { actorRef: requestHumanActorRef(req) });
1022
1104
  if (!updated)
1023
1105
  throw new Error('Durable workstream capture did not return a provisional workstream.');
1024
1106
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1028,6 +1110,30 @@ export function registerTaskRoutes(deps) {
1028
1110
  writeWorkstreamMutationError(res, error, 'Failed to update workstream');
1029
1111
  }
1030
1112
  });
1113
+ addRoute('PATCH', '/api/taskforce/workstream/:id/identity', async (req, res, params) => {
1114
+ if (!ensureAppAccess(req, res))
1115
+ return;
1116
+ const body = await parseJsonBody(req);
1117
+ try {
1118
+ const workspaceId = requestWorkspaceId(req);
1119
+ const source = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
1120
+ const keys = Object.keys(source);
1121
+ if (keys.length === 0 || keys.some((key) => key !== 'icon' && key !== 'color')) {
1122
+ throw new TaskforceRuleError('Planning identity update must contain only icon or color.', 400, 'PLANNING_IDENTITY_INVALID');
1123
+ }
1124
+ const updated = core.updatePlanningEntityIdentity('workstream', params.id, source, workspaceId);
1125
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1126
+ res.end(JSON.stringify(updated));
1127
+ }
1128
+ catch (error) {
1129
+ const statusCode = resolveErrorStatusCode(error, 400);
1130
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1131
+ res.end(JSON.stringify({
1132
+ error: error instanceof Error ? error.message : 'Failed to update workstream identity',
1133
+ code: error instanceof TaskforceRuleError ? error.code : undefined,
1134
+ }));
1135
+ }
1136
+ });
1031
1137
  addRoute('POST', '/api/taskforce/workstream/:id/archive', async (req, res, params) => {
1032
1138
  if (!ensureAppAccess(req, res))
1033
1139
  return;
@@ -1036,7 +1142,7 @@ export function registerTaskRoutes(deps) {
1036
1142
  const router = createWorkstreamRouter(req, workspaceId);
1037
1143
  const archived = router.enabled
1038
1144
  ? workstreamResult(router.archive(params.id))
1039
- : core.archiveWorkstream(params.id, workspaceId, { actorRef: 'user' });
1145
+ : core.archiveWorkstream(params.id, workspaceId, { actorRef: requestHumanActorRef(req) });
1040
1146
  if (!archived)
1041
1147
  throw new Error('Durable workstream capture did not return a provisional workstream.');
1042
1148
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1054,7 +1160,7 @@ export function registerTaskRoutes(deps) {
1054
1160
  const router = createWorkstreamRouter(req, workspaceId);
1055
1161
  const restored = router.enabled
1056
1162
  ? workstreamResult(router.unarchive(params.id))
1057
- : core.unarchiveWorkstream(params.id, workspaceId, { actorRef: 'user' });
1163
+ : core.unarchiveWorkstream(params.id, workspaceId, { actorRef: requestHumanActorRef(req) });
1058
1164
  if (!restored)
1059
1165
  throw new Error('Durable workstream capture did not return a provisional workstream.');
1060
1166
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1180,9 +1286,13 @@ export function registerTaskRoutes(deps) {
1180
1286
  if (!ensureAppAccess(req, res))
1181
1287
  return;
1182
1288
  const workspaceId = requestWorkspaceId(req);
1183
- const data = core.listArchive(workspaceId);
1184
- res.writeHead(200, { 'Content-Type': 'application/json' });
1185
- res.end(JSON.stringify(data));
1289
+ respondWithPerformanceTraceJson({
1290
+ req,
1291
+ res,
1292
+ route: '/api/taskforce/archive',
1293
+ workspaceId,
1294
+ load: () => core.listArchive(workspaceId),
1295
+ });
1186
1296
  });
1187
1297
  // POST /api/taskforce/task/:id/complete - Archive as complete
1188
1298
  addRoute('POST', '/api/taskforce/task/:id/complete', async (req, res, params) => {
@@ -1193,7 +1303,7 @@ export function registerTaskRoutes(deps) {
1193
1303
  const durableRouter = createTaskHttpRootRouter(req, workspaceId);
1194
1304
  const archived = durableRouter.enabled
1195
1305
  ? durableRouter.archive(params.id, false, null)
1196
- : core.archiveTask(params.id, false, undefined, workspaceId, { actorRef: 'user' });
1306
+ : core.archiveTask(params.id, false, undefined, workspaceId, { actorRef: requestHumanActorRef(req) });
1197
1307
  if (!archived) {
1198
1308
  res.writeHead(404, { 'Content-Type': 'application/json' });
1199
1309
  res.end(JSON.stringify({ error: 'Task not found' }));
@@ -1221,7 +1331,7 @@ export function registerTaskRoutes(deps) {
1221
1331
  const durableRouter = createTaskHttpRootRouter(req, workspaceId);
1222
1332
  const archived = durableRouter.enabled
1223
1333
  ? durableRouter.archive(params.id, true, reason)
1224
- : core.archiveTask(params.id, true, reason, workspaceId, { actorRef: 'user' });
1334
+ : core.archiveTask(params.id, true, reason, workspaceId, { actorRef: requestHumanActorRef(req) });
1225
1335
  if (!archived) {
1226
1336
  res.writeHead(404, { 'Content-Type': 'application/json' });
1227
1337
  res.end(JSON.stringify({ error: 'Task not found' }));
@@ -1247,7 +1357,7 @@ export function registerTaskRoutes(deps) {
1247
1357
  const durableRouter = createTaskHttpRootRouter(req, workspaceId);
1248
1358
  const deleted = durableRouter.enabled
1249
1359
  ? durableRouter.delete(params.id)
1250
- : core.softDeleteTask(params.id, workspaceId, { actorRef: 'user', source: 'ui' });
1360
+ : core.softDeleteTask(params.id, workspaceId, { actorRef: requestHumanActorRef(req), source: 'ui' });
1251
1361
  if (!deleted) {
1252
1362
  res.writeHead(404, { 'Content-Type': 'application/json' });
1253
1363
  res.end(JSON.stringify({ error: 'Task not found' }));
@@ -1272,7 +1382,7 @@ export function registerTaskRoutes(deps) {
1272
1382
  const durableRouter = createTaskHttpRootRouter(req, workspaceId);
1273
1383
  const restored = durableRouter.enabled
1274
1384
  ? durableRouter.restore(params.id)
1275
- : core.restoreDeletedTask(params.id, workspaceId, { actorRef: 'user' });
1385
+ : core.restoreDeletedTask(params.id, workspaceId, { actorRef: requestHumanActorRef(req) });
1276
1386
  if (!restored) {
1277
1387
  res.writeHead(404, { 'Content-Type': 'application/json' });
1278
1388
  res.end(JSON.stringify({ error: 'Deleted task not found' }));
@@ -1362,7 +1472,7 @@ export function registerTaskRoutes(deps) {
1362
1472
  continue;
1363
1473
  }
1364
1474
  const task = core.restoreDeletedTask(deletedRecordId, workspaceId, {
1365
- actorRef: 'user',
1475
+ actorRef: requestHumanActorRef(req),
1366
1476
  restoreDestination,
1367
1477
  });
1368
1478
  if (!task) {
@@ -1533,7 +1643,7 @@ export function registerTaskRoutes(deps) {
1533
1643
  const durableRouter = createTaskHttpRootRouter(req, workspaceId);
1534
1644
  const restored = durableRouter.enabled
1535
1645
  ? durableRouter.unarchive(params.id)
1536
- : core.unarchiveTask(params.id, workspaceId, { actorRef: 'user' });
1646
+ : core.unarchiveTask(params.id, workspaceId, { actorRef: requestHumanActorRef(req) });
1537
1647
  if (!restored) {
1538
1648
  res.writeHead(404, { 'Content-Type': 'application/json' });
1539
1649
  res.end(JSON.stringify({ error: 'Archived task not found' }));
@@ -602,7 +602,7 @@ export function createRoutes(core, context = {}) {
602
602
  taskId: input.taskId,
603
603
  workspaceId: input.workspaceId,
604
604
  action: 'task-attachments-changed',
605
- actor: 'user',
605
+ actor: String(input.actorRef || '').trim() || 'user',
606
606
  createdAt: input.createdAt || new Date().toISOString(),
607
607
  details: {
608
608
  changes: {
@@ -725,6 +725,7 @@ export function createRoutes(core, context = {}) {
725
725
  asset,
726
726
  beforeTask: taskBeforeLink,
727
727
  createdAt: asset.updatedAt,
728
+ actorRef: params.actorRef,
728
729
  });
729
730
  }
730
731
  }
@@ -788,7 +789,7 @@ export function createRoutes(core, context = {}) {
788
789
  return false;
789
790
  return /^workspaces\/[^/]+\/assets\/documents\/.+\.(md|markdown)$/.test(normalized);
790
791
  };
791
- const resolveCanonicalDocumentRead = async (params) => {
792
+ const resolveCanonicalContentRead = async (params) => {
792
793
  const resolvedObjectStorage = resolveEffectiveObjectStorageConfig();
793
794
  const objectStorageClient = getObjectStorageClient();
794
795
  const { basePath } = core.getPaths();
@@ -800,17 +801,19 @@ export function createRoutes(core, context = {}) {
800
801
  : params.asset.storageProvider === 'local'
801
802
  ? 'local'
802
803
  : null;
804
+ const contentKind = params.asset.kind === 'document' ? 'document' : 'asset';
805
+ const contentLabel = params.asset.kind === 'document' ? 'Document' : 'Asset';
803
806
  const resolvedContent = publishedContentResolver && resolverOwnedProvider
804
807
  ? publishedContentResolver.resolve({
805
808
  tenantId: core.getTenantId(),
806
809
  workspaceId: params.asset.workspaceId,
807
- kind: 'document',
810
+ kind: contentKind,
808
811
  logicalPath: logicalStorageKey,
809
812
  legacyProvider: resolverOwnedProvider
810
813
  })
811
814
  : {
812
815
  source: 'legacy',
813
- kind: 'document',
816
+ kind: contentKind,
814
817
  logicalPath: logicalStorageKey,
815
818
  physicalKey: logicalStorageKey,
816
819
  storageProvider: params.asset.storageProvider
@@ -818,6 +821,31 @@ export function createRoutes(core, context = {}) {
818
821
  if (resolvedContent.source === 'deleted') {
819
822
  return { statusCode: 404 };
820
823
  }
824
+ if (resolvedContent.source === 'versioned' && (resolvedContent.storageProvider !== params.asset.storageProvider
825
+ || resolvedContent.contentSha256 !== params.asset.contentSha256
826
+ || resolvedContent.sizeBytes !== params.asset.sizeBytes)) {
827
+ return {
828
+ statusCode: 409,
829
+ headers: { 'Content-Type': 'application/json' },
830
+ body: Buffer.from(JSON.stringify({
831
+ error: params.asset.kind === 'document'
832
+ ? 'The published document head does not match the current canonical document metadata.'
833
+ : 'The published asset head does not match the current canonical asset metadata.',
834
+ code: params.asset.kind === 'document' ? 'MCP_DOCUMENT_HEAD_STALE' : 'WORKSPACE_ASSET_HEAD_STALE',
835
+ details: {
836
+ assetId: params.asset.assetId,
837
+ currentVersion: params.asset.version,
838
+ currentStorageProvider: params.asset.storageProvider,
839
+ currentContentSha256: params.asset.contentSha256,
840
+ currentSizeBytes: params.asset.sizeBytes,
841
+ headVersionId: resolvedContent.versionId,
842
+ headStorageProvider: resolvedContent.storageProvider,
843
+ headContentSha256: resolvedContent.contentSha256,
844
+ headSizeBytes: resolvedContent.sizeBytes,
845
+ },
846
+ })),
847
+ };
848
+ }
821
849
  const physicalStorageKey = normalizeRelativeStorageKey(resolvedContent.physicalKey);
822
850
  if (!physicalStorageKey)
823
851
  return { statusCode: 404 };
@@ -852,7 +880,7 @@ export function createRoutes(core, context = {}) {
852
880
  'Content-Disposition': contentDisposition
853
881
  },
854
882
  body: resolvedContent.source === 'versioned'
855
- ? verifyWorkspaceSyncPublishedContentBody(object.body, resolvedContent, `Document ${params.asset.assetId}`)
883
+ ? verifyWorkspaceSyncPublishedContentBody(object.body, resolvedContent, `${contentLabel} ${params.asset.assetId}`)
856
884
  : object.body
857
885
  };
858
886
  }
@@ -879,7 +907,7 @@ export function createRoutes(core, context = {}) {
879
907
  'Content-Disposition': contentDisposition
880
908
  },
881
909
  body: resolvedContent.source === 'versioned'
882
- ? verifyWorkspaceSyncPublishedContentBody(body, resolvedContent, `Document ${params.asset.assetId}`)
910
+ ? verifyWorkspaceSyncPublishedContentBody(body, resolvedContent, `${contentLabel} ${params.asset.assetId}`)
883
911
  : body
884
912
  };
885
913
  }
@@ -1497,7 +1525,7 @@ export function createRoutes(core, context = {}) {
1497
1525
  sanitizeDownloadFilename,
1498
1526
  contentTypeForExt,
1499
1527
  inferInlineDocumentFromPath,
1500
- resolveCanonicalDocumentRead,
1528
+ resolveCanonicalContentRead,
1501
1529
  inferWorkspaceIdFromStoragePath,
1502
1530
  inferTaskIdFromStoragePath,
1503
1531
  });