@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
@@ -7,6 +7,7 @@ export declare function prepareWorkspaceSyncV3CombinedRepairSnapshot(input: {
7
7
  nestedCoverage?: boolean;
8
8
  workflowCoverage?: boolean;
9
9
  planningOrderCoverage?: boolean;
10
+ coveredLegacyWritesFenced?: boolean;
10
11
  }): {
11
12
  highWatermark: string;
12
13
  insertedEntities: number;
@@ -20,16 +21,19 @@ export declare function prepareWorkspaceSyncV3NestedRepairSnapshot(input: {
20
21
  core: TaskforceCore;
21
22
  workspaceId: string;
22
23
  now?: string;
24
+ coveredLegacyWritesFenced?: boolean;
23
25
  }): ReturnType<typeof prepareWorkspaceSyncV3CombinedRepairSnapshot>;
24
26
  export declare function prepareWorkspaceSyncV3WorkflowRepairSnapshot(input: {
25
27
  core: TaskforceCore;
26
28
  workspaceId: string;
27
29
  now?: string;
30
+ coveredLegacyWritesFenced?: boolean;
28
31
  }): ReturnType<typeof prepareWorkspaceSyncV3CombinedRepairSnapshot>;
29
32
  export declare function prepareWorkspaceSyncV3PlanningOrderRepairSnapshot(input: {
30
33
  core: TaskforceCore;
31
34
  workspaceId: string;
32
35
  now?: string;
36
+ coveredLegacyWritesFenced?: boolean;
33
37
  }): ReturnType<typeof prepareWorkspaceSyncV3CombinedRepairSnapshot>;
34
38
  export declare function ensureWorkspaceSyncV3CombinedRepairBaselines(input: {
35
39
  core: TaskforceCore;
@@ -12,7 +12,7 @@ import { serializeWorkspaceSyncV3TaskAttachmentLinksPayload, serializeWorkspaceS
12
12
  import { projectWorkspaceSyncV3NestedTaskJournalRow } from './workspaceSyncV3NestedTaskProjection.js';
13
13
  import { serializeTaskRelationshipForSync } from './workspaceSyncV3TaskRelationshipProjection.js';
14
14
  import { projectWorkspaceSyncV3TaskRootJournalRow, serializeTaskRootForSync, } from './workspaceSyncV3TaskRootProjection.js';
15
- const SQL_BATCH_SIZE = 400;
15
+ import { workspaceSyncV3RepairEntityReadBatchSize } from './workspaceSyncV3RepairSqlBatching.js';
16
16
  const BASELINE_CLIENT_ID = 'sync-v3-repair';
17
17
  const LEGACY_REPAIRABLE_NESTED_DOMAINS = new Set([
18
18
  'task-comments', 'task-checklist', 'task-attachment-links',
@@ -84,10 +84,11 @@ function lifecycleState(candidate) {
84
84
  function readRevisions(input) {
85
85
  const result = new Map();
86
86
  const db = input.core.getDatabaseAdapter();
87
+ const batchSize = workspaceSyncV3RepairEntityReadBatchSize(db);
87
88
  for (const domain of [...new Set(input.candidates.map((candidate) => candidate.domain))]) {
88
89
  const ids = input.candidates.filter((candidate) => candidate.domain === domain).map((candidate) => candidate.entityId);
89
- for (let offset = 0; offset < ids.length; offset += SQL_BATCH_SIZE) {
90
- const batch = ids.slice(offset, offset + SQL_BATCH_SIZE);
90
+ for (let offset = 0; offset < ids.length; offset += batchSize) {
91
+ const batch = ids.slice(offset, offset + batchSize);
91
92
  const placeholders = batch.map(() => '?').join(', ');
92
93
  const rows = db.prepare(`
93
94
  SELECT entity_id, contract_version,
@@ -250,6 +251,7 @@ export function prepareWorkspaceSyncV3CombinedRepairSnapshot(input) {
250
251
  const tenantId = input.core.getTenantId();
251
252
  const db = input.core.getDatabaseAdapter();
252
253
  return db.transaction(() => {
254
+ const startedAt = Date.now();
253
255
  const lock = db.prepare(`
254
256
  INSERT INTO workspace_sync_sequences (tenant_id, workspace_id, last_sequence, updated_at)
255
257
  VALUES (?, ?, 0, ?)
@@ -257,6 +259,7 @@ export function prepareWorkspaceSyncV3CombinedRepairSnapshot(input) {
257
259
  RETURNING CAST(last_sequence AS TEXT) AS last_sequence
258
260
  `).get(tenantId, workspaceId, now);
259
261
  sequence(lock?.last_sequence);
262
+ const lockCompletedAt = Date.now();
260
263
  const candidates = input.workflowCoverage
261
264
  ? listWorkspaceSyncV3WorkflowRepairCandidates(input.core, workspaceId)
262
265
  : input.planningOrderCoverage
@@ -264,10 +267,12 @@ export function prepareWorkspaceSyncV3CombinedRepairSnapshot(input) {
264
267
  : input.nestedCoverage
265
268
  ? listWorkspaceSyncV3NestedRepairCandidates(input.core, workspaceId)
266
269
  : listWorkspaceSyncV3CombinedRepairCandidates(input.core, workspaceId);
270
+ const inventoryCompletedAt = Date.now();
267
271
  const keys = candidates.map((candidate) => `${candidate.domain}\0${candidate.entityId}`);
268
272
  if (new Set(keys).size !== keys.length)
269
273
  throw new Error('Combined repair baseline contains duplicate entities.');
270
274
  const revisions = readRevisions({ core: input.core, tenantId, workspaceId, candidates });
275
+ const revisionsCompletedAt = Date.now();
271
276
  const missing = [];
272
277
  const legacyRelationshipSkews = [];
273
278
  const legacyNestedDrifts = [];
@@ -383,6 +388,7 @@ export function prepareWorkspaceSyncV3CombinedRepairSnapshot(input) {
383
388
  });
384
389
  }
385
390
  }
391
+ const validationCompletedAt = Date.now();
386
392
  const rowCount = legacyRelationshipSkews.length + legacyNestedDrifts.length + missing.reduce((count, candidate) => {
387
393
  if (candidate.domain === 'workflow-published-definition'
388
394
  || candidate.domain === 'workflow-execution'
@@ -541,6 +547,7 @@ export function prepareWorkspaceSyncV3CombinedRepairSnapshot(input) {
541
547
  if (Number(updated.changes) !== 1)
542
548
  throw repairConflict('legacy-nested-repair-raced', repair.candidate);
543
549
  }
550
+ const baselineWritesCompletedAt = Date.now();
544
551
  const profile = input.workflowCoverage
545
552
  ? WORKSPACE_SYNC_V3_WORKFLOW_COVERAGE_PROFILE
546
553
  : input.planningOrderCoverage
@@ -557,9 +564,10 @@ export function prepareWorkspaceSyncV3CombinedRepairSnapshot(input) {
557
564
  `).get(tenantId, workspaceId, WORKSPACE_SYNC_COMBINED_PROJECTION, WORKSPACE_SYNC_CONTRACT_VERSION, profile.coverageProfileId, profile.coverageVersion, profile.coverageDigest);
558
565
  // Exact active v3 ownership closes every ordinary writer over the same
559
566
  // sequence lock, so the inventory read above remains the authoritative
560
- // cut. Activation repair still re-inventories because legacy/v2 writers
561
- // do not reserve that lock and could otherwise create undetectable drift.
562
- const snapshot = activeOwner?.active === 1
567
+ // cut. The cloud activation route may attest the same invariant by
568
+ // fencing covered legacy/v2 writers. Unfenced rollback repair still
569
+ // re-inventories because those writers do not reserve this lock.
570
+ const snapshot = activeOwner?.active === 1 || input.coveredLegacyWritesFenced === true
563
571
  ? buildWorkspaceSyncV3CombinedRepairSnapshotFromLockedCandidates({
564
572
  core: input.core,
565
573
  workspaceId,
@@ -576,9 +584,24 @@ export function prepareWorkspaceSyncV3CombinedRepairSnapshot(input) {
576
584
  : input.nestedCoverage
577
585
  ? buildWorkspaceSyncV3NestedRepairSnapshot({ core: input.core, workspaceId })
578
586
  : buildWorkspaceSyncV3CombinedRepairSnapshot({ core: input.core, workspaceId });
587
+ const snapshotCompletedAt = Date.now();
579
588
  if (snapshot.snapshotSequence !== highWatermark) {
580
589
  throw new Error('Combined repair baseline snapshot sequence changed while locked.');
581
590
  }
591
+ console.info('[Taskforce Sync Repair Baseline Timing]', JSON.stringify({
592
+ workspaceId,
593
+ coverage: input.workflowCoverage ? 'workflow'
594
+ : input.planningOrderCoverage ? 'planning-order'
595
+ : input.nestedCoverage ? 'nested' : 'combined',
596
+ candidates: candidates.length,
597
+ lock: lockCompletedAt - startedAt,
598
+ inventory: inventoryCompletedAt - lockCompletedAt,
599
+ revisions: revisionsCompletedAt - inventoryCompletedAt,
600
+ validation: validationCompletedAt - revisionsCompletedAt,
601
+ baselineWrites: baselineWritesCompletedAt - validationCompletedAt,
602
+ snapshot: snapshotCompletedAt - baselineWritesCompletedAt,
603
+ total: snapshotCompletedAt - startedAt,
604
+ }));
582
605
  return { highWatermark, insertedEntities: missing.length + legacyRelationshipSkews.length + legacyNestedDrifts.length,
583
606
  insertedJournalRows: rowCount, snapshot };
584
607
  })();
@@ -59,6 +59,7 @@ export declare function buildWorkspaceSyncV3CombinedRepairPage(input: {
59
59
  nestedCoverage?: boolean;
60
60
  workflowCoverage?: boolean;
61
61
  planningOrderCoverage?: boolean;
62
+ coveredLegacyWritesFenced?: boolean;
62
63
  }): WorkspaceSyncV3CombinedRepairPage;
63
64
  export declare function buildWorkspaceSyncV3NestedRepairPage(input: Omit<Parameters<typeof buildWorkspaceSyncV3CombinedRepairPage>[0], 'nestedCoverage' | 'workflowCoverage' | 'planningOrderCoverage'>): WorkspaceSyncV3CombinedRepairPage;
64
65
  export declare function buildWorkspaceSyncV3WorkflowRepairPage(input: Omit<Parameters<typeof buildWorkspaceSyncV3CombinedRepairPage>[0], 'nestedCoverage' | 'workflowCoverage' | 'planningOrderCoverage'>): WorkspaceSyncV3CombinedRepairPage;
@@ -14,7 +14,11 @@ const MAX_PAGE_SIZE = 100;
14
14
  const DEFAULT_MAX_RESPONSE_BYTES = 1024 * 1024;
15
15
  const DEFAULT_SNAPSHOT_TTL_MS = 60 * 60_000;
16
16
  const MAX_SNAPSHOT_TTL_MS = 24 * 60 * 60_000;
17
- const INSERT_BATCH_SIZE = 50;
17
+ const SQLITE_INSERT_BATCH_SIZE = 50;
18
+ // Postgres permits 65,535 bind parameters. Each materialized entry uses 15,
19
+ // so 1,000 entries keeps a wide safety margin while avoiding hundreds of
20
+ // synchronous worker/database round trips for large repair snapshots.
21
+ const POSTGRES_INSERT_BATCH_SIZE = 1000;
18
22
  export class WorkspaceSyncV3CombinedRepairSnapshotUnavailableError extends Error {
19
23
  code = 'WORKSPACE_SYNC_V3_COMBINED_REPAIR_SNAPSHOT_UNAVAILABLE';
20
24
  statusCode = 410;
@@ -170,8 +174,11 @@ function persistMaterialization(input) {
170
174
  coverage_version, coverage_digest, snapshot_sequence, entry_count, created_at, expires_at
171
175
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
172
176
  `).run(input.tenantId, input.workspaceId, snapshotId, WORKSPACE_SYNC_CONTRACT_VERSION, profile.coverageProfileId, profile.coverageVersion, profile.coverageDigest, snapshot.snapshotSequence, snapshot.entries.length, input.startedAt, provisionalExpiresAt);
173
- for (let offset = 0; offset < snapshot.entries.length; offset += INSERT_BATCH_SIZE) {
174
- const batch = snapshot.entries.slice(offset, offset + INSERT_BATCH_SIZE);
177
+ const insertBatchSize = db.provider === 'postgres'
178
+ ? POSTGRES_INSERT_BATCH_SIZE
179
+ : SQLITE_INSERT_BATCH_SIZE;
180
+ for (let offset = 0; offset < snapshot.entries.length; offset += insertBatchSize) {
181
+ const batch = snapshot.entries.slice(offset, offset + insertBatchSize);
175
182
  const values = batch.map(() => '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').join(', ');
176
183
  const args = [];
177
184
  batch.forEach((entry, index) => args.push(input.tenantId, input.workspaceId, snapshotId, offset + index, entry.projection, entry.domain, entry.entityId, entry.sequence, entry.mutationKind, entry.entityRevision, entry.lifecycleRevision, entry.canonicalFingerprint ?? null, entry.payloadFingerprint, canonicalWorkspaceSyncJson(entry.payload), entry.occurredAt));
@@ -414,18 +421,24 @@ export function buildWorkspaceSyncV3CombinedRepairPage(input) {
414
421
  if (reusable)
415
422
  return { metadata: reusable, reused: true,
416
423
  insertedBaselineEntities: 0, insertedBaselineJournalRows: 0 };
424
+ const repairStartedAt = Date.now();
417
425
  const prepared = coverage === 'workflow'
418
- ? prepareWorkspaceSyncV3WorkflowRepairSnapshot({ core: input.core, workspaceId, now })
426
+ ? prepareWorkspaceSyncV3WorkflowRepairSnapshot({ core: input.core, workspaceId, now,
427
+ coveredLegacyWritesFenced: input.coveredLegacyWritesFenced })
419
428
  : coverage === 'planning-order'
420
- ? prepareWorkspaceSyncV3PlanningOrderRepairSnapshot({ core: input.core, workspaceId, now })
429
+ ? prepareWorkspaceSyncV3PlanningOrderRepairSnapshot({ core: input.core, workspaceId, now,
430
+ coveredLegacyWritesFenced: input.coveredLegacyWritesFenced })
421
431
  : coverage === 'nested'
422
- ? prepareWorkspaceSyncV3NestedRepairSnapshot({ core: input.core, workspaceId, now })
423
- : prepareWorkspaceSyncV3CombinedRepairSnapshot({ core: input.core, workspaceId, now });
432
+ ? prepareWorkspaceSyncV3NestedRepairSnapshot({ core: input.core, workspaceId, now,
433
+ coveredLegacyWritesFenced: input.coveredLegacyWritesFenced })
434
+ : prepareWorkspaceSyncV3CombinedRepairSnapshot({ core: input.core, workspaceId, now,
435
+ coveredLegacyWritesFenced: input.coveredLegacyWritesFenced });
424
436
  // Production expiry is sampled at the end of persistence, after
425
437
  // every immutable payload row has committed in the same lock.
426
438
  const materializationStartedAt = input.now
427
439
  ? now
428
440
  : timestamp(new Date().toISOString(), 'Combined repair materialization start time');
441
+ const persistenceStartedAt = Date.now();
429
442
  const persisted = persistMaterialization({
430
443
  core: input.core, tenantId, workspaceId, startedAt: materializationStartedAt,
431
444
  fixedCompletionTime: input.now ? now : undefined, ttlMs,
@@ -434,6 +447,15 @@ export function buildWorkspaceSyncV3CombinedRepairPage(input) {
434
447
  insertedBaselineJournalRows: prepared.insertedJournalRows,
435
448
  coverage,
436
449
  });
450
+ console.info('[Taskforce Sync Repair Timing]', JSON.stringify({
451
+ workspaceId,
452
+ coverage,
453
+ entries: prepared.snapshot.entries.length,
454
+ insertedBaselineEntities: prepared.insertedEntities,
455
+ insertedBaselineJournalRows: prepared.insertedJournalRows,
456
+ persistence: Date.now() - persistenceStartedAt,
457
+ endToEnd: Date.now() - repairStartedAt,
458
+ }));
437
459
  return { metadata: persisted, reused: false,
438
460
  insertedBaselineEntities: prepared.insertedEntities,
439
461
  insertedBaselineJournalRows: prepared.insertedJournalRows };
@@ -18,8 +18,8 @@ import { projectWorkspaceSyncV3WorkstreamTaskOrderJournalRow } from './workspace
18
18
  import { serializeWorkstreamTaskOrderForSync } from './workstreamTaskOrderSync.js';
19
19
  import { serializeTaxonomyStateForSync } from './taxonomySyncPayload.js';
20
20
  import { fingerprintWorkspaceSyncPayload, WORKSPACE_SYNC_CONTRACT_VERSION } from './syncDurabilityStore.js';
21
+ import { workspaceSyncV3RepairEntityReadBatchSize } from './workspaceSyncV3RepairSqlBatching.js';
21
22
  const READ_PAGE_SIZE = 1000;
22
- const SQL_BATCH_SIZE = 400;
23
23
  const COMBINED_PROJECTION_RANK = new Map(WORKSPACE_SYNC_V3_COMBINED_COVERAGE_PROFILE.projections
24
24
  .map((projection, index) => [projection.projectionId, index]));
25
25
  const NESTED_PROJECTION_RANK = new Map(WORKSPACE_SYNC_V3_NESTED_COVERAGE_PROFILE.projections
@@ -53,6 +53,9 @@ function projectionRank(entry, coverage) {
53
53
  return rank;
54
54
  }
55
55
  function listTasks(core, workspaceId) {
56
+ if (typeof core.listTasksForAuthoritativeRepair === 'function') {
57
+ return core.listTasksForAuthoritativeRepair(workspaceId);
58
+ }
56
59
  const result = [];
57
60
  let cursor = null;
58
61
  for (;;) {
@@ -150,18 +153,27 @@ function listLiveEvents(core, workspaceId, liveEntityIds) {
150
153
  }
151
154
  /** Candidate v4 inventory. The coverage-v3 inventory above intentionally remains unchanged. */
152
155
  export function listWorkspaceSyncV3NestedRepairCandidates(core, workspaceId) {
156
+ const startedAt = Date.now();
153
157
  const tasks = listTasks(core, workspaceId);
158
+ const tasksCompletedAt = Date.now();
154
159
  const relationships = listRelationships(core, workspaceId);
160
+ const relationshipsCompletedAt = Date.now();
155
161
  const initiatives = listInitiatives(core, workspaceId);
162
+ const initiativesCompletedAt = Date.now();
156
163
  const workstreams = listWorkstreams(core, workspaceId);
164
+ const workstreamsCompletedAt = Date.now();
157
165
  const taxonomy = serializeTaxonomyStateForSync(core.getTaxonomyState(workspaceId));
166
+ const taxonomyCompletedAt = Date.now();
158
167
  const liveEntityIds = new Set([
159
168
  ...tasks.map((task) => `task\0${task.id}`),
160
169
  ...initiatives.map((candidate) => `initiative\0${candidate.entityId}`),
161
170
  ...workstreams.map((candidate) => `workstream\0${candidate.entityId}`),
162
171
  ]);
163
172
  const attachmentLinksByTaskId = core.listWorkspaceTaskAttachmentLinksForDurableSync(new Set(tasks.map((task) => task.id)), workspaceId);
164
- return [
173
+ const attachmentLinksCompletedAt = Date.now();
174
+ const events = listLiveEvents(core, workspaceId, liveEntityIds);
175
+ const eventsCompletedAt = Date.now();
176
+ const candidates = [
165
177
  ...tasks.map((task) => ({
166
178
  domain: 'task', entityId: task.id, payload: serializeTaskRootForSync(task),
167
179
  })),
@@ -171,10 +183,32 @@ export function listWorkspaceSyncV3NestedRepairCandidates(core, workspaceId) {
171
183
  ...workstreams,
172
184
  ...buildWorkspaceSyncV3NestedTaskRepairCandidates({
173
185
  tasks,
174
- events: listLiveEvents(core, workspaceId, liveEntityIds),
186
+ events,
175
187
  attachmentLinksByTaskId,
176
188
  }),
177
189
  ];
190
+ const completedAt = Date.now();
191
+ if (completedAt - startedAt >= 1_000) {
192
+ console.info('[Taskforce Sync Repair Inventory Timing]', JSON.stringify({
193
+ workspaceId,
194
+ tasks: tasks.length,
195
+ relationships: relationships.length,
196
+ initiatives: initiatives.length,
197
+ workstreams: workstreams.length,
198
+ events: events.length,
199
+ candidates: candidates.length,
200
+ listTasks: tasksCompletedAt - startedAt,
201
+ listRelationships: relationshipsCompletedAt - tasksCompletedAt,
202
+ listInitiatives: initiativesCompletedAt - relationshipsCompletedAt,
203
+ listWorkstreams: workstreamsCompletedAt - initiativesCompletedAt,
204
+ taxonomy: taxonomyCompletedAt - workstreamsCompletedAt,
205
+ attachmentLinks: attachmentLinksCompletedAt - taxonomyCompletedAt,
206
+ listEvents: eventsCompletedAt - attachmentLinksCompletedAt,
207
+ serializeCandidates: completedAt - eventsCompletedAt,
208
+ total: completedAt - startedAt,
209
+ }));
210
+ }
211
+ return candidates;
178
212
  }
179
213
  /** Candidate v5 inventory. Coverage-v3/v4 inventories intentionally remain unchanged. */
180
214
  export function listWorkspaceSyncV3WorkflowRepairCandidates(core, workspaceId) {
@@ -258,10 +292,11 @@ function journalRow(row) {
258
292
  }
259
293
  function readLastRows(db, tenantId, workspaceId, inventory) {
260
294
  const result = new Map();
295
+ const batchSize = workspaceSyncV3RepairEntityReadBatchSize(db);
261
296
  for (const domain of [...new Set(inventory.map((candidate) => candidate.domain))]) {
262
297
  const ids = inventory.filter((candidate) => candidate.domain === domain).map((candidate) => candidate.entityId);
263
- for (let offset = 0; offset < ids.length; offset += SQL_BATCH_SIZE) {
264
- const batch = ids.slice(offset, offset + SQL_BATCH_SIZE);
298
+ for (let offset = 0; offset < ids.length; offset += batchSize) {
299
+ const batch = ids.slice(offset, offset + batchSize);
265
300
  const placeholders = batch.map(() => '?').join(', ');
266
301
  const rows = db.prepare(`
267
302
  SELECT journal.*
@@ -284,8 +319,9 @@ function readLastRows(db, tenantId, workspaceId, inventory) {
284
319
  }
285
320
  function readTaskFullProvenanceRows(input) {
286
321
  const result = new Map();
287
- for (let offset = 0; offset < input.taskIds.length; offset += SQL_BATCH_SIZE) {
288
- const batch = input.taskIds.slice(offset, offset + SQL_BATCH_SIZE);
322
+ const batchSize = workspaceSyncV3RepairEntityReadBatchSize(input.db);
323
+ for (let offset = 0; offset < input.taskIds.length; offset += batchSize) {
324
+ const batch = input.taskIds.slice(offset, offset + batchSize);
289
325
  const placeholders = batch.map(() => '?').join(', ');
290
326
  const rows = input.db.prepare(`
291
327
  SELECT journal.*
@@ -427,7 +463,9 @@ export function buildWorkspaceSyncV3CombinedRepairSnapshotFromLockedCandidates(i
427
463
  const tenantId = input.core.getTenantId();
428
464
  const db = input.core.getDatabaseAdapter();
429
465
  const inventory = input.inventory;
466
+ const startedAt = Date.now();
430
467
  const rows = readLastRows(db, tenantId, workspaceId, inventory);
468
+ const rowsCompletedAt = Date.now();
431
469
  const missing = inventory.filter((candidate) => !rows.has(`${candidate.domain}\0${candidate.entityId}`))
432
470
  .map((candidate) => ({ domain: candidate.domain, entityId: candidate.entityId }));
433
471
  if (missing.length > 0)
@@ -436,7 +474,9 @@ export function buildWorkspaceSyncV3CombinedRepairSnapshotFromLockedCandidates(i
436
474
  .filter((candidate) => rows.get(`task\0${candidate.entityId}`)?.mutationKind === 'taxonomy-remap')
437
475
  .map((candidate) => candidate.entityId);
438
476
  const taskFullProvenance = readTaskFullProvenanceRows({ db, tenantId, workspaceId, taskIds: remappedTaskIds });
477
+ const provenanceCompletedAt = Date.now();
439
478
  const entries = inventory.map((candidate) => entry(candidate, rows.get(`${candidate.domain}\0${candidate.entityId}`), taskFullProvenance));
479
+ const entriesCompletedAt = Date.now();
440
480
  const coverage = input.workflowCoverage
441
481
  ? 'workflow'
442
482
  : input.planningOrderCoverage
@@ -451,12 +491,27 @@ export function buildWorkspaceSyncV3CombinedRepairSnapshotFromLockedCandidates(i
451
491
  : coverage === 'nested'
452
492
  ? validateWorkspaceSyncV3NestedCoverageFeedEntry
453
493
  : validateWorkspaceSyncV3CombinedFeedEntry);
494
+ const validationCompletedAt = Date.now();
454
495
  if (entries.some((candidate) => BigInt(candidate.sequence) > BigInt(snapshotSequence))) {
455
496
  throw new Error('Combined repair entry exceeds the locked snapshot sequence.');
456
497
  }
457
498
  entries.sort((left, right) => (projectionRank(left, coverage)
458
499
  - projectionRank(right, coverage))
459
500
  || compareCodeUnits(left.entityId, right.entityId));
501
+ const completedAt = Date.now();
502
+ if (completedAt - startedAt >= 1_000) {
503
+ console.info('[Taskforce Sync Repair Snapshot Timing]', JSON.stringify({
504
+ workspaceId,
505
+ coverage,
506
+ entries: entries.length,
507
+ readRows: rowsCompletedAt - startedAt,
508
+ readProvenance: provenanceCompletedAt - rowsCompletedAt,
509
+ projectEntries: entriesCompletedAt - provenanceCompletedAt,
510
+ validateEntries: validationCompletedAt - entriesCompletedAt,
511
+ sortEntries: completedAt - validationCompletedAt,
512
+ total: completedAt - startedAt,
513
+ }));
514
+ }
460
515
  return { snapshotSequence: BigInt(snapshotSequence).toString(10), entries };
461
516
  }
462
517
  export function buildWorkspaceSyncV3NestedRepairSnapshot(input) {
@@ -134,6 +134,7 @@ export type WorkspaceSyncV3TaskAttachmentLinksEnvelope = WorkspaceSyncV3Operatio
134
134
  baseLifecycleRevision: '0';
135
135
  payload: {
136
136
  links: WorkspaceSyncV3TaskAttachmentLink[];
137
+ cardCoverAssetId?: string | null;
137
138
  originActor?: WorkspaceSyncV3OriginActor;
138
139
  };
139
140
  }>;
@@ -0,0 +1,2 @@
1
+ import type { DatabaseAdapter } from '../../storage/databaseAdapter.js';
2
+ export declare function workspaceSyncV3RepairEntityReadBatchSize(db: DatabaseAdapter): number;
@@ -0,0 +1,10 @@
1
+ const SQLITE_ENTITY_READ_BATCH_SIZE = 400;
2
+ // These reads bind one parameter per entity plus a small fixed scope. Keeping
3
+ // the batch at 4,000 remains far below PostgreSQL's 65,535 parameter limit
4
+ // while avoiding dozens of synchronous database-worker round trips.
5
+ const POSTGRES_ENTITY_READ_BATCH_SIZE = 4000;
6
+ export function workspaceSyncV3RepairEntityReadBatchSize(db) {
7
+ return db.provider === 'postgres'
8
+ ? POSTGRES_ENTITY_READ_BATCH_SIZE
9
+ : SQLITE_ENTITY_READ_BATCH_SIZE;
10
+ }
@@ -2,7 +2,7 @@ import { serializeTaskForSync, type TaskSyncPayload } from '../taskSyncPayload.j
2
2
  import type { TaxonomyTaskRemapAuthoritativePayload } from './taxonomyCompositeMutationService.js';
3
3
  import type { WorkspaceSyncJournalRow } from './workspaceSyncJournalReader.js';
4
4
  export type WorkspaceSyncV3TaskRootMutationKind = 'create' | 'metadata-update' | 'archive' | 'unarchive' | 'complete' | 'reopen' | 'set-status' | 'taxonomy-remap' | 'delete';
5
- export type WorkspaceSyncV3TaskRootPayload = Omit<TaskSyncPayload, 'attachments' | 'checklistItems' | 'comments'>;
5
+ export type WorkspaceSyncV3TaskRootPayload = Omit<TaskSyncPayload, 'attachments' | 'cardCoverAssetId' | 'checklistItems' | 'comments'>;
6
6
  export type WorkspaceSyncV3TaskRootTombstone = {
7
7
  taskId: string;
8
8
  deletedAt: string;
@@ -20,7 +20,7 @@ const FULL_MUTATIONS = new Set([
20
20
  ]);
21
21
  const TASK_STATUSES = new Set(['task', 'on-hold', 'in-progress', 'review', 'done', 'cancelled']);
22
22
  function taskRootFromSerialized(task) {
23
- const { attachments: _attachments, checklistItems: _checklistItems, comments: _comments, ...root } = task;
23
+ const { attachments: _attachments, cardCoverAssetId: _cardCoverAssetId, checklistItems: _checklistItems, comments: _comments, ...root } = task;
24
24
  return root;
25
25
  }
26
26
  export function serializeTaskRootForSync(task) {
@@ -1,6 +1,6 @@
1
1
  import type { TaskforceCore, TaskforceSyncCapable, TaskRelationshipRecord } from '../core/Taskforce.js';
2
2
  import type { WorkspaceTaxonomyState } from '../shared/taxonomyState.js';
3
- import type { WorkspaceSyncAiProfileSnapshot, WorkspaceSyncAgentConversationSnapshot, WorkspaceSyncAgentRoleSnapshot, WorkspaceSyncAnnotatedAttachmentSessionSnapshot, WorkspaceSyncChange, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncDurableTaskRevision, WorkspaceSyncDurablePlanningRevision, WorkspaceSyncDurableTaxonomyRevision, WorkspaceSyncEntityEventSnapshot, WorkspaceSyncInitiativeSnapshot, WorkspaceSyncTaskEventSnapshot, WorkspaceSyncTaskSnapshot, WorkspaceSyncTaskforceAgentSnapshot, WorkspaceSyncTaskforceAgentSkillSnapshot, WorkspaceSyncWorkspaceMemberSnapshot, WorkspaceSyncWorkstreamSnapshot } from './workspaceSyncModel.js';
3
+ import type { WorkspaceSyncAiProfileSnapshot, WorkspaceSyncAgentConversationSnapshot, WorkspaceSyncAgentRoleSnapshot, WorkspaceSyncAnnotatedAttachmentSessionSnapshot, WorkspaceSyncChange, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncDurableTaskRevision, WorkspaceSyncDurablePlanningRevision, WorkspaceSyncDurableTaxonomyRevision, WorkspaceSyncEntityEventSnapshot, WorkspaceSyncInitiativeSnapshot, WorkspaceSyncPlanningIdentitySnapshot, WorkspaceSyncTaskEventSnapshot, WorkspaceSyncTaskSnapshot, WorkspaceSyncTaskforceAgentSnapshot, WorkspaceSyncTaskforceAgentSkillSnapshot, WorkspaceSyncWorkspaceMemberSnapshot, WorkspaceSyncWorkstreamSnapshot } from './workspaceSyncModel.js';
4
4
  import { createCollaborationSyncState } from './collaborationSyncState.js';
5
5
  import { createContentSyncState } from './contentSyncState.js';
6
6
  import type { WorkspaceAssetSyncLinkPayload } from './contentSyncPayload.js';
@@ -52,6 +52,11 @@ type PullFeedEntry = {
52
52
  watermark: string;
53
53
  sortId: string;
54
54
  workstream: WorkspaceSyncWorkstreamSnapshot;
55
+ } | {
56
+ kind: 'planning-identity';
57
+ watermark: string;
58
+ sortId: string;
59
+ identity: WorkspaceSyncPlanningIdentitySnapshot;
55
60
  } | {
56
61
  kind: 'ai-profile';
57
62
  watermark: string;