@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
@@ -0,0 +1,120 @@
1
+ import { getRequestPerformanceDiagnostics, runWithRequestPerformanceDiagnostics, } from '../storage/postgresRequestDiagnostics.js';
2
+ import { resolveAppAccessDeniedResponse } from './appAccess.js';
3
+ import { resolveAuthorizedWorkspaceAccess, resolveDisabledAccessError, } from './authorizedWorkspaceAccess.js';
4
+ const ROUTE_BY_OPERATION = {
5
+ tasks: '/api/taskforce/tasks',
6
+ archive: '/api/taskforce/archive',
7
+ deleted: '/api/taskforce/deleted',
8
+ 'planning-bootstrap': '/api/taskforce/planning/bootstrap',
9
+ };
10
+ export function resolveCloudCollectionReadOperation(method, pathname) {
11
+ if (String(method || 'GET').toUpperCase() !== 'GET')
12
+ return null;
13
+ if (pathname === '/api/taskforce/tasks')
14
+ return 'tasks';
15
+ if (pathname === '/api/taskforce/archive')
16
+ return 'archive';
17
+ if (pathname === '/api/taskforce/deleted' || pathname === '/api/taskforce/trash')
18
+ return 'deleted';
19
+ if (pathname === '/api/taskforce/planning/bootstrap')
20
+ return 'planning-bootstrap';
21
+ return null;
22
+ }
23
+ function serverTimingMetric(name, durationMs) {
24
+ return `${name};dur=${Math.max(0, Math.round(durationMs))}`;
25
+ }
26
+ function performanceHeaders(diagnostics, responseBytes, metrics) {
27
+ const headers = {
28
+ 'Content-Type': 'application/json',
29
+ 'Server-Timing': metrics.map((metric) => serverTimingMetric(metric.name, metric.durationMs)).join(', '),
30
+ };
31
+ if (diagnostics) {
32
+ headers['X-Taskforce-Postgres-Query-Count'] = String(diagnostics.postgresQueryCount);
33
+ headers['X-Taskforce-Postgres-Worker-Call-Count'] = String(diagnostics.postgresWorkerCallCount);
34
+ headers['X-Taskforce-Response-Bytes'] = String(responseBytes);
35
+ }
36
+ return headers;
37
+ }
38
+ export function executeCloudCollectionRead(core, request) {
39
+ return runWithRequestPerformanceDiagnostics(request.performanceTrace, () => {
40
+ const access = resolveAuthorizedWorkspaceAccess(core, request.access, request.requestedWorkspaceId);
41
+ const workspaceId = String(access.workspaceId || '').trim();
42
+ const disabledAccessError = resolveDisabledAccessError(access, request.pathname);
43
+ if (disabledAccessError) {
44
+ return {
45
+ statusCode: 403,
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify({ error: disabledAccessError }),
48
+ workspaceId,
49
+ };
50
+ }
51
+ if (access.authenticated && access.userId && access.userId !== 'anonymous') {
52
+ const denied = resolveAppAccessDeniedResponse(core.getAccountAccessStatus(access.userId));
53
+ if (denied) {
54
+ return {
55
+ statusCode: denied.statusCode,
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify(denied.body),
58
+ workspaceId,
59
+ };
60
+ }
61
+ }
62
+ const startedAt = performance.now();
63
+ let data;
64
+ const planningDiagnostics = {};
65
+ if (request.operation === 'tasks')
66
+ data = core.listTasks(workspaceId);
67
+ else if (request.operation === 'archive')
68
+ data = core.listArchive(workspaceId);
69
+ else if (request.operation === 'deleted')
70
+ data = core.listDeletedTasksReadOnly(workspaceId);
71
+ else
72
+ data = core.listPlanningBootstrap(workspaceId, planningDiagnostics);
73
+ const loadCompletedAt = performance.now();
74
+ const serializeStartedAt = performance.now();
75
+ const body = JSON.stringify(data);
76
+ const serializeDurationMs = performance.now() - serializeStartedAt;
77
+ const totalDurationMs = performance.now() - startedAt;
78
+ const responseBytes = Buffer.byteLength(body, 'utf8');
79
+ const diagnostics = getRequestPerformanceDiagnostics();
80
+ const metrics = request.operation === 'planning-bootstrap'
81
+ ? [
82
+ { name: 'planning-bootstrap', durationMs: totalDurationMs },
83
+ { name: 'planning-bootstrap-initiatives', durationMs: planningDiagnostics.initiativeLoadMs || 0 },
84
+ { name: 'planning-bootstrap-workstreams', durationMs: planningDiagnostics.workstreamLoadMs || 0 },
85
+ { name: 'planning-bootstrap-tasks', durationMs: planningDiagnostics.taskSummaryQueryMs || 0 },
86
+ { name: 'planning-bootstrap-build', durationMs: planningDiagnostics.taskSummaryBuildMs || 0 },
87
+ { name: 'planning-bootstrap-json', durationMs: serializeDurationMs },
88
+ ]
89
+ : [
90
+ { name: 'route-total', durationMs: totalDurationMs },
91
+ { name: 'route-load', durationMs: loadCompletedAt - startedAt },
92
+ { name: 'route-json', durationMs: serializeDurationMs },
93
+ ];
94
+ if (diagnostics) {
95
+ metrics.push({ name: 'postgres-worker-wait', durationMs: diagnostics.postgresWorkerWaitMs });
96
+ }
97
+ return {
98
+ statusCode: 200,
99
+ headers: performanceHeaders(diagnostics, responseBytes, metrics),
100
+ body,
101
+ workspaceId,
102
+ performance: {
103
+ durationMs: totalDurationMs,
104
+ details: {
105
+ responseBytes,
106
+ jsonSerializeMs: serializeDurationMs,
107
+ ...(diagnostics ? {
108
+ postgresQueryCount: diagnostics.postgresQueryCount,
109
+ postgresWorkerCallCount: diagnostics.postgresWorkerCallCount,
110
+ postgresWorkerWaitMs: Math.round(diagnostics.postgresWorkerWaitMs),
111
+ } : {}),
112
+ ...(request.operation === 'planning-bootstrap' ? planningDiagnostics : {}),
113
+ },
114
+ },
115
+ };
116
+ });
117
+ }
118
+ export function cloudCollectionReadRoute(operation) {
119
+ return ROUTE_BY_OPERATION[operation];
120
+ }
@@ -0,0 +1,52 @@
1
+ import type { CloudCollectionReadRequest, CloudCollectionReadResponse } from './cloudCollectionRead.js';
2
+ export interface CloudCollectionReadPoolOptions {
3
+ projectRoot: string;
4
+ tenantId: string;
5
+ connectionString: string;
6
+ size?: number;
7
+ requestTimeoutMs?: number;
8
+ workerFactory?: () => CloudCollectionReadWorker;
9
+ }
10
+ export interface CloudCollectionReadWorker {
11
+ postMessage(message: unknown): void;
12
+ terminate(): Promise<number>;
13
+ on(event: 'message', listener: (message: CloudCollectionReadWorkerResponse) => void): this;
14
+ on(event: 'error', listener: (error: unknown) => void): this;
15
+ on(event: 'exit', listener: (code: number) => void): this;
16
+ }
17
+ export type CloudCollectionReadWorkerResponse = {
18
+ type: 'ready';
19
+ } | {
20
+ type: 'result';
21
+ requestId: number;
22
+ response: CloudCollectionReadResponse;
23
+ } | {
24
+ type: 'error';
25
+ requestId: number;
26
+ error: string;
27
+ };
28
+ export declare class CloudCollectionReadPool {
29
+ private readonly slots;
30
+ private readonly queue;
31
+ private readonly requestTimeoutMs;
32
+ private readonly desiredSize;
33
+ private readonly workerData;
34
+ private readonly workerFactory;
35
+ private nextRequestId;
36
+ private closing;
37
+ constructor(options: CloudCollectionReadPoolOptions);
38
+ execute(request: CloudCollectionReadRequest): Promise<CloudCollectionReadResponse>;
39
+ close(): Promise<void>;
40
+ private spawnSlot;
41
+ private handleMessage;
42
+ private handleWorkerFailure;
43
+ private ensureSlots;
44
+ private pump;
45
+ }
46
+ export declare function resolveCloudCollectionReadPoolSize(env?: NodeJS.ProcessEnv): number;
47
+ export declare function shouldUseCloudCollectionReadPool(input: {
48
+ runtimeMode: 'local' | 'cloud';
49
+ databaseProvider: 'sqlite' | 'postgres';
50
+ connectionString?: string;
51
+ workerCount: number;
52
+ }): boolean;
@@ -0,0 +1,140 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ const DEFAULT_POOL_SIZE = 2;
3
+ const MAX_POOL_SIZE = 8;
4
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
5
+ function positiveInteger(value, fallback) {
6
+ if (!Number.isFinite(value) || Number(value) <= 0)
7
+ return fallback;
8
+ return Math.max(1, Math.floor(Number(value)));
9
+ }
10
+ export class CloudCollectionReadPool {
11
+ slots = [];
12
+ queue = [];
13
+ requestTimeoutMs;
14
+ desiredSize;
15
+ workerData;
16
+ workerFactory;
17
+ nextRequestId = 1;
18
+ closing = false;
19
+ constructor(options) {
20
+ this.requestTimeoutMs = positiveInteger(options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS);
21
+ this.workerData = {
22
+ projectRoot: options.projectRoot,
23
+ tenantId: options.tenantId,
24
+ connectionString: options.connectionString,
25
+ };
26
+ this.workerFactory = options.workerFactory || (() => new Worker(new URL('./cloudCollectionReadWorker.js', import.meta.url), { workerData: this.workerData }));
27
+ this.desiredSize = Math.min(MAX_POOL_SIZE, positiveInteger(options.size, DEFAULT_POOL_SIZE));
28
+ }
29
+ execute(request) {
30
+ if (this.closing) {
31
+ return Promise.reject(new Error('[Taskforce] Cloud collection read pool is closed.'));
32
+ }
33
+ this.ensureSlots();
34
+ return new Promise((resolve, reject) => {
35
+ this.queue.push({ request, resolve, reject });
36
+ this.pump();
37
+ });
38
+ }
39
+ async close() {
40
+ if (this.closing)
41
+ return;
42
+ this.closing = true;
43
+ const closedError = new Error('[Taskforce] Cloud collection read pool closed before the request completed.');
44
+ for (const pending of this.queue.splice(0))
45
+ pending.reject(closedError);
46
+ await Promise.all(this.slots.splice(0).map(async (slot) => {
47
+ if (slot.active) {
48
+ clearTimeout(slot.active.timeout);
49
+ slot.active.reject(closedError);
50
+ slot.active = null;
51
+ }
52
+ slot.worker.postMessage({ type: 'close' });
53
+ await slot.worker.terminate();
54
+ }));
55
+ }
56
+ spawnSlot() {
57
+ if (this.closing)
58
+ return;
59
+ const worker = this.workerFactory();
60
+ const slot = { worker, ready: false, active: null };
61
+ this.slots.push(slot);
62
+ worker.on('message', (message) => this.handleMessage(slot, message));
63
+ worker.on('error', (error) => this.handleWorkerFailure(slot, error instanceof Error ? error : new Error(String(error))));
64
+ worker.on('exit', (code) => {
65
+ if (!this.closing && this.slots.includes(slot)) {
66
+ this.handleWorkerFailure(slot, new Error(`[Taskforce] Cloud collection read worker exited with code ${code}.`));
67
+ }
68
+ });
69
+ }
70
+ handleMessage(slot, message) {
71
+ if (message.type === 'ready') {
72
+ slot.ready = true;
73
+ this.ensureSlots();
74
+ this.pump();
75
+ return;
76
+ }
77
+ const active = slot.active;
78
+ if (!active || active.requestId !== message.requestId)
79
+ return;
80
+ clearTimeout(active.timeout);
81
+ slot.active = null;
82
+ if (message.type === 'result')
83
+ active.resolve(message.response);
84
+ else
85
+ active.reject(new Error(message.error || '[Taskforce] Cloud collection read worker failed.'));
86
+ this.pump();
87
+ }
88
+ handleWorkerFailure(slot, error) {
89
+ const slotIndex = this.slots.indexOf(slot);
90
+ if (slotIndex < 0)
91
+ return;
92
+ this.slots.splice(slotIndex, 1);
93
+ if (slot.active) {
94
+ clearTimeout(slot.active.timeout);
95
+ slot.active.reject(error);
96
+ slot.active = null;
97
+ }
98
+ void slot.worker.terminate();
99
+ if (!this.closing && this.slots.length === 0) {
100
+ for (const pending of this.queue.splice(0))
101
+ pending.reject(error);
102
+ }
103
+ this.pump();
104
+ }
105
+ ensureSlots() {
106
+ if (this.closing)
107
+ return;
108
+ if (this.slots.some((slot) => !slot.ready))
109
+ return;
110
+ if (this.slots.length < this.desiredSize)
111
+ this.spawnSlot();
112
+ }
113
+ pump() {
114
+ if (this.closing || this.queue.length === 0)
115
+ return;
116
+ for (const slot of this.slots) {
117
+ if (!slot.ready || slot.active || this.queue.length === 0)
118
+ continue;
119
+ const pending = this.queue.shift();
120
+ const requestId = this.nextRequestId++;
121
+ const timeout = setTimeout(() => {
122
+ this.handleWorkerFailure(slot, new Error(`[Taskforce] Cloud collection read worker timed out after ${this.requestTimeoutMs}ms.`));
123
+ }, this.requestTimeoutMs);
124
+ timeout.unref?.();
125
+ slot.active = { ...pending, requestId, timeout };
126
+ slot.worker.postMessage({ type: 'execute', requestId, request: pending.request });
127
+ }
128
+ }
129
+ }
130
+ export function resolveCloudCollectionReadPoolSize(env = process.env) {
131
+ if (String(env.TASKFORCE_CLOUD_READ_WORKERS || '').trim() === '0')
132
+ return 0;
133
+ return Math.min(MAX_POOL_SIZE, positiveInteger(Number(env.TASKFORCE_CLOUD_READ_WORKERS), DEFAULT_POOL_SIZE));
134
+ }
135
+ export function shouldUseCloudCollectionReadPool(input) {
136
+ return input.runtimeMode === 'cloud'
137
+ && input.databaseProvider === 'postgres'
138
+ && Boolean(String(input.connectionString || '').trim())
139
+ && input.workerCount > 0;
140
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { parentPort, workerData } from 'node:worker_threads';
2
+ import { TaskforceCore } from '../core/Taskforce.js';
3
+ import { executeCloudCollectionRead, } from './cloudCollectionRead.js';
4
+ if (!parentPort) {
5
+ throw new Error('[Taskforce] Cloud collection read worker requires a parent port.');
6
+ }
7
+ const config = workerData;
8
+ const core = new TaskforceCore({
9
+ projectRoot: config.projectRoot,
10
+ tenantId: config.tenantId,
11
+ runtimeMode: 'cloud',
12
+ initializationMode: 'cloud-read-worker',
13
+ database: {
14
+ provider: 'postgres',
15
+ connectionString: config.connectionString,
16
+ },
17
+ });
18
+ parentPort.on('message', (message) => {
19
+ if (message.type === 'close') {
20
+ core.close();
21
+ parentPort?.close();
22
+ return;
23
+ }
24
+ try {
25
+ const response = executeCloudCollectionRead(core, message.request);
26
+ parentPort?.postMessage({ type: 'result', requestId: message.requestId, response });
27
+ }
28
+ catch (error) {
29
+ parentPort?.postMessage({
30
+ type: 'error',
31
+ requestId: message.requestId,
32
+ error: error instanceof Error ? error.message : String(error),
33
+ });
34
+ }
35
+ });
36
+ parentPort.postMessage({ type: 'ready' });
@@ -6,8 +6,8 @@
6
6
  */
7
7
  import * as http from 'http';
8
8
  import { WebSocketServer } from 'ws';
9
- import { TaskforceCore } from '../core/Taskforce.js';
10
- import type { RequestAccessContext } from './auth.js';
9
+ export { resolveAuthorizedWorkspaceAccess, resolveDisabledAccessError, isWorkspaceAccessRecoveryRoute, } from './authorizedWorkspaceAccess.js';
10
+ import type { LocalServiceCloudTarget } from '../config/localServiceCloudTarget.js';
11
11
  import type { LocalSyncCoordinatorHost } from '../sync/coordinator/localSyncCoordinatorHost.js';
12
12
  import type { LocalSyncProcessLock } from '../sync/coordinator/localSyncProcessLock.js';
13
13
  import type { LocalDataOwnerPermit } from './localDataOwnerPermit.js';
@@ -25,6 +25,7 @@ export interface StandaloneServerOptions {
25
25
  ownerPid: number;
26
26
  bootstrapCapability: string;
27
27
  ownershipKey: string;
28
+ cloudTarget: LocalServiceCloudTarget;
28
29
  };
29
30
  }
