@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
@@ -26,6 +26,7 @@ export declare function enqueueLocalTaskAttachmentLinksMutation(input: {
26
26
  identity: WorkspaceSyncOperationIdentity;
27
27
  taskId: string;
28
28
  links: readonly WorkspaceSyncV3TaskAttachmentLink[];
29
+ cardCoverAssetId?: string | null;
29
30
  now: string;
30
31
  actorRef: string;
31
32
  requireV3CombinedOwnership?: boolean;
@@ -1,4 +1,4 @@
1
- import { projectResolvedTaskAttachmentLinksForDurableSync } from '../../core/TaskAttachmentLinksDurableMutationService.js';
1
+ import { projectResolvedTaskAttachmentLinksForDurableSync, resolveTaskCardCoverAssetIdForLinks, } from '../../core/TaskAttachmentLinksDurableMutationService.js';
2
2
  import { selectDurableTaskMetadataPatch } from '../../core/TaskDurableSyncMutationService.js';
3
3
  import { canonicalWorkspaceSyncJson } from './canonicalWorkspaceSyncJson.js';
4
4
  import { buildTaskAttachmentLinksActivityEventId } from './taskAttachmentLinksActivityIdentity.js';
@@ -111,7 +111,12 @@ export function enqueueLocalTaskAttachmentLinksMutation(input) {
111
111
  const result = input.store.enqueueLocalOperation({
112
112
  identity: input.identity,
113
113
  domain: 'task-attachment-links', entityId: input.taskId, mutationKind: 'replace',
114
- revisionSource: 'current', payload: withOriginActorPayload({ links }, input.actorRef),
114
+ revisionSource: 'current', payload: withOriginActorPayload({
115
+ links,
116
+ ...(Object.prototype.hasOwnProperty.call(input, 'cardCoverAssetId')
117
+ ? { cardCoverAssetId: input.cardCoverAssetId ?? null }
118
+ : {}),
119
+ }, input.actorRef),
115
120
  additionalTargets: [{
116
121
  domain: 'task', entityId: input.taskId, mutationKind: 'metadata-update',
117
122
  baseEntityRevision: rootRevision.entityRevision, baseLifecycleRevision: rootRevision.lifecycleRevision,
@@ -127,11 +132,18 @@ export function enqueueLocalTaskAttachmentLinksMutation(input) {
127
132
  if (existing.isArchived)
128
133
  throw new LocalAuthoritativeTaskAttachmentLinksError('Archived tasks are read-only.');
129
134
  const previousLinks = input.core.listTaskAttachmentLinksForDurableSync(input.taskId, input.identity.workspaceId);
135
+ const cardCoverAssetId = resolveTaskCardCoverAssetIdForLinks({
136
+ currentCardCoverAssetId: existing.cardCoverAssetId,
137
+ ...(Object.prototype.hasOwnProperty.call(input, 'cardCoverAssetId')
138
+ ? { requestedCardCoverAssetId: input.cardCoverAssetId }
139
+ : {}),
140
+ links,
141
+ });
130
142
  const currentMs = Date.parse(String(existing.updatedAt || existing.createdAt));
131
143
  const requestedMs = Date.parse(input.now);
132
144
  const updatedAt = Number.isFinite(currentMs) && currentMs >= requestedMs
133
145
  ? new Date(currentMs + 1).toISOString() : input.now;
134
- const task = input.core.updateTask(input.taskId, { attachments }, input.identity.workspaceId, {
146
+ const task = input.core.updateTask(input.taskId, { attachments, cardCoverAssetId }, input.identity.workspaceId, {
135
147
  persistBackup: false, emitRealtime: false, syncUpdatedAt: updatedAt,
136
148
  actorRef: input.actorRef, taskEventId: activityEventId, taskEventAction: 'task-attachments-changed',
137
149
  taskEventDetails: canonicalizeEntityActivityDetailsForSync({
@@ -300,8 +300,9 @@ function validateActivitySemantics(input) {
300
300
  const authoritativeAttachmentLinks = validateCanonicalPlanningAttachmentLinks(input.authoritative.attachments);
301
301
  const comparableExpectedDetails = normalizePlanningActivityDetailsForComparison(expectedDetails, input.record.mutationKind, authoritativeAttachmentLinks);
302
302
  const comparableAuthoritativeDetails = normalizePlanningActivityDetailsForComparison((input.activity.details || {}), input.record.mutationKind, authoritativeAttachmentLinks);
303
- if (fingerprintWorkspaceSyncPayload(comparableExpectedDetails)
304
- !== fingerprintWorkspaceSyncPayload(comparableAuthoritativeDetails)) {
303
+ if (!input.boundedAttachmentRebase
304
+ && fingerprintWorkspaceSyncPayload(comparableExpectedDetails)
305
+ !== fingerprintWorkspaceSyncPayload(comparableAuthoritativeDetails)) {
305
306
  throw new LocalAuthoritativeWorkstreamError('Authoritative workstream activity details differ from the captured mutation.');
306
307
  }
307
308
  }
@@ -500,6 +501,7 @@ export function applyAuthoritativeWorkstreamAndAck(input) {
500
501
  payload: payload,
501
502
  authoritative: workstream,
502
503
  activity,
504
+ boundedAttachmentRebase,
503
505
  });
504
506
  const revision = validateWorkstreamActivityAuthoritativeRevision({
505
507
  response: response,
@@ -152,6 +152,9 @@ export type WorkspaceSyncDomainCoexistenceTarget = {
152
152
  domain: string;
153
153
  entityId: string;
154
154
  };
155
+ export type WorkspaceSyncRecoveryBaselineTarget = WorkspaceSyncDomainCoexistenceTarget & {
156
+ required: boolean;
157
+ };
155
158
  export type WorkspaceSyncPlanningRevisionProjection = {
156
159
  entityId: string;
157
160
  coexistenceOwned: boolean;
@@ -374,6 +377,11 @@ export declare class WorkspaceSyncDurabilityStore {
374
377
  workspaceId: string;
375
378
  recoveryBatchId: string;
376
379
  }): WorkspaceSyncDomainCoexistenceTarget[];
380
+ listOutboxRecoveryBaselineTargets(input: {
381
+ tenantId: string;
382
+ workspaceId: string;
383
+ recoveryBatchId: string;
384
+ }): WorkspaceSyncRecoveryBaselineTarget[];
377
385
  prepareTaskRootOutboxRecoveryReplay(input: {
378
386
  tenantId: string;
379
387
  workspaceId: string;
@@ -1,8 +1,9 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
2
  import { normalizePlanningComments, } from '../planningCanonicalFields.js';
3
3
  import { canonicalWorkspaceSyncJson } from './canonicalWorkspaceSyncJson.js';
4
- import { buildTaskCreateActivityEventId } from './taskCreateActivityIdentity.js';
4
+ import { buildTaskAttachmentLinksActivityEventId } from './taskAttachmentLinksActivityIdentity.js';
5
5
  import { buildTaskCommentActivityEventId } from './taskCommentActivityIdentity.js';
6
+ import { buildTaskCreateActivityEventId } from './taskCreateActivityIdentity.js';
6
7
  import { buildTaskLifecycleActivityEventId, } from './taskLifecycleActivityIdentity.js';
7
8
  import { buildTaskMetadataActivityEventId } from './taskMetadataActivityIdentity.js';
8
9
  import { parseWorkspaceSyncV3OriginActor } from './workspaceSyncV3OriginActor.js';
@@ -251,7 +252,8 @@ export function hasCompletedRecoveryForSupersededEntityEvent(db, input) {
251
252
  if (entityType !== 'task')
252
253
  return false;
253
254
  const sources = db.prepare(`
254
- SELECT source.client_id, source.operation_id, source.mutation_kind
255
+ SELECT source.client_id, source.operation_id, source.domain, source.mutation_kind,
256
+ source.last_error_code
255
257
  FROM workspace_sync_outbox AS source
256
258
  INNER JOIN workspace_sync_outbox_recoveries AS recovery
257
259
  ON recovery.tenant_id = source.tenant_id
@@ -259,7 +261,7 @@ export function hasCompletedRecoveryForSupersededEntityEvent(db, input) {
259
261
  AND recovery.recovery_batch_id = source.recovery_batch_id
260
262
  WHERE source.tenant_id = ?
261
263
  AND source.workspace_id = ?
262
- AND source.domain = 'task'
264
+ AND source.domain IN ('task', 'task-attachment-links')
263
265
  AND source.entity_id = ?
264
266
  AND source.status = 'superseded'
265
267
  AND recovery.state = 'completed'
@@ -268,9 +270,19 @@ export function hasCompletedRecoveryForSupersededEntityEvent(db, input) {
268
270
  return sources.some((source) => {
269
271
  const clientId = String(source.client_id || '').trim();
270
272
  const operationId = String(source.operation_id || '').trim();
273
+ const domain = String(source.domain || '').trim();
271
274
  const mutationKind = String(source.mutation_kind || '').trim();
272
275
  if (!clientId || !operationId)
273
276
  return false;
277
+ if (domain === 'task-attachment-links'
278
+ && mutationKind === 'replace'
279
+ && String(source.last_error_code || '') === 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID') {
280
+ return buildTaskAttachmentLinksActivityEventId({
281
+ workspaceId, clientId, operationId, taskId: entityId,
282
+ }) === eventId;
283
+ }
284
+ if (domain !== 'task')
285
+ return false;
274
286
  if (mutationKind === 'create') {
275
287
  return buildTaskCreateActivityEventId({
276
288
  workspaceId, clientId, operationId, taskId: entityId,
@@ -1283,11 +1295,16 @@ export class WorkspaceSyncDurabilityStore {
1283
1295
  })();
1284
1296
  }
1285
1297
  listOutboxRecoveryTargets(input) {
1298
+ return this.listOutboxRecoveryBaselineTargets(input).map(({ domain, entityId }) => ({ domain, entityId }));
1299
+ }
1300
+ listOutboxRecoveryBaselineTargets(input) {
1286
1301
  const tenantId = requireText(input.tenantId, 'Tenant id');
1287
1302
  const workspaceId = requireText(input.workspaceId, 'Workspace id');
1288
1303
  const recoveryBatchId = requireText(input.recoveryBatchId, 'Recovery batch id');
1289
1304
  const rows = this.db.prepare(`
1290
- SELECT DISTINCT target.domain, target.entity_id
1305
+ SELECT target.domain, target.entity_id,
1306
+ source.dispatch_class, source.domain AS source_domain,
1307
+ source.mutation_kind AS source_mutation_kind
1291
1308
  FROM workspace_sync_outbox AS source
1292
1309
  INNER JOIN workspace_sync_outbox_targets AS target
1293
1310
  ON target.tenant_id = source.tenant_id
@@ -1298,14 +1315,52 @@ export class WorkspaceSyncDurabilityStore {
1298
1315
  AND source.recovery_batch_id = ?
1299
1316
  AND source.status = 'superseded'
1300
1317
  AND source.replay_of_operation_id IS NULL
1301
- ORDER BY target.domain, target.entity_id
1318
+ ORDER BY target.domain, target.entity_id, source.enqueue_sequence, source.operation_id
1302
1319
  `).all(tenantId, workspaceId, recoveryBatchId);
1303
- if (rows.length === 0)
1320
+ const targets = new Map();
1321
+ for (const row of rows) {
1322
+ const domain = requireText(row.domain, 'Recovery target domain');
1323
+ const entityId = requireText(row.entity_id, 'Recovery target entity id');
1324
+ const key = `${domain}\0${entityId}`;
1325
+ const legacyAdoptionEvent = domain === 'entity-event'
1326
+ && row.dispatch_class === 'legacy-adoption'
1327
+ && row.source_domain === 'entity-event'
1328
+ && row.source_mutation_kind === 'adopt-legacy-batch';
1329
+ const existing = targets.get(key);
1330
+ targets.set(key, {
1331
+ domain,
1332
+ entityId,
1333
+ required: Boolean(existing?.required || !legacyAdoptionEvent),
1334
+ });
1335
+ }
1336
+ const attachmentSources = this.db.prepare(`
1337
+ SELECT source.client_id, source.operation_id, source.entity_id
1338
+ FROM workspace_sync_outbox AS source
1339
+ WHERE source.tenant_id = ? AND source.workspace_id = ?
1340
+ AND source.recovery_batch_id = ?
1341
+ AND source.status = 'superseded'
1342
+ AND source.replay_of_operation_id IS NULL
1343
+ AND source.domain = 'task-attachment-links'
1344
+ AND source.mutation_kind = 'replace'
1345
+ AND source.last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
1346
+ `).all(tenantId, workspaceId, recoveryBatchId);
1347
+ for (const source of attachmentSources) {
1348
+ const target = {
1349
+ domain: 'entity-event',
1350
+ entityId: buildTaskAttachmentLinksActivityEventId({
1351
+ workspaceId,
1352
+ clientId: requireText(source.client_id, 'Recovery client id'),
1353
+ operationId: requireText(source.operation_id, 'Recovery operation id'),
1354
+ taskId: requireText(source.entity_id, 'Recovery task id'),
1355
+ }),
1356
+ required: true,
1357
+ };
1358
+ targets.set(`${target.domain}\0${target.entityId}`, target);
1359
+ }
1360
+ const deduplicated = [...targets.values()].sort((left, right) => (compareCodeUnits(left.domain, right.domain) || compareCodeUnits(left.entityId, right.entityId)));
1361
+ if (deduplicated.length === 0)
1304
1362
  throw new Error('Workspace sync recovery snapshot has no targets.');
1305
- return rows.map((row) => ({
1306
- domain: requireText(row.domain, 'Recovery target domain'),
1307
- entityId: requireText(row.entity_id, 'Recovery target entity id'),
1308
- }));
1363
+ return deduplicated;
1309
1364
  }
1310
1365
  prepareTaskRootOutboxRecoveryReplay(input) {
1311
1366
  const tenantId = requireText(input.tenantId, 'Tenant id');
@@ -1395,6 +1450,34 @@ export class WorkspaceSyncDurabilityStore {
1395
1450
  this.readEntityEventLegacyAdoptionRecoveryIntent(workspaceId, source);
1396
1451
  }
1397
1452
  }
1453
+ const recoverySourceKey = (source) => (`${requireText(source.client_id, 'Source client id')}\0${requireText(source.operation_id, 'Source operation id')}`);
1454
+ const legacyAdoptionEventsBySource = new Map();
1455
+ const latestLegacyAdoptionSourceByEvent = new Map();
1456
+ for (const source of recoverySources) {
1457
+ if (source.dispatch_class !== 'legacy-adoption'
1458
+ || source.domain !== 'entity-event'
1459
+ || source.mutation_kind !== 'adopt-legacy-batch')
1460
+ continue;
1461
+ const sourceKey = recoverySourceKey(source);
1462
+ const events = this.readEntityEventLegacyAdoptionRecoveryIntent(workspaceId, source).events;
1463
+ legacyAdoptionEventsBySource.set(sourceKey, events);
1464
+ for (const event of events)
1465
+ latestLegacyAdoptionSourceByEvent.set(event.id, sourceKey);
1466
+ }
1467
+ const legacyAdoptionReplayEventsBySource = new Map();
1468
+ for (const [sourceKey, events] of legacyAdoptionEventsBySource) {
1469
+ legacyAdoptionReplayEventsBySource.set(sourceKey, events.filter((event) => {
1470
+ if (latestLegacyAdoptionSourceByEvent.get(event.id) !== sourceKey)
1471
+ return false;
1472
+ const revision = this.readEntityRevisionCoordinates({
1473
+ tenantId,
1474
+ workspaceId,
1475
+ domain: 'entity-event',
1476
+ entityId: event.id,
1477
+ });
1478
+ return revision.entityRevision === '0';
1479
+ }));
1480
+ }
1398
1481
  const latestAttachmentSourceByEntity = new Map();
1399
1482
  for (const source of recoverySources) {
1400
1483
  if (source.domain === 'task-attachment-links' && source.mutation_kind === 'replace') {
@@ -1419,14 +1502,11 @@ export class WorkspaceSyncDurabilityStore {
1419
1502
  if (source.dispatch_class === 'legacy-adoption'
1420
1503
  && source.domain === 'entity-event'
1421
1504
  && source.mutation_kind === 'adopt-legacy-batch') {
1422
- // Legacy adoption migrates immutable historical activity; it is
1423
- // not a user-authored mutation that can be rebased. A targeted
1424
- // authoritative baseline proves that every event identity exists
1425
- // in cloud and installs the canonical cloud body locally. Trying
1426
- // to replay the older frozen body can only re-create the same
1427
- // identity/revision conflict forever, so the baseline itself
1428
- // satisfies this migration intent.
1429
- return false;
1505
+ // The authoritative baseline satisfies events already present in
1506
+ // cloud. A mixed legacy batch may also contain new immutable
1507
+ // events; replay only those still lacking authoritative ownership,
1508
+ // and let the latest overlapping batch own each event once.
1509
+ return (legacyAdoptionReplayEventsBySource.get(recoverySourceKey(source)) || []).length > 0;
1430
1510
  }
1431
1511
  if (Number(source.baseline_satisfaction_candidate || 0) !== 1) {
1432
1512
  return true;
@@ -1492,7 +1572,22 @@ export class WorkspaceSyncDurabilityStore {
1492
1572
  throw new Error('Workspace sync recovery replay currently supports task-root, task-comment append, '
1493
1573
  + 'task attachment-link, and legacy entity-event adoption intents only.');
1494
1574
  }
1495
- replayPayloads.set(`${requireText(source.client_id, 'Source client id')}\0${requireText(source.operation_id, 'Source operation id')}`, this.prepareRecoveryPayload(tenantId, workspaceId, source));
1575
+ const sourceKey = recoverySourceKey(source);
1576
+ if (supportedEntityEventAdoption) {
1577
+ const events = legacyAdoptionReplayEventsBySource.get(sourceKey) || [];
1578
+ if (events.length === 0)
1579
+ throw new Error('Legacy entity-event adoption recovery replay is empty.');
1580
+ const payloadJson = canonicalWorkspaceSyncJson({ events });
1581
+ replayPayloads.set(sourceKey, {
1582
+ payloadJson,
1583
+ payloadFingerprint: fingerprintWorkspaceSyncPayload({ events }),
1584
+ replayMutationKind: 'adopt-legacy-batch',
1585
+ workflowDependencyCoalesced: false,
1586
+ });
1587
+ }
1588
+ else {
1589
+ replayPayloads.set(sourceKey, this.prepareRecoveryPayload(tenantId, workspaceId, source));
1590
+ }
1496
1591
  }
1497
1592
  for (const source of sources) {
1498
1593
  const sourceClientId = requireText(source.client_id, 'Source client id');
@@ -1512,11 +1607,22 @@ export class WorkspaceSyncDurabilityStore {
1512
1607
  WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1513
1608
  ORDER BY domain, entity_id
1514
1609
  `).all(tenantId, workspaceId, sourceClientId, sourceOperationId);
1610
+ const legacyReplayEvents = source.dispatch_class === 'legacy-adoption'
1611
+ && source.domain === 'entity-event'
1612
+ && source.mutation_kind === 'adopt-legacy-batch'
1613
+ ? legacyAdoptionReplayEventsBySource.get(recoverySourceKey(source)) || []
1614
+ : null;
1615
+ const replayEventIds = legacyReplayEvents
1616
+ ? new Set(legacyReplayEvents.map((event) => event.id))
1617
+ : null;
1515
1618
  const replayTargets = source.domain === 'task-comments'
1516
1619
  && source.mutation_kind === 'append'
1517
1620
  ? sourceTargets.filter((target) => (target.domain === 'task-comments'
1518
1621
  && target.entity_id === source.entity_id))
1519
- : sourceTargets;
1622
+ : replayEventIds
1623
+ ? sourceTargets.filter((target) => (target.domain === 'entity-event'
1624
+ && replayEventIds.has(String(target.entity_id || ''))))
1625
+ : sourceTargets;
1520
1626
  if (replayTargets.length === 0)
1521
1627
  throw new Error('Workspace sync recovery source has no targets.');
1522
1628
  const targetRevisions = replayTargets.map((target) => ({
@@ -1528,7 +1634,9 @@ export class WorkspaceSyncDurabilityStore {
1528
1634
  entityId: requireText(target.entity_id, 'Target entity id'),
1529
1635
  }),
1530
1636
  }));
1531
- const primary = targetRevisions.find(({ target }) => (target.domain === source.domain && target.entity_id === source.entity_id));
1637
+ const replayEntityId = legacyReplayEvents?.[0]?.id
1638
+ || requireText(source.entity_id, 'Source entity id');
1639
+ const primary = targetRevisions.find(({ target }) => (target.domain === source.domain && target.entity_id === replayEntityId));
1532
1640
  if (!primary)
1533
1641
  throw new Error('Workspace sync recovery source primary target is missing.');
1534
1642
  const enqueueSequence = this.allocateOutboxEnqueueSequence(tenantId, workspaceId, now);
@@ -1545,7 +1653,7 @@ export class WorkspaceSyncDurabilityStore {
1545
1653
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, 0, ?, 1,
1546
1654
  ?, ?, ?, ?, ?, ?)
1547
1655
  ON CONFLICT (tenant_id, workspace_id, client_id, operation_id) DO NOTHING
1548
- `).run(tenantId, workspaceId, operationId, WORKSPACE_SYNC_CONTRACT_VERSION, sourceClientId, requireOutboxDispatchClass(source.dispatch_class), requireText(source.domain, 'Source domain'), requireText(source.entity_id, 'Source entity id'), replayPayload.replayMutationKind, Number(source.owns_task_metadata) === 1 ? 1 : 0, Number(source.owns_task_lifecycle) === 1 ? 1 : 0, Number(source.owns_task_assignee) === 1 ? 1 : 0, primary.revision.entityRevision, primary.revision.lifecycleRevision, replayPayload.payloadJson, replayPayload.payloadFingerprint, enqueueSequence, now, recoveryBatchId, sourceClientId, sourceOperationId, now, now);
1656
+ `).run(tenantId, workspaceId, operationId, WORKSPACE_SYNC_CONTRACT_VERSION, sourceClientId, requireOutboxDispatchClass(source.dispatch_class), requireText(source.domain, 'Source domain'), replayEntityId, replayPayload.replayMutationKind, Number(source.owns_task_metadata) === 1 ? 1 : 0, Number(source.owns_task_lifecycle) === 1 ? 1 : 0, Number(source.owns_task_assignee) === 1 ? 1 : 0, primary.revision.entityRevision, primary.revision.lifecycleRevision, replayPayload.payloadJson, replayPayload.payloadFingerprint, enqueueSequence, now, recoveryBatchId, sourceClientId, sourceOperationId, now, now);
1549
1657
  this.db.prepare(`
1550
1658
  UPDATE workspace_sync_outbox
1551
1659
  SET base_entity_revision = ?, base_lifecycle_revision = ?,
@@ -1577,7 +1685,7 @@ export class WorkspaceSyncDurabilityStore {
1577
1685
  `).get(tenantId, workspaceId, sourceClientId, operationId);
1578
1686
  if (!replayRow
1579
1687
  || replayRow.domain !== source.domain
1580
- || replayRow.entity_id !== source.entity_id
1688
+ || replayRow.entity_id !== replayEntityId
1581
1689
  || replayRow.mutation_kind !== replayPayload.replayMutationKind
1582
1690
  || replayRow.payload_fingerprint !== replayPayload.payloadFingerprint
1583
1691
  || replayRow.recovery_batch_id !== recoveryBatchId
@@ -1936,9 +2044,15 @@ export class WorkspaceSyncDurabilityStore {
1936
2044
  AND (
1937
2045
  (domain = 'task-attachment-links' AND mutation_kind = 'replace')
1938
2046
  OR (domain = 'task' AND mutation_kind = 'create')
2047
+ OR (domain = 'workstream' AND mutation_kind IN ('create', 'metadata-update'))
2048
+ OR (domain = 'initiative' AND mutation_kind IN ('create', 'metadata-update'))
1939
2049
  )
1940
2050
  AND status IN ('pending', 'in_flight')
1941
- AND last_error_code = 'TASK_ATTACHMENT_ASSET_NOT_READY'
2051
+ AND last_error_code IN (
2052
+ 'TASK_ATTACHMENT_ASSET_NOT_READY',
2053
+ 'WORKSTREAM_ATTACHMENT_NOT_READY',
2054
+ 'INITIATIVE_ATTACHMENT_NOT_READY'
2055
+ )
1942
2056
  ORDER BY enqueue_sequence
1943
2057
  `).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION);
1944
2058
  const assetIds = new Set();
@@ -1952,9 +2066,13 @@ export class WorkspaceSyncDurabilityStore {
1952
2066
  }
1953
2067
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
1954
2068
  continue;
1955
- const links = String(row.domain || '') === 'task'
2069
+ const domain = String(row.domain || '');
2070
+ const links = domain === 'task'
1956
2071
  ? payload.attachmentLinks
1957
- : payload.links;
2072
+ : domain === 'task-attachment-links'
2073
+ ? payload.links
2074
+ : payload.attachments
2075
+ ?? payload.patch?.attachments;
1958
2076
  if (!Array.isArray(links))
1959
2077
  continue;
1960
2078
  for (const link of links) {
@@ -3268,9 +3386,9 @@ export class WorkspaceSyncDurabilityStore {
3268
3386
  // Workstream/initiative attachment-only metadata patches are Partial<>
3269
3387
  // merges, not full snapshots. The cloud accepts those patches against its
3270
3388
  // current revision, so this bounded retry can preserve unrelated concurrent
3271
- // metadata. Other metadata patches remain exact-revision operations: this
3272
- // selector may retry one once, but the cloud will reject it again rather
3273
- // than allowing a stale title or structural edit to overwrite newer state.
3389
+ // metadata. It also covers an idempotent acknowledgement whose revision was
3390
+ // advanced by the prerequisite V2 content push before local installation.
3391
+ // Other metadata patches remain exact-revision operations and are excluded.
3274
3392
  requeueBoundedWorkstreamMetadataRevisionConflicts(input) {
3275
3393
  const tenantId = requireText(input.tenantId, 'Tenant id');
3276
3394
  const workspaceId = requireText(input.workspaceId, 'Workspace id');
@@ -3285,9 +3403,19 @@ export class WorkspaceSyncDurabilityStore {
3285
3403
  updated_at = ?
3286
3404
  WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3287
3405
  AND domain = 'workstream' AND mutation_kind = 'metadata-update'
3288
- AND attempt_count = 1
3289
3406
  AND workstream_metadata_revision_rebase_count < 1
3290
- AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT'
3407
+ AND (
3408
+ (attempt_count = 1 AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT')
3409
+ OR (
3410
+ last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
3411
+ AND last_error_message = 'Authoritative workstream revisions do not advance the captured bases exactly once.'
3412
+ AND json_type(payload_json, '$.patch.attachments') = 'array'
3413
+ AND NOT EXISTS (
3414
+ SELECT 1 FROM json_each(payload_json, '$.patch')
3415
+ WHERE key <> 'attachments'
3416
+ )
3417
+ )
3418
+ )
3291
3419
  `).run(now, now, tenantId, workspaceId);
3292
3420
  return Number(result.changes || 0);
3293
3421
  }
@@ -3305,9 +3433,19 @@ export class WorkspaceSyncDurabilityStore {
3305
3433
  updated_at = ?
3306
3434
  WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3307
3435
  AND domain = 'initiative' AND mutation_kind = 'metadata-update'
3308
- AND attempt_count = 1
3309
3436
  AND initiative_metadata_revision_rebase_count < 1
3310
- AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT'
3437
+ AND (
3438
+ (attempt_count = 1 AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT')
3439
+ OR (
3440
+ last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
3441
+ AND last_error_message = 'Authoritative initiative revisions do not advance the captured bases exactly once.'
3442
+ AND json_type(payload_json, '$.patch.attachments') = 'array'
3443
+ AND NOT EXISTS (
3444
+ SELECT 1 FROM json_each(payload_json, '$.patch')
3445
+ WHERE key <> 'attachments'
3446
+ )
3447
+ )
3448
+ )
3311
3449
  `).run(now, now, tenantId, workspaceId);
3312
3450
  return Number(result.changes || 0);
3313
3451
  }
@@ -99,6 +99,7 @@ export declare function runDurableTaskAttachmentLinksReplace(input: {
99
99
  operationId: string;
100
100
  taskId: string;
101
101
  links: readonly WorkspaceSyncV3TaskAttachmentLink[];
102
+ cardCoverAssetId?: string | null;
102
103
  baseEntityRevision: string | number;
103
104
  baseLifecycleRevision: string | number;
104
105
  now: string;
@@ -1,4 +1,4 @@
1
- import { projectResolvedTaskAttachmentLinksForDurableSync } from '../../core/TaskAttachmentLinksDurableMutationService.js';
1
+ import { projectResolvedTaskAttachmentLinksForDurableSync, resolveTaskCardCoverAssetIdForLinks, } from '../../core/TaskAttachmentLinksDurableMutationService.js';
2
2
  import { serializeTaskRootForSync } from './workspaceSyncV3TaskRootProjection.js';
3
3
  import { buildTaskAttachmentLinksActivityEventId } from './taskAttachmentLinksActivityIdentity.js';
4
4
  import { asWorkspaceSyncActivityRevision, buildTaskActivityJournalEntry, canonicalizeEntityActivityDetailsForSync, serializeTaskActivityForSync, } from './taskActivityJournal.js';
@@ -57,6 +57,9 @@ export function runDurableTaskAttachmentLinksReplace(input) {
57
57
  domain: 'task-attachment-links', entityId: input.taskId, mutationKind: 'replace',
58
58
  baseEntityRevision: String(input.baseEntityRevision),
59
59
  baseLifecycleRevision: String(input.baseLifecycleRevision), links: requestedLinks,
60
+ ...(Object.prototype.hasOwnProperty.call(input, 'cardCoverAssetId')
61
+ ? { cardCoverAssetId: input.cardCoverAssetId }
62
+ : {}),
60
63
  ...(input.originActorRef ? { originActorRef: input.originActorRef } : {}),
61
64
  },
62
65
  });
@@ -113,6 +116,19 @@ export function runDurableTaskAttachmentLinksReplace(input) {
113
116
  throw new DurableTaskAttachmentLinksMutationError('Task not found.', 'TASK_NOT_FOUND', 404);
114
117
  if (existing.isArchived)
115
118
  throw new DurableTaskAttachmentLinksMutationError('Archived tasks are read-only. Unarchive first to make changes.', 'TASK_ARCHIVED_READ_ONLY', 409);
119
+ let cardCoverAssetId;
120
+ try {
121
+ cardCoverAssetId = resolveTaskCardCoverAssetIdForLinks({
122
+ currentCardCoverAssetId: existing.cardCoverAssetId,
123
+ ...(Object.prototype.hasOwnProperty.call(input, 'cardCoverAssetId')
124
+ ? { requestedCardCoverAssetId: input.cardCoverAssetId }
125
+ : {}),
126
+ links,
127
+ });
128
+ }
129
+ catch (error) {
130
+ throw new DurableTaskAttachmentLinksMutationError(error instanceof Error ? error.message : 'Task card cover is invalid.', 'TASK_ATTACHMENT_LINKS_INVALID', Number(error?.statusCode) || 400);
131
+ }
116
132
  const previousLinks = input.core.listTaskAttachmentLinksForDurableSync(input.taskId, input.workspaceId);
117
133
  let attachments;
118
134
  try {
@@ -126,7 +142,7 @@ export function runDurableTaskAttachmentLinksReplace(input) {
126
142
  }
127
143
  const updatedAt = nextUpdatedAt(String(existing.updatedAt || existing.createdAt), input.now);
128
144
  const actorRef = resolveDeferredMutationActorRef(input);
129
- const task = input.core.updateTask(existing.id, { attachments }, input.workspaceId, {
145
+ const task = input.core.updateTask(existing.id, { attachments, cardCoverAssetId }, input.workspaceId, {
130
146
  persistBackup: false, emitRealtime: false, syncUpdatedAt: updatedAt,
131
147
  actorRef, skipTaskEvents: true,
132
148
  preferExplicitAttachmentRole: true,
@@ -57,8 +57,12 @@ export function preserveV3OwnedTaskMetadata(incoming, existing) {
57
57
  return merged;
58
58
  }
59
59
  export function preserveV3OwnedTaskRoot(incoming, existing) {
60
+ const hasCardCoverAssetId = Object.prototype.hasOwnProperty.call(incoming, 'cardCoverAssetId');
60
61
  return {
61
62
  ...serializeTaskRootForSync(existing),
63
+ cardCoverAssetId: hasCardCoverAssetId
64
+ ? incoming.cardCoverAssetId ?? null
65
+ : existing.cardCoverAssetId ?? null,
62
66
  comments: incoming.comments ?? existing.comments,
63
67
  attachments: incoming.attachments ?? existing.attachments,
64
68
  checklistItems: incoming.checklistItems ?? existing.checklistItems,
@@ -50,9 +50,6 @@ export function prepareWorkspaceSyncV2TaskCarriers(taskMutations, excludedProjec
50
50
  const commentsOwned = excludedProjections.has('task-comments/full-v1');
51
51
  const checklistOwned = excludedProjections.has('task-checklist/full-v1');
52
52
  const attachmentsOwned = excludedProjections.has('task-attachment-links/full-v1');
53
- if (commentsOwned && checklistOwned && attachmentsOwned) {
54
- return { upserts: [], deletes: taskMutations.deletes };
55
- }
56
53
  return {
57
54
  upserts: taskMutations.upserts.map(({ archived, task }) => {
58
55
  const existing = readExistingTask?.(String(task.id || '').trim());
@@ -104,13 +101,8 @@ export function partitionWorkspaceSyncV2LegacyChanges(changes, excludedProjectio
104
101
  const commentsOwned = excludedProjections.has('task-comments/full-v1');
105
102
  const checklistOwned = excludedProjections.has('task-checklist/full-v1');
106
103
  const attachmentsOwned = excludedProjections.has('task-attachment-links/full-v1');
107
- if (excludedProjections.has('task/root-v1') && commentsOwned && checklistOwned && attachmentsOwned) {
104
+ if (excludedProjections.has('task/root-v1'))
108
105
  suppress('task/root-v1');
109
- suppress('task-comments/full-v1');
110
- suppress('task-checklist/full-v1');
111
- suppress('task-attachment-links/full-v1');
112
- continue;
113
- }
114
106
  if (commentsOwned || checklistOwned || attachmentsOwned) {
115
107
  const task = { ...change.task };
116
108
  if (commentsOwned) {
@@ -261,6 +261,7 @@ export function createWorkspaceSyncV3CloudOperationHandlers() {
261
261
  ...withDeferredOriginActor(context, operation.payload.originActor),
262
262
  taskId: operation.entityId,
263
263
  links: operation.payload.links,
264
+ cardCoverAssetId: operation.payload.cardCoverAssetId,
264
265
  baseEntityRevision: operation.baseEntityRevision,
265
266
  baseLifecycleRevision: operation.baseLifecycleRevision,
266
267
  });
@@ -221,6 +221,12 @@ function peerReadinessError(response) {
221
221
  ? `Cloud combined workspace sync prerequisite is not ready (${firstMissing}).`
222
222
  : 'Cloud combined workspace sync capability attestation is missing or incompatible.';
223
223
  }
224
+ function repairResponseError(response, workspaceId, expected) {
225
+ const code = String(response.data?.code || '').trim();
226
+ return code && !['CURSOR_EXPIRED', 'COVERAGE_CHANGED', 'EXPLICIT_REPAIR'].includes(code)
227
+ ? invalidRepairResponseError(response, workspaceId, expected)
228
+ : peerReadinessError(response);
229
+ }
224
230
  function isUnavailablePersistedRepairCursor(response) {
225
231
  if (response.data?.success !== false)
226
232
  return false;
@@ -431,7 +437,7 @@ async function runWorkspaceSyncV3CombinedProjectionCycleInternal(input, staleRep
431
437
  }
432
438
  const repair = repairFromFeed(feed, input.workspaceId, coverage);
433
439
  if (!repair)
434
- return localFailure(projection, peerReadinessError(feed));
440
+ return localFailure(projection, repairResponseError(feed, input.workspaceId, coverage));
435
441
  const applied = await applySelectedProjection(dependencies, input.workspaceId, {
436
442
  kind: 'repair', epoch: projection.epoch, previousCursor, page: repair,
437
443
  ...(input.recoveryBatchId ? { recoveryBatchId: input.recoveryBatchId } : {}),
@@ -172,6 +172,15 @@ function isExactNestedProjectionReplay(input) {
172
172
  && input.current.lastSequence === input.entry.sequence
173
173
  && input.current.canonicalFingerprint === (input.entry.canonicalFingerprint || input.entry.payloadFingerprint);
174
174
  }
175
+ function isExactPlanningRootProjectionReplay(input) {
176
+ return (input.entry.projection === 'initiative/full-v1'
177
+ || input.entry.projection === 'workstream/full-v1')
178
+ && input.current.owned
179
+ && input.current.entityRevision === input.entry.entityRevision
180
+ && input.current.lifecycleRevision === input.entry.lifecycleRevision
181
+ && input.current.lastSequence === input.entry.sequence
182
+ && input.current.canonicalFingerprint === (input.entry.canonicalFingerprint || input.entry.payloadFingerprint);
183
+ }
175
184
  function applyEntries(input) {
176
185
  const store = new WorkspaceSyncDurabilityStore(input.core.getDatabaseAdapter());
177
186
  const workflowApply = prepareWorkspaceSyncV3WorkflowRuntimeApply({
@@ -198,6 +207,7 @@ function applyEntries(input) {
198
207
  workspaceId: input.workspaceId, domain: entry.domain, entityId: entry.entityId });
199
208
  const exactTaskRootReplay = isExactTaskRootProjectionReplay({ entry, current });
200
209
  const exactNestedReplay = isExactNestedProjectionReplay({ entry, current });
210
+ const exactPlanningRootReplay = isExactPlanningRootProjectionReplay({ entry, current });
201
211
  const applyEntry = () => {
202
212
  if (entry.projection === 'task/root-v1') {
203
213
  taskChange = applyTaskEntry({ core: input.core, workspaceId: input.workspaceId, entry });
@@ -259,7 +269,7 @@ function applyEntries(input) {
259
269
  }
260
270
  };
261
271
  let applied;
262
- if (exactTaskRootReplay || exactNestedReplay) {
272
+ if (exactTaskRootReplay || exactNestedReplay || exactPlanningRootReplay) {
263
273
  const result = applyEntry();
264
274
  applied = result?.state === 'deferred'
265
275
  ? { state: 'deferred' }
@@ -957,13 +967,15 @@ export function applyWorkspaceSyncV3CombinedRepairPage(input) {
957
967
  })();
958
968
  }
959
969
  const recoveryTargets = recoveryBatchId
960
- ? recoveryStore.listOutboxRecoveryTargets({ tenantId, workspaceId, recoveryBatchId })
970
+ ? recoveryStore.listOutboxRecoveryBaselineTargets({ tenantId, workspaceId, recoveryBatchId })
961
971
  : [];
962
972
  const selectRecoveryEntries = (entries) => {
963
973
  const byTarget = new Map(entries.map((entry) => [`${entry.domain}\0${entry.entityId}`, entry]));
964
974
  return recoveryTargets.map((target) => {
965
975
  const entry = byTarget.get(`${target.domain}\0${target.entityId}`);
966
976
  if (!entry) {
977
+ if (!target.required)
978
+ return null;
967
979
  throw staleRepairSnapshotError(target.domain, target.entityId);
968
980
  }
969
981
  const supportedTarget = ((target.domain === 'task' && entry.projection === 'task/root-v1')
@@ -975,7 +987,7 @@ export function applyWorkspaceSyncV3CombinedRepairPage(input) {
975
987
  throw new Error('Targeted workspace sync recovery does not support this projection.');
976
988
  }
977
989
  return entry;
978
- });
990
+ }).filter((entry) => entry !== null);
979
991
  };
980
992
  const projections = coveredProjections(input.page);
981
993
  let notices = notifications();