@taskforcehq/taskforce 0.3.330 → 0.3.333

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 (107) hide show
  1. package/README.md +7 -2
  2. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +2 -2
  3. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.js +2 -2
  4. package/dist/components/features/planning/PlanningModule.js +15 -3
  5. package/dist/components/features/planning/planningMetadata.d.ts +10 -0
  6. package/dist/components/features/planning/planningMetadata.js +8 -0
  7. package/dist/components/views/StandaloneLayout.js +35 -1
  8. package/dist/components/views/panels/PlanningDrawer.d.ts +2 -1
  9. package/dist/components/views/panels/PlanningDrawer.js +12 -8
  10. package/dist/config/envSchema.js +7 -0
  11. package/dist/core/McpTokenService.d.ts +2 -0
  12. package/dist/core/McpTokenService.js +13 -5
  13. package/dist/core/Taskforce.d.ts +11 -3
  14. package/dist/core/Taskforce.js +33 -3
  15. package/dist/core/types.d.ts +1 -0
  16. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +2 -0
  17. package/dist/documentReviews/DocumentReviewCommentStore.js +28 -9
  18. package/dist/mcp/httpProtocolRouting.d.ts +1 -0
  19. package/dist/mcp/httpProtocolRouting.js +8 -5
  20. package/dist/mcp/runtime.js +24 -5
  21. package/dist/mcp/taskPlanningRegistrar.js +2 -2
  22. package/dist/mcp/toolProfiles.js +1 -0
  23. package/dist/mcp/toolRegistry.js +8 -8
  24. package/dist/server/buildInfo.d.ts +1 -0
  25. package/dist/server/buildInfo.js +5 -1
  26. package/dist/server/cloudMcpCapacityTelemetry.d.ts +68 -0
  27. package/dist/server/cloudMcpCapacityTelemetry.js +221 -0
  28. package/dist/server/localWorkspaceSyncServerRuntime.js +13 -5
  29. package/dist/server/routes/admin.js +14 -2
  30. package/dist/server/routes/localService.js +1 -0
  31. package/dist/server/routes/shared.d.ts +3 -0
  32. package/dist/server/routes/shared.js +280 -240
  33. package/dist/server/routes/syncPushRoutes.d.ts +3 -0
  34. package/dist/server/routes/syncPushRoutes.js +8 -2
  35. package/dist/service/localServiceCli.d.ts +2 -2
  36. package/dist/service/localServiceCli.js +4 -3
  37. package/dist/services/executorPhaseAIssuerCore.d.ts +13 -0
  38. package/dist/services/executorPhaseAIssuerCore.js +107 -0
  39. package/dist/services/taskforceAgentMcpBridge.d.ts +1 -1
  40. package/dist/services/taskforceAgentMcpBridge.js +35 -5
  41. package/dist/shared/executorPhaseASchemaValidation.js +1 -1
  42. package/dist/storage/postgresAdapter.js +3 -1
  43. package/dist/storage/postgresRequestDiagnostics.d.ts +9 -1
  44. package/dist/storage/postgresRequestDiagnostics.js +15 -2
  45. package/dist/storage/postgresWorker.js +13 -3
  46. package/dist/storage/postgresWorkerProtocol.d.ts +6 -0
  47. package/dist/storage/workflowStore.d.ts +2 -0
  48. package/dist/storage/workflowStore.js +39 -2
  49. package/dist/sync/cloudSyncApi.js +4 -2
  50. package/dist/sync/collaborationSyncState.js +5 -0
  51. package/dist/sync/coordinator/localWorkspaceSyncRunner.d.ts +3 -1
  52. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +18 -0
  53. package/dist/sync/coordinator/localWorkspaceSyncRunnerState.d.ts +1 -0
  54. package/dist/sync/coordinator/localWorkspaceSyncRunnerState.js +7 -6
  55. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +2 -2
  56. package/dist/sync/engine/workspaceSyncEngineLocalApply.d.ts +1 -0
  57. package/dist/sync/engine/workspaceSyncEnginePushCompletion.d.ts +6 -0
  58. package/dist/sync/engine/workspaceSyncEnginePushCompletion.js +34 -1
  59. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +2 -2
  60. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +4 -1
  61. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.d.ts +1 -0
  62. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +55 -10
  63. package/dist/sync/syncApplyHandlers.d.ts +1 -0
  64. package/dist/sync/syncApplyHandlers.js +72 -15
  65. package/dist/sync/syncService.d.ts +1 -0
  66. package/dist/sync/syncService.js +5 -2
  67. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.d.ts +7 -7
  68. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.js +50 -13
  69. package/dist/sync/v3/localOutboxRecoveryPreparationService.js +8 -2
  70. package/dist/sync/v3/localTaskCreateOutboxService.d.ts +3 -1
  71. package/dist/sync/v3/localTaskCreateOutboxService.js +6 -1
  72. package/dist/sync/v3/localTaskDeleteOutboxService.js +1 -0
  73. package/dist/sync/v3/localTaskLifecycleOutboxService.d.ts +2 -1
  74. package/dist/sync/v3/localTaskLifecycleOutboxService.js +5 -1
  75. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -1
  76. package/dist/sync/v3/localTaskMetadataOutboxService.js +6 -2
  77. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -1
  78. package/dist/sync/v3/localTaskStatusOutboxService.js +20 -5
  79. package/dist/sync/v3/syncDurabilityStore.d.ts +2 -1
  80. package/dist/sync/v3/syncDurabilityStore.js +34 -6
  81. package/dist/sync/v3/workflowRuntimeMutationService.d.ts +5 -0
  82. package/dist/sync/v3/workflowRuntimeMutationService.js +124 -71
  83. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +25 -3
  84. package/dist/sync/v3/workspaceSyncV3Feed.d.ts +1 -0
  85. package/dist/sync/v3/workspaceSyncV3Feed.js +6 -1
  86. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +2 -0
  87. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +49 -18
  88. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.d.ts +1 -0
  89. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.js +50 -5
  90. package/dist/ui/assets/{AiIdentityRosterCard-OcSz1PkK.js → AiIdentityRosterCard-Cfvb9Nuv.js} +1 -1
  91. package/dist/ui/assets/{AiProfilesModule-ByhYW5Xi.js → AiProfilesModule-0oR70eFG.js} +1 -1
  92. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTM6nIg_.js → AnnotatedAttachmentWorkspace-C33iH_r_.js} +1 -1
  93. package/dist/ui/assets/{AssetTraySortControl-DabEG8L4.js → AssetTraySortControl-BAqxUtke.js} +1 -1
  94. package/dist/ui/assets/{ContextAttachmentManager-Coa8yVGM.js → ContextAttachmentManager-BudIbOR9.js} +2 -2
  95. package/dist/ui/assets/{DocumentWorkspace-BB0qhdix.js → DocumentWorkspace-A8dFQyId.js} +1 -1
  96. package/dist/ui/assets/{EntityActivityTimeline-CZ8x4UJE.js → EntityActivityTimeline-g7vtdsx1.js} +1 -1
  97. package/dist/ui/assets/PlanningModule-D-DeQs4F.js +1 -0
  98. package/dist/ui/assets/{PlansPage-D86pZSSl.js → PlansPage-BWmfGi0Z.js} +1 -1
  99. package/dist/ui/assets/{TaskContextUpload-BVIkU9yT.js → TaskContextUpload-BA_lQmS9.js} +1 -1
  100. package/dist/ui/assets/{TaskSettings-Cv1mNhv_.js → TaskSettings-DAlH4YIP.js} +1 -1
  101. package/dist/ui/assets/{TaskforceAgentsModule-gmH-PD2q.js → TaskforceAgentsModule-DmeCf6pJ.js} +2 -2
  102. package/dist/ui/assets/{WorkflowManagerModule-DbfnHJ-f.js → WorkflowManagerModule-p08eDOaa.js} +1 -1
  103. package/dist/ui/assets/index-GHJgcW3j.js +7 -0
  104. package/dist/ui/index.html +1 -1
  105. package/package.json +3 -1
  106. package/dist/ui/assets/PlanningModule-pRPjR7v5.js +0 -1
  107. package/dist/ui/assets/index-CTsDBaef.js +0 -7
