@taskforcehq/taskforce 0.3.330 → 0.3.333

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +7 -2
  2. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +2 -2
  3. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.js +2 -2
  4. package/dist/components/features/planning/PlanningModule.js +15 -3
  5. package/dist/components/features/planning/planningMetadata.d.ts +10 -0
  6. package/dist/components/features/planning/planningMetadata.js +8 -0
  7. package/dist/components/views/StandaloneLayout.js +35 -1
  8. package/dist/components/views/panels/PlanningDrawer.d.ts +2 -1
  9. package/dist/components/views/panels/PlanningDrawer.js +12 -8
  10. package/dist/config/envSchema.js +7 -0
  11. package/dist/core/McpTokenService.d.ts +2 -0
  12. package/dist/core/McpTokenService.js +13 -5
  13. package/dist/core/Taskforce.d.ts +11 -3
  14. package/dist/core/Taskforce.js +33 -3
  15. package/dist/core/types.d.ts +1 -0
  16. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +2 -0
  17. package/dist/documentReviews/DocumentReviewCommentStore.js +28 -9
  18. package/dist/mcp/httpProtocolRouting.d.ts +1 -0
  19. package/dist/mcp/httpProtocolRouting.js +8 -5
  20. package/dist/mcp/runtime.js +24 -5
  21. package/dist/mcp/taskPlanningRegistrar.js +2 -2
  22. package/dist/mcp/toolProfiles.js +1 -0
  23. package/dist/mcp/toolRegistry.js +8 -8
  24. package/dist/server/buildInfo.d.ts +1 -0
  25. package/dist/server/buildInfo.js +5 -1
  26. package/dist/server/cloudMcpCapacityTelemetry.d.ts +68 -0
  27. package/dist/server/cloudMcpCapacityTelemetry.js +221 -0
  28. package/dist/server/localWorkspaceSyncServerRuntime.js +13 -5
  29. package/dist/server/routes/admin.js +14 -2
  30. package/dist/server/routes/localService.js +1 -0
  31. package/dist/server/routes/shared.d.ts +3 -0
  32. package/dist/server/routes/shared.js +280 -240
  33. package/dist/server/routes/syncPushRoutes.d.ts +3 -0
  34. package/dist/server/routes/syncPushRoutes.js +8 -2
  35. package/dist/service/localServiceCli.d.ts +2 -2
  36. package/dist/service/localServiceCli.js +4 -3
  37. package/dist/services/executorPhaseAIssuerCore.d.ts +13 -0
  38. package/dist/services/executorPhaseAIssuerCore.js +107 -0
  39. package/dist/services/taskforceAgentMcpBridge.d.ts +1 -1
  40. package/dist/services/taskforceAgentMcpBridge.js +35 -5
  41. package/dist/shared/executorPhaseASchemaValidation.js +1 -1
  42. package/dist/storage/postgresAdapter.js +3 -1
  43. package/dist/storage/postgresRequestDiagnostics.d.ts +9 -1
  44. package/dist/storage/postgresRequestDiagnostics.js +15 -2
  45. package/dist/storage/postgresWorker.js +13 -3
  46. package/dist/storage/postgresWorkerProtocol.d.ts +6 -0
  47. package/dist/storage/workflowStore.d.ts +2 -0
  48. package/dist/storage/workflowStore.js +39 -2
  49. package/dist/sync/cloudSyncApi.js +4 -2
  50. package/dist/sync/collaborationSyncState.js +5 -0
  51. package/dist/sync/coordinator/localWorkspaceSyncRunner.d.ts +3 -1
  52. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +18 -0
  53. package/dist/sync/coordinator/localWorkspaceSyncRunnerState.d.ts +1 -0
  54. package/dist/sync/coordinator/localWorkspaceSyncRunnerState.js +7 -6
  55. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +2 -2
  56. package/dist/sync/engine/workspaceSyncEngineLocalApply.d.ts +1 -0
  57. package/dist/sync/engine/workspaceSyncEnginePushCompletion.d.ts +6 -0
  58. package/dist/sync/engine/workspaceSyncEnginePushCompletion.js +34 -1
  59. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +2 -2
  60. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +4 -1
  61. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.d.ts +1 -0
  62. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +55 -10
  63. package/dist/sync/syncApplyHandlers.d.ts +1 -0
  64. package/dist/sync/syncApplyHandlers.js +72 -15
  65. package/dist/sync/syncService.d.ts +1 -0
  66. package/dist/sync/syncService.js +5 -2
  67. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.d.ts +7 -7
  68. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.js +50 -13
  69. package/dist/sync/v3/localOutboxRecoveryPreparationService.js +8 -2
  70. package/dist/sync/v3/localTaskCreateOutboxService.d.ts +3 -1
  71. package/dist/sync/v3/localTaskCreateOutboxService.js +6 -1
  72. package/dist/sync/v3/localTaskDeleteOutboxService.js +1 -0
  73. package/dist/sync/v3/localTaskLifecycleOutboxService.d.ts +2 -1
  74. package/dist/sync/v3/localTaskLifecycleOutboxService.js +5 -1
  75. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -1
  76. package/dist/sync/v3/localTaskMetadataOutboxService.js +6 -2
  77. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -1
  78. package/dist/sync/v3/localTaskStatusOutboxService.js +20 -5
  79. package/dist/sync/v3/syncDurabilityStore.d.ts +2 -1
  80. package/dist/sync/v3/syncDurabilityStore.js +34 -6
  81. package/dist/sync/v3/workflowRuntimeMutationService.d.ts +5 -0
  82. package/dist/sync/v3/workflowRuntimeMutationService.js +124 -71
  83. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +25 -3
  84. package/dist/sync/v3/workspaceSyncV3Feed.d.ts +1 -0
  85. package/dist/sync/v3/workspaceSyncV3Feed.js +6 -1
  86. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +2 -0
  87. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +49 -18
  88. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.d.ts +1 -0
  89. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.js +50 -5
  90. package/dist/ui/assets/{AiIdentityRosterCard-OcSz1PkK.js → AiIdentityRosterCard-Cfvb9Nuv.js} +1 -1
  91. package/dist/ui/assets/{AiProfilesModule-ByhYW5Xi.js → AiProfilesModule-0oR70eFG.js} +1 -1
  92. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTM6nIg_.js → AnnotatedAttachmentWorkspace-C33iH_r_.js} +1 -1
  93. package/dist/ui/assets/{AssetTraySortControl-DabEG8L4.js → AssetTraySortControl-BAqxUtke.js} +1 -1
  94. package/dist/ui/assets/{ContextAttachmentManager-Coa8yVGM.js → ContextAttachmentManager-BudIbOR9.js} +2 -2
  95. package/dist/ui/assets/{DocumentWorkspace-BB0qhdix.js → DocumentWorkspace-A8dFQyId.js} +1 -1
  96. package/dist/ui/assets/{EntityActivityTimeline-CZ8x4UJE.js → EntityActivityTimeline-g7vtdsx1.js} +1 -1
  97. package/dist/ui/assets/PlanningModule-D-DeQs4F.js +1 -0
  98. package/dist/ui/assets/{PlansPage-D86pZSSl.js → PlansPage-BWmfGi0Z.js} +1 -1
  99. package/dist/ui/assets/{TaskContextUpload-BVIkU9yT.js → TaskContextUpload-BA_lQmS9.js} +1 -1
  100. package/dist/ui/assets/{TaskSettings-Cv1mNhv_.js → TaskSettings-DAlH4YIP.js} +1 -1
  101. package/dist/ui/assets/{TaskforceAgentsModule-gmH-PD2q.js → TaskforceAgentsModule-DmeCf6pJ.js} +2 -2
  102. package/dist/ui/assets/{WorkflowManagerModule-DbfnHJ-f.js → WorkflowManagerModule-p08eDOaa.js} +1 -1
  103. package/dist/ui/assets/index-GHJgcW3j.js +7 -0
  104. package/dist/ui/index.html +1 -1
  105. package/package.json +3 -1
  106. package/dist/ui/assets/PlanningModule-pRPjR7v5.js +0 -1
  107. package/dist/ui/assets/index-CTsDBaef.js +0 -7
