@taskforcehq/taskforce 0.3.330 → 0.3.332

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 (93) 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/shared.d.ts +3 -0
  31. package/dist/server/routes/shared.js +280 -240
  32. package/dist/service/localServiceCli.d.ts +2 -2
  33. package/dist/service/localServiceCli.js +4 -3
  34. package/dist/services/executorPhaseAIssuerCore.d.ts +13 -0
  35. package/dist/services/executorPhaseAIssuerCore.js +107 -0
  36. package/dist/services/taskforceAgentMcpBridge.d.ts +1 -1
  37. package/dist/services/taskforceAgentMcpBridge.js +35 -5
  38. package/dist/shared/executorPhaseASchemaValidation.js +1 -1
  39. package/dist/storage/postgresAdapter.js +3 -1
  40. package/dist/storage/postgresRequestDiagnostics.d.ts +9 -1
  41. package/dist/storage/postgresRequestDiagnostics.js +15 -2
  42. package/dist/storage/postgresWorker.js +13 -3
  43. package/dist/storage/postgresWorkerProtocol.d.ts +6 -0
  44. package/dist/storage/workflowStore.d.ts +2 -0
  45. package/dist/storage/workflowStore.js +39 -2
  46. package/dist/sync/cloudSyncApi.js +4 -2
  47. package/dist/sync/collaborationSyncState.js +5 -0
  48. package/dist/sync/coordinator/localWorkspaceSyncRunner.d.ts +3 -1
  49. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +18 -0
  50. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
  51. package/dist/sync/syncApplyHandlers.d.ts +1 -0
  52. package/dist/sync/syncApplyHandlers.js +72 -15
  53. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.d.ts +7 -7
  54. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.js +50 -13
  55. package/dist/sync/v3/localOutboxRecoveryPreparationService.js +8 -2
  56. package/dist/sync/v3/localTaskCreateOutboxService.d.ts +3 -1
  57. package/dist/sync/v3/localTaskCreateOutboxService.js +6 -1
  58. package/dist/sync/v3/localTaskDeleteOutboxService.js +1 -0
  59. package/dist/sync/v3/localTaskLifecycleOutboxService.d.ts +2 -1
  60. package/dist/sync/v3/localTaskLifecycleOutboxService.js +5 -1
  61. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -1
  62. package/dist/sync/v3/localTaskMetadataOutboxService.js +6 -2
  63. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -1
  64. package/dist/sync/v3/localTaskStatusOutboxService.js +20 -5
  65. package/dist/sync/v3/syncDurabilityStore.d.ts +2 -1
  66. package/dist/sync/v3/syncDurabilityStore.js +34 -6
  67. package/dist/sync/v3/workflowRuntimeMutationService.d.ts +5 -0
  68. package/dist/sync/v3/workflowRuntimeMutationService.js +124 -71
  69. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +25 -3
  70. package/dist/sync/v3/workspaceSyncV3Feed.d.ts +1 -0
  71. package/dist/sync/v3/workspaceSyncV3Feed.js +6 -1
  72. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +2 -0
  73. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +49 -18
  74. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.d.ts +1 -0
  75. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.js +50 -5
  76. package/dist/ui/assets/{AiIdentityRosterCard-OcSz1PkK.js → AiIdentityRosterCard-CDMPtnbC.js} +1 -1
  77. package/dist/ui/assets/{AiProfilesModule-ByhYW5Xi.js → AiProfilesModule-Cww85C4E.js} +1 -1
  78. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTM6nIg_.js → AnnotatedAttachmentWorkspace-Dt9TKYSH.js} +1 -1
  79. package/dist/ui/assets/{AssetTraySortControl-DabEG8L4.js → AssetTraySortControl-CKgapnxu.js} +1 -1
  80. package/dist/ui/assets/{ContextAttachmentManager-Coa8yVGM.js → ContextAttachmentManager-T-cEVcOZ.js} +2 -2
  81. package/dist/ui/assets/{DocumentWorkspace-BB0qhdix.js → DocumentWorkspace-HD4MUjAZ.js} +1 -1
  82. package/dist/ui/assets/{EntityActivityTimeline-CZ8x4UJE.js → EntityActivityTimeline-_V0zfMuh.js} +1 -1
  83. package/dist/ui/assets/PlanningModule-mTkBygsB.js +1 -0
  84. package/dist/ui/assets/{PlansPage-D86pZSSl.js → PlansPage-Btiq06VO.js} +1 -1
  85. package/dist/ui/assets/{TaskContextUpload-BVIkU9yT.js → TaskContextUpload-C00NVUpn.js} +1 -1
  86. package/dist/ui/assets/{TaskSettings-Cv1mNhv_.js → TaskSettings-Cp-L-4xh.js} +1 -1
  87. package/dist/ui/assets/{TaskforceAgentsModule-gmH-PD2q.js → TaskforceAgentsModule-Coq9-PUp.js} +2 -2
  88. package/dist/ui/assets/{WorkflowManagerModule-DbfnHJ-f.js → WorkflowManagerModule-CuqRzMmM.js} +1 -1
  89. package/dist/ui/assets/index-DSQINwt9.js +7 -0
  90. package/dist/ui/index.html +1 -1
  91. package/package.json +3 -1
  92. package/dist/ui/assets/PlanningModule-pRPjR7v5.js +0 -1
  93. package/dist/ui/assets/index-CTsDBaef.js +0 -7
