@taskforcehq/taskforce 0.3.303 → 0.3.304

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 (165) hide show
  1. package/dist/Taskforce.module.css +102 -5
  2. package/dist/TaskforceCore.js +514 -212
  3. package/dist/TaskforceCore.test.js +1172 -133
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  21. package/dist/components/task/TaskStatusActions.js +8 -3
  22. package/dist/components/task/TaskStatusActions.test.js +9 -0
  23. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  24. package/dist/components/task/taskKanbanDropRules.js +187 -0
  25. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  26. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  27. package/dist/components/ui/Modal.d.ts +1 -1
  28. package/dist/components/ui/Modal.js +1 -0
  29. package/dist/components/views/AppShellHeader.js +5 -1
  30. package/dist/components/views/AppShellHeader.test.js +4 -0
  31. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  32. package/dist/components/views/PlanComparisonPage.js +4 -2
  33. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  34. package/dist/components/views/PlansPage.d.ts +5 -1
  35. package/dist/components/views/PlansPage.js +39 -12
  36. package/dist/components/views/PlansPage.test.js +59 -2
  37. package/dist/components/views/StandaloneLayout.js +113 -38
  38. package/dist/components/views/WidgetView.js +11 -2
  39. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  40. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  41. package/dist/core/PlanEntitlementService.d.ts +3 -1
  42. package/dist/core/PlanEntitlementService.js +61 -11
  43. package/dist/core/SignupMonetizationSettings.test.js +37 -2
  44. package/dist/core/TaskActivityService.d.ts +4 -1
  45. package/dist/core/TaskActivityService.js +45 -0
  46. package/dist/core/TaskAuditTimeline.test.js +52 -1
  47. package/dist/core/Taskforce.d.ts +15 -11
  48. package/dist/core/Taskforce.js +43 -2
  49. package/dist/core/WorkspacePlanMode.test.js +65 -0
  50. package/dist/documentReviews/service.d.ts +1 -1
  51. package/dist/documentReviews/types.d.ts +1 -1
  52. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  53. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  54. package/dist/hooks/sync/auth.d.ts +37 -0
  55. package/dist/hooks/sync/auth.js +61 -0
  56. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  57. package/dist/hooks/sync/bootstrap.js +52 -0
  58. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  59. package/dist/hooks/sync/orchestratorShared.js +209 -0
  60. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  61. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  62. package/dist/hooks/sync/realtime.d.ts +25 -0
  63. package/dist/hooks/sync/realtime.js +60 -0
  64. package/dist/hooks/sync/recovery.d.ts +69 -0
  65. package/dist/hooks/sync/recovery.js +118 -0
  66. package/dist/hooks/sync/transfers.d.ts +122 -0
  67. package/dist/hooks/sync/transfers.js +431 -0
  68. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  69. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  70. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  71. package/dist/hooks/ui/useResourceExport.js +5 -2
  72. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  73. package/dist/hooks/useRealtimeSync.js +2 -1
  74. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  75. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  76. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  77. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  78. package/dist/hooks/useSyncOrchestrator.js +314 -873
  79. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
  80. package/dist/hooks/useTaskMutations.js +45 -6
  81. package/dist/hooks/useTaskMutations.test.js +147 -0
  82. package/dist/hooks/useTaskforce.d.ts +8 -2
  83. package/dist/hooks/useTaskforce.js +285 -812
  84. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  85. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  86. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  87. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  88. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  89. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  90. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  91. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  92. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  93. package/dist/localization/locales/en-US.js +2 -2
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  95. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  96. package/dist/mcp/clientIntegrations.d.ts +12 -1
  97. package/dist/mcp/clientIntegrations.js +161 -6
  98. package/dist/mcp/clientIntegrations.test.js +76 -4
  99. package/dist/mcp/documentHelpers.d.ts +100 -0
  100. package/dist/mcp/documentHelpers.js +161 -0
  101. package/dist/mcp/runtime.js +174 -476
  102. package/dist/mcp/runtime.test.js +81 -8
  103. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  104. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  105. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  106. package/dist/mcp/taskAttachmentTypes.js +1 -0
  107. package/dist/plugins/vite.js +1 -1
  108. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  109. package/dist/runtime/appGateDefinitions.js +1 -1
  110. package/dist/server/index.d.ts +1 -0
  111. package/dist/server/index.js +46 -3
  112. package/dist/server/index.test.js +12 -1
  113. package/dist/server/routes/admin.js +13 -19
  114. package/dist/server/routes/auth.js +37 -2
  115. package/dist/server/routes/billing.js +18 -67
  116. package/dist/server/routes/billing.test.js +43 -0
  117. package/dist/server/routes/shared.d.ts +30 -0
  118. package/dist/server/routes/shared.js +26 -0
  119. package/dist/server/routes/sync.integration.test.js +20 -0
  120. package/dist/server/routes/sync.js +110 -237
  121. package/dist/server/routes/tasks.js +49 -7
  122. package/dist/server/routes.js +8 -19
  123. package/dist/server/routes.test.js +243 -5
  124. package/dist/shared/taskActor.d.ts +10 -0
  125. package/dist/shared/taskActor.js +1 -0
  126. package/dist/sync/collaborationSyncPayload.js +4 -0
  127. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  128. package/dist/sync/collaborationSyncState.d.ts +3 -1
  129. package/dist/sync/collaborationSyncState.js +19 -0
  130. package/dist/sync/contentSyncState.js +12 -10
  131. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  132. package/dist/sync/syncApplyHandlers.js +370 -8
  133. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  134. package/dist/sync/syncApplyHandlers.test.js +377 -0
  135. package/dist/sync/syncService.d.ts +8 -1
  136. package/dist/sync/syncService.js +36 -2
  137. package/dist/sync/syncService.test.js +48 -0
  138. package/dist/sync/workspacePullFeed.d.ts +1 -0
  139. package/dist/sync/workspacePullFeed.js +66 -59
  140. package/dist/sync/workspacePullFeed.test.js +72 -4
  141. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  142. package/dist/sync/workspaceSyncModel.js +65 -0
  143. package/dist/sync/workspaceSyncModel.test.js +119 -0
  144. package/dist/types.d.ts +2 -10
  145. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  146. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  147. package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  148. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
  149. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  150. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  151. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  152. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
  153. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  154. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  155. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
  156. package/dist/ui/index.html +4 -4
  157. package/dist/utils/taskActivity.js +4 -0
  158. package/dist/utils/taskActivity.test.js +25 -1
  159. package/package.json +3 -1
  160. package/scripts/playwright-auth.sh +4 -0
  161. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  162. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  163. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  164. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  165. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -211,6 +211,18 @@ function setupBillingSchema(db) {
211
211
  function buildBillingRouteHandlers(db, opts) {
212
212
  const routes = new Map();
213
213
  const tenantId = 'default';
214
+ const resolvePlanIdForVersion = (planVersionIdRaw) => {
215
+ const planVersionId = String(planVersionIdRaw || '').trim();
216
+ if (!planVersionId)
217
+ return '';
218
+ const row = db.prepare(`
219
+ SELECT plan_id
220
+ FROM plan_versions
221
+ WHERE tenant_id = ? AND plan_version_id = ?
222
+ LIMIT 1
223
+ `).get(tenantId, planVersionId);
224
+ return String(row?.plan_id || '').trim();
225
+ };
214
226
  const core = {
215
227
  getDatabaseAdapter: () => db,
216
228
  getTenantId: () => tenantId,
@@ -223,6 +235,37 @@ function buildBillingRouteHandlers(db, opts) {
223
235
  ON CONFLICT (user_id) DO NOTHING
224
236
  `).run(userId, tenantId, now, now);
225
237
  },
238
+ transitionCanonicalCommercialState: (input) => {
239
+ const now = new Date().toISOString();
240
+ const planVersionId = String(input.planVersionId || '').trim();
241
+ const effectiveAt = input.effectiveAt || now;
242
+ const effectiveUntil = input.effectiveUntil || null;
243
+ db.prepare(`
244
+ INSERT INTO account_entitlements (
245
+ account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at
246
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
247
+ ON CONFLICT (account_id) DO UPDATE SET
248
+ plan_version_id = excluded.plan_version_id,
249
+ entitlement_state = excluded.entitlement_state,
250
+ effective_at = excluded.effective_at,
251
+ effective_until = excluded.effective_until,
252
+ updated_by = excluded.updated_by,
253
+ updated_at = excluded.updated_at
254
+ `).run(input.accountId, tenantId, planVersionId, input.state, effectiveAt, effectiveUntil, input.updatedBy, input.createdAt || now, input.updatedAt || now);
255
+ const planId = typeof input.billingMirrorPlanId === 'string'
256
+ ? input.billingMirrorPlanId
257
+ : resolvePlanIdForVersion(planVersionId);
258
+ db.prepare(`
259
+ INSERT INTO user_billing (
260
+ user_id, tenant_id, tier, entitlement_state, effective_until, created_at, updated_at
261
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
262
+ ON CONFLICT (user_id) DO UPDATE SET
263
+ tier = excluded.tier,
264
+ entitlement_state = excluded.entitlement_state,
265
+ effective_until = excluded.effective_until,
266
+ updated_at = excluded.updated_at
267
+ `).run(input.accountId, tenantId, planId, input.state, effectiveUntil, now, now);
268
+ },
226
269
  listPlanCatalog: () => [
227
270
  { planId: 'pro', displayName: 'Pro Plan', status: 'active', createdAt: '', updatedAt: '' },
228
271
  { planId: 'inactive-plan', displayName: 'Inactive', status: 'archived', createdAt: '', updatedAt: '' },
@@ -7,8 +7,38 @@
7
7
  */
8
8
  import { IncomingMessage, ServerResponse } from 'http';
9
9
  import { TaskforceCore } from '../../core/Taskforce.js';
10
+ import type { McpAccessTokenAuthResult, McpOAuthAccessTokenAuthResult, McpAccessTokenScope } from '../../core/Taskforce.js';
10
11
  import type { RouteContext } from '../routes.js';
12
+ import type { AiProfileBootstrapConfig } from '../../mcp/aiProfileBootstrap.js';
11
13
  export declare function resolveErrorStatusCode(error: unknown, fallback?: number): number;
14
+ export declare function buildCloudMcpRuntimeAuth(input: {
15
+ req: IncomingMessage;
16
+ auth: {
17
+ authenticatedViaToken: McpAccessTokenAuthResult | null;
18
+ authenticatedViaOAuth: McpOAuthAccessTokenAuthResult | null;
19
+ userId: string | null;
20
+ } & Required<AiProfileBootstrapConfig>;
21
+ requestHeader: (req: IncomingMessage, name: string, fallback?: string) => string;
22
+ requestId?: string | null;
23
+ authScopes?: McpAccessTokenScope[] | null;
24
+ }): {
25
+ scopes?: McpAccessTokenScope[] | null;
26
+ userId?: string | null;
27
+ tokenId?: string | null;
28
+ aiProfileToken?: AiProfileBootstrapConfig['aiProfileToken'];
29
+ aiProfileName?: AiProfileBootstrapConfig['aiProfileName'];
30
+ aiProfileIcon?: AiProfileBootstrapConfig['aiProfileIcon'];
31
+ aiProfileColor?: AiProfileBootstrapConfig['aiProfileColor'];
32
+ aiProfileDescription?: AiProfileBootstrapConfig['aiProfileDescription'];
33
+ aiProfileRole?: AiProfileBootstrapConfig['aiProfileRole'];
34
+ aiProfileProvider?: AiProfileBootstrapConfig['aiProfileProvider'];
35
+ aiProfileModel?: AiProfileBootstrapConfig['aiProfileModel'];
36
+ aiProfileSurfaceType?: AiProfileBootstrapConfig['aiProfileSurfaceType'];
37
+ actorType?: 'local' | 'user' | 'token' | 'oauth';
38
+ requestId?: string | null;
39
+ ip?: string | null;
40
+ userAgent?: string | null;
41
+ };
12
42
  export declare function requestHeader(req: IncomingMessage, name: string, fallback?: string): string;
13
43
  export declare function requestWorkspaceHeader(req: IncomingMessage): string;
14
44
  export declare function requestAuthoritativeWorkspaceId(req: IncomingMessage): string;
@@ -17,6 +17,32 @@ export function resolveErrorStatusCode(error, fallback = 400) {
17
17
  }
18
18
  return fallback;
19
19
  }
20
+ export function buildCloudMcpRuntimeAuth(input) {
21
+ const { req, auth, requestHeader, requestId, authScopes } = input;
22
+ const actorType = auth.authenticatedViaToken
23
+ ? 'token'
24
+ : auth.authenticatedViaOAuth
25
+ ? 'oauth'
26
+ : 'user';
27
+ return {
28
+ scopes: authScopes || auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [],
29
+ userId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || null,
30
+ tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
31
+ aiProfileToken: auth.aiProfileToken,
32
+ aiProfileName: auth.aiProfileName,
33
+ aiProfileIcon: auth.aiProfileIcon,
34
+ aiProfileColor: auth.aiProfileColor,
35
+ aiProfileDescription: auth.aiProfileDescription,
36
+ aiProfileRole: auth.aiProfileRole,
37
+ aiProfileProvider: auth.aiProfileProvider,
38
+ aiProfileModel: auth.aiProfileModel,
39
+ aiProfileSurfaceType: auth.aiProfileSurfaceType,
40
+ actorType,
41
+ requestId: String(requestId || req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || undefined,
42
+ ip: req.socket?.remoteAddress || null,
43
+ userAgent: requestHeader(req, 'user-agent', '') || null,
44
+ };
45
+ }
20
46
  // === SECTION: Request header helpers ===
21
47
  export function requestHeader(req, name, fallback = '') {
22
48
  const value = req.headers?.[name.toLowerCase()];
@@ -344,6 +344,10 @@ describe('Server Routes Workspace Sync Integration', () => {
344
344
  username: 'codex',
345
345
  icon: 'Bot',
346
346
  color: '#0ea5e9',
347
+ description: 'Terminal-first coding agent',
348
+ role: 'Implementer',
349
+ provider: 'OpenAI',
350
+ model: 'gpt-5.4',
347
351
  surfaceType: 'coding_tool',
348
352
  createdAt: '2026-03-22T10:00:00.000Z',
349
353
  updatedAt: '2026-03-22T10:05:00.000Z',
@@ -361,6 +365,10 @@ describe('Server Routes Workspace Sync Integration', () => {
361
365
  id: 'ai-profile-codex',
362
366
  name: 'Codex',
363
367
  username: 'codex',
368
+ description: 'Terminal-first coding agent',
369
+ role: 'Implementer',
370
+ provider: 'OpenAI',
371
+ model: 'gpt-5.4',
364
372
  surfaceType: 'coding_tool'
365
373
  })
366
374
  ]);
@@ -1757,6 +1765,18 @@ describe('Server Routes Workspace Sync Integration', () => {
1757
1765
  const pushRes = createMockRes();
1758
1766
  await pushMatch?.handler(pushReq, pushRes, {});
1759
1767
  expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1768
+ const pushBody = readJsonBody(pushRes);
1769
+ expect(Array.isArray(pushBody?.appliedTaskUpserts)).toBe(true);
1770
+ expect(pushBody?.appliedTaskUpserts).toEqual(expect.arrayContaining([
1771
+ expect.objectContaining({
1772
+ archived: false,
1773
+ task: expect.objectContaining({
1774
+ id: taskId,
1775
+ title: 'Offline provisional task',
1776
+ referenceNumber: 1
1777
+ })
1778
+ })
1779
+ ]));
1760
1780
  const persisted = core.getTask(taskId, workspaceId);
1761
1781
  expect(persisted?.referenceNumber).toBe(1);
1762
1782
  expect(Date.parse(String(persisted?.updatedAt || ''))).toBeGreaterThan(Date.parse(staleUpdatedAt));
@@ -5,15 +5,14 @@ import { WorkspaceAssetStore, } from '../../storage/workspaceAssetStore.js';
5
5
  import { resolveSessionCookieAttributesForRequest } from '../auth.js';
6
6
  import { decryptDocument, ENCRYPTION_PREFIX } from '../../sync/encryptionService.js';
7
7
  import { isAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
8
- import { getTaskMutationTieBreak, getTaskMutationWatermark } from '../../sync/workspaceSyncSurface.js';
9
- import { normalizeTaskSyncPayload, taskSyncPayloadToCoreTask } from '../../sync/taskSyncPayload.js';
10
- import { normalizeBinaryAssetDeleteSyncPayload, normalizeBinaryAssetSyncPayload, normalizeDocumentDeleteSyncPayload, normalizeDocumentSyncPayload, serializeBinaryAssetSyncPayload, serializeDocumentSyncPayload } from '../../sync/contentSyncPayload.js';
8
+ import { serializeTaskForSync, } from '../../sync/taskSyncPayload.js';
9
+ import { serializeBinaryAssetSyncPayload, serializeDocumentSyncPayload, } from '../../sync/contentSyncPayload.js';
11
10
  import { createContentSyncState } from '../../sync/contentSyncState.js';
12
- import { normalizeAiProfileSyncPayload, normalizeAnnotatedAttachmentSessionSyncPayload, normalizeDeleteSyncPayload, normalizeDocumentReviewCommentSyncPayload, normalizeDocumentReviewSessionSyncPayload, } from '../../sync/collaborationSyncPayload.js';
11
+ import { normalizeAiProfileSyncPayload, normalizeAnnotatedAttachmentSessionSyncPayload, normalizeDocumentReviewCommentSyncPayload, normalizeDocumentReviewSessionSyncPayload, } from '../../sync/collaborationSyncPayload.js';
13
12
  import { createCollaborationSyncState } from '../../sync/collaborationSyncState.js';
14
13
  import { buildWorkspacePullFeed } from '../../sync/workspacePullFeed.js';
15
14
  import { normalizeInitiativeSyncPayload, normalizeTaxonomySyncPayload, normalizeWorkstreamSyncPayload, } from '../../sync/planningSyncPayload.js';
16
- import { applyAiProfileAndMemberSyncChanges, applyCollaborationSyncChanges, applyContentSyncChanges, applyPlanningSyncChanges, resolveSyncConflictOrdering, } from '../../sync/syncApplyHandlers.js';
15
+ import { applyAiProfileAndMemberSyncChanges, applyCollaborationSyncChanges, applyContentSyncChanges, applyPlanningSyncChanges, applyPreparedTaskSyncSnapshot, applyTaskActivitySyncChanges, applyTaskSyncDeletes, classifyWorkspaceSyncChanges, prepareTaskSyncSnapshotApply, } from '../../sync/syncApplyHandlers.js';
17
16
  import { applyWorkspaceRepair } from '../../sync/workspaceRepair.js';
18
17
  function decodeWorkspaceSyncCursor(raw) {
19
18
  const value = String(raw || '').trim();
@@ -60,6 +59,40 @@ function normalizeIncomingAiProfileSurfaceType(value, context) {
60
59
  }
61
60
  return normalized;
62
61
  }
62
+ function normalizeTaskEventSyncPayload(raw, workspaceId, sourceWatermark) {
63
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
64
+ return null;
65
+ const source = raw;
66
+ const id = String(source.id || '').trim();
67
+ const taskId = String(source.taskId || source.task_id || '').trim();
68
+ const action = String(source.action || '').trim();
69
+ const actor = String(source.actor || 'system').trim() || 'system';
70
+ const createdAt = String(source.createdAt || source.created_at || sourceWatermark || '').trim();
71
+ if (!id || !taskId || !action || !Number.isFinite(Date.parse(createdAt)))
72
+ return null;
73
+ const actorType = source.actorType === 'ai' || source.actorType === 'human' || source.actorType === 'system'
74
+ ? source.actorType
75
+ : source.actor_type === 'ai' || source.actor_type === 'human' || source.actor_type === 'system'
76
+ ? source.actor_type
77
+ : actor.startsWith('ai-profile-')
78
+ ? 'ai'
79
+ : actor === 'system'
80
+ ? 'system'
81
+ : 'human';
82
+ const details = source.details && typeof source.details === 'object' && !Array.isArray(source.details)
83
+ ? source.details
84
+ : {};
85
+ return {
86
+ id,
87
+ taskId,
88
+ workspaceId,
89
+ action,
90
+ actor,
91
+ actorType,
92
+ details,
93
+ createdAt
94
+ };
95
+ }
63
96
  export function registerSyncRoutes(deps) {
64
97
  const { addRoute, core, context, auditAuthAction, resolveErrorStatusCode, requestWorkspaceId, resolveRequestAccess, isReservedWorkspaceId, createSessionTokenCookie } = deps;
65
98
  const readPushIdempotency = (workspaceId, key) => {
@@ -190,32 +223,7 @@ export function registerSyncRoutes(deps) {
190
223
  workspaceDocumentSnapshotCache,
191
224
  });
192
225
  const collaborationSyncState = createCollaborationSyncState({ core });
193
- const hasTaskReferenceNumberDrift = (workspaceId, task) => {
194
- const taskId = String(task?.id || '').trim();
195
- const incomingReferenceNumber = Number.isFinite(Number(task?.referenceNumber))
196
- ? Math.max(1, Math.floor(Number(task?.referenceNumber)))
197
- : null;
198
- if (!incomingReferenceNumber)
199
- return false;
200
- const existingTask = core.resolveTaskIdentifier(`T-${incomingReferenceNumber}`, workspaceId);
201
- if (!existingTask || existingTask.id === taskId)
202
- return false;
203
- recordSyncDiagnostic(workspaceId, 'apply', 'Detected task reference number mismatch during normal sync.', {
204
- source: 'tasks.referenceNumber.mismatch',
205
- taskId: taskId || null,
206
- taskTitle: typeof task?.title === 'string' ? task.title.trim() || null : null,
207
- existingTaskId: existingTask.id,
208
- existingTaskTitle: existingTask.title || null,
209
- referenceLabel: `T-${incomingReferenceNumber}`,
210
- existingReferenceNumber: existingTask.referenceNumber ?? incomingReferenceNumber,
211
- incomingReferenceNumber
212
- });
213
- return true;
214
- };
215
226
  const dispatchWorkspaceSyncChanges = async (workspaceId, incomingChanges, options) => {
216
- const tasks = [];
217
- const archived = [];
218
- const incomingTaskDeletes = new Map();
219
227
  const emittedEventIds = new Set();
220
228
  const unsubscribeRealtime = core.onRealtimeMutation((event) => {
221
229
  const eventWorkspaceId = String(event?.workspaceId || '').trim();
@@ -225,224 +233,63 @@ export function registerSyncRoutes(deps) {
225
233
  if (eventId)
226
234
  emittedEventIds.add(eventId);
227
235
  });
228
- let incomingTaxonomies = null;
229
- let incomingTaxonomyState = null;
230
- let incomingTaxonomiesUpdatedAt = '';
231
- const incomingInitiatives = new Map();
232
- const incomingWorkstreams = new Map();
233
- const incomingAiProfiles = new Map();
234
- const incomingDocuments = [];
235
- const incomingDocumentDeletes = new Map();
236
- const incomingDocumentReviewSessions = new Map();
237
- const incomingDocumentReviewSessionDeletes = new Map();
238
- const incomingDocumentReviewComments = new Map();
239
- const incomingDocumentReviewCommentDeletes = new Map();
240
- const incomingAnnotatedAttachmentSessions = new Map();
241
- const incomingAnnotatedAttachmentSessionDeletes = new Map();
242
- const incomingAssets = [];
243
- const incomingAssetDeletes = new Map();
244
236
  try {
245
- for (const change of incomingChanges) {
246
- const raw = change;
247
- const op = String(raw.op || '').trim().toLowerCase();
248
- if (op === 'taxonomy-upsert') {
249
- const taxonomy = normalizeTaxonomySyncPayload(raw);
250
- if (taxonomy?.taxonomyState)
251
- incomingTaxonomyState = taxonomy.taxonomyState;
252
- if (Array.isArray(taxonomy?.taxonomies))
253
- incomingTaxonomies = taxonomy.taxonomies;
254
- if (taxonomy?.updatedAt)
255
- incomingTaxonomiesUpdatedAt = taxonomy.updatedAt;
256
- continue;
257
- }
258
- if (op === 'initiative-upsert') {
259
- const initiative = normalizeInitiativeSyncPayload(raw.initiative, String(raw.updatedAt || raw.watermark || '').trim());
260
- if (initiative)
261
- incomingInitiatives.set(initiative.id, initiative);
262
- continue;
263
- }
264
- if (op === 'workstream-upsert') {
265
- const workstream = normalizeWorkstreamSyncPayload(raw.workstream, String(raw.updatedAt || raw.watermark || '').trim());
266
- if (workstream)
267
- incomingWorkstreams.set(workstream.id, workstream);
268
- continue;
269
- }
270
- if (op === 'ai-profile-upsert') {
271
- const profile = normalizeAiProfileSyncPayload(raw.profile, workspaceId, normalizeIncomingAiProfileSurfaceType, 'apply.aiProfileUpsert');
272
- const profileId = String(profile?.id || '').trim();
273
- if (profileId && profile) {
274
- const profileWorkspaceId = String(profile.workspaceId || '').trim();
275
- if (profileWorkspaceId && profileWorkspaceId !== workspaceId) {
276
- recordSyncDiagnostic(workspaceId, 'apply', `Rejected ai-profile-upsert for profile ${profileId}: workspace mismatch (expected ${workspaceId}, got ${profileWorkspaceId}).`, {
277
- source: 'apply.aiProfileWorkspaceMismatch',
278
- workspaceId,
279
- profileId,
280
- expectedWorkspaceId: workspaceId,
281
- receivedWorkspaceId: profileWorkspaceId
282
- });
283
- continue;
284
- }
285
- incomingAiProfiles.set(profileId, {
286
- ...profile,
287
- workspaceId,
288
- createdAt: profile.createdAt || String(raw.updatedAt || raw.watermark || '').trim(),
289
- updatedAt: profile.updatedAt || String(raw.updatedAt || raw.watermark || '').trim(),
290
- });
291
- }
292
- continue;
293
- }
294
- if (op === 'document-upsert') {
295
- const document = normalizeDocumentSyncPayload(raw, normalizeWorkspaceSyncDocumentPath);
296
- if (document)
297
- incomingDocuments.push(document);
298
- continue;
299
- }
300
- if (op === 'document-delete') {
301
- const deletedDocument = normalizeDocumentDeleteSyncPayload(raw, normalizeWorkspaceSyncDocumentPath);
302
- if (deletedDocument)
303
- incomingDocumentDeletes.set(deletedDocument.path, deletedDocument.deletedAt);
304
- continue;
305
- }
306
- if (op === 'asset-upsert') {
307
- const asset = normalizeBinaryAssetSyncPayload(raw, (rawPath) => normalizeTaskDataRelativePath(String(rawPath || '').trim()));
308
- if (asset)
309
- incomingAssets.push(asset);
310
- continue;
311
- }
312
- if (op === 'asset-delete') {
313
- const deletedAsset = normalizeBinaryAssetDeleteSyncPayload(raw, (rawPath) => normalizeTaskDataRelativePath(String(rawPath || '').trim()));
314
- if (deletedAsset)
315
- incomingAssetDeletes.set(deletedAsset.path, deletedAsset.deletedAt);
316
- continue;
317
- }
318
- if (op === 'document-review-session-upsert') {
319
- const session = normalizeDocumentReviewSessionSyncPayload(raw.session, String(raw.updatedAt || raw.watermark || '').trim());
320
- if (session)
321
- incomingDocumentReviewSessions.set(session.id, session);
322
- continue;
323
- }
324
- if (op === 'document-review-session-delete') {
325
- const deletedSession = normalizeDeleteSyncPayload(raw, 'sessionId');
326
- if (deletedSession)
327
- incomingDocumentReviewSessionDeletes.set(deletedSession.id, deletedSession.deletedAt);
328
- continue;
329
- }
330
- if (op === 'document-review-comment-upsert') {
331
- const comment = normalizeDocumentReviewCommentSyncPayload(raw.comment, { sourceWatermark: String(raw.updatedAt || raw.watermark || '').trim() });
332
- if (comment)
333
- incomingDocumentReviewComments.set(comment.id, comment);
334
- continue;
335
- }
336
- if (op === 'document-review-comment-delete') {
337
- const deletedComment = normalizeDeleteSyncPayload(raw, 'commentId');
338
- if (deletedComment)
339
- incomingDocumentReviewCommentDeletes.set(deletedComment.id, deletedComment.deletedAt);
340
- continue;
341
- }
342
- if (op === 'annotated-attachment-session-upsert') {
343
- const session = normalizeAnnotatedAttachmentSessionSyncPayload(raw.session, workspaceId, String(raw.updatedAt || raw.watermark || '').trim());
344
- if (session)
345
- incomingAnnotatedAttachmentSessions.set(session.id, session);
346
- continue;
347
- }
348
- if (op === 'annotated-attachment-session-delete') {
349
- const deletedSession = normalizeDeleteSyncPayload(raw, 'sessionId');
350
- if (deletedSession)
351
- incomingAnnotatedAttachmentSessionDeletes.set(deletedSession.id, deletedSession.deletedAt);
352
- continue;
353
- }
354
- if (op !== 'delete')
355
- continue;
356
- const task = normalizeTaskSyncPayload(raw.task);
357
- const deleteTaskId = String(raw.taskId || raw.id || task?.id || '').trim();
358
- if (deleteTaskId) {
359
- incomingTaskDeletes.set(deleteTaskId, String(raw.deletedAt || raw.watermark || new Date().toISOString()).trim());
360
- }
361
- }
362
- for (const change of incomingChanges) {
363
- const raw = change;
364
- const op = String(raw.op || '').trim().toLowerCase();
365
- if (op === 'delete'
366
- || op === 'initiative-upsert'
367
- || op === 'workstream-upsert'
368
- || op === 'document-upsert'
369
- || op === 'document-delete'
370
- || op === 'asset-upsert'
371
- || op === 'asset-delete'
372
- || op === 'document-review-session-upsert'
373
- || op === 'document-review-session-delete'
374
- || op === 'document-review-comment-upsert'
375
- || op === 'document-review-comment-delete'
376
- || op === 'annotated-attachment-session-upsert'
377
- || op === 'annotated-attachment-session-delete')
378
- continue;
379
- const task = normalizeTaskSyncPayload(raw.task);
380
- if (!task)
381
- continue;
382
- const taskId = String(task.id || '').trim();
383
- hasTaskReferenceNumberDrift(workspaceId, task);
384
- const deleteWatermark = taskId ? String(incomingTaskDeletes.get(taskId) || '').trim() : '';
385
- if (taskId && deleteWatermark) {
386
- const compareAgainstDelete = resolveSyncConflictOrdering(deleteWatermark, String(task.updatedAt || task.createdAt || '').trim(), `delete:${taskId}`, getTaskMutationTieBreak(task, raw.archived === true || task?.isArchived === true));
387
- if (compareAgainstDelete >= 0)
388
- continue;
389
- }
390
- const markedArchived = raw.archived === true || task?.isArchived === true;
391
- if (markedArchived)
392
- archived.push(task);
393
- else
394
- tasks.push(task);
395
- }
396
- for (const [taskId, deleteWatermark] of incomingTaskDeletes.entries()) {
397
- try {
398
- const existingTask = core.getTask(taskId, workspaceId);
399
- if (existingTask) {
400
- const compareAgainstExisting = resolveSyncConflictOrdering(deleteWatermark, getTaskMutationWatermark(existingTask), `delete:${taskId}`, getTaskMutationTieBreak(existingTask, Boolean(existingTask.isArchived)));
401
- if (compareAgainstExisting < 0)
402
- continue;
403
- }
404
- core.deleteTaskForSync(taskId, workspaceId, { persistBackup: false });
405
- }
406
- catch (error) {
407
- recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete task during sync apply.', {
408
- source: 'apply.deleteTask',
409
- taskId,
410
- error: String(error?.message || error || '')
411
- });
412
- }
413
- }
237
+ const applyModel = classifyWorkspaceSyncChanges({
238
+ workspaceId,
239
+ incomingChanges,
240
+ normalizeWorkspaceSyncDocumentPath,
241
+ normalizeTaskAssetPath: (rawPath) => normalizeTaskDataRelativePath(String(rawPath || '').trim()),
242
+ normalizeIncomingAiProfileSurfaceType,
243
+ normalizeTaskEventSyncPayload,
244
+ recordSyncDiagnostic,
245
+ });
414
246
  applyPlanningSyncChanges({
415
247
  core,
416
248
  workspaceId,
417
- incomingInitiatives,
418
- incomingWorkstreams,
419
- incomingTaxonomies,
420
- incomingTaxonomyState,
421
- incomingTaxonomiesUpdatedAt,
249
+ incomingInitiatives: applyModel.planning.incomingInitiatives,
250
+ incomingWorkstreams: applyModel.planning.incomingWorkstreams,
251
+ incomingTaxonomies: applyModel.planning.incomingTaxonomies,
252
+ incomingTaxonomyState: applyModel.planning.incomingTaxonomyState,
253
+ incomingTaxonomiesUpdatedAt: applyModel.planning.incomingTaxonomiesUpdatedAt,
422
254
  workspaceTaxonomySyncMeta,
423
255
  recordSyncDiagnostic,
424
256
  });
425
257
  applyAiProfileAndMemberSyncChanges({
426
258
  core,
427
259
  workspaceId,
428
- incomingAiProfiles,
260
+ incomingAiProfiles: applyModel.profiles.incomingAiProfiles,
429
261
  workspaceMembers: options?.workspaceMembers,
430
262
  recordSyncDiagnostic,
431
263
  assertSyncedAiProfilesPersisted,
432
264
  createSyncRouteError,
433
265
  });
434
- validateTaskActorAssignments(workspaceId, tasks, archived);
266
+ const preparedTaskSnapshot = prepareTaskSyncSnapshotApply({
267
+ core,
268
+ workspaceId,
269
+ taskMutations: applyModel.taskMutations,
270
+ recordSyncDiagnostic,
271
+ });
272
+ validateTaskActorAssignments(workspaceId, preparedTaskSnapshot.tasks, preparedTaskSnapshot.archived);
435
273
  await applyContentSyncChanges({
436
274
  core,
437
275
  workspaceId,
438
- incomingDocuments,
439
- incomingDocumentDeletes,
440
- incomingAssets,
441
- incomingAssetDeletes,
276
+ incomingDocuments: applyModel.content.incomingDocuments,
277
+ incomingDocumentDeletes: applyModel.content.incomingDocumentDeletes,
278
+ incomingAssets: applyModel.content.incomingAssets,
279
+ incomingAssetDeletes: applyModel.content.incomingAssetDeletes,
442
280
  basePath: core.getPaths().basePath,
443
281
  hasObjectStorageClient: () => Boolean(getObjectStorageClient()),
444
282
  hasDocumentSyncMetadataDrift: contentSyncState.hasDocumentSyncMetadataDrift,
445
283
  hasBinaryAssetSyncMetadataDrift: contentSyncState.hasBinaryAssetSyncMetadataDrift,
284
+ getExistingDocument: (docPath, targetWorkspaceId) => {
285
+ const asset = workspaceAssetStore?.getByStorageKey(docPath, targetWorkspaceId) || null;
286
+ return asset
287
+ ? {
288
+ updatedAt: asset.updatedAt,
289
+ contentHash: asset.contentSha256
290
+ }
291
+ : null;
292
+ },
446
293
  writeWorkspaceSyncDocument: contentSyncState.writeWorkspaceSyncDocument,
447
294
  deleteWorkspaceSyncDocument: contentSyncState.deleteWorkspaceSyncDocument,
448
295
  writeWorkspaceSyncBinaryAsset: contentSyncState.writeWorkspaceSyncBinaryAsset,
@@ -454,20 +301,34 @@ export function registerSyncRoutes(deps) {
454
301
  applyCollaborationSyncChanges({
455
302
  core,
456
303
  workspaceId,
457
- incomingDocumentReviewSessions,
458
- incomingDocumentReviewSessionDeletes,
459
- incomingDocumentReviewComments,
460
- incomingDocumentReviewCommentDeletes,
461
- incomingAnnotatedAttachmentSessions,
462
- incomingAnnotatedAttachmentSessionDeletes,
304
+ incomingDocumentReviewSessions: applyModel.collaboration.incomingDocumentReviewSessions,
305
+ incomingDocumentReviewSessionDeletes: applyModel.collaboration.incomingDocumentReviewSessionDeletes,
306
+ incomingDocumentReviewComments: applyModel.collaboration.incomingDocumentReviewComments,
307
+ incomingDocumentReviewCommentDeletes: applyModel.collaboration.incomingDocumentReviewCommentDeletes,
308
+ incomingAnnotatedAttachmentSessions: applyModel.collaboration.incomingAnnotatedAttachmentSessions,
309
+ incomingAnnotatedAttachmentSessionDeletes: applyModel.collaboration.incomingAnnotatedAttachmentSessionDeletes,
310
+ recordSyncDiagnostic,
311
+ createSyncRouteError,
312
+ });
313
+ const deleteCount = applyTaskSyncDeletes({
314
+ core,
315
+ workspaceId,
316
+ taskMutations: applyModel.taskMutations,
317
+ recordSyncDiagnostic,
318
+ });
319
+ const result = applyPreparedTaskSyncSnapshot({
320
+ core,
321
+ workspaceId,
322
+ prepared: preparedTaskSnapshot,
323
+ });
324
+ applyTaskActivitySyncChanges({
325
+ core,
326
+ workspaceId,
327
+ incomingTaskEvents: applyModel.taskActivity.incomingTaskEvents,
463
328
  recordSyncDiagnostic,
464
329
  createSyncRouteError,
465
330
  });
466
- const result = core.applyWorkspaceSyncSnapshot({
467
- tasks: tasks.map(taskSyncPayloadToCoreTask),
468
- archived: archived.map(taskSyncPayloadToCoreTask)
469
- }, workspaceId);
470
- return { result, deleteCount: incomingTaskDeletes.size, emittedEventIds: Array.from(emittedEventIds) };
331
+ return { result, deleteCount, emittedEventIds: Array.from(emittedEventIds) };
471
332
  }
472
333
  finally {
473
334
  unsubscribeRealtime();
@@ -1257,6 +1118,10 @@ export function registerSyncRoutes(deps) {
1257
1118
  }
1258
1119
  }
1259
1120
  let incomingChanges = Array.isArray(body?.changes) ? body.changes : [];
1121
+ const incomingTaskUpsertIds = incomingChanges
1122
+ .filter((change) => change?.op === 'upsert')
1123
+ .map((change) => String(change.task?.id || '').trim())
1124
+ .filter((taskId) => taskId.length > 0);
1260
1125
  const allowReferenceNumberReassignment = body?.repair === true;
1261
1126
  // Decrypt asset and document content if TASKFORCE_SYNC_KEY is configured on this server.
1262
1127
  // The local runtime encrypts content before pushing; the cloud must decrypt before writing to storage.
@@ -1311,12 +1176,20 @@ export function registerSyncRoutes(deps) {
1311
1176
  const { result, deleteCount, emittedEventIds } = await dispatchWorkspaceSyncChanges(targetWorkspaceId, incomingChanges, {
1312
1177
  allowReferenceNumberReassignment
1313
1178
  });
1179
+ const appliedTaskUpserts = incomingTaskUpsertIds
1180
+ .map((taskId) => core.getTask(taskId, targetWorkspaceId))
1181
+ .filter((task) => Boolean(task))
1182
+ .map((task) => ({
1183
+ archived: Boolean(task.isArchived),
1184
+ task: serializeTaskForSync(task)
1185
+ }));
1314
1186
  const durationMs = Date.now() - startedAtMs;
1315
1187
  const payload = JSON.stringify({
1316
1188
  success: true,
1317
1189
  workspaceId: targetWorkspaceId,
1318
1190
  idempotencyKey: idempotencyKey || null,
1319
1191
  emittedEventIds,
1192
+ appliedTaskUpserts,
1320
1193
  result,
1321
1194
  durationMs
1322
1195
  });