@@ -19,10 +19,15 @@ export interface WorkspaceSyncEngineAppliedTaskUpsert {
19
19
  }
20
20
  export declare function workspaceSyncEngineExpectedTaskEchoIds(changes: readonly WorkspaceSyncChange[]): string[];
21
21
  export declare function parseWorkspaceSyncEngineAppliedTaskUpserts(value: unknown, expectedTaskIds: readonly string[]): WorkspaceSyncEngineAppliedTaskUpsert[];
22
+ export declare function parseWorkspaceSyncEnginePushTaskSettlement(value: unknown, expectedTaskIds: readonly string[]): {
23
+ appliedTaskUpserts: WorkspaceSyncEngineAppliedTaskUpsert[];
24
+ settledTaskDeleteIds: string[];
25
+ };
22
26
  export declare function createWorkspaceSyncEngineV2SettlementIdentityProjection(input: {
23
27
  operationIdentity: string;
24
28
  changes: readonly WorkspaceSyncChange[];
25
29
  appliedTaskUpserts: readonly WorkspaceSyncEngineAppliedTaskUpsert[];
30
+ settledTaskDeleteIds?: readonly string[];
26
31
  excludedProjections?: readonly WorkspaceSyncV2ProjectionExclusion[];
27
32
  }): {
28
33
  version: 1;
@@ -31,4 +36,5 @@ export declare function createWorkspaceSyncEngineV2SettlementIdentityProjection(
31
36
  archived: boolean;
32
37
  task: Record<string, unknown>;
33
38
  }>;
39
+ settledTaskDeleteIds?: string[];
34
40
  };
@@ -86,6 +86,28 @@ export function parseWorkspaceSyncEngineAppliedTaskUpserts(value, expectedTaskId
86
86
  }
87
87
  return normalized;
88
88
  }