@@ -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;
@@ -16,7 +16,9 @@ export interface LocalTaskCreateOutboxCore {
16
16
  allowV3OwnedCreate?: boolean;
17
17
  preferExplicitAttachmentAssetId?: boolean;
18
18
  }): TaskItem;
19
- installDurableTaskCreateAuthoritativeForSync(task: TaskItem, workspaceId: string): TaskItem;
19
+ installDurableTaskCreateAuthoritativeForSync(task: TaskItem, workspaceId: string, options?: {
20
+ preserveDeletedSuccessor?: boolean;
21
+ }): TaskItem;
20
22
  upsertEntityEventForSync(event: EntityEvent, options?: {
21
23
  allowAuthoritativeReplacement?: boolean;
22
24
  }): unknown;
@@ -356,10 +356,15 @@ export function applyAuthoritativeTaskCreateAndAck(input) {
356
356
  canonicalFingerprint: fingerprintWorkspaceSyncPayload(canonicalDependentActivities[index]),
357
357
  }))],
358
358
  applyAuthoritative: () => {
359
+ const hasDeletedSuccessor = input.store.listActiveOutboxSuccessors(input.identity).some((successor) => (successor.domain === 'task'
360
+ && successor.entityId === task.id
361
+ && successor.mutationKind === 'delete'));
359
362
  const updated = input.core.installDurableTaskCreateAuthoritativeForSync({
360
363
  ...task,
361
364
  localReferenceNumber: null,
362
- }, input.identity.workspaceId);
365
+ }, input.identity.workspaceId, {
366
+ preserveDeletedSuccessor: hasDeletedSuccessor,
367
+ });
363
368
  const installedWithAttachments = canonicalAttachmentProjection