@@ -21,6 +21,7 @@ import { handleModernMcpHttpRequest, isModernMcpHttpRequest, } from '../../mcp/h
21
21
  import { resolveAppAccessDeniedResponse } from '../appAccess.js';
22
22
  import { resolveCloudApplicationBaseUrl } from '../../config/applicationBaseUrl.js';
23
23
  import { getRequestPerformanceDiagnostics, runWithRequestPerformanceDiagnostics, } from '../../storage/postgresRequestDiagnostics.js';
24
+ import { beginCloudMcpCapacityRequest, isCloudMcpCapacityTelemetryEnabled, TASKFORCE_CAPACITY_TRACE_HEADER, } from '../cloudMcpCapacityTelemetry.js';
24
25
  // === SECTION: Error helpers ===
25
26
  export function resolveErrorStatusCode(error, fallback = 400) {
26
27
  if (error instanceof TaskforceRuleError)
@@ -324,7 +325,7 @@ function findDeniedCloudMcpToolName(body, scopes, resolveToolPolicy) {
324
325
  return null;
325
326
  }
326
327
  export function makeHandleCloudMcpRequest(core, deps) {
327
- const { ensureAppAccess, resolveAuthenticatedUserId, writeRateLimit, createMcpServer = createTaskforceMcpServer, resolveMcpToolPolicy = resolveProductionCloudMcpToolPolicy, acquireMcpRuntime, } = deps;
328
+ const { ensureAppAccess, resolveAuthenticatedUserId, writeRateLimit, createMcpServer = createTaskforceMcpServer, resolveMcpToolPolicy = resolveProductionCloudMcpToolPolicy, acquireMcpRuntime, capacityTelemetryEnabled = isCloudMcpCapacityTelemetryEnabled, beginCapacityRequest = beginCloudMcpCapacityRequest, } = deps;
328
329
  const authenticateCloudMcpRequest = makeAuthenticateCloudMcpRequest(core, {
329
330
  resolveAuthenticatedUserId,
330
331
  writeMcpHttpError,
@@ -405,105 +406,209 @@ export function makeHandleCloudMcpRequest(core, deps) {
405
406
  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>`).');
406
407
  return;
407
408
  }
408
- if (!ensureAppAccess(req, res))
409
- return;
410
- if (req.method !== 'POST' && req.method !== 'GET' && req.method !== 'DELETE') {
411
- writeMcpHttpError(res, 405, 'Method not allowed.');
412
- return;
413
- }
414
- const authStartedAtMs = Date.now();
415
- const auth = authenticateCloudMcpRequest(req, res);
416
- authMs = Date.now() - authStartedAtMs;
417
- if (!auth)
418
- return;
419
- if (req.method !== 'POST') {
420
- writeMcpHttpError(res, 405, 'Method not allowed.');
421
- return;
422
- }
423
- if (!isJsonContentType(requestHeader(req, 'content-type', ''))) {
424
- writeMcpHttpError(res, 415, 'Unsupported Media Type: Content-Type must be application/json');
425
- return;
426
- }
427
- let body = undefined;
428
- try {
429
- const bodyParseStartedAtMs = Date.now();
430
- body = await parseJsonBody(req);
431
- bodyParseMs = Date.now() - bodyParseStartedAtMs;
432
- }
433
- catch (error) {
434
- writeMcpHttpError(res, resolveErrorStatusCode(error, 400), 'Invalid JSON body.');
435
- return;
436
- }
437
- const authScopes = auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [];
438
- const requestId = String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID();
439
- res.setHeader('X-Request-ID', requestId);
440
- const mcpMethod = !Array.isArray(body) && body !== null && typeof body === 'object'
441
- ? String(body.method || '')
442
- : '';
443
- const initializeRequest = isInitializeRequest(body)
444
- || mcpMethod === 'initialize';
445
- const modernProtocolRequest = await isModernMcpHttpRequest(req, body);
446
- const deniedToolName = findDeniedCloudMcpToolName(body, authScopes, resolveMcpToolPolicy);
447
- if (deniedToolName) {
448
- core.addMcpAuditLog({
449
- workspaceId: auth.workspaceId,
450
- actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
451
- actorUserId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || 'anonymous',
452
- tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
453
- toolName: deniedToolName,
454
- details: {
455
- requestId,
456
- ip: req.socket?.remoteAddress || null,
457
- userAgent: requestHeader(req, 'user-agent', '') || null,
458
- outcome: 'deny',
459
- reasonCode: 'scope_denied',
460
- scopes: authScopes
409
+ const capacityRequest = beginCapacityRequest();
410
+ const capacityTraceEnabled = requestHeader(req, TASKFORCE_CAPACITY_TRACE_HEADER, '').trim() === '1';
411
+ return runWithRequestPerformanceDiagnostics(capacityTelemetryEnabled(), async () => {
412
+ let capacitySettled = false;
413
+ const settleCapacity = (aborted = false) => {
414
+ if (capacitySettled)
415
+ return;
416
+ capacitySettled = true;
417
+ capacityRequest.finish({
418
+ statusCode: res.statusCode,
419
+ aborted,
420
+ postgres: getRequestPerformanceDiagnostics(),
421
+ });
422
+ };
423
+ const onFinish = () => settleCapacity(false);
424
+ const onClose = () => settleCapacity(!res.writableEnded);
425
+ res.once('finish', onFinish);
426
+ res.once('close', onClose);
427
+ try {
428
+ if (!ensureAppAccess(req, res))
429
+ return;
430
+ if (req.method !== 'POST' && req.method !== 'GET' && req.method !== 'DELETE') {
431
+ writeMcpHttpError(res, 405, 'Method not allowed.');
432
+ return;
461
433
  }
462
- });
463
- writeMcpHttpError(res, 403, 'Forbidden.');
464
- return;
465
- }
466
- const writeToolNames = extractMcpWriteToolNames(body, resolveMcpToolPolicy);
467
- const rateLimitStartedAtMs = Date.now();
468
- const rateLimitAllowed = applyCloudMcpWriteRateLimit(req, res, auth, body);
469
- rateLimitMs = Date.now() - rateLimitStartedAtMs;
470
- if (!rateLimitAllowed)
471
- return;
472
- let runtime = null;
473
- let runtimeLease = null;
474
- let runtimeConstructionMs = 0;
475
- let transportConnectMs = 0;
476
- let requestHandlingMs = 0;
477
- let cleanupMs = 0;
478
- let outcome = 'success';
479
- const toolTimingState = { value: null };
480
- try {
481
- const runtimeStartedAtMs = Date.now();
482
- // Production intentionally isolates the MCP runtime per HTTP request. The runtime closes over
483
- // workspace auth, AI identity, permissions, subscriptions, and mutable helper state;
484
- // sharing it could leak state across tenants or callers. Keep the transport stateless
485
- // too. The timing event below measures construction cost so caching is only revisited
486
- // with evidence and must preserve these boundaries (including dynamic tool schemas). The optional
487
- // lease seam exists only for transport conformance tests that must carry legacy bidirectional calls.
488
- const runtimeOptions = {
489
- projectRoot: core.getPaths().projectRoot,
490
- tenantId: core.getTenantId(),
491
- workspaceId: auth.workspaceId,
492
- core,
493
- serverName: 'taskforce-cloud',
494
- logStartup: false,
495
- runtimeMode: 'cloud',
496
- toolProfile: auth.authenticatedViaToken?.toolProfile || auth.authenticatedViaOAuth?.toolProfile || 'default',
497
- applicationBaseUrl: resolveCloudApplicationBaseUrl(),
498
- auth: buildCloudMcpRuntimeAuth({
499
- req,
500
- auth,
501
- requestHeader,
502
- requestId,
503
- authScopes,
504
- }),
505
- onToolCallTiming: (timing) => {
506
- toolTimingState.value = timing;
434
+ const authStartedAtMs = Date.now();
435
+ const auth = authenticateCloudMcpRequest(req, res);
436
+ authMs = Date.now() - authStartedAtMs;
437
+ if (!auth)
438
+ return;
439
+ if (capacityTraceEnabled)
440
+ capacityRequest.setTraceHeaders(res);
441
+ if (req.method !== 'POST') {
442
+ writeMcpHttpError(res, 405, 'Method not allowed.');
443
+ return;
444
+ }
445
+ if (!isJsonContentType(requestHeader(req, 'content-type', ''))) {
446
+ writeMcpHttpError(res, 415, 'Unsupported Media Type: Content-Type must be application/json');
447
+ return;
448
+ }
449
+ let body = undefined;
450
+ try {
451
+ const bodyParseStartedAtMs = Date.now();
452
+ body = await parseJsonBody(req);
453
+ bodyParseMs = Date.now() - bodyParseStartedAtMs;
454
+ }
455
+ catch (error) {
456
+ writeMcpHttpError(res, resolveErrorStatusCode(error, 400), 'Invalid JSON body.');
457
+ return;
458
+ }
459
+ const authScopes = auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [];
460
+ const requestId = String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID();
461
+ res.setHeader('X-Request-ID', requestId);
462
+ const mcpMethod = !Array.isArray(body) && body !== null && typeof body === 'object'
463
+ ? String(body.method || '')
464
+ : '';
465
+ const initializeRequest = isInitializeRequest(body)
466
+ || mcpMethod === 'initialize';
467
+ const modernProtocolRequest = await isModernMcpHttpRequest(req, body);
468
+ const writeToolNames = extractMcpWriteToolNames(body, resolveMcpToolPolicy);
469
+ const requestClass = initializeRequest
470
+ ? 'initialize'
471
+ : mcpMethod === 'server/discover'
472
+ ? 'discover'
473
+ : mcpMethod === 'tools/list'
474
+ ? 'tools_list'
475
+ : writeToolNames.length > 0
476
+ ? 'write'
477
+ : extractFirstMcpToolName(body)
478
+ ? 'read'
479
+ : 'other';
480
+ capacityRequest.classify(requestClass);
481
+ const deniedToolName = findDeniedCloudMcpToolName(body, authScopes, resolveMcpToolPolicy);
482
+ if (deniedToolName) {
483
+ core.addMcpAuditLog({
484
+ workspaceId: auth.workspaceId,
485
+ actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
486
+ actorUserId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || 'anonymous',
487
+ tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
488
+ toolName: deniedToolName,
489
+ details: {
490
+ requestId,
491
+ ip: req.socket?.remoteAddress || null,
492
+ userAgent: requestHeader(req, 'user-agent', '') || null,
493
+ outcome: 'deny',
494
+ reasonCode: 'scope_denied',
495
+ scopes: authScopes
496
+ }
497
+ });
498
+ writeMcpHttpError(res, 403, 'Forbidden.');
499
+ return;
500
+ }
501
+ const rateLimitStartedAtMs = Date.now();
502
+ const rateLimitAllowed = applyCloudMcpWriteRateLimit(req, res, auth, body);
503
+ rateLimitMs = Date.now() - rateLimitStartedAtMs;
504
+ if (!rateLimitAllowed)
505
+ return;
506
+ let runtime = null;
507
+ let runtimeLease = null;
508
+ let runtimeConstructionMs = 0;
509
+ let transportConnectMs = 0;
510
+ let requestHandlingMs = 0;
511
+ let cleanupMs = 0;
512
+ let outcome = 'success';
513
+ const toolTimingState = { value: null };
514
+ try {
515
+ const runtimeStartedAtMs = Date.now();
516
+ // Production intentionally isolates the MCP runtime per HTTP request. The runtime closes over
517
+ // workspace auth, AI identity, permissions, subscriptions, and mutable helper state;
518
+ // sharing it could leak state across tenants or callers. Keep the transport stateless
519
+ // too. The timing event below measures construction cost so caching is only revisited
520
+ // with evidence and must preserve these boundaries (including dynamic tool schemas). The optional
521
+ // lease seam exists only for transport conformance tests that must carry legacy bidirectional calls.
522
+ const runtimeOptions = {
523
+ projectRoot: core.getPaths().projectRoot,
524
+ tenantId: core.getTenantId(),
525
+ workspaceId: auth.workspaceId,
526
+ core,
527
+ serverName: 'taskforce-cloud',
528
+ logStartup: false,
529
+ runtimeMode: 'cloud',
530
+ toolProfile: auth.authenticatedViaToken?.toolProfile || auth.authenticatedViaOAuth?.toolProfile || 'default',
531
+ applicationBaseUrl: resolveCloudApplicationBaseUrl(),
532
+ auth: buildCloudMcpRuntimeAuth({
533
+ req,
534
+ auth,
535
+ requestHeader,
536
+ requestId,
537
+ authScopes,
538
+ }),
539
+ onToolCallTiming: (timing) => {
540
+ toolTimingState.value = timing;
541
+ if (!res.headersSent) {
542
+ if (capacityTraceEnabled) {
543
+ capacityRequest.setTraceHeaders(res, getRequestPerformanceDiagnostics());
544
+ }
545
+ setServerTimingHeader(res, [
546
+ { name: 'auth', durationMs: authMs },
547
+ { name: 'body-parse', durationMs: bodyParseMs },
548
+ { name: 'rate-limit', durationMs: rateLimitMs },
549
+ { name: 'runtime', durationMs: runtimeConstructionMs },
550
+ { name: 'transport-connect', durationMs: transportConnectMs },
551
+ { name: 'tool-envelope-prepare', durationMs: timing.preparationMs },
552
+ { name: 'tool-envelope-validate', durationMs: timing.validationMs },
553
+ { name: 'tool-handler', durationMs: timing.handlerMs },
554
+ ...Object.entries(timing.subphases).map(([phase, durationMs]) => ({
555
+ name: `tool-${phase}`,
556
+ durationMs,
557
+ })),
558
+ ]);
559
+ }
560
+ },
561
+ };
562
+ if (modernProtocolRequest) {
563
+ const requestHandlingStartedAtMs = Date.now();
564
+ await handleModernMcpHttpRequest({
565
+ req,
566
+ res,
567
+ parsedBody: body,
568
+ factory: ({ era }) => {
569
+ const constructionStartedAtMs = Date.now();
570
+ const modernRuntime = createStatelessTaskforceMcpServer({
571
+ ...runtimeOptions,
572
+ protocolEra: era,
573
+ }, createMcpServer);
574
+ runtimeConstructionMs = Date.now() - constructionStartedAtMs;
575
+ if (!res.headersSent) {
576
+ setServerTimingHeader(res, [
577
+ { name: 'auth', durationMs: authMs },
578
+ { name: 'body-parse', durationMs: bodyParseMs },
579
+ { name: 'rate-limit', durationMs: rateLimitMs },
580
+ { name: 'runtime', durationMs: runtimeConstructionMs },
581
+ ]);
582
+ }
583
+ return modernRuntime.server;
584
+ },
585
+ onerror: (error) => {
586
+ console.warn('[Taskforce MCP] Modern cloud request rejected:', {
587
+ requestId,
588
+ message: error.message,
589
+ });
590
+ },
591
+ });
592
+ requestHandlingMs = Date.now() - requestHandlingStartedAtMs;
593
+ recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
594
+ return;
595
+ }
596
+ runtimeLease = acquireMcpRuntime
597
+ ? await acquireMcpRuntime({ req, options: runtimeOptions })
598
+ : null;
599
+ runtime = runtimeLease?.runtime
600
+ || createStatelessTaskforceMcpServer(runtimeOptions, createMcpServer);
601
+ runtimeConstructionMs = Date.now() - runtimeStartedAtMs;
602
+ const transport = runtimeLease?.transport || new NodeStreamableHTTPServerTransport({
603
+ sessionIdGenerator: undefined,
604
+ enableJsonResponse: true,
605
+ });
606
+ const transportStartedAtMs = Date.now();
607
+ if (!runtimeLease)
608
+ await runtime.server.connect(transport);
609
+ transportConnectMs = Date.now() - transportStartedAtMs;
610
+ // Set request-level phases before the SDK writes response headers. Tool calls replace this
611
+ // with the richer envelope/handler breakdown from onToolCallTiming above.
507
612
  if (!res.headersSent) {
508
613
  setServerTimingHeader(res, [
509
614
  { name: 'auth', durationMs: authMs },
@@ -511,155 +616,90 @@ export function makeHandleCloudMcpRequest(core, deps) {
511
616
  { name: 'rate-limit', durationMs: rateLimitMs },
512
617
  { name: 'runtime', durationMs: runtimeConstructionMs },
513
618
  { name: 'transport-connect', durationMs: transportConnectMs },
514
- { name: 'tool-envelope-prepare', durationMs: timing.preparationMs },
515
- { name: 'tool-envelope-validate', durationMs: timing.validationMs },
516
- { name: 'tool-handler', durationMs: timing.handlerMs },
517
- ...Object.entries(timing.subphases).map(([phase, durationMs]) => ({
518
- name: `tool-${phase}`,
519
- durationMs,
520
- })),
521
619
  ]);
522
620
  }
523
- },
524
- };
525
- if (modernProtocolRequest) {
526
- const requestHandlingStartedAtMs = Date.now();
527
- await handleModernMcpHttpRequest({
528
- req,
529
- res,
530
- parsedBody: body,
531
- factory: ({ era }) => {
532
- const constructionStartedAtMs = Date.now();
533
- const modernRuntime = createStatelessTaskforceMcpServer({
534
- ...runtimeOptions,
535
- protocolEra: era,
536
- }, createMcpServer);
537
- runtimeConstructionMs = Date.now() - constructionStartedAtMs;
538
- if (!res.headersSent) {
539
- setServerTimingHeader(res, [
540
- { name: 'auth', durationMs: authMs },
541
- { name: 'body-parse', durationMs: bodyParseMs },
542
- { name: 'rate-limit', durationMs: rateLimitMs },
543
- { name: 'runtime', durationMs: runtimeConstructionMs },
544
- ]);
545
- }
546
- return modernRuntime.server;
547
- },
548
- onerror: (error) => {
549
- console.warn('[Taskforce MCP] Modern cloud request rejected:', {
621
+ const requestHandlingStartedAtMs = Date.now();
622
+ await transport.handleRequest(req, res, body);
623
+ requestHandlingMs = Date.now() - requestHandlingStartedAtMs;
624
+ recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
625
+ }
626
+ catch (error) {
627
+ outcome = 'failure';
628
+ console.error('[Taskforce MCP] Cloud transport request failed:', {
629
+ requestId,
630
+ kind: 'tool_transport_failure',
631
+ message: error instanceof Error ? error.message : String(error),
632
+ });
633
+ if (!res.headersSent) {
634
+ writeMcpHttpError(res, 500, 'MCP transport failure.', {
635
+ kind: 'tool_transport_failure',
636
+ retryable: true,
550
637
  requestId,
551
- message: error.message,
552
638
  });
553
- },
554
- });
555
- requestHandlingMs = Date.now() - requestHandlingStartedAtMs;
556
- recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
557
- return;
558
- }
559
- runtimeLease = acquireMcpRuntime
560
- ? await acquireMcpRuntime({ req, options: runtimeOptions })
561
- : null;
562
- runtime = runtimeLease?.runtime
563
- || createStatelessTaskforceMcpServer(runtimeOptions, createMcpServer);
564
- runtimeConstructionMs = Date.now() - runtimeStartedAtMs;
565
- const transport = runtimeLease?.transport || new NodeStreamableHTTPServerTransport({
566
- sessionIdGenerator: undefined,
567
- enableJsonResponse: true,
568
- });
569
- const transportStartedAtMs = Date.now();
570
- if (!runtimeLease)
571
- await runtime.server.connect(transport);
572
- transportConnectMs = Date.now() - transportStartedAtMs;
573
- // Set request-level phases before the SDK writes response headers. Tool calls replace this
574
- // with the richer envelope/handler breakdown from onToolCallTiming above.
575
- if (!res.headersSent) {
576
- setServerTimingHeader(res, [
577
- { name: 'auth', durationMs: authMs },
578
- { name: 'body-parse', durationMs: bodyParseMs },
579
- { name: 'rate-limit', durationMs: rateLimitMs },
580
- { name: 'runtime', durationMs: runtimeConstructionMs },
581
- { name: 'transport-connect', durationMs: transportConnectMs },
582
- ]);
583
- }
584
- if (!initializeRequest) {
585
- // Stateless cloud sessions still require a valid MCP JSON-RPC request;
586
- // let the SDK validate and respond rather than trying to track sessions server-side.
587
- }
588
- const requestHandlingStartedAtMs = Date.now();
589
- await transport.handleRequest(req, res, body);
590
- requestHandlingMs = Date.now() - requestHandlingStartedAtMs;
591
- recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
592
- }
593
- catch (error) {
594
- outcome = 'failure';
595
- console.error('[Taskforce MCP] Cloud transport request failed:', {
596
- requestId,
597
- kind: 'tool_transport_failure',
598
- message: error instanceof Error ? error.message : String(error),
599
- });
600
- if (!res.headersSent) {
601
- writeMcpHttpError(res, 500, 'MCP transport failure.', {
602
- kind: 'tool_transport_failure',
603
- retryable: true,
604
- requestId,
605
- });
606
- }
607
- }
608
- finally {
609
- if (runtimeLease) {
610
- const cleanupStartedAtMs = Date.now();
611
- try {
612
- await runtimeLease.release();
613
- }
614
- catch (closeError) {
615
- console.warn('[Taskforce MCP] Cloud test runtime lease cleanup failed:', closeError);
616
- }
617
- cleanupMs = Date.now() - cleanupStartedAtMs;
618
- }
619
- else if (runtime) {
620
- const cleanupStartedAtMs = Date.now();
621
- try {
622
- await runtime.server.close();
639
+ }
623
640
  }
624
- catch (closeError) {
625
- console.warn('[Taskforce MCP] Cloud transport cleanup failed:', closeError);
641
+ finally {
642
+ if (runtimeLease) {
643
+ const cleanupStartedAtMs = Date.now();
644
+ try {
645
+ await runtimeLease.release();
646
+ }
647
+ catch (closeError) {
648
+ console.warn('[Taskforce MCP] Cloud test runtime lease cleanup failed:', closeError);
649
+ }
650
+ cleanupMs = Date.now() - cleanupStartedAtMs;
651
+ }
652
+ else if (runtime) {
653
+ const cleanupStartedAtMs = Date.now();
654
+ try {
655
+ await runtime.server.close();
656
+ }
657
+ catch (closeError) {
658
+ console.warn('[Taskforce MCP] Cloud transport cleanup failed:', closeError);
659
+ }
660
+ cleanupMs = Date.now() - cleanupStartedAtMs;
661
+ }
662
+ const totalDurationMs = Date.now() - requestStartedAtMs;
663
+ if (initializeRequest || mcpMethod === 'tools/list' || writeToolNames.length > 0 || totalDurationMs >= 250) {
664
+ const timingSnapshot = toolTimingState.value;
665
+ console.info(`[Taskforce MCP] ${JSON.stringify({
666
+ event: 'cloud_request_timing',
667
+ requestId,
668
+ requestKind: initializeRequest
669
+ ? 'initialize'
670
+ : mcpMethod === 'tools/list'
671
+ ? 'tools_list'
672
+ : (extractFirstMcpToolName(body) ? 'tool_call' : 'other'),
673
+ method: mcpMethod || null,
674
+ toolName: extractFirstMcpToolName(body),
675
+ workspaceId: auth.workspaceId,
676
+ databaseProvider: core.getDatabaseProvider(),
677
+ authMode: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
678
+ batchSize: Array.isArray(body) ? body.length : 1,
679
+ isWrite: writeToolNames.length > 0,
680
+ outcome,
681
+ httpStatus: res.statusCode,
682
+ responseBytes: timingSnapshot?.responseBytes ?? null,
683
+ totalDurationMs,
684
+ preRuntimeMs: Math.max(0, totalDurationMs - runtimeConstructionMs - transportConnectMs - requestHandlingMs - cleanupMs),
685
+ authMs,
686
+ bodyParseMs,
687
+ rateLimitMs,
688
+ runtimeConstructionMs,
689
+ transportConnectMs,
690
+ requestHandlingMs,
691
+ cleanupMs,
692
+ toolTiming: timingSnapshot,
693
+ })}`);
694
+ }
626
695
  }
627
- cleanupMs = Date.now() - cleanupStartedAtMs;
628
696
  }
629
- const totalDurationMs = Date.now() - requestStartedAtMs;
630
- if (initializeRequest || mcpMethod === 'tools/list' || writeToolNames.length > 0 || totalDurationMs >= 250) {
631
- const timingSnapshot = toolTimingState.value;
632
- console.info(`[Taskforce MCP] ${JSON.stringify({
633
- event: 'cloud_request_timing',
634
- requestId,
635
- requestKind: initializeRequest
636
- ? 'initialize'
637
- : mcpMethod === 'tools/list'
638
- ? 'tools_list'
639
- : (extractFirstMcpToolName(body) ? 'tool_call' : 'other'),
640
- method: mcpMethod || null,
641
- toolName: extractFirstMcpToolName(body),
642
- workspaceId: auth.workspaceId,
643
- databaseProvider: core.getDatabaseProvider(),
644
- authMode: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
645
- batchSize: Array.isArray(body) ? body.length : 1,
646
- isWrite: writeToolNames.length > 0,
647
- outcome,
648
- httpStatus: res.statusCode,
649
- responseBytes: timingSnapshot?.responseBytes ?? null,
650
- totalDurationMs,
651
- preRuntimeMs: Math.max(0, totalDurationMs - runtimeConstructionMs - transportConnectMs - requestHandlingMs - cleanupMs),
652
- authMs,
653
- bodyParseMs,
654
- rateLimitMs,
655
- runtimeConstructionMs,
656
- transportConnectMs,
657
- requestHandlingMs,
658
- cleanupMs,
659
- toolTiming: timingSnapshot,
660
- })}`);
697
+ finally {
698
+ settleCapacity(Boolean(req.destroyed && !res.writableEnded));
699
+ res.off('finish', onFinish);
700
+ res.off('close', onClose);
661
701
  }
662
- }
702
+ });
663
703
  };
664
704
  }
665
705
  // === SECTION: Request header helpers ===
@@ -23,6 +23,8 @@ interface RegisterWorkspacePushSyncRoutesArgs {
23
23
  coexistenceDirection?: 'cloud-push';
24
24
  actorUserId?: string;
25
25
  assertedActiveV3TaskMetadataIds?: ReadonlySet<string>;
26
+ assertedActiveV3TaskDeleteIds?: ReadonlySet<string>;
27
+ candidateTerminalV3TaskDeleteIds?: ReadonlySet<string>;
26
28
  assertedActiveV3TaskLifecycleIds?: ReadonlySet<string>;
27
29
  assertedActiveV3TaskReopenAssigneeIds?: ReadonlySet<string>;
28
30
  assertedActiveV3InitiativeIds?: ReadonlySet<string>;
@@ -31,6 +33,7 @@ interface RegisterWorkspacePushSyncRoutesArgs {
31
33
  result: any;
32
34
  deleteCount: number;
33
35
  emittedEventIds: string[];
36
+ settledTaskDeleteIds?: string[];
34
37
  }>;
35
38
  }
36
39
  export interface WorkspaceSyncPushAppliedTaskUpsert {
@@ -211,6 +211,9 @@ export function registerWorkspacePushSyncRoutes({ addRoute, core, context, audit
211
211
  code: 'SYNC_V2_PROJECTION_BOUNDARY_MISMATCH', statusCode: 409,
212
212
  });
213
213
  }
214
+ const candidateTerminalV3TaskDeleteIds = v2ExcludedProjections.includes('task/root-v1')
215
+ ? incomingTaskUpsertIdSet
216
+ : new Set();
214
217
  if (typeof process !== 'undefined' && process.env?.TASKFORCE_SYNC_KEY) {
215
218
  const syncKey = process.env.TASKFORCE_SYNC_KEY;
216
219
  const decryptedChanges = [];
@@ -267,11 +270,12 @@ export function registerWorkspacePushSyncRoutes({ addRoute, core, context, audit
267
270
  code: 'WORKSPACE_SYNC_V3_INITIATIVE_LEGACY_PUSH_DISABLED', statusCode: 409,
268
271
  });
269
272
  }
270
- const { result, deleteCount, emittedEventIds } = await dispatchWorkspaceSyncChanges(targetWorkspaceId, incomingChanges, {
273
+ const { result, deleteCount, emittedEventIds, settledTaskDeleteIds = [], } = await dispatchWorkspaceSyncChanges(targetWorkspaceId, incomingChanges, {
271
274
  allowReferenceNumberReassignment,
272
275
  coexistenceDirection: 'cloud-push',
273
276
  actorUserId: String(access.userId || '').trim(),
274
277
  assertedActiveV3TaskMetadataIds,
278
+ candidateTerminalV3TaskDeleteIds,
275
279
  assertedActiveV3TaskLifecycleIds,
276
280
  assertedActiveV3TaskReopenAssigneeIds,
277
281
  assertedActiveV3InitiativeIds,
@@ -281,8 +285,9 @@ export function registerWorkspacePushSyncRoutes({ addRoute, core, context, audit
281
285
  let appliedTaskUpserts;
282
286
  try {
283
287
  const durabilityStore = new WorkspaceSyncDurabilityStore(core.getDatabaseAdapter());
288
+ const settledTaskDeleteIdSet = new Set(settledTaskDeleteIds);
284
289
  appliedTaskUpserts = readWorkspaceSyncPushAppliedTaskUpserts({
285
- taskIds: incomingTaskUpsertIds,
290
+ taskIds: incomingTaskUpsertIds.filter((taskId) => !settledTaskDeleteIdSet.has(taskId)),
286
291
  getTask: (taskId) => core.getTask(taskId, targetWorkspaceId),
287
292
  listTaskRevisionProjections: (taskIds) => (durabilityStore.listTaskRevisionProjections({
288
293
  tenantId: core.getTenantId(),
@@ -306,6 +311,7 @@ export function registerWorkspacePushSyncRoutes({ addRoute, core, context, audit
306
311
  idempotencyKey: idempotencyKey || null,
307
312
  emittedEventIds,
308
313
  appliedTaskUpserts,
314
+ ...(settledTaskDeleteIds.length > 0 ? { settledTaskDeleteIds } : {}),
309
315
  result,
310
316
  suppressedV2ProjectionCounts,
311
317
  durationMs
@@ -1,4 +1,4 @@
1
- import { createStandaloneServer } from '../server/index.js';
1
+ import type { StandaloneServerOptions, TaskforceStandaloneServer } from '../server/index.js';
2
2
  import { inspectLocalServiceDescriptor } from '../server/localServiceLease.js';
3
3
  import { acquireLocalServiceOwnershipForRuntime } from '../server/localServiceOwnershipBootstrap.js';
4
4
  import { type SqliteWalHolder } from '../storage/sqliteWalHealth.js';
@@ -18,7 +18,7 @@ export interface LocalServiceCliOptions {
18
18
  executablePath: string;
19
19
  cliEntryPath: string;
20
20
  execArgv?: readonly string[];
21
- createServer?: typeof createStandaloneServer;
21
+ createServer?: (options: StandaloneServerOptions) => TaskforceStandaloneServer;
22
22
  acquireOwnership?: typeof acquireLocalServiceOwnershipForRuntime;
23
23
  inspectDescriptor?: typeof inspectLocalServiceDescriptor;
24
24
  spawnDetached?: (executablePath: string, args: readonly string[], env: NodeJS.ProcessEnv) => void;