89
+ export function parseWorkspaceSyncEnginePushTaskSettlement(value, expectedTaskIds) {
90
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
91
+ throw new TypeError('The V2 push authoritative echo response is invalid.');
92
+ }
93
+ const source = value;
94
+ const settledTaskDeleteIds = source.settledTaskDeleteIds === undefined
95
+ ? []
96
+ : source.settledTaskDeleteIds;
97
+ if (!Array.isArray(settledTaskDeleteIds)
98
+ || settledTaskDeleteIds.some((taskId) => (typeof taskId !== 'string'
99
+ || taskId.trim().length === 0
100
+ || taskId !== taskId.trim()))
101
+ || new Set(settledTaskDeleteIds).size !== settledTaskDeleteIds.length
102
+ || settledTaskDeleteIds.some((taskId) => !expectedTaskIds.includes(taskId))) {
103
+ throw new TypeError('The V2 push terminal task settlement identities are invalid.');
104
+ }
105
+ const settledIds = new Set(settledTaskDeleteIds);
106
+ return {
107
+ appliedTaskUpserts: parseWorkspaceSyncEngineAppliedTaskUpserts(value, expectedTaskIds.filter((taskId) => !settledIds.has(taskId))),
108
+ settledTaskDeleteIds: [...settledTaskDeleteIds],
109
+ };
110
+ }
89
111
  function taskSettlementProjection(task, preserveReferenceNumber) {
90
112
  const { updatedAt: _updatedAt, ...stableTask } = structuredClone(task);
91
113
  if (!preserveReferenceNumber)
@@ -136,7 +158,17 @@ export function createWorkspaceSyncEngineV2SettlementIdentityProjection(input) {
136
158
  if (!operationIdentity || operationIdentity !== input.operationIdentity) {
137
159
  throw new TypeError('The V2 push settlement operation identity is invalid.');
138
160
  }
139
- const expectedTaskUpserts = input.changes.filter((change) => change.op === 'upsert');
161
+ const allExpectedTaskUpserts = input.changes.filter((change) => change.op === 'upsert');
162
+ const expectedTaskIds = allExpectedTaskUpserts.map((change) => String(change.task.id || '').trim());
163
+ const settledTaskDeleteIds = [...(input.settledTaskDeleteIds || [])];
164
+ if ((settledTaskDeleteIds.length > 0
165
+ && !input.excludedProjections?.includes('task/root-v1'))
166
+ || settledTaskDeleteIds.some((taskId) => !expectedTaskIds.includes(taskId))
167
+ || new Set(settledTaskDeleteIds).size !== settledTaskDeleteIds.length) {
168
+ throw new TypeError('The V2 push terminal task settlement identities are invalid.');
169
+ }
170
+ const settledIds = new Set(settledTaskDeleteIds);
171
+ const expectedTaskUpserts = allExpectedTaskUpserts.filter((change) => !settledIds.has(String(change.task.id || '').trim()));
140
172
  if (expectedTaskUpserts.length !== input.appliedTaskUpserts.length) {
141
173
  throw new TypeError('The V2 push authoritative settlement projection is incomplete.');
142
174
  }
@@ -167,5 +199,6 @@ export function createWorkspaceSyncEngineV2SettlementIdentityProjection(input) {
167
199
  version: 1,
168
200
  operationIdentity,
169
201
  taskUpserts: expectedProjection,
202
+ ...(settledTaskDeleteIds.length > 0 ? { settledTaskDeleteIds } : {}),
170
203
  };
171
204
  }
@@ -2,7 +2,7 @@ import { workspaceSyncGatewayOperationClassFor, workspaceSyncGatewayTimeoutMs, }
2
2
  import { canonicalWorkspaceSyncJson } from '../v3/canonicalWorkspaceSyncJson.js';
3
3
  import { WORKSPACE_SYNC_V3_COMBINED_COVERAGE_SELECTOR, resolveWorkspaceSyncV3CombinedCoverageForExclusions, } from '../v3/workspaceSyncV3CombinedWireContract.js';
4
4
  import { WORKSPACE_SYNC_ENGINE_TRANSFER_COMPLETION_VERSION, } from './workspaceSyncEngineTransferCompletion.js';
5
- import { parseWorkspaceSyncEngineAppliedTaskUpserts, parseWorkspaceSyncEngineV2PushBoundary, workspaceSyncEngineExpectedTaskEchoIds, } from './workspaceSyncEnginePushCompletion.js';
5
+ import { parseWorkspaceSyncEnginePushTaskSettlement, parseWorkspaceSyncEngineV2PushBoundary, workspaceSyncEngineExpectedTaskEchoIds, } from './workspaceSyncEnginePushCompletion.js';
6
6
  const PERMIT_DEADLINE_SLACK_MS = 5_000;
7
7
  function isPlainObject(value) {
8
8
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
@@ -311,7 +311,7 @@ export function createServerWorkspaceSyncEngineRemoteServices(options) {
311
311
  requireAcceptedJsonObject: true,
312
312
  validateAcceptedData: (value) => {
313
313
  try {
314
- parseWorkspaceSyncEngineAppliedTaskUpserts(value, expectedTaskEchoIds);
314
+ parseWorkspaceSyncEnginePushTaskSettlement(value, expectedTaskEchoIds);
315
315
  return true;
316
316
  }
317
317
  catch {
@@ -623,7 +623,7 @@ export function createWorkspaceSyncEngineServerRunnerOperations(options) {
623
623
  acknowledgedOperationIdentities.length;
624
624
  }
625
625
  },
626
- onAcceptedBatchSettled: async ({ operationIdentity, settlementIdentityFingerprint, appliedTaskUpserts, transferReceipt, pushBoundary, excludedProjections, }) => {
626
+ onAcceptedBatchSettled: async ({ operationIdentity, settlementIdentityFingerprint, appliedTaskUpserts, settledTaskDeleteIds, transferReceipt, pushBoundary, excludedProjections, }) => {
627
627
  const pendingV2Push = checkpoint.pendingV2Push;
628
628
  if (!pendingV2Push
629
629
  || pendingV2Push.settlementVersion !== 1
@@ -661,6 +661,9 @@ export function createWorkspaceSyncEngineServerRunnerOperations(options) {
661
661
  operationIdentity,
662
662
  settlementIdentityFingerprint,
663
663
  appliedTaskUpserts: structuredClone(appliedTaskUpserts),
664
+ ...(settledTaskDeleteIds.length > 0 ? {
665
+ settledTaskDeleteIds: [...settledTaskDeleteIds],
666
+ } : {}),
664
667
  },
665
668
  ],
666
669
  },
@@ -17,6 +17,7 @@ export interface WorkspaceSyncV2DispatchOptions {
17
17
  coexistenceDirection?: TaskMetadataCoexistenceDirection;
18
18
  assertedActiveV3TaskMetadataIds?: ReadonlySet<string>;
19
19
  assertedActiveV3TaskDeleteIds?: ReadonlySet<string>;
20
+ candidateTerminalV3TaskDeleteIds?: ReadonlySet<string>;
20
21
  assertedActiveV3TaskLifecycleIds?: ReadonlySet<string>;
21
22
  assertedActiveV3TaskReopenAssigneeIds?: ReadonlySet<string>;
22
23
  assertedActiveV3Taxonomy?: boolean;
@@ -156,11 +156,40 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
156
156
  throw createSyncRouteError('Cloud pull contained content from an excluded v2 projection.', { excludedProjections: Array.from(v2ExcludedProjections) }, { code: 'SYNC_V2_EXCLUDED_PROJECTION_PRESENT', statusCode: 409 });
157
157
  }
158
158
  const taskMutationsForApply = prepareWorkspaceSyncV2TaskCarriers(applyModel.taskMutations, v2ExcludedProjections, (taskId) => core.getTask(taskId, workspaceId));
159
- if (v2ExcludedProjections.has('task/root-v1')) {
159
+ if (v2ExcludedProjections.has('task/root-v1')
160
+ && !(options.coexistenceDirection === 'cloud-push'
161
+ && options.candidateTerminalV3TaskDeleteIds?.size)) {
160
162
  assertWorkspaceSyncV2TaskCarrierRootsExist(taskMutationsForApply, (taskId) => Boolean(core.getTask(taskId, workspaceId)));
161
163
  }
162
164
  return { applyModel, taskMutationsForApply, v2ExcludedProjections };
163
165
  };
166
+ const resolveTaskCarrierSettlement = (workspaceId, taskMutations, ownership, options) => {
167
+ const candidates = options.coexistenceDirection === 'cloud-push'
168
+ ? options.candidateTerminalV3TaskDeleteIds
169
+ : undefined;
170
+ if (!candidates?.size) {
171
+ return { taskMutationsForSettlement: taskMutations, settledTaskDeleteIds: [] };
172
+ }
173
+ const terminalOwnership = new Set(ownership
174
+ .filter((entry) => entry.activeDeleteOperation === true || entry.deleteOwned === true)
175
+ .map((entry) => entry.taskId));
176
+ const settledTaskDeleteIds = candidates
177
+ ? taskMutations.upserts
178
+ .map((mutation) => mutation.task.id)
179
+ .filter((taskId) => (candidates.has(taskId)
180
+ && terminalOwnership.has(taskId)
181
+ && !core.getTask(taskId, workspaceId)))
182
+ : [];
183
+ const settledIds = new Set(settledTaskDeleteIds);
184
+ const taskMutationsForSettlement = settledIds.size > 0
185
+ ? {
186
+ ...taskMutations,
187
+ upserts: taskMutations.upserts.filter((mutation) => !settledIds.has(mutation.task.id)),
188
+ }
189
+ : taskMutations;
190
+ assertWorkspaceSyncV2TaskCarrierRootsExist(taskMutationsForSettlement, (taskId) => Boolean(core.getTask(taskId, workspaceId)));
191
+ return { taskMutationsForSettlement, settledTaskDeleteIds };
192
+ };
164
193
  const prepareTaskSnapshotForDispatch = (workspaceId, taskMutationsForApply, v2ExcludedProjections, options) => {
165
194
  const prepared = prepareTaskSyncSnapshotApply({
166
195
  core,
@@ -317,6 +346,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
317
346
  taskforceAgentPushActorUserId: String(options.actorUserId || '').trim() || null,
318
347
  taskforceAgentPushAccessPreflighted: true,
319
348
  } : {}),
349
+ coexistenceDirection: options.coexistenceDirection,
320
350
  recordSyncDiagnostic,
321
351
  createSyncRouteError,
322
352
  });
@@ -334,14 +364,15 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
334
364
  if (options.coexistenceDirection === 'cloud-push'
335
365
  && taskMutationsForApply.upserts.some((mutation) => mutation.durableRevision))
336
366
  throw new Error('Task durable revisions are only accepted from the cloud pull feed.');
367
+ const { taskMutationsForSettlement, settledTaskDeleteIds, } = resolveTaskCarrierSettlement(workspaceId, taskMutationsForApply, ownership, options);
337
368
  const durablePull = options.coexistenceDirection === 'local-pull'
338
369
  ? prepareDurableTaskPullMutations({
339
370
  store: durabilityStore,
340
371
  tenantId: core.getTenantId(),
341
372
  workspaceId,
342
- taskMutations: taskMutationsForApply,
373
+ taskMutations: taskMutationsForSettlement,
343
374
  })
344
- : { taskMutations: taskMutationsForApply, pendingRevisions: [] };
375
+ : { taskMutations: taskMutationsForSettlement, pendingRevisions: [] };
345
376
  const prepared = prepareTaskSyncSnapshotApply({
346
377
  core,
347
378
  workspaceId,
@@ -360,7 +391,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
360
391
  ownership: durabilityStore.listTaskTaxonomyProjectionOwnership({
361
392
  tenantId: core.getTenantId(),
362
393
  workspaceId,
363
- taskIds: taskMutationsForApply.upserts.map((mutation) => mutation.task.id),
394
+ taskIds: taskMutationsForSettlement.upserts.map((mutation) => mutation.task.id),
364
395
  }),
365
396
  }),
366
397
  v3OwnedTaskDeleteIds: resolveV3OwnedTaskDeleteIds({
@@ -395,7 +426,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
395
426
  revisions: durablePull.pendingRevisions,
396
427
  now: revisionNow,
397
428
  });
398
- return { prepared, result };
429
+ return { prepared, result, settledTaskDeleteIds };
399
430
  },
400
431
  });
401
432
  })()
