@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
@@ -17,7 +17,9 @@ import { resolveCanonicalMcpToolName } from '../../mcp/toolCallCompatibility.js'
17
17
  import { applyPrefixedRateLimitHeaders } from '../rateLimit.js';
18
18
  import { parseAiProfileBootstrapHeaders } from '../../mcp/aiProfileBootstrap.js';
19
19
  import { parseMcpClientId } from '../../mcp/clientRegistry.js';
20
+ import { resolveAppAccessDeniedResponse } from '../appAccess.js';
20
21
  import { resolveCloudApplicationBaseUrl } from '../../config/applicationBaseUrl.js';
22
+ import { getRequestPerformanceDiagnostics, runWithRequestPerformanceDiagnostics, } from '../../storage/postgresRequestDiagnostics.js';
21
23
  // === SECTION: Error helpers ===
22
24
  export function resolveErrorStatusCode(error, fallback = 400) {
23
25
  if (error instanceof TaskforceRuleError)
@@ -39,6 +41,7 @@ export function buildCloudMcpProfileConnectionId(input) {
39
41
  // === SECTION: Route timing helpers ===
40
42
  export const BOOTSTRAP_ROUTE_SLOW_THRESHOLD_MS = 1500;
41
43
  export const BOOTSTRAP_ROUTE_SEVERE_THRESHOLD_MS = 5000;
44
+ export const TASKFORCE_PERFORMANCE_TRACE_HEADER = 'x-taskforce-performance-trace';
42
45
  function sanitizeServerTimingMetricName(name) {
43
46
  return String(name || 'taskforce-route')
44
47
  .trim()
@@ -68,6 +71,55 @@ export function serializeJsonWithTiming(payload, startedAt = getRouteTimingNow()
68
71
  durationMs: Math.max(0, getRouteTimingNow() - startedAt),
69
72
  };
70
73
  }
74
+ export function runWithPerformanceTrace(req, callback) {
75
+ const enabled = requestHeader(req, TASKFORCE_PERFORMANCE_TRACE_HEADER, '').trim() === '1';
76
+ return runWithRequestPerformanceDiagnostics(enabled, callback);
77
+ }
78
+ export function currentPerformanceTraceDiagnostics() {
79
+ return getRequestPerformanceDiagnostics();
80
+ }
81
+ export function performanceTraceServerTimingMetrics(diagnostics) {
82
+ return diagnostics ? [{ name: 'postgres-worker-wait', durationMs: diagnostics.postgresWorkerWaitMs }] : [];
83
+ }
84
+ export function setPerformanceTraceHeaders(res, diagnostics, responseBytes) {
85
+ if (!diagnostics)
86
+ return;
87
+ res.setHeader('X-Taskforce-Postgres-Query-Count', String(diagnostics.postgresQueryCount));
88
+ res.setHeader('X-Taskforce-Postgres-Worker-Call-Count', String(diagnostics.postgresWorkerCallCount));
89
+ res.setHeader('X-Taskforce-Response-Bytes', String(responseBytes));
90
+ }
91
+ export function respondWithPerformanceTraceJson(input) {
92
+ runWithPerformanceTrace(input.req, () => {
93
+ const startedAt = getRouteTimingNow();
94
+ const data = input.load();
95
+ const loadCompletedAt = getRouteTimingNow();
96
+ const serialized = serializeJsonWithTiming(data);
97
+ const diagnostics = getRequestPerformanceDiagnostics();
98
+ const totalDurationMs = Math.max(0, getRouteTimingNow() - startedAt);
99
+ setServerTimingHeader(input.res, [
100
+ { name: 'route-total', durationMs: totalDurationMs },
101
+ { name: 'route-load', durationMs: loadCompletedAt - startedAt },
102
+ { name: 'route-json', durationMs: serialized.durationMs },
103
+ ...performanceTraceServerTimingMetrics(diagnostics),
104
+ ]);
105
+ setPerformanceTraceHeaders(input.res, diagnostics, serialized.byteLength);
106
+ logSlowBootstrapRoute({
107
+ route: input.route,
108
+ workspaceId: input.workspaceId,
109
+ traceId: requestBootstrapTraceId(input.req),
110
+ durationMs: totalDurationMs,
111
+ details: diagnostics ? {
112
+ responseBytes: serialized.byteLength,
113
+ postgresQueryCount: diagnostics.postgresQueryCount,
114
+ postgresWorkerCallCount: diagnostics.postgresWorkerCallCount,
115
+ postgresWorkerWaitMs: Math.round(diagnostics.postgresWorkerWaitMs),
116
+ jsonSerializeMs: serialized.durationMs,
117
+ } : { responseBytes: serialized.byteLength },
118
+ });
119
+ input.res.writeHead(200, { 'Content-Type': 'application/json' });
120
+ input.res.end(serialized.body);
121
+ });
122
+ }
71
123
  export function requestBootstrapTraceId(req) {
72
124
  return requestHeader(req, 'x-taskforce-bootstrap-trace-id', '').trim();
73
125
  }
@@ -100,6 +152,7 @@ export function buildCloudMcpRuntimeAuth(input) {
100
152
  tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
101
153
  aiProfileToken: auth.aiProfileToken,
102
154
  aiProfileId: auth.authenticatedViaOAuth?.aiProfileId || null,
155
+ aiProfileIdentityAsserted: auth.aiProfileIdentityAsserted === true,
103
156
  aiProfileName: auth.aiProfileName,
104
157
  aiProfileIcon: auth.aiProfileIcon,
105
158
  aiProfileColor: auth.aiProfileColor,
@@ -183,6 +236,7 @@ export function makeAuthenticateCloudMcpRequest(core, deps) {
183
236
  userId: null,
184
237
  ...bootstrap,
185
238
  aiProfileId: oauthAuth?.aiProfileId || null,
239
+ aiProfileIdentityAsserted: Boolean(bootstrap.aiProfileName || bootstrap.aiProfileProvider),
186
240
  aiProfileToken: bootstrap.aiProfileToken || oauthAuth?.aiProfileToken || null,
187
241
  aiProfileName: bootstrap.aiProfileName || oauthAuth?.aiProfileName || null,
188
242
  mcpClientId: oauthAuth
@@ -337,6 +391,9 @@ export function makeHandleCloudMcpRequest(core, deps) {
337
391
  };
338
392
  return async function handleCloudMcpRequest(req, res) {
339
393
  const requestStartedAtMs = Date.now();
394
+ let authMs = 0;
395
+ let bodyParseMs = 0;
396
+ let rateLimitMs = 0;
340
397
  if (requestRuntimeMode(req) !== 'cloud') {
341
398
  writeMcpHttpError(res, 400, 'The /mcp HTTP endpoint is only available in cloud runtime. In local runtime, use the local stdio MCP server (for example: `taskforce mcp <project-path>`).');
342
399
  return;
@@ -347,7 +404,9 @@ export function makeHandleCloudMcpRequest(core, deps) {
347
404
  writeMcpHttpError(res, 405, 'Method not allowed.');
348
405
  return;
349
406
  }
407
+ const authStartedAtMs = Date.now();
350
408
  const auth = authenticateCloudMcpRequest(req, res);
409
+ authMs = Date.now() - authStartedAtMs;
351
410
  if (!auth)
352
411
  return;
353
412
  if (req.method !== 'POST') {
@@ -356,7 +415,9 @@ export function makeHandleCloudMcpRequest(core, deps) {
356
415
  }
357
416
  let body = undefined;
358
417
  try {
418
+ const bodyParseStartedAtMs = Date.now();
359
419
  body = await parseJsonBody(req);
420
+ bodyParseMs = Date.now() - bodyParseStartedAtMs;
360
421
  }
361
422
  catch (error) {
362
423
  writeMcpHttpError(res, resolveErrorStatusCode(error, 400), 'Invalid JSON body.');
@@ -364,6 +425,7 @@ export function makeHandleCloudMcpRequest(core, deps) {
364
425
  }
365
426
  const authScopes = auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [];
366
427
  const requestId = String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID();
428
+ res.setHeader('X-Request-ID', requestId);
367
429
  const mcpMethod = !Array.isArray(body) && body !== null && typeof body === 'object'
368
430
  ? String(body.method || '')
369
431
  : '';
@@ -389,7 +451,11 @@ export function makeHandleCloudMcpRequest(core, deps) {
389
451
  writeMcpHttpError(res, 403, 'Forbidden.');
390
452
  return;
391
453
  }
392
- if (!applyCloudMcpWriteRateLimit(req, res, auth, body))
454
+ const writeToolNames = extractMcpWriteToolNames(body);
455
+ const rateLimitStartedAtMs = Date.now();
456
+ const rateLimitAllowed = applyCloudMcpWriteRateLimit(req, res, auth, body);
457
+ rateLimitMs = Date.now() - rateLimitStartedAtMs;
458
+ if (!rateLimitAllowed)
393
459
  return;
394
460
  let runtime = null;
395
461
  let runtimeConstructionMs = 0;
@@ -397,6 +463,7 @@ export function makeHandleCloudMcpRequest(core, deps) {
397
463
  let requestHandlingMs = 0;
398
464
  let cleanupMs = 0;
399
465
  let outcome = 'success';
466
+ const toolTimingState = { value: null };
400
467
  try {
401
468
  const runtimeStartedAtMs = Date.now();
402
469
  // Intentionally isolate the MCP runtime per HTTP request. The runtime closes over
@@ -420,6 +487,25 @@ export function makeHandleCloudMcpRequest(core, deps) {
420
487
  requestId,
421
488
  authScopes,
422
489
  }),
490
+ onToolCallTiming: (timing) => {
491
+ toolTimingState.value = timing;
492
+ if (!res.headersSent) {
493
+ setServerTimingHeader(res, [
494
+ { name: 'auth', durationMs: authMs },
495
+ { name: 'body-parse', durationMs: bodyParseMs },
496
+ { name: 'rate-limit', durationMs: rateLimitMs },
497
+ { name: 'runtime', durationMs: runtimeConstructionMs },
498
+ { name: 'transport-connect', durationMs: transportConnectMs },
499
+ { name: 'tool-envelope-prepare', durationMs: timing.preparationMs },
500
+ { name: 'tool-envelope-validate', durationMs: timing.validationMs },
501
+ { name: 'tool-handler', durationMs: timing.handlerMs },
502
+ ...Object.entries(timing.subphases).map(([phase, durationMs]) => ({
503
+ name: `tool-${phase}`,
504
+ durationMs,
505
+ })),
506
+ ]);
507
+ }
508
+ },
423
509
  });
424
510
  runtimeConstructionMs = Date.now() - runtimeStartedAtMs;
425
511
  const transport = new StreamableHTTPServerTransport({
@@ -465,7 +551,8 @@ export function makeHandleCloudMcpRequest(core, deps) {
465
551
  cleanupMs = Date.now() - cleanupStartedAtMs;
466
552
  }
467
553
  const totalDurationMs = Date.now() - requestStartedAtMs;
468
- if (initializeRequest || mcpMethod === 'tools/list' || totalDurationMs >= 250) {
554
+ if (initializeRequest || mcpMethod === 'tools/list' || writeToolNames.length > 0 || totalDurationMs >= 250) {
555
+ const timingSnapshot = toolTimingState.value;
469
556
  console.info(`[Taskforce MCP] ${JSON.stringify({
470
557
  event: 'cloud_request_timing',
471
558
  requestId,
@@ -477,13 +564,23 @@ export function makeHandleCloudMcpRequest(core, deps) {
477
564
  method: mcpMethod || null,
478
565
  toolName: extractFirstMcpToolName(body),
479
566
  workspaceId: auth.workspaceId,
567
+ databaseProvider: core.getDatabaseProvider(),
568
+ authMode: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
569
+ batchSize: Array.isArray(body) ? body.length : 1,
570
+ isWrite: writeToolNames.length > 0,
480
571
  outcome,
572
+ httpStatus: res.statusCode,
573
+ responseBytes: timingSnapshot?.responseBytes ?? null,
481
574
  totalDurationMs,
482
575
  preRuntimeMs: Math.max(0, totalDurationMs - runtimeConstructionMs - transportConnectMs - requestHandlingMs - cleanupMs),
576
+ authMs,
577
+ bodyParseMs,
578
+ rateLimitMs,
483
579
  runtimeConstructionMs,
484
580
  transportConnectMs,
485
581
  requestHandlingMs,
486
582
  cleanupMs,
583
+ toolTiming: timingSnapshot,
487
584
  })}`);
488
585
  }
489
586
  }
@@ -521,6 +618,17 @@ export function requestSystemRole(req) {
521
618
  export function requestUserId(req) {
522
619
  return requestHeader(req, 'x-taskforce-user-id', 'anonymous') || 'anonymous';
523
620
  }
621
+ /**
622
+ * Resolve the human actor recorded by ordinary HTTP mutations. The standalone
623
+ * server has already replaced this header with authenticated cloud identity
624
+ * when available; local signed-in clients provide the same account id through
625
+ * the request-scoped mutation header. Anonymous sessions retain the legacy
626
+ * generic actor for compatibility.
627
+ */
628
+ export function requestHumanActorRef(req) {
629
+ const userId = String(requestUserId(req) || '').trim();
630
+ return userId && userId !== 'anonymous' ? userId : 'user';
631
+ }
524
632
  // === SECTION: Workspace ID resolution ===
525
633
  export function makeRequestWorkspaceId(core, context) {
526
634
  return function requestWorkspaceId(req) {
@@ -625,23 +733,11 @@ export function makeEnsureAppAccess(core, context) {
625
733
  if (!access.authenticated || !access.userId || access.userId === 'anonymous')
626
734
  return true;
627
735
  const accountAccess = core.getAccountAccessStatus(access.userId);
628
- if (accountAccess.canAccessApp)
736
+ const denied = resolveAppAccessDeniedResponse(accountAccess);
737
+ if (!denied)
629
738
  return true;
630
- const code = accountAccess.gate === 'plan_selection_required'
631
- ? 'PLAN_SELECTION_REQUIRED'
632
- : accountAccess.gate === 'checkout_pending'
633
- ? 'CHECKOUT_PENDING'
634
- : accountAccess.gate === 'misconfigured'
635
- ? 'ACCOUNT_ACCESS_MISCONFIGURED'
636
- : 'ACCOUNT_ENTITLEMENT_REQUIRED';
637
- res.writeHead(403, { 'Content-Type': 'application/json' });
638
- res.end(JSON.stringify({
639
- error: accountAccess.message || 'This account cannot access the app.',
640
- code,
641
- gate: accountAccess.gate,
642
- canAccessPlans: accountAccess.canAccessPlans,
643
- planSelectionRequired: accountAccess.planSelectionRequired
644
- }));
739
+ res.writeHead(denied.statusCode, { 'Content-Type': 'application/json' });
740
+ res.end(JSON.stringify(denied.body));
645
741
  return false;
646
742
  };
647
743
  }
@@ -2,6 +2,7 @@ import { parseJsonBody } from './primitives.js';
2
2
  import { resolveSessionCookieAttributesForRequest } from '../auth.js';
3
3
  import { isAiProfileSurfaceType, parseAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
4
4
  import { isAiProfileSeatScope } from '../../shared/aiProfileSeatScope.js';
5
+ import { assertPlanningIdentityColor, assertPlanningIdentityIcon } from '../../shared/planningIdentity.js';
5
6
  import { serializeBinaryAssetSyncPayload, serializeDocumentSyncPayload, } from '../../sync/contentSyncPayload.js';
6
7
  import { normalizeAiProfileSyncPayload, normalizeAgentConversationSyncPayload, normalizeAgentRoleSyncPayload, normalizeAnnotatedAttachmentSessionSyncPayload, normalizeDocumentReviewCommentSyncPayload, normalizeDocumentReviewSessionSyncPayload, normalizeTaskforceAgentSkillSyncPayload, normalizeTaskforceAgentSyncPayload, } from '../../sync/collaborationSyncPayload.js';
7
8
  import { normalizeInitiativeSyncPayload, normalizeTaxonomySyncPayload, normalizeWorkstreamSyncPayload, } from '../../sync/planningSyncPayload.js';
@@ -451,6 +452,27 @@ export function registerSyncRoutes(deps) {
451
452
  op: 'workstream-upsert',
452
453
  workstream
453
454
  })),
455
+ ...[
456
+ ...incomingInitiatives.map((entity) => ({
457
+ entityType: 'initiative',
458
+ entityId: String(entity.id || '').trim(),
459
+ icon: assertPlanningIdentityIcon(entity.icon),
460
+ color: assertPlanningIdentityColor(entity.color),
461
+ updatedAt: String(entity.identityUpdatedAt || '').trim(),
462
+ })),
463
+ ...incomingWorkstreams.map((entity) => ({
464
+ entityType: 'workstream',
465
+ entityId: String(entity.id || '').trim(),
466
+ icon: assertPlanningIdentityIcon(entity.icon),
467
+ color: assertPlanningIdentityColor(entity.color),
468
+ updatedAt: String(entity.identityUpdatedAt || '').trim(),
469
+ })),
470
+ ]
471
+ .filter((identity) => identity.entityId.length > 0 && Number.isFinite(Date.parse(identity.updatedAt)))
472
+ .map((identity) => ({
473
+ op: 'planning-identity-upsert',
474
+ identity,
475
+ })),
454
476
  ...incomingAiProfiles
455
477
  .map((profile) => normalizeAiProfileSyncPayload(profile, cloudWorkspaceId, normalizeIncomingAiProfileSurfaceType, normalizeIncomingAiProfileSeatScope, 'push.aiProfileUpsert'))
456
478
  .filter((profile) => Boolean(profile && profile.id.length > 0))
@@ -352,7 +352,8 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
352
352
  pageSize: Number.isFinite(limit) ? limit : undefined,
353
353
  nestedCoverage: coverage.wire.nestedWriterPrerequisitesRequired,
354
354
  workflowCoverage: coverage.wire.workflowWriterPrerequisitesRequired,
355
- planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired })
355
+ planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired,
356
+ coveredLegacyWritesFenced: combinedLegacyPushDisabled })
356
357
  : buildWorkspaceSyncV3InitiativeRepairPage({ core: input.core, workspaceId,
357
358
  cursorKeyring: cursorKeyring, cursor: repairCursor,
358
359
  pageSize: Number.isFinite(limit) ? limit : undefined });
@@ -397,7 +398,8 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
397
398
  cursorKeyring: cursorKeyring, pageSize: Number.isFinite(limit) ? limit : undefined,
398
399
  nestedCoverage: coverage.wire.nestedWriterPrerequisitesRequired,
399
400
  workflowCoverage: coverage.wire.workflowWriterPrerequisitesRequired,
400
- planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired })
401
+ planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired,
402
+ coveredLegacyWritesFenced: combinedLegacyPushDisabled })
401
403
  : buildWorkspaceSyncV3InitiativeRepairPage({ core: input.core, workspaceId,
402
404
  cursorKeyring: cursorKeyring, pageSize: Number.isFinite(limit) ? limit : undefined });
403
405
  const repair = coverage.kind === 'combined' ? page : { ...page,
@@ -203,7 +203,8 @@ function parseEnvelope(raw) {
203
203
  return { ...common, domain: 'task-checklist', mutationKind: 'replace', items };
204
204
  }
205
205
  if (operation.domain === 'task-attachment-links' && operation.mutationKind === 'replace') {
206
- assertExactKeys(payload, ['links'], 'operation.payload');
206
+ const hasCardCoverAssetId = Object.prototype.hasOwnProperty.call(payload, 'cardCoverAssetId');
207
+ assertExactKeys(payload, hasCardCoverAssetId ? ['links', 'cardCoverAssetId'] : ['links'], 'operation.payload');
207
208
  let links;
208
209
  try {
209
210
  links = validateWorkspaceSyncV3TaskAttachmentLinksPayload({ taskId: common.entityId, links: payload.links }).links;
@@ -211,7 +212,21 @@ function parseEnvelope(raw) {
211
212
  catch (error) {
212
213
  throw new LocalCaptureRequestError(error instanceof Error ? error.message : 'operation.payload.links is invalid.');
213
214
  }
214
- return { ...common, domain: 'task-attachment-links', mutationKind: 'replace', links };
215
+ if (hasCardCoverAssetId && payload.cardCoverAssetId !== null
216
+ && (typeof payload.cardCoverAssetId !== 'string' || !payload.cardCoverAssetId.trim())) {
217
+ throw new LocalCaptureRequestError('operation.payload.cardCoverAssetId must be a non-empty string or null.');
218
+ }
219
+ return {
220
+ ...common,
221
+ domain: 'task-attachment-links',
222
+ mutationKind: 'replace',
223
+ links,
224
+ ...(hasCardCoverAssetId ? {
225
+ cardCoverAssetId: payload.cardCoverAssetId === null
226
+ ? null
227
+ : String(payload.cardCoverAssetId).trim(),
228
+ } : {}),
229
+ };
215
230
  }
216
231
  if (operation.domain === 'initiative') {
217
232
  const base = {
@@ -701,6 +716,9 @@ export function registerWorkspaceSyncV3LocalCaptureRoutes(input) {
701
716
  identity,
702
717
  taskId: envelope.entityId,
703
718
  links: envelope.links,
719
+ ...(Object.prototype.hasOwnProperty.call(envelope, 'cardCoverAssetId')
720
+ ? { cardCoverAssetId: envelope.cardCoverAssetId ?? null }
721
+ : {}),
704
722
  now: acceptedAt,
705
723
  actorRef: 'user',
706
724
  onPostCommitError: input.onPostCommitError,
@@ -471,7 +471,11 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
471
471
  if (baseLifecycleRevision !== '0') {
472
472
  throw new SyncV3RequestError('Task attachment-link lifecycle revision must be 0.');
473
473
  }
474
- assertExactKeys(payload, payload.originActor === undefined ? ['links'] : ['links', 'originActor'], 'operation.payload');
474
+ assertExactKeys(payload, [
475
+ 'links',
476
+ ...(payload.cardCoverAssetId === undefined ? [] : ['cardCoverAssetId']),
477
+ ...(payload.originActor === undefined ? [] : ['originActor']),
478
+ ], 'operation.payload');
475
479
  let links;
476
480
  try {
477
481
  links = validateWorkspaceSyncV3TaskAttachmentLinksPayload({ taskId: entityId, links: payload.links }).links;
@@ -482,10 +486,21 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
482
486
  const originActor = payload.originActor === undefined
483
487
  ? undefined
484
488
  : requireOriginActor(payload.originActor);
489
+ const cardCoverAssetId = payload.cardCoverAssetId === undefined
490
+ ? undefined
491
+ : payload.cardCoverAssetId === null
492
+ ? null
493
+ : typeof payload.cardCoverAssetId === 'string' && payload.cardCoverAssetId.trim()
494
+ ? payload.cardCoverAssetId.trim()
495
+ : (() => { throw new SyncV3RequestError('operation.payload.cardCoverAssetId must be a string or null.'); })();
485
496
  return { ...common, operation: {
486
497
  domain: 'task-attachment-links', entityId, mutationKind: 'replace', baseEntityRevision,
487
498
  baseLifecycleRevision: '0',
488
- payload: { links, ...(originActor ? { originActor } : {}) },
499
+ payload: {
500
+ links,
501
+ ...(cardCoverAssetId !== undefined ? { cardCoverAssetId } : {}),
502
+ ...(originActor ? { originActor } : {}),
503
+ },
489
504
  } };
490
505
  }
491
506
  if (operation.domain === 'entity-event'