30
31
  export interface TaskforceStandaloneServer extends http.Server {
@@ -45,17 +46,9 @@ export declare const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = "x-taskforce-origin-p
45
46
  export declare const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = "x-taskforce-origin-base-url";
46
47
  export declare const TASKFORCE_PROXY_ORIGIN_RUNTIME_HEADER = "x-taskforce-origin-runtime";
47
48
  export declare const TASKFORCE_PROXY_CLOUD_ENVIRONMENT_HEADER = "x-taskforce-cloud-environment";
48
- interface AuthorizedRequestAccess extends RequestAccessContext {
49
- disabled?: boolean;
50
- accountDisabled?: boolean;
51
- workspaceDisabled?: boolean;
52
- }
53
- export declare function isWorkspaceAccessRecoveryRoute(pathname: string): boolean;
54
- export declare function resolveDisabledAccessError(access: Pick<AuthorizedRequestAccess, 'disabled' | 'accountDisabled' | 'workspaceDisabled'>, pathname: string): string | null;
55
49
  export declare function shouldProxyCloudPath(pathname: string): boolean;
56
50
  export declare function shouldProxyCloudRequest(url: Pick<URL, 'pathname' | 'searchParams'>, env?: NodeJS.ProcessEnv): boolean;
57
51
  export declare function resolveCloudProxyUpstreamBaseUrl(req: http.IncomingMessage, fallbackBaseUrl: string): string;
58
- export declare function resolveAuthorizedWorkspaceAccess(core: Pick<TaskforceCore, 'resolveUserAccess' | 'getSystemRole'>, access: AuthorizedRequestAccess, requestedWorkspaceId: string): AuthorizedRequestAccess;
59
52
  export declare function shouldApplyReadOnlyMutationGuard(methodRaw: string | undefined, pathname: string): boolean;
60
53
  export declare function resolveEffectiveRequestUserId(accessUserId: string, explicitUserId: string): string;
61
54
  export declare function resolveMaxPayloadBytesForPath(pathname: string, defaultMaxBytes: number): number;
@@ -72,4 +65,3 @@ export declare function ensurePublicPricingAuthExclusion(excludedPaths: string[]
72
65
  export declare function ensurePublicSiteConfigAuthExclusion(excludedPaths: string[]): string[];
73
66
  export declare function isBillingWebhookPath(pathname: string): boolean;
74
67
  export declare function createStandaloneServer(options: StandaloneServerOptions): TaskforceStandaloneServer;
75
- export {};
@@ -14,9 +14,12 @@ import chokidar from 'chokidar';
14
14
  import { TaskforceCore } from '../core/Taskforce.js';
15
15
  import { createRoutes, matchRoute } from './routes.js';
16
16
  import { formatApiErrorResponse } from './routes/primitives.js';
17
+ import { logSlowBootstrapRoute } from './routes/shared.js';
17
18
  import { applyCorsHeaders, getAllowedOriginsFromEnv } from './cors.js';
18
19
  import { applyPrefixedRateLimitHeaders, applyRateLimitHeaders, InMemoryRateLimiter, resolveRateLimitKey } from './rateLimit.js';
19
20
  import { isRequestAuthorized, resolveAuthConfigFromEnv, resolveRequestAccess } from './auth.js';
21
+ import { resolveAuthorizedWorkspaceAccess, resolveDisabledAccessError, } from './authorizedWorkspaceAccess.js';
22
+ export { resolveAuthorizedWorkspaceAccess, resolveDisabledAccessError, isWorkspaceAccessRecoveryRoute, } from './authorizedWorkspaceAccess.js';
20
23
  import { assertCloudDatabaseProvider, resolveRuntimeModeFromEnv } from './runtimeMode.js';
21
24
  import { applySecurityHeaders } from './securityHeaders.js';
22
25
  import { resolveDatabaseConfigFromEnv } from '../storage/providerConfig.js';
@@ -37,6 +40,8 @@ import { createWorkspaceSyncServerComposition, shutdownWorkspaceSyncServerCompos
37
40
  import { LocalServiceSessionManager } from './localServiceSessions.js';
38
41
  import { LocalSyncIdentityStore } from '../sync/v3/localSyncIdentityStore.js';
39
42
  import { shouldBroadcastLocalFileInvalidation } from './localRealtimeFileInvalidation.js';
43
+ import { resolveCloudCollectionReadOperation, } from './cloudCollectionRead.js';
44
+ import { CloudCollectionReadPool, resolveCloudCollectionReadPoolSize, shouldUseCloudCollectionReadPool, } from './cloudCollectionReadPool.js';
40
45
  const __filename = fileURLToPath(import.meta.url);
41
46
  const __dirname = path.dirname(__filename);
42
47
  const MIME_TYPES = {
@@ -173,21 +178,6 @@ export const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = 'x-taskforce-origin-proto';
173
178
  export const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = 'x-taskforce-origin-base-url';
174
179
  export const TASKFORCE_PROXY_ORIGIN_RUNTIME_HEADER = 'x-taskforce-origin-runtime';
175
180
  export const TASKFORCE_PROXY_CLOUD_ENVIRONMENT_HEADER = 'x-taskforce-cloud-environment';
176
- export function isWorkspaceAccessRecoveryRoute(pathname) {
177
- return pathname === '/api/taskforce/workspaces'
178
- || pathname === '/api/taskforce/session/workspace';
179
- }
180
- export function resolveDisabledAccessError(access, pathname) {
181
- if (pathname.startsWith('/api/taskforce/auth/'))
182
- return null;
183
- if (access.accountDisabled ?? access.disabled) {
184
- return 'Forbidden: user account is disabled.';
185
- }
186
- if (access.workspaceDisabled && !isWorkspaceAccessRecoveryRoute(pathname)) {
187
- return 'Forbidden: workspace access is not active for this user.';
188
- }
189
- return null;
190
- }
191
181
  function shouldLogRefreshDiagnostic(pathname) {
192
182
  return REFRESH_DIAGNOSTIC_PATH_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(prefix));
193
183
  }
@@ -301,31 +291,6 @@ export function resolveCloudProxyUpstreamBaseUrl(req, fallbackBaseUrl) {
301
291
  return fallbackBaseUrl;
302
292
  return TASKFORCE_CLOUD_ENVIRONMENTS[requestedEnvironment].appBaseUrl;
303
293
  }
304
- export function resolveAuthorizedWorkspaceAccess(core, access, requestedWorkspaceId) {
305
- if (!access.authenticated || !access.userId || access.userId === 'anonymous')
306
- return access;
307
- const trimmedRequestedWorkspaceId = String(requestedWorkspaceId || '').trim();
308
- const candidateWorkspaceIds = [
309
- trimmedRequestedWorkspaceId,
310
- String(access.workspaceId || '').trim()
311
- ].filter((workspaceId, index, values) => Boolean(workspaceId) && values.indexOf(workspaceId) === index);
312
- for (const workspaceId of candidateWorkspaceIds) {
313
- const resolved = core.resolveUserAccess(access.userId, workspaceId, access.role);
314
- if (!resolved)
315
- continue;
316
- return {
317
- ...access,
318
- role: resolved.role,
319
- workspaceId: resolved.workspaceId,
320
- userId: resolved.userId,
321
- systemRole: core.getSystemRole(resolved.userId),
322
- disabled: resolved.disabled,
323
- accountDisabled: resolved.accountDisabled ?? resolved.disabled,
324
- workspaceDisabled: resolved.workspaceDisabled ?? false
325
- };
326
- }
327
- return access;
328
- }
329
294
  export function shouldApplyReadOnlyMutationGuard(methodRaw, pathname) {
330
295
  const method = String(methodRaw || 'GET').toUpperCase();
331
296
  if (method !== 'POST' && method !== 'PATCH' && method !== 'DELETE')
@@ -673,6 +638,20 @@ export function createStandaloneServer(options) {
673
638
  },
674
639
  } : undefined,
675
640
  });
641
+ const cloudCollectionReadWorkerCount = resolveCloudCollectionReadPoolSize(process.env);
642
+ const cloudCollectionReadPool = shouldUseCloudCollectionReadPool({
643
+ runtimeMode,
644
+ databaseProvider: db.provider,
645
+ connectionString: db.connectionString,
646
+ workerCount: cloudCollectionReadWorkerCount,
647
+ })
648
+ ? new CloudCollectionReadPool({
649
+ projectRoot: options.projectRoot,
650
+ tenantId: core.getTenantId(),
651
+ connectionString: db.connectionString,
652
+ size: cloudCollectionReadWorkerCount,
653
+ })
654
+ : null;
676
655
  const allowedOrigins = getAllowedOriginsFromEnv(process.env.TASKFORCE_ALLOWED_ORIGINS, runtimeMode);
677
656
  const rateLimitEnabled = typeof process.env.TASKFORCE_API_RATE_LIMIT_ENABLED === 'string'
678
657
  ? process.env.TASKFORCE_API_RATE_LIMIT_ENABLED !== 'false'
@@ -1066,6 +1045,39 @@ export function createStandaloneServer(options) {
1066
1045
  res.end(JSON.stringify({ error: auth.reason || 'Unauthorized' }));
1067
1046
  return;
1068
1047
  }
1048
+ const cloudCollectionReadOperation = cloudCollectionReadPool
1049
+ ? resolveCloudCollectionReadOperation(req.method, url.pathname)
1050
+ : null;
1051
+ if (cloudCollectionReadPool && cloudCollectionReadOperation) {
1052
+ try {
1053
+ const requestedWorkspaceId = Array.isArray(req.headers['x-taskforce-workspace-id'])
1054
+ ? String(req.headers['x-taskforce-workspace-id'][0] || '')
1055
+ : String(req.headers['x-taskforce-workspace-id'] || '');
1056
+ const response = await cloudCollectionReadPool.execute({
1057
+ operation: cloudCollectionReadOperation,
1058
+ pathname: url.pathname,
1059
+ requestedWorkspaceId,
1060
+ access: accessForRateLimit,
1061
+ performanceTrace: String(req.headers['x-taskforce-performance-trace'] || '').trim() === '1',
1062
+ });
1063
+ if (response.performance) {
1064
+ logSlowBootstrapRoute({
1065
+ route: url.pathname,
1066
+ workspaceId: response.workspaceId,
1067
+ traceId: String(req.headers['x-taskforce-bootstrap-trace-id'] || '').trim(),
1068
+ durationMs: response.performance.durationMs,
1069
+ details: response.performance.details,
1070
+ });
1071
+ }
1072
+ response.headers['X-Taskforce-Cloud-Read-Execution'] = 'worker';
1073
+ res.writeHead(response.statusCode, response.headers);
1074
+ res.end(response.body);
1075
+ return;
1076
+ }
1077
+ catch (error) {
1078
+ console.error(`[Taskforce] Cloud collection read worker failed; using synchronous fallback path=${url.pathname}:`, error instanceof Error ? error.message : String(error));
1079
+ }
1080
+ }
1069
1081
  const pathname = url.pathname;
1070
1082
  const isMutation = shouldApplyReadOnlyMutationGuard(req.method, pathname);
1071
1083
  let access = accessForRateLimit;
@@ -1203,6 +1215,7 @@ export function createStandaloneServer(options) {
1203
1215
  if (serverResult.status === 'rejected') {
1204
1216
  throw serverResult.reason;
1205
1217
  }
1218
+ await cloudCollectionReadPool?.close();
1206
1219
  },
1207
1220
  closeCore: () => core.close(),
1208
1221
  });