@@ -406,6 +437,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
406
437
  workspaceId,
407
438
  prepared: preparedTaskSnapshot,
408
439
  }),
440
+ settledTaskDeleteIds: [],
409
441
  };
410
442
  const result = finalPreparedTaskSnapshot.result;
411
443
  const committedTaskIds = finalPreparedTaskSnapshot.result.changedTaskIds || [];
@@ -479,7 +511,14 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
479
511
  recordSyncDiagnostic,
480
512
  createSyncRouteError,
481
513
  });
482
- return { result, deleteCount, emittedEventIds: Array.from(emittedEventIds) };
514
+ return {
515
+ result,
516
+ deleteCount,
517
+ emittedEventIds: Array.from(emittedEventIds),
518
+ ...(finalPreparedTaskSnapshot.settledTaskDeleteIds.length > 0 ? {
519
+ settledTaskDeleteIds: finalPreparedTaskSnapshot.settledTaskDeleteIds,
520
+ } : {}),
521
+ };
483
522
  }
484
523
  finally {
485
524
  unsubscribeRealtime();
@@ -777,6 +816,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
777
816
  taskforceAgentPushActorUserId: String(options.actorUserId || '').trim() || null,
778
817
  taskforceAgentPushAccessPreflighted: true,
779
818
  } : {}),
819
+ coexistenceDirection: options.coexistenceDirection,
780
820
  recordSyncDiagnostic,
781
821
  createSyncRouteError,
782
822
  });
@@ -794,14 +834,15 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
794
834
  if (options.coexistenceDirection === 'cloud-push'
795
835
  && taskMutationsForApply.upserts.some((mutation) => mutation.durableRevision))
796
836
  throw new Error('Task durable revisions are only accepted from the cloud pull feed.');
837
+ const { taskMutationsForSettlement, settledTaskDeleteIds, } = resolveTaskCarrierSettlement(workspaceId, taskMutationsForApply, ownership, options);
797
838
  const durablePull = options.coexistenceDirection === 'local-pull'
798
839
  ? prepareDurableTaskPullMutations({
799
840
  store: durabilityStore,
800
841
  tenantId: core.getTenantId(),
801
842
  workspaceId,
802
- taskMutations: taskMutationsForApply,
843
+ taskMutations: taskMutationsForSettlement,
803
844
  })