364
369
  ? input.core.updateTask(task.id, {
365
370
  attachments: input.core.resolveTaskAttachmentLinksForDurableSync(canonicalAttachmentProjection.links, input.identity.workspaceId, { adoptAuthoritativeReferenceNumber: true }),
@@ -99,6 +99,7 @@ export function enqueueLocalTaskDeleteMutation(input) {
99
99
  entityId: taskId,
100
100
  mutationKind: 'delete',
101
101
  revisionSource: 'current',
102
+ queuedTaskSuccessor: true,
102
103
  ownership: { taskMetadata: true, taskLifecycle: true },
103
104
  payload: { relationships },
104
105
  now: input.now,
@@ -1,4 +1,4 @@
1
- import type { TaskEvent, TaskItem } from '../../core/Taskforce.js';
1
+ import type { DeletedTaskRecord, TaskEvent, TaskItem } from '../../core/Taskforce.js';
2
2
  import { type DurableTaskLifecycleMutationCore, type DurableTaskLifecycleMutationKind } from '../../core/TaskDurableLifecycleMutationService.js';
3
3
  import { type DurableTaskWorkflowMutationCore, type DurableTaskWorkflowMutationKind } from '../../core/TaskDurableWorkflowMutationService.js';
4
4
  import { WorkspaceSyncDurabilityStore, type WorkspaceSyncOperationIdentity, type WorkspaceSyncOutboxRecord } from './syncDurabilityStore.js';
@@ -16,6 +16,7 @@ export declare function normalizeLocalTaskLifecyclePayload(mutationKind: LocalTa
16
16
  };
17
17
  };
18
18
  export interface LocalTaskLifecycleOutboxCore extends DurableTaskLifecycleMutationCore, DurableTaskWorkflowMutationCore {
19
+ getDeletedTask(id: string, workspaceId?: string): DeletedTaskRecord | null;
19
20
  installDurableTaskLifecycleAuthoritativeForSync(task: TaskItem, mutationKind: LocalTaskLifecycleMutationKind, workspaceId: string): TaskItem;
20
21
  upsertEntityEventForSync(event: TaskEvent, options?: {
21
22
  allowAuthoritativeReplacement?: boolean;
@@ -276,9 +276,13 @@ export function applyAuthoritativeTaskLifecycleAndAck(input) {
276
276
  canonicalFingerprint: fingerprintWorkspaceSyncPayload(serializeTaskActivityForSync(activity)),
277
277
  }] : [])],
278
278
  applyAuthoritative: () => {
279
- const hasSuccessor = input.store.hasActiveOutboxSuccessor(input.identity);
279
+ const successors = input.store.listActiveOutboxSuccessors(input.identity);
280
+ const hasSuccessor = successors.length > 0;
280
281
  const updated = hasSuccessor
281
282
  ? input.core.getTask(task.id, input.identity.workspaceId)
283
+ || (successors.some((successor) => successor.mutationKind === 'delete')
284
+ ? input.core.getDeletedTask(task.id, input.identity.workspaceId)?.taskSnapshot
285
+ : null)
282
286
  : input.core.installDurableTaskLifecycleAuthoritativeForSync(task, input.mutationKind, input.identity.workspaceId);
283
287
  if (!updated)
284
288
  throw new LocalAuthoritativeTaskLifecycleError('Local task lifecycle projection is missing.');
@@ -1,7 +1,8 @@
1
- import type { EntityEvent, TaskItem } from '../../core/Taskforce.js';
1
+ import type { DeletedTaskRecord, EntityEvent, TaskItem } from '../../core/Taskforce.js';
2
2
  import { WorkspaceSyncDurabilityStore, type WorkspaceSyncOperationIdentity } from './syncDurabilityStore.js';
3
3
  export interface LocalTaskMetadataOutboxCore {
4
4
  getTask(id: string, workspaceId?: string): TaskItem | null | undefined;
5
+ getDeletedTask(id: string, workspaceId?: string): DeletedTaskRecord | null;
5
6
  updateTask(id: string, updates: Partial<TaskItem>, workspaceId?: string, options?: {
6
7
  persistBackup?: boolean;
7
8
  emitRealtime?: boolean;
@@ -196,8 +196,12 @@ export function applyAuthoritativeTaskMetadataAndAck(input) {
196
196
  for (const dependentActivity of canonicalDependentActivities) {
197
197
  input.core.upsertEntityEventForSync(dependentActivity, { allowAuthoritativeReplacement: true });
198
198
  }
199
- if (input.store.hasActiveOutboxSuccessor(input.identity)) {
200
- const provisional = input.core.getTask(taskId, input.identity.workspaceId);
199
+ const successors = input.store.listActiveOutboxSuccessors(input.identity);
200
+ if (successors.length > 0) {
201
+ const provisional = input.core.getTask(taskId, input.identity.workspaceId)
202
+ || (successors.some((successor) => successor.mutationKind === 'delete')
203
+ ? input.core.getDeletedTask(taskId, input.identity.workspaceId)?.taskSnapshot
204
+ : null);
201
205
  if (!provisional)
202
206
  throw new DurableTaskMetadataMutationError('Task not found.', 'TASK_NOT_FOUND', 404);
203
207
  return provisional;
@@ -1,8 +1,9 @@
1
- import type { EntityEvent, TaskEvent, TaskItem } from '../../core/Taskforce.js';
1
+ import type { DeletedTaskRecord, EntityEvent, TaskEvent, TaskItem } from '../../core/Taskforce.js';
2
2
  import { type DurableTaskStatusMutationCore, type DurableTaskStatusMutationPayload } from '../../core/TaskDurableStatusMutationService.js';
3
3
  import { WorkspaceSyncDurabilityStore, type WorkspaceSyncOperationIdentity, type WorkspaceSyncOutboxRecord } from './syncDurabilityStore.js';
4
4
  import type { DatabaseAdapter } from '../../storage/databaseAdapter.js';
5
5
  export interface LocalTaskStatusOutboxCore extends DurableTaskStatusMutationCore {
6
+ getDeletedTask(id: string, workspaceId?: string): DeletedTaskRecord | null;
6
7
  assertTaskDetailRevisionForUpdate?(id: string, expectedDetailRevision: string, workspaceId?: string): void;
7
8
  getDatabaseAdapter(): DatabaseAdapter;
8
9
  getTenantId(): string;
@@ -347,7 +347,11 @@ export function applyAuthoritativeTaskStatusAndAck(input) {
347
347
  if (!validLifecycle || authoritative.metadataOwned !== expectsMetadataOwnership) {
348
348
  throw new LocalAuthoritativeTaskStatusError('Authoritative task status state does not match the persisted request.');
349
349
  }
350
- const localCanonical = input.core.getTask(task.id, input.identity.workspaceId);
350
+ const activeSuccessors = input.store.listActiveOutboxSuccessors(input.identity);
351
+ const localCanonical = input.core.getTask(task.id, input.identity.workspaceId)
352
+ || (activeSuccessors.some((successor) => successor.mutationKind === 'delete')
353
+ ? input.core.getDeletedTask(task.id, input.identity.workspaceId)?.taskSnapshot
354
+ : null);
351
355
  if (!localCanonical)
352
356
  throw new LocalAuthoritativeTaskStatusError('Local task status projection is missing.');
353
357
  let canonicalMetadataIntent = null;
@@ -581,14 +585,20 @@ export function applyAuthoritativeTaskStatusAndAck(input) {
581
585
  expectedTaskId: task.id,
582
586
  });
583
587
  }
584
- const successors = input.store.listActiveOutboxSuccessors(input.identity);
588
+ const successors = activeSuccessors;
585
589
  const hasSuccessor = successors.length > 0;
586
- let updated = hasSuccessor
590
+ const activeTask = hasSuccessor
587
591
  ? input.core.getTask(task.id, input.identity.workspaceId)
592
+ : null;
593
+ const deletedSnapshot = !activeTask && successors.some((successor) => successor.mutationKind === 'delete')
594
+ ? input.core.getDeletedTask(task.id, input.identity.workspaceId)?.taskSnapshot
595
+ : null;
596
+ let updated = hasSuccessor
597
+ ? activeTask || deletedSnapshot
588
598
  : input.core.installDurableTaskStatusAuthoritativeForSync(task, payload, installAssignee, input.identity.workspaceId);
589
599
  if (!updated)
590
600
  throw new LocalAuthoritativeTaskStatusError('Local task status projection is missing.');
591
- if (reviewHandoff && reviewCommentsPayload && reviewCommentActivity) {
601
+ if (reviewHandoff && reviewCommentsPayload && reviewCommentActivity && !deletedSnapshot) {
592
602
  const localComments = serializeWorkspaceSyncV3TaskCommentsPayload({
593
603
  taskId: updated.id,
594
604
  comments: updated.comments || [],
@@ -612,7 +622,12 @@ export function applyAuthoritativeTaskStatusAndAck(input) {
612
622
  input.core.upsertEntityEventForSync(reviewCommentActivity, { allowAuthoritativeReplacement: true });
613
623
  activityReconciled = true;
614
624
  }
615
- if (hasSuccessor
625
+ else if (reviewHandoff && reviewCommentActivity && deletedSnapshot) {
626
+ input.core.upsertEntityEventForSync(reviewCommentActivity, { allowAuthoritativeReplacement: true });
627
+ activityReconciled = true;
628
+ }
629
+ if (!deletedSnapshot
630
+ && hasSuccessor
616
631
  && installAssignee
617
632
  && !successors.some((successor) => successor.ownsTaskAssignee)
618
633
  && normalizeAssignee(updated.assignee) !== normalizeAssignee(task.assignee)) {
@@ -90,6 +90,7 @@ export type WorkspaceSyncOutboxRecord = WorkspaceSyncOperationIdentity & {
90
90
  initiativeMetadataRevisionRebaseCount?: number;
91
91
  leaseOwner: string | null;
92
92
  leaseExpiresAt: string | null;
93
+ lastErrorCode: string | null;
93
94
  };
94
95
  export type WorkspaceSyncOutboxOperationLookup = WorkspaceSyncOperationIdentity & {
95
96
  status: string;
@@ -397,7 +398,7 @@ export declare class WorkspaceSyncDurabilityStore {
397
398
  identity: WorkspaceSyncOperationIdentity;
398
399
  recoveryBatchId: string;
399
400
  now: string;
400
- replayPolicy?: 'task-root';
401
+ replayPolicy?: 'task-root' | 'authoritative-workflow-runtime';
401
402
  appendToExistingRecovery?: boolean;
402
403
  }): WorkspaceSyncOutboxSupersession;
403
404
  recordOutboxRecoveryBaseline(input: {
@@ -27,6 +27,7 @@ const TASK_AGGREGATE_SUCCESSOR_MUTATIONS = new Map([
27
27
  const TASK_ROOT_SUCCESSOR_MUTATIONS = new Set([
28
28
  'metadata-update', 'set-status', 'archive', 'unarchive', 'complete', 'reopen',
29
29
  ]);
30
+ const TASK_TERMINAL_SUCCESSOR_MUTATIONS = new Set(['delete']);
30
31
  const TASK_CREATE_RECOVERY_ERROR_CODES = new Set([
31
32
  'TASK_CREATE_ID_CONFLICT',
32
33
  'WORKSPACE_SYNC_REVISION_CONFLICT',
@@ -795,7 +796,8 @@ export class WorkspaceSyncDurabilityStore {
795
796
  payload_json, payload_fingerprint, canonical_intent_json, status, attempt_count,
796
797
  initial_not_found_attempt_count,
797
798
  CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
798
- rebase_before_first_attempt, lease_owner, lease_expires_at
799
+ rebase_before_first_attempt, lease_owner, lease_expires_at,
800
+ last_error_code
799
801
  FROM workspace_sync_outbox
800
802
  WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
801
803
  `).get(...operationParams(identity));
@@ -1343,6 +1345,17 @@ export class WorkspaceSyncDurabilityStore {
1343
1345
  }
1344
1346
  }
1345
1347
  }
1348
+ else if (input.replayPolicy === 'authoritative-workflow-runtime') {
1349
+ if (selected.length !== 1) {
1350
+ throw new Error('Workflow runtime recovery has intersecting intent that requires semantic history reconciliation.');
1351
+ }
1352
+ const candidate = selected[0];
1353
+ if (candidate.domain !== 'workflow-execution'
1354
+ || candidate.mutation_kind !== 'complete-step'
1355
+ || candidate.last_error_code !== 'WORKFLOW_RUNTIME_SEMANTIC_REPLAY_REQUIRED') {
1356
+ throw new Error('Workflow runtime recovery requires an exact authoritative semantic replay conflict.');
1357
+ }
1358
+ }
1346
1359
  this.db.prepare(`
1347
1360
  INSERT INTO workspace_sync_outbox_recoveries (
1348
1361
  tenant_id, workspace_id, recovery_batch_id,
@@ -1611,6 +1624,11 @@ export class WorkspaceSyncDurabilityStore {
1611
1624
  }
1612
1625
  }
1613
1626
  const sources = recoverySources.filter((source) => {
1627
+ if (source.domain === 'workflow-execution'
1628
+ && source.mutation_kind === 'complete-step'
1629
+ && source.last_error_code === 'WORKFLOW_RUNTIME_SEMANTIC_REPLAY_REQUIRED') {
1630
+ return false;
1631
+ }
1614
1632
  if (source.domain === 'task-comments' && source.mutation_kind === 'append') {
1615
1633
  return !this.isTaskCommentAppendRecoveryIntentSatisfied(tenantId, workspaceId, source);
1616
1634
  }
@@ -2591,6 +2609,12 @@ export class WorkspaceSyncDurabilityStore {
2591
2609
  const queueableTaskRoot = domain === 'task'
2592
2610
  && TASK_ROOT_SUCCESSOR_MUTATIONS.has(mutationKind)
2593
2611
  && (additionalTargets.length === 0 || queueableTaskReviewHandoff);
2612
+ const queueableTaskDelete = domain === 'task'
2613
+ && TASK_TERMINAL_SUCCESSOR_MUTATIONS.has(mutationKind)
2614
+ && additionalTargets.length === 0
2615
+ && ownsTaskMetadata
2616
+ && ownsTaskLifecycle
2617
+ && !ownsTaskAssignee;
2594
2618
  const queueableTaskAggregate = Boolean(TASK_AGGREGATE_SUCCESSOR_MUTATIONS.get(domain)?.has(mutationKind))
2595
2619
  && ((domain === 'task-comments' && mutationKind === 'reconcile' && additionalTargets.length === 0)
2596
2620
  || (additionalTargets.length === 1
@@ -2601,7 +2625,8 @@ export class WorkspaceSyncDurabilityStore {
2601
2625
  && !taskAggregateRootTarget.ownsTaskLifecycle
2602
2626
  && !taskAggregateRootTarget.ownsTaskAssignee))
2603
2627
  && !ownsTaskMetadata && !ownsTaskLifecycle && !ownsTaskAssignee;
2604
- if (queuedTaskSuccessor && (input.revisionSource !== 'current' || (!queueableTaskRoot && !queueableTaskAggregate))) {
2628
+ if (queuedTaskSuccessor && (input.revisionSource !== 'current'
2629
+ || (!queueableTaskRoot && !queueableTaskDelete && !queueableTaskAggregate))) {
2605
2630
  throw new Error('Queued task successors require a recognized task root or aggregate operation.');
2606
2631
  }
2607
2632
  const additionalTargetKeys = new Set();
@@ -2813,11 +2838,13 @@ export class WorkspaceSyncDurabilityStore {
2813
2838
  const isTaskRoot = activeDomain === 'task'
2814
2839
  && row.entity_id === entityId
2815
2840
  && (TASK_ROOT_SUCCESSOR_MUTATIONS.has(activeMutationKind)
2816
- || (queueableTaskAggregate && activeMutationKind === 'create'))
2841
+ || (activeMutationKind === 'create'
2842
+ && Number(row.owns_task_metadata) === 0
2843
+ && Number(row.owns_task_lifecycle) === 0
2844
+ && Number(row.owns_task_assignee) === 0))
2817
2845
  && Number(row.target_count) === 1;
2818
- return isTaskRoot
2819
- || isCompatibleTaskAggregate(row)
2820
- || isCompatibleWorkflowRuntimeAggregate(row);
2846
+ return isTaskRoot || (!queueableTaskDelete && (isCompatibleTaskAggregate(row)
2847
+ || isCompatibleWorkflowRuntimeAggregate(row)));
2821
2848
  });
2822
2849
  const compatibleWorkflowRuntimeSuccessor = queuedWorkflowRuntimeSuccessor
2823
2850
  && active.every((row) => (row.domain === 'workflow-execution'
@@ -4914,6 +4941,7 @@ export class WorkspaceSyncDurabilityStore {
4914
4941
  initiativeMetadataRevisionRebaseCount: Number(row.initiative_metadata_revision_rebase_count || 0),
4915
4942
  leaseOwner: row.lease_owner == null ? null : String(row.lease_owner),
4916
4943
  leaseExpiresAt: row.lease_expires_at == null ? null : String(row.lease_expires_at),
4944
+ lastErrorCode: row.last_error_code == null ? null : String(row.last_error_code),
4917
4945
  };
4918
4946
  }
4919
4947
  }
@@ -53,6 +53,11 @@ export type PreparedWorkflowRuntimeMutation = {
53
53
  terminalEventId: string;
54
54
  taskActivityId: string;
55
55
  };
56
+ export declare class WorkflowRuntimeSemanticReplayError extends Error {
57
+ readonly code = "WORKFLOW_RUNTIME_SEMANTIC_REPLAY_REQUIRED";
58
+ readonly statusCode = 409;
59
+ constructor();
60
+ }
56
61
  export declare function prepareWorkflowRuntimeMutation(input: {
57
62
  workflowStore: WorkflowStore;
58
63
  workspaceId: string;