@@ -1,3 +1,4 @@
1
+ import { type LocalServiceCloudTarget } from '../config/localServiceCloudTarget.js';
1
2
  import { type WindowsAclCommandRunner } from './localServiceWindowsAcl.js';
2
3
  export interface LocalServiceDescriptor {
3
4
  schemaVersion: 1;
@@ -9,6 +10,7 @@ export interface LocalServiceDescriptor {
9
10
  generation: string;
10
11
  bootstrapCapability: string;
11
12
  publishedAt: string;
13
+ cloudTarget?: LocalServiceCloudTarget | null;
12
14
  }
13
15
  export interface LocalServiceLeaseIdentityInput {
14
16
  projectRoot: string;
@@ -27,6 +29,7 @@ export interface LocalServiceDescriptorPublication {
27
29
  endpoint: string;
28
30
  protocolVersion: string;
29
31
  appVersion: string;
32
+ cloudTarget: LocalServiceCloudTarget;
30
33
  }
31
34
  export interface LocalServiceLease {
32
35
  readonly identity: LocalServiceLeaseIdentity;
@@ -3,6 +3,7 @@ import * as fs from 'fs';
3
3
  import * as net from 'net';
4
4
  import * as os from 'os';
5
5
  import * as path from 'path';
6
+ import { parseLocalServiceCloudTarget, } from '../config/localServiceCloudTarget.js';
6
7
  import { resolveLocalSyncProcessLockIdentity } from './localDatabaseIdentity.js';
7
8
  import { ensureWindowsUserOnlyDirectoryAcl, } from './localServiceWindowsAcl.js';
8
9
  const DESCRIPTOR_SCHEMA_VERSION = 1;
@@ -161,8 +162,14 @@ function parseDescriptor(value, ownershipKey) {
161
162
  || typeof value.publishedAt !== 'string') {
162
163
  throw new Error('Local service descriptor is invalid.');
163
164
  }
165
+ const cloudTarget = value.cloudTarget == null
166
+ ? null
167
+ : parseLocalServiceCloudTarget(value.cloudTarget);
168
+ if (value.cloudTarget != null && !cloudTarget) {
169
+ throw new Error('Local service descriptor cloud target is invalid.');
170
+ }
164
171
  assertLoopbackEndpoint(value.endpoint);
165
- return value;
172
+ return { ...value, cloudTarget };
166
173
  }
167
174
  function readOptionalDescriptor(identity) {
168
175
  try {
@@ -363,6 +370,7 @@ export async function acquireLocalServiceLease(input, dependencies = {}) {
363
370
  generation,
364
371
  bootstrapCapability,
365
372
  publishedAt: (dependencies.now?.() || new Date()).toISOString(),
373
+ cloudTarget: publication.cloudTarget,
366
374
  };
367
375
  publishJsonFileAtomically(identity.descriptorPath, descriptor, platform);
368
376
  return descriptor;
@@ -8,6 +8,8 @@
8
8
  * POST /api/taskforce/context-upload/init
9
9
  * POST /api/taskforce/context-upload/finalize
10
10
  * POST /api/taskforce/context-upload
11
+ * POST /api/taskforce/context-upload/draft/promote
12
+ * POST /api/taskforce/context-upload/draft/discard
11
13
  * GET /api/taskforce/context/:filename
12
14
  * GET /api/taskforce/documents/:assetId/content
13
15
  * GET /api/taskforce/context-link
@@ -39,7 +41,7 @@ type AssetMetadata = DocumentRegistryEntry & {
39
41
  originalFilename?: string | null;
40
42
  };
41
43
  interface CanonicalReadResult {
42
- statusCode: 200 | 302 | 404;
44
+ statusCode: 200 | 302 | 404 | 409;
43
45
  headers?: Record<string, string>;
44
46
  body?: Buffer;
45
47
  }
@@ -81,6 +83,7 @@ export interface DocumentRouteDependencies {
81
83
  workspaceId?: string;
82
84
  taskId?: string | null;
83
85
  originalName?: string | null;
86
+ actorRef?: string | null;
84
87
  }) => Promise<{
85
88
  scan?: {
86
89
  verdict?: string;
@@ -110,7 +113,7 @@ export interface DocumentRouteDependencies {
110
113
  sanitizeDownloadFilename: (nameRaw: string, fallback?: string) => string;
111
114
  contentTypeForExt: (ext: string) => string;
112
115
  inferInlineDocumentFromPath: (pathValue: string | null | undefined) => boolean;
113
- resolveCanonicalDocumentRead: (params: {
116
+ resolveCanonicalContentRead: (params: {
114
117
  asset: WorkspaceAssetEntry;
115
118
  forceDownload: boolean;
116
119
  requestedFilename?: string | null;