804
- : { taskMutations: taskMutationsForApply, pendingRevisions: [] };
845
+ : { taskMutations: taskMutationsForSettlement, pendingRevisions: [] };
805
846
  const prepared = prepareTaskSyncSnapshotApply({
806
847
  core,
807
848
  workspaceId,
@@ -820,7 +861,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
820
861
  ownership: durabilityStore.listTaskTaxonomyProjectionOwnership({
821
862
  tenantId: core.getTenantId(),
822
863
  workspaceId,
823
- taskIds: taskMutationsForApply.upserts.map((mutation) => mutation.task.id),
864
+ taskIds: taskMutationsForSettlement.upserts.map((mutation) => mutation.task.id),
824
865
  }),
825
866
  }),
826
867
  v3OwnedTaskDeleteIds: resolveV3OwnedTaskDeleteIds({
@@ -855,7 +896,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
855
896
  revisions: durablePull.pendingRevisions,
856
897
  now: revisionNow,
857
898
  });
858
- return { prepared, result };
899
+ return { prepared, result, settledTaskDeleteIds };
859
900
  },
860
901
  });
861
902
  })()
@@ -867,6 +908,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
867
908
  prepared: preparedTaskSnapshot,
868
909
  deferSideEffects: true,
869
910
  }),
911
+ settledTaskDeleteIds: [],
870
912
  };
871
913
  const protectedTaskCount = finalPreparedTaskSnapshot.prepared.metadataPreservedTaskIds.length;
872
914
  const protectedLifecycleTaskCount = finalPreparedTaskSnapshot.prepared.lifecyclePreservedTaskIds.length;
@@ -932,6 +974,9 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
932
974
  result: finalPreparedTaskSnapshot.result,
933
975
  deleteCount,
934
976
  emittedEventIds: [],
977
+ ...(finalPreparedTaskSnapshot.settledTaskDeleteIds.length > 0 ? {
978
+ settledTaskDeleteIds: finalPreparedTaskSnapshot.settledTaskDeleteIds,
979
+ } : {}),
935
980
  };
936
981
  const beforeCommitResult = options.transactionHooks?.beforeCommit?.(result);
937
982
  assertSynchronousTransactionResult(beforeCommitResult);
@@ -257,6 +257,7 @@ export declare function applyCollaborationSyncChanges(input: {
257
257
  incomingAgentConversations?: Map<string, WorkspaceSyncAgentConversationSnapshot>;
258
258
  taskforceAgentPushActorUserId?: string | null;
259
259
  taskforceAgentPushAccessPreflighted?: boolean;
260
+ coexistenceDirection?: 'local-pull' | 'cloud-push';
260
261
  recordSyncDiagnostic: RecordSyncDiagnostic;
261
262
  createSyncRouteError: CreateSyncRouteError;
262
263
  }): void;
@@ -1053,7 +1053,7 @@ export async function applyContentSyncChanges(input) {
1053
1053
  }
1054
1054
  }
