@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.
- package/README.md +7 -2
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +2 -2
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.js +2 -2
- package/dist/components/features/planning/PlanningModule.js +15 -3
- package/dist/components/features/planning/planningMetadata.d.ts +10 -0
- package/dist/components/features/planning/planningMetadata.js +8 -0
- package/dist/components/views/StandaloneLayout.js +35 -1
- package/dist/components/views/panels/PlanningDrawer.d.ts +2 -1
- package/dist/components/views/panels/PlanningDrawer.js +12 -8
- package/dist/config/envSchema.js +7 -0
- package/dist/core/McpTokenService.d.ts +2 -0
- package/dist/core/McpTokenService.js +13 -5
- package/dist/core/Taskforce.d.ts +11 -3
- package/dist/core/Taskforce.js +33 -3
- package/dist/core/types.d.ts +1 -0
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +2 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +28 -9
- package/dist/mcp/httpProtocolRouting.d.ts +1 -0
- package/dist/mcp/httpProtocolRouting.js +8 -5
- package/dist/mcp/runtime.js +24 -5
- package/dist/mcp/taskPlanningRegistrar.js +2 -2
- package/dist/mcp/toolProfiles.js +1 -0
- package/dist/mcp/toolRegistry.js +8 -8
- package/dist/server/buildInfo.d.ts +1 -0
- package/dist/server/buildInfo.js +5 -1
- package/dist/server/cloudMcpCapacityTelemetry.d.ts +68 -0
- package/dist/server/cloudMcpCapacityTelemetry.js +221 -0
- package/dist/server/localWorkspaceSyncServerRuntime.js +13 -5
- package/dist/server/routes/admin.js +14 -2
- package/dist/server/routes/localService.js +1 -0
- package/dist/server/routes/shared.d.ts +3 -0
- package/dist/server/routes/shared.js +280 -240
- package/dist/server/routes/syncPushRoutes.d.ts +3 -0
- package/dist/server/routes/syncPushRoutes.js +8 -2
- package/dist/service/localServiceCli.d.ts +2 -2
- package/dist/service/localServiceCli.js +4 -3
- package/dist/services/executorPhaseAIssuerCore.d.ts +13 -0
- package/dist/services/executorPhaseAIssuerCore.js +107 -0
- package/dist/services/taskforceAgentMcpBridge.d.ts +1 -1
- package/dist/services/taskforceAgentMcpBridge.js +35 -5
- package/dist/shared/executorPhaseASchemaValidation.js +1 -1
- package/dist/storage/postgresAdapter.js +3 -1
- package/dist/storage/postgresRequestDiagnostics.d.ts +9 -1
- package/dist/storage/postgresRequestDiagnostics.js +15 -2
- package/dist/storage/postgresWorker.js +13 -3
- package/dist/storage/postgresWorkerProtocol.d.ts +6 -0
- package/dist/storage/workflowStore.d.ts +2 -0
- package/dist/storage/workflowStore.js +39 -2
- package/dist/sync/cloudSyncApi.js +4 -2
- package/dist/sync/collaborationSyncState.js +5 -0
- package/dist/sync/coordinator/localWorkspaceSyncRunner.d.ts +3 -1
- package/dist/sync/coordinator/localWorkspaceSyncRunner.js +18 -0
- package/dist/sync/coordinator/localWorkspaceSyncRunnerState.d.ts +1 -0
- package/dist/sync/coordinator/localWorkspaceSyncRunnerState.js +7 -6
- package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +2 -2
- package/dist/sync/engine/workspaceSyncEngineLocalApply.d.ts +1 -0
- package/dist/sync/engine/workspaceSyncEnginePushCompletion.d.ts +6 -0
- package/dist/sync/engine/workspaceSyncEnginePushCompletion.js +34 -1
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +2 -2
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +4 -1
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.d.ts +1 -0
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +55 -10
- package/dist/sync/syncApplyHandlers.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.js +72 -15
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +5 -2
- package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.d.ts +7 -7
- package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.js +50 -13
- package/dist/sync/v3/localOutboxRecoveryPreparationService.js +8 -2
- package/dist/sync/v3/localTaskCreateOutboxService.d.ts +3 -1
- package/dist/sync/v3/localTaskCreateOutboxService.js +6 -1
- package/dist/sync/v3/localTaskDeleteOutboxService.js +1 -0
- package/dist/sync/v3/localTaskLifecycleOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskLifecycleOutboxService.js +5 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.js +6 -2
- package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskStatusOutboxService.js +20 -5
- package/dist/sync/v3/syncDurabilityStore.d.ts +2 -1
- package/dist/sync/v3/syncDurabilityStore.js +34 -6
- package/dist/sync/v3/workflowRuntimeMutationService.d.ts +5 -0
- package/dist/sync/v3/workflowRuntimeMutationService.js +124 -71
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +25 -3
- package/dist/sync/v3/workspaceSyncV3Feed.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3Feed.js +6 -1
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +49 -18
- package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.js +50 -5
- package/dist/ui/assets/{AiIdentityRosterCard-OcSz1PkK.js → AiIdentityRosterCard-Cfvb9Nuv.js} +1 -1
- package/dist/ui/assets/{AiProfilesModule-ByhYW5Xi.js → AiProfilesModule-0oR70eFG.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTM6nIg_.js → AnnotatedAttachmentWorkspace-C33iH_r_.js} +1 -1
- package/dist/ui/assets/{AssetTraySortControl-DabEG8L4.js → AssetTraySortControl-BAqxUtke.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-Coa8yVGM.js → ContextAttachmentManager-BudIbOR9.js} +2 -2
- package/dist/ui/assets/{DocumentWorkspace-BB0qhdix.js → DocumentWorkspace-A8dFQyId.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-CZ8x4UJE.js → EntityActivityTimeline-g7vtdsx1.js} +1 -1
- package/dist/ui/assets/PlanningModule-D-DeQs4F.js +1 -0
- package/dist/ui/assets/{PlansPage-D86pZSSl.js → PlansPage-BWmfGi0Z.js} +1 -1
- package/dist/ui/assets/{TaskContextUpload-BVIkU9yT.js → TaskContextUpload-BA_lQmS9.js} +1 -1
- package/dist/ui/assets/{TaskSettings-Cv1mNhv_.js → TaskSettings-DAlH4YIP.js} +1 -1
- package/dist/ui/assets/{TaskforceAgentsModule-gmH-PD2q.js → TaskforceAgentsModule-DmeCf6pJ.js} +2 -2
- package/dist/ui/assets/{WorkflowManagerModule-DbfnHJ-f.js → WorkflowManagerModule-p08eDOaa.js} +1 -1
- package/dist/ui/assets/index-GHJgcW3j.js +7 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -1
- package/dist/ui/assets/PlanningModule-pRPjR7v5.js +0 -1
- 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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
625
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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 {
|
|
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?:
|
|
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;
|