@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.
- package/README.md +7 -2
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +2 -2
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.js +2 -2
- package/dist/components/features/planning/PlanningModule.js +15 -3
- package/dist/components/features/planning/planningMetadata.d.ts +10 -0
- package/dist/components/features/planning/planningMetadata.js +8 -0
- package/dist/components/views/StandaloneLayout.js +35 -1
- package/dist/components/views/panels/PlanningDrawer.d.ts +2 -1
- package/dist/components/views/panels/PlanningDrawer.js +12 -8
- package/dist/config/envSchema.js +7 -0
- package/dist/core/McpTokenService.d.ts +2 -0
- package/dist/core/McpTokenService.js +13 -5
- package/dist/core/Taskforce.d.ts +11 -3
- package/dist/core/Taskforce.js +33 -3
- package/dist/core/types.d.ts +1 -0
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +2 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +28 -9
- package/dist/mcp/httpProtocolRouting.d.ts +1 -0
- package/dist/mcp/httpProtocolRouting.js +8 -5
- package/dist/mcp/runtime.js +24 -5
- package/dist/mcp/taskPlanningRegistrar.js +2 -2
- package/dist/mcp/toolProfiles.js +1 -0
- package/dist/mcp/toolRegistry.js +8 -8
- package/dist/server/buildInfo.d.ts +1 -0
- package/dist/server/buildInfo.js +5 -1
- package/dist/server/cloudMcpCapacityTelemetry.d.ts +68 -0
- package/dist/server/cloudMcpCapacityTelemetry.js +221 -0
- package/dist/server/localWorkspaceSyncServerRuntime.js +13 -5
- package/dist/server/routes/admin.js +14 -2
- package/dist/server/routes/shared.d.ts +3 -0
- package/dist/server/routes/shared.js +280 -240
- package/dist/service/localServiceCli.d.ts +2 -2
- package/dist/service/localServiceCli.js +4 -3
- package/dist/services/executorPhaseAIssuerCore.d.ts +13 -0
- package/dist/services/executorPhaseAIssuerCore.js +107 -0
- package/dist/services/taskforceAgentMcpBridge.d.ts +1 -1
- package/dist/services/taskforceAgentMcpBridge.js +35 -5
- package/dist/shared/executorPhaseASchemaValidation.js +1 -1
- package/dist/storage/postgresAdapter.js +3 -1
- package/dist/storage/postgresRequestDiagnostics.d.ts +9 -1
- package/dist/storage/postgresRequestDiagnostics.js +15 -2
- package/dist/storage/postgresWorker.js +13 -3
- package/dist/storage/postgresWorkerProtocol.d.ts +6 -0
- package/dist/storage/workflowStore.d.ts +2 -0
- package/dist/storage/workflowStore.js +39 -2
- package/dist/sync/cloudSyncApi.js +4 -2
- package/dist/sync/collaborationSyncState.js +5 -0
- package/dist/sync/coordinator/localWorkspaceSyncRunner.d.ts +3 -1
- package/dist/sync/coordinator/localWorkspaceSyncRunner.js +18 -0
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
- package/dist/sync/syncApplyHandlers.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.js +72 -15
- package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.d.ts +7 -7
- package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.js +50 -13
- package/dist/sync/v3/localOutboxRecoveryPreparationService.js +8 -2
- package/dist/sync/v3/localTaskCreateOutboxService.d.ts +3 -1
- package/dist/sync/v3/localTaskCreateOutboxService.js +6 -1
- package/dist/sync/v3/localTaskDeleteOutboxService.js +1 -0
- package/dist/sync/v3/localTaskLifecycleOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskLifecycleOutboxService.js +5 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.js +6 -2
- package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskStatusOutboxService.js +20 -5
- package/dist/sync/v3/syncDurabilityStore.d.ts +2 -1
- package/dist/sync/v3/syncDurabilityStore.js +34 -6
- package/dist/sync/v3/workflowRuntimeMutationService.d.ts +5 -0
- package/dist/sync/v3/workflowRuntimeMutationService.js +124 -71
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +25 -3
- package/dist/sync/v3/workspaceSyncV3Feed.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3Feed.js +6 -1
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +49 -18
- package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.js +50 -5
- package/dist/ui/assets/{AiIdentityRosterCard-OcSz1PkK.js → AiIdentityRosterCard-CDMPtnbC.js} +1 -1
- package/dist/ui/assets/{AiProfilesModule-ByhYW5Xi.js → AiProfilesModule-Cww85C4E.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTM6nIg_.js → AnnotatedAttachmentWorkspace-Dt9TKYSH.js} +1 -1
- package/dist/ui/assets/{AssetTraySortControl-DabEG8L4.js → AssetTraySortControl-CKgapnxu.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-Coa8yVGM.js → ContextAttachmentManager-T-cEVcOZ.js} +2 -2
- package/dist/ui/assets/{DocumentWorkspace-BB0qhdix.js → DocumentWorkspace-HD4MUjAZ.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-CZ8x4UJE.js → EntityActivityTimeline-_V0zfMuh.js} +1 -1
- package/dist/ui/assets/PlanningModule-mTkBygsB.js +1 -0
- package/dist/ui/assets/{PlansPage-D86pZSSl.js → PlansPage-Btiq06VO.js} +1 -1
- package/dist/ui/assets/{TaskContextUpload-BVIkU9yT.js → TaskContextUpload-C00NVUpn.js} +1 -1
- package/dist/ui/assets/{TaskSettings-Cv1mNhv_.js → TaskSettings-Cp-L-4xh.js} +1 -1
- package/dist/ui/assets/{TaskforceAgentsModule-gmH-PD2q.js → TaskforceAgentsModule-Coq9-PUp.js} +2 -2
- package/dist/ui/assets/{WorkflowManagerModule-DbfnHJ-f.js → WorkflowManagerModule-CuqRzMmM.js} +1 -1
- package/dist/ui/assets/index-DSQINwt9.js +7 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -1
- package/dist/ui/assets/PlanningModule-pRPjR7v5.js +0 -1
- 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:
|
|
60
|
-
event:
|
|
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:
|
|
79
|
-
event:
|
|
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.
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
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
|
|
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 =
|
|
588
|
+
const successors = activeSuccessors;
|
|
585
589
|
const hasSuccessor = successors.length > 0;
|
|
586
|
-
|
|
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 (
|
|
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'
|
|
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
|
-
|| (
|
|
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
|
-
||
|
|
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;
|