1055
1055
  export function applyCollaborationSyncChanges(input) {
1056
- const { core, workspaceId, incomingDocumentReviewSessions, incomingDocumentReviewSessionDeletes, incomingDocumentReviewComments, incomingDocumentReviewCommentDeletes, incomingAnnotatedAttachmentSessions, incomingAnnotatedAttachmentSessionDeletes, incomingTaskforceAgents = new Map(), incomingAgentRoles = new Map(), incomingTaskforceAgentSkills = new Map(), incomingAgentConversations = new Map(), taskforceAgentPushActorUserId, taskforceAgentPushAccessPreflighted = false, recordSyncDiagnostic, createSyncRouteError, } = input;
1056
+ const { core, workspaceId, incomingDocumentReviewSessions, incomingDocumentReviewSessionDeletes, incomingDocumentReviewComments, incomingDocumentReviewCommentDeletes, incomingAnnotatedAttachmentSessions, incomingAnnotatedAttachmentSessionDeletes, incomingTaskforceAgents = new Map(), incomingAgentRoles = new Map(), incomingTaskforceAgentSkills = new Map(), incomingAgentConversations = new Map(), taskforceAgentPushActorUserId, taskforceAgentPushAccessPreflighted = false, coexistenceDirection, recordSyncDiagnostic, createSyncRouteError, } = input;
1057
1057
  if (taskforceAgentPushActorUserId !== undefined && !taskforceAgentPushAccessPreflighted) {
1058
1058
  assertTaskforceAgentSyncPushAccess({
1059
1059
  agents: incomingTaskforceAgents.values(),
@@ -1141,6 +1141,48 @@ export function applyCollaborationSyncChanges(input) {
1141
1141
  .digest('hex')
1142
1142
  : '';
1143
1143
  const metadataCompare = resolveSyncConflictOrdering(comment.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
1144
+ const parentCommentConflict = Boolean(existing
1145
+ && comment.parentCommentId !== undefined
1146
+ && comment.parentCommentId !== existing.parentCommentId);
1147
+ const nonThreadFieldsMatch = Boolean(existing
1148
+ && comment.sessionId === existing.sessionId
1149
+ && comment.body === existing.body
1150
+ && JSON.stringify(comment.anchor ?? null) === JSON.stringify(existing.anchor ?? null)
1151
+ && comment.order === existing.order
1152
+ && (comment.authorActorId ?? null) === (existing.authorActorId ?? null)
1153
+ && (comment.updatedByActorId ?? null) === (existing.updatedByActorId ?? null)
1154
+ && (comment.deletedByActorId ?? null) === (existing.deletedByActorId ?? null)
1155
+ && comment.createdAt === existing.createdAt
1156
+ && comment.updatedAt === existing.updatedAt
1157
+ && (comment.deletedAt ?? null) === (existing.deletedAt ?? null));
1158
+ const incomingIsLegacyProjectedRoot = Boolean(nonThreadFieldsMatch
1159
+ && comment.parentCommentId === null
1160
+ && comment.threadStatus === 'open'
1161
+ && comment.threadStatusUpdatedAt === comment.createdAt
1162
+ && !comment.resolvedAt
1163
+ && !comment.resolvedByActorId);
1164
+ const existingIsLegacyProjectedRoot = Boolean(nonThreadFieldsMatch
1165
+ && existing
1166
+ && existing.parentCommentId === null
1167
+ && existing.threadStatus === 'open'
1168
+ && existing.threadStatusUpdatedAt === existing.createdAt
1169
+ && !existing.resolvedAt
1170
+ && !existing.resolvedByActorId);
1171
+ const preserveLocalReplyParent = parentCommentConflict
1172
+ && coexistenceDirection === 'local-pull'
1173
+ && Boolean(existing?.parentCommentId)
1174
+ && incomingIsLegacyProjectedRoot;
1175
+ const reconcileAuthoritativeParent = parentCommentConflict
1176
+ && coexistenceDirection === 'local-pull'
1177
+ && !preserveLocalReplyParent;
1178
+ const reconcileLegacyCloudRoot = parentCommentConflict
1179
+ && coexistenceDirection === 'cloud-push'
1180
+ && existingIsLegacyProjectedRoot
1181
+ && Boolean(comment.parentCommentId);
1182
+ const applyIncomingMetadata = metadataCompare >= 0
1183
+ && !(parentCommentConflict
1184
+ && coexistenceDirection === 'cloud-push'
1185
+ && !reconcileLegacyCloudRoot);
1144
1186
  const incomingLifecycleTieBreak = createHash('sha256')
1145
1187
  .update(JSON.stringify({
1146
1188
  threadStatus: comment.threadStatus,
@@ -1160,28 +1202,43 @@ export function applyCollaborationSyncChanges(input) {
1160
1202
  const lifecycleCompare = comment.threadStatusUpdatedAt === undefined
1161
1203
  ? -1
1162
1204
  : resolveSyncConflictOrdering(String(comment.threadStatusUpdatedAt || '').trim(), String(existing?.threadStatusUpdatedAt || '').trim(), incomingLifecycleTieBreak, existingLifecycleTieBreak);
1163
- if (metadataCompare < 0 && lifecycleCompare < 0)
1205
+ if (metadataCompare < 0
1206
+ && lifecycleCompare < 0
1207
+ && !reconcileAuthoritativeParent
1208
+ && !reconcileLegacyCloudRoot)
1164
1209
  continue;
1165
- core.upsertDocumentReviewCommentForSync({
1210
+ const commentForSync = {
1166
1211
  id: comment.id,
1167
1212
  tenantId: core.getTenantId(),
1168
1213
  workspaceId,
1169
- sessionId: metadataCompare >= 0 ? comment.sessionId : existing.sessionId,
1170
- body: metadataCompare >= 0 ? comment.body : existing.body,
1171
- anchor: (metadataCompare >= 0 ? comment.anchor : existing.anchor),
1172
- order: metadataCompare >= 0 ? comment.order : existing.order,
1173
- authorActorId: metadataCompare >= 0 ? comment.authorActorId : existing.authorActorId,
1174
- updatedByActorId: metadataCompare >= 0 ? comment.updatedByActorId : existing.updatedByActorId,
1175
- deletedByActorId: metadataCompare >= 0 ? comment.deletedByActorId : existing.deletedByActorId,
1176
- parentCommentId: metadataCompare >= 0 ? comment.parentCommentId : existing.parentCommentId,
1214
+ sessionId: applyIncomingMetadata ? comment.sessionId : existing.sessionId,
1215
+ body: applyIncomingMetadata ? comment.body : existing.body,
1216
+ anchor: (applyIncomingMetadata ? comment.anchor : existing.anchor),
1217
+ order: applyIncomingMetadata ? comment.order : existing.order,
1218
+ authorActorId: applyIncomingMetadata ? comment.authorActorId : existing.authorActorId,
1219
+ updatedByActorId: applyIncomingMetadata ? comment.updatedByActorId : existing.updatedByActorId,
1220
+ deletedByActorId: applyIncomingMetadata ? comment.deletedByActorId : existing.deletedByActorId,
1221
+ parentCommentId: preserveLocalReplyParent
1222
+ ? existing.parentCommentId
1223
+ : reconcileAuthoritativeParent || reconcileLegacyCloudRoot || applyIncomingMetadata
1224
+ ? comment.parentCommentId
1225
+ : existing.parentCommentId,
1177
1226
  threadStatus: lifecycleCompare >= 0 ? comment.threadStatus : undefined,
1178
1227
  threadStatusUpdatedAt: lifecycleCompare >= 0 ? comment.threadStatusUpdatedAt : undefined,
1179
1228
  resolvedAt: lifecycleCompare >= 0 ? comment.resolvedAt : undefined,
1180
1229
  resolvedByActorId: lifecycleCompare >= 0 ? comment.resolvedByActorId : undefined,
1181
- createdAt: metadataCompare >= 0 ? comment.createdAt : existing.createdAt,
1182
- updatedAt: metadataCompare >= 0 ? comment.updatedAt : existing.updatedAt,
1183
- deletedAt: metadataCompare >= 0 ? comment.deletedAt : existing.deletedAt,
1184
- });
1230
+ createdAt: applyIncomingMetadata ? comment.createdAt : existing.createdAt,
1231
+ updatedAt: applyIncomingMetadata ? comment.updatedAt : existing.updatedAt,
1232
+ deletedAt: applyIncomingMetadata ? comment.deletedAt : existing.deletedAt,
1233
+ };
1234
+ if (reconcileAuthoritativeParent || reconcileLegacyCloudRoot) {
1235
+ core.upsertDocumentReviewCommentForSync(commentForSync, {
1236
+ allowAuthoritativeThreadReconciliation: true,
1237
+ });
1238
+ }
1239
+ else {
1240
+ core.upsertDocumentReviewCommentForSync(commentForSync);
1241
+ }
1185
1242
  if (comment.parentCommentId) {
1186
1243
  const rootExists = core.listDocumentReviewCommentsForSync({
1187
1244
  workspaceId,
@@ -72,6 +72,7 @@ export interface WorkspacePushSyncInput {
72
72
  operationIdentity: string;
73
73
  settlementIdentityFingerprint: string;
74
74
  appliedTaskUpserts: WorkspacePushSyncResult['appliedTaskUpserts'];
75
+ settledTaskDeleteIds: string[];
75
76
  transferReceipt: WorkspaceSyncEngineRemoteTransferReceipt;
76
77
  pushBoundary: WorkspaceSyncEngineV2PushBoundary;
77
78
  excludedProjections?: readonly WorkspaceSyncV2ProjectionExclusion[];
@@ -1,7 +1,7 @@
1
1
  import { applyLocalWorkspaceChanges, getLocalWorkspaceContentPullObligations, recordLocalWorkspaceContentPullObligationFailures, getLocalWorkspaceContentManifest, pullUserSettings, pushUserSettings, prepareWorkspacePushChanges, } from './cloudSyncApi.js';
2
2
  import { workspaceSyncContentPullObligationId, } from './engine/workspaceSyncContentPullObligationStore.js';
3
3
  import { createBrowserWorkspaceSyncEngineRemoteServices, } from './engine/workspaceSyncEngineRemoteServices.js';
4
- import { createWorkspaceSyncEngineV2SettlementIdentityProjection, createWorkspaceSyncEngineV2PushBoundary, parseWorkspaceSyncEngineAppliedTaskUpserts, workspaceSyncEngineExpectedTaskEchoIds, } from './engine/workspaceSyncEnginePushCompletion.js';
4
+ import { createWorkspaceSyncEngineV2SettlementIdentityProjection, createWorkspaceSyncEngineV2PushBoundary, parseWorkspaceSyncEnginePushTaskSettlement, workspaceSyncEngineExpectedTaskEchoIds, } from './engine/workspaceSyncEnginePushCompletion.js';
5
5
  import { encryptDocument } from './encryptionService.js';
6
6
  import { decryptWorkspaceSyncContentChangeInPlace } from './workspaceSyncContentDecryption.js';
7
7
  import { decideWorkspaceContentFetch, fingerprintWorkspaceContentBody, } from './workspaceContentManifest.js';
@@ -835,7 +835,8 @@ export async function runWorkspacePushSyncService(input) {
835
835
  emittedEventIds.add(eventId);
836
836
  }
837
837
  }
838
- const normalizedTaskUpserts = parseWorkspaceSyncEngineAppliedTaskUpserts(pushData, expectedTaskEchoIds);
838
+ const taskSettlement = parseWorkspaceSyncEnginePushTaskSettlement(pushData, expectedTaskEchoIds);
839
+ const normalizedTaskUpserts = taskSettlement.appliedTaskUpserts;
839
840
  appliedTaskUpserts.push(...normalizedTaskUpserts);
840
841
  if (input.onAcceptedBatchSettled) {
841
842
  if (!pushResult.transferReceipt) {
@@ -847,9 +848,11 @@ export async function runWorkspacePushSyncService(input) {
847
848
  operationIdentity: idempotencyKey,
848
849
  changes: plainBatchChanges,
849
850
  appliedTaskUpserts: normalizedTaskUpserts,
851
+ settledTaskDeleteIds: taskSettlement.settledTaskDeleteIds,
850
852
  excludedProjections: effectiveV2ExcludedProjections,
851
853
  })),
852
854
  appliedTaskUpserts: normalizedTaskUpserts,
855
+ settledTaskDeleteIds: taskSettlement.settledTaskDeleteIds,
853
856
  transferReceipt: pushResult.transferReceipt,
854
857
  pushBoundary,
855
858
  excludedProjections: effectiveV2ExcludedProjections,
@@ -1,5 +1,5 @@
1
1
  import type { WorkflowExecutionAction, WorkflowStepCompletionActorType } from '../../core/WorkflowExecutionService.js';
2
- import { type WorkflowExecutionStatus } from '../../storage/workflowStore.js';
2
+ import { type WorkflowExecutionEventRecord, type WorkflowExecutionStatus } from '../../storage/workflowStore.js';
3
3
  import { type LocalWorkflowRuntimeOutboxCore } from './localWorkflowRuntimeOutboxService.js';
4
4
  import { type DurableWorkflowRuntimeServiceCore } from './workflowRuntimeMutationService.js';
5
5
  export type DurableWorkflowRuntimeSurface = 'http' | 'mcp';
@@ -53,11 +53,11 @@ export declare function createDurableWorkflowRuntimeMutationRouter(input: {
53
53
  details?: Record<string, unknown>;
54
54
  }): {
55
55
  state: "pending" | "committed";
56
- route: "local-outbox" | "cloud-acceptance";
56
+ route: "local-outbox" | "cloud-acceptance" | "local-projection-replay";
57
57
  operationId: string;
58
58
  execution: import("../../storage/workflowStore.js").WorkflowExecutionRecord;
59
- events: import("../../storage/workflowStore.js").WorkflowExecutionEventRecord[];
60
- event: import("../../storage/workflowStore.js").WorkflowExecutionEventRecord | null;
59
+ events: WorkflowExecutionEventRecord[];
60
+ event: WorkflowExecutionEventRecord | null;
61
61
  completedStep: import("../../storage/workflowStore.js").WorkflowStepRecord | null;
62
62
  nextStep: import("../../storage/workflowStore.js").WorkflowStepRecord | null;
63
63
  taskAssignee: string | null;
@@ -72,11 +72,11 @@ export declare function createDurableWorkflowRuntimeMutationRouter(input: {
72
72
  evidenceCommentId?: string;
73
73
  }): {
74
74
  state: "pending" | "committed";
75
- route: "local-outbox" | "cloud-acceptance";
75
+ route: "local-outbox" | "cloud-acceptance" | "local-projection-replay";
76
76
  operationId: string;
77
77
  execution: import("../../storage/workflowStore.js").WorkflowExecutionRecord;
78
- events: import("../../storage/workflowStore.js").WorkflowExecutionEventRecord[];
79
- event: import("../../storage/workflowStore.js").WorkflowExecutionEventRecord | null;
78
+ events: WorkflowExecutionEventRecord[];
79
+ event: WorkflowExecutionEventRecord | null;
80
80
  completedStep: import("../../storage/workflowStore.js").WorkflowStepRecord | null;
81
81
  nextStep: import("../../storage/workflowStore.js").WorkflowStepRecord | null;
82
82
  taskAssignee: string | null;
@@ -1,4 +1,4 @@
1
- import { WorkflowStore } from '../../storage/workflowStore.js';
1
+ import { WorkflowStore, } from '../../storage/workflowStore.js';
2
2
  import { LocalSyncIdentityStore } from './localSyncIdentityStore.js';
3
3
  import { canonicalWorkspaceSyncJson } from './canonicalWorkspaceSyncJson.js';
4
4
  import { enqueueLocalWorkflowRuntimeMutation, } from './localWorkflowRuntimeOutboxService.js';
@@ -110,6 +110,31 @@ function taskTarget(input) {
110
110
  baseLifecycleRevision: coordinates.lifecycleRevision,
111
111
  };
112
112
  }
113
+ function matchCompletedStepReplay(input) {
114
+ const completedEvent = input.events.find((event) => event.idempotencyKey === `${input.operationId}:step-completed`);
115
+ const terminalEvent = input.events.find((event) => (event.idempotencyKey === `${input.operationId}:step-started`
116
+ || event.idempotencyKey === `${input.operationId}:execution-completed`));
117
+ if (!completedEvent && !terminalEvent)
118
+ return 'absent';
119
+ if (!completedEvent
120
+ || !terminalEvent
121
+ || completedEvent.eventType !== 'step-completed'
122
+ || (terminalEvent.eventType !== 'step-started'
123
+ && terminalEvent.eventType !== 'execution-completed')
124
+ || completedEvent.actorId !== input.actorId
125
+ || terminalEvent.actorId !== input.actorId
126
+ || (input.expectedStepId !== undefined && completedEvent.stepId !== input.expectedStepId)) {
127
+ return 'conflict';
128
+ }
129
+ const evidence = completedEvent.details.evidence;
130
+ const replayCommentId = evidence
131
+ && typeof evidence === 'object'
132
+ && !Array.isArray(evidence)
133
+ && evidence.type === 'comment'
134
+ ? String(evidence.commentId || '')
135
+ : '';
136
+ return (input.evidenceCommentId || '') === replayCommentId ? 'exact' : 'conflict';
137
+ }
113
138
  function buildResult(input) {
114
139
  const workflowStore = new WorkflowStore(input.core.getDatabaseAdapter(), input.core.getTenantId());
115
140
  const assignmentSnapshot = workflowStore.getAssignmentForExecutionSync(input.workspaceId, input.executionId);
@@ -120,7 +145,8 @@ function buildResult(input) {
120
145
  const completedStep = completedEvent?.stepId
121
146
  ? assignmentSnapshot.steps.find((step) => step.id === completedEvent.stepId) || null
122
147
  : null;
123
- const nextStep = assignmentSnapshot.execution.currentStepId
148
+ const nextStep = assignmentSnapshot.execution.status === 'running'
149
+ && assignmentSnapshot.execution.currentStepId
124
150
  ? assignmentSnapshot.steps.find((step) => step.id === assignmentSnapshot.execution.currentStepId) || null
125
151
  : null;
126
152
  const taskSnapshot = input.core.getTask(assignmentSnapshot.execution.taskId, input.workspaceId);
@@ -338,18 +364,15 @@ export function createDurableWorkflowRuntimeMutationRouter(input) {
338
364
  if (!snapshot)
339
365
  throw new DurableWorkflowRuntimeExecutionNotFoundError();
340
366
  const committed = readCommittedCloudResponse();
367
+ const replayMatch = matchCompletedStepReplay({
368
+ events: snapshot.events,
369
+ operationId,
370
+ actorId,
371
+ expectedStepId: args.expectedStepId,
372
+ evidenceCommentId: args.evidenceCommentId,
373
+ });
341
374
  if (committed) {
342
- const completedEvent = snapshot.events.find((event) => event.idempotencyKey === `${operationId}:step-completed`);
343
- const terminalEvent = snapshot.events.find((event) => (event.idempotencyKey === `${operationId}:step-started`
344
- || event.idempotencyKey === `${operationId}:execution-completed`));
345
- if (!completedEvent
346
- || !terminalEvent
347
- || completedEvent.actorId !== actorId
348
- || terminalEvent.actorId !== actorId
349
- || (args.expectedStepId !== undefined && completedEvent.stepId !== args.expectedStepId)
350
- || (args.evidenceCommentId !== undefined
351
- && completedEvent.details.evidence?.commentId
352
- !== args.evidenceCommentId)) {
375
+ if (replayMatch !== 'exact') {
353
376
  throw new Error('Operation identity is assigned to a different workflow step command.');
354
377
  }
355
378
  return buildResult({
@@ -363,6 +386,20 @@ export function createDurableWorkflowRuntimeMutationRouter(input) {
363
386
  replayed: true,
364
387
  });
365
388
  }
389
+ if (!existing && replayMatch !== 'absent') {
390
+ if (replayMatch === 'conflict') {
391
+ throw new Error('Operation identity is assigned to a different workflow step command.');
392
+ }
393
+ return buildResult({
394
+ core: input.core,
395
+ workspaceId,
396
+ executionId: args.executionId,
397
+ operationId,
398
+ state: 'committed',
399
+ route: 'local-projection-replay',
400
+ replayed: true,
401
+ });
402
+ }
366
403
  const expectedStepId = args.expectedStepId || snapshot.execution.currentStepId;
367
404
  if (!expectedStepId)
368
405
  throw new Error('Workflow execution current step is unavailable.');
@@ -43,11 +43,14 @@ function appendEligibleBlockedChains(input) {
43
43
  workspaceId: input.workspaceId,
44
44
  })) {
45
45
  try {
46
+ const blocked = input.store.readOutboxOperation(identity);
46
47
  input.store.supersedeBlockedOutboxChain({
47
48
  identity,
48
49
  recoveryBatchId: input.recovery.recoveryBatchId,
49
50
  now: input.now,
50
- replayPolicy: 'task-root',
51
+ replayPolicy: blocked?.lastErrorCode === 'WORKFLOW_RUNTIME_SEMANTIC_REPLAY_REQUIRED'
52
+ ? 'authoritative-workflow-runtime'
53
+ : 'task-root',
51
54
  appendToExistingRecovery: true,
52
55
  });
53
56
  }
@@ -104,11 +107,14 @@ export function prepareLocalOutboxRecovery(input) {
104
107
  for (const root of blockedRoots) {
105
108
  const candidateBatchId = input.createRecoveryBatchId();
106
109
  try {
110
+ const blocked = input.store.readOutboxOperation(root);
107
111
  input.store.supersedeBlockedOutboxChain({
108
112
  identity: root,
109
113
  recoveryBatchId: candidateBatchId,
110
114
  now: input.now,
111
- replayPolicy: 'task-root',
115
+ replayPolicy: blocked?.lastErrorCode === 'WORKFLOW_RUNTIME_SEMANTIC_REPLAY_REQUIRED'
116
+ ? 'authoritative-workflow-runtime'
117
+ : 'task-root',
112
118
  });
113
119
  recoveryBatchId = candidateBatchId;
114
120
  break;