@taskforcehq/taskforce 0.3.328 → 0.3.329
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/dist/Taskforce.module.css +233 -34
- package/dist/TaskforceCore.js +237 -230
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +1 -1
- package/dist/cli.js +34 -0
- package/dist/cliCloudTargetGuard.d.ts +5 -0
- package/dist/cliCloudTargetGuard.js +11 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +24 -8
- package/dist/components/features/TaskSettings.d.ts +12 -1
- package/dist/components/features/TaskSettings.js +147 -40
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +1 -1
- package/dist/components/features/planning/PlanningModule.js +2 -5
- package/dist/components/features/planning/PlanningTaskOrderList.d.ts +1 -1
- package/dist/components/features/planning/PlanningTaskOrderList.js +17 -6
- package/dist/components/features/planning/planningTaskArrangement.d.ts +1 -0
- package/dist/components/features/planning/planningTaskArrangement.js +5 -5
- package/dist/components/features/taskforceAgentEditorModel.js +3 -1
- package/dist/components/task/TaskCard.d.ts +5 -0
- package/dist/components/task/TaskCard.js +65 -10
- package/dist/components/task/TaskImageReviews.js +14 -5
- package/dist/components/task/TaskKanban.d.ts +6 -1
- package/dist/components/task/TaskKanban.js +152 -6
- package/dist/components/task/TaskList.d.ts +5 -0
- package/dist/components/task/TaskList.js +3 -3
- package/dist/components/ui/PasswordInput.d.ts +6 -0
- package/dist/components/ui/PasswordInput.js +11 -0
- package/dist/components/ui/TopNoticeLayer.js +1 -1
- package/dist/components/views/AppShellHeader.js +32 -11
- package/dist/components/views/PlanComparisonPage.js +12 -0
- package/dist/components/views/StandaloneLayout.js +7 -31
- package/dist/components/views/WidgetView.js +3 -3
- package/dist/components/views/panels/PlanningDrawer.js +5 -12
- package/dist/components/views/standalone/modals/AccountSettingsModal.js +26 -17
- package/dist/config/envSchema.js +12 -0
- package/dist/config/localServiceRuntimeContract.d.ts +13 -0
- package/dist/config/localServiceRuntimeContract.js +135 -0
- package/dist/core/AiProfileService.js +46 -8
- package/dist/core/DeletedTaskLifecycleService.d.ts +1 -0
- package/dist/core/DeletedTaskLifecycleService.js +37 -6
- package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +3 -2
- package/dist/core/TaskAttachmentLinksDurableMutationService.js +33 -9
- package/dist/core/Taskforce.d.ts +62 -2
- package/dist/core/Taskforce.js +635 -32
- package/dist/core/types.d.ts +6 -0
- package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +1 -1
- package/dist/hooks/sync/useDurableTaskCreateMutation.js +1 -1
- package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +1 -0
- package/dist/hooks/sync/useDurableTaskMetadataMutation.js +1 -0
- package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +1 -0
- package/dist/hooks/tasks/taskHttpMutation.js +1 -0
- package/dist/hooks/tasks/useCloudTaskSearch.d.ts +12 -0
- package/dist/hooks/tasks/useCloudTaskSearch.js +71 -0
- package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.d.ts +16 -0
- package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.js +197 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +16 -3
- package/dist/hooks/tasks/useTaskEditorNavigation.js +72 -13
- package/dist/hooks/tasks/useTaskFormActions.d.ts +6 -0
- package/dist/hooks/tasks/useTaskFormActions.js +65 -12
- package/dist/hooks/ui/useSettingsModel.d.ts +24 -2
- package/dist/hooks/ui/useSettingsModel.js +13 -2
- package/dist/hooks/ui/useSettingsPersistence.d.ts +12 -1
- package/dist/hooks/ui/useSettingsPersistence.js +74 -47
- package/dist/hooks/useFilterSort.d.ts +7 -3
- package/dist/hooks/useFilterSort.js +15 -5
- package/dist/hooks/useTaskData.d.ts +10 -1
- package/dist/hooks/useTaskData.js +216 -7
- package/dist/hooks/useTaskMutations.d.ts +10 -2
- package/dist/hooks/useTaskMutations.js +223 -43
- package/dist/hooks/useTaskforce.d.ts +37 -9
- package/dist/hooks/useTaskforce.js +349 -39
- package/dist/hooks/useUiNotice.d.ts +4 -0
- package/dist/hooks/useUiNotice.js +4 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +3 -3
- package/dist/mcp/documentAssetRegistrar.js +44 -35
- package/dist/mcp/documentHelpers.d.ts +1 -0
- package/dist/mcp/documentHelpers.js +7 -3
- package/dist/mcp/runtime.js +172 -42
- package/dist/mcp/taskPlanningRegistrar.js +41 -38
- package/dist/mcp/toolCallCompatibility.js +38 -1
- package/dist/mcp/toolCatalog.d.ts +1 -0
- package/dist/migrations/taskSchemaMigrations.js +12 -0
- package/dist/runtime/appGateDefinitions.d.ts +30 -6
- package/dist/runtime/appGateDefinitions.js +6 -6
- package/dist/runtime/appGates.d.ts +12 -13
- package/dist/runtime/appGates.js +29 -13
- package/dist/runtime/nodeAppGates.d.ts +8 -0
- package/dist/runtime/nodeAppGates.js +27 -0
- package/dist/server/cloudCollectionRead.d.ts +1 -1
- package/dist/server/cloudCollectionRead.js +2 -2
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +15 -3
- package/dist/server/localServiceLease.d.ts +3 -0
- package/dist/server/localServiceLease.js +9 -1
- package/dist/server/localWorkspaceSyncServerRuntime.js +10 -3
- package/dist/server/rateLimit.d.ts +1 -0
- package/dist/server/rateLimit.js +4 -0
- package/dist/server/realtimeSyncWs.js +36 -12
- package/dist/server/routes/admin.js +1 -0
- package/dist/server/routes/agentRoles.js +3 -2
- package/dist/server/routes/agentSkills.js +3 -2
- package/dist/server/routes/agents.js +118 -16
- package/dist/server/routes/annotatedAttachments.js +3 -2
- package/dist/server/routes/auth.js +3 -1
- package/dist/server/routes/durableTaskHttpRouters.d.ts +9 -2
- package/dist/server/routes/localService.js +2 -0
- package/dist/server/routes/primitives.d.ts +2 -0
- package/dist/server/routes/syncAuxRoutes.js +5 -2
- package/dist/server/routes/syncPullApplyRoutes.js +23 -4
- package/dist/server/routes/syncV3FeedRoutes.js +18 -16
- package/dist/server/routes/syncV3LocalCaptureRoutes.js +28 -2
- package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +1 -0
- package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +26 -0
- package/dist/server/routes/syncV3OperationRoutes.js +10 -0
- package/dist/server/routes/tasks.js +103 -11
- package/dist/server/routes/workspaces.js +43 -1
- package/dist/server/workspaceDeletionAdmission.d.ts +23 -0
- package/dist/server/workspaceDeletionAdmission.js +50 -0
- package/dist/service/localServiceCli.js +16 -0
- package/dist/service/localServiceClientLifecycle.js +36 -10
- package/dist/services/codexAppServerModelGateway.d.ts +112 -0
- package/dist/services/codexAppServerModelGateway.js +438 -0
- package/dist/services/taskforceAgentMcpBridge.d.ts +7 -1
- package/dist/services/taskforceAgentMcpBridge.js +23 -3
- package/dist/shared/taskforceAgentDefinition.js +7 -1
- package/dist/shared/taskforceAgentModels.d.ts +1 -0
- package/dist/shared/taskforceAgentModels.js +30 -1
- package/dist/storage/postgresAdapter.d.ts +20 -3
- package/dist/storage/postgresAdapter.js +106 -14
- package/dist/storage/postgresWorker.js +13 -4
- package/dist/storage/postgresWorkerProtocol.d.ts +29 -0
- package/dist/storage/postgresWorkerProtocol.js +30 -0
- package/dist/sync/cloudSyncApi.d.ts +4 -1
- package/dist/sync/cloudSyncApi.js +7 -2
- package/dist/sync/contentSyncState.d.ts +1 -1
- package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +1 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.js +7 -1
- package/dist/sync/coordinator/localWorkspaceSyncRunner.js +6 -2
- package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +20 -2
- package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +4 -0
- package/dist/sync/engine/workspaceSyncEngineLocalServices.js +11 -0
- package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +11 -0
- package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +2 -1
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +31 -4
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +3 -1
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +25 -8
- package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +1 -1
- package/dist/sync/engine/workspaceSyncEngineTransfers.js +8 -3
- package/dist/sync/syncService.d.ts +2 -0
- package/dist/sync/syncService.js +4 -1
- package/dist/sync/v3/combinedFeedCapability.js +4 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +2 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +11 -1
- package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +2 -0
- package/dist/sync/v3/durableTaskChecklistMutationRouter.js +4 -0
- package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +1 -0
- package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +30 -8
- package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +5 -3
- package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +27 -2
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +2 -0
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +47 -7
- package/dist/sync/v3/localTaskCreateOutboxService.js +15 -6
- package/dist/sync/v3/localTaskMetadataClient.d.ts +1 -0
- package/dist/sync/v3/localTaskMetadataClient.js +1 -0
- package/dist/sync/v3/localTaskMetadataOutboxHandler.js +5 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -0
- package/dist/sync/v3/localTaskMetadataOutboxService.js +7 -0
- package/dist/sync/v3/localTaskRestoreOutboxService.js +14 -6
- package/dist/sync/v3/localTaskStatusClient.d.ts +1 -0
- package/dist/sync/v3/localTaskStatusClient.js +6 -1
- package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -0
- package/dist/sync/v3/localTaskStatusOutboxService.js +54 -13
- package/dist/sync/v3/syncDurabilityStore.d.ts +7 -0
- package/dist/sync/v3/syncDurabilityStore.js +31 -11
- package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +6 -4
- package/dist/sync/v3/taskAttachmentLinksMutationService.js +19 -10
- package/dist/sync/v3/taskChecklistMutationService.d.ts +4 -2
- package/dist/sync/v3/taskChecklistMutationService.js +4 -0
- package/dist/sync/v3/taskCreateMutationService.d.ts +2 -2
- package/dist/sync/v3/taskCreateMutationService.js +7 -4
- package/dist/sync/v3/taskMetadataMutationService.d.ts +3 -0
- package/dist/sync/v3/taskMetadataMutationService.js +4 -0
- package/dist/sync/v3/taskRestoreMutationService.d.ts +2 -2
- package/dist/sync/v3/taskRestoreMutationService.js +7 -4
- package/dist/sync/v3/taskStatusMutationService.d.ts +2 -0
- package/dist/sync/v3/taskStatusMutationService.js +4 -0
- package/dist/sync/v3/workflowAssignmentSync.d.ts +1 -0
- package/dist/sync/v3/workflowAssignmentSync.js +3 -0
- package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +1 -1
- package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +13 -3
- package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +11 -0
- package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +27 -10
- package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +2 -1
- package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +52 -10
- package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +9 -1
- package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +8 -3
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +51 -10
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +7 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +30 -18
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +9 -4
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +98 -47
- package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +10 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +58 -21
- package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +3 -1
- package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +18 -0
- package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +6 -0
- package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +1 -1
- package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +23 -10
- package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +3 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +64 -13
- package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +9 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +26 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +15 -5
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +50 -14
- package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +2 -1
- package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +11 -3
- package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +19 -7
- package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +9 -2
- package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.d.ts +7 -0
- package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.js +37 -0
- package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.d.ts +6 -0
- package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.js +9 -0
- package/dist/sync/workspacePullFeed.js +2 -1
- package/dist/sync/workspaceRepair.d.ts +3 -0
- package/dist/sync/workspaceRepair.js +67 -6
- package/dist/types.d.ts +47 -1
- package/dist/types.js +6 -0
- package/dist/ui/assets/{AiIdentityRosterCard-MZs7lVED.js → AiIdentityRosterCard-BZM4aQnM.js} +1 -1
- package/dist/ui/assets/AiProfilesModule-BbAEWtHH.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-B68Wg-RV.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-TzYu7pkG.css +1 -0
- package/dist/ui/assets/{AssetTraySortControl-Kf1pelSy.js → AssetTraySortControl-C8Rztyal.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-B8boOs6Q.js → ContextAttachmentManager-Cj3c7X9y.js} +2 -2
- package/dist/ui/assets/{DocumentWorkspace-DXH4XUsB.js → DocumentWorkspace-B5uvzems.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-BtSTq0ON.js → EntityActivityTimeline-BVFvCB5R.js} +1 -1
- package/dist/ui/assets/PlanningModule-B5DWQOGz.js +1 -0
- package/dist/ui/assets/PlansPage-AY4cGWco.js +1 -0
- package/dist/ui/assets/TaskContextUpload-DZxTVesW.js +1 -0
- package/dist/ui/assets/TaskSettings-BeWAuyZj.js +12 -0
- package/dist/ui/assets/{TaskforceAgentsModule-BazdUwxZ.js → TaskforceAgentsModule-BifCYtjR.js} +6 -6
- package/dist/ui/assets/{WorkflowManagerModule-DWFspC-o.js → WorkflowManagerModule-DEr5di_r.js} +1 -1
- package/dist/ui/assets/index-I5zhdtgt.js +7 -0
- package/dist/ui/assets/index-VRFsx3wz.css +1 -0
- package/dist/ui/assets/{vendor-icons-BkFLXavV.js → vendor-icons-D9Lpw-j4.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/taskActivity.d.ts +3 -2
- package/dist/utils/taskActivity.js +19 -2
- package/dist/utils/taskEditorReconciliation.d.ts +40 -0
- package/dist/utils/taskEditorReconciliation.js +217 -0
- package/dist/utils/taskNormalization.d.ts +1 -0
- package/dist/utils/taskNormalization.js +39 -0
- package/dist/utils/taskSearch.js +2 -2
- package/dist/utils/uiNotice.d.ts +4 -0
- package/package.json +16 -4
- package/dist/ui/assets/AiProfilesModule-C3i0LOxe.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-DVZk5Tln.js +0 -3
- package/dist/ui/assets/PlanningModule-BPVyEa-f.js +0 -1
- package/dist/ui/assets/PlansPage-ChqgxALc.js +0 -1
- package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +0 -1
- package/dist/ui/assets/TaskSettings-g4ECKNOz.js +0 -12
- package/dist/ui/assets/index-BUplSsv_.js +0 -7
- package/dist/ui/assets/index-gz2EXuoK.css +0 -1
|
@@ -186,6 +186,7 @@ export function registerLocalServiceRoutes(deps) {
|
|
|
186
186
|
projectIdentity: context.projectIdentity,
|
|
187
187
|
dataIdentity: context.ownershipKey,
|
|
188
188
|
cloudTarget: context.cloudTarget,
|
|
189
|
+
runtimeContract: context.runtimeContract,
|
|
189
190
|
transports: ['streamable-http'],
|
|
190
191
|
health: {
|
|
191
192
|
lifecycle: context.sessions.state(),
|
|
@@ -281,6 +282,7 @@ export function registerLocalServiceRoutes(deps) {
|
|
|
281
282
|
protocolVersion: context.protocolVersion,
|
|
282
283
|
ownerPid: context.ownerPid,
|
|
283
284
|
cloudTarget: context.cloudTarget,
|
|
285
|
+
runtimeContract: context.runtimeContract,
|
|
284
286
|
sessions: context.sessions.list(),
|
|
285
287
|
...(context.diagnostics?.() || {}),
|
|
286
288
|
});
|
|
@@ -5,6 +5,7 @@ import type { LocalSyncCoordinatorRouteServices } from '../../sync/coordinator/l
|
|
|
5
5
|
import type { WorkspaceSyncServerDataServices } from '../../sync/engine/workspaceSyncServerDataServices.js';
|
|
6
6
|
import type { LocalServiceSessionManager } from '../localServiceSessions.js';
|
|
7
7
|
import type { LocalServiceCloudTarget } from '../../config/localServiceCloudTarget.js';
|
|
8
|
+
import type { LocalServiceRuntimeContract } from '../../config/localServiceRuntimeContract.js';
|
|
8
9
|
export interface LocalServiceRouteContext {
|
|
9
10
|
generation: string;
|
|
10
11
|
protocolVersion: string;
|
|
@@ -13,6 +14,7 @@ export interface LocalServiceRouteContext {
|
|
|
13
14
|
bootstrapCapability: string;
|
|
14
15
|
ownershipKey: string;
|
|
15
16
|
cloudTarget: LocalServiceCloudTarget;
|
|
17
|
+
runtimeContract: LocalServiceRuntimeContract;
|
|
16
18
|
projectIdentity: string;
|
|
17
19
|
operationClientId: string;
|
|
18
20
|
sessions: LocalServiceSessionManager;
|
|
@@ -50,8 +50,11 @@ export function registerWorkspaceSyncAuxRoutes({ addRoute, core, context, resolv
|
|
|
50
50
|
res.end(JSON.stringify({ success: true }));
|
|
51
51
|
});
|
|
52
52
|
// POST /api/taskforce/sync/workspace/repair-startup - Auto-repair workspace asset/document
|
|
53
|
-
// metadata on workspace open. Safe fixups re-register metadata for local files and
|
|
54
|
-
// stale
|
|
53
|
+
// metadata on workspace open. Safe fixups re-register metadata for local files and clean up
|
|
54
|
+
// stale links. Does NOT tombstone metadata for files that appear missing locally — a single
|
|
55
|
+
// failed existence check at startup can mean the content is only temporarily unavailable
|
|
56
|
+
// (e.g. storage not yet mounted/synced), not deleted; that requires explicit admin
|
|
57
|
+
// confirmation via /api/taskforce/admin/workspace-repair/apply. Never deletes files.
|
|
55
58
|
// Called once by the sync orchestrator when the workspace becomes active.
|
|
56
59
|
addRoute('POST', '/api/taskforce/sync/workspace/repair-startup', async (req, res) => {
|
|
57
60
|
const authConfig = context.authConfig;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_WORKSPACE_PULL_PAGE_MAX_BYTES, buildWorkspacePullFeed, WorkspacePullEntryTooLargeError, } from '../../sync/workspacePullFeed.js';
|
|
2
2
|
import { applyWorkspaceSyncLocalBatch } from '../../sync/engine/workspaceSyncEngineLocalApply.js';
|
|
3
3
|
import { parseJsonBody } from './primitives.js';
|
|
4
|
-
import { normalizeWorkspaceSyncV2ProjectionExclusions, parseWorkspaceSyncV2ProjectionExclusions, WORKSPACE_SYNC_V3_COMBINED_V2_EXCLUSIONS, } from '../../sync/v3/workspaceSyncV2ProjectionExclusions.js';
|
|
4
|
+
import { normalizeWorkspaceSyncV2ProjectionExclusions, parseWorkspaceSyncV2ProjectionExclusions, WORKSPACE_SYNC_V3_COMBINED_V2_EXCLUSIONS, workspaceSyncV2ProjectionExclusionsEqual, } from '../../sync/v3/workspaceSyncV2ProjectionExclusions.js';
|
|
5
5
|
import { resolveWorkspaceSyncV3AttestedV2Exclusions } from '../../sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js';
|
|
6
6
|
import { requireAuthenticatedSyncRequest, requireEnabledSyncAuth, resolveAuthorizedSyncWorkspace, } from './syncRouteGuards.js';
|
|
7
7
|
import { completeLocalSyncBrowserPushEchoPermitGroup, createLocalSyncBrowserV2ApplyTransactionHooks, parseLocalSyncBrowserApplyPermitContext, resolveLocalSyncBrowserApplyPermitOperationClass, } from '../../sync/coordinator/localSyncBrowserApplyPermitContext.js';
|
|
@@ -301,7 +301,16 @@ export function registerWorkspacePullApplySyncRoutes({ addRoute, core, context,
|
|
|
301
301
|
};
|
|
302
302
|
}
|
|
303
303
|
if (!change) {
|
|
304
|
-
missing.push(
|
|
304
|
+
missing.push({
|
|
305
|
+
...request,
|
|
306
|
+
disposition: request.kind === 'asset'
|
|
307
|
+
&& 'assetId' in request
|
|
308
|
+
&& (!requestedAsset
|
|
309
|
+
|| requestedAsset.deletedAt
|
|
310
|
+
|| requestedAsset.storageProvider === 'external')
|
|
311
|
+
? 'terminal-unavailable'
|
|
312
|
+
: 'retryable',
|
|
313
|
+
});
|
|
305
314
|
consumed += 1;
|
|
306
315
|
continue;
|
|
307
316
|
}
|
|
@@ -554,6 +563,16 @@ export function registerWorkspacePullApplySyncRoutes({ addRoute, core, context,
|
|
|
554
563
|
: null;
|
|
555
564
|
let applyResult;
|
|
556
565
|
if (permitContext && coordinatorServices && permitOperationClass === 'push') {
|
|
566
|
+
const attestedV2ExcludedProjections = v2ExcludedProjections.length > 0
|
|
567
|
+
? (() => {
|
|
568
|
+
const attested = resolveWorkspaceSyncV3AttestedV2Exclusions({
|
|
569
|
+
coverage: body?.combinedCoverage,
|
|
570
|
+
excludedProjections: v2ExcludedProjections,
|
|
571
|
+
runtimeMode: 'local',
|
|
572
|
+
});
|
|
573
|
+
return attested && workspaceSyncV2ProjectionExclusionsEqual(attested, v2ExcludedProjections) ? attested : null;
|
|
574
|
+
})()
|
|
575
|
+
: [];
|
|
557
576
|
applyResult = completeLocalSyncBrowserPushEchoPermitGroup({
|
|
558
577
|
services: coordinatorServices,
|
|
559
578
|
cookieHeader: req.headers.cookie,
|
|
@@ -567,13 +586,13 @@ export function registerWorkspacePullApplySyncRoutes({ addRoute, core, context,
|
|
|
567
586
|
|| workspaceMembers !== undefined
|
|
568
587
|
|| body?.bootstrapSnapshot !== undefined
|
|
569
588
|
|| allowReferenceNumberReassignment
|
|
570
|
-
||
|
|
589
|
+
|| !attestedV2ExcludedProjections) {
|
|
571
590
|
throw new Error('Browser push echo apply accepts only authoritative task upserts.');
|
|
572
591
|
}
|
|
573
592
|
return dispatchWorkspaceSyncMetadataAtomically(workspaceId, incomingChanges, {
|
|
574
593
|
allowReferenceNumberReassignment: false,
|
|
575
594
|
coexistenceDirection: 'local-pull',
|
|
576
|
-
v2ExcludedProjections:
|
|
595
|
+
v2ExcludedProjections: attestedV2ExcludedProjections,
|
|
577
596
|
});
|
|
578
597
|
},
|
|
579
598
|
});
|
|
@@ -5,7 +5,7 @@ import { buildWorkspaceSyncV3InitiativeRepairPage, decodeWorkspaceSyncV3Initiati
|
|
|
5
5
|
import { WORKSPACE_SYNC_V3_COVERAGE_PROFILE } from '../../sync/v3/workspaceSyncV3FeedProfile.js';
|
|
6
6
|
import { isWorkspaceSyncV3InitiativeLegacyPushDisabled } from '../../sync/v3/planningActivation.js';
|
|
7
7
|
import { WORKSPACE_SYNC_V3_COMBINED_COVERAGE_PROFILE } from '../../sync/v3/workspaceSyncV3CombinedCoverage.js';
|
|
8
|
-
import { buildWorkspaceSyncV3CombinedRepairPage, decodeWorkspaceSyncV3CombinedRepairCursor, decodeWorkspaceSyncV3NestedRepairCursor, decodeWorkspaceSyncV3PlanningOrderRepairCursor, decodeWorkspaceSyncV3WorkflowRepairCursor, WorkspaceSyncV3CombinedRepairSnapshotUnavailableError, } from '../../sync/v3/workspaceSyncV3CombinedRepairMaterialization.js';
|
|
8
|
+
import { buildWorkspaceSyncV3CombinedRepairPage, decodeWorkspaceSyncV3CombinedRepairCursor, decodeWorkspaceSyncV3NestedRepairCursor, decodeWorkspaceSyncV3PlanningOrderRepairCursor, decodeWorkspaceSyncV3TaskCoverRepairCursor, decodeWorkspaceSyncV3WorkflowRepairCursor, WorkspaceSyncV3CombinedRepairSnapshotUnavailableError, } from '../../sync/v3/workspaceSyncV3CombinedRepairMaterialization.js';
|
|
9
9
|
import { isWorkspaceSyncV3CombinedLegacyFenceEnabled } from '../../sync/v3/workspaceSyncV2ProjectionExclusions.js';
|
|
10
10
|
import { resolveCombinedFeedCapability } from '../../sync/v3/combinedFeedCapability.js';
|
|
11
11
|
import { resolveWorkspaceSyncV3RegisteredCoverage } from '../../sync/v3/workspaceSyncV3CoverageRegistry.js';
|
|
@@ -21,6 +21,17 @@ function json(res, status, body) {
|
|
|
21
21
|
res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
22
22
|
res.end(JSON.stringify(body));
|
|
23
23
|
}
|
|
24
|
+
function decodeCombinedRepairCursorForContract(rawCursor, keyring, wire) {
|
|
25
|
+
if (wire.taskCoverProjectionRequired)
|
|
26
|
+
return decodeWorkspaceSyncV3TaskCoverRepairCursor(rawCursor, keyring);
|
|
27
|
+
if (wire.workflowWriterPrerequisitesRequired)
|
|
28
|
+
return decodeWorkspaceSyncV3WorkflowRepairCursor(rawCursor, keyring);
|
|
29
|
+
if (wire.planningOrderWriterPrerequisitesRequired)
|
|
30
|
+
return decodeWorkspaceSyncV3PlanningOrderRepairCursor(rawCursor, keyring);
|
|
31
|
+
if (wire.nestedWriterPrerequisitesRequired)
|
|
32
|
+
return decodeWorkspaceSyncV3NestedRepairCursor(rawCursor, keyring);
|
|
33
|
+
return decodeWorkspaceSyncV3CombinedRepairCursor(rawCursor, keyring);
|
|
34
|
+
}
|
|
24
35
|
function attestLegacyPushBoundary(body, boundary) {
|
|
25
36
|
const attested = { ...body, ...boundary };
|
|
26
37
|
const mutable = attested;
|
|
@@ -215,13 +226,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
|
|
|
215
226
|
return;
|
|
216
227
|
}
|
|
217
228
|
const rawCursor = String(url.searchParams.get('cursor') || '').trim();
|
|
218
|
-
const cursor = requestedWire
|
|
219
|
-
? decodeWorkspaceSyncV3WorkflowRepairCursor(rawCursor, keyring)
|
|
220
|
-
: requestedWire.planningOrderWriterPrerequisitesRequired
|
|
221
|
-
? decodeWorkspaceSyncV3PlanningOrderRepairCursor(rawCursor, keyring)
|
|
222
|
-
: requestedWire.nestedWriterPrerequisitesRequired
|
|
223
|
-
? decodeWorkspaceSyncV3NestedRepairCursor(rawCursor, keyring)
|
|
224
|
-
: decodeWorkspaceSyncV3CombinedRepairCursor(rawCursor, keyring);
|
|
229
|
+
const cursor = decodeCombinedRepairCursorForContract(rawCursor, keyring, requestedWire);
|
|
225
230
|
if (rawCursor && !cursor) {
|
|
226
231
|
json(res, 400, { success: false, code: 'INVALID_SYNC_COMBINED_REPAIR_CURSOR', error: 'Invalid combined repair cursor.' });
|
|
227
232
|
return;
|
|
@@ -231,7 +236,8 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
|
|
|
231
236
|
cursorKeyring: keyring, cursor, pageSize: Number.isFinite(limit) ? limit : undefined,
|
|
232
237
|
nestedCoverage: requestedWire.nestedWriterPrerequisitesRequired,
|
|
233
238
|
workflowCoverage: requestedWire.workflowWriterPrerequisitesRequired,
|
|
234
|
-
planningOrderCoverage: requestedWire.planningOrderWriterPrerequisitesRequired
|
|
239
|
+
planningOrderCoverage: requestedWire.planningOrderWriterPrerequisitesRequired,
|
|
240
|
+
taskCoverCoverage: requestedWire.taskCoverProjectionRequired });
|
|
235
241
|
json(res, 200, attestLegacyPushBoundary({ ...page }, {
|
|
236
242
|
contractVersion: 3,
|
|
237
243
|
legacyV2Boundary: { cloudWritesFenced: false, excludedProjections: [] },
|
|
@@ -328,13 +334,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
|
|
|
328
334
|
return;
|
|
329
335
|
}
|
|
330
336
|
const repairCursor = coverage.kind === 'combined'
|
|
331
|
-
? (coverage.wire
|
|
332
|
-
? decodeWorkspaceSyncV3WorkflowRepairCursor(rawRepairCursor, cursorKeyring)
|
|
333
|
-
: coverage.wire.planningOrderWriterPrerequisitesRequired
|
|
334
|
-
? decodeWorkspaceSyncV3PlanningOrderRepairCursor(rawRepairCursor, cursorKeyring)
|
|
335
|
-
: coverage.wire.nestedWriterPrerequisitesRequired
|
|
336
|
-
? decodeWorkspaceSyncV3NestedRepairCursor(rawRepairCursor, cursorKeyring)
|
|
337
|
-
: decodeWorkspaceSyncV3CombinedRepairCursor(rawRepairCursor, cursorKeyring))
|
|
337
|
+
? decodeCombinedRepairCursorForContract(rawRepairCursor, cursorKeyring, coverage.wire)
|
|
338
338
|
: decodeWorkspaceSyncV3InitiativeRepairCursor(rawRepairCursor, cursorKeyring);
|
|
339
339
|
if (rawRepairCursor && (!repairEnabled || !repairCursor)) {
|
|
340
340
|
json(res, 400, { success: false, code: 'INVALID_SYNC_V3_REPAIR_CURSOR', error: 'Invalid repair cursor.' });
|
|
@@ -353,6 +353,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
|
|
|
353
353
|
nestedCoverage: coverage.wire.nestedWriterPrerequisitesRequired,
|
|
354
354
|
workflowCoverage: coverage.wire.workflowWriterPrerequisitesRequired,
|
|
355
355
|
planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired,
|
|
356
|
+
taskCoverCoverage: coverage.wire.taskCoverProjectionRequired,
|
|
356
357
|
coveredLegacyWritesFenced: combinedLegacyPushDisabled })
|
|
357
358
|
: buildWorkspaceSyncV3InitiativeRepairPage({ core: input.core, workspaceId,
|
|
358
359
|
cursorKeyring: cursorKeyring, cursor: repairCursor,
|
|
@@ -399,6 +400,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
|
|
|
399
400
|
nestedCoverage: coverage.wire.nestedWriterPrerequisitesRequired,
|
|
400
401
|
workflowCoverage: coverage.wire.workflowWriterPrerequisitesRequired,
|
|
401
402
|
planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired,
|
|
403
|
+
taskCoverCoverage: coverage.wire.taskCoverProjectionRequired,
|
|
402
404
|
coveredLegacyWritesFenced: combinedLegacyPushDisabled })
|
|
403
405
|
: buildWorkspaceSyncV3InitiativeRepairPage({ core: input.core, workspaceId,
|
|
404
406
|
cursorKeyring: cursorKeyring, pageSize: Number.isFinite(limit) ? limit : undefined });
|
|
@@ -96,7 +96,14 @@ function parseEnvelope(raw) {
|
|
|
96
96
|
};
|
|
97
97
|
if (operation.domain === 'task' && operation.mutationKind === 'metadata-update') {
|
|
98
98
|
const saveSource = payload.saveSource;
|
|
99
|
-
|
|
99
|
+
const expectedDetailRevision = payload.expectedDetailRevision === undefined
|
|
100
|
+
? undefined
|
|
101
|
+
: requireId(payload.expectedDetailRevision, 'operation.payload.expectedDetailRevision');
|
|
102
|
+
assertExactKeys(payload, [
|
|
103
|
+
'patch',
|
|
104
|
+
...(saveSource === undefined ? [] : ['saveSource']),
|
|
105
|
+
...(expectedDetailRevision === undefined ? [] : ['expectedDetailRevision']),
|
|
106
|
+
], 'operation.payload');
|
|
100
107
|
if (saveSource !== undefined && saveSource !== 'manual' && saveSource !== 'autosave') {
|
|
101
108
|
throw new LocalCaptureRequestError('operation.payload.saveSource must be manual or autosave.');
|
|
102
109
|
}
|
|
@@ -106,6 +113,7 @@ function parseEnvelope(raw) {
|
|
|
106
113
|
mutationKind: 'metadata-update',
|
|
107
114
|
patch: requireRecord(payload.patch, 'operation.payload.patch'),
|
|
108
115
|
...(saveSource ? { saveSource } : {}),
|
|
116
|
+
...(expectedDetailRevision ? { expectedDetailRevision } : {}),
|
|
109
117
|
};
|
|
110
118
|
}
|
|
111
119
|
if (operation.domain === 'task' && operation.mutationKind === 'create') {
|
|
@@ -170,11 +178,27 @@ function parseEnvelope(raw) {
|
|
|
170
178
|
};
|
|
171
179
|
}
|
|
172
180
|
if (operation.domain === 'task' && operation.mutationKind === 'set-status') {
|
|
181
|
+
const expectedDetailRevision = payload.expectedDetailRevision === undefined
|
|
182
|
+
? undefined
|
|
183
|
+
: requireId(payload.expectedDetailRevision, 'operation.payload.expectedDetailRevision');
|
|
184
|
+
assertExactKeys(payload, [
|
|
185
|
+
'status',
|
|
186
|
+
'assigneeIntent',
|
|
187
|
+
'canceledReason',
|
|
188
|
+
'metadataPatch',
|
|
189
|
+
...(expectedDetailRevision === undefined ? [] : ['expectedDetailRevision']),
|
|
190
|
+
], 'operation.payload');
|
|
173
191
|
return {
|
|
174
192
|
...common,
|
|
175
193
|
domain: 'task',
|
|
176
194
|
mutationKind: 'set-status',
|
|
177
|
-
payload: validateDurableTaskStatusMutationPayload(
|
|
195
|
+
payload: validateDurableTaskStatusMutationPayload({
|
|
196
|
+
status: payload.status,
|
|
197
|
+
assigneeIntent: payload.assigneeIntent,
|
|
198
|
+
canceledReason: payload.canceledReason,
|
|
199
|
+
metadataPatch: payload.metadataPatch,
|
|
200
|
+
}),
|
|
201
|
+
...(expectedDetailRevision ? { expectedDetailRevision } : {}),
|
|
178
202
|
};
|
|
179
203
|
}
|
|
180
204
|
if (operation.domain === 'task-comments' && operation.mutationKind === 'append') {
|
|
@@ -731,6 +755,7 @@ export function registerWorkspaceSyncV3LocalCaptureRoutes(input) {
|
|
|
731
755
|
taskId: envelope.entityId,
|
|
732
756
|
patch: envelope.patch,
|
|
733
757
|
saveSource: envelope.saveSource,
|
|
758
|
+
expectedDetailRevision: envelope.expectedDetailRevision,
|
|
734
759
|
now: acceptedAt,
|
|
735
760
|
actorRef: 'user',
|
|
736
761
|
onPostCommitError: input.onPostCommitError,
|
|
@@ -786,6 +811,7 @@ export function registerWorkspaceSyncV3LocalCaptureRoutes(input) {
|
|
|
786
811
|
identity,
|
|
787
812
|
taskId: envelope.entityId,
|
|
788
813
|
payload: envelope.payload,
|
|
814
|
+
expectedDetailRevision: envelope.expectedDetailRevision,
|
|
789
815
|
now: acceptedAt,
|
|
790
816
|
actorRef: 'user',
|
|
791
817
|
onPostCommitError: input.onPostCommitError,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SyncAddRoute, SyncRouteAuthConfig, SyncRouteCore } from './syncRouteTypes.js';
|
|
2
2
|
import type { LocalSyncCoordinatorRouteServices } from '../../sync/coordinator/localSyncCoordinatorRouteServices.js';
|
|
3
|
+
export declare function revisionConflictDetails(error: unknown): Record<string, unknown> | undefined;
|
|
3
4
|
export declare function isWorkspaceSyncV3LocalFeedApplyEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
4
5
|
export declare function isWorkspaceSyncV3CombinedLocalFeedApplyEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
5
6
|
export declare function registerWorkspaceSyncV3LocalFeedApplyRoutes(input: {
|
|
@@ -8,6 +8,7 @@ import { resolveCombinedFeedCapability, resolveRequestedCombinedFeedCoverage } f
|
|
|
8
8
|
import { parseJsonBody } from './primitives.js';
|
|
9
9
|
import { applyWorkspaceSyncV3TaskCommentAuditPage, readWorkspaceSyncV3TaskCommentAuditCursor, readWorkspaceSyncV3TaskCommentAuditState, } from '../../sync/v3/workspaceSyncV3TaskCommentAudit.js';
|
|
10
10
|
import { createLocalSyncBrowserApplyPermitTransactionHooks, parseLocalSyncBrowserApplyPermitContext, } from '../../sync/coordinator/localSyncBrowserApplyPermitContext.js';
|
|
11
|
+
import { WorkspaceSyncRevisionConflictError } from '../../sync/v3/syncDurabilityStore.js';
|
|
11
12
|
const ROUTE_PATH = '/api/taskforce/sync/v3/local/feed';
|
|
12
13
|
const TASK_COMMENT_AUDIT_PATH = '/api/taskforce/sync/v3/local/task-comment-audit';
|
|
13
14
|
const MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
@@ -15,6 +16,29 @@ function writeJson(res, statusCode, body) {
|
|
|
15
16
|
res.writeHead(statusCode, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
16
17
|
res.end(JSON.stringify(body));
|
|
17
18
|
}
|
|
19
|
+
export function revisionConflictDetails(error) {
|
|
20
|
+
if (!(error instanceof WorkspaceSyncRevisionConflictError) || !error.details)
|
|
21
|
+
return undefined;
|
|
22
|
+
const details = error.details;
|
|
23
|
+
const coordinates = (value) => {
|
|
24
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
25
|
+
return undefined;
|
|
26
|
+
const source = value;
|
|
27
|
+
const sequence = source.sequence ?? source.lastSequence;
|
|
28
|
+
return {
|
|
29
|
+
sequence: sequence == null ? null : String(sequence),
|
|
30
|
+
entityRevision: source.entityRevision == null ? null : String(source.entityRevision),
|
|
31
|
+
lifecycleRevision: source.lifecycleRevision == null ? null : String(source.lifecycleRevision),
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
domain: String(details.domain || ''),
|
|
36
|
+
entityId: String(details.entityId || ''),
|
|
37
|
+
reason: String(details.reason || ''),
|
|
38
|
+
...(coordinates(details.incoming) ? { incoming: coordinates(details.incoming) } : {}),
|
|
39
|
+
...(coordinates(details.current) ? { current: coordinates(details.current) } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
18
42
|
function workspaceFromUrl(req) {
|
|
19
43
|
try {
|
|
20
44
|
return String(new URL(req.url || ROUTE_PATH, 'http://localhost').searchParams.get('workspaceId') || '').trim();
|
|
@@ -481,12 +505,14 @@ export function registerWorkspaceSyncV3LocalFeedApplyRoutes(input) {
|
|
|
481
505
|
catch (error) {
|
|
482
506
|
const causeCode = String(error?.code || '').trim();
|
|
483
507
|
const causeAssetId = String(error?.assetId || '').trim();
|
|
508
|
+
const causeDetails = revisionConflictDetails(error);
|
|
484
509
|
writeJson(res, 409, {
|
|
485
510
|
success: false,
|
|
486
511
|
code: 'WORKSPACE_SYNC_V3_LOCAL_FEED_APPLY_REJECTED',
|
|
487
512
|
error: String(error?.message || error),
|
|
488
513
|
...(causeCode ? { causeCode } : {}),
|
|
489
514
|
...(causeAssetId ? { causeAssetId } : {}),
|
|
515
|
+
...(causeDetails ? { causeDetails } : {}),
|
|
490
516
|
});
|
|
491
517
|
}
|
|
492
518
|
});
|
|
@@ -157,9 +157,13 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
|
|
|
157
157
|
const payload = requireObject(operation.payload, 'operation.payload');
|
|
158
158
|
if (operation.domain === 'task' && operation.mutationKind === 'metadata-update') {
|
|
159
159
|
const saveSource = payload.saveSource;
|
|
160
|
+
const expectedDetailRevision = payload.expectedDetailRevision === undefined
|
|
161
|
+
? undefined
|
|
162
|
+
: requireId(payload.expectedDetailRevision, 'operation.payload.expectedDetailRevision');
|
|
160
163
|
assertExactKeys(payload, [
|
|
161
164
|
'patch',
|
|
162
165
|
...(saveSource === undefined ? [] : ['saveSource']),
|
|
166
|
+
...(expectedDetailRevision === undefined ? [] : ['expectedDetailRevision']),
|
|
163
167
|
...(payload.originActor === undefined ? [] : ['originActor']),
|
|
164
168
|
], 'operation.payload');
|
|
165
169
|
if (saveSource !== undefined && saveSource !== 'manual' && saveSource !== 'autosave') {
|
|
@@ -173,6 +177,7 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
|
|
|
173
177
|
payload: {
|
|
174
178
|
patch: requireObject(payload.patch, 'operation.payload.patch'),
|
|
175
179
|
...(saveSource ? { saveSource } : {}),
|
|
180
|
+
...(expectedDetailRevision ? { expectedDetailRevision } : {}),
|
|
176
181
|
...(originActor ? { originActor } : {}),
|
|
177
182
|
},
|
|
178
183
|
} };
|
|
@@ -364,6 +369,7 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
|
|
|
364
369
|
...baseKeys,
|
|
365
370
|
...(payload.workflowAssignment === undefined ? [] : ['workflowAssignment']),
|
|
366
371
|
...(payload.originActor === undefined ? [] : ['originActor']),
|
|
372
|
+
...(payload.expectedDetailRevision === undefined ? [] : ['expectedDetailRevision']),
|
|
367
373
|
], 'operation.payload');
|
|
368
374
|
const normalizedPayload = validateDurableTaskStatusMutationPayload({
|
|
369
375
|
status: payload.status,
|
|
@@ -382,12 +388,16 @@ export function parseWorkspaceSyncV3OperationEnvelope(body) {
|
|
|
382
388
|
const originActor = payload.originActor === undefined
|
|
383
389
|
? undefined
|
|
384
390
|
: requireOriginActor(payload.originActor);
|
|
391
|
+
const expectedDetailRevision = payload.expectedDetailRevision === undefined
|
|
392
|
+
? undefined
|
|
393
|
+
: requireId(payload.expectedDetailRevision, 'operation.payload.expectedDetailRevision');
|
|
385
394
|
return { ...common, operation: {
|
|
386
395
|
domain: 'task', entityId, mutationKind: 'set-status', baseEntityRevision, baseLifecycleRevision,
|
|
387
396
|
payload: {
|
|
388
397
|
...normalizedPayload,
|
|
389
398
|
...(workflowAssignment ? { workflowAssignment } : {}),
|
|
390
399
|
...(originActor ? { originActor } : {}),
|
|
400
|
+
...(expectedDetailRevision ? { expectedDetailRevision } : {}),
|
|
391
401
|
},
|
|
392
402
|
} };
|
|
393
403
|
}
|
|
@@ -470,9 +470,66 @@ export function registerTaskRoutes(deps) {
|
|
|
470
470
|
res,
|
|
471
471
|
route: '/api/taskforce/tasks',
|
|
472
472
|
workspaceId,
|
|
473
|
-
|
|
473
|
+
// Local V2 sync still builds an outbound snapshot from these collections.
|
|
474
|
+
// Keep its full graph until that writer is retired; cloud is read-authoritative.
|
|
475
|
+
load: () => (context.authConfig?.runtimeMode === 'cloud' ? 'cloud' : requestRuntimeMode(req)) === 'cloud'
|
|
476
|
+
? core.listActiveTaskSummaries(workspaceId)
|
|
477
|
+
: core.listTasks(workspaceId),
|
|
474
478
|
});
|
|
475
479
|
});
|
|
480
|
+
// GET /api/taskforce/tasks/search - Search lightweight task summaries across lifecycles
|
|
481
|
+
addRoute('GET', '/api/taskforce/tasks/search', async (req, res) => {
|
|
482
|
+
if (!ensureAppAccess(req, res))
|
|
483
|
+
return;
|
|
484
|
+
try {
|
|
485
|
+
const requestUrl = new URL(req.url || '', 'http://localhost');
|
|
486
|
+
const query = String(requestUrl.searchParams.get('query') || '').trim();
|
|
487
|
+
if (!query) {
|
|
488
|
+
throw new TaskforceRuleError('query is required.', 400, 'TASK_SEARCH_QUERY_REQUIRED');
|
|
489
|
+
}
|
|
490
|
+
if (query.length > 200) {
|
|
491
|
+
throw new TaskforceRuleError('query must be 200 characters or fewer.', 400, 'TASK_SEARCH_QUERY_TOO_LONG');
|
|
492
|
+
}
|
|
493
|
+
const scopeInput = String(requestUrl.searchParams.get('scope') || 'all').trim();
|
|
494
|
+
if (!['all', 'active', 'archive'].includes(scopeInput)) {
|
|
495
|
+
throw new TaskforceRuleError('scope must be all, active, or archive.', 400, 'TASK_SEARCH_SCOPE_INVALID');
|
|
496
|
+
}
|
|
497
|
+
const parseBoundedInteger = (name, fallback, max) => {
|
|
498
|
+
const raw = requestUrl.searchParams.get(name);
|
|
499
|
+
if (raw === null || raw === '')
|
|
500
|
+
return fallback;
|
|
501
|
+
const parsed = Number(raw);
|
|
502
|
+
if (!Number.isInteger(parsed) || parsed < (name === 'limit' ? 1 : 0) || parsed > max) {
|
|
503
|
+
throw new TaskforceRuleError(`${name} must be an integer between ${name === 'limit' ? 1 : 0} and ${max}.`, 400, `TASK_SEARCH_${name.toUpperCase()}_INVALID`);
|
|
504
|
+
}
|
|
505
|
+
return parsed;
|
|
506
|
+
};
|
|
507
|
+
const limit = parseBoundedInteger('limit', 50, 50);
|
|
508
|
+
const offset = parseBoundedInteger('offset', 0, 10_000);
|
|
509
|
+
const workspaceId = requestWorkspaceId(req);
|
|
510
|
+
const rows = core.searchTaskSummaries(query, scopeInput, workspaceId, { limit: limit + 1, offset });
|
|
511
|
+
const hasMore = rows.length > limit;
|
|
512
|
+
const matches = rows.slice(0, limit);
|
|
513
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
514
|
+
res.end(JSON.stringify({
|
|
515
|
+
matches,
|
|
516
|
+
pagination: {
|
|
517
|
+
limit,
|
|
518
|
+
offset,
|
|
519
|
+
returned: matches.length,
|
|
520
|
+
hasMore,
|
|
521
|
+
nextOffset: hasMore ? offset + matches.length : null,
|
|
522
|
+
},
|
|
523
|
+
}));
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
527
|
+
const message = error instanceof Error ? error.message : 'Failed to search tasks.';
|
|
528
|
+
const code = error instanceof TaskforceRuleError ? error.code : error?.code;
|
|
529
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
530
|
+
res.end(JSON.stringify({ error: message, code }));
|
|
531
|
+
}
|
|
532
|
+
});
|
|
476
533
|
addRoute('GET', '/api/taskforce/task-relationships', async (req, res) => {
|
|
477
534
|
if (!ensureAppAccess(req, res))
|
|
478
535
|
return;
|
|
@@ -617,6 +674,20 @@ export function registerTaskRoutes(deps) {
|
|
|
617
674
|
}));
|
|
618
675
|
}
|
|
619
676
|
});
|
|
677
|
+
// GET /api/taskforce/task/:id - Read complete active or archived task detail
|
|
678
|
+
addRoute('GET', '/api/taskforce/task/:id', async (req, res, params) => {
|
|
679
|
+
if (!ensureAppAccess(req, res))
|
|
680
|
+
return;
|
|
681
|
+
const workspaceId = requestWorkspaceId(req);
|
|
682
|
+
const task = core.getTaskDetailSnapshot(params.id, workspaceId);
|
|
683
|
+
if (!task) {
|
|
684
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
685
|
+
res.end(JSON.stringify({ error: 'Task not found' }));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
689
|
+
res.end(JSON.stringify(task));
|
|
690
|
+
});
|
|
620
691
|
// PATCH /api/taskforce/task/:id - Update a task
|
|
621
692
|
addRoute('PATCH', '/api/taskforce/task/:id', async (req, res, params) => {
|
|
622
693
|
if (!ensureAppAccess(req, res))
|
|
@@ -625,8 +696,15 @@ export function registerTaskRoutes(deps) {
|
|
|
625
696
|
try {
|
|
626
697
|
const workspaceId = requestWorkspaceId(req);
|
|
627
698
|
const saveSource = body?.saveSource === 'autosave' ? 'autosave' : 'manual';
|
|
699
|
+
const expectedDetailRevision = typeof body?.expectedDetailRevision === 'string'
|
|
700
|
+
? body.expectedDetailRevision.trim()
|
|
701
|
+
: null;
|
|
702
|
+
if (body && Object.prototype.hasOwnProperty.call(body, 'expectedDetailRevision')
|
|
703
|
+
&& !expectedDetailRevision) {
|
|
704
|
+
throw new TaskforceRuleError('expectedDetailRevision must be a non-empty string.', 400, 'TASK_DETAIL_REVISION_INVALID');
|
|
705
|
+
}
|
|
628
706
|
const updates = body && typeof body === 'object'
|
|
629
|
-
? Object.fromEntries(Object.entries(body).filter(([key]) => key !== 'saveSource'))
|
|
707
|
+
? Object.fromEntries(Object.entries(body).filter(([key]) => (key !== 'saveSource' && key !== 'expectedDetailRevision')))
|
|
630
708
|
: body;
|
|
631
709
|
const durableRouters = createTaskHttpDurableRouters({ core, context, req, workspaceId });
|
|
632
710
|
const durableRouter = durableRouters.root;
|
|
@@ -658,10 +736,14 @@ export function registerTaskRoutes(deps) {
|
|
|
658
736
|
const durableTargetCount = Number(checklistDurable)
|
|
659
737
|
+ Number(attachmentLinksDurable)
|
|
660
738
|
+ Number(Object.keys(remainingUpdates).length > 0);
|
|
661
|
-
if (durableTargetCount > 1) {
|
|
739
|
+
if (durableTargetCount > 1 || Boolean(expectedDetailRevision && (checklistDurable || attachmentLinksDurable))) {
|
|
662
740
|
const aggregate = durableRouters.bulk.updateFields([
|
|
663
741
|
{ id: params.id, ...updates },
|
|
664
|
-
], {
|
|
742
|
+
], {
|
|
743
|
+
saveSource,
|
|
744
|
+
atomicItemFailures: true,
|
|
745
|
+
...(expectedDetailRevision ? { expectedDetailRevision } : {}),
|
|
746
|
+
});
|
|
665
747
|
const failure = aggregate.errors[0];
|
|
666
748
|
if (failure) {
|
|
667
749
|
throw new TaskforceRuleError(failure.message, failure.statusCode, failure.code || 'TASK_UPDATE_FAILED', failure.details);
|
|
@@ -671,11 +753,13 @@ export function registerTaskRoutes(deps) {
|
|
|
671
753
|
else {
|
|
672
754
|
updated = Object.keys(remainingUpdates || {}).length > 0
|
|
673
755
|
? (durableRouter.enabled
|
|
674
|
-
? durableRouter.update(params.id, remainingUpdates, saveSource)
|
|
675
|
-
:
|
|
676
|
-
actorRef: requestHumanActorRef(req),
|
|
677
|
-
|
|
678
|
-
|
|
756
|
+
? durableRouter.update(params.id, remainingUpdates, saveSource, undefined, expectedDetailRevision || undefined)
|
|
757
|
+
: expectedDetailRevision !== null
|
|
758
|
+
? core.updateTaskIfDetailRevisionMatches(params.id, expectedDetailRevision, remainingUpdates, workspaceId, { actorRef: requestHumanActorRef(req), saveSource })
|
|
759
|
+
: core.updateTask(params.id, remainingUpdates, workspaceId, {
|
|
760
|
+
actorRef: requestHumanActorRef(req),
|
|
761
|
+
saveSource,
|
|
762
|
+
}))
|
|
679
763
|
: core.getTask(params.id, workspaceId);
|
|
680
764
|
if (updated && checklistDurable) {
|
|
681
765
|
const checklistRouter = durableRouters.createChecklistRouter(durableRouters.deriveChildOperationId('task-patch-checklist', 0, params.id));
|
|
@@ -711,8 +795,14 @@ export function registerTaskRoutes(deps) {
|
|
|
711
795
|
res.end(JSON.stringify({ error: 'Task not found' }));
|
|
712
796
|
return;
|
|
713
797
|
}
|
|
798
|
+
const authoritative = core.getTaskDetailSnapshot(params.id, workspaceId);
|
|
799
|
+
if (!authoritative) {
|
|
800
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
801
|
+
res.end(JSON.stringify({ error: 'Task not found' }));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
714
804
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
715
|
-
res.end(JSON.stringify(
|
|
805
|
+
res.end(JSON.stringify(authoritative));
|
|
716
806
|
}
|
|
717
807
|
catch (error) {
|
|
718
808
|
const statusCode = resolveErrorStatusCode(error, 400);
|
|
@@ -1291,7 +1381,9 @@ export function registerTaskRoutes(deps) {
|
|
|
1291
1381
|
res,
|
|
1292
1382
|
route: '/api/taskforce/archive',
|
|
1293
1383
|
workspaceId,
|
|
1294
|
-
load: () =>
|
|
1384
|
+
load: () => (context.authConfig?.runtimeMode === 'cloud' ? 'cloud' : requestRuntimeMode(req)) === 'cloud'
|
|
1385
|
+
? core.listArchiveSummaries(workspaceId)
|
|
1386
|
+
: core.listArchive(workspaceId),
|
|
1295
1387
|
});
|
|
1296
1388
|
});
|
|
1297
1389
|
// POST /api/taskforce/task/:id/complete - Archive as complete
|
|
@@ -17,9 +17,11 @@ import { parseJsonBody } from './primitives.js';
|
|
|
17
17
|
import { resolveRequestAccess, createSessionTokenCookie, resolveSessionCookieAttributesForRequest, } from '../auth.js';
|
|
18
18
|
import { isWorkspaceSwitchingEnabledForRuntime } from '../../shared/runtimeContract.js';
|
|
19
19
|
import { getRouteTimingNow, logSlowBootstrapRoute, requestBootstrapTraceId, resolveErrorStatusCode, serializeJsonWithTiming, setServerTimingHeader } from './shared.js';
|
|
20
|
+
import { WorkspaceDeletionAdmission } from '../workspaceDeletionAdmission.js';
|
|
20
21
|
// === SECTION: Registration ===
|
|
21
22
|
export function registerWorkspaceRoutes(deps) {
|
|
22
23
|
const { addRoute, core, context, ensureAppAccess, auditAuthAction, getObjectStorageClient, ensureWithinDir, sanitizeStorageSegment, sanitizeTaskIdForPath, normalizeDocumentPathInput, } = deps;
|
|
24
|
+
const workspaceDeletionAdmission = new WorkspaceDeletionAdmission();
|
|
23
25
|
// === SECTION: Storage cleanup helper ===
|
|
24
26
|
const cleanupDeletedWorkspaceStorage = async (summary) => {
|
|
25
27
|
const warnings = [];
|
|
@@ -231,12 +233,49 @@ export function registerWorkspaceRoutes(deps) {
|
|
|
231
233
|
res.end(JSON.stringify({ success: false, error: 'workspaceId is required.', code: 'WORKSPACE_ID_REQUIRED' }));
|
|
232
234
|
return;
|
|
233
235
|
}
|
|
234
|
-
const
|
|
236
|
+
const writeAdmissionError = (decision) => {
|
|
237
|
+
res.setHeader('Retry-After', String(Math.max(1, Math.ceil(decision.retryAfterMs / 1_000))));
|
|
238
|
+
const inProgress = decision.code === 'WORKSPACE_DELETE_IN_PROGRESS';
|
|
239
|
+
res.writeHead(inProgress ? 409 : 429, { 'Content-Type': 'application/json' });
|
|
240
|
+
res.end(JSON.stringify({
|
|
241
|
+
success: false,
|
|
242
|
+
error: inProgress
|
|
243
|
+
? 'Workspace deletion is already running for this workspace.'
|
|
244
|
+
: 'Workspace deletion was attempted recently. Wait before retrying.',
|
|
245
|
+
code: decision.code,
|
|
246
|
+
retryAfterMs: decision.retryAfterMs,
|
|
247
|
+
}));
|
|
248
|
+
};
|
|
249
|
+
const existingAdmission = workspaceDeletionAdmission.checkExisting(targetWorkspaceId);
|
|
250
|
+
if (existingAdmission) {
|
|
251
|
+
writeAdmissionError(existingAdmission);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
let resolvedAccess;
|
|
255
|
+
try {
|
|
256
|
+
resolvedAccess = core.resolveUserAccess(access.userId, targetWorkspaceId, 'member');
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
260
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
261
|
+
res.end(JSON.stringify({
|
|
262
|
+
success: false,
|
|
263
|
+
error: String(error?.message || 'Unable to resolve workspace access'),
|
|
264
|
+
code: error?.code,
|
|
265
|
+
}));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
235
268
|
if (!resolvedAccess || resolvedAccess.role !== 'owner' || resolvedAccess.disabled) {
|
|
236
269
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
237
270
|
res.end(JSON.stringify({ success: false, error: 'Only workspace owners can delete this workspace.' }));
|
|
238
271
|
return;
|
|
239
272
|
}
|
|
273
|
+
const deletionAdmissionKey = targetWorkspaceId;
|
|
274
|
+
const admission = workspaceDeletionAdmission.acquire(deletionAdmissionKey);
|
|
275
|
+
if (!admission.allowed) {
|
|
276
|
+
writeAdmissionError(admission);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
240
279
|
try {
|
|
241
280
|
const deleted = core.deleteWorkspace({ workspaceId: targetWorkspaceId });
|
|
242
281
|
const cleanupWarnings = await cleanupDeletedWorkspaceStorage(deleted);
|
|
@@ -277,6 +316,9 @@ export function registerWorkspaceRoutes(deps) {
|
|
|
277
316
|
code: error?.code
|
|
278
317
|
}));
|
|
279
318
|
}
|
|
319
|
+
finally {
|
|
320
|
+
workspaceDeletionAdmission.settle(deletionAdmissionKey);
|
|
321
|
+
}
|
|
280
322
|
});
|
|
281
323
|
// === SECTION: Session workspace switch ===
|
|
282
324
|
// POST /api/taskforce/session/workspace - Switch active workspace in auth session
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const WORKSPACE_DELETE_RETRY_COOLDOWN_MS = 30000;
|
|
2
|
+
export type WorkspaceDeletionAdmissionDecision = {
|
|
3
|
+
allowed: true;
|
|
4
|
+
} | {
|
|
5
|
+
allowed: false;
|
|
6
|
+
code: 'WORKSPACE_DELETE_IN_PROGRESS' | 'WORKSPACE_DELETE_RETRY_COOLDOWN';
|
|
7
|
+
retryAfterMs: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Serializes destructive workspace cleanup inside one server process and
|
|
11
|
+
* throttles immediate retries after an attempt settles. The caller supplies
|
|
12
|
+
* time so the admission policy can be verified without timers.
|
|
13
|
+
*/
|
|
14
|
+
export declare class WorkspaceDeletionAdmission {
|
|
15
|
+
private readonly cooldownMs;
|
|
16
|
+
private readonly entries;
|
|
17
|
+
constructor(cooldownMs?: number);
|
|
18
|
+
checkExisting(key: string, now?: number): Exclude<WorkspaceDeletionAdmissionDecision, {
|
|
19
|
+
allowed: true;
|
|
20
|
+
}> | null;
|
|
21
|
+
acquire(key: string, now?: number): WorkspaceDeletionAdmissionDecision;
|
|
22
|
+
settle(key: string, now?: number): void;
|
|
23
|
+
}
|